From ef8c7a5eb989d17a5890aecc0d4aa142ccbd538e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 23 Feb 2022 12:47:33 +0000 Subject: [PATCH 01/18] Etag returned after caldav put --- daemon.py | 56 ++++++++++++++++++++++++++++++---------------------- happening.py | 2 +- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/daemon.py b/daemon.py index ee114533b..5ae348e8a 100644 --- a/daemon.py +++ b/daemon.py @@ -949,7 +949,7 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() def _http_return_code(self, http_code: int, http_description: str, - long_description: str) -> None: + long_description: str, etag: str) -> None: msg = \ '' + str(http_code) + '' \ '' \ @@ -965,6 +965,8 @@ class PubServer(BaseHTTPRequestHandler): self.send_header('Content-Type', 'text/html; charset=utf-8') msg_len_str = str(len(msg)) self.send_header('Content-Length', msg_len_str) + if etag: + self.send_header('ETag', etag) self.end_headers() if not self._write(msg): print('Error when showing ' + str(http_code)) @@ -973,71 +975,77 @@ class PubServer(BaseHTTPRequestHandler): if self.server.translate: ok_str = self.server.translate['This is nothing ' + 'less than an utter triumph'] - self._http_return_code(200, self.server.translate['Ok'], ok_str) + self._http_return_code(200, self.server.translate['Ok'], + ok_str, None) else: self._http_return_code(200, 'Ok', 'This is nothing less ' + - 'than an utter triumph') + 'than an utter triumph', None) - def _201(self) -> None: + def _201(self, etag: str) -> None: if self.server.translate: - ok_str = self.server.translate['Done'] + done_str = self.server.translate['It is done'] self._http_return_code(201, - self.server.translate['Created'], ok_str) + self.server.translate['Created'], done_str, + etag) else: - self._http_return_code(201, 'Created', - 'Done') + self._http_return_code(201, 'Created', 'It is done', etag) def _207(self) -> None: if self.server.translate: multi_str = self.server.translate['Lots of things'] self._http_return_code(207, self.server.translate['Multi Status'], - multi_str) + multi_str, None) else: self._http_return_code(207, 'Multi Status', - 'Lots of things') + 'Lots of things', None) def _403(self) -> None: if self.server.translate: self._http_return_code(403, self.server.translate['Forbidden'], - self.server.translate["You're not allowed"]) + self.server.translate["You're not allowed"], + None) else: self._http_return_code(403, 'Forbidden', - "You're not allowed") + "You're not allowed", None) def _404(self) -> None: if self.server.translate: self._http_return_code(404, self.server.translate['Not Found'], self.server.translate['These are not the ' + 'droids you are ' + - 'looking for']) + 'looking for'], + None) else: self._http_return_code(404, 'Not Found', 'These are not the ' + 'droids you are ' + - 'looking for') + 'looking for', None) def _304(self) -> None: if self.server.translate: self._http_return_code(304, self.server.translate['Not changed'], self.server.translate['The contents of ' + 'your local cache ' + - 'are up to date']) + 'are up to date'], + None) else: self._http_return_code(304, 'Not changed', 'The contents of ' + 'your local cache ' + - 'are up to date') + 'are up to date', + None) def _400(self) -> None: if self.server.translate: self._http_return_code(400, self.server.translate['Bad Request'], self.server.translate['Better luck ' + - 'next time']) + 'next time'], + None) else: self._http_return_code(400, 'Bad Request', - 'Better luck next time') + 'Better luck next time', None) def _503(self) -> None: if self.server.translate: @@ -1045,11 +1053,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.translate['The server is busy. ' + 'Please try again later'] self._http_return_code(503, self.server.translate['Unavailable'], - busy_str) + busy_str, None) else: self._http_return_code(503, 'Unavailable', 'The server is busy. Please try again ' + - 'later') + 'later', None) def _write(self, msg) -> bool: tries = 0 @@ -16926,6 +16934,9 @@ class PubServer(BaseHTTPRequestHandler): return if response_str == 'Not modified': return self._304() + elif response_str.startswith('ETag:') and endpoint_type == 'put': + response_etag = response_str.split('ETag:', 1)[1] + self._201(response_etag) elif response_str != 'Ok': message_xml = response_str.encode('utf-8') message_xml_len = len(message_xml) @@ -16935,10 +16946,7 @@ class PubServer(BaseHTTPRequestHandler): self._write(message_xml) if 'multistatus' in response_str: return self._207() - if endpoint_type == 'put': - self._201() - else: - self._200() + self._200() def do_PROPFIND(self): self._dav_handler('propfind') diff --git a/happening.py b/happening.py index 72e38c9c1..c68ff03c7 100644 --- a/happening.py +++ b/happening.py @@ -968,7 +968,7 @@ def dav_put_response(base_dir: str, nickname: str, domain: str, event_list.append(line) if stored_count == 0: return None - return 'Ok' + return 'ETag:' + md5(xml_str).hexdigest() def dav_report_response(base_dir: str, nickname: str, domain: str, From 26610e455521b8f340626dbdfcb86b9a0d2cf919 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 23 Feb 2022 18:04:34 +0000 Subject: [PATCH 02/18] caldav report implementation --- daemon.py | 12 +- happening.py | 323 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 249 insertions(+), 86 deletions(-) diff --git a/daemon.py b/daemon.py index 5ae348e8a..b0b83d708 100644 --- a/daemon.py +++ b/daemon.py @@ -16906,7 +16906,8 @@ class PubServer(BaseHTTPRequestHandler): nickname, self.server.domain, depth, propfind_xml, self.server.http_prefix, - self.server.system_language) + self.server.system_language, + self.server.recent_dav_etags) elif endpoint_type == 'report': curr_etag = None if self.headers.get('ETag'): @@ -16920,6 +16921,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.person_cache, self.server.http_prefix, curr_etag, + self.server.recent_dav_etags, self.server.domain_full) elif endpoint_type == 'delete': response_str = \ @@ -16933,7 +16935,10 @@ class PubServer(BaseHTTPRequestHandler): self._404() return if response_str == 'Not modified': - return self._304() + if endpoint_type == 'put': + return self._200() + else: + return self._304() elif response_str.startswith('ETag:') and endpoint_type == 'put': response_etag = response_str.split('ETag:', 1)[1] self._201(response_etag) @@ -19282,6 +19287,9 @@ def run_daemon(dyslexic_font: bool, default_reply_interval_hrs = 9999999 httpd.default_reply_interval_hrs = default_reply_interval_hrs + # recent caldav etags for each account + httpd.recent_dav_etags = {} + httpd.key_shortcuts = {} load_access_keys_for_accounts(base_dir, httpd.key_shortcuts, httpd.access_keys) diff --git a/happening.py b/happening.py index c68ff03c7..22bff046f 100644 --- a/happening.py +++ b/happening.py @@ -27,6 +27,31 @@ from filters import is_filtered from context import get_individual_post_context +def _dav_date_from_string(timestamp: str) -> str: + """Returns a datetime from a caldav date + """ + timestamp_year = timestamp[:4] + timestamp_month = timestamp[4:][:2] + timestamp_day = timestamp[6:][:2] + timestamp_hour = timestamp[9:][:2] + timestamp_min = timestamp[11:][:2] + timestamp_sec = timestamp[13:][:2] + + if not timestamp_year.isdigit() or \ + not timestamp_month.isdigit() or \ + not timestamp_day.isdigit() or \ + not timestamp_hour.isdigit() or \ + not timestamp_min.isdigit() or \ + not timestamp_sec.isdigit(): + return None + if int(timestamp_year) < 2020 or int(timestamp_year) > 2100: + return None + published = \ + timestamp_year + '-' + timestamp_month + '-' + timestamp_day + 'T' + \ + timestamp_hour + ':' + timestamp_min + ':' + timestamp_sec + 'Z' + return published + + def _valid_uuid(test_uuid: str, version: int): """Check if uuid_to_test is a valid UUID """ @@ -784,66 +809,15 @@ def _dav_store_event(base_dir: str, nickname: str, domain: str, return False # convert to the expected time format - timestamp_year = timestamp[:4] - timestamp_month = timestamp[4:][:2] - timestamp_day = timestamp[6:][:2] - timestamp_hour = timestamp[9:][:2] - timestamp_min = timestamp[11:][:2] - timestamp_sec = timestamp[13:][:2] - - if not timestamp_year.isdigit() or \ - not timestamp_month.isdigit() or \ - not timestamp_day.isdigit() or \ - not timestamp_hour.isdigit() or \ - not timestamp_min.isdigit() or \ - not timestamp_sec.isdigit(): + published = _dav_date_from_string(timestamp) + if not published: return False - if int(timestamp_year) < 2020 or int(timestamp_year) > 2100: + start_time = _dav_date_from_string(start_time) + if not start_time: return False - published = \ - timestamp_year + '-' + timestamp_month + '-' + timestamp_day + 'T' + \ - timestamp_hour + ':' + timestamp_min + ':' + timestamp_sec + 'Z' - - start_time_year = start_time[:4] - start_time_month = start_time[4:][:2] - start_time_day = start_time[6:][:2] - start_time_hour = start_time[9:][:2] - start_time_min = start_time[11:][:2] - start_time_sec = start_time[13:][:2] - - if not start_time_year.isdigit() or \ - not start_time_month.isdigit() or \ - not start_time_day.isdigit() or \ - not start_time_hour.isdigit() or \ - not start_time_min.isdigit() or \ - not start_time_sec.isdigit(): + end_time = _dav_date_from_string(end_time) + if not end_time: return False - if int(start_time_year) < 2020 or int(start_time_year) > 2100: - return False - start_time = \ - start_time_year + '-' + start_time_month + '-' + \ - start_time_day + 'T' + \ - start_time_hour + ':' + start_time_min + ':' + start_time_sec + 'Z' - - end_time_year = end_time[:4] - end_time_month = end_time[4:][:2] - end_time_day = end_time[6:][:2] - end_time_hour = end_time[9:][:2] - end_time_min = end_time[11:][:2] - end_time_sec = end_time[13:][:2] - - if not end_time_year.isdigit() or \ - not end_time_month.isdigit() or \ - not end_time_day.isdigit() or \ - not end_time_hour.isdigit() or \ - not end_time_min.isdigit() or \ - not end_time_sec.isdigit(): - return False - if int(end_time_year) < 2020 or int(end_time_year) > 2100: - return False - end_time = \ - end_time_year + '-' + end_time_month + '-' + end_time_day + 'T' + \ - end_time_hour + ':' + end_time_min + ':' + end_time_sec + 'Z' post_id = '' post_context = get_individual_post_context() @@ -932,9 +906,26 @@ def _dav_store_event(base_dir: str, nickname: str, domain: str, return True +def _dav_update_recent_etags(etag: str, nickname: str, + recent_dav_etags: {}) -> None: + """Updates the recent etags for each account + """ + # update the recent caldav etags for each account + if not recent_dav_etags.get(nickname): + recent_dav_etags[nickname] = [etag] + else: + # only keep a limited number of recent etags + while len(recent_dav_etags[nickname]) > 32: + recent_dav_etags[nickname].pop(0) + # append the etag to the recent list + if etag not in recent_dav_etags[nickname]: + recent_dav_etags[nickname].append(etag) + + def dav_put_response(base_dir: str, nickname: str, domain: str, depth: int, xml_str: str, http_prefix: str, - system_language: str) -> str: + system_language: str, + recent_dav_etags: {}) -> str: """Returns the response to caldav PUT """ if '\n' not in xml_str: @@ -946,6 +937,11 @@ def dav_put_response(base_dir: str, nickname: str, domain: str, 'END:VEVENT' not in xml_str: return None + etag = md5(xml_str).hexdigest() + if recent_dav_etags.get(nickname): + if etag in recent_dav_etags[nickname]: + return 'Not modified' + stored_count = 0 reading_event = False lines_list = xml_str.split('\n') @@ -968,13 +964,14 @@ def dav_put_response(base_dir: str, nickname: str, domain: str, event_list.append(line) if stored_count == 0: return None - return 'ETag:' + md5(xml_str).hexdigest() + _dav_update_recent_etags(etag, nickname, recent_dav_etags) + return 'ETag:' + etag def dav_report_response(base_dir: str, nickname: str, domain: str, depth: int, xml_str: str, person_cache: {}, http_prefix: str, - curr_etag: str, + curr_etag: str, recent_dav_etags: {}, domain_full: str) -> str: """Returns the response to caldav REPORT """ @@ -983,42 +980,200 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, if '' not in xml_str: return None + + if curr_etag: + if recent_dav_etags.get(nickname): + if curr_etag in recent_dav_etags[nickname]: + return "Not modified" + + xml_str_lower = xml_str.lower() + query_start_time = None + query_end_time = None + if ':time-range' in xml_str_lower: + time_range_str = xml_str_lower.split(':time-range')[1] + if 'start=' in time_range_str and 'end=' in time_range_str: + start_time_str = time_range_str.split('start=')[1] + if start_time_str.startswith("'"): + query_start_time_str = start_time_str.split("'")[1] + query_start_time = _dav_date_from_string(query_start_time_str) + elif start_time_str.startswith('"'): + query_start_time_str = start_time_str.split('"')[1] + query_start_time = _dav_date_from_string(query_start_time_str) + + end_time_str = time_range_str.split('end=')[1] + if end_time_str.startswith("'"): + query_end_time_str = end_time_str.split("'")[1] + query_end_time = _dav_date_from_string(query_end_time_str) + elif end_time_str.startswith('"'): + query_end_time_str = end_time_str.split('"')[1] + query_end_time = _dav_date_from_string(query_end_time_str) + + ical_events = None + etag = None + events_href = '' + responses = '' + search_date = datetime.now() + if query_start_time and query_end_time: + query_start_year = int(query_start_time.split('-')[0]) + query_start_month = int(query_start_time.split('-')[1]) + query_start_day = query_start_time.split('-')[2] + query_start_day = int(query_start_day.split('T')[0]) + query_end_year = int(query_end_time.split('-')[0]) + query_end_month = int(query_end_time.split('-')[1]) + query_end_day = query_end_time.split('-')[2] + query_end_day = int(query_end_day.split('T')[0]) + if query_start_year == query_end_year and \ + query_start_month == query_end_month: + if query_start_day == query_end_day: + # calendar for one day + search_date = \ + datetime.datetime(year=query_start_year, + month=query_start_month, + day=query_start_day) + ical_events = \ + get_todays_events_icalendar(base_dir, nickname, domain, + search_date.year, + search_date.month, + search_date.day, person_cache, + http_prefix) + events_href = \ + http_prefix + '://' + domain_full + '/users/' + \ + nickname + '/calendar?year=' + \ + str(search_date.year) + '?month=' + \ + str(search_date.month) + '?day=' + str(search_date.day) + if ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' + elif query_start_day == 1 and query_start_day >= 28: + # calendar for a month + ical_events = \ + get_month_events_icalendar(base_dir, nickname, domain, + query_start_year, + query_start_month, + person_cache, + http_prefix) + events_href = \ + http_prefix + '://' + domain_full + '/users/' + \ + nickname + '/calendar?year=' + \ + str(query_start_year) + '?month=' + \ + str(query_start_month) + if ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' + if not responses: + all_events = '' + for year in range(query_start_year, query_end_year+1): + if query_start_year == query_end_year: + start_month_number = query_start_month + end_month_number = query_end_month + elif year == query_end_year: + start_month_number = 1 + end_month_number = query_end_month + elif year == query_start_year: + start_month_number = query_start_month + end_month_number = 12 + else: + start_month_number = 1 + end_month_number = 12 + for month in range(start_month_number, end_month_number+1): + ical_events = \ + get_month_events_icalendar(base_dir, + nickname, domain, + year, month, + person_cache, + http_prefix) + events_href = \ + http_prefix + '://' + domain_full + '/users/' + \ + nickname + '/calendar?year=' + \ + str(year) + '?month=' + \ + str(month) + if ical_events: + all_events += ical_events + responses += \ + ' \n' + \ + ' ' + events_href + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + \ + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' + etag = md5(all_events).hexdigest() + # today's calendar events - now = datetime.now() - ical_events = \ - get_todays_events_icalendar(base_dir, nickname, domain, - now.year, now.month, - now.day, person_cache, - http_prefix) if not ical_events: + ical_events = \ + get_todays_events_icalendar(base_dir, nickname, domain, + search_date.year, search_date.month, + search_date.day, person_cache, + http_prefix) + events_href = \ + http_prefix + '://' + domain_full + '/users/' + \ + nickname + '/calendar?year=' + \ + str(search_date.year) + '?month=' + \ + str(search_date.month) + '?day=' + str(search_date.day) + if ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK\n' + \ + ' \n' + \ + ' \n' + + if not ical_events or not etag: return None if 'VEVENT' not in ical_events: return None - etag = md5(ical_events).hexdigest() if etag == curr_etag: return "Not modified" - events_href = \ - http_prefix + '://' + domain_full + '/users/' + \ - nickname + '/calendar?year=' + \ - str(now.year) + '?month=' + str(now.month) + '?day=' + str(now.day) response_str = \ + '\n' + \ '\n' + \ - ' \n' + \ - ' ' + events_href + '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + etag + '"\n' + \ - ' ' + ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK\n' + \ - ' \n' + \ - ' \n' + \ - ' \n' + \ - '' + responses + '' + _dav_update_recent_etags(etag, nickname, recent_dav_etags) return response_str From 0b36feebc2fd8dd59da615aadafbe2e26e4410c9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 23 Feb 2022 18:46:20 +0000 Subject: [PATCH 03/18] Caldav report based on matched text --- happening.py | 196 +++++++++++++++++++++++++++++---------------- webapp_calendar.py | 12 ++- 2 files changed, 134 insertions(+), 74 deletions(-) diff --git a/happening.py b/happening.py index 22bff046f..a3148ed98 100644 --- a/happening.py +++ b/happening.py @@ -206,9 +206,26 @@ def _is_happening_post(post_json_object: {}) -> bool: return True +def _event_text_match(content: str, text_match: str) -> bool: + """Returns true of the content matches the search text + """ + if not text_match: + return True + if '+' not in text_match: + if text_match.strip().lower() in content.lower(): + return True + else: + match_list = text_match.split('+') + for possible_match in match_list: + if possible_match.strip().lower() in content.lower(): + return True + return False + + def get_todays_events(base_dir: str, nickname: str, domain: str, curr_year: int, curr_month_number: int, - curr_day_of_month: int) -> {}: + curr_day_of_month: int, + text_match: str) -> {}: """Retrieves calendar events for today Returns a dictionary of lists containing Event and Place activities """ @@ -247,6 +264,12 @@ def get_todays_events(base_dir: str, nickname: str, domain: str, if not _is_happening_post(post_json_object): continue + if post_json_object.get('object'): + if post_json_object['object'].get('content'): + content = post_json_object['object']['content'] + if not _event_text_match(content, text_match): + continue + public_event = is_public_post(post_json_object) post_event = [] @@ -438,13 +461,15 @@ def _icalendar_day(base_dir: str, nickname: str, domain: 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: + http_prefix: str, + text_match: 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) + year, month_number, day_number, + text_match) if events: if events.get(str(day_number)): day_events = events[str(day_number)] @@ -472,12 +497,13 @@ def get_month_events_icalendar(base_dir: str, nickname: str, domain: str, year: int, month_number: int, person_cache: {}, - http_prefix: str) -> str: + http_prefix: str, + text_match: str) -> str: """Returns today's events in icalendar format """ month_events = \ get_calendar_events(base_dir, nickname, domain, year, - month_number) + month_number, text_match) ical_str = \ 'BEGIN:VCALENDAR\n' + \ @@ -622,7 +648,8 @@ def get_this_weeks_events(base_dir: str, nickname: str, domain: str) -> {}: def get_calendar_events(base_dir: str, nickname: str, domain: str, - year: int, month_number: int) -> {}: + year: int, month_number: int, + text_match: str) -> {}: """Retrieves calendar events Returns a dictionary indexed by day number of lists containing Event and Place activities @@ -646,9 +673,17 @@ def get_calendar_events(base_dir: str, nickname: str, domain: str, continue post_json_object = load_json(post_filename) + if not post_json_object: + continue if not _is_happening_post(post_json_object): continue + if post_json_object.get('object'): + if post_json_object['object'].get('content'): + content = post_json_object['object']['content'] + if not _event_text_match(content, text_match): + continue + post_event = [] day_of_month = None for tag in post_json_object['object']['tag']: @@ -1008,6 +1043,16 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, query_end_time_str = end_time_str.split('"')[1] query_end_time = _dav_date_from_string(query_end_time_str) + text_match = '' + if ':text-match' in xml_str_lower: + match_str = xml_str_lower.split(':text-match')[1] + if '>' in match_str and '<' in match_str: + text_match = match_str.split('>')[1] + if '<' in text_match: + text_match = text_match.split('<')[0] + else: + text_match = '' + ical_events = None etag = None events_href = '' @@ -1035,28 +1080,31 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, search_date.year, search_date.month, search_date.day, person_cache, - http_prefix) + http_prefix, text_match) events_href = \ http_prefix + '://' + domain_full + '/users/' + \ nickname + '/calendar?year=' + \ str(search_date.year) + '?month=' + \ str(search_date.month) + '?day=' + str(search_date.day) if ical_events: - etag = md5(ical_events).hexdigest() - responses = \ - ' \n' + \ - ' ' + events_href + '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + \ - etag + '"\n' + \ - ' ' + ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK' + \ - '\n' + \ - ' \n' + \ - ' \n' + if 'VEVENT' in ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + \ + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' elif query_start_day == 1 and query_start_day >= 28: # calendar for a month ical_events = \ @@ -1064,28 +1112,32 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, query_start_year, query_start_month, person_cache, - http_prefix) + http_prefix, + text_match) events_href = \ http_prefix + '://' + domain_full + '/users/' + \ nickname + '/calendar?year=' + \ str(query_start_year) + '?month=' + \ str(query_start_month) if ical_events: - etag = md5(ical_events).hexdigest() - responses = \ - ' \n' + \ - ' ' + events_href + '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + \ - etag + '"\n' + \ - ' ' + ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK' + \ - '\n' + \ - ' \n' + \ - ' \n' + if 'VEVENT' in ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + \ + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' if not responses: all_events = '' for year in range(query_start_year, query_end_year+1): @@ -1107,30 +1159,32 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, nickname, domain, year, month, person_cache, - http_prefix) + http_prefix, + text_match) events_href = \ http_prefix + '://' + domain_full + '/users/' + \ nickname + '/calendar?year=' + \ str(year) + '?month=' + \ str(month) if ical_events: - all_events += ical_events - responses += \ - ' \n' + \ - ' ' + events_href + \ - '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + \ - etag + '"\n' + \ - ' ' + \ - ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK' + \ - '\n' + \ - ' \n' + \ - ' \n' + if 'VEVENT' in ical_events: + all_events += ical_events + responses += \ + ' \n' + \ + ' ' + events_href + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + \ + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \n' etag = md5(all_events).hexdigest() # today's calendar events @@ -1139,26 +1193,28 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, get_todays_events_icalendar(base_dir, nickname, domain, search_date.year, search_date.month, search_date.day, person_cache, - http_prefix) + http_prefix, text_match) events_href = \ http_prefix + '://' + domain_full + '/users/' + \ nickname + '/calendar?year=' + \ str(search_date.year) + '?month=' + \ str(search_date.month) + '?day=' + str(search_date.day) if ical_events: - etag = md5(ical_events).hexdigest() - responses = \ - ' \n' + \ - ' ' + events_href + '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + etag + '"\n' + \ - ' ' + ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK\n' + \ - ' \n' + \ - ' \n' + if 'VEVENT' in ical_events: + etag = md5(ical_events).hexdigest() + responses = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + etag + \ + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK\n' + \ + ' \n' + \ + ' \n' if not ical_events or not etag: return None diff --git a/webapp_calendar.py b/webapp_calendar.py index 38a3afa86..576fae8e3 100644 --- a/webapp_calendar.py +++ b/webapp_calendar.py @@ -267,6 +267,7 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {}, """ domain = remove_domain_port(domain_full) + text_match = '' default_year = 1970 default_month = 0 month_number = default_month @@ -320,11 +321,13 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {}, year, month_number, day_number, person_cache, - http_prefix) + http_prefix, + text_match) day_events = None events = \ get_todays_events(base_dir, nickname, domain, - year, month_number, day_number) + year, month_number, day_number, + text_match) if events: if events.get(str(day_number)): day_events = events[str(day_number)] @@ -337,10 +340,11 @@ def html_calendar(person_cache: {}, css_cache: {}, translate: {}, if icalendar: return get_month_events_icalendar(base_dir, nickname, domain, year, month_number, person_cache, - http_prefix) + http_prefix, text_match) events = \ - get_calendar_events(base_dir, nickname, domain, year, month_number) + get_calendar_events(base_dir, nickname, domain, year, month_number, + text_match) prev_year = year prev_month_number = month_number - 1 From 53a72091ae7995326d03c6f814e8f9643b5f334a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 10:18:54 +0000 Subject: [PATCH 04/18] dav handler method name --- daemon.py | 7 +++-- epicyon.py | 55 +++++++++++++++++++++++++++++++++++++ happening.py | 45 ++++++++++++++++++++++++++++++ session.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index b0b83d708..ed2bb9154 100644 --- a/daemon.py +++ b/daemon.py @@ -16856,7 +16856,7 @@ class PubServer(BaseHTTPRequestHandler): self._404() return if not self._is_authorized(): - print('PROPFIND Not authorized') + print(endpoint_type.upper() + ' not authorized') self._403() return nickname = self.path.split('/calendars/')[1] @@ -16877,9 +16877,10 @@ class PubServer(BaseHTTPRequestHandler): propfind_bytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: - print('EX: PROPFIND connection reset by peer') + print('EX: ' + endpoint_type.upper() + + ' connection reset by peer') else: - print('EX: PROPFIND socket error') + print('EX: ' + endpoint_type.upper() + ' socket error') self._400() return except ValueError as ex: diff --git a/epicyon.py b/epicyon.py index a98d161a7..86f8f2a67 100644 --- a/epicyon.py +++ b/epicyon.py @@ -13,6 +13,7 @@ import sys import time import argparse import getpass +import datetime from person import get_actor_json from person import create_person from person import create_group @@ -101,6 +102,7 @@ from announce import send_announce_via_server from socnet import instances_graph from migrate import migrate_accounts from desktop_client import run_desktop_client +from happening import dav_month_via_server def str2bool(value_str) -> bool: @@ -115,6 +117,7 @@ def str2bool(value_str) -> bool: raise argparse.ArgumentTypeError('Boolean value expected.') +search_date = datetime.datetime.now() parser = argparse.ArgumentParser(description='ActivityPub Server') parser.add_argument('--content_license_url', type=str, default='https://creativecommons.org/licenses/by/4.0', @@ -172,6 +175,12 @@ parser.add_argument('--i2p_domain', dest='i2p_domain', type=str, parser.add_argument('-p', '--port', dest='port', type=int, default=None, help='Port number to run on') +parser.add_argument('--year', dest='year', type=int, + default=search_date.year, + help='Year for calendar query') +parser.add_argument('--month', dest='month', type=int, + default=search_date.month, + help='Month for calendar query') parser.add_argument('--postsPerSource', dest='max_newswire_postsPerSource', type=int, default=4, @@ -329,6 +338,11 @@ parser.add_argument("--repliesEnabled", "--commentsEnabled", type=str2bool, nargs='?', const=True, default=True, help="Enable replies to a post") +parser.add_argument("--dav", + dest='dav', + type=str2bool, nargs='?', + const=True, default=False, + help="Caldav") parser.add_argument("--show_publish_as_icon", dest='show_publish_as_icon', type=str2bool, nargs='?', @@ -1375,6 +1389,47 @@ if args.message: time.sleep(1) sys.exit() +if args.dav: + if not args.nickname: + print('Please specify a nickname with --nickname') + sys.exit() + if not args.domain: + print('Please specify a domain with --domain') + sys.exit() + if not args.year: + print('Please specify a year with --year') + sys.exit() + if not args.month: + print('Please specify a month with --month') + sys.exit() + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + proxy_type = None + if args.tor or domain.endswith('.onion'): + proxy_type = 'tor' + if domain.endswith('.onion'): + args.port = 80 + elif args.i2p or domain.endswith('.i2p'): + proxy_type = 'i2p' + if domain.endswith('.i2p'): + args.port = 80 + elif args.gnunet: + proxy_type = 'gnunet' + session = create_session(proxy_type) + result = \ + dav_month_via_server(session, http_prefix, + args.nickname, args.domain, args.port, + args.debug, + args.year, args.month, + args.password) + if result: + print(str(result)) + sys.exit() + if args.announce: if not args.nickname: print('Specify a nickname with the --nickname option') diff --git a/happening.py b/happening.py index a3148ed98..9c70092da 100644 --- a/happening.py +++ b/happening.py @@ -23,8 +23,11 @@ from utils import remove_html from utils import get_display_name from utils import delete_post from utils import get_status_number +from utils import get_full_domain from filters import is_filtered from context import get_individual_post_context +from session import get_method +from auth import create_basic_auth_header def _dav_date_from_string(timestamp: str) -> str: @@ -1259,3 +1262,45 @@ def dav_delete_response(base_dir: str, nickname: str, domain: str, nickname, domain, post_filename, debug, recent_posts_cache) return 'Ok' + + +def dav_month_via_server(session, http_prefix: str, + nickname: str, domain: str, port: int, + debug: bool, + year: int, month: int, + password: str) -> str: + """Gets the icalendar for a month via caldav + """ + auth_header = create_basic_auth_header(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/xml', + 'Authorization': auth_header + } + domain_full = get_full_domain(domain, port) + params = {} + url = http_prefix + '://' + domain_full + '/calendars/' + nickname + month_str = str(month) + if month < 10: + month_str = '0' + month_str + xml_str = \ + '\n' + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + '' + result = get_method("REPORT", xml_str, session, url, params, debug) + return result diff --git a/session.py b/session.py index 398c89328..9d951d0d1 100644 --- a/session.py +++ b/session.py @@ -664,3 +664,80 @@ def download_image_any_mime_type(session, url: str, if 'image/' + m_type in content_type: mime_type = 'image/' + m_type return result.content, mime_type + + +def get_method(method_name: str, xml_str: str, + session, url: str, params: {}, debug: bool, + version: str = '1.3.0', http_prefix: str = 'https', + domain: str = 'testdomain', + timeout_sec: int = 20, quiet: bool = False) -> {}: + if method_name not in ("REPORT", "PUT", "PROPFIND"): + print("Unrecognized method: " + method_name) + return None + if not isinstance(url, str): + if debug and not quiet: + print('url: ' + str(url)) + print('ERROR: get_method failed, url should be a string') + return None + headers = { + 'Accept': 'application/xml' + } + session_params = {} + session_headers = {} + if headers: + session_headers = headers + if params: + session_params = params + session_headers['User-Agent'] = 'Epicyon/' + version + if domain: + session_headers['User-Agent'] += \ + '; +' + http_prefix + '://' + domain + '/' + if not session: + if not quiet: + print('WARN: get_method failed, no session specified for get_vcard') + return None + + if debug: + HTTPConnection.debuglevel = 1 + + try: + result = session.request(method_name, url, headers=session_headers, + data=xml_str, + params=session_params, timeout=timeout_sec) + if result.status_code != 200 and result.status_code != 207: + if result.status_code == 401: + print("WARN: get_method " + url + ' rejected by secure mode') + elif result.status_code == 403: + print('WARN: get_method Forbidden url: ' + url) + elif result.status_code == 404: + print('WARN: get_method Not Found url: ' + url) + elif result.status_code == 410: + print('WARN: get_method no longer available url: ' + url) + else: + print('WARN: get_method url: ' + url + + ' failed with error code ' + + str(result.status_code) + + ' headers: ' + str(session_headers)) + return result.content.decode('utf-8') + except requests.exceptions.RequestException as ex: + session_headers2 = session_headers.copy() + if session_headers2.get('Authorization'): + session_headers2['Authorization'] = 'REDACTED' + if debug and not quiet: + print('EX: get_method failed, url: ' + str(url) + ', ' + + 'headers: ' + str(session_headers2) + ', ' + + 'params: ' + str(session_params) + ', ' + str(ex)) + except ValueError as ex: + session_headers2 = session_headers.copy() + if session_headers2.get('Authorization'): + session_headers2['Authorization'] = 'REDACTED' + if debug and not quiet: + print('EX: get_method failed, url: ' + str(url) + ', ' + + 'headers: ' + str(session_headers2) + ', ' + + 'params: ' + str(session_params) + ', ' + str(ex)) + except SocketError as ex: + if not quiet: + if ex.errno == errno.ECONNRESET: + print('EX: get_method failed, ' + + 'connection was reset during get_vcard ' + str(ex)) + return None From 5ee4ec84aab02ceb455453f6e3e6b662fb96053f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 11:10:14 +0000 Subject: [PATCH 05/18] Tidying --- happening.py | 3 ++- session.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/happening.py b/happening.py index 9c70092da..e484ff110 100644 --- a/happening.py +++ b/happening.py @@ -1302,5 +1302,6 @@ def dav_month_via_server(session, http_prefix: str, ' \n' + \ ' \n' + \ '' - result = get_method("REPORT", xml_str, session, url, params, debug) + result = \ + get_method("REPORT", xml_str, session, url, params, headers, debug) return result diff --git a/session.py b/session.py index 9d951d0d1..27b653ac7 100644 --- a/session.py +++ b/session.py @@ -667,7 +667,7 @@ def download_image_any_mime_type(session, url: str, def get_method(method_name: str, xml_str: str, - session, url: str, params: {}, debug: bool, + session, url: str, params: {}, headers: {}, debug: bool, version: str = '1.3.0', http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: @@ -679,9 +679,10 @@ def get_method(method_name: str, xml_str: str, print('url: ' + str(url)) print('ERROR: get_method failed, url should be a string') return None - headers = { - 'Accept': 'application/xml' - } + if not headers: + headers = { + 'Accept': 'application/xml' + } session_params = {} session_headers = {} if headers: @@ -694,7 +695,8 @@ def get_method(method_name: str, xml_str: str, '; +' + http_prefix + '://' + domain + '/' if not session: if not quiet: - print('WARN: get_method failed, no session specified for get_vcard') + print('WARN: get_method failed, ' + + 'no session specified for get_vcard') return None if debug: From 5f5fd75f579f4db6036fc324c79344795e97dce6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 11:17:32 +0000 Subject: [PATCH 06/18] Check that field exists --- webfinger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webfinger.py b/webfinger.py index 440d96b3e..0393c1140 100644 --- a/webfinger.py +++ b/webfinger.py @@ -316,8 +316,9 @@ def _webfinger_update_vcard(wf_json: {}, actor_json: {}) -> bool: """Updates the vcard link """ for link in wf_json['links']: - if link['type'] == 'text/vcard': - return False + if link.get('type'): + if link['type'] == 'text/vcard': + return False wf_json['links'].append({ "href": actor_json['url'], "rel": "http://webfinger.net/rel/profile-page", From 1c31d54636afafd8bfa410581418b4e3e481a690 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 11:37:35 +0000 Subject: [PATCH 07/18] Redact sensitive info so that it doesn't get logged --- session.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/session.py b/session.py index 27b653ac7..ad7a9c8bc 100644 --- a/session.py +++ b/session.py @@ -102,6 +102,8 @@ def _get_json_request(session, url: str, domain_full: str, session_headers: {}, elif result.status_code == 410: print('WARN: get_json no longer available url: ' + url) else: + if session_headers.get('Authorization'): + session_headers['Authorization'] = 'REDACTED' print('WARN: get_json url: ' + url + ' failed with error code ' + str(result.status_code) + @@ -716,6 +718,8 @@ def get_method(method_name: str, xml_str: str, elif result.status_code == 410: print('WARN: get_method no longer available url: ' + url) else: + if session_headers.get('Authorization'): + session_headers['Authorization'] = 'REDACTED' print('WARN: get_method url: ' + url + ' failed with error code ' + str(result.status_code) + From 51d59f3706134e930f386e9ab8dd4fccbf342828 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 12:06:26 +0000 Subject: [PATCH 08/18] Use a copy of the session headers for debug output --- session.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/session.py b/session.py index ad7a9c8bc..e3c11ce65 100644 --- a/session.py +++ b/session.py @@ -102,12 +102,13 @@ def _get_json_request(session, url: str, domain_full: str, session_headers: {}, elif result.status_code == 410: print('WARN: get_json no longer available url: ' + url) else: - if session_headers.get('Authorization'): - session_headers['Authorization'] = 'REDACTED' + session_headers2 = session_headers.copy() + if session_headers2.get('Authorization'): + session_headers2['Authorization'] = 'REDACTED' print('WARN: get_json url: ' + url + ' failed with error code ' + str(result.status_code) + - ' headers: ' + str(session_headers)) + ' headers: ' + str(session_headers2)) if return_json: return result.json() return result.content @@ -294,10 +295,13 @@ def get_vcard(xml_format: bool, elif result.status_code == 410: print('WARN: get_vcard no longer available url: ' + url) else: + session_headers2 = session_headers.copy() + if session_headers2.get('Authorization'): + session_headers2['Authorization'] = 'REDACTED' print('WARN: get_vcard url: ' + url + ' failed with error code ' + str(result.status_code) + - ' headers: ' + str(session_headers)) + ' headers: ' + str(session_headers2)) return result.content.decode('utf-8') except requests.exceptions.RequestException as ex: session_headers2 = session_headers.copy() @@ -718,12 +722,13 @@ def get_method(method_name: str, xml_str: str, elif result.status_code == 410: print('WARN: get_method no longer available url: ' + url) else: - if session_headers.get('Authorization'): - session_headers['Authorization'] = 'REDACTED' + session_headers2 = session_headers.copy() + if session_headers2.get('Authorization'): + session_headers2['Authorization'] = 'REDACTED' print('WARN: get_method url: ' + url + ' failed with error code ' + str(result.status_code) + - ' headers: ' + str(session_headers)) + ' headers: ' + str(session_headers2)) return result.content.decode('utf-8') except requests.exceptions.RequestException as ex: session_headers2 = session_headers.copy() From 4b433644ca5f6ade2e752b766298d9b39786ce91 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 13:10:34 +0000 Subject: [PATCH 09/18] Default version number for session functions --- session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/session.py b/session.py index e3c11ce65..82b06031d 100644 --- a/session.py +++ b/session.py @@ -213,7 +213,7 @@ def _get_json_signed(session, url: str, domain_full: str, session_headers: {}, def get_json(signing_priv_key_pem: str, session, url: str, headers: {}, params: {}, debug: bool, - version: str = '1.3.0', http_prefix: str = 'https', + version: str = __version__, http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: if not isinstance(url, str): @@ -251,7 +251,7 @@ def get_json(signing_priv_key_pem: str, def get_vcard(xml_format: bool, session, url: str, params: {}, debug: bool, - version: str = '1.3.0', http_prefix: str = 'https', + version: str = __version__, http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: if not isinstance(url, str): @@ -329,7 +329,7 @@ def get_vcard(xml_format: bool, def download_html(signing_priv_key_pem: str, session, url: str, headers: {}, params: {}, debug: bool, - version: str = '1.3.0', http_prefix: str = 'https', + version: str = __version__, http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: if not isinstance(url, str): @@ -674,7 +674,7 @@ def download_image_any_mime_type(session, url: str, def get_method(method_name: str, xml_str: str, session, url: str, params: {}, headers: {}, debug: bool, - version: str = '1.3.0', http_prefix: str = 'https', + version: str = __version__, http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: if method_name not in ("REPORT", "PUT", "PROPFIND"): From c41069cf90744e326bba76b71662e5707c25212f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 14:10:32 +0000 Subject: [PATCH 10/18] Add calendar event parameters when sending c2s post --- desktop_client.py | 13 +++++++++++++ epicyon.py | 27 +++++++++++++++++++++++++-- posts.py | 7 +++++-- tests.py | 7 +++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index db0d89bf0..1b816b04d 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -466,6 +466,9 @@ def _desktop_reply_to_post(session, post_id: str, comments_enabled = True city = 'London, England' say_str = 'Sending reply' + event_date = None + event_time = None + location = None _say_command(say_str, say_str, screenreader, system_language, espeak) if send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, nickname, password, @@ -477,6 +480,7 @@ def _desktop_reply_to_post(session, post_id: str, cached_webfingers, person_cache, is_article, system_language, languages_understood, low_bandwidth, content_license_url, + event_date, event_time, location, debug, post_id, post_id, conversation_id, subject) == 0: say_str = 'Reply sent' @@ -535,6 +539,9 @@ def _desktop_new_post(session, comments_enabled = True subject = None say_str = 'Sending' + event_date = None + event_time = None + location = None _say_command(say_str, say_str, screenreader, system_language, espeak) if send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, nickname, password, @@ -546,6 +553,7 @@ def _desktop_new_post(session, cached_webfingers, person_cache, is_article, system_language, languages_understood, low_bandwidth, content_license_url, + event_date, event_time, location, debug, None, None, conversation_id, subject) == 0: say_str = 'Post sent' @@ -1260,6 +1268,10 @@ def _desktop_new_dm_base(session, to_handle: str, _say_command(say_str, say_str, screenreader, system_language, espeak) return + event_date = None + event_time = None + location = None + say_str = 'Sending' _say_command(say_str, say_str, screenreader, system_language, espeak) if send_post_via_server(signing_priv_key_pem, __version__, @@ -1272,6 +1284,7 @@ def _desktop_new_dm_base(session, to_handle: str, cached_webfingers, person_cache, is_article, system_language, languages_understood, low_bandwidth, content_license_url, + event_date, event_time, location, debug, None, None, conversation_id, subject) == 0: say_str = 'Direct message sent' diff --git a/epicyon.py b/epicyon.py index 86f8f2a67..b4c32a61e 100644 --- a/epicyon.py +++ b/epicyon.py @@ -119,6 +119,17 @@ def str2bool(value_str) -> bool: search_date = datetime.datetime.now() parser = argparse.ArgumentParser(description='ActivityPub Server') +parser.add_argument('--eventDate', type=str, + default=None, + help='Date for an event when sending a c2s post' + + ' YYYY-MM-DD') +parser.add_argument('--eventTime', type=str, + default=None, + help='Time for an event when sending a c2s post' + + ' HH:MM') +parser.add_argument('--eventLocation', type=str, + default=None, + help='Location for an event when sending a c2s post') parser.add_argument('--content_license_url', type=str, default='https://creativecommons.org/licenses/by/4.0', help='Url of the license used for the instance content') @@ -1310,6 +1321,16 @@ if args.message: print('Specify a nickname with the --nickname option') sys.exit() + if args.eventDate: + if '-' not in args.eventDate or len(args.eventDate) != 10: + print('Event date format should be YYYY-MM-DD') + sys.exit() + + if args.eventTime: + if ':' not in args.eventTime or len(args.eventTime) != 5: + print('Event time format should be HH:MM') + sys.exit() + if not args.password: args.password = getpass.getpass('Password: ') if not args.password: @@ -1370,8 +1391,8 @@ if args.message: if args.secure_mode: signing_priv_key_pem = get_instance_actor_key(base_dir, domain) languages_understood = [args.language] + print('Sending post to ' + args.sendto) - send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, args.nickname, args.password, domain, port, @@ -1382,7 +1403,9 @@ if args.message: cached_webfingers, person_cache, is_article, args.language, languages_understood, args.low_bandwidth, - args.content_license_url, args.debug, + args.content_license_url, + args.eventDate, args.eventTime, args.eventLocation, + args.debug, reply_to, reply_to, args.conversationId, subject) for i in range(10): # TODO detect send success/fail diff --git a/posts.py b/posts.py index 7137e4ede..256589f3b 100644 --- a/posts.py +++ b/posts.py @@ -2490,6 +2490,8 @@ def send_post_via_server(signing_priv_key_pem: str, project_version: str, languages_understood: [], low_bandwidth: bool, content_license_url: str, + event_date: str, event_time: str, + location: str, debug: bool = False, in_reply_to: str = None, in_reply_to_atom_uri: str = None, @@ -2574,8 +2576,9 @@ def send_post_via_server(signing_priv_key_pem: str, project_version: str, image_description, city, False, is_article, in_reply_to, in_reply_to_atom_uri, subject, - False, None, None, None, None, None, - None, None, None, + False, + event_date, event_time, location, + None, None, None, None, None, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood) diff --git a/tests.py b/tests.py index a1c572ace..966330e5d 100644 --- a/tests.py +++ b/tests.py @@ -2981,6 +2981,12 @@ def test_client_to_server(base_dir: str): if os.path.isfile(os.path.join(bob_outbox_path, name))]) == 0 print('EVENT: all inboxes and outboxes are empty') signing_priv_key_pem = None + test_date = datetime.datetime.now() + event_date = \ + str(test_date.year) + '-' + str(test_date.month) + '-' + \ + str(test_date.day) + event_time = '11:45' + location = "Kinshasa" send_result = \ send_post_via_server(signing_priv_key_pem, __version__, alice_dir, session_alice, 'alice', password, @@ -2993,6 +2999,7 @@ def test_client_to_server(base_dir: str): cached_webfingers, person_cache, is_article, system_language, languages_understood, low_bandwidth, content_license_url, + event_date, event_time, location, True, None, None, conversation_id, None) print('send_result: ' + str(send_result)) From 3aac49eb64362992ea53071213a37c6348197701 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 14:59:10 +0000 Subject: [PATCH 11/18] Check that calendar items get created --- epicyon.py | 2 +- tests.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/epicyon.py b/epicyon.py index b4c32a61e..5b7dc15ee 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1391,7 +1391,7 @@ if args.message: if args.secure_mode: signing_priv_key_pem = get_instance_actor_key(base_dir, domain) languages_understood = [args.language] - + print('Sending post to ' + args.sendto) send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, args.nickname, args.password, diff --git a/tests.py b/tests.py index 966330e5d..6b95c7b0e 100644 --- a/tests.py +++ b/tests.py @@ -2946,6 +2946,13 @@ def test_client_to_server(base_dir: str): time.sleep(1) + # set bob to be following the calendar of alice + print('Bob follows the calendar of Alice') + following_cal_path = \ + bob_dir + '/accounts/bob@' + bob_domain + '/followingCalendar.txt' + with open(following_cal_path, 'w+') as fp: + fp.write('alice@' + alice_domain + '\n') + print('\n\n*******************************************************') print('EVENT: Alice sends to Bob via c2s') @@ -3035,6 +3042,17 @@ def test_client_to_server(base_dir: str): if os.path.isfile(os.path.join(bob_outbox_path, name))]) == 0 print(">>> s2s post arrived in Bob's inbox") + + calendar_path = bob_dir + '/accounts/bob@' + bob_domain + '/calendar' + if not os.path.isdir(calendar_path): + print('Missing calendar path: ' + calendar_path) + assert os.path.isdir(calendar_path) + assert os.path.isdir(calendar_path + '/' + str(test_date.year)) + assert os.path.isfile(calendar_path + '/' + str(test_date.year) + '/' + + str(test_date.month) + '.txt') + print(">>> calendar entry created for s2s post which arrived at " + + "Bob's inbox") + print("c2s send success\n\n\n") print('\n\nEVENT: Getting message id for the post') From 44ba3bf35c718b43fc64255c80fa436598b9143d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 16:55:37 +0000 Subject: [PATCH 12/18] Unit test for caldav report --- auth.py | 29 +++++++++++++++++++---------- daemon.py | 14 +++++++++----- happening.py | 18 ++++++++++++------ session.py | 2 ++ tests.py | 18 ++++++++++++++++++ 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/auth.py b/auth.py index 13f89c40f..e137eeab6 100644 --- a/auth.py +++ b/auth.py @@ -94,16 +94,25 @@ def authorize_basic(base_dir: str, path: str, auth_header: str, 'contain a space character') return False if not has_users_path(path): - if debug: - print('DEBUG: basic auth - ' + - 'path for Authorization does not contain a user') - return False - path_users_section = path.split('/users/')[1] - if '/' not in path_users_section: - if debug: - print('DEBUG: basic auth - this is not a users endpoint') - return False - nickname_from_path = path_users_section.split('/')[0] + if not path.startswith('/calendars/'): + if debug: + print('DEBUG: basic auth - ' + + 'path for Authorization does not contain a user') + return False + if path.startswith('/calendars/'): + path_users_section = path.split('/calendars/')[1] + nickname_from_path = path_users_section + if '/' in nickname_from_path: + nickname_from_path = nickname_from_path.split('/')[0] + if '?' in nickname_from_path: + nickname_from_path = nickname_from_path.split('?')[0] + else: + path_users_section = path.split('/users/')[1] + if '/' not in path_users_section: + if debug: + print('DEBUG: basic auth - this is not a users endpoint') + return False + nickname_from_path = path_users_section.split('/')[0] if is_system_account(nickname_from_path): print('basic auth - attempted login using system account ' + nickname_from_path + ' in path') diff --git a/daemon.py b/daemon.py index ed2bb9154..bcf65847b 100644 --- a/daemon.py +++ b/daemon.py @@ -16832,13 +16832,15 @@ class PubServer(BaseHTTPRequestHandler): '_GET', 'end benchmarks', self.server.debug) - def _dav_handler(self, endpoint_type: str): + def _dav_handler(self, endpoint_type: str, debug: bool): calling_domain = self.server.domain_full if not self._has_accept(calling_domain): self._400() return accept_str = self.headers['Accept'] if 'application/xml' not in accept_str: + if debug: + print(endpoint_type.upper() + ' is not of xml type') self._400() return if not self.headers.get('Content-length'): @@ -16855,6 +16857,8 @@ class PubServer(BaseHTTPRequestHandler): print(endpoint_type.upper() + ' without /calendars ' + self.path) self._404() return + if debug: + print(endpoint_type.upper() + ' checking authorization') if not self._is_authorized(): print(endpoint_type.upper() + ' not authorized') self._403() @@ -16955,16 +16959,16 @@ class PubServer(BaseHTTPRequestHandler): self._200() def do_PROPFIND(self): - self._dav_handler('propfind') + self._dav_handler('propfind', self.server.debug) def do_PUT(self): - self._dav_handler('put') + self._dav_handler('put', self.server.debug) def do_REPORT(self): - self._dav_handler('report') + self._dav_handler('report', self.server.debug) def do_DELETE(self): - self._dav_handler('delete') + self._dav_handler('delete', self.server.debug) def do_HEAD(self): calling_domain = self.server.domain_full diff --git a/happening.py b/happening.py index e484ff110..a3d536637 100644 --- a/happening.py +++ b/happening.py @@ -975,7 +975,7 @@ def dav_put_response(base_dir: str, nickname: str, domain: str, 'END:VEVENT' not in xml_str: return None - etag = md5(xml_str).hexdigest() + etag = md5(xml_str.encode('utf-8')).hexdigest() if recent_dav_etags.get(nickname): if etag in recent_dav_etags[nickname]: return 'Not modified' @@ -1091,7 +1091,8 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, str(search_date.month) + '?day=' + str(search_date.day) if ical_events: if 'VEVENT' in ical_events: - etag = md5(ical_events).hexdigest() + ical_events_encoded = ical_events.encode('utf-8') + etag = md5(ical_events_encoded).hexdigest() responses = \ ' \n' + \ ' ' + events_href + \ @@ -1124,7 +1125,8 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, str(query_start_month) if ical_events: if 'VEVENT' in ical_events: - etag = md5(ical_events).hexdigest() + ical_events_encoded = ical_events.encode('utf-8') + etag = md5(ical_events_encoded).hexdigest() responses = \ ' \n' + \ ' ' + events_href + \ @@ -1172,6 +1174,8 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, if ical_events: if 'VEVENT' in ical_events: all_events += ical_events + ical_events_encoded = ical_events.encode('utf-8') + local_etag = md5(ical_events_encoded).hexdigest() responses += \ ' \n' + \ ' ' + events_href + \ @@ -1179,7 +1183,7 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, ' \n' + \ ' \n' + \ ' "' + \ - etag + '"\n' + \ + local_etag + '"\n' + \ ' ' + \ ical_events + \ ' \n' + \ @@ -1188,7 +1192,8 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, '\n' + \ ' \n' + \ ' \n' - etag = md5(all_events).hexdigest() + ical_events_encoded = all_events.encode('utf-8') + etag = md5(ical_events_encoded).hexdigest() # today's calendar events if not ical_events: @@ -1204,7 +1209,8 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, str(search_date.month) + '?day=' + str(search_date.day) if ical_events: if 'VEVENT' in ical_events: - etag = md5(ical_events).hexdigest() + ical_events_encoded = ical_events.encode('utf-8') + etag = md5(ical_events_encoded).hexdigest() responses = \ ' \n' + \ ' ' + events_href + '\n' + \ diff --git a/session.py b/session.py index 82b06031d..0a2fc4247 100644 --- a/session.py +++ b/session.py @@ -689,6 +689,8 @@ def get_method(method_name: str, xml_str: str, headers = { 'Accept': 'application/xml' } + else: + headers['Accept'] = 'application/xml' session_params = {} session_headers = {} if headers: diff --git a/tests.py b/tests.py index 6b95c7b0e..51a04b075 100644 --- a/tests.py +++ b/tests.py @@ -176,6 +176,8 @@ from shares import send_share_via_server from shares import get_shared_items_catalog_via_server from blocking import load_cw_lists from blocking import add_cw_from_lists +from happening import dav_month_via_server + TEST_SERVER_GROUP_RUNNING = False TEST_SERVER_ALICE_RUNNING = False @@ -3172,6 +3174,22 @@ def test_client_to_server(base_dir: str): show_test_boxes('bob', bob_inbox_path, bob_outbox_path) assert len([name for name in os.listdir(alice_inbox_path) if os.path.isfile(os.path.join(alice_inbox_path, name))]) == 0 + + print('\n\nEVENT: Bob checks his calendar via caldav') + if os.path.isfile(bob_dir + '/basic_auth_fail.txt'): + os.remove(bob_dir + '/basic_auth_fail.txt') + result = \ + dav_month_via_server(session_bob, http_prefix, + 'bob', bob_domain, bob_port, True, + test_date.year, test_date.month, + 'bobpass') + print('response: ' + str(result)) + if os.path.isfile(bob_dir + '/basic_auth_fail.txt'): + with open(bob_dir + '/basic_auth_fail.txt', 'r') as fp_fail: + print(fp_fail.read()) + assert 'VCALENDAR' in str(result) + assert 'VEVENT' in str(result) + print('\n\nEVENT: Bob likes the post') send_like_via_server(bob_dir, session_bob, 'bob', 'bobpass', From 7ec097cade90d2432b8bc4edabdf4467056a28d6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 17:23:41 +0000 Subject: [PATCH 13/18] Unit test for getting day via caldav --- epicyon.py | 24 +++++++++++++++++------ happening.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++---- tests.py | 14 +++++++++----- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/epicyon.py b/epicyon.py index 5b7dc15ee..043c51601 100644 --- a/epicyon.py +++ b/epicyon.py @@ -103,6 +103,7 @@ from socnet import instances_graph from migrate import migrate_accounts from desktop_client import run_desktop_client from happening import dav_month_via_server +from happening import dav_day_via_server def str2bool(value_str) -> bool: @@ -192,6 +193,9 @@ parser.add_argument('--year', dest='year', type=int, parser.add_argument('--month', dest='month', type=int, default=search_date.month, help='Month for calendar query') +parser.add_argument('--day', dest='day', type=int, + default=None, + help='Day for calendar query') parser.add_argument('--postsPerSource', dest='max_newswire_postsPerSource', type=int, default=4, @@ -1443,12 +1447,20 @@ if args.dav: elif args.gnunet: proxy_type = 'gnunet' session = create_session(proxy_type) - result = \ - dav_month_via_server(session, http_prefix, - args.nickname, args.domain, args.port, - args.debug, - args.year, args.month, - args.password) + if args.day: + result = \ + dav_day_via_server(session, http_prefix, + args.nickname, args.domain, args.port, + args.debug, + args.year, args.month, args.day, + args.password) + else: + result = \ + dav_month_via_server(session, http_prefix, + args.nickname, args.domain, args.port, + args.debug, + args.year, args.month, + args.password) if result: print(str(result)) sys.exit() diff --git a/happening.py b/happening.py index a3d536637..947a993f7 100644 --- a/happening.py +++ b/happening.py @@ -1075,9 +1075,9 @@ def dav_report_response(base_dir: str, nickname: str, domain: str, if query_start_day == query_end_day: # calendar for one day search_date = \ - datetime.datetime(year=query_start_year, - month=query_start_month, - day=query_start_day) + datetime(year=query_start_year, + month=query_start_month, + day=query_start_day) ical_events = \ get_todays_events_icalendar(base_dir, nickname, domain, search_date.year, @@ -1303,7 +1303,53 @@ def dav_month_via_server(session, http_prefix: str, ' \n' + \ + '31T235959Z"/>\n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + '' + result = \ + get_method("REPORT", xml_str, session, url, params, headers, debug) + return result + + +def dav_day_via_server(session, http_prefix: str, + nickname: str, domain: str, port: int, + debug: bool, + year: int, month: int, day: int, + password: str) -> str: + """Gets the icalendar for a day via caldav + """ + auth_header = create_basic_auth_header(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/xml', + 'Authorization': auth_header + } + domain_full = get_full_domain(domain, port) + params = {} + url = http_prefix + '://' + domain_full + '/calendars/' + nickname + month_str = str(month) + if month < 10: + month_str = '0' + month_str + day_str = str(day) + if day < 10: + day_str = '0' + day_str + xml_str = \ + '\n' + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ + ' \n' + \ ' \n' + \ ' \n' + \ ' \n' + \ diff --git a/tests.py b/tests.py index 51a04b075..8d958c92d 100644 --- a/tests.py +++ b/tests.py @@ -177,6 +177,7 @@ from shares import get_shared_items_catalog_via_server from blocking import load_cw_lists from blocking import add_cw_from_lists from happening import dav_month_via_server +from happening import dav_day_via_server TEST_SERVER_GROUP_RUNNING = False @@ -3176,17 +3177,20 @@ def test_client_to_server(base_dir: str): if os.path.isfile(os.path.join(alice_inbox_path, name))]) == 0 print('\n\nEVENT: Bob checks his calendar via caldav') - if os.path.isfile(bob_dir + '/basic_auth_fail.txt'): - os.remove(bob_dir + '/basic_auth_fail.txt') result = \ dav_month_via_server(session_bob, http_prefix, 'bob', bob_domain, bob_port, True, test_date.year, test_date.month, 'bobpass') print('response: ' + str(result)) - if os.path.isfile(bob_dir + '/basic_auth_fail.txt'): - with open(bob_dir + '/basic_auth_fail.txt', 'r') as fp_fail: - print(fp_fail.read()) + assert 'VCALENDAR' in str(result) + assert 'VEVENT' in str(result) + result = \ + dav_day_via_server(session_bob, http_prefix, + 'bob', bob_domain, bob_port, True, + test_date.year, test_date.month, + test_date.day, 'bobpass') + print('response: ' + str(result)) assert 'VCALENDAR' in str(result) assert 'VEVENT' in str(result) From f54591b2c9424bb7902989c2cb08dc7f7c390efe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 17:29:12 +0000 Subject: [PATCH 14/18] Test for incorrect caldav login --- tests.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 8d958c92d..8029d584f 100644 --- a/tests.py +++ b/tests.py @@ -3177,6 +3177,7 @@ def test_client_to_server(base_dir: str): if os.path.isfile(os.path.join(alice_inbox_path, name))]) == 0 print('\n\nEVENT: Bob checks his calendar via caldav') + # test caldav result for a month result = \ dav_month_via_server(session_bob, http_prefix, 'bob', bob_domain, bob_port, True, @@ -3185,6 +3186,7 @@ def test_client_to_server(base_dir: str): print('response: ' + str(result)) assert 'VCALENDAR' in str(result) assert 'VEVENT' in str(result) + # test caldav result for a day result = \ dav_day_via_server(session_bob, http_prefix, 'bob', bob_domain, bob_port, True, @@ -3193,7 +3195,15 @@ def test_client_to_server(base_dir: str): print('response: ' + str(result)) assert 'VCALENDAR' in str(result) assert 'VEVENT' in str(result) - + # test for incorrect caldav login + result = \ + dav_day_via_server(session_bob, http_prefix, + 'bob', bob_domain, bob_port, True, + test_date.year, test_date.month, + test_date.day, 'wrongpass') + assert 'VCALENDAR' not in str(result) + assert 'VEVENT' not in str(result) + print('\n\nEVENT: Bob likes the post') send_like_via_server(bob_dir, session_bob, 'bob', 'bobpass', From f9d621f940f7c8b1b27407e689fb7ab7fead17f3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 17:31:12 +0000 Subject: [PATCH 15/18] Don't try to preview youtube channels --- webapp_media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp_media.py b/webapp_media.py index 758c6ec95..a86865dfa 100644 --- a/webapp_media.py +++ b/webapp_media.py @@ -53,7 +53,7 @@ def _add_embedded_video_from_sites(translate: {}, content: str, content = content.replace('https://m.youtube.com', video_site) if '"' + video_site in content: url = content.split('"' + video_site)[1] - if '"' in url: + if '"' in url and '/channel/' not in url: url = url.split('"')[0].replace('/watch?v=', '/embed/') if '&' in url: url = url.split('&')[0] @@ -70,7 +70,7 @@ def _add_embedded_video_from_sites(translate: {}, content: str, video_site = 'https://youtu.be/' if '"' + video_site in content: url = content.split('"' + video_site)[1] - if '"' in url: + if '"' in url and '/channel/' not in url: url = 'embed/' + url.split('"')[0] if '&' in url: url = url.split('&')[0] From 266a74e85ab00b19e0ee4ea552e19a5b20316f1a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 17:34:59 +0000 Subject: [PATCH 16/18] Tidying --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 8029d584f..b0a21d116 100644 --- a/tests.py +++ b/tests.py @@ -3203,7 +3203,7 @@ def test_client_to_server(base_dir: str): test_date.day, 'wrongpass') assert 'VCALENDAR' not in str(result) assert 'VEVENT' not in str(result) - + print('\n\nEVENT: Bob likes the post') send_like_via_server(bob_dir, session_bob, 'bob', 'bobpass', From c28e6b5f0f38fe61f86fcc67de7fcb61cdb18091 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 19:03:11 +0000 Subject: [PATCH 17/18] Check for channel after url is extracted --- webapp_media.py | 58 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/webapp_media.py b/webapp_media.py index a86865dfa..db642168b 100644 --- a/webapp_media.py +++ b/webapp_media.py @@ -53,37 +53,41 @@ def _add_embedded_video_from_sites(translate: {}, content: str, content = content.replace('https://m.youtube.com', video_site) if '"' + video_site in content: url = content.split('"' + video_site)[1] - if '"' in url and '/channel/' not in url: - url = url.split('"')[0].replace('/watch?v=', '/embed/') - if '&' in url: - url = url.split('&')[0] - if '?utm_' in url: - url = url.split('?utm_')[0] - content += \ - "
\n\n
\n" - return content + if '"' in url: + url = url.split('"')[0] + if '/channel/' not in url: + url = url.replace('/watch?v=', '/embed/') + if '&' in url: + url = url.split('&')[0] + if '?utm_' in url: + url = url.split('?utm_')[0] + content += \ + "
\n\n
\n" + return content video_site = 'https://youtu.be/' if '"' + video_site in content: url = content.split('"' + video_site)[1] - if '"' in url and '/channel/' not in url: - url = 'embed/' + url.split('"')[0] - if '&' in url: - url = url.split('&')[0] - if '?utm_' in url: - url = url.split('?utm_')[0] - video_site = 'https://www.youtube.com/' - content += \ - "
\n\n
\n" - return content + if '"' in url: + url = url.split('"')[0] + if '/channel/' not in url: + url = 'embed/' + url + if '&' in url: + url = url.split('&')[0] + if '?utm_' in url: + url = url.split('?utm_')[0] + video_site = 'https://www.youtube.com/' + content += \ + "
\n\n
\n" + return content invidious_sites = ( 'https://invidious.snopyta.org', From a3bc8c3b9c292e6625d4bc9d1f7a9fddbe49612d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 24 Feb 2022 20:37:47 +0000 Subject: [PATCH 18/18] Note about caldav --- README_commandline.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README_commandline.md b/README_commandline.md index 9529671b9..27e57c1ad 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -372,3 +372,19 @@ To remove a shared item: ``` bash python3 epicyon.py --undoItemName "spanner" --nickname [yournick] --domain [yourdomain] --password [c2s password] ``` + +## Calendar + +The calendar for each account can be accessed via CalDav (RFC4791). This makes it easy to integrate the social calendar into other applications. For example, to obtain events for a month: + +```bash +python3 epicyon.py --dav --nickname [yournick] --domain [yourdomain] --year [year] --month [month number] +``` + +You will be prompted for your login password, or you can use the **--password** option. You can also use the **--day** option to obtain events for a particular day. + +The CalDav endpoint for an account is: + +```bash +yourdomain/calendars/yournick +```