Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2020-09-13 18:16:50 +01:00
commit 01c34045a4
19 changed files with 357 additions and 75 deletions

View File

@ -182,7 +182,7 @@ ln -s /etc/nginx/sites-available/YOUR_DOMAIN /etc/nginx/sites-enabled/
Generate a LetsEncrypt certificate. Generate a LetsEncrypt certificate.
``` bash ``` bash
certbot certonly -n --server https://acme-v01.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL certbot certonly -n --server https://acme-v02.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
``` ```
And restart the web server: And restart the web server:

44
auth.py
View File

@ -24,17 +24,49 @@ def hashPassword(password: str) -> str:
return (salt + pwdhash).decode('ascii') return (salt + pwdhash).decode('ascii')
def verifyPassword(storedPassword: str, providedPassword: str) -> bool: def getPasswordHash(salt: str, providedPassword: str) -> str:
"""Verify a stored password against one provided by user """Returns the hash of a password
""" """
salt = storedPassword[:64]
storedPassword = storedPassword[64:]
pwdhash = hashlib.pbkdf2_hmac('sha512', pwdhash = hashlib.pbkdf2_hmac('sha512',
providedPassword.encode('utf-8'), providedPassword.encode('utf-8'),
salt.encode('ascii'), salt.encode('ascii'),
100000) 100000)
pwdhash = binascii.hexlify(pwdhash).decode('ascii') return binascii.hexlify(pwdhash).decode('ascii')
return pwdhash == storedPassword
def constantTimeStringCheck(string1: str, string2: str) -> bool:
"""Compares two string and returns if they are the same
using a constant amount of time
See https://sqreen.github.io/DevelopersSecurityBestPractices/
timing-attack/python
"""
# strings must be of equal length
if len(string1) != len(string2):
return False
ctr = 0
matched = True
for ch in string1:
if ch != string2[ctr]:
matched = False
else:
# this is to make the timing more even
# and not provide clues
matched = matched
ctr += 1
return matched
def verifyPassword(storedPassword: str, providedPassword: str) -> bool:
"""Verify a stored password against one provided by user
"""
if not storedPassword:
return False
if not providedPassword:
return False
salt = storedPassword[:64]
storedPassword = storedPassword[64:]
pwHash = getPasswordHash(salt, providedPassword)
return constantTimeStringCheck(pwHash, storedPassword)
def createBasicAuthHeader(nickname: str, password: str) -> str: def createBasicAuthHeader(nickname: str, password: str) -> str:

View File

