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

main
Bob Mottram 2020-09-03 11:22:16 +01:00
commit 97ff118508
13 changed files with 6927 additions and 5822 deletions

22
README_roadmap.md 100644
View File

@ -0,0 +1,22 @@
# Roadman
## UX
* Change animation on buttons (themeable?)
## Teams
* Test groups
* Groups can be defined as having particular roles/skills
* Templates for different group organizations
## Events
* Events timeline
* Events appear on calendar
* Check compatibility with Mobilizon
## Code
* Modularize daemon
* Move modules out of the daemon
* Make comment notes linking daemon functions to webinterface

View File

@ -40,7 +40,7 @@ def createAcceptReject(baseDir: str, federationList: [],
newAccept = { newAccept = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
'type': acceptType, 'type': acceptType,
'actor': httpPrefix+'://'+domain+'/users/'+nickname, 'actor': httpPrefix+'://' + domain + '/users/' + nickname,
'to': [toUrl], 'to': [toUrl],
'cc': [], 'cc': [],
'object': objectJson 'object': objectJson
@ -107,33 +107,33 @@ def acceptFollow(baseDir: str, domain: str, messageJson: {},
thisActor = messageJson['object']['actor'] thisActor = messageJson['object']['actor']
nickname = getNicknameFromActor(thisActor) nickname = getNicknameFromActor(thisActor)
if not nickname: if not nickname:
print('WARN: no nickname found in '+thisActor) print('WARN: no nickname found in ' + thisActor)
return return
acceptedDomain, acceptedPort = getDomainFromActor(thisActor) acceptedDomain, acceptedPort = getDomainFromActor(thisActor)
if not acceptedDomain: if not acceptedDomain:
if debug: if debug:
print('DEBUG: domain not found in '+thisActor) print('DEBUG: domain not found in ' + thisActor)
return return
if not nickname: if not nickname:
if debug: if debug:
print('DEBUG: nickname not found in '+thisActor) print('DEBUG: nickname not found in ' + thisActor)
return return
if acceptedPort: if acceptedPort:
if '/' + acceptedDomain + ':' + str(acceptedPort) + \ if '/' + acceptedDomain + ':' + str(acceptedPort) + \
'/users/' + nickname not in thisActor: '/users/' + nickname not in thisActor:
if debug: if debug:
print('Port: '+str(acceptedPort)) print('Port: ' + str(acceptedPort))
print('Expected: /' + acceptedDomain + ':' + print('Expected: /' + acceptedDomain + ':' +
str(acceptedPort) + '/users/'+nickname) str(acceptedPort) + '/users/' + nickname)
print('Actual: '+thisActor) print('Actual: ' + thisActor)
print('DEBUG: unrecognized actor '+thisActor) print('DEBUG: unrecognized actor ' + thisActor)
return return
else: else:
if not '/' + acceptedDomain+'/users/' + nickname in thisActor: if not '/' + acceptedDomain+'/users/' + nickname in thisActor:
if debug: if debug:
print('Expected: /'+acceptedDomain+'/users/'+nickname) print('Expected: /' + acceptedDomain+'/users/' + nickname)
print('Actual: '+thisActor) print('Actual: ' + thisActor)
print('DEBUG: unrecognized actor '+thisActor) print('DEBUG: unrecognized actor ' + thisActor)
return return
followedActor = messageJson['object']['object'] followedActor = messageJson['object']['object']
followedDomain, port = getDomainFromActor(followedActor) followedDomain, port = getDomainFromActor(followedActor)

View File

@ -45,8 +45,9 @@ def getPersonFromCache(baseDir: str, personUrl: str, personCache: {},
# does the person exist as a cached file? # does the person exist as a cached file?
cacheFilename = baseDir + '/cache/actors/' + \ cacheFilename = baseDir + '/cache/actors/' + \
personUrl.replace('/', '#')+'.json' personUrl.replace('/', '#')+'.json'
if os.path.isfile(getFileCaseInsensitive(cacheFilename)): actorFilename = getFileCaseInsensitive(cacheFilename)
personJson = loadJson(getFileCaseInsensitive(cacheFilename)) if actorFilename:
personJson = loadJson(actorFilename)
if personJson: if personJson:
storePersonInCache(baseDir, personUrl, personJson, storePersonInCache(baseDir, personUrl, personJson,
personCache, False) personCache, False)

12488
daemon.py

File diff suppressed because it is too large Load Diff

View File

@ -992,8 +992,8 @@ aside .toggle-inside li {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding); padding: var(--button-height-padding);
width: 10%; width: 10%;
max-width: 100px; max-width: 200px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
@ -1009,7 +1009,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding); padding: var(--button-height-padding);
width: 10%; width: 10%;
max-width: 100px; max-width: 100px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
@ -1025,7 +1025,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding); padding: var(--button-height-padding);
width: 10%; width: 10%;
max-width: 100px; max-width: 100px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
@ -1041,7 +1041,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding); padding: var(--button-height-padding);
width: 10%; width: 10%;
max-width: 100px; max-width: 100px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
@ -1432,7 +1432,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile); padding: var(--button-height-padding-mobile);
width: 20%; width: 20%;
max-width: 400px; max-width: 400px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 15px; margin: 15px;
@ -1448,7 +1448,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile); padding: var(--button-height-padding-mobile);
width: 20%; width: 20%;
max-width: 400px; max-width: 400px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 15px; margin: 15px;
@ -1464,7 +1464,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile); padding: var(--button-height-padding-mobile);
width: 20%; width: 20%;
max-width: 400px; max-width: 400px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 15px; margin: 15px;
@ -1480,7 +1480,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile); padding: var(--button-height-padding-mobile);
width: 20%; width: 20%;
max-width: 400px; max-width: 400px;
min-width: 80px; min-width: 10ch;
transition: all 0.5s; transition: all 0.5s;
cursor: pointer; cursor: pointer;
margin: 15px; margin: 15px;

