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 += \ + ' ' + \ + 'iCalendar\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

\n\n' + calendar_icon_str = \ + ' ' + \ + 'iCalendar\n' + cal_str = \ header_str + screen_reader_cal + calendar_str + \ - new_event_str + html_footer() + new_event_str + calendar_icon_str + html_footer() return cal_str