@ -21,18 +21,22 @@ def addGlobalBlock(baseDir: str,
""" """
blockingFilename = baseDir + '/accounts/blocking.txt' blockingFilename = baseDir + '/accounts/blocking.txt'
if not blockNickname.startswith('#'): if not blockNickname.startswith('#'):
# is the handle already blocked?
blockHandle = blockNickname + '@' + blockDomain blockHandle = blockNickname + '@' + blockDomain
if os.path.isfile(blockingFilename): if os.path.isfile(blockingFilename):
if blockHandle in open(blockingFilename).read(): if blockHandle in open(blockingFilename).read():
return False return False
# block an account handle or domain
blockFile = open(blockingFilename, "a+") blockFile = open(blockingFilename, "a+")
blockFile.write(blockHandle + '\n') blockFile.write(blockHandle + '\n')
blockFile.close() blockFile.close()
else: else:
blockHashtag = blockNickname blockHashtag = blockNickname
# is the hashtag already blocked?
if os.path.isfile(blockingFilename): if os.path.isfile(blockingFilename):
if blockHashtag + '\n' in open(blockingFilename).read(): if blockHashtag + '\n' in open(blockingFilename).read():
return False return False
# block a hashtag
blockFile = open(blockingFilename, "a+") blockFile = open(blockingFilename, "a+")
blockFile.write(blockHashtag + '\n') blockFile.write(blockHashtag + '\n')
blockFile.close() blockFile.close()

View File

@ -722,7 +722,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
editBlogImageSection += \ editBlogImageSection += \
' <input type="file" id="attachpic" name="attachpic"' ' <input type="file" id="attachpic" name="attachpic"'
editBlogImageSection += \ editBlogImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, ' + \ ' accept=".png, .jpg, .jpeg, .gif, .webp, .avif, ' + \
'.mp4, .webm, .ogv, .mp3, .ogg">' '.mp4, .webm, .ogv, .mp3, .ogg">'
editBlogImageSection += ' </div>' editBlogImageSection += ' </div>'

View File

@ -570,6 +570,41 @@ def removeLongWords(content: str, maxWordLength: int,
return content return content
def loadAutoTags(baseDir: str, nickname: str, domain: str) -> []:
"""Loads automatic tags file and returns a list containing
the lines of the file
"""
filename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/autotags.txt'
if not os.path.isfile(filename):
return []
with open(filename, "r") as f:
return f.readlines()
return []
def autoTag(baseDir: str, nickname: str, domain: str,
wordStr: str, autoTagList: [],
appendTags: []):
"""Generates a list of tags to be automatically appended to the content
"""
for tagRule in autoTagList:
if wordStr not in tagRule:
continue
if '->' not in tagRule:
continue
match = tagRule.split('->')[0].strip()
if match != wordStr:
continue
tagName = tagRule.split('->')[1].strip()
if tagName.startswith('#'):
if tagName not in appendTags:
appendTags.append(tagName)
else:
if '#' + tagName not in appendTags:
appendTags.append('#' + tagName)
def addHtmlTags(baseDir: str, httpPrefix: str, def addHtmlTags(baseDir: str, httpPrefix: str,
nickname: str, domain: str, content: str, nickname: str, domain: str, content: str,
recipients: [], hashtags: {}, isJsonContent=False) -> str: recipients: [], hashtags: {}, isJsonContent=False) -> str:
@ -616,6 +651,9 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
# extract mentions and tags from words # extract mentions and tags from words
longWordsList = [] longWordsList = []
prevWordStr = ''
autoTagsList = loadAutoTags(baseDir, nickname, domain)
appendTags = []
for wordStr in words: for wordStr in words:
wordLen = len(wordStr) wordLen = len(wordStr)
if wordLen > 2: if wordLen > 2:
@ -625,10 +663,12 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
if firstChar == '@': if firstChar == '@':
if addMention(wordStr, httpPrefix, following, if addMention(wordStr, httpPrefix, following,
replaceMentions, recipients, hashtags): replaceMentions, recipients, hashtags):
prevWordStr = ''
continue continue
elif firstChar == '#': elif firstChar == '#':
if addHashTags(wordStr, httpPrefix, originalDomain, if addHashTags(wordStr, httpPrefix, originalDomain,
replaceHashTags, hashtags): replaceHashTags, hashtags):
prevWordStr = ''
continue continue
elif ':' in wordStr: elif ':' in wordStr:
wordStr2 = wordStr.split(':')[1] wordStr2 = wordStr.split(':')[1]
@ -646,6 +686,24 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
addEmoji(baseDir, ':' + wordStr2 + ':', httpPrefix, addEmoji(baseDir, ':' + wordStr2 + ':', httpPrefix,
originalDomain, replaceEmoji, hashtags, originalDomain, replaceEmoji, hashtags,
emojiDict) emojiDict)
else:
if autoTag(baseDir, nickname, domain, wordStr,
autoTagsList, appendTags):
prevWordStr = ''
continue
if prevWordStr:
if autoTag(baseDir, nickname, domain,
prevWordStr + ' ' + wordStr,
autoTagsList, appendTags):
prevWordStr = ''
continue
prevWordStr = wordStr
# add any auto generated tags
for appended in appendTags:
content = content + ' ' + appended
addHashTags(appended, httpPrefix, originalDomain,
replaceHashTags, hashtags)
# replace words with their html versions # replace words with their html versions
for wordStr, replaceStr in replaceMentions.items(): for wordStr, replaceStr in replaceMentions.items():
@ -737,6 +795,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
'jpeg': 'image/jpeg', 'jpeg': 'image/jpeg',
'gif': 'image/gif', 'gif': 'image/gif',
'webp': 'image/webp', 'webp': 'image/webp',
'avif': 'image/avif',
'mp4': 'video/mp4', 'mp4': 'video/mp4',
'ogv': 'video/ogv', 'ogv': 'video/ogv',
'mp3': 'audio/mpeg', 'mp3': 'audio/mpeg',
@ -771,7 +830,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
break break
# remove any existing image files with a different format # remove any existing image files with a different format
extensionTypes = ('png', 'jpg', 'jpeg', 'gif', 'webp') extensionTypes = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif')
for ex in extensionTypes: for ex in extensionTypes:
if ex == detectedExtension: if ex == detectedExtension:
continue continue

View File

@ -254,6 +254,7 @@ class PubServer(BaseHTTPRequestHandler):
if path.endswith('.png') or \ if path.endswith('.png') or \
path.endswith('.jpg') or \ path.endswith('.jpg') or \
path.endswith('.gif') or \ path.endswith('.gif') or \
path.endswith('.avif') or \
path.endswith('.webp'): path.endswith('.webp'):
return True return True
return False return False
@ -1407,6 +1408,7 @@ class PubServer(BaseHTTPRequestHandler):
fullBlockDomain = None fullBlockDomain = None
if moderationText.startswith('http') or \ if moderationText.startswith('http') or \
moderationText.startswith('dat'): moderationText.startswith('dat'):
# https://domain
blockDomain, blockPort = \ blockDomain, blockPort = \
getDomainFromActor(moderationText) getDomainFromActor(moderationText)
fullBlockDomain = blockDomain fullBlockDomain = blockDomain
@ -1416,13 +1418,20 @@ class PubServer(BaseHTTPRequestHandler):
fullBlockDomain = \ fullBlockDomain = \
blockDomain + ':' + str(blockPort) blockDomain + ':' + str(blockPort)
if '@' in moderationText: if '@' in moderationText:
# nick@domain or *@domain
fullBlockDomain = moderationText.split('@')[1] fullBlockDomain = moderationText.split('@')[1]
else:
# assume the text is a domain name
if not fullBlockDomain and '.' in moderationText:
nickname = '*'
fullBlockDomain = moderationText.strip()
if fullBlockDomain or nickname.startswith('#'): if fullBlockDomain or nickname.startswith('#'):
addGlobalBlock(baseDir, nickname, fullBlockDomain) addGlobalBlock(baseDir, nickname, fullBlockDomain)
if moderationButton == 'unblock': if moderationButton == 'unblock':
fullBlockDomain = None fullBlockDomain = None
if moderationText.startswith('http') or \ if moderationText.startswith('http') or \
moderationText.startswith('dat'): moderationText.startswith('dat'):
# https://domain
blockDomain, blockPort = \ blockDomain, blockPort = \
getDomainFromActor(moderationText) getDomainFromActor(moderationText)
fullBlockDomain = blockDomain fullBlockDomain = blockDomain
@ -1432,7 +1441,13 @@ class PubServer(BaseHTTPRequestHandler):
fullBlockDomain = \ fullBlockDomain = \
blockDomain + ':' + str(blockPort) blockDomain + ':' + str(blockPort)
if '@' in moderationText: if '@' in moderationText:
# nick@domain or *@domain
fullBlockDomain = moderationText.split('@')[1] fullBlockDomain = moderationText.split('@')[1]
else:
# assume the text is a domain name
if not fullBlockDomain and '.' in moderationText:
nickname = '*'
fullBlockDomain = moderationText.strip()
if fullBlockDomain or nickname.startswith('#'): if fullBlockDomain or nickname.startswith('#'):
removeGlobalBlock(baseDir, nickname, fullBlockDomain) removeGlobalBlock(baseDir, nickname, fullBlockDomain)
if moderationButton == 'remove': if moderationButton == 'remove':
@ -2508,6 +2523,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFilename = mediaFilenameBase + '.gif' mediaFilename = mediaFilenameBase + '.gif'
if self.headers['Content-type'].endswith('webp'): if self.headers['Content-type'].endswith('webp'):
mediaFilename = mediaFilenameBase + '.webp' mediaFilename = mediaFilenameBase + '.webp'
if self.headers['Content-type'].endswith('avif'):
mediaFilename = mediaFilenameBase + '.avif'
with open(mediaFilename, 'wb') as avFile: with open(mediaFilename, 'wb') as avFile:
avFile.write(mediaBytes) avFile.write(mediaBytes)
if debug: if debug:
@ -3538,6 +3555,9 @@ class PubServer(BaseHTTPRequestHandler):
if 'image/webp' in self.headers['Accept']: if 'image/webp' in self.headers['Accept']:
favType = 'image/webp' favType = 'image/webp'
favFilename = 'favicon.webp' favFilename = 'favicon.webp'
if 'image/avif' in self.headers['Accept']:
favType = 'image/avif'
favFilename = 'favicon.avif'
# custom favicon # custom favicon
faviconFilename = baseDir + '/' + favFilename faviconFilename = baseDir + '/' + favFilename
if not os.path.isfile(faviconFilename): if not os.path.isfile(faviconFilename):
@ -3834,6 +3854,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFileType = 'image/gif' mediaFileType = 'image/gif'
elif mediaFilename.endswith('.webp'): elif mediaFilename.endswith('.webp'):
mediaFileType = 'image/webp' mediaFileType = 'image/webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'image/avif'
elif mediaFilename.endswith('.mp4'): elif mediaFilename.endswith('.mp4'):
mediaFileType = 'video/mp4' mediaFileType = 'video/mp4'
elif mediaFilename.endswith('.ogv'): elif mediaFilename.endswith('.ogv'):
@ -3876,6 +3898,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaImageType = 'jpeg' mediaImageType = 'jpeg'
elif emojiFilename.endswith('.webp'): elif emojiFilename.endswith('.webp'):
mediaImageType = 'webp' mediaImageType = 'webp'
elif emojiFilename.endswith('.avif'):
mediaImageType = 'avif'
else: else:
mediaImageType = 'gif' mediaImageType = 'gif'
with open(emojiFilename, 'rb') as avFile: with open(emojiFilename, 'rb') as avFile:
@ -3960,6 +3984,11 @@ class PubServer(BaseHTTPRequestHandler):
'image/webp', 'image/webp',
mediaBinary, None, mediaBinary, None,
callingDomain) callingDomain)
elif mediaFilename.endswith('.avif'):
self._set_headers_etag(mediaFilename,
'image/avif',
mediaBinary, None,
callingDomain)
else: else:
# default to jpeg # default to jpeg
self._set_headers_etag(mediaFilename, self._set_headers_etag(mediaFilename,
@ -6951,7 +6980,7 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {}) -> bool: GETstartTime, GETtimings: {}) -> bool:
"""Show a background image """Show a background image
""" """
for ext in ('webp', 'gif', 'jpg', 'png'): for ext in ('webp', 'gif', 'jpg', 'png', 'avif'):
for bg in ('follow', 'options', 'login'): for bg in ('follow', 'options', 'login'):
# follow screen background image # follow screen background image
if path.endswith('/' + bg + '-background.' + ext): if path.endswith('/' + bg + '-background.' + ext):
@ -7014,6 +7043,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFileType = 'jpeg' mediaFileType = 'jpeg'
elif mediaFilename.endswith('.webp'): elif mediaFilename.endswith('.webp'):
mediaFileType = 'webp' mediaFileType = 'webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'avif'
else: else:
mediaFileType = 'gif' mediaFileType = 'gif'
with open(mediaFilename, 'rb') as avFile: with open(mediaFilename, 'rb') as avFile:
@ -7061,6 +7092,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaImageType = 'jpeg' mediaImageType = 'jpeg'
elif avatarFile.endswith('.gif'): elif avatarFile.endswith('.gif'):
mediaImageType = 'gif' mediaImageType = 'gif'
elif avatarFile.endswith('.avif'):
mediaImageType = 'avif'
else: else:
mediaImageType = 'webp' mediaImageType = 'webp'
with open(avatarFilename, 'rb') as avFile: with open(avatarFilename, 'rb') as avFile:
@ -7805,6 +7838,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.path == '/login.png' or \ if self.path == '/login.png' or \
self.path == '/login.gif' or \ self.path == '/login.gif' or \
self.path == '/login.webp' or \ self.path == '/login.webp' or \
self.path == '/login.avif' or \
self.path == '/login.jpeg' or \ self.path == '/login.jpeg' or \
self.path == '/login.jpg' or \ self.path == '/login.jpg' or \
self.path == '/qrcode.png': self.path == '/qrcode.png':
@ -8991,6 +9025,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFileType = 'image/gif' mediaFileType = 'image/gif'
elif checkPath.endswith('.webp'): elif checkPath.endswith('.webp'):
mediaFileType = 'image/webp' mediaFileType = 'image/webp'
elif checkPath.endswith('.avif'):
mediaFileType = 'image/avif'
elif checkPath.endswith('.mp4'): elif checkPath.endswith('.mp4'):
mediaFileType = 'video/mp4' mediaFileType = 'video/mp4'
elif checkPath.endswith('.ogv'): elif checkPath.endswith('.ogv'):
@ -9065,6 +9101,7 @@ class PubServer(BaseHTTPRequestHandler):
if filename.endswith('.png') or \ if filename.endswith('.png') or \
filename.endswith('.jpg') or \ filename.endswith('.jpg') or \
filename.endswith('.webp') or \ filename.endswith('.webp') or \
filename.endswith('.avif') or \
filename.endswith('.gif'): filename.endswith('.gif'):
if self.server.debug: if self.server.debug:
print('DEBUG: POST media removing metadata') print('DEBUG: POST media removing metadata')