View File

@ -1819,21 +1819,22 @@ if YTDomain:
if setTheme(baseDir, themeName): if setTheme(baseDir, themeName):
print('Theme set to ' + themeName) print('Theme set to ' + themeName)
runDaemon(args.blogsinstance, args.mediainstance, if __name__ == "__main__":
args.maxRecentPosts, runDaemon(args.blogsinstance, args.mediainstance,
not args.nosharedinbox, args.maxRecentPosts,
registration, args.language, __version__, not args.nosharedinbox,
instanceId, args.client, baseDir, registration, args.language, __version__,
domain, onionDomain, i2pDomain, instanceId, args.client, baseDir,
args.YTReplacementDomain, domain, onionDomain, i2pDomain,
port, proxyPort, httpPrefix, args.YTReplacementDomain,
federationList, args.maxMentions, port, proxyPort, httpPrefix,
args.maxEmoji, args.authenticatedFetch, federationList, args.maxMentions,
args.noreply, args.nolike, args.nopics, args.maxEmoji, args.authenticatedFetch,
args.noannounce, args.cw, ocapAlways, args.noreply, args.nolike, args.nopics,
proxyType, args.maxReplies, args.noannounce, args.cw, ocapAlways,
args.domainMaxPostsPerDay, proxyType, args.maxReplies,
args.accountMaxPostsPerDay, args.domainMaxPostsPerDay,
args.allowdeletion, debug, False, args.accountMaxPostsPerDay,
args.instanceOnlySkillsSearch, [], args.allowdeletion, debug, False,
args.blurhash, not args.noapproval) args.instanceOnlySkillsSearch, [],
args.blurhash, not args.noapproval)

Binary file not shown.

View File

@ -8,6 +8,7 @@ Domestic_Manners by Dustin Norlander is under GPLv2. See https://www.1001fonts.c
Edition is public domain. See https://www.fontspace.com/edition-font-f35311 Edition is public domain. See https://www.fontspace.com/edition-font-f35311
ElectrumADFExp-Regular is by Arkandis Digital Foundry under GPLv2. See https://www.1001fonts.com/electrum-adf-exp-font.html ElectrumADFExp-Regular is by Arkandis Digital Foundry under GPLv2. See https://www.1001fonts.com/electrum-adf-exp-font.html
GeneralFailureRegular is public domain. See https://www.fontspace.com/general-failure-font-f32565 GeneralFailureRegular is public domain. See https://www.fontspace.com/general-failure-font-f32565
JetBrains is Apache 2.0. See https://www.jetbrains.com/lp/mono/
Judges is under GPL. See https://webfonts.ffonts.net/Judges.font Judges is under GPL. See https://webfonts.ffonts.net/Judges.font
LinBiolinum is under GPLv2. See https://www.1001fonts.com/linux-biolinum-font.html LinBiolinum is under GPLv2. See https://www.1001fonts.com/linux-biolinum-font.html
LcdSolid is public domain. See https://www.fontspace.com/lcd-solid-font-f11346 LcdSolid is public domain. See https://www.fontspace.com/lcd-solid-font-f11346

