mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
commit
01c34045a4
|
|
@ -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
44
auth.py
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
2
blog.py
2
blog.py
|
|
@ -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>'
|
||||||
|
|
||||||
|
|
|
||||||
61
content.py
61
content.py
|
|
@ -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
|
||||||
|
|
|
||||||
39
daemon.py
39
daemon.py
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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] {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
8
inbox.py
8
inbox.py
|
|
@ -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()
|
||||||
|
|
|
||||||
6
media.py
6
media.py
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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'):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
try:
|
||||||
tagFilename = os.path.join(directory, filename)
|
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():
|
||||||
|
|
|
||||||
54
tests.py
54
tests.py
|
|
@ -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'):
|
||||||
|
if os.path.isfile(aliceDir + '/accounts/alice@' +
|
||||||
|
aliceDomain + '/followingCalendar.txt'):
|
||||||
break
|
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()
|
||||||
|
|
|
||||||
19
theme.py
19
theme.py
|
|
@ -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:
|
||||||
|
|
|
||||||
21
utils.py
21
utils.py
|
|
@ -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,7 +369,6 @@ 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 ' +
|
||||||
|
|
@ -377,16 +376,20 @@ def followPerson(baseDir: str, nickname: str, domain: str,
|
||||||
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: {}):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue