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

merge-requests/30/head
Bob Mottram 2022-02-23 12:24:11 +00:00
commit 30459db36a
21 changed files with 617 additions and 18 deletions

151
daemon.py
View File

@ -341,6 +341,10 @@ from schedule import run_post_schedule_watchdog
from schedule import remove_scheduled_posts from schedule import remove_scheduled_posts
from outbox import post_message_to_outbox from outbox import post_message_to_outbox
from happening import remove_calendar_event from happening import remove_calendar_event
from happening import dav_propfind_response
from happening import dav_put_response
from happening import dav_report_response
from happening import dav_delete_response
from bookmarks import bookmark_post from bookmarks import bookmark_post
from bookmarks import undo_bookmark_post from bookmarks import undo_bookmark_post
from petnames import set_pet_name from petnames import set_pet_name
@ -975,6 +979,25 @@ class PubServer(BaseHTTPRequestHandler):
'This is nothing less ' + 'This is nothing less ' +
'than an utter triumph') 'than an utter triumph')
def _201(self) -> None:
if self.server.translate:
ok_str = self.server.translate['Done']
self._http_return_code(201,
self.server.translate['Created'], ok_str)
else:
self._http_return_code(201, 'Created',
'Done')
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)
else:
self._http_return_code(207, 'Multi Status',
'Lots of things')
def _403(self) -> None: def _403(self) -> None:
if self.server.translate: if self.server.translate:
self._http_return_code(403, self.server.translate['Forbidden'], self._http_return_code(403, self.server.translate['Forbidden'],
@ -16801,6 +16824,134 @@ class PubServer(BaseHTTPRequestHandler):
'_GET', 'end benchmarks', '_GET', 'end benchmarks',
self.server.debug) self.server.debug)
def _dav_handler(self, endpoint_type: str):
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:
self._400()
return
if not self.headers.get('Content-length'):
print(endpoint_type.upper() + ' has no content-length')
self._400()
return
length = int(self.headers['Content-length'])
if length > self.server.max_post_length:
print(endpoint_type.upper() +
' request size too large ' + self.path)
self._400()
return
if not self.path.startswith('/calendars/'):
print(endpoint_type.upper() + ' without /calendars ' + self.path)
self._404()
return
if not self._is_authorized():
print('PROPFIND Not authorized')
self._403()
return
nickname = self.path.split('/calendars/')[1]
if '/' in nickname:
nickname = nickname.split('/')[0]
if not nickname:
print(endpoint_type.upper() + ' no nickname ' + self.path)
self._400()
return
if not os.path.isdir(self.server.base_dir + '/accounts/' +
nickname + '@' + self.server.domain):
print(endpoint_type.upper() +
' for non-existent account ' + self.path)
self._404()
return
propfind_bytes = None
try:
propfind_bytes = self.rfile.read(length)
except SocketError as ex:
if ex.errno == errno.ECONNRESET:
print('EX: PROPFIND connection reset by peer')
else:
print('EX: PROPFIND socket error')
self._400()
return
except ValueError as ex:
print('EX: ' + endpoint_type.upper() +
' rfile.read failed, ' + str(ex))
self._400()
return
if not propfind_bytes:
self._404()
return
depth = 0
if self.headers.get('Depth'):
depth = self.headers['Depth']
propfind_xml = propfind_bytes.decode('utf-8')
response_str = None
if endpoint_type == 'propfind':
response_str = \
dav_propfind_response(self.server.base_dir,
nickname, self.server.domain,
depth, propfind_xml)
elif endpoint_type == 'put':
response_str = \
dav_put_response(self.server.base_dir,
nickname, self.server.domain,
depth, propfind_xml,
self.server.http_prefix,
self.server.system_language)
elif endpoint_type == 'report':
curr_etag = None
if self.headers.get('ETag'):
curr_etag = self.headers['ETag']
elif self.headers.get('Etag'):
curr_etag = self.headers['Etag']
response_str = \
dav_report_response(self.server.base_dir,
nickname, self.server.domain,
depth, propfind_xml,
self.server.person_cache,
self.server.http_prefix,
curr_etag,
self.server.domain_full)
elif endpoint_type == 'delete':
response_str = \
dav_delete_response(self.server.base_dir,
nickname, self.server.domain,
depth, self.path,
self.server.http_prefix,
self.server.debug,
self.server.recent_posts_cache)
if not response_str:
self._404()
return
if response_str == 'Not modified':
return self._304()
elif response_str != 'Ok':
message_xml = response_str.encode('utf-8')
message_xml_len = len(message_xml)
self._set_headers('application/xml; charset=utf-8',
message_xml_len,
None, calling_domain, False)
self._write(message_xml)
if 'multistatus' in response_str:
return self._207()
if endpoint_type == 'put':
self._201()
else:
self._200()
def do_PROPFIND(self):
self._dav_handler('propfind')
def do_PUT(self):
self._dav_handler('put')
def do_REPORT(self):
self._dav_handler('report')
def do_DELETE(self):
self._dav_handler('delete')
def do_HEAD(self): def do_HEAD(self):
calling_domain = self.server.domain_full calling_domain = self.server.domain_full
if self.headers.get('Host'): if self.headers.get('Host'):

