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.
|
||||
|
||||
``` 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:
|
||||
|
|
|
|||
44
auth.py
44
auth.py
|
|
@ -24,17 +24,49 @@ def hashPassword(password: str) -> str:
|
|||
return (salt + pwdhash).decode('ascii')
|
||||
|
||||
|
||||
def verifyPassword(storedPassword: str, providedPassword: str) -> bool:
|
||||
"""Verify a stored password against one provided by user
|
||||
def getPasswordHash(salt: str, providedPassword: str) -> str:
|
||||
"""Returns the hash of a password
|
||||
"""
|
||||
salt = storedPassword[:64]
|
||||
storedPassword = storedPassword[64:]
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha512',
|
||||
providedPassword.encode('utf-8'),
|
||||
salt.encode('ascii'),
|
||||
100000)
|
||||
pwdhash = binascii.hexlify(pwdhash).decode('ascii')
|
||||
return pwdhash == storedPassword
|
||||
return binascii.hexlify(pwdhash).decode('ascii')
|
||||
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -21,18 +21,22 @@ def addGlobalBlock(baseDir: str,
|
|||
"""
|
||||
blockingFilename = baseDir + '/accounts/blocking.txt'
|
||||
if not blockNickname.startswith('#'):
|
||||
# is the handle already blocked?
|
||||
blockHandle = blockNickname + '@' + blockDomain
|
||||
if os.path.isfile(blockingFilename):
|
||||
if blockHandle in open(blockingFilename).read():
|
||||
return False
|
||||
# block an account handle or domain
|
||||
blockFile = open(blockingFilename, "a+")
|
||||
blockFile.write(blockHandle + '\n')
|
||||
blockFile.close()
|
||||
else:
|
||||
blockHashtag = blockNickname
|
||||
# is the hashtag already blocked?
|
||||
if os.path.isfile(blockingFilename):
|
||||
if blockHashtag + '\n' in open(blockingFilename).read():
|
||||
return False
|
||||
# block a hashtag
|
||||
blockFile = open(blockingFilename, "a+")
|
||||
blockFile.write(blockHashtag + '\n')
|
||||
blockFile.close()
|
||||
|
|
|
|||
2
blog.py
2
blog.py
|
|
@ -722,7 +722,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
|
|||
editBlogImageSection += \
|
||||
' <input type="file" id="attachpic" name="attachpic"'
|
||||
editBlogImageSection += \
|
||||
' accept=".png, .jpg, .jpeg, .gif, .webp, ' + \
|
||||
' accept=".png, .jpg, .jpeg, .gif, .webp, .avif, ' + \
|
||||
'.mp4, .webm, .ogv, .mp3, .ogg">'
|
||||
editBlogImageSection += ' </div>'
|
||||
|
||||
|
|
|
|||
61
content.py
61
content.py
|
|
@ -570,6 +570,41 @@ def removeLongWords(content: str, maxWordLength: int,
|
|||
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,
|
||||
nickname: str, domain: str, content: str,
|
||||
recipients: [], hashtags: {}, isJsonContent=False) -> str:
|
||||
|
|
@ -616,6 +651,9 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
|||
|
||||
# extract mentions and tags from words
|
||||
longWordsList = []
|
||||
prevWordStr = ''
|
||||
autoTagsList = loadAutoTags(baseDir, nickname, domain)
|
||||
appendTags = []
|
||||
for wordStr in words:
|
||||
wordLen = len(wordStr)
|
||||
if wordLen > 2:
|
||||
|
|
@ -625,10 +663,12 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
|||
if firstChar == '@':
|
||||
if addMention(wordStr, httpPrefix, following,
|
||||
replaceMentions, recipients, hashtags):
|
||||
prevWordStr = ''
|
||||
continue
|
||||
elif firstChar == '#':
|
||||
if addHashTags(wordStr, httpPrefix, originalDomain,
|
||||
replaceHashTags, hashtags):
|
||||
prevWordStr = ''
|
||||
continue
|
||||
elif ':' in wordStr:
|
||||
wordStr2 = wordStr.split(':')[1]
|
||||
|
|
@ -646,6 +686,24 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
|||
addEmoji(baseDir, ':' + wordStr2 + ':', httpPrefix,
|
||||
originalDomain, replaceEmoji, hashtags,
|
||||
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
|
||||
for wordStr, replaceStr in replaceMentions.items():
|
||||
|
|
@ -737,6 +795,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
|
|||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'webp': 'image/webp',
|
||||
'avif': 'image/avif',
|
||||
'mp4': 'video/mp4',
|
||||
'ogv': 'video/ogv',
|
||||
'mp3': 'audio/mpeg',
|
||||
|
|
@ -771,7 +830,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
|
|||
break
|
||||
|
||||
# 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:
|
||||
if ex == detectedExtension:
|
||||
continue
|
||||
|
|
|
|||
39
daemon.py
39
daemon.py
|
|
@ -254,6 +254,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if path.endswith('.png') or \
|
||||
path.endswith('.jpg') or \
|
||||
path.endswith('.gif') or \
|
||||
path.endswith('.avif') or \
|
||||
path.endswith('.webp'):
|
||||
return True
|
||||
return False
|
||||
|
|
@ -1407,6 +1408,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
fullBlockDomain = None
|
||||
if moderationText.startswith('http') or \
|
||||
moderationText.startswith('dat'):
|
||||
# https://domain
|
||||
blockDomain, blockPort = \
|
||||
getDomainFromActor(moderationText)
|
||||
fullBlockDomain = blockDomain
|
||||
|
|
@ -1416,13 +1418,20 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
fullBlockDomain = \
|
||||
blockDomain + ':' + str(blockPort)
|
||||
if '@' in moderationText:
|
||||
# nick@domain or *@domain
|
||||
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('#'):
|
||||
addGlobalBlock(baseDir, nickname, fullBlockDomain)
|
||||
if moderationButton == 'unblock':
|
||||
fullBlockDomain = None
|
||||
if moderationText.startswith('http') or \
|
||||
moderationText.startswith('dat'):
|
||||
# https://domain
|
||||
blockDomain, blockPort = \
|
||||
getDomainFromActor(moderationText)
|
||||
fullBlockDomain = blockDomain
|
||||
|
|
@ -1432,7 +1441,13 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
fullBlockDomain = \
|
||||
blockDomain + ':' + str(blockPort)
|
||||
if '@' in moderationText:
|
||||
# nick@domain or *@domain
|
||||
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('#'):
|
||||
removeGlobalBlock(baseDir, nickname, fullBlockDomain)
|
||||
if moderationButton == 'remove':
|
||||
|
|
@ -2508,6 +2523,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaFilename = mediaFilenameBase + '.gif'
|
||||
if self.headers['Content-type'].endswith('webp'):
|
||||
mediaFilename = mediaFilenameBase + '.webp'
|
||||
if self.headers['Content-type'].endswith('avif'):
|
||||
mediaFilename = mediaFilenameBase + '.avif'
|
||||
with open(mediaFilename, 'wb') as avFile:
|
||||
avFile.write(mediaBytes)
|
||||
if debug:
|
||||
|
|
@ -3538,6 +3555,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if 'image/webp' in self.headers['Accept']:
|
||||
favType = 'image/webp'
|
||||
favFilename = 'favicon.webp'
|
||||
if 'image/avif' in self.headers['Accept']:
|
||||
favType = 'image/avif'
|
||||
favFilename = 'favicon.avif'
|
||||
# custom favicon
|
||||
faviconFilename = baseDir + '/' + favFilename
|
||||
if not os.path.isfile(faviconFilename):
|
||||
|
|
@ -3834,6 +3854,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaFileType = 'image/gif'
|
||||
elif mediaFilename.endswith('.webp'):
|
||||
mediaFileType = 'image/webp'
|
||||
elif mediaFilename.endswith('.avif'):
|
||||
mediaFileType = 'image/avif'
|
||||
elif mediaFilename.endswith('.mp4'):
|
||||
mediaFileType = 'video/mp4'
|
||||
elif mediaFilename.endswith('.ogv'):
|
||||
|
|
@ -3876,6 +3898,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaImageType = 'jpeg'
|
||||
elif emojiFilename.endswith('.webp'):
|
||||
mediaImageType = 'webp'
|
||||
elif emojiFilename.endswith('.avif'):
|
||||
mediaImageType = 'avif'
|
||||
else:
|
||||
mediaImageType = 'gif'
|
||||
with open(emojiFilename, 'rb') as avFile:
|
||||
|
|
@ -3960,6 +3984,11 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'image/webp',
|
||||
mediaBinary, None,
|
||||
callingDomain)
|
||||
elif mediaFilename.endswith('.avif'):
|
||||
self._set_headers_etag(mediaFilename,
|
||||
'image/avif',
|
||||
mediaBinary, None,
|
||||
callingDomain)
|
||||
else:
|
||||
# default to jpeg
|
||||
self._set_headers_etag(mediaFilename,
|
||||
|
|
@ -6951,7 +6980,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
GETstartTime, GETtimings: {}) -> bool:
|
||||
"""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'):
|
||||
# follow screen background image
|
||||
if path.endswith('/' + bg + '-background.' + ext):
|
||||
|
|
@ -7014,6 +7043,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaFileType = 'jpeg'
|
||||
elif mediaFilename.endswith('.webp'):
|
||||
mediaFileType = 'webp'
|
||||
elif mediaFilename.endswith('.avif'):
|
||||
mediaFileType = 'avif'
|
||||
else:
|
||||
mediaFileType = 'gif'
|
||||
with open(mediaFilename, 'rb') as avFile:
|
||||
|
|
@ -7061,6 +7092,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaImageType = 'jpeg'
|
||||
elif avatarFile.endswith('.gif'):
|
||||
mediaImageType = 'gif'
|
||||
elif avatarFile.endswith('.avif'):
|
||||
mediaImageType = 'avif'
|
||||
else:
|
||||
mediaImageType = 'webp'
|
||||
with open(avatarFilename, 'rb') as avFile:
|
||||
|
|
@ -7805,6 +7838,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if self.path == '/login.png' or \
|
||||
self.path == '/login.gif' or \
|
||||
self.path == '/login.webp' or \
|
||||
self.path == '/login.avif' or \
|
||||
self.path == '/login.jpeg' or \
|
||||
self.path == '/login.jpg' or \
|
||||
self.path == '/qrcode.png':
|
||||
|
|
@ -8991,6 +9025,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mediaFileType = 'image/gif'
|
||||
elif checkPath.endswith('.webp'):
|
||||
mediaFileType = 'image/webp'
|
||||
elif checkPath.endswith('.avif'):
|
||||
mediaFileType = 'image/avif'
|
||||
elif checkPath.endswith('.mp4'):
|
||||
mediaFileType = 'video/mp4'
|
||||
elif checkPath.endswith('.ogv'):
|
||||
|
|
@ -9065,6 +9101,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if filename.endswith('.png') or \
|
||||
filename.endswith('.jpg') or \
|
||||
filename.endswith('.webp') or \
|
||||
filename.endswith('.avif') or \
|
||||
filename.endswith('.gif'):
|
||||
if self.server.debug:
|
||||
print('DEBUG: POST media removing metadata')
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
--time-color: #aaa;
|
||||
--button-text: #FFFFFF;
|
||||
--button-small-text: #FFFFFF;
|
||||
--button-background-hover: #777;
|
||||
--button-background: #999;
|
||||
--button-small-background: #999;
|
||||
--button-selected: #666;
|
||||
--hashtag-margin: 2%;
|
||||
--hashtag-vertical-spacing1: 50px;
|
||||
--hashtag-vertical-spacing2: 100px;
|
||||
|
|
@ -107,8 +107,7 @@ a:focus {
|
|||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #555;
|
||||
color: white;
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
.options {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
--main-header-color-roles: #282237;
|
||||
--main-fg-color: #dddddd;
|
||||
--main-link-color: #999;
|
||||
--main-link-color-hover: #bbb;
|
||||
--main-visited-color: #888;
|
||||
--border-color: #505050;
|
||||
--border-width: 2px;
|
||||
|
|
@ -25,16 +26,17 @@
|
|||
--font-size3: 38px;
|
||||
--font-size4: 22px;
|
||||
--font-size5: 20px;
|
||||
--font-size-likes: 20px;
|
||||
--font-size-likes-mobile: 32px;
|
||||
--font-size-pgp-key: 16px;
|
||||
--font-size-pgp-key2: 8px;
|
||||
--font-size-tox: 16px;
|
||||
--font-size-tox2: 8px;
|
||||
--text-entry-foreground: #ccc;
|
||||
--text-entry-background: #111;
|
||||
--time-color: #aaa;
|
||||
--time-vertical-align: 4px;
|
||||
--button-text: #FFFFFF;
|
||||
--button-background: #999;
|
||||
--button-background-hover: #777;
|
||||
--button-selected: #666;
|
||||
--button-highlighted: green;
|
||||
--button-fg-highlighted: #FFFFFF;
|
||||
|
|
@ -101,6 +103,18 @@ a:link {
|
|||
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 {
|
||||
border: 2px solid var(--focus-color);
|
||||
}
|
||||
|
|
@ -214,6 +228,14 @@ a:focus {
|
|||
width: 10%;
|
||||
}
|
||||
|
||||
.container img.timelineicon:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.buttonunfollow:hover {
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
.followRequestHandle {
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
|
@ -236,13 +258,12 @@ a:focus {
|
|||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.button:hover span {
|
||||
padding-right: 25px;
|
||||
.button:hover {
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
.button:hover span:after {
|
||||
opacity: 1;
|
||||
right: 0;
|
||||
.donateButton:hover {
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
.buttonselected span {
|
||||
|
|
@ -263,13 +284,8 @@ a:focus {
|
|||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.buttonselected:hover span {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.buttonselected:hover span:after {
|
||||
opacity: 1;
|
||||
right: 0;
|
||||
.buttonselected:hover {
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
|
@ -406,6 +422,10 @@ a:focus {
|
|||
margin-right: 0;
|
||||
}
|
||||
|
||||
.containericons img:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-top: 0px;
|
||||
color: #444;
|
||||
|
|
@ -547,6 +567,10 @@ input[type=submit]:hover {
|
|||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
.timeline-avatar:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.timeline-avatar-reply {
|
||||
padding: 0px 0px;
|
||||
width: 80%;
|
||||
|
|
@ -860,6 +884,14 @@ aside .toggle-inside li {
|
|||
}
|
||||
|
||||
@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 {
|
||||
font-size: var(--font-size-header);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
@ -1301,6 +1333,14 @@ aside .toggle-inside li {
|
|||
}
|
||||
|
||||
@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 {
|
||||
font-size: var(--font-size-tox2);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
--link-bg-color: #282c37;
|
||||
--main-fg-color: #dddddd;
|
||||
--main-link-color: #999;
|
||||
--main-link-color-hover: #bbb;
|
||||
--main-visited-color: #888;
|
||||
--border-color: #505050;
|
||||
--font-size-header: 18px;
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
--text-entry-background: #111;
|
||||
--time-color: #aaa;
|
||||
--button-text: #FFFFFF;
|
||||
--button-background-hover: #777;
|
||||
--button-background: #999;
|
||||
--button-selected: #666;
|
||||
--hashtag-margin: 2%;
|
||||
|
|
@ -68,6 +70,14 @@ a:link {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
border: 2px solid var(--focus-color);
|
||||
}
|
||||
|
|
@ -80,7 +90,7 @@ a:focus {
|
|||
}
|
||||
|
||||
.follow {
|
||||
background-image: url("search-background.jpg");
|
||||
background-image: url("follow-background.jpg");
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
|
|
@ -141,8 +151,7 @@ a:focus {
|
|||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #555;
|
||||
color: white;
|
||||
background-color: var(--button-background-hover);
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
|
|
|
|||
|
|
@ -42,14 +42,19 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
|
|||
indicating whether to receive calendar events from that account
|
||||
"""
|
||||
# check that a following file exists
|
||||
if ':' in domain:
|
||||
domain = domain.split(':')[0]
|
||||
followingFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/following.txt'
|
||||
if not os.path.isfile(followingFilename):
|
||||
print("WARN: following.txt doesn't exist for " +
|
||||
nickname + '@' + domain)
|
||||
return
|
||||
handle = followingNickname + '@' + followingDomain
|
||||
|
||||
# check that you are following this handle
|
||||
if handle + '\n' not in open(followingFilename).read():
|
||||
print('WARN: ' + handle + ' is not in ' + followingFilename)
|
||||
return
|
||||
|
||||
calendarFilename = baseDir + '/accounts/' + \
|
||||
|
|
@ -59,17 +64,22 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
|
|||
# a set of handles
|
||||
followingHandles = ''
|
||||
if os.path.isfile(calendarFilename):
|
||||
print('Calendar file exists')
|
||||
with open(calendarFilename, 'r') as calendarFile:
|
||||
followingHandles = calendarFile.read()
|
||||
else:
|
||||
# create a new calendar file from the following file
|
||||
print('Creating calendar file ' + calendarFilename)
|
||||
followingHandles = ''
|
||||
with open(followingFilename, 'r') as followingFile:
|
||||
followingHandles = followingFile.read()
|
||||
if add:
|
||||
with open(calendarFilename, 'w+') as fp:
|
||||
fp.write(followingHandles)
|
||||
fp.write(followingHandles + handle + '\n')
|
||||
|
||||
# already in the calendar file?
|
||||
if handle + '\n' in followingHandles:
|
||||
print(handle + ' exists in followingCalendar.txt')
|
||||
if add:
|
||||
# already added
|
||||
return
|
||||
|
|
@ -78,6 +88,7 @@ def receiveCalendarEvents(baseDir: str, nickname: str, domain: str,
|
|||
with open(calendarFilename, 'w+') as fp:
|
||||
fp.write(followingHandles)
|
||||
else:
|
||||
print(handle + ' not in followingCalendar.txt')
|
||||
# not already in the calendar file
|
||||
if add:
|
||||
# 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 loadJson
|
||||
from utils import saveJson
|
||||
from utils import updateLikesCollection
|
||||
from utils import undoLikesCollectionEntry
|
||||
from httpsig import verifyPostHeaders
|
||||
from session import createSession
|
||||
from session import getJson
|
||||
|
|
@ -41,8 +43,6 @@ from acceptreject import receiveAcceptReject
|
|||
from capabilities import getOcapFilename
|
||||
from capabilities import CapablePost
|
||||
from capabilities import capabilitiesReceiveUpdate
|
||||
from like import updateLikesCollection
|
||||
from like import undoLikesCollectionEntry
|
||||
from bookmarks import updateBookmarksCollection
|
||||
from bookmarks import undoBookmarksCollectionEntry
|
||||
from blocking import isBlocked
|
||||
|
|
@ -319,6 +319,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
|
|||
postDomain = None
|
||||
actor = None
|
||||
if postJsonObject.get('actor'):
|
||||
if not isinstance(postJsonObject['actor'], str):
|
||||
return None
|
||||
actor = postJsonObject['actor']
|
||||
postNickname = getNicknameFromActor(postJsonObject['actor'])
|
||||
if not postNickname:
|
||||
|
|
@ -371,6 +373,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
|
|||
return None
|
||||
originalPostId = None
|
||||
if postJsonObject.get('id'):
|
||||
if not isinstance(postJsonObject['id'], str):
|
||||
return None
|
||||
originalPostId = removeIdEnding(postJsonObject['id'])
|
||||
|
||||
currTime = datetime.datetime.utcnow()
|
||||
|
|
|
|||
8
like.py
8
like.py
|
|
@ -25,7 +25,7 @@ def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool:
|
|||
"""
|
||||
if noOfLikes(postJsonObject) == 0:
|
||||
return False
|
||||
actorMatch = domain+'/users/'+nickname
|
||||
actorMatch = domain + '/users/' + nickname
|
||||
for item in postJsonObject['object']['likes']['items']:
|
||||
if item['actor'].endswith(actorMatch):
|
||||
return True
|
||||
|
|
@ -70,7 +70,7 @@ def like(recentPostsCache: {},
|
|||
if port:
|
||||
if port != 80 and port != 443:
|
||||
if ':' not in domain:
|
||||
fullDomain = domain+':'+str(port)
|
||||
fullDomain = domain + ':' + str(port)
|
||||
|
||||
newLikeJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
|
|
@ -174,7 +174,7 @@ def undolike(recentPostsCache: {},
|
|||
newUndoLikeJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'type': 'Undo',
|
||||
'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
|
||||
'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
|
||||
'object': {
|
||||
'type': 'Like',
|
||||
'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
|
||||
|
|
@ -476,4 +476,4 @@ def outboxUndoLike(recentPostsCache: {},
|
|||
messageId, messageJson['actor'],
|
||||
domain, debug)
|
||||
if debug:
|
||||
print('DEBUG: post undo liked via c2s - '+postFilename)
|
||||
print('DEBUG: post undo liked via c2s - ' + postFilename)
|
||||
|
|
|
|||
6
media.py
6
media.py
|
|
@ -56,7 +56,7 @@ def getImageHash(imageFilename: str) -> str:
|
|||
|
||||
|
||||
def isMedia(imageFilename: str) -> bool:
|
||||
permittedMedia = ('png', 'jpg', 'gif', 'webp',
|
||||
permittedMedia = ('png', 'jpg', 'gif', 'webp', 'avif',
|
||||
'mp4', 'ogv', 'mp3', 'ogg')
|
||||
for m in permittedMedia:
|
||||
if imageFilename.endswith('.' + m):
|
||||
|
|
@ -84,7 +84,7 @@ def getAttachmentMediaType(filename: str) -> str:
|
|||
"""
|
||||
mediaType = None
|
||||
imageTypes = ('png', 'jpg', 'jpeg',
|
||||
'gif', 'webp')
|
||||
'gif', 'webp', 'avif')
|
||||
for mType in imageTypes:
|
||||
if filename.endswith('.' + mType):
|
||||
return 'image'
|
||||
|
|
@ -143,7 +143,7 @@ def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int,
|
|||
return postJson
|
||||
|
||||
fileExtension = None
|
||||
acceptedTypes = ('png', 'jpg', 'gif', 'webp',
|
||||
acceptedTypes = ('png', 'jpg', 'gif', 'webp', 'avif',
|
||||
'mp4', 'webm', 'ogv', 'mp3', 'ogg')
|
||||
for mType in acceptedTypes:
|
||||
if imageFilename.endswith('.' + mType):
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
fileExtension = 'gif'
|
||||
elif mediaTypeStr.endswith('webp'):
|
||||
fileExtension = 'webp'
|
||||
elif mediaTypeStr.endswith('avif'):
|
||||
fileExtension = 'avif'
|
||||
elif mediaTypeStr.endswith('audio/mpeg'):
|
||||
fileExtension = 'mp3'
|
||||
elif mediaTypeStr.endswith('ogg'):
|
||||
|
|
|
|||
|
|
@ -890,7 +890,10 @@ def removeTagsForNickname(baseDir: str, nickname: str,
|
|||
filename = os.fsdecode(f)
|
||||
if not filename.endswith(".txt"):
|
||||
continue
|
||||
tagFilename = os.path.join(directory, filename)
|
||||
try:
|
||||
tagFilename = os.path.join(directory, filename)
|
||||
except BaseException:
|
||||
continue
|
||||
if not os.path.isfile(tagFilename):
|
||||
continue
|
||||
if matchStr not in open(tagFilename).read():
|
||||
|
|
|
|||
56
tests.py
56
tests.py
|
|
@ -54,6 +54,7 @@ from person import setBio
|
|||
from skills import setSkillLevel
|
||||
from roles import setRole
|
||||
from roles import outboxDelegate
|
||||
from auth import constantTimeStringCheck
|
||||
from auth import createBasicAuthHeader
|
||||
from auth import authorizeBasic
|
||||
from auth import storeBasicCredentials
|
||||
|
|
@ -777,12 +778,21 @@ def testFollowBetweenServers():
|
|||
bobDomain + '/followers.txt'):
|
||||
if os.path.isfile(aliceDir + '/accounts/alice@' +
|
||||
aliceDomain + '/following.txt'):
|
||||
break
|
||||
if os.path.isfile(aliceDir + '/accounts/alice@' +
|
||||
aliceDomain + '/followingCalendar.txt'):
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
assert validInbox(bobDir, 'bob', bobDomain)
|
||||
assert validInboxFilenames(bobDir, 'bob', bobDomain,
|
||||
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('Alice sends a message to Bob')
|
||||
|
|
@ -828,11 +838,6 @@ def testFollowBetweenServers():
|
|||
thrBob.join()
|
||||
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
|
||||
time.sleep(4)
|
||||
assert len([name for name in os.listdir(queuePath)
|
||||
|
|
@ -2081,8 +2086,47 @@ def testTranslations():
|
|||
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():
|
||||
print('Running tests...')
|
||||
testConstantTimeStringCheck()
|
||||
testTranslations()
|
||||
testValidContentWarning()
|
||||
testRemoveIdEnding()
|
||||
|
|
|
|||
19
theme.py
19
theme.py
|
|
@ -294,7 +294,8 @@ def setThemeNight(baseDir: str):
|
|||
"main-bg-color-report": "#0f0d10",
|
||||
"hashtag-vertical-spacing3": "100px",
|
||||
"hashtag-vertical-spacing4": "150px",
|
||||
"button-background": "#7961ab",
|
||||
"button-background-hover": "#6961ab",
|
||||
"button-background": "#a961ab",
|
||||
"button-selected": "#86579d",
|
||||
"calendar-bg-color": "#0f0d10",
|
||||
"lines-color": "#7961ab",
|
||||
|
|
@ -332,6 +333,7 @@ def setThemeStarlight(baseDir: str):
|
|||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "#0f0d10",
|
||||
"main-link-color": "#ffc4bc",
|
||||
"main-link-color-hover": "white",
|
||||
"title-color": "#ffc4bc",
|
||||
"main-visited-color": "#e1c4bc",
|
||||
"main-fg-color": "#ffc4bc",
|
||||
|
|
@ -342,6 +344,7 @@ def setThemeStarlight(baseDir: str):
|
|||
"main-bg-color-report": "#0f0d10",
|
||||
"hashtag-vertical-spacing3": "100px",
|
||||
"hashtag-vertical-spacing4": "150px",
|
||||
"button-background-hover": "#a9282c",
|
||||
"button-background": "#69282c",
|
||||
"button-small-background": "darkblue",
|
||||
"button-selected": "#a34046",
|
||||
|
|
@ -388,6 +391,7 @@ def setThemeHenge(baseDir: str):
|
|||
"text-entry-background": "#383335",
|
||||
"link-bg-color": "#383335",
|
||||
"main-link-color": "white",
|
||||
"main-link-color-hover": "#ddd",
|
||||
"title-color": "white",
|
||||
"main-visited-color": "#e1c4bc",
|
||||
"main-fg-color": "white",
|
||||
|
|
@ -398,6 +402,7 @@ def setThemeHenge(baseDir: str):
|
|||
"main-bg-color-report": "#383335",
|
||||
"hashtag-vertical-spacing3": "100px",
|
||||
"hashtag-vertical-spacing4": "150px",
|
||||
"button-background-hover": "#444",
|
||||
"button-background": "#222",
|
||||
"button-selected": "black",
|
||||
"dropdown-fg-color": "#dddddd",
|
||||
|
|
@ -439,8 +444,10 @@ def setThemeZen(baseDir: str):
|
|||
"border-color": "#463b35",
|
||||
"border-width": "7px",
|
||||
"main-link-color": "#dddddd",
|
||||
"main-link-color-hover": "white",
|
||||
"title-color": "#dddddd",
|
||||
"main-visited-color": "#dddddd",
|
||||
"button-background-hover": "#a63b35",
|
||||
"button-background": "#463b35",
|
||||
"button-selected": "#26201d",
|
||||
"main-bg-color-dm": "#5c4a40",
|
||||
|
|
@ -499,10 +506,12 @@ def setThemeLCD(baseDir: str):
|
|||
"border-color": "#33390d",
|
||||
"border-width": "5px",
|
||||
"main-link-color": "#9fb42b",
|
||||
"main-link-color-hover": "#cfb42b",
|
||||
"title-color": "#9fb42b",
|
||||
"main-visited-color": "#9fb42b",
|
||||
"button-selected": "black",
|
||||
"button-highlighted": "green",
|
||||
"button-background-hover": "#a3390d",
|
||||
"button-background": "#33390d",
|
||||
"button-small-background": "#33390d",
|
||||
"button-text": "#9fb42b",
|
||||
|
|
@ -569,9 +578,11 @@ def setThemePurple(baseDir: str):
|
|||
"main-fg-color": "#f98bb0",
|
||||
"border-color": "#3f2145",
|
||||
"main-link-color": "#ff42a0",
|
||||
"main-link-color-hover": "white",
|
||||
"title-color": "white",
|
||||
"main-visited-color": "#f93bb0",
|
||||
"button-selected": "#c042a0",
|
||||
"button-background-hover": "#af42a0",
|
||||
"button-background": "#ff42a0",
|
||||
"button-small-background": "#ff42a0",
|
||||
"button-text": "white",
|
||||
|
|
@ -616,9 +627,11 @@ def setThemeHacker(baseDir: str):
|
|||
"main-fg-color": "#00ff00",
|
||||
"border-color": "#035103",
|
||||
"main-link-color": "#2fff2f",
|
||||
"main-link-color-hover": "#afff2f",
|
||||
"title-color": "#2fff2f",
|
||||
"main-visited-color": "#3c8234",
|
||||
"button-selected": "#063200",
|
||||
"button-background-hover": "#a62200",
|
||||
"button-background": "#062200",
|
||||
"button-small-background": "#062200",
|
||||
"button-text": "#00ff00",
|
||||
|
|
@ -673,6 +686,7 @@ def setThemeLight(baseDir: str):
|
|||
"main-fg-color": "#2d2c37",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#2a2c37",
|
||||
"main-link-color-hover": "#aa2c37",
|
||||
"title-color": "#2a2c37",
|
||||
"main-visited-color": "#232c37",
|
||||
"text-entry-foreground": "#111",
|
||||
|
|
@ -728,6 +742,7 @@ def setThemeSolidaric(baseDir: str):
|
|||
"main-fg-color": "#2d2c37",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#2a2c37",
|
||||
"main-link-color-hover": "#aa2c37",
|
||||
"title-color": "#2a2c37",
|
||||
"main-visited-color": "#232c37",
|
||||
"text-entry-foreground": "#111",
|
||||
|
|
@ -786,7 +801,7 @@ def setThemeImages(baseDir: str, name: str) -> None:
|
|||
|
||||
backgroundNames = ('login', 'shares', 'delete', 'follow',
|
||||
'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 acct in dirs:
|
||||
|
|
|
|||
23
utils.py
23
utils.py
|
|
@ -51,7 +51,7 @@ def removeAvatarFromCache(baseDir: str, actorStr: str) -> None:
|
|||
"""Removes any existing avatar entries from the cache
|
||||
This avoids duplicate entries with differing extensions
|
||||
"""
|
||||
avatarFilenameExtensions = ('png', 'jpg', 'gif', 'webp')
|
||||
avatarFilenameExtensions = ('png', 'jpg', 'gif', 'webp', 'avif')
|
||||
for extension in avatarFilenameExtensions:
|
||||
avatarFilename = \
|
||||
baseDir + '/cache/avatars/' + actorStr + '.' + extension
|
||||
|
|
@ -369,24 +369,27 @@ def followPerson(baseDir: str, nickname: str, domain: str,
|
|||
content = f.read()
|
||||
f.seek(0, 0)
|
||||
f.write(handleToFollow + '\n' + content)
|
||||
if debug:
|
||||
print('DEBUG: follow added')
|
||||
print('DEBUG: follow added')
|
||||
except Exception as e:
|
||||
print('WARN: Failed to write entry to follow file ' +
|
||||
filename + ' ' + str(e))
|
||||
else:
|
||||
# first follow
|
||||
if debug:
|
||||
print('DEBUG: creating new following file to follow ' +
|
||||
handleToFollow)
|
||||
print('DEBUG: ' + handle +
|
||||
' creating new following file to follow ' + handleToFollow +
|
||||
', filename is ' + filename)
|
||||
with open(filename, 'w+') as f:
|
||||
f.write(handleToFollow + '\n')
|
||||
|
||||
# Default to adding new follows to the calendar.
|
||||
# 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
|
||||
# calendar follows
|
||||
print('DEBUG: adding ' +
|
||||
followNickname + '@' + followDomain + ' to calendar of ' +
|
||||
nickname + '@' + domain)
|
||||
addPersonToCalendar(baseDir, nickname, domain,
|
||||
followNickname, followDomain)
|
||||
return True
|
||||
|
|
@ -728,11 +731,9 @@ def getCachedPostFilename(baseDir: str, nickname: str, domain: str,
|
|||
if '@' not in cachedPostDir:
|
||||
# print('ERROR: invalid html cache directory '+cachedPostDir)
|
||||
return None
|
||||
cachedPostFilename = \
|
||||
cachedPostDir + \
|
||||
'/' + removeIdEnding(postJsonObject['id']).replace('/', '#')
|
||||
cachedPostFilename = cachedPostFilename + '.html'
|
||||
return cachedPostFilename
|
||||
cachedPostId = removeIdEnding(postJsonObject['id'])
|
||||
cachedPostFilename = cachedPostDir + '/' + cachedPostId.replace('/', '#')
|
||||
return cachedPostFilename + '.html'
|
||||
|
||||
|
||||
def removePostFromCache(postJsonObject: {}, recentPostsCache: {}):
|
||||
|
|
|
|||
|
|
@ -230,6 +230,11 @@ def updateAvatarImageCache(session, baseDir: str, httpPrefix: str,
|
|||
'Accept': 'image/webp'
|
||||
}
|
||||
avatarImageFilename = avatarImagePath + '.webp'
|
||||
elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl:
|
||||
sessionHeaders = {
|
||||
'Accept': 'image/avif'
|
||||
}
|
||||
avatarImageFilename = avatarImagePath + '.avif'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -304,7 +309,7 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {},
|
|||
actorStr = personJson['id'].replace('/', '-')
|
||||
avatarImagePath = baseDir + '/cache/avatars/' + actorStr
|
||||
|
||||
imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp')
|
||||
imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif')
|
||||
for ext in imageExtension:
|
||||
if os.path.isfile(avatarImagePath + '.' + ext):
|
||||
return '/avatars/' + actorStr + '.' + ext
|
||||
|
|
@ -1064,7 +1069,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
domain: str, port: int, httpPrefix: str) -> str:
|
||||
"""Shows the edit profile screen
|
||||
"""
|
||||
imageFormats = '.png, .jpg, .jpeg, .gif, .webp'
|
||||
imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif'
|
||||
pathOriginal = path
|
||||
path = path.replace('/inbox', '').replace('/outbox', '')
|
||||
path = path.replace('/shares', '')
|
||||
|
|
@ -1621,6 +1626,9 @@ def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str:
|
|||
elif os.path.isfile(baseDir + '/accounts/login.webp'):
|
||||
loginImage = 'login.webp'
|
||||
loginImageFilename = baseDir + '/accounts/' + loginImage
|
||||
elif os.path.isfile(baseDir + '/accounts/login.avif'):
|
||||
loginImage = 'login.avif'
|
||||
loginImageFilename = baseDir + '/accounts/' + loginImage
|
||||
|
||||
if not loginImageFilename:
|
||||
loginImageFilename = baseDir + '/accounts/' + loginImage
|
||||
|
|
@ -1965,13 +1973,13 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
newPostImageSection += \
|
||||
' <input type="file" id="attachpic" name="attachpic"'
|
||||
newPostImageSection += \
|
||||
' accept=".png, .jpg, .jpeg, .gif, .webp">\n'
|
||||
' accept=".png, .jpg, .jpeg, .gif, .webp, .avif">\n'
|
||||
else:
|
||||
newPostImageSection += \
|
||||
' <input type="file" id="attachpic" name="attachpic"'
|
||||
newPostImageSection += \
|
||||
' accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, ' + \
|
||||
'.webm, .ogv, .mp3, .ogg">\n'
|
||||
' accept=".png, .jpg, .jpeg, .gif, ' + \
|
||||
'.webp, .avif, .mp4, .webm, .ogv, .mp3, .ogg">\n'
|
||||
newPostImageSection += ' </div>\n'
|
||||
|
||||
scopeIcon = 'scope_public.png'
|
||||
|
|
@ -2913,7 +2921,7 @@ def htmlProfile(defaultTimeline: str,
|
|||
'/followapprove=' + followerHandle + '">'
|
||||
followApprovalsSection += \
|
||||
'<button class="followApprove">' + \
|
||||
translate['Approve'] + '</button></a>'
|
||||
translate['Approve'] + '</button></a><br><br>'
|
||||
followApprovalsSection += \
|
||||
'<a href="' + basePath + \
|
||||
'/followdeny=' + followerHandle + '">'
|
||||
|
|
@ -3623,11 +3631,13 @@ def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
|
|||
if mediaType == 'image/png' or \
|
||||
mediaType == 'image/jpeg' or \
|
||||
mediaType == 'image/webp' or \
|
||||
mediaType == 'image/avif' or \
|
||||
mediaType == 'image/gif':
|
||||
if attach['url'].endswith('.png') or \
|
||||
attach['url'].endswith('.jpg') or \
|
||||
attach['url'].endswith('.jpeg') or \
|
||||
attach['url'].endswith('.webp') or \
|
||||
attach['url'].endswith('.avif') or \
|
||||
attach['url'].endswith('.gif'):
|
||||
if attachmentCtr > 0:
|
||||
attachmentStr += '<br>'
|
||||
|
|
@ -4214,13 +4224,15 @@ def individualPostAsHtml(allowDownloads: bool,
|
|||
|
||||
likeCountStr = ''
|
||||
if likeCount > 0:
|
||||
if likeCount > 1:
|
||||
if likeCount <= 10:
|
||||
likeCountStr = ' (' + str(likeCount) + ')'
|
||||
else:
|
||||
likeCountStr = ' (10+)'
|
||||
likeIcon = 'like.png'
|
||||
if likeCount <= 10:
|
||||
likeCountStr = ' (' + str(likeCount) + ')'
|
||||
else:
|
||||
likeCountStr = ' (10+)'
|
||||
if likedByPerson(postJsonObject, nickname, fullDomain):
|
||||
if likeCount == 1:
|
||||
# liked by the reader only
|
||||
likeCountStr = ''
|
||||
likeIcon = 'like.png'
|
||||
likeLink = 'unlike'
|
||||
likeTitle = translate['Undo the like']
|
||||
|
||||
|
|
@ -4230,7 +4242,13 @@ def individualPostAsHtml(allowDownloads: bool,
|
|||
if timeDiff > 100:
|
||||
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 + '?' + \
|
||||
likeLink + '=' + postJsonObject['object']['id'] + \
|
||||
pageNumberParam + \
|
||||
|
|
@ -4914,6 +4932,10 @@ def htmlTimeline(defaultTimeline: str,
|
|||
bannerFile = 'banner.gif'
|
||||
bannerFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/' + bannerFile
|
||||
if not os.path.isfile(bannerFilename):
|
||||
bannerFile = 'banner.avif'
|
||||
bannerFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/' + bannerFile
|
||||
if not os.path.isfile(bannerFilename):
|
||||
bannerFile = 'banner.webp'
|
||||
|
||||
|
|
@ -6814,8 +6836,8 @@ def htmlSearch(translate: {},
|
|||
baseDir + '/accounts/search-background.png')
|
||||
|
||||
cssFilename = baseDir + '/epicyon-search.css'
|
||||
if os.path.isfile(baseDir + '/follow.css'):
|
||||
cssFilename = baseDir + '/follow.css'
|
||||
if os.path.isfile(baseDir + '/search.css'):
|
||||
cssFilename = baseDir + '/search.css'
|
||||
with open(cssFilename, 'r') as cssFile:
|
||||
profileStyle = cssFile.read()
|
||||
followStr = htmlHeader(cssFilename, profileStyle)
|
||||
|
|
|
|||
Loading…
Reference in New Issue