Merge branch 'main' of gitlab.com:bashrc2/epicyon

merge-requests/30/head
Bob Mottram 2022-02-24 20:39:00 +00:00
commit e26c1b2b56
12 changed files with 791 additions and 174 deletions

View File

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

29
auth.py
View File

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

View File

@ -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 = \
'<html><head><title>' + str(http_code) + '</title></head>' \
'<body bgcolor="linen" text="black">' \
@ -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)

View File

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

View File

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

View File

@ -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 '<c:calendar-multiget' not in xml_str or \
'</c:calendar-multiget>' 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 = \
' <d:response>\n' + \
' <d:href>' + events_href + \
'</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + \
ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\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 = \
' <d:response>\n' + \
' <d:href>' + events_href + \
'</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + \
ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\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 += \
' <d:response>\n' + \
' <d:href>' + events_href + \
'</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
local_etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + \
ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\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 = \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + etag + \
'"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\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 = \
'<?xml version="1.0" encoding="utf-8" ?>\n' + \
'<d:multistatus xmlns:d="DAV:" ' + \
'xmlns:cs="http://calendarserver.org/ns/">\n' + \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n' + \
' <d:response>\n' + \
'</d:multistatus>'
responses + '</d:multistatus>'
_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 = \
'<?xml version="1.0" encoding="utf-8" ?>\n' + \
'<c:calendar-query xmlns:d="DAV:"\n' + \
' xmlns:c="urn:ietf:params:xml:ns:caldav">\n' + \
' <d:prop>\n' + \
' <d:getetag/>\n' + \
' </d:prop>\n' + \
' <c:filter>\n' + \
' <c:comp-filter name="VCALENDAR">\n' + \
' <c:comp-filter name="VEVENT">\n' + \
' <c:time-range start="' + str(year) + month_str + \
'01T000000Z"\n' + \
' end="' + str(year) + month_str + \
'31T235959Z"/>\n' + \
' </c:comp-filter>\n' + \
' </c:comp-filter>\n' + \
' </c:filter>\n' + \
'</c:calendar-query>'
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 = \
'<?xml version="1.0" encoding="utf-8" ?>\n' + \
'<c:calendar-query xmlns:d="DAV:"\n' + \
' xmlns:c="urn:ietf:params:xml:ns:caldav">\n' + \
' <d:prop>\n' + \
' <d:getetag/>\n' + \
' </d:prop>\n' + \
' <c:filter>\n' + \
' <c:comp-filter name="VCALENDAR">\n' + \
' <c:comp-filter name="VEVENT">\n' + \
' <c:time-range start="' + str(year) + month_str + \
day_str + 'T000000Z"\n' + \
' end="' + str(year) + month_str + \
day_str + 'T235959Z"/>\n' + \
' </c:comp-filter>\n' + \
' </c:comp-filter>\n' + \
' </c:filter>\n' + \
'</c:calendar-query>'
result = \
get_method("REPORT", xml_str, session, url, params, headers, debug)
return result

View File

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

View File

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

View File

@ -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',

View File

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

View File

@ -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 += \
"<center>\n<iframe loading=\"lazy\" src=\"" + \
video_site + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 += \
"<center>\n<iframe loading=\"lazy\" src=\"" + \
video_site + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 += \
"<center>\n<iframe loading=\"lazy\" src=\"" + \
video_site + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 += \
"<center>\n<iframe loading=\"lazy\" src=\"" + \
video_site + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\n"
return content
invidious_sites = (
'https://invidious.snopyta.org',

View File

@ -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",