diff --git a/README.md b/README.md
index ecc98bf50..b215bdc9b 100644
--- a/README.md
+++ b/README.md
@@ -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:
diff --git a/auth.py b/auth.py
index 7ac45cecf..021a17b61 100644
--- a/auth.py
+++ b/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:
diff --git a/blocking.py b/blocking.py
index 8d6436f3b..b82cf3e06 100644
--- a/blocking.py
+++ b/blocking.py
@@ -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()
diff --git a/blog.py b/blog.py
index 91db9ca66..e1db75975 100644
--- a/blog.py
+++ b/blog.py
@@ -722,7 +722,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
editBlogImageSection += \
' '
editBlogImageSection += ' '
diff --git a/content.py b/content.py
index 488601702..3f85eff5b 100644
--- a/content.py
+++ b/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
diff --git a/daemon.py b/daemon.py
index c4c929ca2..d3076d150 100644
--- a/daemon.py
+++ b/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')
diff --git a/epicyon-options.css b/epicyon-options.css
index c7f07905d..328a39ffc 100644
--- a/epicyon-options.css
+++ b/epicyon-options.css
@@ -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 {
diff --git a/epicyon-profile.css b/epicyon-profile.css
index 00a6d3f28..43b9dd4e4 100644
--- a/epicyon-profile.css
+++ b/epicyon-profile.css
@@ -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;
diff --git a/epicyon-search.css b/epicyon-search.css
index ab4a91a19..eda0516de 100644
--- a/epicyon-search.css
+++ b/epicyon-search.css
@@ -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] {
diff --git a/followingCalendar.py b/followingCalendar.py
index 10c5ae39e..2281fda63 100644
--- a/followingCalendar.py
+++ b/followingCalendar.py
@@ -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
diff --git a/inbox.py b/inbox.py
index 89f8c6949..e1b0c0b89 100644
--- a/inbox.py
+++ b/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()
diff --git a/like.py b/like.py
index fb38d8100..d63709616 100644
--- a/like.py
+++ b/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)
diff --git a/media.py b/media.py
index ec6626958..a231c7906 100644
--- a/media.py
+++ b/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):
diff --git a/outbox.py b/outbox.py
index e532393f7..4254bb101 100644
--- a/outbox.py
+++ b/outbox.py
@@ -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'):
diff --git a/person.py b/person.py
index af7020df6..b1fa69444 100644
--- a/person.py
+++ b/person.py
@@ -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():
diff --git a/tests.py b/tests.py
index 13e3a2892..51b00cbd4 100644
--- a/tests.py
+++ b/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()
diff --git a/theme.py b/theme.py
index 9227aa99a..6a7bd5377 100644
--- a/theme.py
+++ b/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:
diff --git a/utils.py b/utils.py
index c39b042fd..97fccaa8b 100644
--- a/utils.py
+++ b/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: {}):
diff --git a/webinterface.py b/webinterface.py
index 47912e3e0..6a23e7aac 100644
--- a/webinterface.py
+++ b/webinterface.py
@@ -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 += \
' \n'
+ ' accept=".png, .jpg, .jpeg, .gif, .webp, .avif">\n'
else:
newPostImageSection += \
' \n'
+ ' accept=".png, .jpg, .jpeg, .gif, ' + \
+ '.webp, .avif, .mp4, .webm, .ogv, .mp3, .ogg">\n'
newPostImageSection += ' \n'
scopeIcon = 'scope_public.png'
@@ -2913,7 +2921,7 @@ def htmlProfile(defaultTimeline: str,
'/followapprove=' + followerHandle + '">'
followApprovalsSection += \
''
+ translate['Approve'] + '
'
followApprovalsSection += \
''
@@ -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 += '
'
@@ -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 += '\n'
+ likeStr += \
'