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 += \ '