View File

@ -9,6 +9,7 @@ __module_group__ = "Core"
import os import os
from uuid import UUID from uuid import UUID
from hashlib import md5
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
@ -20,6 +21,10 @@ from utils import has_object_dict
from utils import acct_dir from utils import acct_dir
from utils import remove_html from utils import remove_html
from utils import get_display_name from utils import get_display_name
from utils import delete_post
from utils import get_status_number
from filters import is_filtered
from context import get_individual_post_context
def _valid_uuid(test_uuid: str, version: int): def _valid_uuid(test_uuid: str, version: int):
@ -274,6 +279,14 @@ def _ical_date_string(date_str: str) -> str:
return date_str.replace(' ', '') return date_str.replace(' ', '')
def _dav_encode_token(year: int, month_number: int,
message_id: str) -> str:
"""Returns a token corresponding to a calendar event
"""
return str(year) + '_' + str(month_number) + '_' + \
message_id.replace('/', '--').replace('#', '--')
def _icalendar_day(base_dir: str, nickname: str, domain: str, def _icalendar_day(base_dir: str, nickname: str, domain: str,
day_events: [], person_cache: {}, day_events: [], person_cache: {},
http_prefix: str) -> str: http_prefix: str) -> str:
@ -357,10 +370,14 @@ def _icalendar_day(base_dir: str, nickname: str, domain: str,
event_end = \ event_end = \
_ical_date_string(event_end.strftime("%Y-%m-%dT%H:%M:%SZ")) _ical_date_string(event_end.strftime("%Y-%m-%dT%H:%M:%SZ"))
token_year = int(event_start[:4])
token_month_number = int(event_start[4:][:2])
uid = _dav_encode_token(token_year, token_month_number, post_id)
ical_str += \ ical_str += \
'BEGIN:VEVENT\n' + \ 'BEGIN:VEVENT\n' + \
'DTSTAMP:' + published + '\n' + \ 'DTSTAMP:' + published + '\n' + \
'UID:' + post_id + '\n' + \ 'UID:' + uid + '\n' + \
'DTSTART:' + event_start + '\n' + \ 'DTSTART:' + event_start + '\n' + \
'DTEND:' + event_end + '\n' + \ 'DTEND:' + event_end + '\n' + \
'STATUS:CONFIRMED\n' 'STATUS:CONFIRMED\n'
@ -677,3 +694,357 @@ def remove_calendar_event(base_dir: str, nickname: str, domain: str,
fp_cal.write(line) fp_cal.write(line)
except OSError: except OSError:
print('EX: unable to write ' + calendar_filename) print('EX: unable to write ' + calendar_filename)
def _dav_decode_token(token: str) -> (int, int, str):
"""Decodes a token corresponding to a calendar event
"""
if '_' not in token or '--' not in token:
return None, None, None
token_sections = token.split('_')
if len(token_sections) != 3:
return None, None, None
if not token_sections[0].isdigit():
return None, None, None
if not token_sections[1].isdigit():
return None, None, None
token_year = int(token_sections[0])
token_month_number = int(token_sections[1])
token_post_id = token_sections[2].replace('--', '/')
return token_year, token_month_number, token_post_id
def dav_propfind_response(base_dir: str, nickname: str, domain: str,
depth: int, xml_str: str) -> str:
"""Returns the response to caldav PROPFIND
"""
if '<d:propfind' not in xml_str or \
'</d:propfind>' not in xml_str:
return None
response_str = \
'<d:multistatus xmlns:d="DAV:" ' + \
'xmlns:cs="http://calendarserver.org/ns/">\n' + \
' <d:response>\n' + \
' <d:href>/calendars/' + nickname + '/</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:displayname />\n' + \
' <cs:getctag />\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n' + \
'</d:multistatus>'
return response_str
def _dav_store_event(base_dir: str, nickname: str, domain: str,
event_list: [], http_prefix: str,
system_language: str) -> bool:
"""Stores a calendar event obtained via caldav PUT
"""
event_str = str(event_list)
if 'DTSTAMP:' not in event_str or \
'DTSTART:' not in event_str or \
'DTEND:' not in event_str:
return False
if 'STATUS:' not in event_str and 'DESCRIPTION:' not in event_str:
return False
timestamp = None
start_time = None
end_time = None
description = None
for line in event_list:
if line.startswith('DTSTAMP:'):
timestamp = line.split(':', 1)[1]
elif line.startswith('DTSTART:'):
start_time = line.split(':', 1)[1]
elif line.startswith('DTEND:'):
end_time = line.split(':', 1)[1]
elif line.startswith('SUMMARY:') or line.startswith('DESCRIPTION:'):
description = line.split(':', 1)[1]
elif line.startswith('LOCATION:'):
location = line.split(':', 1)[1]
if not timestamp or \
not start_time or \
not end_time or \
not description:
return False
if len(timestamp) < 15:
return False
if len(start_time) < 15:
return False
if len(end_time) < 15:
return False
# check that the description is valid
if is_filtered(base_dir, nickname, domain, description):
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():
return False
if int(timestamp_year) < 2020 or int(timestamp_year) > 2100:
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():
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()
# create the status number from DTSTAMP
status_number, published = get_status_number(published)
# get the post id
actor = http_prefix + "://" + domain + "/users/" + nickname
actor2 = http_prefix + "://" + domain + "/@" + nickname
post_id = actor + "/statuses/" + status_number
next_str = post_id + "/replies?only_other_accounts=true&page=true"
content = \
'<p><span class=\"h-card\"><a href=\"' + actor2 + \
'\" class=\"u-url mention\">@<span>' + nickname + \
'</span></a></span>' + remove_html(description) + '</p>'
event_json = {
"@context": post_context,
"id": post_id + "/activity",
"type": "Create",
"actor": actor,
"published": published,
"to": [actor],
"cc": [],
"object": {
"id": post_id,
"conversation": post_id,
"type": "Note",
"summary": None,
"inReplyTo": None,
"published": published,
"url": actor + "/" + status_number,
"attributedTo": actor,
"to": [actor],
"cc": [],
"sensitive": False,
"atomUri": post_id,
"inReplyToAtomUri": None,
"commentsEnabled": False,
"rejectReplies": True,
"mediaType": "text/html",
"content": content,
"contentMap": {
system_language: content
},
"attachment": [],
"tag": [
{
"href": actor2,
"name": "@" + nickname + "@" + domain,
"type": "Mention"
},
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Event",
"name": content,
"startTime": start_time,
"endTime": end_time
}
],
"replies": {
"id": post_id + "/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": next_str,
"partOf": post_id + "/replies",
"items": []
}
}
}
}
if location:
event_json['object']['tag'].append({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Place",
"name": location
})
handle = nickname + '@' + domain
outbox_dir = base_dir + '/accounts/' + handle + '/outbox'
if not os.path.isdir(outbox_dir):
return False
filename = outbox_dir + '/' + post_id.replace('/', '#') + '.json'
save_json(event_json, filename)
save_event_post(base_dir, handle, post_id, event_json)
return True
def dav_put_response(base_dir: str, nickname: str, domain: str,
depth: int, xml_str: str, http_prefix: str,
system_language: str) -> str:
"""Returns the response to caldav PUT
"""
if '\n' not in xml_str:
return None
if 'BEGIN:VCALENDAR' not in xml_str or \
'END:VCALENDAR' not in xml_str:
return None
if 'BEGIN:VEVENT' not in xml_str or \
'END:VEVENT' not in xml_str:
return None
stored_count = 0
reading_event = False
lines_list = xml_str.split('\n')
event_list = []
for line in lines_list:
line = line.strip()
if not reading_event:
if line == 'BEGIN:VEVENT':
reading_event = True
event_list = []
else:
if line == 'END:VEVENT':
if event_list:
_dav_store_event(base_dir, nickname, domain,
event_list, http_prefix,
system_language)
stored_count += 1
reading_event = False
else:
event_list.append(line)
if stored_count == 0:
return None
return 'Ok'
def dav_report_response(base_dir: str, nickname: str, domain: str,
depth: int, xml_str: str,
person_cache: {}, http_prefix: str,
curr_etag: str,
domain_full: str) -> str:
"""Returns the response to caldav REPORT
"""
if '<c:calendar-query' not in xml_str or \
'</c:calendar-query>' not in xml_str:
if '<c:calendar-multiget' not in xml_str or \
'</c:calendar-multiget>' not in xml_str:
return None
# 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:
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 = \
'<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>'
return response_str
def dav_delete_response(base_dir: str, nickname: str, domain: str,
depth: int, path: str,
http_prefix: str, debug: bool,
recent_posts_cache: {}) -> str:
"""Returns the response to caldav DELETE
"""
token = path.split('/calendars/' + nickname + '/')[1]
token_year, token_month_number, token_post_id = \
_dav_decode_token(token)
if not token_year:
return None
post_filename = locate_post(base_dir, nickname, domain, token_post_id)
if not post_filename:
print('Calendar post not found ' + token_post_id)
return None
post_json_object = load_json(post_filename)
if not _is_happening_post(post_json_object):
print(token_post_id + ' is not a calendar post')
return None
remove_calendar_event(base_dir, nickname, domain,
token_year, token_month_number,
token_post_id)
delete_post(base_dir, http_prefix,
nickname, domain, post_filename,
debug, recent_posts_cache)
return 'Ok'

