Support for icalendar format

merge-requests/30/head
Bob Mottram 2022-02-21 11:08:48 +00:00
parent cc30725f6c
commit 0b686f799a
20 changed files with 270 additions and 13 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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 = \
'<a href="' + sender_actor + '">' + \
disp_name + '</a>: '
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) + \

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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 + '</span></td>' + \
delete_button_str + '</tr>\n'
# icalendar download link
calendar_str += \
' <a href="/users/' + path + '?ical=true" ' + \
'download="icalendar.ics">' + \
'<img class="ical" src="/icons/ical.png" ' + \
'title="iCalendar" alt="iCalendar" /></a>\n'
calendar_str += '</tbody>\n'
calendar_str += '</table></main>\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)