Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2020-09-13 18:16:50 +01:00
commit 01c34045a4
19 changed files with 357 additions and 75 deletions

View File

@ -182,7 +182,7 @@ ln -s /etc/nginx/sites-available/YOUR_DOMAIN /etc/nginx/sites-enabled/
Generate a LetsEncrypt certificate.
``` bash
certbot certonly -n --server https://acme-v01.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
certbot certonly -n --server https://acme-v02.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
```
And restart the web server:

44
auth.py
View File

@ -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:

View File

@ -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()

View File

@ -722,7 +722,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
editBlogImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
editBlogImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, ' + \
' accept=".png, .jpg, .jpeg, .gif, .webp, .avif, ' + \
'.mp4, .webm, .ogv, .mp3, .ogg">'
editBlogImageSection += ' </div>'

View File

@ -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

View File

@ -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')

View File

@ -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 {

View File

@ -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;

View File

@ -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] {

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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'):

View File

@ -890,7 +890,10 @@ def removeTagsForNickname(baseDir: str, nickname: str,
filename = os.fsdecode(f)
if not filename.endswith(".txt"):
continue
try:
tagFilename = os.path.join(directory, filename)
except BaseException:
continue
if not os.path.isfile(tagFilename):
continue
if matchStr not in open(tagFilename).read():

View File

@ -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'):
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()

View File

@ -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:

View File

@ -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,7 +369,6 @@ 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')
except Exception as e:
print('WARN: Failed to write entry to follow file ' +
@ -377,16 +376,20 @@ def followPerson(baseDir: str, nickname: str, domain: str,
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: {}):

View File

@ -230,6 +230,11 @@ def updateAvatarImageCache(session, baseDir: str, httpPrefix: str,
'Accept': 'image/webp'
}
avatarImageFilename = avatarImagePath + '.webp'
elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl:
sessionHeaders = {
'Accept': 'image/avif'
}
avatarImageFilename = avatarImagePath + '.avif'
else:
return None
@ -304,7 +309,7 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {},
actorStr = personJson['id'].replace('/', '-')
avatarImagePath = baseDir + '/cache/avatars/' + actorStr
imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp')
imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif')
for ext in imageExtension:
if os.path.isfile(avatarImagePath + '.' + ext):
return '/avatars/' + actorStr + '.' + ext
@ -1064,7 +1069,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit profile screen
"""
imageFormats = '.png, .jpg, .jpeg, .gif, .webp'
imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif'
pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
@ -1621,6 +1626,9 @@ def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str:
elif os.path.isfile(baseDir + '/accounts/login.webp'):
loginImage = 'login.webp'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.avif'):
loginImage = 'login.avif'
loginImageFilename = baseDir + '/accounts/' + loginImage
if not loginImageFilename:
loginImageFilename = baseDir + '/accounts/' + loginImage
@ -1965,13 +1973,13 @@ def htmlNewPost(mediaInstance: bool, translate: {},
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp">\n'
' accept=".png, .jpg, .jpeg, .gif, .webp, .avif">\n'
else:
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, ' + \
'.webm, .ogv, .mp3, .ogg">\n'
' accept=".png, .jpg, .jpeg, .gif, ' + \
'.webp, .avif, .mp4, .webm, .ogv, .mp3, .ogg">\n'
newPostImageSection += ' </div>\n'
scopeIcon = 'scope_public.png'
@ -2913,7 +2921,7 @@ def htmlProfile(defaultTimeline: str,
'/followapprove=' + followerHandle + '">'
followApprovalsSection += \
'<button class="followApprove">' + \
translate['Approve'] + '</button></a>'
translate['Approve'] + '</button></a><br><br>'
followApprovalsSection += \
'<a href="' + basePath + \
'/followdeny=' + followerHandle + '">'
@ -3623,11 +3631,13 @@ def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
if mediaType == 'image/png' or \
mediaType == 'image/jpeg' or \
mediaType == 'image/webp' or \
mediaType == 'image/avif' or \
mediaType == 'image/gif':
if attach['url'].endswith('.png') or \
attach['url'].endswith('.jpg') or \
attach['url'].endswith('.jpeg') or \
attach['url'].endswith('.webp') or \
attach['url'].endswith('.avif') or \
attach['url'].endswith('.gif'):
if attachmentCtr > 0:
attachmentStr += '<br>'
@ -4214,13 +4224,15 @@ def individualPostAsHtml(allowDownloads: bool,
likeCountStr = ''
if likeCount > 0:
if likeCount > 1:
if likeCount <= 10:
likeCountStr = ' (' + str(likeCount) + ')'
else:
likeCountStr = ' (10+)'
likeIcon = 'like.png'
if likedByPerson(postJsonObject, nickname, fullDomain):
if likeCount == 1:
# liked by the reader only
likeCountStr = ''
likeIcon = 'like.png'
likeLink = 'unlike'
likeTitle = translate['Undo the like']
@ -4230,7 +4242,13 @@ def individualPostAsHtml(allowDownloads: bool,
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff))
likeStr = \
likeStr = ''
if likeCountStr:
# show the number of likes next to icon
likeStr += '<label class="likesCount">'
likeStr += likeCountStr.replace('(', '').replace(')', '').strip()
likeStr += '</label>\n'
likeStr += \
'<a class="imageAnchor" href="/users/' + nickname + '?' + \
likeLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \
@ -4914,6 +4932,10 @@ def htmlTimeline(defaultTimeline: str,
bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.avif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.webp'
@ -6814,8 +6836,8 @@ def htmlSearch(translate: {},
baseDir + '/accounts/search-background.png')
cssFilename = baseDir + '/epicyon-search.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
if os.path.isfile(baseDir + '/search.css'):
cssFilename = baseDir + '/search.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)