View File

@ -4956,6 +4956,10 @@ def _test_functions():
'do_GET', 'do_GET',
'do_POST', 'do_POST',
'do_HEAD', 'do_HEAD',
'do_PROPFIND',
'do_PUT',
'do_REPORT',
'do_DELETE',
'__run', '__run',
'_send_to_named_addresses', '_send_to_named_addresses',
'globaltrace', 'globaltrace',

View File

@ -506,5 +506,9 @@
"Write your news report": "اكتب تقريرك الإخباري", "Write your news report": "اكتب تقريرك الإخباري",
"Dyslexic font": "الخط المعسر القراءة", "Dyslexic font": "الخط المعسر القراءة",
"Leave a comment": "اترك تعليقا", "Leave a comment": "اترك تعليقا",
"View comments": "تعليقات عرض" "View comments": "تعليقات عرض",
"Multi Status": "متعدد الحالات",
"Lots of things": "أشياء كثيرة",
"Created": "مخلوق",
"It is done": "تم"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Escriu la teva notícia", "Write your news report": "Escriu la teva notícia",
"Dyslexic font": "Tipus de lletra dislèxic", "Dyslexic font": "Tipus de lletra dislèxic",
"Leave a comment": "Deixa un comentari", "Leave a comment": "Deixa un comentari",
"View comments": "Veure comentaris" "View comments": "Veure comentaris",
"Multi Status": "Estat múltiple",
"Lots of things": "Moltes coses",
"Created": "Creat",
"It is done": "Esta fet"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Ysgrifennwch eich adroddiad newyddion", "Write your news report": "Ysgrifennwch eich adroddiad newyddion",
"Dyslexic font": "Ffont dyslecsig", "Dyslexic font": "Ffont dyslecsig",
"Leave a comment": "Gadael sylw", "Leave a comment": "Gadael sylw",
"View comments": "Gweld sylwadau" "View comments": "Gweld sylwadau",
"Multi Status": "Statws Aml",
"Lots of things": "Llawer o pethau",
"Created": "Wedi creu",
"It is done": "Mae'n cael ei wneud"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Schreiben Sie Ihren Nachrichtenbericht", "Write your news report": "Schreiben Sie Ihren Nachrichtenbericht",
"Dyslexic font": "Schriftart für Legastheniker", "Dyslexic font": "Schriftart für Legastheniker",
"Leave a comment": "Hinterlasse einen Kommentar", "Leave a comment": "Hinterlasse einen Kommentar",
"View comments": "Kommentare ansehen" "View comments": "Kommentare ansehen",
"Multi Status": "Multi-Status",
"Lots of things": "Viele Dinge",
"Created": "Erstellt",
"It is done": "Es ist vollbracht"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Write your news report", "Write your news report": "Write your news report",
"Dyslexic font": "Dyslexic font", "Dyslexic font": "Dyslexic font",
"Leave a comment": "Leave a comment", "Leave a comment": "Leave a comment",
"View comments": "View comments" "View comments": "View comments",
"Multi Status": "Multi Status",
"Lots of things": "Lots of things",
"Created": "Created",
"It is done": "It is done"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Escribe tu informe de noticias", "Write your news report": "Escribe tu informe de noticias",
"Dyslexic font": "Fuente disléxica", "Dyslexic font": "Fuente disléxica",
"Leave a comment": "Deja un comentario", "Leave a comment": "Deja un comentario",
"View comments": "Ver comentarios" "View comments": "Ver comentarios",
"Multi Status": "Estado múltiple",
"Lots of things": "Muchas cosas",
"Created": "Creada",
"It is done": "Se hace"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Rédigez votre reportage", "Write your news report": "Rédigez votre reportage",
"Dyslexic font": "Police dyslexique", "Dyslexic font": "Police dyslexique",
"Leave a comment": "Laissez un commentaire", "Leave a comment": "Laissez un commentaire",
"View comments": "Voir les commentaires" "View comments": "Voir les commentaires",
"Multi Status": "Statut multiple",
"Lots of things": "Beaucoup de choses",
"Created": "Créé",
"It is done": "C'est fait"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Scríobh do thuairisc nuachta", "Write your news report": "Scríobh do thuairisc nuachta",
"Dyslexic font": "Cló disléicseach", "Dyslexic font": "Cló disléicseach",
"Leave a comment": "Fág trácht", "Leave a comment": "Fág trácht",
"View comments": "Féach ar thuairimí" "View comments": "Féach ar thuairimí",
"Multi Status": "Stádas Il",
"Lots of things": "A lán rudaí",
"Created": "Cruthaithe",
"It is done": "Déantar é"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "अपनी समाचार रिपोर्ट लिखें", "Write your news report": "अपनी समाचार रिपोर्ट लिखें",
"Dyslexic font": "डिस्लेक्सिक फ़ॉन्ट", "Dyslexic font": "डिस्लेक्सिक फ़ॉन्ट",
"Leave a comment": "एक टिप्पणी छोड़ें", "Leave a comment": "एक टिप्पणी छोड़ें",
"View comments": "टिप्पणियाँ देखें" "View comments": "टिप्पणियाँ देखें",
"Multi Status": "बहु स्थिति",
"Lots of things": "बहुत सी बातें",
"Created": "बनाया था",
"It is done": "हो गया है"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Scrivi il tuo reportage", "Write your news report": "Scrivi il tuo reportage",
"Dyslexic font": "Carattere dislessico", "Dyslexic font": "Carattere dislessico",
"Leave a comment": "Lascia un commento", "Leave a comment": "Lascia un commento",
"View comments": "Visualizza commenti" "View comments": "Visualizza commenti",
"Multi Status": "Stato multiplo",
"Lots of things": "Un sacco di cose",
"Created": "Creata",
"It is done": "È fatta"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "ニュースレポートを書く", "Write your news report": "ニュースレポートを書く",
"Dyslexic font": "失読症フォント", "Dyslexic font": "失読症フォント",
"Leave a comment": "コメントを残す", "Leave a comment": "コメントを残す",
"View comments": "コメントを見る" "View comments": "コメントを見る",
"Multi Status": "マルチステータス",
"Lots of things": "多くの物",
"Created": "作成した",
"It is done": "されております"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Rapora xwe ya nûçeyan binivîsin", "Write your news report": "Rapora xwe ya nûçeyan binivîsin",
"Dyslexic font": "Font Dyslexic", "Dyslexic font": "Font Dyslexic",
"Leave a comment": "Bihêle şîroveyek", "Leave a comment": "Bihêle şîroveyek",
"View comments": "Binêre şîroveyan" "View comments": "Binêre şîroveyan",
"Multi Status": "Multi Status",
"Lots of things": "Gelek tişt",
"Created": "Afirandin",
"It is done": "Tê kirin"
} }