View File

@ -87,7 +87,13 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
return return
if not isinstance(postJsonObject['object']['tag'], list): if not isinstance(postJsonObject['object']['tag'], list):
return return
tagsDir = baseDir+'/tags' tagsDir = baseDir + '/tags'
# add tags directory if it doesn't exist
if not os.path.isdir(tagsDir):
print('Creating tags directory')
os.mkdir(tagsDir)
for tag in postJsonObject['object']['tag']: for tag in postJsonObject['object']['tag']:
if not tag.get('type'): if not tag.get('type'):
continue continue
@ -134,7 +140,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
avatarUrl = None avatarUrl = None
if boxname != 'tlevents' and boxname != 'outbox': if boxname != 'tlevents' and boxname != 'outbox':
boxName = 'inbox' boxName = 'inbox'
individualPostAsHtml(recentPostsCache, maxRecentPosts, individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
getIconsDir(baseDir), translate, pageNumber, getIconsDir(baseDir), translate, pageNumber,
baseDir, session, cachedWebfingers, personCache, baseDir, session, cachedWebfingers, personCache,
nickname, domain, port, postJsonObject, nickname, domain, port, postJsonObject,
@ -2364,17 +2370,18 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if nickname != 'inbox': if nickname != 'inbox':
# replies index will be updated # replies index will be updated
updateIndexList.append('tlreplies') updateIndexList.append('tlreplies')
inReplyTo = postJsonObject['object']['inReplyTo'] if postJsonObject['object'].get('inReplyTo'):
if inReplyTo: inReplyTo = postJsonObject['object']['inReplyTo']
if isinstance(inReplyTo, str): if inReplyTo:
if not isMuted(baseDir, nickname, domain, if isinstance(inReplyTo, str):
inReplyTo): if not isMuted(baseDir, nickname, domain,
replyNotify(baseDir, handle, inReplyTo):
httpPrefix + '://' + domain + replyNotify(baseDir, handle,
'/users/' + nickname + httpPrefix + '://' + domain +
'/tlreplies') '/users/' + nickname +
else: '/tlreplies')
isReplyToMutedPost = True else:
isReplyToMutedPost = True
if isImageMedia(session, baseDir, httpPrefix, if isImageMedia(session, baseDir, httpPrefix,
nickname, domain, postJsonObject, nickname, domain, postJsonObject,

View File

@ -31,7 +31,6 @@ from webfinger import webfingerHandle
from httpsig import createSignedHeader from httpsig import createSignedHeader
from utils import removeIdEnding from utils import removeIdEnding
from utils import siteIsActive from utils import siteIsActive
from utils import removePostFromCache
from utils import getCachedPostFilename from utils import getCachedPostFilename
from utils import getStatusNumber from utils import getStatusNumber
from utils import createPersonDir from utils import createPersonDir
@ -3535,19 +3534,20 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str,
if not postJsonObject: if not postJsonObject:
return return
print('MUTE: ' + postFilename) # remove cached post so that the muted version gets recreated
muteFile = open(postFilename + '.muted', 'w+') # without its content text and/or image
if muteFile:
muteFile.write('\n')
muteFile.close()
# remove cached posts so that the muted version gets created
cachedPostFilename = \ cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject) getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
if cachedPostFilename: if cachedPostFilename:
if os.path.isfile(cachedPostFilename): if os.path.isfile(cachedPostFilename):
os.remove(cachedPostFilename) os.remove(cachedPostFilename)
muteFile = open(postFilename + '.muted', 'w+')
if muteFile:
muteFile.write('\n')
muteFile.close()
print('MUTE: ' + postFilename + '.muted file added')
# if the post is in the recent posts cache then mark it as muted # if the post is in the recent posts cache then mark it as muted
if recentPostsCache.get('index'): if recentPostsCache.get('index'):
postId = \ postId = \
@ -3557,8 +3557,11 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str,
if recentPostsCache['json'].get(postId): if recentPostsCache['json'].get(postId):
postJsonObject['muted'] = True postJsonObject['muted'] = True
recentPostsCache['json'][postId] = json.dumps(postJsonObject) recentPostsCache['json'][postId] = json.dumps(postJsonObject)
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(postId):
del recentPostsCache['html'][postId]
print('MUTE: ' + postId + print('MUTE: ' + postId +
' marked as muted in recent posts cache') ' marked as muted in recent posts memory cache')
def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, def unmutePost(baseDir: str, nickname: str, domain: str, postId: str,
@ -3572,18 +3575,33 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str,
if not postJsonObject: if not postJsonObject:
return return
print('UNMUTE: ' + postFilename)
muteFilename = postFilename + '.muted' muteFilename = postFilename + '.muted'
if os.path.isfile(muteFilename): if os.path.isfile(muteFilename):
os.remove(muteFilename) os.remove(muteFilename)
print('UNMUTE: ' + muteFilename + ' file removed')
# remove cached posts so that it gets recreated # remove cached post so that the muted version gets recreated
# with its content text and/or image
cachedPostFilename = \ cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject) getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
if cachedPostFilename: if cachedPostFilename:
if os.path.isfile(cachedPostFilename): if os.path.isfile(cachedPostFilename):
os.remove(cachedPostFilename) os.remove(cachedPostFilename)
removePostFromCache(postJsonObject, recentPostsCache)
# if the post is in the recent posts cache then mark it as unmuted
if recentPostsCache.get('index'):
postId = \
removeIdEnding(postJsonObject['id']).replace('/', '#')
if postId in recentPostsCache['index']:
print('UNMUTE: ' + postId + ' is in recent posts cache')
if recentPostsCache['json'].get(postId):
postJsonObject['muted'] = False
recentPostsCache['json'][postId] = json.dumps(postJsonObject)
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(postId):
del recentPostsCache['html'][postId]
print('UNMUTE: ' + postId +
' marked as unmuted in recent posts cache')
def sendBlockViaServer(baseDir: str, session, def sendBlockViaServer(baseDir: str, session,

View File

@ -223,15 +223,15 @@ def setThemeDefault(baseDir: str):
name = 'default' name = 'default'
removeTheme(baseDir) removeTheme(baseDir)
setThemeInConfig(baseDir, name) setThemeInConfig(baseDir, name)
themeParams = {
"dummyValue": "1234"
}
bgParams = { bgParams = {
"login": "jpg", "login": "jpg",
"follow": "jpg", "follow": "jpg",
"options": "jpg", "options": "jpg",
"search": "jpg" "search": "jpg"
} }
themeParams = {
"dummy": "1234"
}
setThemeFromDict(baseDir, name, themeParams, bgParams) setThemeFromDict(baseDir, name, themeParams, bgParams)

View File

@ -365,26 +365,30 @@ def followPerson(baseDir: str, nickname: str, domain: str,
return True return True
# prepend to follow file # prepend to follow file
try: try:
with open(filename, 'r+') as followFile: with open(filename, 'r+') as f:
content = followFile.read() content = f.read()
followFile.seek(0, 0) f.seek(0, 0)
followFile.write(handleToFollow + '\n' + content) f.write(handleToFollow + '\n' + content)
if debug: if debug:
print('DEBUG: follow added') print('DEBUG: follow added')
return True
except Exception as e: except Exception as e:
print('WARN: Failed to write entry to follow file ' + print('WARN: Failed to write entry to follow file ' +
filename + ' ' + str(e)) filename + ' ' + str(e))
else:
# first follow
if debug:
print('DEBUG: creating new following file to follow ' +
handleToFollow)
with open(filename, 'w+') as f:
f.write(handleToFollow + '\n')
if followFile == 'following.txt': # Default to adding new follows to the calendar.
# if following a person add them to the list of # Possibly this could be made optional
# calendar follows if followFile == 'following.txt':
addPersonToCalendar(baseDir, nickname, domain, # if following a person add them to the list of
followNickname, followDomain) # calendar follows
if debug: addPersonToCalendar(baseDir, nickname, domain,
print('DEBUG: creating new following file to follow ' + handleToFollow) followNickname, followDomain)
with open(filename, 'w+') as followfile:
followfile.write(handleToFollow + '\n')
return True return True
@ -621,7 +625,7 @@ def validNickname(domain: str, nickname: str) -> bool:
'likes', 'users', 'statuses', 'likes', 'users', 'statuses',
'accounts', 'channels', 'profile', 'accounts', 'channels', 'profile',
'updates', 'repeat', 'announce', 'updates', 'repeat', 'announce',
'shares', 'fonts', 'icons') 'shares', 'fonts', 'icons', 'avatars')
if nickname in reservedNames: if nickname in reservedNames:
return False return False
return True return True
@ -907,21 +911,19 @@ def searchBoxPosts(baseDir: str, nickname: str, domain: str,
def getFileCaseInsensitive(path: str) -> str: def getFileCaseInsensitive(path: str) -> str:
"""Returns a case specific filename given a case insensitive version of it """Returns a case specific filename given a case insensitive version of it
""" """
# does the given file exist? If so then we don't need
# to do a directory search
if os.path.isfile(path): if os.path.isfile(path):
return path return path
if path != path.lower(): if path != path.lower():
if os.path.isfile(path.lower()): if os.path.isfile(path.lower()):
return path.lower() return path.lower()
directory, filename = os.path.split(path) # directory, filename = os.path.split(path)
directory, filename = (directory or '.'), filename.lower() # directory, filename = (directory or '.'), filename.lower()
for f in os.listdir(directory): # for f in os.listdir(directory):
if f.lower() == filename: # if f.lower() == filename:
newpath = os.path.join(directory, f) # newpath = os.path.join(directory, f)
if os.path.isfile(newpath): # if os.path.isfile(newpath):
return newpath # return newpath
return path return None
def undoLikesCollectionEntry(recentPostsCache: {}, def undoLikesCollectionEntry(recentPostsCache: {},

View File

@ -27,7 +27,6 @@ from matrix import getMatrixAddress
from donate import getDonationUrl from donate import getDonationUrl
from utils import removeIdEnding from utils import removeIdEnding
from utils import getProtocolPrefixes from utils import getProtocolPrefixes
from utils import getFileCaseInsensitive
from utils import searchBoxPosts from utils import searchBoxPosts
from utils import isEventPost from utils import isEventPost
from utils import isBlogPost from utils import isBlogPost
@ -304,16 +303,13 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {},
# get from locally stored image # get from locally stored image
actorStr = personJson['id'].replace('/', '-') actorStr = personJson['id'].replace('/', '-')
avatarImagePath = baseDir + '/cache/avatars/' + actorStr avatarImagePath = baseDir + '/cache/avatars/' + actorStr
if os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.png')):
return '/avatars/' + actorStr + '.png' imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp')
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.jpg')): for ext in imageExtension:
return '/avatars/' + actorStr + '.jpg' if os.path.isfile(avatarImagePath + '.' + ext):
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.gif')): return '/avatars/' + actorStr + '.' + ext
return '/avatars/' + actorStr + '.gif' elif os.path.isfile(avatarImagePath.lower() + '.' + ext):
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.webp')): return '/avatars/' + actorStr.lower() + '.' + ext
return '/avatars/' + actorStr + '.webp'
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath)):
return '/avatars/' + actorStr
if personJson.get('icon'): if personJson.get('icon'):
if personJson['icon'].get('url'): if personJson['icon'].get('url'):
@ -3812,6 +3808,9 @@ def individualPostAsHtml(allowDownloads: bool,
storeToCache=True) -> str: storeToCache=True) -> str:
""" Shows a single post as html """ Shows a single post as html
""" """
if not postJsonObject:
return ''
# benchmark # benchmark
postStartTime = time.time() postStartTime = time.time()
@ -6649,7 +6648,7 @@ def htmlCalendar(translate: {},
for weekOfMonth in range(1, 7): for weekOfMonth in range(1, 7):
if dayOfMonth == daysInMonth: if dayOfMonth == daysInMonth:
continue continue
calendarStr += ' <tr>\n' calendarStr += ' <tr>\n'
for dayNumber in range(1, 8): for dayNumber in range(1, 8):
if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \ if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \
(weekOfMonth == 1 and dayNumber >= dow): (weekOfMonth == 1 and dayNumber >= dow):