From 949f54f5f3fd7244d39d10eb864a86e57bb7fea5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 13:16:24 +0100 Subject: [PATCH 01/39] Unit test for adding new follow to calendar --- followingCalendar.py | 13 ++++++++++++- tests.py | 16 ++++++++++------ utils.py | 13 ++++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) 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/tests.py b/tests.py index 13e3a2892..127d0c798 100644 --- a/tests.py +++ b/tests.py @@ -777,12 +777,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 +837,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) diff --git a/utils.py b/utils.py index c39b042fd..875bb0582 100644 --- a/utils.py +++ b/utils.py @@ -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 From 6568be91ff8864d0bc55c158e978cc441e17aa0a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 19:07:02 +0100 Subject: [PATCH 02/39] Constant time password hash match --- auth.py | 35 ++++++++++++++++++++++++++++------- tests.py | 1 - 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/auth.py b/auth.py index 7ac45cecf..07cf91af2 100644 --- a/auth.py +++ b/auth.py @@ -10,7 +10,6 @@ import base64 import hashlib import binascii import os -import secrets def hashPassword(password: str) -> str: @@ -24,17 +23,39 @@ 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 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) + # check that hashes are of equal length + if len(pwHash) != len(storedPassword): + return False + # Compare all of the characters before returning true or false. + # Hence the match should take a constant amount of time. + # See https://sqreen.github.io/DevelopersSecurityBestPractices/ + # timing-attack/python + ctr = 0 + matched = True + for ch in pwHash: + if ch != storedPassword[ctr]: + matched = False + ctr += 1 + return matched def createBasicAuthHeader(nickname: str, password: str) -> str: diff --git a/tests.py b/tests.py index 127d0c798..7c5c2865b 100644 --- a/tests.py +++ b/tests.py @@ -2084,7 +2084,6 @@ def testTranslations(): print(englishStr + ' is missing from ' + lang + '.json') assert langJson.get(englishStr) - def runAllTests(): print('Running tests...') testTranslations() From 7e87bbe2aa2ec6c433b4645b168a05a9a343cd66 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 19:13:29 +0100 Subject: [PATCH 03/39] Avoid providing password hash match timing clues --- auth.py | 6 ++++++ tests.py | 1 + 2 files changed, 7 insertions(+) diff --git a/auth.py b/auth.py index 07cf91af2..317ca0bb4 100644 --- a/auth.py +++ b/auth.py @@ -10,6 +10,7 @@ import base64 import hashlib import binascii import os +import secrets def hashPassword(password: str) -> str: @@ -32,6 +33,7 @@ def getPasswordHash(salt: str, providedPassword: str) -> str: 100000) return binascii.hexlify(pwdhash).decode('ascii') + def verifyPassword(storedPassword: str, providedPassword: str) -> bool: """Verify a stored password against one provided by user """ @@ -54,6 +56,10 @@ def verifyPassword(storedPassword: str, providedPassword: str) -> bool: for ch in pwHash: if ch != storedPassword[ctr]: matched = False + else: + # this is to make the timing more even + # and not provide clues + matched = matched ctr += 1 return matched diff --git a/tests.py b/tests.py index 7c5c2865b..127d0c798 100644 --- a/tests.py +++ b/tests.py @@ -2084,6 +2084,7 @@ def testTranslations(): print(englishStr + ' is missing from ' + lang + '.json') assert langJson.get(englishStr) + def runAllTests(): print('Running tests...') testTranslations() From e584076acaa6e44bacfd8263609bf0126fcb3700 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 19:48:32 +0100 Subject: [PATCH 04/39] Unit test for constant time string check --- auth.py | 41 +++++++++++++++++++++++------------------ tests.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/auth.py b/auth.py index 317ca0bb4..021a17b61 100644 --- a/auth.py +++ b/auth.py @@ -34,6 +34,28 @@ def getPasswordHash(salt: str, providedPassword: str) -> str: 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 """ @@ -44,24 +66,7 @@ def verifyPassword(storedPassword: str, providedPassword: str) -> bool: salt = storedPassword[:64] storedPassword = storedPassword[64:] pwHash = getPasswordHash(salt, providedPassword) - # check that hashes are of equal length - if len(pwHash) != len(storedPassword): - return False - # Compare all of the characters before returning true or false. - # Hence the match should take a constant amount of time. - # See https://sqreen.github.io/DevelopersSecurityBestPractices/ - # timing-attack/python - ctr = 0 - matched = True - for ch in pwHash: - if ch != storedPassword[ctr]: - matched = False - else: - # this is to make the timing more even - # and not provide clues - matched = matched - ctr += 1 - return matched + return constantTimeStringCheck(pwHash, storedPassword) def createBasicAuthHeader(nickname: str, password: str) -> str: diff --git a/tests.py b/tests.py index 127d0c798..ccbf216c5 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 @@ -2085,8 +2086,36 @@ 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 characters 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 + + def runAllTests(): print('Running tests...') + testConstantTimeStringCheck() testTranslations() testValidContentWarning() testRemoveIdEnding() From bbe414d3a67f2713eef9fbaa43baf69d5818c796 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 19:52:18 +0100 Subject: [PATCH 05/39] Single and multiple character change test --- tests.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index ccbf216c5..51b00cbd4 100644 --- a/tests.py +++ b/tests.py @@ -2101,7 +2101,7 @@ def testConstantTimeStringCheck(): end = time.time() avTime1 = ((end - start) * 1000000 / itterations) - # change characters and observe timing difference + # change a single character and observe timing difference start = time.time() for timingTest in range(itterations): constantTimeStringCheck('nnjfbefefbsnjsdnvbcueftqfeuqfbqefnjeniwufgy', @@ -2112,6 +2112,17 @@ def testConstantTimeStringCheck(): # 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...') From 79b7f3839f40212e0466e40cb8b7918fe4fa90dc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 22:18:10 +0100 Subject: [PATCH 06/39] Change hover style --- epicyon-profile.css | 19 +++++-------------- theme.py | 9 ++++++++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 00a6d3f28..c85fc16c2 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -35,6 +35,7 @@ --time-vertical-align: 4px; --button-text: #FFFFFF; --button-background: #999; + --button-background-hover: #777; --button-selected: #666; --button-highlighted: green; --button-fg-highlighted: #FFFFFF; @@ -236,13 +237,8 @@ a:focus { transition: 0.5s; } -.button:hover span { - padding-right: 25px; -} - -.button:hover span:after { - opacity: 1; - right: 0; +.button:hover { + background-color: var(--button-background-hover); } .buttonselected span { @@ -263,13 +259,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 { diff --git a/theme.py b/theme.py index 9227aa99a..a4984a6f1 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", @@ -342,6 +343,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", @@ -398,6 +400,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", @@ -441,6 +444,7 @@ def setThemeZen(baseDir: str): "main-link-color": "#dddddd", "title-color": "#dddddd", "main-visited-color": "#dddddd", + "button-background-hover": "#a63b35", "button-background": "#463b35", "button-selected": "#26201d", "main-bg-color-dm": "#5c4a40", @@ -503,6 +507,7 @@ def setThemeLCD(baseDir: str): "main-visited-color": "#9fb42b", "button-selected": "black", "button-highlighted": "green", + "button-background-hover": "#a3390d", "button-background": "#33390d", "button-small-background": "#33390d", "button-text": "#9fb42b", @@ -572,6 +577,7 @@ def setThemePurple(baseDir: str): "title-color": "white", "main-visited-color": "#f93bb0", "button-selected": "#c042a0", + "button-background-hover": "#af42a0", "button-background": "#ff42a0", "button-small-background": "#ff42a0", "button-text": "white", @@ -619,6 +625,7 @@ def setThemeHacker(baseDir: str): "title-color": "#2fff2f", "main-visited-color": "#3c8234", "button-selected": "#063200", + "button-background-hover": "#a62200", "button-background": "#062200", "button-small-background": "#062200", "button-text": "#00ff00", From f8bf4e3c7b69c0e1f1059e78f829354105355bd4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 22:22:35 +0100 Subject: [PATCH 07/39] Consistent button hover --- epicyon-options.css | 4 ++-- epicyon-search.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/epicyon-options.css b/epicyon-options.css index c7f07905d..074b32cca 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -19,6 +19,7 @@ --time-color: #aaa; --button-text: #FFFFFF; --button-small-text: #FFFFFF; + --button-background-hover: #777; --button-background: #999; --button-small-background: #999; --button-selected: #666; @@ -107,8 +108,7 @@ a:focus { } .button:hover { - background-color: #555; - color: white; + background-color: var(--button-background-hover); } .options { diff --git a/epicyon-search.css b/epicyon-search.css index ab4a91a19..08a2f6467 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -18,6 +18,7 @@ --text-entry-background: #111; --time-color: #aaa; --button-text: #FFFFFF; + --button-background-hover: #777; --button-background: #999; --button-selected: #666; --hashtag-margin: 2%; @@ -141,8 +142,7 @@ a:focus { } .button:hover { - background-color: #555; - color: white; + background-color: var(--button-background-hover); } input[type=text] { From 2498ad97666a04e3a3fc174435e732f3efc52eb6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 09:57:09 +0100 Subject: [PATCH 08/39] Donate button hover effect --- epicyon-options.css | 1 - epicyon-profile.css | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/epicyon-options.css b/epicyon-options.css index 074b32cca..328a39ffc 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -22,7 +22,6 @@ --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; diff --git a/epicyon-profile.css b/epicyon-profile.css index c85fc16c2..656f308a0 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -241,6 +241,10 @@ a:focus { background-color: var(--button-background-hover); } +.donateButton:hover { + background-color: var(--button-background-hover); +} + .buttonselected span { font-family: Arial, Helvetica, sans-serif; cursor: pointer; From 24a4734da69dd7b79a1da71b33bd6e739c38fe1d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:09:43 +0100 Subject: [PATCH 09/39] Change brightness of icons when hovering --- epicyon-profile.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index 656f308a0..4ada60eb3 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -401,6 +401,10 @@ a:focus { margin-right: 0; } +.containericons img:hover { + filter: brightness(150%); +} + .post-title { margin-top: 0px; color: #444; From 5c403ad376925051e9bb6f6b78f555c3958a944e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:13:03 +0100 Subject: [PATCH 10/39] Change brightness of timeline icons when hovering --- epicyon-profile.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index 4ada60eb3..019f100c1 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -215,6 +215,10 @@ a:focus { width: 10%; } +.container img.timelineicon:hover { + filter: brightness(150%); +} + .followRequestHandle { padding: 0px 20px; } From 0d2f2369a02ffb7c128aa66a53922ed4709e0800 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:25:28 +0100 Subject: [PATCH 11/39] Change color of links when hovering over them --- epicyon-profile.css | 5 +++++ theme.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index 019f100c1..e68c77408 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; @@ -102,6 +103,10 @@ a:link { font-weight: bold; } +a:link:hover { + color: var(--main-link-color-hover); +} + a:focus { border: 2px solid var(--focus-color); } diff --git a/theme.py b/theme.py index a4984a6f1..7ef1f6e75 100644 --- a/theme.py +++ b/theme.py @@ -333,6 +333,7 @@ def setThemeStarlight(baseDir: str): "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", "main-link-color": "#ffc4bc", + "main-link-color-hover": "#ffffbc", "title-color": "#ffc4bc", "main-visited-color": "#e1c4bc", "main-fg-color": "#ffc4bc", @@ -390,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", @@ -442,6 +444,7 @@ 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", @@ -503,6 +506,7 @@ 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", @@ -574,6 +578,7 @@ 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", @@ -622,6 +627,7 @@ 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", @@ -680,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", @@ -735,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", From 94d2e84bfbc1ebca0d7f0af4ebd25021dcc8ed9f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:31:03 +0100 Subject: [PATCH 12/39] Change brightness of avatar image when hovering --- epicyon-profile.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index e68c77408..a246175ca 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -555,6 +555,10 @@ input[type=submit]:hover { padding: 0px 0px; } +.timeline-avatar:hover { + filter: brightness(120%); +} + .timeline-avatar-reply { padding: 0px 0px; width: 80%; From 5db888574c5374f2d84ac21a7779a4689b883a4e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:33:43 +0100 Subject: [PATCH 13/39] Change link brightness when hovering on search screen --- epicyon-search.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/epicyon-search.css b/epicyon-search.css index 08a2f6467..14d86437f 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; @@ -69,6 +70,10 @@ a:link { font-weight: bold; } +a:link:hover { + color: var(--main-link-color-hover); +} + a:focus { border: 2px solid var(--focus-color); } From 59fc0ddd147d0b3bcb6f7c3a26e8bef0f65a55ea Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:53:26 +0100 Subject: [PATCH 14/39] Change brightness of block/unfollow when hovering --- epicyon-profile.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index a246175ca..c68ed2fb5 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -223,6 +223,10 @@ a:focus { .container img.timelineicon:hover { filter: brightness(150%); } + +.buttonunfollow:hover { + filter: brightness(150%); +} .followRequestHandle { padding: 0px 20px; From 2d55e673b69e049efa02b31199a67eb3280dfce8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 10:55:23 +0100 Subject: [PATCH 15/39] Change background color --- epicyon-profile.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index c68ed2fb5..287788adf 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -225,7 +225,7 @@ a:focus { } .buttonunfollow:hover { - filter: brightness(150%); + background-color: var(--button-background-hover); } .followRequestHandle { From b0d2e81c8f5eb5bba1762a3091aa7fb8c88448dd Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 11:11:30 +0100 Subject: [PATCH 16/39] css file name --- webinterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 47912e3e0..fadaf609b 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6814,8 +6814,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) From d853fa3ba35030cc53b935785bc51ed97962074d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 11:14:47 +0100 Subject: [PATCH 17/39] Change background --- epicyon-search.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epicyon-search.css b/epicyon-search.css index 14d86437f..819a2ecf8 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -86,7 +86,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; From ce4bb5626ba642050a8da1a13892e7712cba5db0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 11:18:38 +0100 Subject: [PATCH 18/39] Change link color when hovering on visited link --- epicyon-profile.css | 4 ++++ epicyon-search.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/epicyon-profile.css b/epicyon-profile.css index 287788adf..044c56d71 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -107,6 +107,10 @@ 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); } diff --git a/epicyon-search.css b/epicyon-search.css index 819a2ecf8..eda0516de 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -74,6 +74,10 @@ 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); } From c6a7080adcddfb4dce44233c5ed87f7516d3336c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 11:22:38 +0100 Subject: [PATCH 19/39] Link hover color --- theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.py b/theme.py index 7ef1f6e75..1c749b84c 100644 --- a/theme.py +++ b/theme.py @@ -333,7 +333,7 @@ def setThemeStarlight(baseDir: str): "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", "main-link-color": "#ffc4bc", - "main-link-color-hover": "#ffffbc", + "main-link-color-hover": "#f9282c", "title-color": "#ffc4bc", "main-visited-color": "#e1c4bc", "main-fg-color": "#ffc4bc", From 286c02aa395701712a16f0c32bad44e044ef5738 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 11:23:50 +0100 Subject: [PATCH 20/39] Link hover color --- theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.py b/theme.py index 1c749b84c..2b4b3ff70 100644 --- a/theme.py +++ b/theme.py @@ -333,7 +333,7 @@ def setThemeStarlight(baseDir: str): "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", "main-link-color": "#ffc4bc", - "main-link-color-hover": "#f9282c", + "main-link-color-hover": "white", "title-color": "#ffc4bc", "main-visited-color": "#e1c4bc", "main-fg-color": "#ffc4bc", From 0e15d936b3d4800529fd7dde84dad5d2c838825b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 4 Sep 2020 19:57:43 +0100 Subject: [PATCH 21/39] Newlines to provide more space for handle --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index fadaf609b..14dafb02c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -2913,7 +2913,7 @@ def htmlProfile(defaultTimeline: str, '/followapprove=' + followerHandle + '">' followApprovalsSection += \ '' + translate['Approve'] + '