View File

@ -502,5 +502,9 @@
"Write your news report": "Write your news report", "Write your news report": "Write your news report",
"Dyslexic font": "Dyslexic font", "Dyslexic font": "Dyslexic font",
"Leave a comment": "Leave a comment", "Leave a comment": "Leave a comment",
"View comments": "View comments" "View comments": "View comments",
"Multi Status": "Multi Status",
"Lots of things": "Lots of things",
"Created": "Created",
"It is done": "It is done"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Escreva sua reportagem", "Write your news report": "Escreva sua reportagem",
"Dyslexic font": "Fonte disléxica", "Dyslexic font": "Fonte disléxica",
"Leave a comment": "Deixe um comentário", "Leave a comment": "Deixe um comentário",
"View comments": "Ver comentários" "View comments": "Ver comentários",
"Multi Status": "Vários status",
"Lots of things": "Muitas coisas",
"Created": "Criada",
"It is done": "Está feito"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Напишите свой новостной репортаж", "Write your news report": "Напишите свой новостной репортаж",
"Dyslexic font": "Дислексический шрифт", "Dyslexic font": "Дислексический шрифт",
"Leave a comment": "Оставить комментарий", "Leave a comment": "Оставить комментарий",
"View comments": "Посмотреть комментарии" "View comments": "Посмотреть комментарии",
"Multi Status": "Мульти статус",
"Lots of things": "Много всего",
"Created": "Созданный",
"It is done": "Сделано"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "Andika ripoti yako ya habari", "Write your news report": "Andika ripoti yako ya habari",
"Dyslexic font": "Fonti ya Dyslexic", "Dyslexic font": "Fonti ya Dyslexic",
"Leave a comment": "Acha maoni", "Leave a comment": "Acha maoni",
"View comments": "Tazama maoni" "View comments": "Tazama maoni",
"Multi Status": "Hali nyingi",
"Lots of things": "Mambo mengi",
"Created": "Imeundwa",
"It is done": "Imefanyika"
} }