View File

@ -19,9 +19,9 @@
--time-color: #aaa; --time-color: #aaa;
--button-text: #FFFFFF; --button-text: #FFFFFF;
--button-small-text: #FFFFFF; --button-small-text: #FFFFFF;
--button-background-hover: #777;
--button-background: #999; --button-background: #999;
--button-small-background: #999; --button-small-background: #999;
--button-selected: #666;
--hashtag-margin: 2%; --hashtag-margin: 2%;
--hashtag-vertical-spacing1: 50px; --hashtag-vertical-spacing1: 50px;
--hashtag-vertical-spacing2: 100px; --hashtag-vertical-spacing2: 100px;
@ -107,8 +107,7 @@ a:focus {
} }
.button:hover { .button:hover {
background-color: #555; background-color: var(--button-background-hover);
color: white;
} }
.options { .options {

View File

@ -13,6 +13,7 @@
--main-header-color-roles: #282237; --main-header-color-roles: #282237;
--main-fg-color: #dddddd; --main-fg-color: #dddddd;
--main-link-color: #999; --main-link-color: #999;
--main-link-color-hover: #bbb;
--main-visited-color: #888; --main-visited-color: #888;
--border-color: #505050; --border-color: #505050;
--border-width: 2px; --border-width: 2px;
@ -25,16 +26,17 @@
--font-size3: 38px; --font-size3: 38px;
--font-size4: 22px; --font-size4: 22px;
--font-size5: 20px; --font-size5: 20px;
--font-size-likes: 20px;
--font-size-likes-mobile: 32px;
--font-size-pgp-key: 16px; --font-size-pgp-key: 16px;
--font-size-pgp-key2: 8px; --font-size-pgp-key2: 8px;
--font-size-tox: 16px; --font-size-tox: 16px;
--font-size-tox2: 8px; --font-size-tox2: 8px;
--text-entry-foreground: #ccc;
--text-entry-background: #111;
--time-color: #aaa; --time-color: #aaa;
--time-vertical-align: 4px; --time-vertical-align: 4px;
--button-text: #FFFFFF; --button-text: #FFFFFF;
--button-background: #999; --button-background: #999;
--button-background-hover: #777;
--button-selected: #666; --button-selected: #666;
--button-highlighted: green; --button-highlighted: green;
--button-fg-highlighted: #FFFFFF; --button-fg-highlighted: #FFFFFF;
@ -101,6 +103,18 @@ a:link {
font-weight: bold; font-weight: bold;
} }
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
.buttonevent:hover {
filter: brightness(150%);
}
a:focus { a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
@ -214,6 +228,14 @@ a:focus {
width: 10%; width: 10%;
} }
.container img.timelineicon:hover {
filter: brightness(150%);
}
.buttonunfollow:hover {
background-color: var(--button-background-hover);
}
.followRequestHandle { .followRequestHandle {
padding: 0px 20px; padding: 0px 20px;
} }
@ -236,13 +258,12 @@ a:focus {
transition: 0.5s; transition: 0.5s;
} }
.button:hover span { .button:hover {
padding-right: 25px; background-color: var(--button-background-hover);
} }
.button:hover span:after { .donateButton:hover {
opacity: 1; background-color: var(--button-background-hover);
right: 0;
} }
.buttonselected span { .buttonselected span {
@ -263,13 +284,8 @@ a:focus {
transition: 0.5s; transition: 0.5s;
} }
.buttonselected:hover span { .buttonselected:hover {
padding-right: 25px; background-color: var(--button-background-hover);
}
.buttonselected:hover span:after {
opacity: 1;
right: 0;
} }
.container { .container {
@ -406,6 +422,10 @@ a:focus {
margin-right: 0; margin-right: 0;
} }
.containericons img:hover {
filter: brightness(150%);
}
.post-title { .post-title {
margin-top: 0px; margin-top: 0px;
color: #444; color: #444;
@ -547,6 +567,10 @@ input[type=submit]:hover {
padding: 0px 0px; padding: 0px 0px;
} }
.timeline-avatar:hover {
filter: brightness(120%);
}
.timeline-avatar-reply { .timeline-avatar-reply {
padding: 0px 0px; padding: 0px 0px;
width: 80%; width: 80%;
@ -860,6 +884,14 @@ aside .toggle-inside li {
} }
@media screen and (min-width: 400px) { @media screen and (min-width: 400px) {
.likesCount {
font-size: var(--font-size-likes);
font-family: Arial, Helvetica, sans-serif;
float: right;
padding: 10px 0;
transform: translateX(-10px);
font-weight: bold;
}
.container p.administeredby { .container p.administeredby {
font-size: var(--font-size-header); font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -1301,6 +1333,14 @@ aside .toggle-inside li {
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.likesCount {
font-size: var(--font-size-likes-mobile);
font-family: Arial, Helvetica, sans-serif;
float: right;
padding: 32px 0;
transform: translateX(-20px);
font-weight: bold;
}
.container p.administeredby { .container p.administeredby {
font-size: var(--font-size-tox2); font-size: var(--font-size-tox2);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;

View File

@ -5,6 +5,7 @@
--link-bg-color: #282c37; --link-bg-color: #282c37;
--main-fg-color: #dddddd; --main-fg-color: #dddddd;
--main-link-color: #999; --main-link-color: #999;
--main-link-color-hover: #bbb;
--main-visited-color: #888; --main-visited-color: #888;
--border-color: #505050; --border-color: #505050;
--font-size-header: 18px; --font-size-header: 18px;
@ -18,6 +19,7 @@
--text-entry-background: #111; --text-entry-background: #111;
--time-color: #aaa; --time-color: #aaa;
--button-text: #FFFFFF; --button-text: #FFFFFF;
--button-background-hover: #777;
--button-background: #999; --button-background: #999;
--button-selected: #666; --button-selected: #666;
--hashtag-margin: 2%; --hashtag-margin: 2%;
@ -68,6 +70,14 @@ a:link {
font-weight: bold; font-weight: bold;
} }
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus { a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
@ -80,7 +90,7 @@ a:focus {
} }
.follow { .follow {
background-image: url("search-background.jpg"); background-image: url("follow-background.jpg");
background-size: cover; background-size: cover;
-webkit-background-size: cover; -webkit-background-size: cover;
-moz-background-size: cover; -moz-background-size: cover;
@ -141,8 +151,7 @@ a:focus {
} }
.button:hover { .button:hover {
background-color: #555; background-color: var(--button-background-hover);
color: white;
} }
input[type=text] { input[type=text] {

View File

@ -42,14 +42,19 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
indicating whether to receive calendar events from that account indicating whether to receive calendar events from that account
""" """
# check that a following file exists # check that a following file exists
if ':' in domain:
domain = domain.split(':')[0]
followingFilename = baseDir + '/accounts/' + \ followingFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/following.txt' nickname + '@' + domain + '/following.txt'
if not os.path.isfile(followingFilename): if not os.path.isfile(followingFilename):
print("WARN: following.txt doesn't exist for " +
nickname + '@' + domain)
return return
handle = followingNickname + '@' + followingDomain handle = followingNickname + '@' + followingDomain
# check that you are following this handle # check that you are following this handle
if handle + '\n' not in open(followingFilename).read(): if handle + '\n' not in open(followingFilename).read():
print('WARN: ' + handle + ' is not in ' + followingFilename)
return return
calendarFilename = baseDir + '/accounts/' + \ calendarFilename = baseDir + '/accounts/' + \
@ -59,17 +64,22 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
# a set of handles # a set of handles
followingHandles = '' followingHandles = ''
if os.path.isfile(calendarFilename): if os.path.isfile(calendarFilename):
print('Calendar file exists')
with open(calendarFilename, 'r') as calendarFile: with open(calendarFilename, 'r') as calendarFile:
followingHandles = calendarFile.read() followingHandles = calendarFile.read()
else: else:
# create a new calendar file from the following file # create a new calendar file from the following file
print('Creating calendar file ' + calendarFilename)
followingHandles = ''
with open(followingFilename, 'r') as followingFile: with open(followingFilename, 'r') as followingFile:
followingHandles = followingFile.read() followingHandles = followingFile.read()
if add:
with open(calendarFilename, 'w+') as fp: with open(calendarFilename, 'w+') as fp:
fp.write(followingHandles) fp.write(followingHandles + handle + '\n')
# already in the calendar file? # already in the calendar file?
if handle + '\n' in followingHandles: if handle + '\n' in followingHandles:
print(handle + ' exists in followingCalendar.txt')
if add: if add:
# already added # already added
return return
@ -78,6 +88,7 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
with open(calendarFilename, 'w+') as fp: with open(calendarFilename, 'w+') as fp:
fp.write(followingHandles) fp.write(followingHandles)
else: else:
print(handle + ' not in followingCalendar.txt')
# not already in the calendar file # not already in the calendar file
if add: if add:
# append to the list of handles # append to the list of handles

View File

@ -28,6 +28,8 @@ from utils import deletePost
from utils import removeModerationPostFromIndex from utils import removeModerationPostFromIndex
from utils import loadJson from utils import loadJson
from utils import saveJson from utils import saveJson
from utils import updateLikesCollection
from utils import undoLikesCollectionEntry
from httpsig import verifyPostHeaders from httpsig import verifyPostHeaders
from session import createSession from session import createSession
from session import getJson from session import getJson
@ -41,8 +43,6 @@ from acceptreject import receiveAcceptReject
from capabilities import getOcapFilename from capabilities import getOcapFilename
from capabilities import CapablePost from capabilities import CapablePost
from capabilities import capabilitiesReceiveUpdate from capabilities import capabilitiesReceiveUpdate
from like import updateLikesCollection
from like import undoLikesCollectionEntry
from bookmarks import updateBookmarksCollection from bookmarks import updateBookmarksCollection
from bookmarks import undoBookmarksCollectionEntry from bookmarks import undoBookmarksCollectionEntry
from blocking import isBlocked from blocking import isBlocked
@ -319,6 +319,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
postDomain = None postDomain = None
actor = None actor = None
if postJsonObject.get('actor'): if postJsonObject.get('actor'):
if not isinstance(postJsonObject['actor'], str):
return None
actor = postJsonObject['actor'] actor = postJsonObject['actor']
postNickname = getNicknameFromActor(postJsonObject['actor']) postNickname = getNicknameFromActor(postJsonObject['actor'])
if not postNickname: if not postNickname:
@ -371,6 +373,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
return None return None
originalPostId = None originalPostId = None
if postJsonObject.get('id'): if postJsonObject.get('id'):
if not isinstance(postJsonObject['id'], str):
return None
originalPostId = removeIdEnding(postJsonObject['id']) originalPostId = removeIdEnding(postJsonObject['id'])
currTime = datetime.datetime.utcnow() currTime = datetime.datetime.utcnow()

View File

@ -25,7 +25,7 @@ def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool:
""" """
if noOfLikes(postJsonObject) == 0: if noOfLikes(postJsonObject) == 0:
return False return False
actorMatch = domain+'/users/'+nickname actorMatch = domain + '/users/' + nickname
for item in postJsonObject['object']['likes']['items']: for item in postJsonObject['object']['likes']['items']:
if item['actor'].endswith(actorMatch): if item['actor'].endswith(actorMatch):
return True return True
@ -70,7 +70,7 @@ def like(recentPostsCache: {},
if port: if port:
if port != 80 and port != 443: if port != 80 and port != 443:
if ':' not in domain: if ':' not in domain:
fullDomain = domain+':'+str(port) fullDomain = domain + ':' + str(port)
newLikeJson = { newLikeJson = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
@ -174,7 +174,7 @@ def undolike(recentPostsCache: {},
newUndoLikeJson = { newUndoLikeJson = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
'type': 'Undo', 'type': 'Undo',
'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname, 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
'object': { 'object': {
'type': 'Like', 'type': 'Like',
'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
@ -476,4 +476,4 @@ def outboxUndoLike(recentPostsCache: {},
messageId, messageJson['actor'], messageId, messageJson['actor'],
domain, debug) domain, debug)
if debug: if debug:
print('DEBUG: post undo liked via c2s - '+postFilename) print('DEBUG: post undo liked via c2s - ' + postFilename)

View File

@ -56,7 +56,7 @@ def getImageHash(imageFilename: str) -> str:
def isMedia(imageFilename: str) -> bool: def isMedia(imageFilename: str) -> bool:
permittedMedia = ('png', 'jpg', 'gif', 'webp', permittedMedia = ('png', 'jpg', 'gif', 'webp', 'avif',
'mp4', 'ogv', 'mp3', 'ogg') 'mp4', 'ogv', 'mp3', 'ogg')
for m in permittedMedia: for m in permittedMedia:
if imageFilename.endswith('.' + m): if imageFilename.endswith('.' + m):
@ -84,7 +84,7 @@ def getAttachmentMediaType(filename: str) -> str:
""" """
mediaType = None mediaType = None
imageTypes = ('png', 'jpg', 'jpeg', imageTypes = ('png', 'jpg', 'jpeg',
'gif', 'webp') 'gif', 'webp', 'avif')
for mType in imageTypes: for mType in imageTypes:
if filename.endswith('.' + mType): if filename.endswith('.' + mType):
return 'image' return 'image'
@ -143,7 +143,7 @@ def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int,
return postJson return postJson
fileExtension = None fileExtension = None
acceptedTypes = ('png', 'jpg', 'gif', 'webp', acceptedTypes = ('png', 'jpg', 'gif', 'webp', 'avif',
'mp4', 'webm', 'ogv', 'mp3', 'ogg') 'mp4', 'webm', 'ogv', 'mp3', 'ogg')
for mType in acceptedTypes: for mType in acceptedTypes:
if imageFilename.endswith('.' + mType): if imageFilename.endswith('.' + mType):

View File

@ -122,6 +122,8 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
fileExtension = 'gif' fileExtension = 'gif'
elif mediaTypeStr.endswith('webp'): elif mediaTypeStr.endswith('webp'):
fileExtension = 'webp' fileExtension = 'webp'
elif mediaTypeStr.endswith('avif'):
fileExtension = 'avif'
elif mediaTypeStr.endswith('audio/mpeg'): elif mediaTypeStr.endswith('audio/mpeg'):
fileExtension = 'mp3' fileExtension = 'mp3'
elif mediaTypeStr.endswith('ogg'): elif mediaTypeStr.endswith('ogg'):

View File

@ -890,7 +890,10 @@ def removeTagsForNickname(baseDir: str, nickname: str,
filename = os.fsdecode(f) filename = os.fsdecode(f)
if not filename.endswith(".txt"): if not filename.endswith(".txt"):
continue continue
tagFilename = os.path.join(directory, filename) try:
tagFilename = os.path.join(directory, filename)
except BaseException:
continue
if not os.path.isfile(tagFilename): if not os.path.isfile(tagFilename):
continue continue
if matchStr not in open(tagFilename).read(): if matchStr not in open(tagFilename).read():

View File

@ -54,6 +54,7 @@ from person import setBio
from skills import setSkillLevel from skills import setSkillLevel
from roles import setRole from roles import setRole
from roles import outboxDelegate from roles import outboxDelegate
from auth import constantTimeStringCheck
from auth import createBasicAuthHeader from auth import createBasicAuthHeader
from auth import authorizeBasic from auth import authorizeBasic
from auth import storeBasicCredentials from auth import storeBasicCredentials
@ -777,12 +778,21 @@ def testFollowBetweenServers():
bobDomain + '/followers.txt'): bobDomain + '/followers.txt'):
if os.path.isfile(aliceDir + '/accounts/alice@' + if os.path.isfile(aliceDir + '/accounts/alice@' +
aliceDomain + '/following.txt'): aliceDomain + '/following.txt'):
break if os.path.isfile(aliceDir + '/accounts/alice@' +
aliceDomain + '/followingCalendar.txt'):
break
time.sleep(1) time.sleep(1)
assert validInbox(bobDir, 'bob', bobDomain) assert validInbox(bobDir, 'bob', bobDomain)
assert validInboxFilenames(bobDir, 'bob', bobDomain, assert validInboxFilenames(bobDir, 'bob', bobDomain,
aliceDomain, alicePort) aliceDomain, alicePort)
assert 'alice@' + aliceDomain in open(bobDir + '/accounts/bob@' +
bobDomain + '/followers.txt').read()
assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' +
aliceDomain + '/following.txt').read()
assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' +
aliceDomain +
'/followingCalendar.txt').read()
print('\n\n*********************************************************') print('\n\n*********************************************************')
print('Alice sends a message to Bob') print('Alice sends a message to Bob')
@ -828,11 +838,6 @@ def testFollowBetweenServers():
thrBob.join() thrBob.join()
assert thrBob.isAlive() is False assert thrBob.isAlive() is False
assert 'alice@' + aliceDomain in open(bobDir + '/accounts/bob@' +
bobDomain + '/followers.txt').read()
assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' +
aliceDomain + '/following.txt').read()
# queue item removed # queue item removed
time.sleep(4) time.sleep(4)
assert len([name for name in os.listdir(queuePath) assert len([name for name in os.listdir(queuePath)
@ -2081,8 +2086,47 @@ def testTranslations():
assert langJson.get(englishStr) assert langJson.get(englishStr)
def testConstantTimeStringCheck():
print('testConstantTimeStringCheck')
assert constantTimeStringCheck('testing', 'testing')
assert not constantTimeStringCheck('testing', '1234')
assert not constantTimeStringCheck('testing', '1234567')
itterations = 256
start = time.time()
for timingTest in range(itterations):
constantTimeStringCheck('nnjfbefefbsnjsdnvbcueftqfeuqfbqefnjeniwufgy',
'nnjfbefefbsnjsdnvbcueftqfeuqfbqefnjeniwufgy')
end = time.time()
avTime1 = ((end - start) * 1000000 / itterations)
# change a single character and observe timing difference
start = time.time()
for timingTest in range(itterations):
constantTimeStringCheck('nnjfbefefbsnjsdnvbcueftqfeuqfbqefnjeniwufgy',
'nnjfbefefbsnjsdnvbcueftqfeuqfbqeznjeniwufgy')
end = time.time()
avTime2 = ((end - start) * 1000000 / itterations)
timeDiffMicroseconds = abs(avTime2 - avTime1)
# time difference should be less than 10uS
assert timeDiffMicroseconds < 10
# change multiple characters and observe timing difference
start = time.time()
for timingTest in range(itterations):
constantTimeStringCheck('nnjfbefefbsnjsdnvbcueftqfeuqfbqefnjeniwufgy',
'ano1befffbsn7sd3vbluef6qseuqfpqeznjgni9bfgi')
end = time.time()
avTime2 = ((end - start) * 1000000 / itterations)
timeDiffMicroseconds = abs(avTime2 - avTime1)
# time difference should be less than 10uS
assert timeDiffMicroseconds < 10
def runAllTests(): def runAllTests():
print('Running tests...') print('Running tests...')
testConstantTimeStringCheck()
testTranslations() testTranslations()
testValidContentWarning() testValidContentWarning()
testRemoveIdEnding() testRemoveIdEnding()

View File

@ -294,7 +294,8 @@ def setThemeNight(baseDir: str):
"main-bg-color-report": "#0f0d10", "main-bg-color-report": "#0f0d10",
"hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px", "hashtag-vertical-spacing4": "150px",
"button-background": "#7961ab", "button-background-hover": "#6961ab",
"button-background": "#a961ab",
"button-selected": "#86579d", "button-selected": "#86579d",
"calendar-bg-color": "#0f0d10", "calendar-bg-color": "#0f0d10",
"lines-color": "#7961ab", "lines-color": "#7961ab",
@ -332,6 +333,7 @@ def setThemeStarlight(baseDir: str):
"text-entry-background": "#0f0d10", "text-entry-background": "#0f0d10",
"link-bg-color": "#0f0d10", "link-bg-color": "#0f0d10",
"main-link-color": "#ffc4bc", "main-link-color": "#ffc4bc",
"main-link-color-hover": "white",
"title-color": "#ffc4bc", "title-color": "#ffc4bc",
"main-visited-color": "#e1c4bc", "main-visited-color": "#e1c4bc",
"main-fg-color": "#ffc4bc", "main-fg-color": "#ffc4bc",
@ -342,6 +344,7 @@ def setThemeStarlight(baseDir: str):
"main-bg-color-report": "#0f0d10", "main-bg-color-report": "#0f0d10",
"hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px", "hashtag-vertical-spacing4": "150px",
"button-background-hover": "#a9282c",
"button-background": "#69282c", "button-background": "#69282c",
"button-small-background": "darkblue", "button-small-background": "darkblue",
"button-selected": "#a34046", "button-selected": "#a34046",
@ -388,6 +391,7 @@ def setThemeHenge(baseDir: str):
"text-entry-background": "#383335", "text-entry-background": "#383335",
"link-bg-color": "#383335", "link-bg-color": "#383335",
"main-link-color": "white", "main-link-color": "white",
"main-link-color-hover": "#ddd",
"title-color": "white", "title-color": "white",
"main-visited-color": "#e1c4bc", "main-visited-color": "#e1c4bc",
"main-fg-color": "white", "main-fg-color": "white",
@ -398,6 +402,7 @@ def setThemeHenge(baseDir: str):
"main-bg-color-report": "#383335", "main-bg-color-report": "#383335",
"hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px", "hashtag-vertical-spacing4": "150px",
"button-background-hover": "#444",
"button-background": "#222", "button-background": "#222",
"button-selected": "black", "button-selected": "black",
"dropdown-fg-color": "#dddddd", "dropdown-fg-color": "#dddddd",
@ -439,8 +444,10 @@ def setThemeZen(baseDir: str):
"border-color": "#463b35", "border-color": "#463b35",
"border-width": "7px", "border-width": "7px",
"main-link-color": "#dddddd", "main-link-color": "#dddddd",
"main-link-color-hover": "white",
"title-color": "#dddddd", "title-color": "#dddddd",
"main-visited-color": "#dddddd", "main-visited-color": "#dddddd",
"button-background-hover": "#a63b35",
"button-background": "#463b35", "button-background": "#463b35",
"button-selected": "#26201d", "button-selected": "#26201d",
"main-bg-color-dm": "#5c4a40", "main-bg-color-dm": "#5c4a40",
@ -499,10 +506,12 @@ def setThemeLCD(baseDir: str):
"border-color": "#33390d", "border-color": "#33390d",
"border-width": "5px", "border-width": "5px",
"main-link-color": "#9fb42b", "main-link-color": "#9fb42b",
"main-link-color-hover": "#cfb42b",
"title-color": "#9fb42b", "title-color": "#9fb42b",
"main-visited-color": "#9fb42b", "main-visited-color": "#9fb42b",
"button-selected": "black", "button-selected": "black",
"button-highlighted": "green", "button-highlighted": "green",
"button-background-hover": "#a3390d",
"button-background": "#33390d", "button-background": "#33390d",
"button-small-background": "#33390d", "button-small-background": "#33390d",
"button-text": "#9fb42b", "button-text": "#9fb42b",
@ -569,9 +578,11 @@ def setThemePurple(baseDir: str):
"main-fg-color": "#f98bb0", "main-fg-color": "#f98bb0",
"border-color": "#3f2145", "border-color": "#3f2145",
"main-link-color": "#ff42a0", "main-link-color": "#ff42a0",
"main-link-color-hover": "white",
"title-color": "white", "title-color": "white",
"main-visited-color": "#f93bb0", "main-visited-color": "#f93bb0",
"button-selected": "#c042a0", "button-selected": "#c042a0",
"button-background-hover": "#af42a0",
"button-background": "#ff42a0", "button-background": "#ff42a0",
"button-small-background": "#ff42a0", "button-small-background": "#ff42a0",
"button-text": "white", "button-text": "white",
@ -616,9 +627,11 @@ def setThemeHacker(baseDir: str):
"main-fg-color": "#00ff00", "main-fg-color": "#00ff00",
"border-color": "#035103", "border-color": "#035103",
"main-link-color": "#2fff2f", "main-link-color": "#2fff2f",
"main-link-color-hover": "#afff2f",
"title-color": "#2fff2f", "title-color": "#2fff2f",
"main-visited-color": "#3c8234", "main-visited-color": "#3c8234",
"button-selected": "#063200", "button-selected": "#063200",
"button-background-hover": "#a62200",
"button-background": "#062200", "button-background": "#062200",
"button-small-background": "#062200", "button-small-background": "#062200",
"button-text": "#00ff00", "button-text": "#00ff00",
@ -673,6 +686,7 @@ def setThemeLight(baseDir: str):
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"border-color": "#c0cdd9", "border-color": "#c0cdd9",
"main-link-color": "#2a2c37", "main-link-color": "#2a2c37",
"main-link-color-hover": "#aa2c37",
"title-color": "#2a2c37", "title-color": "#2a2c37",
"main-visited-color": "#232c37", "main-visited-color": "#232c37",
"text-entry-foreground": "#111", "text-entry-foreground": "#111",
@ -728,6 +742,7 @@ def setThemeSolidaric(baseDir: str):
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"border-color": "#c0cdd9", "border-color": "#c0cdd9",
"main-link-color": "#2a2c37", "main-link-color": "#2a2c37",
"main-link-color-hover": "#aa2c37",
"title-color": "#2a2c37", "title-color": "#2a2c37",
"main-visited-color": "#232c37", "main-visited-color": "#232c37",
"text-entry-foreground": "#111", "text-entry-foreground": "#111",
@ -786,7 +801,7 @@ def setThemeImages(baseDir: str, name: str) -> None:
backgroundNames = ('login', 'shares', 'delete', 'follow', backgroundNames = ('login', 'shares', 'delete', 'follow',
'options', 'block', 'search', 'calendar') 'options', 'block', 'search', 'calendar')
extensions = ('webp', 'gif', 'jpg', 'png') extensions = ('webp', 'gif', 'jpg', 'png', 'avif')
for subdir, dirs, files in os.walk(baseDir + '/accounts'): for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs: for acct in dirs:

View File

@ -51,7 +51,7 @@ def removeAvatarFromCache(baseDir: str, actorStr: str) -> None:
"""Removes any existing avatar entries from the cache """Removes any existing avatar entries from the cache
This avoids duplicate entries with differing extensions This avoids duplicate entries with differing extensions
""" """
avatarFilenameExtensions = ('png', 'jpg', 'gif', 'webp') avatarFilenameExtensions = ('png', 'jpg', 'gif', 'webp', 'avif')
for extension in avatarFilenameExtensions: for extension in avatarFilenameExtensions:
avatarFilename = \ avatarFilename = \
baseDir + '/cache/avatars/' + actorStr + '.' + extension baseDir + '/cache/avatars/' + actorStr + '.' + extension
@ -369,24 +369,27 @@ def followPerson(baseDir: str, nickname: str, domain: str,
content = f.read() content = f.read()
f.seek(0, 0) f.seek(0, 0)
f.write(handleToFollow + '\n' + content) f.write(handleToFollow + '\n' + content)
if debug: print('DEBUG: follow added')
print('DEBUG: follow added')
except Exception as e: except Exception as e:
print('WARN: Failed to write entry to follow file ' + print('WARN: Failed to write entry to follow file ' +
filename + ' ' + str(e)) filename + ' ' + str(e))
else: else:
# first follow # first follow
if debug: if debug:
print('DEBUG: creating new following file to follow ' + print('DEBUG: ' + handle +
handleToFollow) ' creating new following file to follow ' + handleToFollow +
', filename is ' + filename)
with open(filename, 'w+') as f: with open(filename, 'w+') as f:
f.write(handleToFollow + '\n') f.write(handleToFollow + '\n')
# Default to adding new follows to the calendar. # Default to adding new follows to the calendar.
# Possibly this could be made optional # Possibly this could be made optional
if followFile == 'following.txt': if followFile.endswith('following.txt'):
# if following a person add them to the list of # if following a person add them to the list of
# calendar follows # calendar follows
print('DEBUG: adding ' +
followNickname + '@' + followDomain + ' to calendar of ' +
nickname + '@' + domain)
addPersonToCalendar(baseDir, nickname, domain, addPersonToCalendar(baseDir, nickname, domain,
followNickname, followDomain) followNickname, followDomain)
return True return True
@ -728,11 +731,9 @@ def getCachedPostFilename(baseDir: str, nickname: str, domain: str,
if '@' not in cachedPostDir: if '@' not in cachedPostDir:
# print('ERROR: invalid html cache directory '+cachedPostDir) # print('ERROR: invalid html cache directory '+cachedPostDir)
return None return None
cachedPostFilename = \ cachedPostId = removeIdEnding(postJsonObject['id'])
cachedPostDir + \ cachedPostFilename = cachedPostDir + '/' + cachedPostId.replace('/', '#')
'/' + removeIdEnding(postJsonObject['id']).replace('/', '#') return cachedPostFilename + '.html'
cachedPostFilename = cachedPostFilename + '.html'
return cachedPostFilename
def removePostFromCache(postJsonObject: {}, recentPostsCache: {}): def removePostFromCache(postJsonObject: {}, recentPostsCache: {}):

View File

@ -230,6 +230,11 @@ def updateAvatarImageCache(session, baseDir: str, httpPrefix: str,
'Accept': 'image/webp' 'Accept': 'image/webp'
} }
avatarImageFilename = avatarImagePath + '.webp' avatarImageFilename = avatarImagePath + '.webp'
elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl:
sessionHeaders = {
'Accept': 'image/avif'
}
avatarImageFilename = avatarImagePath + '.avif'
else: else:
return None return None
@ -304,7 +309,7 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {},
actorStr = personJson['id'].replace('/', '-') actorStr = personJson['id'].replace('/', '-')
avatarImagePath = baseDir + '/cache/avatars/' + actorStr avatarImagePath = baseDir + '/cache/avatars/' + actorStr
imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp') imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif')
for ext in imageExtension: for ext in imageExtension:
if os.path.isfile(avatarImagePath + '.' + ext): if os.path.isfile(avatarImagePath + '.' + ext):
return '/avatars/' + actorStr + '.' + ext return '/avatars/' + actorStr + '.' + ext
@ -1064,7 +1069,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str: domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit profile screen """Shows the edit profile screen
""" """
imageFormats = '.png, .jpg, .jpeg, .gif, .webp' imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif'
pathOriginal = path pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '') path = path.replace('/shares', '')
@ -1621,6 +1626,9 @@ def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str:
elif os.path.isfile(baseDir + '/accounts/login.webp'): elif os.path.isfile(baseDir + '/accounts/login.webp'):
loginImage = 'login.webp' loginImage = 'login.webp'
loginImageFilename = baseDir + '/accounts/' + loginImage loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.avif'):
loginImage = 'login.avif'
loginImageFilename = baseDir + '/accounts/' + loginImage
if not loginImageFilename: if not loginImageFilename:
loginImageFilename = baseDir + '/accounts/' + loginImage loginImageFilename = baseDir + '/accounts/' + loginImage
@ -1965,13 +1973,13 @@ def htmlNewPost(mediaInstance: bool, translate: {},
newPostImageSection += \ newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"' ' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \ newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp">\n' ' accept=".png, .jpg, .jpeg, .gif, .webp, .avif">\n'
else: else:
newPostImageSection += \ newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"' ' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \ newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, ' + \ ' accept=".png, .jpg, .jpeg, .gif, ' + \
'.webm, .ogv, .mp3, .ogg">\n' '.webp, .avif, .mp4, .webm, .ogv, .mp3, .ogg">\n'
newPostImageSection += ' </div>\n' newPostImageSection += ' </div>\n'
scopeIcon = 'scope_public.png' scopeIcon = 'scope_public.png'
@ -2913,7 +2921,7 @@ def htmlProfile(defaultTimeline: str,
'/followapprove=' + followerHandle + '">' '/followapprove=' + followerHandle + '">'
followApprovalsSection += \ followApprovalsSection += \
'<button class="followApprove">' + \ '<button class="followApprove">' + \
translate['Approve'] + '</button></a>' translate['Approve'] + '</button></a><br><br>'
followApprovalsSection += \ followApprovalsSection += \
'<a href="' + basePath + \ '<a href="' + basePath + \
'/followdeny=' + followerHandle + '">' '/followdeny=' + followerHandle + '">'
@ -3623,11 +3631,13 @@ def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
if mediaType == 'image/png' or \ if mediaType == 'image/png' or \
mediaType == 'image/jpeg' or \ mediaType == 'image/jpeg' or \
mediaType == 'image/webp' or \ mediaType == 'image/webp' or \
mediaType == 'image/avif' or \
mediaType == 'image/gif': mediaType == 'image/gif':
if attach['url'].endswith('.png') or \ if attach['url'].endswith('.png') or \
attach['url'].endswith('.jpg') or \ attach['url'].endswith('.jpg') or \
attach['url'].endswith('.jpeg') or \ attach['url'].endswith('.jpeg') or \
attach['url'].endswith('.webp') or \ attach['url'].endswith('.webp') or \
attach['url'].endswith('.avif') or \
attach['url'].endswith('.gif'): attach['url'].endswith('.gif'):
if attachmentCtr > 0: if attachmentCtr > 0:
attachmentStr += '<br>' attachmentStr += '<br>'
@ -4214,13 +4224,15 @@ def individualPostAsHtml(allowDownloads: bool,
likeCountStr = '' likeCountStr = ''
if likeCount > 0: if likeCount > 0:
if likeCount > 1: if likeCount <= 10:
if likeCount <= 10: likeCountStr = ' (' + str(likeCount) + ')'
likeCountStr = ' (' + str(likeCount) + ')' else:
else: likeCountStr = ' (10+)'
likeCountStr = ' (10+)'
likeIcon = 'like.png'
if likedByPerson(postJsonObject, nickname, fullDomain): if likedByPerson(postJsonObject, nickname, fullDomain):
if likeCount == 1:
# liked by the reader only
likeCountStr = ''
likeIcon = 'like.png'
likeLink = 'unlike' likeLink = 'unlike'
likeTitle = translate['Undo the like'] likeTitle = translate['Undo the like']
@ -4230,7 +4242,13 @@ def individualPostAsHtml(allowDownloads: bool,
if timeDiff > 100: if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff)) print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff))
likeStr = \ likeStr = ''
if likeCountStr:
# show the number of likes next to icon
likeStr += '<label class="likesCount">'
likeStr += likeCountStr.replace('(', '').replace(')', '').strip()
likeStr += '</label>\n'
likeStr += \
'<a class="imageAnchor" href="/users/' + nickname + '?' + \ '<a class="imageAnchor" href="/users/' + nickname + '?' + \
likeLink + '=' + postJsonObject['object']['id'] + \ likeLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \ pageNumberParam + \
@ -4914,6 +4932,10 @@ def htmlTimeline(defaultTimeline: str,
bannerFile = 'banner.gif' bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \ bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.avif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename): if not os.path.isfile(bannerFilename):
bannerFile = 'banner.webp' bannerFile = 'banner.webp'
@ -6814,8 +6836,8 @@ def htmlSearch(translate: {},
baseDir + '/accounts/search-background.png') baseDir + '/accounts/search-background.png')
cssFilename = baseDir + '/epicyon-search.css' cssFilename = baseDir + '/epicyon-search.css'
if os.path.isfile(baseDir + '/follow.css'): if os.path.isfile(baseDir + '/search.css'):
cssFilename = baseDir + '/follow.css' cssFilename = baseDir + '/search.css'
with open(cssFilename, 'r') as cssFile: with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read() profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle) followStr = htmlHeader(cssFilename, profileStyle)