diff --git a/daemon.py b/daemon.py index 33b497c4f..6baf527d7 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,9 +15564,11 @@ 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, + self._set_headers('text/html', + msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, @@ -15562,6 +15577,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..8adc6d590 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); } + 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); } + 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); } + img ical { + width: var(--ical-icon-size-tiny); + float: right; + } } diff --git a/happening.py b/happening.py index 4670fc64e..733017fc7 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,20 +177,20 @@ def _is_happening_post(post_json_object: {}) -> bool: def get_todays_events(base_dir: str, nickname: str, domain: str, - currYear: int, currMonthNumber: int, + curr_year: int, curr_month_number: int, currDayOfMonth: 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 + month_number = curr_month_number if not currDayOfMonth: day_number = now.day else: @@ -263,13 +265,181 @@ def get_todays_events(base_dir: str, nickname: str, domain: str, return events +def _icalendar_day(base_dir: str, nickname: str, domain: str, + day_events: [], person_cache: {}) -> str: + """Returns a day's events in icalendar format + """ + ical_str = '' + 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('post_id'): + post_id = evnt['post_id'] + if evnt.get('startTime'): + event_start = \ + datetime.strptime(evnt['startTime'], + "%Y%m%dT%H%M%S%Z") + evnt_end = evnt['startTime'] + timedelta(hours=1) + event_end = \ + datetime.strptime(evnt_end, + "%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'] + + if not post_id or not event_start 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 = post_json_object['object']['published'] + published = published.replace('-', '') + published = published.replace(':', '') + published = published.replace(' ', '') + + 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: {}) -> str: + """Returns today's events in icalendar format + """ + events = \ + get_todays_events(base_dir, nickname, domain, + year, month_number, day_number) + ical_str = \ + 'BEGIN:VCALENDAR\n' + \ + 'PRODID:-//Fediverse//NONSGML Epicyon//EN' + \ + 'VERSION:2.0' + if not events: + ical_str += 'END:VCALENDAR\n' + return ical_str + + if not events.get(str(day_number)): + ical_str += 'END:VCALENDAR\n' + return ical_str + + day_events = events[str(day_number)] + + ical_str += \ + _icalendar_day(base_dir, nickname, domain, day_events, person_cache) + + ical_str += 'END:VCALENDAR\n' + return ical_str + + +def get_month_events_icalendar(base_dir: str, nickname: str, domain: str, + curr_year: int, + curr_month_number: int, + person_cache: {}) -> str: + """Returns today's events in icalendar format + """ + events = \ + get_calendar_events(base_dir, nickname, domain, curr_year, + curr_month_number) + ical_str = \ + 'BEGIN:VCALENDAR\n' + \ + 'PRODID:-//Fediverse//NONSGML Epicyon//EN' + \ + 'VERSION:2.0' + if not events: + ical_str += 'END:VCALENDAR\n' + return ical_str + + for day_number in range(1, 32): + if not events.get(str(day_number)): + continue + day_events = events[str(day_number)] + ical_str += \ + _icalendar_day(base_dir, nickname, domain, day_events, + person_cache) + + 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) + \ 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..e44f5761e 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 @@ -241,6 +243,13 @@ def _html_calendar_day(person_cache: {}, css_cache: {}, translate: {}, event_place + '' + \ delete_button_str + '\n' + # icalendar download link + calendar_str += \ + ' ' + \ + 'iCalendar\n' + calendar_str += '\n' calendar_str += '\n' calendar_str += html_footer() @@ -251,7 +260,8 @@ 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) @@ -277,6 +287,10 @@ 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.tolower().startswith('t'): + icalendar = True first = False actor = actor.split('?')[0] @@ -297,6 +311,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) + day_events = None events = \ get_todays_events(base_dir, nickname, domain, @@ -310,6 +331,10 @@ 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) + events = \ get_calendar_events(base_dir, nickname, domain, year, month_number)