diff --git a/daemon.py b/daemon.py
index 33b497c4f..dc3fe58ab 100644
--- a/daemon.py
+++ b/daemon.py
@@ -679,6 +679,16 @@ class PubServer(BaseHTTPRequestHandler):
return False
return True
+ def _request_icalendar(self) -> bool:
+ """Should an icalendar response be given?
+ """
+ if not self.headers.get('Accept'):
+ return False
+ accept_str = self.headers['Accept']
+ if 'text/calendar' in accept_str:
+ return True
+ return False
+
def _signed_ge_tkey_id(self) -> str:
"""Returns the actor from the signed GET key_id
"""
@@ -14056,9 +14066,12 @@ class PubServer(BaseHTTPRequestHandler):
# is this a html request?
html_getreq = False
+ icalendar_getreq = False
if self._has_accept(calling_domain):
if self._request_http():
html_getreq = True
+ elif self._request_icalendar():
+ icalendar_getreq = True
else:
if self.headers.get('Connection'):
# https://developer.mozilla.org/en-US/
@@ -15532,7 +15545,7 @@ class PubServer(BaseHTTPRequestHandler):
'_GET', 'search screen shown done',
self.server.debug)
- # Show the calendar for a user
+ # Show the html calendar for a user
if html_getreq and users_in_path:
if '/calendar' in self.path:
nickname = self.path.split('/users/')[1]
@@ -15551,10 +15564,17 @@ class PubServer(BaseHTTPRequestHandler):
self.server.http_prefix,
self.server.domain_full,
self.server.text_mode_banner,
- access_keys).encode('utf-8')
+ access_keys,
+ False).encode('utf-8')
msglen = len(msg)
- self._set_headers('text/html', msglen, cookie, calling_domain,
- False)
+ if 'ical=true' in self.path:
+ self._set_headers('text/calendar',
+ msglen, cookie, calling_domain,
+ False)
+ else:
+ self._set_headers('text/html',
+ msglen, cookie, calling_domain,
+ False)
self._write(msg)
fitness_performance(getreq_start_time, self.server.fitness,
'_GET', 'calendar shown',
@@ -15562,6 +15582,38 @@ class PubServer(BaseHTTPRequestHandler):
self.server.getreq_busy = False
return
+ # Show the icalendar for a user
+ if icalendar_getreq and users_in_path:
+ if '/calendar' in self.path:
+ nickname = self.path.split('/users/')[1]
+ if '/' in nickname:
+ nickname = nickname.split('/')[0]
+
+ access_keys = self.server.access_keys
+ if self.server.key_shortcuts.get(nickname):
+ access_keys = self.server.key_shortcuts[nickname]
+
+ # show the calendar screen
+ msg = html_calendar(self.server.person_cache,
+ self.server.css_cache,
+ self.server.translate,
+ self.server.base_dir, self.path,
+ self.server.http_prefix,
+ self.server.domain_full,
+ self.server.text_mode_banner,
+ access_keys,
+ True).encode('utf-8')
+ msglen = len(msg)
+ self._set_headers('text/calendar',
+ msglen, cookie, calling_domain,
+ False)
+ self._write(msg)
+ fitness_performance(getreq_start_time, self.server.fitness,
+ '_GET', 'icalendar shown',
+ self.server.debug)
+ self.server.getreq_busy = False
+ return
+
fitness_performance(getreq_start_time, self.server.fitness,
'_GET', 'calendar shown done',
self.server.debug)
diff --git a/epicyon-calendar.css b/epicyon-calendar.css
index eb279a995..412bf9efa 100644
--- a/epicyon-calendar.css
+++ b/epicyon-calendar.css
@@ -32,6 +32,9 @@
--calendar-header-font-style: italic;
--main-link-color-hover: blue;
--rendering: normal;
+ --ical-icon-size: 32px;
+ --ical-icon-size-mobile: 80px;
+ --ical-icon-size-tiny: 80px;
}
@font-face {
@@ -256,6 +259,10 @@ tr:nth-child(even) > .calendar__day__cell:nth-child(even) {
body {
font-size: var(--font-size-calendar);
}
+ body img.ical {
+ width: var(--ical-icon-size);
+ float: right;
+ }
}
@media screen and (max-width: 1000px) {
@@ -276,6 +283,10 @@ tr:nth-child(even) > .calendar__day__cell:nth-child(even) {
body {
font-size: var(--font-size-calendar-mobile);
}
+ body img.ical {
+ width: var(--ical-icon-size-mobile);
+ float: right;
+ }
}
@media screen and (max-width: 480px) {
@@ -296,4 +307,8 @@ tr:nth-child(even) > .calendar__day__cell:nth-child(even) {
body {
font-size: var(--font-size-calendar-tiny);
}
+ body img.ical {
+ width: var(--ical-icon-size-tiny);
+ float: right;
+ }
}
diff --git a/happening.py b/happening.py
index 4670fc64e..237e5e4a3 100644
--- a/happening.py
+++ b/happening.py
@@ -18,6 +18,8 @@ from utils import save_json
from utils import locate_post
from utils import has_object_dict
from utils import acct_dir
+from utils import remove_html
+from utils import get_display_name
def _valid_uuid(test_uuid: str, version: int):
@@ -175,24 +177,24 @@ def _is_happening_post(post_json_object: {}) -> bool:
def get_todays_events(base_dir: str, nickname: str, domain: str,
- currYear: int, currMonthNumber: int,
- currDayOfMonth: int) -> {}:
+ curr_year: int, curr_month_number: int,
+ curr_day_of_month: int) -> {}:
"""Retrieves calendar events for today
Returns a dictionary of lists containing Event and Place activities
"""
now = datetime.now()
- if not currYear:
+ if not curr_year:
year = now.year
else:
- year = currYear
- if not currMonthNumber:
+ year = curr_year
+ if not curr_month_number:
month_number = now.month
else:
- month_number = currMonthNumber
- if not currDayOfMonth:
+ month_number = curr_month_number
+ if not curr_day_of_month:
day_number = now.day
else:
- day_number = currDayOfMonth
+ day_number = curr_day_of_month
calendar_filename = \
acct_dir(base_dir, nickname, domain) + \
@@ -238,6 +240,7 @@ def get_todays_events(base_dir: str, nickname: str, domain: str,
# link to the id so that the event can be
# easily deleted
tag['post_id'] = post_id.split('#statuses#')[1]
+ tag['id'] = post_id.replace('#', '/')
tag['sender'] = post_id.split('#statuses#')[0]
tag['sender'] = tag['sender'].replace('#', '/')
tag['public'] = public_event
@@ -263,13 +266,206 @@ def get_todays_events(base_dir: str, nickname: str, domain: str,
return events
+def _ical_date_string(date_str: str) -> str:
+ """Returns an icalendar formatted date
+ """
+ date_str = date_str.replace('-', '')
+ date_str = date_str.replace(':', '')
+ return date_str.replace(' ', '')
+
+
+def _icalendar_day(base_dir: str, nickname: str, domain: str,
+ day_events: [], person_cache: {},
+ http_prefix: str) -> str:
+ """Returns a day's events in icalendar format
+ """
+ ical_str = ''
+ print('icalendar: ' + str(day_events))
+ for event_post in day_events:
+ event_description = None
+ event_place = None
+ post_id = None
+ sender_name = ''
+ sender_actor = None
+ event_is_public = False
+ event_start = None
+ event_end = None
+
+ for evnt in event_post:
+ if evnt['type'] == 'Event':
+ if evnt.get('id'):
+ post_id = evnt['id']
+ if evnt.get('startTime'):
+ event_start = \
+ datetime.strptime(evnt['startTime'],
+ "%Y-%m-%dT%H:%M:%S%z")
+ if evnt.get('endTime'):
+ event_end = \
+ datetime.strptime(evnt['startTime'],
+ "%Y-%m-%dT%H:%M:%S%z")
+ if 'public' in evnt:
+ if evnt['public'] is True:
+ event_is_public = True
+ if evnt.get('sender'):
+ # get display name from sending actor
+ if evnt.get('sender'):
+ sender_actor = evnt['sender']
+ disp_name = \
+ get_display_name(base_dir, sender_actor,
+ person_cache)
+ if disp_name:
+ sender_name = \
+ '' + \
+ disp_name + ''
+ if evnt.get('name'):
+ event_description = evnt['name'].strip()
+ elif evnt['type'] == 'Place':
+ if evnt.get('name'):
+ event_place = evnt['name']
+
+ print('icalendar: ' + str(post_id) + ' ' +
+ str(event_start) + ' ' + str(event_description) + ' ' +
+ str(sender_actor))
+
+ if not post_id or not event_start or not event_end or \
+ not event_description or not sender_actor:
+ continue
+
+ # find the corresponding post
+ post_filename = locate_post(base_dir, nickname, domain, post_id)
+ if not post_filename:
+ continue
+
+ post_json_object = load_json(post_filename)
+ if not post_json_object:
+ continue
+
+ # get the published date from the post
+ if not post_json_object.get('object'):
+ continue
+ if not isinstance(post_json_object['object'], dict):
+ continue
+ if not post_json_object['object'].get('published'):
+ continue
+ if not isinstance(post_json_object['object']['published'], str):
+ continue
+ published = \
+ _ical_date_string(post_json_object['object']['published'])
+
+ event_start = \
+ _ical_date_string(event_start.strftime("%Y-%m-%dT%H:%M:%SZ"))
+ event_end = \
+ _ical_date_string(event_end.strftime("%Y-%m-%dT%H:%M:%SZ"))
+
+ ical_str += \
+ 'BEGIN:VEVENT\n' + \
+ 'DTSTAMP:' + published + '\n' + \
+ 'UID:' + post_id + '\n' + \
+ 'DTSTART:' + event_start + '\n' + \
+ 'DTEND:' + event_end + '\n' + \
+ 'STATUS:CONFIRMED\n'
+ descr = remove_html(event_description)
+ if len(descr) < 255:
+ ical_str += \
+ 'SUMMARY:' + descr + '\n'
+ else:
+ ical_str += \
+ 'SUMMARY:' + descr[255:] + '\n'
+ ical_str += \
+ 'DESCRIPTION:' + descr + '\n'
+ if event_is_public:
+ ical_str += \
+ 'CATEGORIES:APPOINTMENT,PUBLIC\n'
+ else:
+ ical_str += \
+ 'CATEGORIES:APPOINTMENT\n'
+ if sender_name:
+ ical_str += \
+ 'ORGANIZER;CN=' + remove_html(sender_name) + ':' + \
+ sender_actor + '\n'
+ else:
+ ical_str += \
+ 'ORGANIZER:' + sender_actor + '\n'
+ if event_place:
+ ical_str += \
+ 'LOCATION:' + remove_html(event_place) + '\n'
+ ical_str += 'END:VEVENT\n'
+ return ical_str
+
+
+def get_todays_events_icalendar(base_dir: str, nickname: str, domain: str,
+ year: int, month_number: int,
+ day_number: int, person_cache: {},
+ http_prefix: str) -> str:
+ """Returns today's events in icalendar format
+ """
+ day_events = None
+ events = \
+ get_todays_events(base_dir, nickname, domain,
+ year, month_number, day_number)
+ if events:
+ if events.get(str(day_number)):
+ day_events = events[str(day_number)]
+
+ ical_str = \
+ 'BEGIN:VCALENDAR\n' + \
+ 'PRODID:-//Fediverse//NONSGML Epicyon//EN\n' + \
+ 'VERSION:2.0\n'
+ if not day_events:
+ print('icalendar daily: ' + nickname + '@' + domain + ' ' +
+ str(year) + '-' + str(month_number) +
+ '-' + str(day_number) + ' ' + str(day_events))
+ ical_str += 'END:VCALENDAR\n'
+ return ical_str
+
+ ical_str += \
+ _icalendar_day(base_dir, nickname, domain, day_events, person_cache,
+ http_prefix)
+
+ ical_str += 'END:VCALENDAR\n'
+ return ical_str
+
+
+def get_month_events_icalendar(base_dir: str, nickname: str, domain: str,
+ year: int,
+ month_number: int,
+ person_cache: {},
+ http_prefix: str) -> str:
+ """Returns today's events in icalendar format
+ """
+ month_events = \
+ get_calendar_events(base_dir, nickname, domain, year,
+ month_number)
+
+ ical_str = \
+ 'BEGIN:VCALENDAR\n' + \
+ 'PRODID:-//Fediverse//NONSGML Epicyon//EN\n' + \
+ 'VERSION:2.0\n'
+ if not month_events:
+ ical_str += 'END:VCALENDAR\n'
+ return ical_str
+
+ print('icalendar month: ' + str(month_events))
+ for day_of_month in range(1, 32):
+ if not month_events.get(str(day_of_month)):
+ continue
+ day_events = month_events[str(day_of_month)]
+ ical_str += \
+ _icalendar_day(base_dir, nickname, domain,
+ day_events, person_cache,
+ http_prefix)
+
+ ical_str += 'END:VCALENDAR\n'
+ return ical_str
+
+
def day_events_check(base_dir: str, nickname: str, domain: str,
- currDate) -> bool:
+ curr_date) -> bool:
"""Are there calendar events for the given date?
"""
- year = currDate.year
- month_number = currDate.month
- day_number = currDate.day
+ year = curr_date.year
+ month_number = curr_date.month
+ day_number = curr_date.day
calendar_filename = \
acct_dir(base_dir, nickname, domain) + \
@@ -427,6 +623,11 @@ def get_calendar_events(base_dir: str, nickname: str, domain: str,
if int(event_time.strftime("%Y")) == year and \
int(event_time.strftime("%m")) == month_number:
day_of_month = str(int(event_time.strftime("%d")))
+ if '#statuses#' in post_id:
+ tag['post_id'] = post_id.split('#statuses#')[1]
+ tag['id'] = post_id.replace('#', '/')
+ tag['sender'] = post_id.split('#statuses#')[0]
+ tag['sender'] = tag['sender'].replace('#', '/')
post_event.append(tag)
else:
# tag is a place
diff --git a/theme/blue/icons/ical.png b/theme/blue/icons/ical.png
new file mode 100644
index 000000000..b8b17a1a0
Binary files /dev/null and b/theme/blue/icons/ical.png differ
diff --git a/theme/debian/icons/ical.png b/theme/debian/icons/ical.png
new file mode 100644
index 000000000..b8b17a1a0
Binary files /dev/null and b/theme/debian/icons/ical.png differ
diff --git a/theme/default/icons/ical.png b/theme/default/icons/ical.png
new file mode 100644
index 000000000..b8b17a1a0
Binary files /dev/null and b/theme/default/icons/ical.png differ
diff --git a/theme/hacker/icons/ical.png b/theme/hacker/icons/ical.png
new file mode 100644
index 000000000..5226ffbae
Binary files /dev/null and b/theme/hacker/icons/ical.png differ
diff --git a/theme/henge/icons/ical.png b/theme/henge/icons/ical.png
new file mode 100644
index 000000000..7929edc79
Binary files /dev/null and b/theme/henge/icons/ical.png differ
diff --git a/theme/indymediaclassic/icons/ical.png b/theme/indymediaclassic/icons/ical.png
new file mode 100644
index 000000000..1bdbd43c5
Binary files /dev/null and b/theme/indymediaclassic/icons/ical.png differ
diff --git a/theme/indymediamodern/icons/ical.png b/theme/indymediamodern/icons/ical.png
new file mode 100644
index 000000000..93d2f809a
Binary files /dev/null and b/theme/indymediamodern/icons/ical.png differ
diff --git a/theme/lcd/icons/ical.png b/theme/lcd/icons/ical.png
new file mode 100644
index 000000000..43a4fa357
Binary files /dev/null and b/theme/lcd/icons/ical.png differ
diff --git a/theme/light/icons/ical.png b/theme/light/icons/ical.png
new file mode 100644
index 000000000..93d2f809a
Binary files /dev/null and b/theme/light/icons/ical.png differ
diff --git a/theme/night/icons/ical.png b/theme/night/icons/ical.png
new file mode 100644
index 000000000..b8b17a1a0
Binary files /dev/null and b/theme/night/icons/ical.png differ
diff --git a/theme/pixel/icons/ical.png b/theme/pixel/icons/ical.png
new file mode 100644
index 000000000..bc14f0e7a
Binary files /dev/null and b/theme/pixel/icons/ical.png differ
diff --git a/theme/purple/icons/ical.png b/theme/purple/icons/ical.png
new file mode 100644
index 000000000..0c54320de
Binary files /dev/null and b/theme/purple/icons/ical.png differ
diff --git a/theme/rc3/icons/ical.png b/theme/rc3/icons/ical.png
new file mode 100644
index 000000000..2269e7d74
Binary files /dev/null and b/theme/rc3/icons/ical.png differ
diff --git a/theme/solidaric/icons/ical.png b/theme/solidaric/icons/ical.png
new file mode 100644
index 000000000..5e60ea341
Binary files /dev/null and b/theme/solidaric/icons/ical.png differ
diff --git a/theme/starlight/icons/ical.png b/theme/starlight/icons/ical.png
new file mode 100644
index 000000000..3336820f8
Binary files /dev/null and b/theme/starlight/icons/ical.png differ
diff --git a/theme/zen/icons/ical.png b/theme/zen/icons/ical.png
new file mode 100644
index 000000000..62828fe10
Binary files /dev/null and b/theme/zen/icons/ical.png differ
diff --git a/webapp_calendar.py b/webapp_calendar.py
index 131e71493..38a3afa86 100644
--- a/webapp_calendar.py
+++ b/webapp_calendar.py
@@ -24,6 +24,8 @@ from utils import local_actor_url
from utils import replace_users_with_at
from happening import get_todays_events
from happening import get_calendar_events
+from happening import get_todays_events_icalendar
+from happening import get_month_events_icalendar
from webapp_utils import set_custom_background
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
@@ -243,6 +245,14 @@ def _html_calendar_day(person_cache: {}, css_cache: {}, translate: {},
calendar_str += '\n'
calendar_str += '\n'
+
+ # icalendar download link
+ calendar_str += \
+ ' ' + \
+ '\n'
+
calendar_str += html_footer()
return calendar_str
@@ -251,14 +261,17 @@ def _html_calendar_day(person_cache: {}, css_cache: {}, translate: {},
def html_calendar(person_cache: {}, css_cache: {}, translate: {},
base_dir: str, path: str,
http_prefix: str, domain_full: str,
- text_mode_banner: str, access_keys: {}) -> str:
+ text_mode_banner: str, access_keys: {},
+ icalendar: bool) -> str:
"""Show the calendar for a person
"""
domain = remove_domain_port(domain_full)
- month_number = 0
+ default_year = 1970
+ default_month = 0
+ month_number = default_month
day_number = None
- year = 1970
+ year = default_year
actor = http_prefix + '://' + domain_full + path.replace('/calendar', '')
if '?' in actor:
first = True
@@ -277,11 +290,15 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {},
num_str = part.split('=')[1]
if num_str.isdigit():
day_number = int(num_str)
+ elif part.split('=')[0] == 'ical':
+ bool_str = part.split('=')[1]
+ if bool_str.lower().startswith('t'):
+ icalendar = True
first = False
actor = actor.split('?')[0]
curr_date = datetime.now()
- if year == 1970 and month_number == 0:
+ if year == default_year and month_number == default_month:
year = curr_date.year
month_number = curr_date.month
@@ -297,6 +314,13 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {},
month_name = translate[months[month_number - 1]]
if day_number:
+ if icalendar:
+ return get_todays_events_icalendar(base_dir,
+ nickname, domain,
+ year, month_number,
+ day_number,
+ person_cache,
+ http_prefix)
day_events = None
events = \
get_todays_events(base_dir, nickname, domain,
@@ -310,6 +334,11 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {},
nickname, domain, day_events,
month_name, actor)
+ if icalendar:
+ return get_month_events_icalendar(base_dir, nickname, domain,
+ year, month_number, person_cache,
+ http_prefix)
+
events = \
get_calendar_events(base_dir, nickname, domain, year, month_number)
@@ -469,8 +498,14 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {},
'➕ ' + \
translate['Add to the calendar'] + '\n