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
+```
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 ee114533b..bcf65847b 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
@@ -16824,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'):
@@ -16847,8 +16857,10 @@ 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('PROPFIND Not authorized')
+ print(endpoint_type.upper() + ' not authorized')
self._403()
return
nickname = self.path.split('/calendars/')[1]
@@ -16869,9 +16881,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:
@@ -16898,7 +16911,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'):
@@ -16912,6 +16926,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 = \
@@ -16925,7 +16940,13 @@ 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)
elif response_str != 'Ok':
message_xml = response_str.encode('utf-8')
message_xml_len = len(message_xml)
@@ -16935,22 +16956,19 @@ 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')
+ 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
@@ -19274,6 +19292,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/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 a98d161a7..043c51601 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,8 @@ 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
+from happening import dav_day_via_server
def str2bool(value_str) -> bool:
@@ -115,7 +118,19 @@ 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('--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')
@@ -172,6 +187,15 @@ 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('--day', dest='day', type=int,
+ default=None,
+ help='Day for calendar query')
parser.add_argument('--postsPerSource',
dest='max_newswire_postsPerSource', type=int,
default=4,
@@ -329,6 +353,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='?',
@@ -1296,6 +1325,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:
@@ -1356,8 +1395,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)
+ print('Sending post to ' + args.sendto)
send_post_via_server(signing_priv_key_pem, __version__,
base_dir, session, args.nickname, args.password,
domain, port,
@@ -1368,13 +1407,64 @@ 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
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)
+ 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()
+
if args.announce:
if not args.nickname:
print('Specify a nickname with the --nickname option')
diff --git a/happening.py b/happening.py
index 72e38c9c1..947a993f7 100644
--- a/happening.py
+++ b/happening.py
@@ -23,8 +23,36 @@ 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:
+ """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):
@@ -181,9 +209,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
"""
@@ -222,6 +267,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 = []
@@ -413,13 +464,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)]
@@ -447,12 +500,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' + \
@@ -597,7 +651,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
@@ -621,9 +676,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']:
@@ -784,66 +847,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 +944,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 +975,11 @@ def dav_put_response(base_dir: str, nickname: str, domain: str,
'END:VEVENT' not in xml_str:
return None
+ etag = md5(xml_str.encode('utf-8')).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 +1002,14 @@ def dav_put_response(base_dir: str, nickname: str, domain: str,
event_list.append(line)
if stored_count == 0:
return None
- return 'Ok'
+ _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 +1018,227 @@ 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)
+
+ 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 = ''
+ 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(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, 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:
+ if 'VEVENT' in ical_events:
+ ical_events_encoded = ical_events.encode('utf-8')
+ etag = md5(ical_events_encoded).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,
+ text_match)
+ events_href = \
+ http_prefix + '://' + domain_full + '/users/' + \
+ nickname + '/calendar?year=' + \
+ str(query_start_year) + '?month=' + \
+ str(query_start_month)
+ if ical_events:
+ if 'VEVENT' in ical_events:
+ ical_events_encoded = ical_events.encode('utf-8')
+ etag = md5(ical_events_encoded).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,
+ text_match)
+ events_href = \
+ http_prefix + '://' + domain_full + '/users/' + \
+ nickname + '/calendar?year=' + \
+ str(year) + '?month=' + \
+ str(month)
+ 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 + \
+ '\n' + \
+ ' \n' + \
+ ' \n' + \
+ ' "' + \
+ local_etag + '"\n' + \
+ ' ' + \
+ ical_events + \
+ ' \n' + \
+ ' \n' + \
+ ' HTTP/1.1 200 OK' + \
+ '\n' + \
+ ' \n' + \
+ ' \n'
+ ical_events_encoded = all_events.encode('utf-8')
+ etag = md5(ical_events_encoded).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, 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:
+ if 'VEVENT' in ical_events:
+ ical_events_encoded = ical_events.encode('utf-8')
+ etag = md5(ical_events_encoded).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
@@ -1048,3 +1268,92 @@ 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, 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' + \
+ ''
+ result = \
+ get_method("REPORT", xml_str, session, url, params, headers, debug)
+ return result
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/session.py b/session.py
index 398c89328..0a2fc4247 100644
--- a/session.py
+++ b/session.py
@@ -102,10 +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:
+ 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
@@ -210,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):
@@ -248,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):
@@ -292,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()
@@ -323,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):
@@ -664,3 +670,87 @@ 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: {}, headers: {}, debug: bool,
+ 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"):
+ 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
+ if not headers:
+ headers = {
+ 'Accept': 'application/xml'
+ }
+ else:
+ 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:
+ 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_headers2))
+ 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
diff --git a/tests.py b/tests.py
index a1c572ace..b0a21d116 100644
--- a/tests.py
+++ b/tests.py
@@ -176,6 +176,9 @@ 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
+from happening import dav_day_via_server
+
TEST_SERVER_GROUP_RUNNING = False
TEST_SERVER_ALICE_RUNNING = False
@@ -2946,6 +2949,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')
@@ -2981,6 +2991,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 +3009,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))
@@ -3028,6 +3045,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')
@@ -3147,6 +3175,35 @@ 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')
+ # test caldav result for a month
+ 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))
+ 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,
+ test_date.year, test_date.month,
+ test_date.day, 'bobpass')
+ 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',
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
diff --git a/webapp_media.py b/webapp_media.py
index 758c6ec95..db642168b 100644
--- a/webapp_media.py
+++ b/webapp_media.py
@@ -54,36 +54,40 @@ def _add_embedded_video_from_sites(translate: {}, content: str,
if '"' + video_site in content:
url = content.split('"' + video_site)[1]
if '"' 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
+ 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:
- 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
+ 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',
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",