View File

@ -506,5 +506,9 @@
"Write your news report": "写你的新闻报道", "Write your news report": "写你的新闻报道",
"Dyslexic font": "阅读障碍字体", "Dyslexic font": "阅读障碍字体",
"Leave a comment": "发表评论", "Leave a comment": "发表评论",
"View comments": "查看评论" "View comments": "查看评论",
"Multi Status": "多状态",
"Lots of things": "很多事情",
"Created": "已创建",
"It is done": "完成了"
} }

View File

@ -121,8 +121,13 @@ def _add_embedded_video_from_sites(translate: {}, content: str,
url = content.split('"' + video_site)[1] url = content.split('"' + video_site)[1]
if '"' in url: if '"' in url:
url = url.split('"')[0] url = url.split('"')[0]
video_site_settings = ''
if '#' in url:
video_site_settings = '#' + url.split('#', 1)[1]
url = url.split('#')[0]
if not url.endswith('/oembed'): if not url.endswith('/oembed'):
url = url + '/oembed' url = url + '/oembed'
url += video_site_settings
content += \ content += \
"<center>\n<iframe loading=\"lazy\" src=\"" + \ "<center>\n<iframe loading=\"lazy\" src=\"" + \
video_site + url + "\" width=\"" + \ video_site + url + "\" width=\"" + \