epicyon/webapp_utils.py

1601 lines
63 KiB
Python
Raw Normal View History

2020-11-09 15:22:59 +00:00
__filename__ = "webapp_utils.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-11-09 15:22:59 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-11-09 15:22:59 +00:00
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "Web Interface"
2020-11-09 15:22:59 +00:00
import os
2021-10-30 11:08:57 +00:00
from shutil import copyfile
2020-11-09 15:22:59 +00:00
from collections import OrderedDict
2021-12-29 21:55:09 +00:00
from session import get_json
2021-12-26 18:46:43 +00:00
from utils import is_account_dir
2021-12-27 15:43:22 +00:00
from utils import remove_html
2021-12-27 17:20:01 +00:00
from utils import get_protocol_prefixes
2021-12-26 15:13:34 +00:00
from utils import load_json
2021-12-26 23:41:34 +00:00
from utils import get_cached_post_filename
2021-12-26 14:08:58 +00:00
from utils import get_config_param
2021-12-26 12:02:29 +00:00
from utils import acct_dir
2021-12-27 22:19:18 +00:00
from utils import get_nickname_from_actor
2021-12-26 18:03:39 +00:00
from utils import is_float
2021-12-26 14:24:03 +00:00
from utils import get_audio_extensions
2021-12-26 14:20:09 +00:00
from utils import get_video_extensions
2021-12-26 14:26:16 +00:00
from utils import get_image_extensions
2021-12-26 10:19:59 +00:00
from utils import local_actor_url
2021-12-29 21:55:09 +00:00
from cache import store_person_in_cache
from content import add_html_tags
from content import replace_emoji_from_tags
from person import get_person_avatar_url
2021-12-28 19:33:29 +00:00
from posts import is_moderator
2021-12-29 21:55:09 +00:00
from blocking import is_blocked
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def get_broken_link_substitute() -> str:
"""Returns html used to show a default image if the link to
an image is broken
"""
return " onerror=\"this.onerror=null; this.src='" + \
"/icons/avatar_default.png'\""
2021-12-29 21:55:09 +00:00
def html_following_list(css_cache: {}, base_dir: str,
followingFilename: str) -> str:
2020-11-28 10:49:10 +00:00
"""Returns a list of handles being followed
"""
with open(followingFilename, 'r') as followingFile:
msg = followingFile.read()
followingList = msg.split('\n')
followingList.sort()
if followingList:
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-profile.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/epicyon.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon.css'
2020-11-28 10:49:10 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
followingListHtml = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename,
2021-12-29 21:55:09 +00:00
instanceTitle, None)
2020-11-28 10:49:10 +00:00
for followingAddress in followingList:
if followingAddress:
followingListHtml += \
'<h3>@' + followingAddress + '</h3>'
2021-12-29 21:55:09 +00:00
followingListHtml += html_footer()
2020-11-28 10:49:10 +00:00
msg = followingListHtml
return msg
return ''
2021-12-29 21:55:09 +00:00
def html_hashtag_blocked(css_cache: {}, base_dir: str, translate: {}) -> str:
2020-11-28 10:49:10 +00:00
"""Show the screen for a blocked hashtag
"""
blockedHashtagForm = ''
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-suspended.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/suspended.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/suspended.css'
2020-11-28 10:49:10 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
blockedHashtagForm = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename, instanceTitle, None)
2020-11-28 10:49:10 +00:00
blockedHashtagForm += '<div><center>\n'
blockedHashtagForm += \
' <p class="screentitle">' + \
translate['Hashtag Blocked'] + '</p>\n'
blockedHashtagForm += \
' <p>See <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>\n'
blockedHashtagForm += '</center></div>\n'
2021-12-29 21:55:09 +00:00
blockedHashtagForm += html_footer()
2020-11-28 10:49:10 +00:00
return blockedHashtagForm
2021-12-29 21:55:09 +00:00
def header_buttons_front_screen(translate: {},
nickname: str, boxName: str,
authorized: bool,
icons_as_buttons: bool) -> str:
2020-11-28 10:19:59 +00:00
"""Returns the header buttons for the front page of a news instance
"""
headerStr = ''
if nickname == 'news':
buttonFeatures = 'buttonMobile'
buttonNewswire = 'buttonMobile'
buttonLinks = 'buttonMobile'
if boxName == 'features':
buttonFeatures = 'buttonselected'
elif boxName == 'newswire':
buttonNewswire = 'buttonselected'
elif boxName == 'links':
buttonLinks = 'buttonselected'
headerStr += \
' <a href="/">' + \
'<button class="' + buttonFeatures + '">' + \
'<span>' + translate['Features'] + \
'</span></button></a>'
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
2021-12-25 19:19:14 +00:00
if icons_as_buttons:
2020-11-28 10:19:59 +00:00
headerStr += \
' <a href="/users/news/newswiremobile">' + \
'<button class="' + buttonNewswire + '">' + \
'<span>' + translate['Newswire'] + \
'</span></button></a>'
headerStr += \
' <a href="/users/news/linksmobile">' + \
'<button class="' + buttonLinks + '">' + \
'<span>' + translate['Links'] + \
'</span></button></a>'
else:
headerStr += \
' <a href="' + \
'/users/news/newswiremobile">' + \
2020-12-09 13:08:26 +00:00
'<img loading="lazy" src="/icons' + \
2020-11-28 10:19:59 +00:00
'/newswire.png" title="' + translate['Newswire'] + \
'" alt="| ' + translate['Newswire'] + '"/></a>\n'
headerStr += \
' <a href="' + \
'/users/news/linksmobile">' + \
2020-12-09 13:08:26 +00:00
'<img loading="lazy" src="/icons' + \
2020-11-28 10:19:59 +00:00
'/links.png" title="' + translate['Links'] + \
'" alt="| ' + translate['Links'] + '"/></a>\n'
else:
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
if headerStr:
headerStr = \
'\n <div class="frontPageMobileButtons">\n' + \
headerStr + \
' </div>\n'
return headerStr
2021-12-29 21:55:09 +00:00
def get_content_warning_button(postID: str, translate: {},
content: str) -> str:
2020-11-09 15:22:59 +00:00
"""Returns the markup for a content warning button
"""
2021-01-19 19:27:32 +00:00
return ' <details><summary class="cw">' + \
2021-01-19 19:24:16 +00:00
translate['SHOW MORE'] + '</summary>' + \
2020-11-09 15:22:59 +00:00
'<div id="' + postID + '">' + content + \
'</div></details>\n'
2021-12-29 21:55:09 +00:00
def _set_actor_property_url(actor_json: {},
property_name: str, url: str) -> None:
2020-11-09 15:22:59 +00:00
"""Sets a url for the given actor property
"""
2021-12-26 10:29:52 +00:00
if not actor_json.get('attachment'):
actor_json['attachment'] = []
2020-11-09 15:22:59 +00:00
2021-12-26 18:19:58 +00:00
property_nameLower = property_name.lower()
2020-11-09 15:22:59 +00:00
# remove any existing value
propertyFound = None
2021-12-26 10:32:45 +00:00
for property_value in actor_json['attachment']:
if not property_value.get('name'):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
if not property_value.get('type'):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 18:19:58 +00:00
if not property_value['name'].lower().startswith(property_nameLower):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
propertyFound = property_value
2020-11-09 15:22:59 +00:00
break
if propertyFound:
2021-12-26 10:29:52 +00:00
actor_json['attachment'].remove(propertyFound)
2020-11-09 15:22:59 +00:00
2021-12-27 17:20:01 +00:00
prefixes = get_protocol_prefixes()
2020-11-09 15:22:59 +00:00
prefixFound = False
for prefix in prefixes:
if url.startswith(prefix):
prefixFound = True
break
if not prefixFound:
return
if '.' not in url:
return
if ' ' in url:
return
if ',' in url:
return
2021-12-26 10:32:45 +00:00
for property_value in actor_json['attachment']:
if not property_value.get('name'):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
if not property_value.get('type'):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 18:19:58 +00:00
if not property_value['name'].lower().startswith(property_nameLower):
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
if property_value['type'] != 'PropertyValue':
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
property_value['value'] = url
2020-11-09 15:22:59 +00:00
return
newAddress = {
2021-12-26 18:19:58 +00:00
"name": property_name,
2020-11-09 15:22:59 +00:00
"type": "PropertyValue",
"value": url
}
2021-12-26 10:29:52 +00:00
actor_json['attachment'].append(newAddress)
2020-11-09 15:22:59 +00:00
def set_blog_address(actor_json: {}, blog_address: str) -> None:
2020-11-09 15:22:59 +00:00
"""Sets an blog address for the given actor
"""
_set_actor_property_url(actor_json, 'Blog', remove_html(blog_address))
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def update_avatar_image_cache(signing_priv_key_pem: str,
session, base_dir: str, http_prefix: str,
actor: str, avatarUrl: str,
person_cache: {}, allowDownloads: bool,
force: bool = False, debug: bool = False) -> str:
2020-11-09 15:22:59 +00:00
"""Updates the cached avatar for the given actor
"""
if not avatarUrl:
return None
actorStr = actor.replace('/', '-')
2021-12-25 16:17:53 +00:00
avatarImagePath = base_dir + '/cache/avatars/' + actorStr
2020-12-12 14:23:14 +00:00
# try different image types
imageFormats = {
'png': 'png',
'jpg': 'jpeg',
'jpeg': 'jpeg',
'gif': 'gif',
2021-01-11 22:27:57 +00:00
'svg': 'svg+xml',
2020-12-12 14:23:14 +00:00
'webp': 'webp',
'avif': 'avif'
}
avatarImageFilename = None
for imFormat, mimeType in imageFormats.items():
if avatarUrl.endswith('.' + imFormat) or \
'.' + imFormat + '?' in avatarUrl:
sessionHeaders = {
'Accept': 'image/' + mimeType
}
avatarImageFilename = avatarImagePath + '.' + imFormat
if not avatarImageFilename:
2020-11-09 15:22:59 +00:00
return None
if (not os.path.isfile(avatarImageFilename) or force) and allowDownloads:
try:
2021-03-14 21:29:40 +00:00
if debug:
print('avatar image url: ' + avatarUrl)
2020-11-09 15:22:59 +00:00
result = session.get(avatarUrl,
headers=sessionHeaders,
params=None)
if result.status_code < 200 or \
result.status_code > 202:
2021-03-14 21:29:40 +00:00
if debug:
print('Avatar image download failed with status ' +
str(result.status_code))
2020-11-09 15:22:59 +00:00
# remove partial download
if os.path.isfile(avatarImageFilename):
try:
os.remove(avatarImageFilename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-12-29 21:55:09 +00:00
print('EX: ' +
'update_avatar_image_cache unable to delete ' +
2021-10-29 18:48:15 +00:00
avatarImageFilename)
2020-11-09 15:22:59 +00:00
else:
with open(avatarImageFilename, 'wb') as f:
f.write(result.content)
2021-03-14 21:29:40 +00:00
if debug:
print('avatar image downloaded for ' + actor)
2021-12-25 16:17:53 +00:00
return avatarImageFilename.replace(base_dir + '/cache', '')
2021-12-25 15:28:52 +00:00
except Exception as ex:
2021-11-01 17:12:17 +00:00
print('EX: Failed to download avatar image: ' +
2021-12-25 15:28:52 +00:00
str(avatarUrl) + ' ' + str(ex))
2020-11-09 15:22:59 +00:00
prof = 'https://www.w3.org/ns/activitystreams'
if '/channel/' not in actor or '/accounts/' not in actor:
sessionHeaders = {
'Accept': 'application/activity+json; profile="' + prof + '"'
}
else:
sessionHeaders = {
'Accept': 'application/ld+json; profile="' + prof + '"'
}
personJson = \
2021-12-29 21:55:09 +00:00
get_json(signing_priv_key_pem, session, actor,
sessionHeaders, None,
debug, __version__, http_prefix, None)
2020-11-09 15:22:59 +00:00
if personJson:
if not personJson.get('id'):
return None
if not personJson.get('publicKey'):
return None
if not personJson['publicKey'].get('publicKeyPem'):
return None
if personJson['id'] != actor:
return None
2021-12-25 22:17:49 +00:00
if not person_cache.get(actor):
2020-11-09 15:22:59 +00:00
return None
2021-12-25 22:17:49 +00:00
if person_cache[actor]['actor']['publicKey']['publicKeyPem'] != \
2020-11-09 15:22:59 +00:00
personJson['publicKey']['publicKeyPem']:
print("ERROR: " +
"public keys don't match when downloading actor for " +
actor)
return None
2021-12-29 21:55:09 +00:00
store_person_in_cache(base_dir, actor, personJson, person_cache,
allowDownloads)
return get_person_avatar_url(base_dir, actor, person_cache,
allowDownloads)
2020-11-09 15:22:59 +00:00
return None
2021-12-25 16:17:53 +00:00
return avatarImageFilename.replace(base_dir + '/cache', '')
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def scheduled_posts_exist(base_dir: str, nickname: str, domain: str) -> bool:
2020-11-09 15:22:59 +00:00
"""Returns true if there are posts scheduled to be delivered
"""
scheduleIndexFilename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, nickname, domain) + '/schedule.index'
2020-11-09 15:22:59 +00:00
if not os.path.isfile(scheduleIndexFilename):
return False
if '#users#' in open(scheduleIndexFilename).read():
return True
return False
2021-12-29 21:55:09 +00:00
def shares_timeline_json(actor: str, pageNumber: int, itemsPerPage: int,
base_dir: str, domain: str, nickname: str,
maxSharesPerAccount: int,
shared_items_federated_domains: [],
sharesFileType: str) -> ({}, bool):
2020-11-09 15:22:59 +00:00
"""Get a page on the shared items timeline as json
maxSharesPerAccount helps to avoid one person dominating the timeline
by sharing a large number of things
"""
allSharesJson = {}
2021-12-25 16:17:53 +00:00
for subdir, dirs, files in os.walk(base_dir + '/accounts'):
2020-11-09 15:22:59 +00:00
for handle in dirs:
2021-12-26 18:46:43 +00:00
if not is_account_dir(handle):
2020-12-06 14:42:42 +00:00
continue
2021-12-25 16:17:53 +00:00
accountDir = base_dir + '/accounts/' + handle
sharesFilename = accountDir + '/' + sharesFileType + '.json'
2020-12-06 14:42:42 +00:00
if not os.path.isfile(sharesFilename):
continue
2021-12-26 15:13:34 +00:00
sharesJson = load_json(sharesFilename)
2020-12-06 14:42:42 +00:00
if not sharesJson:
continue
accountNickname = handle.split('@')[0]
# Don't include shared items from blocked accounts
if accountNickname != nickname:
2021-12-29 21:55:09 +00:00
if is_blocked(base_dir, nickname, domain,
accountNickname, domain, None):
continue
2020-12-06 14:42:42 +00:00
# actor who owns this share
owner = actor.split('/users/')[0] + '/users/' + accountNickname
2020-12-06 14:42:42 +00:00
ctr = 0
for itemID, item in sharesJson.items():
# assign owner to the item
item['actor'] = owner
item['shareId'] = itemID
2020-12-06 14:42:42 +00:00
allSharesJson[str(item['published'])] = item
ctr += 1
if ctr >= maxSharesPerAccount:
break
2020-12-13 22:13:45 +00:00
break
2021-12-25 18:05:01 +00:00
if shared_items_federated_domains:
if sharesFileType == 'shares':
2021-12-25 16:17:53 +00:00
catalogsDir = base_dir + '/cache/catalogs'
else:
2021-12-25 16:17:53 +00:00
catalogsDir = base_dir + '/cache/wantedItems'
if os.path.isdir(catalogsDir):
for subdir, dirs, files in os.walk(catalogsDir):
for f in files:
if '#' in f:
continue
if not f.endswith('.' + sharesFileType + '.json'):
continue
federatedDomain = f.split('.')[0]
2021-12-25 18:05:01 +00:00
if federatedDomain not in shared_items_federated_domains:
continue
sharesFilename = catalogsDir + '/' + f
2021-12-26 15:13:34 +00:00
sharesJson = load_json(sharesFilename)
if not sharesJson:
continue
ctr = 0
for itemID, item in sharesJson.items():
# assign owner to the item
if '--shareditems--' not in itemID:
continue
shareActor = itemID.split('--shareditems--')[0]
shareActor = shareActor.replace('___', '://')
shareActor = shareActor.replace('--', '/')
2021-12-27 22:19:18 +00:00
shareNickname = get_nickname_from_actor(shareActor)
2021-12-29 21:55:09 +00:00
if is_blocked(base_dir, nickname, domain,
shareNickname, federatedDomain, None):
continue
2021-07-27 19:17:15 +00:00
item['actor'] = shareActor
item['shareId'] = itemID
allSharesJson[str(item['published'])] = item
ctr += 1
if ctr >= maxSharesPerAccount:
break
break
2020-11-09 15:22:59 +00:00
# sort the shared items in descending order of publication date
sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True))
lastPage = False
startIndex = itemsPerPage * pageNumber
maxIndex = len(sharesJson.items())
if maxIndex < itemsPerPage:
lastPage = True
if startIndex >= maxIndex - itemsPerPage:
lastPage = True
startIndex = maxIndex - itemsPerPage
if startIndex < 0:
startIndex = 0
ctr = 0
resultJson = {}
for published, item in sharesJson.items():
if ctr >= startIndex + itemsPerPage:
break
if ctr < startIndex:
ctr += 1
continue
resultJson[published] = item
ctr += 1
return resultJson, lastPage
2021-12-29 21:55:09 +00:00
def post_contains_public(post_json_object: {}) -> bool:
2020-11-09 15:22:59 +00:00
"""Does the given post contain #Public
"""
containsPublic = False
2021-12-25 22:09:19 +00:00
if not post_json_object['object'].get('to'):
2020-11-09 15:22:59 +00:00
return containsPublic
2021-12-25 22:09:19 +00:00
for toAddress in post_json_object['object']['to']:
2020-11-09 15:22:59 +00:00
if toAddress.endswith('#Public'):
containsPublic = True
break
if not containsPublic:
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('cc'):
for toAddress in post_json_object['object']['cc']:
2020-11-09 15:22:59 +00:00
if toAddress.endswith('#Public'):
containsPublic = True
break
return containsPublic
2021-12-29 21:55:09 +00:00
def _get_image_file(base_dir: str, name: str, directory: str,
nickname: str, domain: str, theme: str) -> (str, str):
2020-11-09 15:22:59 +00:00
"""
2020-11-09 15:40:24 +00:00
returns the filenames for an image with the given name
"""
2021-12-26 14:26:16 +00:00
bannerExtensions = get_image_extensions()
2021-12-31 21:18:12 +00:00
banner_file = ''
banner_filename = ''
2021-08-21 12:33:49 +00:00
for ext in bannerExtensions:
2021-12-31 21:18:12 +00:00
banner_fileTest = name + '.' + ext
banner_filenameTest = directory + '/' + banner_fileTest
if os.path.isfile(banner_filenameTest):
banner_file = name + '_' + theme + '.' + ext
banner_filename = banner_filenameTest
return banner_file, banner_filename
2021-08-21 13:03:28 +00:00
# if not found then use the default image
theme = 'default'
2021-12-25 16:17:53 +00:00
directory = base_dir + '/theme/' + theme
2021-08-21 13:03:28 +00:00
for ext in bannerExtensions:
2021-12-31 21:18:12 +00:00
banner_fileTest = name + '.' + ext
banner_filenameTest = directory + '/' + banner_fileTest
if os.path.isfile(banner_filenameTest):
banner_file = name + '_' + theme + '.' + ext
banner_filename = banner_filenameTest
2021-08-21 13:03:28 +00:00
break
2021-12-31 21:18:12 +00:00
return banner_file, banner_filename
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_banner_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2021-12-26 12:02:29 +00:00
accountDir = acct_dir(base_dir, nickname, domain)
2021-12-29 21:55:09 +00:00
return _get_image_file(base_dir, 'banner', accountDir,
nickname, domain, theme)
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_search_banner_file(base_dir: str,
nickname: str, domain: str,
theme: str) -> (str, str):
2021-12-26 12:02:29 +00:00
accountDir = acct_dir(base_dir, nickname, domain)
2021-12-29 21:55:09 +00:00
return _get_image_file(base_dir, 'search_banner', accountDir,
nickname, domain, theme)
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_left_image_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2021-12-26 12:02:29 +00:00
accountDir = acct_dir(base_dir, nickname, domain)
2021-12-29 21:55:09 +00:00
return _get_image_file(base_dir, 'left_col_image', accountDir,
nickname, domain, theme)
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_right_image_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2021-12-26 12:02:29 +00:00
accountDir = acct_dir(base_dir, nickname, domain)
2021-12-29 21:55:09 +00:00
return _get_image_file(base_dir, 'right_col_image',
accountDir, nickname, domain, theme)
2020-11-09 19:41:01 +00:00
2021-12-31 21:18:12 +00:00
def html_header_with_external_style(css_filename: str, instanceTitle: str,
2021-12-29 21:55:09 +00:00
metadata: str, lang='en') -> str:
if metadata is None:
metadata = ''
2021-12-31 21:18:12 +00:00
cssFile = '/' + css_filename.split('/')[-1]
2021-07-06 12:50:38 +00:00
htmlStr = \
'<!DOCTYPE html>\n' + \
'<html lang="' + lang + '">\n' + \
' <head>\n' + \
' <meta charset="utf-8">\n' + \
2021-11-08 10:06:32 +00:00
' <link rel="stylesheet" media="all" ' + \
'href="' + cssFile + '">\n' + \
2021-07-06 12:50:38 +00:00
' <link rel="manifest" href="/manifest.json">\n' + \
2021-11-08 10:06:32 +00:00
' <link href="/favicon.ico" rel="icon" type="image/x-icon">\n' + \
' <meta content="/browserconfig.xml" ' + \
'name="msapplication-config">\n' + \
' <meta content="yes" name="apple-mobile-web-app-capable">\n' + \
' <link href="/apple-touch-icon.png" rel="apple-touch-icon" ' + \
'sizes="180x180">\n' + \
2021-07-06 12:50:38 +00:00
' <meta name="theme-color" content="grey">\n' + \
metadata + \
2021-07-06 12:50:38 +00:00
' <title>' + instanceTitle + '</title>\n' + \
' </head>\n' + \
' <body>\n'
2020-11-09 19:41:01 +00:00
return htmlStr
2021-12-31 21:18:12 +00:00
def html_header_with_person_markup(css_filename: str, instanceTitle: str,
2021-12-29 21:55:09 +00:00
actor_json: {}, city: str,
content_license_url: str,
lang='en') -> str:
"""html header which includes person markup
https://schema.org/Person
"""
2021-12-26 10:29:52 +00:00
if not actor_json:
2021-11-07 11:32:08 +00:00
htmlStr = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename,
2021-12-29 21:55:09 +00:00
instanceTitle, None, lang)
return htmlStr
2021-05-17 10:46:31 +00:00
cityMarkup = ''
if city:
city = city.lower().title()
addComma = ''
countryMarkup = ''
if ',' in city:
country = city.split(',', 1)[1].strip().title()
city = city.split(',', 1)[0]
countryMarkup = \
2021-11-07 12:35:52 +00:00
' "addressCountry": "' + country + '"\n'
2021-05-17 10:46:31 +00:00
addComma = ','
cityMarkup = \
2021-11-07 12:31:47 +00:00
' "address": {\n' + \
' "@type": "PostalAddress",\n' + \
' "addressLocality": "' + city + '"' + addComma + '\n' + \
2021-05-17 10:46:31 +00:00
countryMarkup + \
2021-11-07 12:31:47 +00:00
' },\n'
2021-05-17 10:46:31 +00:00
skillsMarkup = ''
2021-12-26 10:29:52 +00:00
if actor_json.get('hasOccupation'):
if isinstance(actor_json['hasOccupation'], list):
2021-11-07 12:31:47 +00:00
skillsMarkup = ' "hasOccupation": [\n'
2021-05-16 16:07:02 +00:00
firstEntry = True
2021-12-26 10:29:52 +00:00
for skillDict in actor_json['hasOccupation']:
2021-05-16 16:07:02 +00:00
if skillDict['@type'] == 'Role':
if not firstEntry:
skillsMarkup += ',\n'
sk = skillDict['hasOccupation']
roleName = sk['name']
2021-05-16 16:37:03 +00:00
if not roleName:
roleName = 'member'
2021-05-16 16:07:02 +00:00
category = \
sk['occupationalCategory']['codeValue']
categoryUrl = \
2021-05-16 16:25:16 +00:00
'https://www.onetonline.org/link/summary/' + category
2021-05-16 16:07:02 +00:00
skillsMarkup += \
2021-07-06 12:50:38 +00:00
' {\n' + \
' "@type": "Role",\n' + \
' "hasOccupation": {\n' + \
' "@type": "Occupation",\n' + \
' "name": "' + roleName + '",\n' + \
' "description": ' + \
'"Fediverse instance role",\n' + \
' "occupationLocation": {\n' + \
' "@type": "City",\n' + \
' "name": "' + city + '"\n' + \
' },\n' + \
' "occupationalCategory": {\n' + \
' "@type": "CategoryCode",\n' + \
' "inCodeSet": {\n' + \
' "@type": "CategoryCodeSet",\n' + \
' "name": "O*Net-SOC",\n' + \
' "dateModified": "2019",\n' + \
2021-05-16 16:20:38 +00:00
' ' + \
2021-07-06 12:50:38 +00:00
'"url": "https://www.onetonline.org/"\n' + \
' },\n' + \
' "codeValue": "' + category + '",\n' + \
' "url": "' + categoryUrl + '"\n' + \
' }\n' + \
' }\n' + \
' }'
2021-05-16 16:07:02 +00:00
elif skillDict['@type'] == 'Occupation':
if not firstEntry:
skillsMarkup += ',\n'
2021-05-16 16:09:24 +00:00
ocName = skillDict['name']
2021-05-16 17:08:17 +00:00
if not ocName:
ocName = 'member'
2021-05-16 16:09:24 +00:00
skillsList = skillDict['skills']
2021-05-16 16:07:02 +00:00
skillsListStr = '['
for skillStr in skillsList:
if skillsListStr != '[':
skillsListStr += ', '
skillsListStr += '"' + skillStr + '"'
skillsListStr += ']'
2021-05-16 16:13:46 +00:00
skillsMarkup += \
2021-07-06 12:50:38 +00:00
' {\n' + \
' "@type": "Occupation",\n' + \
' "name": "' + ocName + '",\n' + \
' "description": ' + \
'"Fediverse instance occupation",\n' + \
' "occupationLocation": {\n' + \
' "@type": "City",\n' + \
' "name": "' + city + '"\n' + \
' },\n' + \
' "skills": ' + skillsListStr + '\n' + \
' }'
2021-05-16 16:07:02 +00:00
firstEntry = False
2021-11-07 12:31:47 +00:00
skillsMarkup += '\n ],\n'
2021-05-16 16:07:02 +00:00
2021-12-27 15:43:22 +00:00
description = remove_html(actor_json['summary'])
nameStr = remove_html(actor_json['name'])
2021-12-26 10:29:52 +00:00
domain_full = actor_json['id'].split('://')[1].split('/')[0]
handle = actor_json['preferredUsername'] + '@' + domain_full
2021-11-07 12:27:52 +00:00
personMarkup = \
2021-11-07 12:27:52 +00:00
' "about": {\n' + \
' "@type" : "Person",\n' + \
' "name": "' + nameStr + '",\n' + \
2021-12-26 10:29:52 +00:00
' "image": "' + actor_json['icon']['url'] + '",\n' + \
2021-11-07 12:27:52 +00:00
' "description": "' + description + '",\n' + \
cityMarkup + skillsMarkup + \
2021-12-26 10:29:52 +00:00
' "url": "' + actor_json['id'] + '"\n' + \
2021-11-07 12:27:52 +00:00
' },\n'
profileMarkup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
' {\n' + \
2021-11-07 12:27:52 +00:00
' "@context":"https://schema.org",\n' + \
' "@type": "ProfilePage",\n' + \
' "mainEntityOfPage": {\n' + \
' "@type": "WebPage",\n' + \
2021-12-26 10:29:52 +00:00
" \"@id\": \"" + actor_json['id'] + "\"\n" + \
2021-11-07 12:27:52 +00:00
' },\n' + personMarkup + \
' "accountablePerson": {\n' + \
' "@type": "Person",\n' + \
' "name": "' + nameStr + '"\n' + \
' },\n' + \
' "copyrightHolder": {\n' + \
' "@type": "Person",\n' + \
' "name": "' + nameStr + '"\n' + \
' },\n' + \
2021-05-16 11:16:50 +00:00
' "name": "' + nameStr + '",\n' + \
2021-12-26 10:29:52 +00:00
' "image": "' + actor_json['icon']['url'] + '",\n' + \
2021-05-16 11:16:50 +00:00
' "description": "' + description + '",\n' + \
2021-12-25 17:13:38 +00:00
' "license": "' + content_license_url + '"\n' + \
' }\n' + \
' </script>\n'
2021-11-07 11:32:08 +00:00
2021-12-27 15:43:22 +00:00
description = remove_html(description)
2021-11-07 11:32:08 +00:00
ogMetadata = \
" <meta content=\"profile\" property=\"og:type\" />\n" + \
" <meta content=\"" + description + \
"\" name='description'>\n" + \
2021-12-26 10:29:52 +00:00
" <meta content=\"" + actor_json['url'] + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:url\" />\n" + \
2021-12-26 10:00:46 +00:00
" <meta content=\"" + domain_full + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:site_name\" />\n" + \
2021-11-07 11:32:08 +00:00
" <meta content=\"" + nameStr + " (@" + handle + \
2021-11-07 11:34:20 +00:00
")\" property=\"og:title\" />\n" + \
2021-11-07 11:32:08 +00:00
" <meta content=\"" + description + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:description\" />\n" + \
2021-12-26 10:29:52 +00:00
" <meta content=\"" + actor_json['icon']['url'] + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:image\" />\n" + \
2021-11-07 11:32:08 +00:00
" <meta content=\"400\" property=\"og:image:width\" />\n" + \
2021-11-07 11:34:20 +00:00
" <meta content=\"400\" property=\"og:image:height\" />\n" + \
" <meta content=\"summary\" property=\"twitter:card\" />\n" + \
" <meta content=\"" + handle + \
"\" property=\"profile:username\" />\n"
2021-12-26 10:29:52 +00:00
if actor_json.get('attachment'):
ogTags = (
'email', 'openpgp', 'blog', 'xmpp', 'matrix', 'briar',
'jami', 'cwtch', 'languages'
)
2021-12-26 10:29:52 +00:00
for attachJson in actor_json['attachment']:
if not attachJson.get('name'):
continue
if not attachJson.get('value'):
continue
name = attachJson['name'].lower()
value = attachJson['value']
for ogTag in ogTags:
if name != ogTag:
continue
ogMetadata += \
" <meta content=\"" + value + \
"\" property=\"og:" + ogTag + "\" />\n"
2021-11-07 11:32:08 +00:00
htmlStr = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename, instanceTitle,
2021-12-29 21:55:09 +00:00
ogMetadata + profileMarkup, lang)
return htmlStr
2021-12-31 21:18:12 +00:00
def html_header_with_website_markup(css_filename: str, instanceTitle: str,
2021-12-29 21:55:09 +00:00
http_prefix: str, domain: str,
system_language: str) -> str:
2021-05-14 11:27:08 +00:00
"""html header which includes website markup
https://schema.org/WebSite
"""
2021-05-15 14:25:51 +00:00
licenseUrl = 'https://www.gnu.org/licenses/agpl-3.0.rdf'
2021-05-15 09:08:01 +00:00
# social networking category
genreUrl = 'http://vocab.getty.edu/aat/300312270'
2021-05-14 11:27:08 +00:00
websiteMarkup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
2021-05-14 11:27:08 +00:00
' {\n' + \
' "@context" : "http://schema.org",\n' + \
' "@type" : "WebSite",\n' + \
' "name": "' + instanceTitle + '",\n' + \
2021-12-25 17:09:22 +00:00
' "url": "' + http_prefix + '://' + domain + '",\n' + \
2021-05-14 11:27:08 +00:00
' "license": "' + licenseUrl + '",\n' + \
2021-12-25 23:03:28 +00:00
' "inLanguage": "' + system_language + '",\n' + \
2021-05-14 11:27:08 +00:00
' "isAccessibleForFree": true,\n' + \
2021-05-15 09:08:01 +00:00
' "genre": "' + genreUrl + '",\n' + \
2021-05-14 11:27:08 +00:00
' "accessMode": ["textual", "visual"],\n' + \
' "accessModeSufficient": ["textual"],\n' + \
2021-05-14 11:30:05 +00:00
' "accessibilityAPI" : ["ARIA"],\n' + \
2021-05-14 11:27:08 +00:00
' "accessibilityControl" : [\n' + \
' "fullKeyboardControl",\n' + \
' "fullTouchControl",\n' + \
' "fullMouseControl"\n' + \
' ],\n' + \
' "encodingFormat" : [\n' + \
' "text/html", "image/png", "image/webp",\n' + \
' "image/jpeg", "image/gif", "text/css"\n' + \
2021-05-14 11:29:20 +00:00
' ]\n' + \
2021-05-14 11:27:08 +00:00
' }\n' + \
' </script>\n'
2021-11-07 23:26:40 +00:00
ogMetadata = \
' <meta content="Epicyon hosted on ' + domain + \
'" property="og:site_name" />\n' + \
2021-12-25 17:09:22 +00:00
' <meta content="' + http_prefix + '://' + domain + \
2021-11-07 23:26:40 +00:00
'/about" property="og:url" />\n' + \
' <meta content="website" property="og:type" />\n' + \
' <meta content="' + instanceTitle + \
'" property="og:title" />\n' + \
2021-12-25 17:09:22 +00:00
' <meta content="' + http_prefix + '://' + domain + \
2021-11-07 23:26:40 +00:00
'/logo.png" property="og:image" />\n' + \
2021-12-25 23:03:28 +00:00
' <meta content="' + system_language + \
2021-11-07 23:26:40 +00:00
'" property="og:locale" />\n' + \
' <meta content="summary_large_image" property="twitter:card" />\n'
2021-11-07 11:35:44 +00:00
htmlStr = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename, instanceTitle,
2021-12-29 21:55:09 +00:00
ogMetadata + websiteMarkup,
system_language)
2021-05-14 11:27:08 +00:00
return htmlStr
2021-12-31 21:18:12 +00:00
def html_header_with_blog_markup(css_filename: str, instanceTitle: str,
2021-12-29 21:55:09 +00:00
http_prefix: str, domain: str, nickname: str,
system_language: str,
published: str, modified: str,
title: str, snippet: str,
translate: {}, url: str,
content_license_url: str) -> str:
2021-05-15 19:39:34 +00:00
"""html header which includes blog post markup
https://schema.org/BlogPosting
"""
2021-12-26 10:19:59 +00:00
authorUrl = local_actor_url(http_prefix, nickname, domain)
2021-12-25 17:09:22 +00:00
aboutUrl = http_prefix + '://' + domain + '/about.html'
2021-05-17 14:25:46 +00:00
# license for content on the site may be different from
# the software license
2021-05-15 19:39:34 +00:00
blogMarkup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
2021-05-15 19:39:34 +00:00
' {\n' + \
' "@context" : "http://schema.org",\n' + \
' "@type" : "BlogPosting",\n' + \
' "headline": "' + title + '",\n' + \
' "datePublished": "' + published + '",\n' + \
2021-11-08 13:20:06 +00:00
' "dateModified": "' + modified + '",\n' + \
2021-05-15 19:39:34 +00:00
' "author": {\n' + \
' "@type": "Person",\n' + \
' "name": "' + nickname + '",\n' + \
2021-05-16 10:42:52 +00:00
' "sameAs": "' + authorUrl + '"\n' + \
2021-05-15 19:39:34 +00:00
' },\n' + \
' "publisher": {\n' + \
' "@type": "WebSite",\n' + \
' "name": "' + instanceTitle + '",\n' + \
2021-05-16 10:42:52 +00:00
' "sameAs": "' + aboutUrl + '"\n' + \
2021-05-15 19:39:34 +00:00
' },\n' + \
2021-12-25 17:13:38 +00:00
' "license": "' + content_license_url + '",\n' + \
2021-05-15 19:39:34 +00:00
' "description": "' + snippet + '"\n' + \
' }\n' + \
' </script>\n'
2021-11-08 13:20:06 +00:00
ogMetadata = \
' <meta property="og:locale" content="' + \
2021-12-25 23:03:28 +00:00
system_language + '" />\n' + \
2021-11-08 13:20:06 +00:00
' <meta property="og:type" content="article" />\n' + \
' <meta property="og:title" content="' + title + '" />\n' + \
' <meta property="og:url" content="' + url + '" />\n' + \
' <meta content="Epicyon hosted on ' + domain + \
'" property="og:site_name" />\n' + \
' <meta property="article:published_time" content="' + \
published + '" />\n' + \
' <meta property="article:modified_time" content="' + \
2021-11-08 13:23:13 +00:00
modified + '" />\n'
2021-11-08 13:20:06 +00:00
2021-11-07 11:36:05 +00:00
htmlStr = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename, instanceTitle,
2021-12-29 21:55:09 +00:00
ogMetadata + blogMarkup,
system_language)
2021-05-15 19:39:34 +00:00
return htmlStr
2021-12-29 21:55:09 +00:00
def html_footer() -> str:
2020-11-09 19:41:01 +00:00
htmlStr = ' </body>\n'
htmlStr += '</html>\n'
return htmlStr
2021-12-29 21:55:09 +00:00
def load_individual_post_as_html_from_cache(base_dir: str,
nickname: str, domain: str,
post_json_object: {}) -> str:
2020-11-09 19:41:01 +00:00
"""If a cached html version of the given post exists then load it and
return the html text
This is much quicker than generating the html from the json object
"""
cachedPostFilename = \
2021-12-26 23:41:34 +00:00
get_cached_post_filename(base_dir, nickname, domain, post_json_object)
2020-11-09 19:41:01 +00:00
postHtml = ''
if not cachedPostFilename:
return postHtml
if not os.path.isfile(cachedPostFilename):
return postHtml
tries = 0
while tries < 3:
try:
with open(cachedPostFilename, 'r') as file:
postHtml = file.read()
break
2021-12-25 15:28:52 +00:00
except Exception as ex:
2021-12-29 21:55:09 +00:00
print('ERROR: load_individual_post_as_html_from_cache ' +
2021-12-25 15:28:52 +00:00
str(tries) + ' ' + str(ex))
2020-11-09 19:41:01 +00:00
# no sleep
tries += 1
if postHtml:
return postHtml
2021-12-29 21:55:09 +00:00
def add_emoji_to_display_name(session, base_dir: str, http_prefix: str,
nickname: str, domain: str,
displayName: str, inProfileName: bool) -> str:
2020-12-29 09:52:52 +00:00
"""Adds emoji icons to display names or CW on individual posts
2020-11-09 19:41:01 +00:00
"""
if ':' not in displayName:
return displayName
displayName = displayName.replace('<p>', '').replace('</p>', '')
emojiTags = {}
2021-01-31 10:48:21 +00:00
# print('TAG: displayName before tags: ' + displayName)
2020-11-09 19:41:01 +00:00
displayName = \
2021-12-29 21:55:09 +00:00
add_html_tags(base_dir, http_prefix,
nickname, domain, displayName, [], emojiTags)
2020-11-09 19:41:01 +00:00
displayName = displayName.replace('<p>', '').replace('</p>', '')
2021-01-31 10:48:21 +00:00
# print('TAG: displayName after tags: ' + displayName)
2020-11-09 19:41:01 +00:00
# convert the emoji dictionary to a list
emojiTagsList = []
for tagName, tag in emojiTags.items():
emojiTagsList.append(tag)
2021-01-31 10:48:21 +00:00
# print('TAG: emoji tags list: ' + str(emojiTagsList))
2020-11-09 19:41:01 +00:00
if not inProfileName:
displayName = \
2021-12-29 21:55:09 +00:00
replace_emoji_from_tags(session, base_dir,
displayName, emojiTagsList, 'post header',
False)
2020-11-09 19:41:01 +00:00
else:
displayName = \
2021-12-29 21:55:09 +00:00
replace_emoji_from_tags(session, base_dir,
displayName, emojiTagsList, 'profile',
False)
2021-01-31 10:48:21 +00:00
# print('TAG: displayName after tags 2: ' + displayName)
2020-11-09 19:41:01 +00:00
# remove any stray emoji
while ':' in displayName:
if '://' in displayName:
break
emojiStr = displayName.split(':')[1]
prevDisplayName = displayName
displayName = displayName.replace(':' + emojiStr + ':', '').strip()
if prevDisplayName == displayName:
break
2021-01-31 10:48:21 +00:00
# print('TAG: displayName after tags 3: ' + displayName)
# print('TAG: displayName after tag replacements: ' + displayName)
2020-11-09 19:41:01 +00:00
return displayName
2021-12-29 21:55:09 +00:00
def _is_image_mime_type(mimeType: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type an image?
"""
2021-08-03 09:09:04 +00:00
if mimeType == 'image/svg+xml':
return True
if not mimeType.startswith('image/'):
return False
2021-12-26 14:26:16 +00:00
extensions = get_image_extensions()
2021-08-03 09:09:04 +00:00
ext = mimeType.split('/')[1]
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2021-12-29 21:55:09 +00:00
def _is_video_mime_type(mimeType: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type a video?
"""
2021-08-03 09:09:04 +00:00
if not mimeType.startswith('video/'):
return False
2021-12-26 14:20:09 +00:00
extensions = get_video_extensions()
2021-08-03 09:09:04 +00:00
ext = mimeType.split('/')[1]
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2021-12-29 21:55:09 +00:00
def _is_audio_mime_type(mimeType: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type an audio file?
"""
2021-08-03 09:09:04 +00:00
if mimeType == 'audio/mpeg':
return True
if not mimeType.startswith('audio/'):
return False
2021-12-26 14:24:03 +00:00
extensions = get_audio_extensions()
2021-08-03 09:09:04 +00:00
ext = mimeType.split('/')[1]
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2021-12-29 21:55:09 +00:00
def _is_attached_image(attachmentFilename: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given attachment filename an image?
"""
if '.' not in attachmentFilename:
return False
imageExt = (
'png', 'jpg', 'jpeg', 'webp', 'avif', 'svg', 'gif'
)
ext = attachmentFilename.split('.')[-1]
if ext in imageExt:
return True
return False
2021-12-29 21:55:09 +00:00
def _is_attached_video(attachmentFilename: str) -> bool:
2021-03-07 10:24:27 +00:00
"""Is the given attachment filename a video?
"""
if '.' not in attachmentFilename:
return False
videoExt = (
'mp4', 'webm', 'ogv'
)
ext = attachmentFilename.split('.')[-1]
if ext in videoExt:
return True
return False
2021-12-29 21:55:09 +00:00
def get_post_attachments_as_html(post_json_object: {}, boxName: str,
translate: {},
is_muted: bool, avatarLink: str,
replyStr: str, announceStr: str, likeStr: str,
bookmarkStr: str, deleteStr: str,
muteStr: str) -> (str, str):
2020-11-09 19:41:01 +00:00
"""Returns a string representing any attachments
"""
attachmentStr = ''
galleryStr = ''
2021-12-25 22:09:19 +00:00
if not post_json_object['object'].get('attachment'):
2020-11-09 19:41:01 +00:00
return attachmentStr, galleryStr
2021-12-25 22:09:19 +00:00
if not isinstance(post_json_object['object']['attachment'], list):
2020-11-09 19:41:01 +00:00
return attachmentStr, galleryStr
attachmentCtr = 0
2021-03-07 12:43:31 +00:00
attachmentStr = ''
mediaStyleAdded = False
2021-12-25 22:09:19 +00:00
for attach in post_json_object['object']['attachment']:
2020-11-09 19:41:01 +00:00
if not (attach.get('mediaType') and attach.get('url')):
continue
mediaType = attach['mediaType']
imageDescription = ''
if attach.get('name'):
imageDescription = attach['name'].replace('"', "'")
2021-12-29 21:55:09 +00:00
if _is_image_mime_type(mediaType):
imageUrl = attach['url']
2021-12-29 21:55:09 +00:00
if _is_attached_image(attach['url']) and 'svg' not in mediaType:
2021-03-07 12:43:31 +00:00
if not attachmentStr:
attachmentStr += '<div class="media">\n'
mediaStyleAdded = True
2020-11-09 19:41:01 +00:00
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
2021-12-29 21:55:09 +00:00
if not is_muted:
galleryStr += ' <a href="' + imageUrl + '">\n'
2020-11-09 19:41:01 +00:00
galleryStr += \
' <img loading="lazy" src="' + \
imageUrl + '" alt="" title="">\n'
2020-11-09 19:41:01 +00:00
galleryStr += ' </a>\n'
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('url'):
imagePostUrl = post_json_object['object']['url']
2020-11-09 19:41:01 +00:00
else:
2021-12-25 22:09:19 +00:00
imagePostUrl = post_json_object['object']['id']
2021-12-29 21:55:09 +00:00
if imageDescription and not is_muted:
2020-11-09 19:41:01 +00:00
galleryStr += \
' <a href="' + imagePostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr+announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<a href="' + imageUrl + '">'
2020-11-09 19:41:01 +00:00
attachmentStr += \
'<img loading="lazy" src="' + imageUrl + \
2020-11-09 19:41:01 +00:00
'" alt="' + imageDescription + '" title="' + \
imageDescription + '" class="attachment"></a>\n'
attachmentCtr += 1
2021-12-29 21:55:09 +00:00
elif _is_video_mime_type(mediaType):
if _is_attached_video(attach['url']):
2021-03-07 10:24:27 +00:00
extension = attach['url'].split('.')[-1]
2020-11-09 19:41:01 +00:00
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
2021-12-29 21:55:09 +00:00
if not is_muted:
2020-11-09 19:41:01 +00:00
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += \
2021-03-07 11:55:06 +00:00
' <figure id="videoContainer" ' + \
'data-fullscreen="false">\n' + \
' <video id="video" controls ' + \
'preload="metadata">\n'
2020-11-09 19:41:01 +00:00
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="video/' + \
2021-03-07 10:24:27 +00:00
extension + '">'
2020-11-09 19:41:01 +00:00
idx = 'Your browser does not support the video tag.'
galleryStr += translate[idx]
galleryStr += ' </video>\n'
2021-03-07 11:55:06 +00:00
galleryStr += ' </figure>\n'
2020-11-09 19:41:01 +00:00
galleryStr += ' </a>\n'
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('url'):
videoPostUrl = post_json_object['object']['url']
2020-11-09 19:41:01 +00:00
else:
2021-12-25 22:09:19 +00:00
videoPostUrl = post_json_object['object']['id']
2021-12-29 21:55:09 +00:00
if imageDescription and not is_muted:
2020-11-09 19:41:01 +00:00
galleryStr += \
' <a href="' + videoPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += \
2021-03-07 12:01:33 +00:00
'<center><figure id="videoContainer" ' + \
'data-fullscreen="false">\n' + \
' <video id="video" controls ' + \
'preload="metadata">\n'
2020-11-09 19:41:01 +00:00
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="video/' + \
2021-03-07 10:24:27 +00:00
extension + '">'
2020-11-09 19:41:01 +00:00
attachmentStr += \
translate['Your browser does not support the video tag.']
2021-03-07 12:01:33 +00:00
attachmentStr += '</video></figure></center>'
2020-11-09 19:41:01 +00:00
attachmentCtr += 1
2021-12-29 21:55:09 +00:00
elif _is_audio_mime_type(mediaType):
2020-11-09 19:41:01 +00:00
extension = '.mp3'
if attach['url'].endswith('.ogg'):
extension = '.ogg'
if attach['url'].endswith(extension):
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
2021-12-29 21:55:09 +00:00
if not is_muted:
2020-11-09 19:41:01 +00:00
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += ' <audio controls>\n'
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the audio tag.'
galleryStr += translate[idx]
galleryStr += ' </audio>\n'
galleryStr += ' </a>\n'
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('url'):
audioPostUrl = post_json_object['object']['url']
2020-11-09 19:41:01 +00:00
else:
2021-12-25 22:09:19 +00:00
audioPostUrl = post_json_object['object']['id']
2021-12-29 21:55:09 +00:00
if imageDescription and not is_muted:
2020-11-09 19:41:01 +00:00
galleryStr += \
' <a href="' + audioPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + \
likeStr + bookmarkStr + \
2021-06-22 12:29:17 +00:00
deleteStr + muteStr + '\n'
2020-11-09 19:41:01 +00:00
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<center>\n<audio controls>\n'
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
attachmentStr += \
translate['Your browser does not support the audio tag.']
attachmentStr += '</audio>\n</center>\n'
attachmentCtr += 1
2021-03-07 12:43:31 +00:00
if mediaStyleAdded:
attachmentStr += '</div>'
2020-11-09 19:41:01 +00:00
return attachmentStr, galleryStr
2021-12-29 21:55:09 +00:00
def html_post_separator(base_dir: str, column: str) -> str:
2020-11-09 19:41:01 +00:00
"""Returns the html for a timeline post separator image
"""
2021-12-26 14:08:58 +00:00
theme = get_config_param(base_dir, 'theme')
2020-11-09 19:41:01 +00:00
filename = 'separator.png'
separatorClass = "postSeparatorImage"
2020-11-09 19:41:01 +00:00
if column:
separatorClass = "postSeparatorImage" + column.title()
2020-11-09 19:41:01 +00:00
filename = 'separator_' + column + '.png'
2021-12-25 16:17:53 +00:00
separatorImageFilename = \
base_dir + '/theme/' + theme + '/icons/' + filename
2020-11-09 19:41:01 +00:00
separatorStr = ''
if os.path.isfile(separatorImageFilename):
separatorStr = \
'<div class="' + separatorClass + '"><center>' + \
2021-02-01 18:38:08 +00:00
'<img src="/icons/' + filename + '" ' + \
'alt="" /></center></div>\n'
2020-11-09 19:41:01 +00:00
return separatorStr
2020-11-09 22:44:03 +00:00
2021-12-29 21:55:09 +00:00
def html_highlight_label(label: str, highlight: bool) -> str:
2020-11-17 20:40:36 +00:00
"""If the given text should be highlighted then return
the appropriate markup.
This is so that in shell browsers, like lynx, it's possible
to see if the replies or DM button are highlighted.
"""
if not highlight:
return label
return '*' + str(label) + '*'
2021-12-29 21:55:09 +00:00
def get_avatar_image_url(session,
base_dir: str, http_prefix: str,
postActor: str, person_cache: {},
avatarUrl: str, allowDownloads: bool,
signing_priv_key_pem: str) -> str:
"""Returns the avatar image url
"""
# get the avatar image url for the post actor
if not avatarUrl:
avatarUrl = \
2021-12-29 21:55:09 +00:00
get_person_avatar_url(base_dir, postActor, person_cache,
allowDownloads)
avatarUrl = \
2021-12-29 21:55:09 +00:00
update_avatar_image_cache(signing_priv_key_pem,
session, base_dir, http_prefix,
postActor, avatarUrl, person_cache,
allowDownloads)
else:
2021-12-29 21:55:09 +00:00
update_avatar_image_cache(signing_priv_key_pem,
session, base_dir, http_prefix,
postActor, avatarUrl, person_cache,
allowDownloads)
if not avatarUrl:
avatarUrl = postActor + '/avatar.png'
return avatarUrl
2021-02-05 17:05:53 +00:00
2021-12-29 21:55:09 +00:00
def html_hide_from_screen_reader(htmlStr: str) -> str:
2021-02-06 10:35:47 +00:00
"""Returns html which is hidden from screen readers
"""
return '<span aria-hidden="true">' + htmlStr + '</span>'
2021-12-31 21:18:12 +00:00
def html_keyboard_navigation(banner: str, links: {}, access_keys: {},
2021-12-29 21:55:09 +00:00
subHeading: str = None,
usersPath: str = None, translate: {} = None,
followApprovals: bool = False) -> str:
2021-02-05 17:05:53 +00:00
"""Given a set of links return the html for keyboard navigation
"""
htmlStr = '<div class="transparent"><ul>\n'
2021-02-05 19:15:52 +00:00
if banner:
2021-02-15 12:13:31 +00:00
htmlStr += '<pre aria-label="">\n' + banner + '\n<br><br></pre>\n'
2021-02-05 19:15:52 +00:00
2021-02-12 15:28:11 +00:00
if subHeading:
2021-02-12 15:31:47 +00:00
htmlStr += '<strong><label class="transparent">' + \
subHeading + '</label></strong><br>\n'
2021-02-12 15:28:11 +00:00
# show new follower approvals
if usersPath and translate and followApprovals:
2021-02-06 11:39:32 +00:00
htmlStr += '<strong><label class="transparent">' + \
'<a href="' + usersPath + '/followers#timeline">' + \
translate['Approve follow requests'] + '</a>' + \
'</label></strong><br><br>\n'
# show the list of links
2021-02-05 17:05:53 +00:00
for title, url in links.items():
2021-04-22 11:51:19 +00:00
accessKeyStr = ''
2021-12-31 21:18:12 +00:00
if access_keys.get(title):
accessKeyStr = 'accesskey="' + access_keys[title] + '"'
2021-04-22 11:51:19 +00:00
2021-02-05 17:33:31 +00:00
htmlStr += '<li><label class="transparent">' + \
2021-04-22 11:51:19 +00:00
'<a href="' + str(url) + '" ' + accessKeyStr + '>' + \
str(title) + '</a></label></li>\n'
htmlStr += '</ul></div>\n'
2021-02-05 17:05:53 +00:00
return htmlStr
2021-07-22 16:58:59 +00:00
2021-12-29 21:55:09 +00:00
def begin_edit_section(label: str) -> str:
2021-07-22 16:58:59 +00:00
"""returns the html for begining a dropdown section on edit profile screen
"""
return \
' <details><summary class="cw">' + label + '</summary>\n' + \
'<div class="container">'
2021-12-29 21:55:09 +00:00
def end_edit_section() -> str:
2021-07-22 16:58:59 +00:00
"""returns the html for ending a dropdown section on edit profile screen
"""
return ' </div></details>\n'
2021-12-29 21:55:09 +00:00
def edit_text_field(label: str, name: str, value: str = "",
placeholder: str = "", required: bool = False) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a text field
"""
if value is None:
value = ''
placeholderStr = ''
if placeholder:
placeholderStr = ' placeholder="' + placeholder + '"'
2021-07-27 18:31:50 +00:00
requiredStr = ''
if required:
requiredStr = ' required'
2021-11-30 10:40:07 +00:00
textFieldStr = ''
if label:
textFieldStr = \
'<label class="labels">' + label + '</label><br>\n'
textFieldStr += \
2021-07-22 16:58:59 +00:00
' <input type="text" name="' + name + '" value="' + \
2021-07-27 18:31:50 +00:00
value + '"' + placeholderStr + requiredStr + '>\n'
return textFieldStr
2021-07-22 16:58:59 +00:00
2021-12-29 21:55:09 +00:00
def edit_number_field(label: str, name: str, value: int,
minValue: int, maxValue: int,
placeholder: int) -> str:
2021-07-24 11:47:51 +00:00
"""Returns html for editing an integer number field
"""
if value is None:
value = ''
placeholderStr = ''
if placeholder:
placeholderStr = ' placeholder="' + str(placeholder) + '"'
return \
'<label class="labels">' + label + '</label><br>\n' + \
' <input type="number" name="' + name + '" value="' + \
str(value) + '"' + placeholderStr + ' ' + \
'min="' + str(minValue) + '" max="' + str(maxValue) + '" step="1">\n'
2021-12-29 21:55:09 +00:00
def edit_currency_field(label: str, name: str, value: str,
placeholder: str, required: bool) -> str:
2021-07-24 22:08:11 +00:00
"""Returns html for editing a currency field
"""
if value is None:
value = '0.00'
2021-07-27 18:46:10 +00:00
placeholderStr = ''
2021-07-24 22:08:11 +00:00
if placeholder:
if placeholder.isdigit():
placeholderStr = ' placeholder="' + str(placeholder) + '"'
2021-07-27 18:56:51 +00:00
requiredStr = ''
if required:
requiredStr = ' required'
2021-07-24 22:08:11 +00:00
return \
'<label class="labels">' + label + '</label><br>\n' + \
' <input type="text" name="' + name + '" value="' + \
str(value) + '"' + placeholderStr + ' ' + \
2021-07-27 18:56:51 +00:00
' pattern="^\\d{1,3}(,\\d{3})*(\\.\\d+)?" data-type="currency"' + \
requiredStr + '>\n'
2021-07-24 22:08:11 +00:00
2021-12-29 21:55:09 +00:00
def edit_check_box(label: str, name: str, checked: bool) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a checkbox field
"""
checkedStr = ''
if checked:
checkedStr = ' checked'
return \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="' + name + '"' + checkedStr + '> ' + label + '<br>\n'
2021-12-29 21:55:09 +00:00
def edit_text_area(label: str, name: str, value: str,
height: int, placeholder: str, spellcheck: bool) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a textarea field
"""
if value is None:
value = ''
2021-07-22 18:35:45 +00:00
text = ''
if label:
text = '<label class="labels">' + label + '</label><br>\n'
text += \
2021-07-22 16:58:59 +00:00
' <textarea id="message" placeholder=' + \
2021-07-22 18:50:31 +00:00
'"' + placeholder + '" '
text += 'name="' + name + '" '
2021-07-22 18:52:47 +00:00
text += 'style="height:' + str(height) + 'px" '
2021-07-22 18:50:31 +00:00
text += 'spellcheck="' + str(spellcheck).lower() + '">'
text += value + '</textarea>\n'
2021-07-22 18:35:45 +00:00
return text
2021-12-29 21:55:09 +00:00
def html_search_result_share(base_dir: str, sharedItem: {}, translate: {},
http_prefix: str, domain_full: str,
contactNickname: str, itemID: str,
actor: str, sharesFileType: str,
category: str) -> str:
"""Returns the html for an individual shared item
"""
sharedItemsForm = '<div class="container">\n'
sharedItemsForm += \
'<p class="share-title">' + sharedItem['displayName'] + '</p>\n'
if sharedItem.get('imageUrl'):
sharedItemsForm += \
'<a href="' + sharedItem['imageUrl'] + '">\n'
sharedItemsForm += \
'<img loading="lazy" src="' + sharedItem['imageUrl'] + \
'" alt="Item image"></a>\n'
sharedItemsForm += '<p>' + sharedItem['summary'] + '</p>\n<p>'
if sharedItem.get('itemQty'):
if sharedItem['itemQty'] > 1:
sharedItemsForm += \
'<b>' + translate['Quantity'] + \
':</b> ' + str(sharedItem['itemQty']) + '<br>'
sharedItemsForm += \
'<b>' + translate['Type'] + ':</b> ' + sharedItem['itemType'] + '<br>'
sharedItemsForm += \
'<b>' + translate['Category'] + ':</b> ' + \
sharedItem['category'] + '<br>'
if sharedItem.get('location'):
sharedItemsForm += \
'<b>' + translate['Location'] + ':</b> ' + \
sharedItem['location'] + '<br>'
2021-09-19 15:54:51 +00:00
contactTitleStr = translate['Contact']
if sharedItem.get('itemPrice') and \
sharedItem.get('itemCurrency'):
2021-12-26 18:03:39 +00:00
if is_float(sharedItem['itemPrice']):
if float(sharedItem['itemPrice']) > 0:
sharedItemsForm += \
' <b>' + translate['Price'] + \
':</b> ' + sharedItem['itemPrice'] + \
' ' + sharedItem['itemCurrency']
2021-09-19 15:54:51 +00:00
contactTitleStr = translate['Buy']
sharedItemsForm += '</p>\n'
contactActor = \
2021-12-26 10:19:59 +00:00
local_actor_url(http_prefix, contactNickname, domain_full)
2021-09-21 09:58:55 +00:00
buttonStyleStr = 'button'
2021-09-19 15:54:51 +00:00
if category == 'accommodation':
contactTitleStr = translate['Request to stay']
2021-09-21 09:58:55 +00:00
buttonStyleStr = 'contactbutton'
2021-09-19 15:54:51 +00:00
sharedItemsForm += \
'<p>' + \
'<a href="' + actor + '?replydm=sharedesc:' + \
2021-09-21 12:44:55 +00:00
sharedItem['displayName'] + '?mention=' + contactActor + \
'?category=' + category + \
2021-09-21 09:58:55 +00:00
'"><button class="' + buttonStyleStr + '">' + contactTitleStr + \
'</button></a>\n' + \
'<a href="' + contactActor + '"><button class="button">' + \
2021-09-19 15:54:51 +00:00
translate['Profile'] + '</button></a>\n'
# should the remove button be shown?
showRemoveButton = False
2021-12-27 22:19:18 +00:00
nickname = get_nickname_from_actor(actor)
if actor.endswith('/users/' + contactNickname):
showRemoveButton = True
2021-12-28 19:33:29 +00:00
elif is_moderator(base_dir, nickname):
showRemoveButton = True
else:
2021-12-31 21:18:12 +00:00
admin_nickname = get_config_param(base_dir, 'admin')
if admin_nickname:
if actor.endswith('/users/' + admin_nickname):
showRemoveButton = True
if showRemoveButton:
2021-08-09 22:07:34 +00:00
if sharesFileType == 'shares':
sharedItemsForm += \
' <a href="' + actor + '?rmshare=' + \
itemID + '"><button class="button">' + \
translate['Remove'] + '</button></a>\n'
else:
sharedItemsForm += \
' <a href="' + actor + '?rmwanted=' + \
itemID + '"><button class="button">' + \
translate['Remove'] + '</button></a>\n'
sharedItemsForm += '</p></div>\n'
return sharedItemsForm
2021-12-29 21:55:09 +00:00
def html_show_share(base_dir: str, domain: str, nickname: str,
http_prefix: str, domain_full: str,
itemID: str, translate: {},
shared_items_federated_domains: [],
2021-12-31 23:50:29 +00:00
default_timeline: str, theme: str,
2021-12-29 21:55:09 +00:00
sharesFileType: str, category: str) -> str:
"""Shows an individual shared item after selecting it from the left column
"""
sharesJson = None
shareUrl = itemID.replace('___', '://').replace('--', '/')
2021-12-27 22:19:18 +00:00
contactNickname = get_nickname_from_actor(shareUrl)
if not contactNickname:
return None
2021-12-26 10:00:46 +00:00
if '://' + domain_full + '/' in shareUrl:
# shared item on this instance
sharesFilename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, contactNickname, domain) + '/' + \
sharesFileType + '.json'
if not os.path.isfile(sharesFilename):
return None
2021-12-26 15:13:34 +00:00
sharesJson = load_json(sharesFilename)
else:
# federated shared item
if sharesFileType == 'shares':
2021-12-25 16:17:53 +00:00
catalogsDir = base_dir + '/cache/catalogs'
else:
2021-12-25 16:17:53 +00:00
catalogsDir = base_dir + '/cache/wantedItems'
if not os.path.isdir(catalogsDir):
return None
for subdir, dirs, files in os.walk(catalogsDir):
for f in files:
if '#' in f:
continue
if not f.endswith('.' + sharesFileType + '.json'):
continue
federatedDomain = f.split('.')[0]
2021-12-25 18:05:01 +00:00
if federatedDomain not in shared_items_federated_domains:
continue
sharesFilename = catalogsDir + '/' + f
2021-12-26 15:13:34 +00:00
sharesJson = load_json(sharesFilename)
if not sharesJson:
continue
if sharesJson.get(itemID):
break
break
if not sharesJson:
return None
if not sharesJson.get(itemID):
return None
sharedItem = sharesJson[itemID]
2021-12-26 10:19:59 +00:00
actor = local_actor_url(http_prefix, nickname, domain_full)
# filename of the banner shown at the top
2021-12-31 21:18:12 +00:00
banner_file, banner_filename = \
2021-12-29 21:55:09 +00:00
get_banner_file(base_dir, nickname, domain, theme)
shareStr = \
'<header>\n' + \
'<a href="/users/' + nickname + '/' + \
2021-12-31 23:50:29 +00:00
default_timeline + '" title="" alt="">\n'
shareStr += '<img loading="lazy" class="timeline-banner" ' + \
'alt="" ' + \
2021-12-31 21:18:12 +00:00
'src="/users/' + nickname + '/' + banner_file + '" /></a>\n' + \
'</header><br>\n'
2021-07-28 19:41:51 +00:00
shareStr += \
2021-12-29 21:55:09 +00:00
html_search_result_share(base_dir, sharedItem, translate, http_prefix,
domain_full, contactNickname, itemID,
actor, sharesFileType, category)
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-profile.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/epicyon.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon.css'
instanceTitle = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
2021-12-31 21:18:12 +00:00
return html_header_with_external_style(css_filename,
2021-12-29 21:55:09 +00:00
instanceTitle, None) + \
shareStr + html_footer()
2021-10-30 11:08:57 +00:00
2021-12-29 21:55:09 +00:00
def set_custom_background(base_dir: str, background: str,
newBackground: str) -> str:
2021-10-30 11:08:57 +00:00
"""Sets a custom background
Returns the extension, if found
"""
2021-10-30 11:51:41 +00:00
ext = 'jpg'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/img/' + background + '.' + ext):
2021-10-30 11:51:41 +00:00
if not newBackground:
newBackground = background
2021-12-25 16:17:53 +00:00
if not os.path.isfile(base_dir + '/accounts/' +
2021-10-30 11:51:41 +00:00
newBackground + '.' + ext):
2021-12-25 16:17:53 +00:00
copyfile(base_dir + '/img/' + background + '.' + ext,
base_dir + '/accounts/' + newBackground + '.' + ext)
2021-10-30 11:51:41 +00:00
return ext
2021-10-30 11:08:57 +00:00
return None