' followApprovalsSection += \ '' From ac67db1679fd8f43da6d1e9b8f75e8ba898a3ae8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 10:41:09 +0100 Subject: [PATCH 22/39] Easier way to block domains --- blocking.py | 6 +++++- daemon.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/blocking.py b/blocking.py index 8d6436f3b..b59cc7f90 100644 --- a/blocking.py +++ b/blocking.py @@ -21,18 +21,22 @@ def addGlobalBlock(baseDir: str, """ blockingFilename = baseDir + '/accounts/blocking.txt' if not blockNickname.startswith('#'): - blockHandle = blockNickname + '@' + blockDomain + # 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/daemon.py b/daemon.py index c4c929ca2..1c2852561 100644 --- a/daemon.py +++ b/daemon.py @@ -1407,6 +1407,7 @@ class PubServer(BaseHTTPRequestHandler): fullBlockDomain = None if moderationText.startswith('http') or \ moderationText.startswith('dat'): + # https://domain blockDomain, blockPort = \ getDomainFromActor(moderationText) fullBlockDomain = blockDomain @@ -1416,13 +1417,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 +1440,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': From 08f62aa32c1bb940f5b25083f8f2a061eb8e6079 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 10:42:52 +0100 Subject: [PATCH 23/39] Tidying --- blocking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocking.py b/blocking.py index b59cc7f90..b82cf3e06 100644 --- a/blocking.py +++ b/blocking.py @@ -22,7 +22,7 @@ def addGlobalBlock(baseDir: str, blockingFilename = baseDir + '/accounts/blocking.txt' if not blockNickname.startswith('#'): # is the handle already blocked? - blockHandle = blockNickname + '@' + blockDomain + blockHandle = blockNickname + '@' + blockDomain if os.path.isfile(blockingFilename): if blockHandle in open(blockingFilename).read(): return False From e61bf9bed120330dace79cecd77addca3b94c290 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:00:21 +0100 Subject: [PATCH 24/39] Tidying --- like.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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) From 07a07d877aaa7d58b60e71d498affc3978d70d51 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:15:09 +0100 Subject: [PATCH 25/39] Like button only highlighted if the reader likes the post --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 14dafb02c..c8381b031 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4219,8 +4219,8 @@ def individualPostAsHtml(allowDownloads: bool, likeCountStr = ' (' + str(likeCount) + ')' else: likeCountStr = ' (10+)' - likeIcon = 'like.png' if likedByPerson(postJsonObject, nickname, fullDomain): + likeIcon = 'like.png' likeLink = 'unlike' likeTitle = translate['Undo the like'] From 4763226f70460c01d491491059208a0d67002688 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:29:52 +0100 Subject: [PATCH 26/39] Show non-reader likes count --- epicyon-profile.css | 6 ++++++ webinterface.py | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 044c56d71..37b6f5eb0 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -26,6 +26,7 @@ --font-size3: 38px; --font-size4: 22px; --font-size5: 20px; + --font-size-likes: 16px; --font-size-pgp-key: 16px; --font-size-pgp-key2: 8px; --font-size-tox: 16px; @@ -138,6 +139,11 @@ a:focus { float: right; } +.likesCount { + font-size: var(--font-size-likes); + font-family: Arial, Helvetica, sans-serif; +} + .about { font-size: var(--font-size5); font-family: Arial, Helvetica, sans-serif; diff --git a/webinterface.py b/webinterface.py index c8381b031..a2f1cf4c3 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4214,12 +4214,14 @@ def individualPostAsHtml(allowDownloads: bool, likeCountStr = '' if likeCount > 0: - if likeCount > 1: - if likeCount <= 10: - likeCountStr = ' (' + str(likeCount) + ')' - else: - likeCountStr = ' (10+)' + 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'] @@ -4241,7 +4243,13 @@ def individualPostAsHtml(allowDownloads: bool, likeStr += \ '' + likeTitle + \
-            ' |\n' + ' |" src="/' + iconsDir + '/' + likeIcon + '"/>' + if likeCountStr: + # show the number of likes next to icon + likeStr += '' + likeStr += '\n' # benchmark 12.5 if not allowDownloads: From 991c84ccab2e3911b6eab82c6b8694cf55160507 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:34:41 +0100 Subject: [PATCH 27/39] Label after link --- webinterface.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webinterface.py b/webinterface.py index a2f1cf4c3..ded1672a0 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4243,13 +4243,12 @@ def individualPostAsHtml(allowDownloads: bool, likeStr += \ '' + likeTitle + \
-            ' |' + ' |" src="/' + iconsDir + '/' + likeIcon + '"/>\n' if likeCountStr: # show the number of likes next to icon likeStr += '' - likeStr += '\n' + likeStr += '\n' # benchmark 12.5 if not allowDownloads: From 14102aede1cfef3aacff51fbd223f5eba9f18a40 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:44:23 +0100 Subject: [PATCH 28/39] Likes count after icon --- epicyon-profile.css | 1 + webinterface.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 37b6f5eb0..e18fa067b 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -142,6 +142,7 @@ a:focus { .likesCount { font-size: var(--font-size-likes); font-family: Arial, Helvetica, sans-serif; + float: right; } .about { diff --git a/webinterface.py b/webinterface.py index ded1672a0..450935fab 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4232,6 +4232,11 @@ def individualPostAsHtml(allowDownloads: bool, if timeDiff > 100: print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff)) + if likeCountStr: + # show the number of likes next to icon + likeStr += '\n' likeStr = \ '\n' - if likeCountStr: - # show the number of likes next to icon - likeStr += '\n' # benchmark 12.5 if not allowDownloads: From 69393c2d7dc56d97eb68c6548b91a5db08867715 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 5 Sep 2020 12:46:33 +0100 Subject: [PATCH 29/39] Construct likes string --- webinterface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 450935fab..01dd70822 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4232,12 +4232,13 @@ def individualPostAsHtml(allowDownloads: bool, if timeDiff > 100: print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff)) + likeStr = '' if likeCountStr: # show the number of likes next to icon likeStr += '\n' - likeStr = \ + likeStr += \ '' editBlogImageSection += ' ' diff --git a/content.py b/content.py index 488601702..e10f2d66c 100644 --- a/content.py +++ b/content.py @@ -737,6 +737,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 +772,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 1c2852561..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 @@ -2522,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: @@ -3552,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): @@ -3848,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'): @@ -3890,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: @@ -3974,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, @@ -6965,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): @@ -7028,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: @@ -7075,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: @@ -7819,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': @@ -9005,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'): @@ -9079,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-profile.css b/epicyon-profile.css index 280bec865..43b9dd4e4 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -32,8 +32,6 @@ --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; 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/theme.py b/theme.py index 2b4b3ff70..6a7bd5377 100644 --- a/theme.py +++ b/theme.py @@ -801,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 67093ac98..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 diff --git a/webinterface.py b/webinterface.py index 01dd70822..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' @@ -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 += '
' @@ -4922,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' From 3ea12bc145c7b234878f131d8099e67ac705349e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 12 Sep 2020 10:50:24 +0100 Subject: [PATCH 37/39] Trap exception on tag --- person.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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(): From 08c18607d0271081e2e6cc17e806843aeb2f3006 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 13 Sep 2020 15:42:17 +0100 Subject: [PATCH 38/39] Automatically add hashtags --- content.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/content.py b/content.py index e10f2d66c..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(): From 7cebf0562ef84ee97cf1962babf8657225ef2634 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 13 Sep 2020 18:09:57 +0100 Subject: [PATCH 39/39] Switch to certbot acme v2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51eea597e..be08f1a32 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,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: