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

main
Bob Mottram 2021-01-22 23:10:52 +00:00
commit d47d260bdf
81 changed files with 638 additions and 229 deletions

499
daemon.py
View File

@ -25,6 +25,9 @@ from webfinger import webfingerMeta
from webfinger import webfingerNodeInfo from webfinger import webfingerNodeInfo
from webfinger import webfingerLookup from webfinger import webfingerLookup
from webfinger import webfingerUpdate from webfinger import webfingerUpdate
from mastoapiv1 import getMastoApiV1Account
from mastoapiv1 import getMastApiV1Id
from mastoapiv1 import getNicknameFromMastoApiV1Id
from metadata import metaDataInstance from metadata import metaDataInstance
from metadata import metaDataNodeInfo from metadata import metaDataNodeInfo
from pgp import getEmailAddress from pgp import getEmailAddress
@ -526,7 +529,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain) self.send_header('Host', callingDomain)
self.send_header('WWW-Authenticate', self.send_header('WWW-Authenticate',
'title="Login to Epicyon", Basic realm="epicyon"') 'title="Login to Epicyon", Basic realm="epicyon"')
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('Referrer-Policy', 'origin')
self.end_headers() self.end_headers()
def _logout_headers(self, fileFormat: str, length: int, def _logout_headers(self, fileFormat: str, length: int,
@ -538,7 +543,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain) self.send_header('Host', callingDomain)
self.send_header('WWW-Authenticate', self.send_header('WWW-Authenticate',
'title="Login to Epicyon", Basic realm="epicyon"') 'title="Login to Epicyon", Basic realm="epicyon"')
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('Referrer-Policy', 'origin')
self.end_headers() self.end_headers()
def _logout_redirect(self, redirect: str, cookie: str, def _logout_redirect(self, redirect: str, cookie: str,
@ -553,7 +560,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain) self.send_header('Host', callingDomain)
self.send_header('InstanceID', self.server.instanceId) self.send_header('InstanceID', self.server.instanceId)
self.send_header('Content-Length', '0') self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('Referrer-Policy', 'origin')
self.end_headers() self.end_headers()
def _set_headers_base(self, fileFormat: str, length: int, cookie: str, def _set_headers_base(self, fileFormat: str, length: int, cookie: str,
@ -571,8 +580,10 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Cookie', cookieStr) self.send_header('Cookie', cookieStr)
self.send_header('Host', callingDomain) self.send_header('Host', callingDomain)
self.send_header('InstanceID', self.server.instanceId) self.send_header('InstanceID', self.server.instanceId)
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('X-Clacks-Overhead', 'GNU Natalie Nguyen') self.send_header('X-Clacks-Overhead', 'GNU Natalie Nguyen')
self.send_header('Referrer-Policy', 'origin')
self.send_header('Accept-Ranges', 'none') self.send_header('Accept-Ranges', 'none')
def _set_headers(self, fileFormat: str, length: int, cookie: str, def _set_headers(self, fileFormat: str, length: int, cookie: str,
@ -657,7 +668,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain) self.send_header('Host', callingDomain)
self.send_header('InstanceID', self.server.instanceId) self.send_header('InstanceID', self.server.instanceId)
self.send_header('Content-Length', '0') self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('Referrer-Policy', 'origin')
self.end_headers() self.end_headers()
def _httpReturnCode(self, httpCode: int, httpDescription: str, def _httpReturnCode(self, httpCode: int, httpDescription: str,
@ -677,7 +690,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Type', 'text/html; charset=utf-8')
msgLenStr = str(len(msg)) msgLenStr = str(len(msg))
self.send_header('Content-Length', msgLenStr) self.send_header('Content-Length', msgLenStr)
self.send_header('X-Robots-Tag', 'noindex') self.send_header('X-Robots-Tag',
'noindex, nofollow, noarchive, nosnippet')
self.send_header('Referrer-Policy', 'origin')
self.end_headers() self.end_headers()
if not self._write(msg): if not self._write(msg):
print('Error when showing ' + str(httpCode)) print('Error when showing ' + str(httpCode))
@ -769,26 +784,101 @@ class PubServer(BaseHTTPRequestHandler):
return True return True
return False return False
def _mastoApi(self, callingDomain: str) -> bool: def _mastoApiV1(self, path: str, callingDomain: str,
authorized: bool,
httpPrefix: str,
baseDir: str, nickname: str, domain: str,
domainFull: str) -> bool:
"""This is a vestigil mastodon API for the purpose """This is a vestigil mastodon API for the purpose
of returning an empty result to sites like of returning an empty result to sites like
https://mastopeek.app-dist.eu https://mastopeek.app-dist.eu
""" """
if not self.path.startswith('/api/v1/'): if not path.startswith('/api/v1/'):
return False return False
if self.server.debug: print('mastodon api v1: ' + path)
print('DEBUG: mastodon api ' + self.path) print('mastodon api v1: authorized ' + str(authorized))
print('mastodon api v1: nickname ' + str(nickname))
sendJson = None
sendJsonStr = ''
# parts of the api needing authorization
if authorized and nickname:
if path == '/api/v1/accounts/verify_credentials':
sendJson = getMastoApiV1Account(baseDir, nickname, domain)
sendJsonStr = 'masto API account sent for ' + nickname
# Parts of the api which don't need authorization
mastoId = getMastApiV1Id(path)
if mastoId is not None:
pathNickname = getNicknameFromMastoApiV1Id(mastoId)
if pathNickname:
originalPath = path
if '/followers?' in path or \
'/following?' in path or \
'/search?' in path or \
'/relationships?' in path or \
'/statuses?' in path:
path = path.split('?')[0]
if path.endswith('/followers'):
sendJson = []
sendJsonStr = 'masto API followers sent for ' + nickname
elif path.endswith('/following'):
sendJson = []
sendJsonStr = 'masto API following sent for ' + nickname
elif path.endswith('/statuses'):
sendJson = []
sendJsonStr = 'masto API statuses sent for ' + nickname
elif path.endswith('/search'):
sendJson = []
sendJsonStr = 'masto API search sent ' + originalPath
elif path.endswith('/relationships'):
sendJson = []
sendJsonStr = \
'masto API relationships sent ' + originalPath
else:
sendJson = \
getMastoApiV1Account(baseDir, pathNickname, domain)
sendJsonStr = 'masto API account sent for ' + nickname
if path.startswith('/api/v1/blocks'):
sendJson = []
sendJsonStr = 'masto API instance blocks sent'
elif path.startswith('/api/v1/favorites'):
sendJson = []
sendJsonStr = 'masto API favorites sent'
elif path.startswith('/api/v1/follow_requests'):
sendJson = []
sendJsonStr = 'masto API follow requests sent'
elif path.startswith('/api/v1/mutes'):
sendJson = []
sendJsonStr = 'masto API mutes sent'
elif path.startswith('/api/v1/notifications'):
sendJson = []
sendJsonStr = 'masto API notifications sent'
elif path.startswith('/api/v1/reports'):
sendJson = []
sendJsonStr = 'masto API reports sent'
elif path.startswith('/api/v1/statuses'):
sendJson = []
sendJsonStr = 'masto API statuses sent'
elif path.startswith('/api/v1/timelines'):
sendJson = []
sendJsonStr = 'masto API timelines sent'
adminNickname = getConfigParam(self.server.baseDir, 'admin') adminNickname = getConfigParam(self.server.baseDir, 'admin')
if adminNickname and self.path == '/api/v1/instance': if adminNickname and path == '/api/v1/instance':
instanceDescriptionShort = \ instanceDescriptionShort = \
getConfigParam(self.server.baseDir, getConfigParam(self.server.baseDir,
'instanceDescriptionShort') 'instanceDescriptionShort')
instanceDescriptionShort = 'Yet another Epicyon Instance' if not instanceDescriptionShort:
instanceDescriptionShort = \
self.server.translate['Yet another Epicyon Instance']
instanceDescription = getConfigParam(self.server.baseDir, instanceDescription = getConfigParam(self.server.baseDir,
'instanceDescription') 'instanceDescription')
instanceTitle = getConfigParam(self.server.baseDir, instanceTitle = getConfigParam(self.server.baseDir,
'instanceTitle') 'instanceTitle')
instanceJson = \ sendJson = \
metaDataInstance(instanceTitle, metaDataInstance(instanceTitle,
instanceDescriptionShort, instanceDescriptionShort,
instanceDescription, instanceDescription,
@ -800,29 +890,21 @@ class PubServer(BaseHTTPRequestHandler):
self.server.registration, self.server.registration,
self.server.systemLanguage, self.server.systemLanguage,
self.server.projectVersion) self.server.projectVersion)
msg = json.dumps(instanceJson).encode('utf-8') sendJsonStr = 'masto API instance metadata sent'
msglen = len(msg) elif path.startswith('/api/v1/instance/peers'):
if self._hasAccept(callingDomain):
if 'application/ld+json' in self.headers['Accept']:
self._set_headers('application/ld+json', msglen,
None, callingDomain)
else:
self._set_headers('application/json', msglen,
None, callingDomain)
else:
self._set_headers('application/ld+json', msglen,
None, callingDomain)
self._write(msg)
print('instance metadata sent')
return True
if self.path.startswith('/api/v1/instance/peers'):
# This is just a dummy result. # This is just a dummy result.
# Showing the full list of peers would have privacy implications. # Showing the full list of peers would have privacy implications.
# On a large instance you are somewhat lost in the crowd, but on # On a large instance you are somewhat lost in the crowd, but on
# small instances a full list of peers would convey a lot of # small instances a full list of peers would convey a lot of
# information about the interests of a small number of accounts # information about the interests of a small number of accounts
msg = json.dumps(['mastodon.social', sendJson = ['mastodon.social', self.server.domainFull]
self.server.domainFull]).encode('utf-8') sendJsonStr = 'masto API peers metadata sent'
elif path.startswith('/api/v1/instance/activity'):
sendJson = []
sendJsonStr = 'masto API activity metadata sent'
if sendJson is not None:
msg = json.dumps(sendJson).encode('utf-8')
msglen = len(msg) msglen = len(msg)
if self._hasAccept(callingDomain): if self._hasAccept(callingDomain):
if 'application/ld+json' in self.headers['Accept']: if 'application/ld+json' in self.headers['Accept']:
@ -835,28 +917,22 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers('application/ld+json', msglen, self._set_headers('application/ld+json', msglen,
None, callingDomain) None, callingDomain)
self._write(msg) self._write(msg)
print('instance peers metadata sent') if sendJsonStr:
return True print(sendJsonStr)
if self.path.startswith('/api/v1/instance/activity'):
# This is just a dummy result.
msg = json.dumps([]).encode('utf-8')
msglen = len(msg)
if self._hasAccept(callingDomain):
if 'application/ld+json' in self.headers['Accept']:
self._set_headers('application/ld+json', msglen,
None, callingDomain)
else:
self._set_headers('application/json', msglen,
None, callingDomain)
else:
self._set_headers('application/ld+json', msglen,
None, callingDomain)
self._write(msg)
print('instance activity metadata sent')
return True return True
# no api endpoints were matched
self._404() self._404()
return True return True
def _mastoApi(self, path: str, callingDomain: str,
authorized: bool, httpPrefix: str,
baseDir: str, nickname: str, domain: str,
domainFull: str) -> bool:
return self._mastoApiV1(path, callingDomain, authorized,
httpPrefix, baseDir, nickname, domain,
domainFull)
def _nodeinfo(self, callingDomain: str) -> bool: def _nodeinfo(self, callingDomain: str) -> bool:
if not self.path.startswith('/nodeinfo/2.0'): if not self.path.startswith('/nodeinfo/2.0'):
return False return False
@ -3872,7 +3948,7 @@ class PubServer(BaseHTTPRequestHandler):
if not actorJson.get('discoverable'): if not actorJson.get('discoverable'):
# discoverable in profile directory # discoverable in profile directory
# which isn't implemented in Epicyon # which isn't implemented in Epicyon
actorJson['discoverable'] = False actorJson['discoverable'] = True
actorChanged = True actorChanged = True
if not actorJson['@context'][2].get('orgSchema'): if not actorJson['@context'][2].get('orgSchema'):
actorJson['@context'][2]['orgSchema'] = \ actorJson['@context'][2]['orgSchema'] = \
@ -3890,10 +3966,6 @@ class PubServer(BaseHTTPRequestHandler):
if not actorJson['@context'][2].get('availability'): if not actorJson['@context'][2].get('availability'):
actorJson['@context'][2]['availaibility'] = \ actorJson['@context'][2]['availaibility'] = \
'toot:availability' 'toot:availability'
if not actorJson['@context'][2].get('nomadicLocations'):
actorJson['@context'][2]['nomadicLocations'] = \
'toot:nomadicLocations'
actorChanged = True
if actorJson.get('capabilityAcquisitionEndpoint'): if actorJson.get('capabilityAcquisitionEndpoint'):
del actorJson['capabilityAcquisitionEndpoint'] del actorJson['capabilityAcquisitionEndpoint']
actorChanged = True actorChanged = True
@ -4239,6 +4311,35 @@ class PubServer(BaseHTTPRequestHandler):
del actorJson['movedTo'] del actorJson['movedTo']
actorChanged = True actorChanged = True
# Other accounts (alsoKnownAs)
alsoKnownAs = []
if actorJson.get('alsoKnownAs'):
alsoKnownAs = actorJson['alsoKnownAs']
if fields.get('alsoKnownAs'):
alsoKnownAsStr = ''
alsoKnownAsCtr = 0
for altActor in alsoKnownAs:
if alsoKnownAsCtr > 0:
alsoKnownAsStr += ', '
alsoKnownAsStr += altActor
alsoKnownAsCtr += 1
if fields['alsoKnownAs'] != alsoKnownAsStr and \
'://' in fields['alsoKnownAs'] and \
'@' not in fields['alsoKnownAs'] and \
'.' in fields['alsoKnownAs']:
newAlsoKnownAs = fields['alsoKnownAs'].split(',')
alsoKnownAs = []
for altActor in newAlsoKnownAs:
altActor = altActor.strip()
if '://' in altActor and '.' in altActor:
alsoKnownAs.append(altActor)
actorJson['alsoKnownAs'] = alsoKnownAs
actorChanged = True
else:
if alsoKnownAs:
del actorJson['alsoKnownAs']
actorChanged = True
# change instance title # change instance title
if fields.get('instanceTitle'): if fields.get('instanceTitle'):
currInstanceTitle = \ currInstanceTitle = \
@ -4718,6 +4819,14 @@ class PubServer(BaseHTTPRequestHandler):
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
getDefaultPersonContext() getDefaultPersonContext()
] ]
if actorJson.get('nomadicLocations'):
del actorJson['nomadicLocations']
if not actorJson.get('featured'):
actorJson['featured'] = \
actorJson['id'] + '/collections/featured'
if not actorJson.get('featuredTags'):
actorJson['featuredTags'] = \
actorJson['id'] + '/collections/tags'
randomizeActorImages(actorJson) randomizeActorImages(actorJson)
saveJson(actorJson, actorFilename) saveJson(actorJson, actorFilename)
webfingerUpdate(baseDir, webfingerUpdate(baseDir,
@ -4920,7 +5029,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(faviconFilename, self._set_headers_etag(faviconFilename,
favType, favType,
favBinary, None, favBinary, None,
callingDomain) self.server.domainFull)
self._write(favBinary) self._write(favBinary)
if debug: if debug:
print('Sent favicon from cache: ' + callingDomain) print('Sent favicon from cache: ' + callingDomain)
@ -4932,7 +5041,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(faviconFilename, self._set_headers_etag(faviconFilename,
favType, favType,
favBinary, None, favBinary, None,
callingDomain) self.server.domainFull)
self._write(favBinary) self._write(favBinary)
self.server.iconsCache[favFilename] = favBinary self.server.iconsCache[favFilename] = favBinary
if self.server.debug: if self.server.debug:
@ -4971,7 +5080,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(fontFilename, self._set_headers_etag(fontFilename,
fontType, fontType,
fontBinary, None, fontBinary, None,
callingDomain) self.server.domainFull)
self._write(fontBinary) self._write(fontBinary)
if debug: if debug:
print('font sent from cache: ' + print('font sent from cache: ' +
@ -4987,7 +5096,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(fontFilename, self._set_headers_etag(fontFilename,
fontType, fontType,
fontBinary, None, fontBinary, None,
callingDomain) self.server.domainFull)
self._write(fontBinary) self._write(fontBinary)
self.server.fontsCache[fontStr] = fontBinary self.server.fontsCache[fontStr] = fontBinary
if debug: if debug:
@ -5279,6 +5388,7 @@ class PubServer(BaseHTTPRequestHandler):
ssbAddress = None ssbAddress = None
emailAddress = None emailAddress = None
lockedAccount = False lockedAccount = False
alsoKnownAs = None
movedTo = '' movedTo = ''
actorJson = getPersonFromCache(baseDir, actorJson = getPersonFromCache(baseDir,
optionsActor, optionsActor,
@ -5299,6 +5409,8 @@ class PubServer(BaseHTTPRequestHandler):
emailAddress = getEmailAddress(actorJson) emailAddress = getEmailAddress(actorJson)
PGPpubKey = getPGPpubKey(actorJson) PGPpubKey = getPGPpubKey(actorJson)
PGPfingerprint = getPGPfingerprint(actorJson) PGPfingerprint = getPGPfingerprint(actorJson)
if actorJson.get('alsoKnownAs'):
alsoKnownAs = actorJson['alsoKnownAs']
msg = htmlPersonOptions(self.server.defaultTimeline, msg = htmlPersonOptions(self.server.defaultTimeline,
self.server.cssCache, self.server.cssCache,
self.server.translate, self.server.translate,
@ -5318,7 +5430,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.dormantMonths, self.server.dormantMonths,
backToPath, backToPath,
lockedAccount, lockedAccount,
movedTo).encode('utf-8') movedTo, alsoKnownAs).encode('utf-8')
msglen = len(msg) msglen = len(msg)
self._set_headers('text/html', msglen, self._set_headers('text/html', msglen,
cookie, callingDomain) cookie, callingDomain)
@ -5367,7 +5479,7 @@ class PubServer(BaseHTTPRequestHandler):
mediaBinary = avFile.read() mediaBinary = avFile.read()
self._set_headers_etag(mediaFilename, mediaFileType, self._set_headers_etag(mediaFilename, mediaFileType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show emoji done', 'show emoji done',
@ -5407,7 +5519,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(emojiFilename, self._set_headers_etag(emojiFilename,
'image/' + mediaImageType, 'image/' + mediaImageType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'background shown done', 'background shown done',
@ -5443,7 +5555,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename, self._set_headers_etag(mediaFilename,
mimeTypeStr, mimeTypeStr,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
return return
else: else:
@ -5454,7 +5566,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename, self._set_headers_etag(mediaFilename,
mimeType, mimeType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self.server.iconsCache[mediaStr] = mediaBinary self.server.iconsCache[mediaStr] = mediaBinary
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
@ -5480,7 +5592,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename, self._set_headers_etag(mediaFilename,
mimeType, mimeType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done', 'icon shown done',
@ -8943,6 +9055,62 @@ class PubServer(BaseHTTPRequestHandler):
return True return True
return False return False
def _getFeaturedCollection(self, callingDomain: str,
path: str,
httpPrefix: str,
domainFull: str):
"""Returns the featured posts collections in
actor/collections/featured
TODO add ability to set a featured post
"""
featuredCollection = {
'@context': ['https://www.w3.org/ns/activitystreams',
{'atomUri': 'ostatus:atomUri',
'conversation': 'ostatus:conversation',
'inReplyToAtomUri': 'ostatus:inReplyToAtomUri',
'sensitive': 'as:sensitive',
'toot': 'http://joinmastodon.org/ns#',
'votersCount': 'toot:votersCount'}],
'id': httpPrefix + '://' + domainFull + path,
'orderedItems': [],
'totalItems': 0,
'type': 'OrderedCollection'
}
msg = json.dumps(featuredCollection,
ensure_ascii=False).encode('utf-8')
msglen = len(msg)
self._set_headers('application/json', msglen,
None, callingDomain)
self._write(msg)
def _getFeaturedTagsCollection(self, callingDomain: str,
path: str,
httpPrefix: str,
domainFull: str):
"""Returns the featured tags collections in
actor/collections/featuredTags
TODO add ability to set a featured tags
"""
featuredTagsCollection = {
'@context': ['https://www.w3.org/ns/activitystreams',
{'atomUri': 'ostatus:atomUri',
'conversation': 'ostatus:conversation',
'inReplyToAtomUri': 'ostatus:inReplyToAtomUri',
'sensitive': 'as:sensitive',
'toot': 'http://joinmastodon.org/ns#',
'votersCount': 'toot:votersCount'}],
'id': httpPrefix + '://' + domainFull + path,
'orderedItems': [],
'totalItems': 0,
'type': 'OrderedCollection'
}
msg = json.dumps(featuredTagsCollection,
ensure_ascii=False).encode('utf-8')
msglen = len(msg)
self._set_headers('application/json', msglen,
None, callingDomain)
self._write(msg)
def _showPersonProfile(self, authorized: bool, def _showPersonProfile(self, authorized: bool,
callingDomain: str, path: str, callingDomain: str, path: str,
baseDir: str, httpPrefix: str, baseDir: str, httpPrefix: str,
@ -8954,61 +9122,61 @@ class PubServer(BaseHTTPRequestHandler):
"""Shows the profile for a person """Shows the profile for a person
""" """
# look up a person # look up a person
getPerson = personLookup(domain, path, baseDir) actorJson = personLookup(domain, path, baseDir)
if getPerson: if not actorJson:
if self._requestHTTP(): return False
if self._requestHTTP():
if not self.server.session:
print('Starting new session during person lookup')
self.server.session = createSession(proxyType)
if not self.server.session: if not self.server.session:
print('Starting new session during person lookup') print('ERROR: GET failed to create session ' +
self.server.session = createSession(proxyType) 'during person lookup')
if not self.server.session: self._404()
print('ERROR: GET failed to create session ' + self.server.GETbusy = False
'during person lookup') return True
self._404() msg = \
self.server.GETbusy = False htmlProfile(self.server.rssIconAtTop,
return True self.server.cssCache,
msg = \ self.server.iconsAsButtons,
htmlProfile(self.server.rssIconAtTop, self.server.defaultTimeline,
self.server.cssCache, self.server.recentPostsCache,
self.server.iconsAsButtons, self.server.maxRecentPosts,
self.server.defaultTimeline, self.server.translate,
self.server.recentPostsCache, self.server.projectVersion,
self.server.maxRecentPosts, baseDir,
self.server.translate, httpPrefix,
self.server.projectVersion, authorized,
baseDir, actorJson, 'posts',
httpPrefix, self.server.session,
authorized, self.server.cachedWebfingers,
getPerson, 'posts', self.server.personCache,
self.server.session, self.server.YTReplacementDomain,
self.server.cachedWebfingers, self.server.showPublishedDateOnly,
self.server.personCache, self.server.newswire,
self.server.YTReplacementDomain, self.server.themeName,
self.server.showPublishedDateOnly, self.server.dormantMonths,
self.server.newswire, self.server.peertubeInstances,
self.server.themeName, None, None).encode('utf-8')
self.server.dormantMonths, msglen = len(msg)
self.server.peertubeInstances, self._set_headers('text/html', msglen,
None, None).encode('utf-8') cookie, callingDomain)
self._write(msg)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show profile 4 done',
'show profile posts')
else:
if self._fetchAuthenticated():
msgStr = json.dumps(actorJson, ensure_ascii=False)
msg = msgStr.encode('utf-8')
msglen = len(msg) msglen = len(msg)
self._set_headers('text/html', msglen, self._set_headers('application/ld+json', msglen,
cookie, callingDomain) cookie, callingDomain)
self._write(msg) self._write(msg)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show profile 4 done',
'show profile posts')
else: else:
if self._fetchAuthenticated(): self._404()
msg = json.dumps(getPerson, self.server.GETbusy = False
ensure_ascii=False).encode('utf-8') return True
msglen = len(msg)
self._set_headers('application/json', msglen,
None, callingDomain)
self._write(msg)
else:
self._404()
self.server.GETbusy = False
return True
return False
def _showBlogPage(self, authorized: bool, def _showBlogPage(self, authorized: bool,
callingDomain: str, path: str, callingDomain: str, path: str,
@ -9189,7 +9357,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(qrFilename) mimeType = mediaFileMimeType(qrFilename)
self._set_headers_etag(qrFilename, mimeType, self._set_headers_etag(qrFilename, mimeType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'login screen logo done', 'login screen logo done',
@ -9228,7 +9396,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(bannerFilename) mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType, self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done', 'account qrcode done',
@ -9270,7 +9438,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(bannerFilename) mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType, self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done', 'account qrcode done',
@ -9315,7 +9483,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(bgFilename, self._set_headers_etag(bgFilename,
'image/' + ext, 'image/' + ext,
bgBinary, None, bgBinary, None,
callingDomain) self.server.domainFull)
self._write(bgBinary) self._write(bgBinary)
self._benchmarkGETtimings(GETstartTime, self._benchmarkGETtimings(GETstartTime,
GETtimings, GETtimings,
@ -9359,7 +9527,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename, self._set_headers_etag(mediaFilename,
'image/' + mediaFileType, 'image/' + mediaFileType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show media done', 'show media done',
@ -9419,7 +9587,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(avatarFilename, self._set_headers_etag(avatarFilename,
'image/' + mediaImageType, 'image/' + mediaImageType,
mediaBinary, None, mediaBinary, None,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done', 'icon shown done',
@ -9722,14 +9890,6 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'start', '_nodeinfo[callingDomain]') 'start', '_nodeinfo[callingDomain]')
# minimal mastodon api
if self._mastoApi(callingDomain):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'_nodeinfo[callingDomain]',
'_mastoApi[callingDomain]')
if self.path == '/logout': if self.path == '/logout':
if not self.server.newsInstance: if not self.server.newsInstance:
msg = \ msg = \
@ -9822,6 +9982,19 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show logout', 'isAuthorized') 'show logout', 'isAuthorized')
# minimal mastodon api
if self._mastoApi(self.path, callingDomain, authorized,
self.server.httpPrefix,
self.server.baseDir,
self.authorizedNickname,
self.server.domain,
self.server.domainFull):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'_nodeinfo[callingDomain]',
'_mastoApi[callingDomain]')
if not self.server.session: if not self.server.session:
print('Starting new session during GET') print('Starting new session during GET')
self.server.session = createSession(self.server.proxyType) self.server.session = createSession(self.server.proxyType)
@ -9880,7 +10053,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.path == '/sharedInbox' or \ if self.path == '/sharedInbox' or \
self.path == '/users/inbox' or \ self.path == '/users/inbox' or \
self.path == '/actor/inbox' or \ self.path == '/actor/inbox' or \
self.path == '/users/'+self.server.domain: self.path == '/users/' + self.server.domain:
# if shared inbox is not enabled # if shared inbox is not enabled
if not self.server.enableSharedInbox: if not self.server.enableSharedInbox:
self._503() self._503()
@ -9958,6 +10131,24 @@ class PubServer(BaseHTTPRequestHandler):
self.server.debug) self.server.debug)
return return
usersInPath = False
if '/users/' in self.path:
usersInPath = True
if usersInPath and self.path.endswith('/collections/featured'):
self._getFeaturedCollection(callingDomain,
self.path,
self.server.httpPrefix,
self.server.domainFull)
return
if usersInPath and self.path.endswith('/collections/featuredTags'):
self._getFeaturedTagsCollection(callingDomain,
self.path,
self.server.httpPrefix,
self.server.domainFull)
return
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'sharedInbox enabled', 'rss3 done') 'sharedInbox enabled', 'rss3 done')
@ -10021,7 +10212,7 @@ class PubServer(BaseHTTPRequestHandler):
# list of registered devices for e2ee # list of registered devices for e2ee
# see https://github.com/tootsuite/mastodon/pull/13820 # see https://github.com/tootsuite/mastodon/pull/13820
if authorized and '/users/' in self.path: if authorized and usersInPath:
if self.path.endswith('/collections/devices'): if self.path.endswith('/collections/devices'):
nickname = self.path.split('/users/') nickname = self.path.split('/users/')
if '/' in nickname: if '/' in nickname:
@ -10047,7 +10238,7 @@ class PubServer(BaseHTTPRequestHandler):
'blog view done', 'blog view done',
'registered devices done') 'registered devices done')
if htmlGET and '/users/' in self.path: if htmlGET and usersInPath:
# show the person options screen with view/follow/block/report # show the person options screen with view/follow/block/report
if '?options=' in self.path: if '?options=' in self.path:
self._showPersonOptions(callingDomain, self.path, self._showPersonOptions(callingDomain, self.path,
@ -10165,7 +10356,7 @@ class PubServer(BaseHTTPRequestHandler):
'terms of service done') 'terms of service done')
# show a list of who you are following # show a list of who you are following
if htmlGET and authorized and '/users/' in self.path and \ if htmlGET and authorized and usersInPath and \
self.path.endswith('/followingaccounts'): self.path.endswith('/followingaccounts'):
nickname = getNicknameFromActor(self.path) nickname = getNicknameFromActor(self.path)
followingFilename = \ followingFilename = \
@ -10282,7 +10473,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(mediaFilename) mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename, mimeType, self._set_headers_etag(mediaFilename, mimeType,
mediaBinary, cookie, mediaBinary, cookie,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'profile.css done', 'profile.css done',
@ -10322,7 +10513,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(screenFilename) mimeType = mediaFileMimeType(screenFilename)
self._set_headers_etag(screenFilename, mimeType, self._set_headers_etag(screenFilename, mimeType,
mediaBinary, cookie, mediaBinary, cookie,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'manifest logo done', 'manifest logo done',
@ -10368,7 +10559,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(iconFilename, self._set_headers_etag(iconFilename,
mimeTypeStr, mimeTypeStr,
mediaBinary, cookie, mediaBinary, cookie,
callingDomain) self.server.domainFull)
self._write(mediaBinary) self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show screenshot done', 'show screenshot done',
@ -10382,7 +10573,7 @@ class PubServer(BaseHTTPRequestHandler):
'login screen logo done') 'login screen logo done')
# QR code for account handle # QR code for account handle
if '/users/' in self.path and \ if usersInPath and \
self.path.endswith('/qrcode.png'): self.path.endswith('/qrcode.png'):
if self._showQRcode(callingDomain, self.path, if self._showQRcode(callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
@ -10396,7 +10587,7 @@ class PubServer(BaseHTTPRequestHandler):
'account qrcode done') 'account qrcode done')
# search screen banner image # search screen banner image
if '/users/' in self.path: if usersInPath:
if self.path.endswith('/search_banner.png'): if self.path.endswith('/search_banner.png'):
if self._searchScreenBanner(callingDomain, self.path, if self._searchScreenBanner(callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
@ -10485,7 +10676,7 @@ class PubServer(BaseHTTPRequestHandler):
# cached avatar images # cached avatar images
# Note that this comes before the busy flag to avoid conflicts # Note that this comes before the busy flag to avoid conflicts
if self.path.startswith('/avatars/'): if self.path.startswith('/avatars/'):
self._showCachedAvatar(callingDomain, self.path, self._showCachedAvatar(self.server.domainFull, self.path,
self.server.baseDir, self.server.baseDir,
GETstartTime, GETtimings) GETstartTime, GETtimings)
return return
@ -10696,7 +10887,7 @@ class PubServer(BaseHTTPRequestHandler):
'hashtag search done') 'hashtag search done')
# show or hide buttons in the web interface # show or hide buttons in the web interface
if htmlGET and '/users/' in self.path and \ if htmlGET and usersInPath and \
self.path.endswith('/minimal') and \ self.path.endswith('/minimal') and \
authorized: authorized:
nickname = self.path.split('/users/')[1] nickname = self.path.split('/users/')[1]
@ -10716,7 +10907,7 @@ class PubServer(BaseHTTPRequestHandler):
# search for a fediverse address, shared item or emoji # search for a fediverse address, shared item or emoji
# from the web interface by selecting search icon # from the web interface by selecting search icon
if htmlGET and '/users/' in self.path: if htmlGET and usersInPath:
if self.path.endswith('/search') or \ if self.path.endswith('/search') or \
'/search?' in self.path: '/search?' in self.path:
if '?' in self.path: if '?' in self.path:
@ -10760,7 +10951,7 @@ class PubServer(BaseHTTPRequestHandler):
'search screen shown done') 'search screen shown done')
# Show the calendar for a user # Show the calendar for a user
if htmlGET and '/users/' in self.path: if htmlGET and usersInPath:
if '/calendar' in self.path: if '/calendar' in self.path:
# show the calendar screen # show the calendar screen
msg = htmlCalendar(self.server.cssCache, msg = htmlCalendar(self.server.cssCache,
@ -10782,7 +10973,7 @@ class PubServer(BaseHTTPRequestHandler):
'calendar shown done') 'calendar shown done')
# Show confirmation for deleting a calendar event # Show confirmation for deleting a calendar event
if htmlGET and '/users/' in self.path: if htmlGET and usersInPath:
if '/eventdelete' in self.path and \ if '/eventdelete' in self.path and \
'?time=' in self.path and \ '?time=' in self.path and \
'?id=' in self.path: '?id=' in self.path:
@ -10802,7 +10993,7 @@ class PubServer(BaseHTTPRequestHandler):
'calendar delete shown done') 'calendar delete shown done')
# search for emoji by name # search for emoji by name
if htmlGET and '/users/' in self.path: if htmlGET and usersInPath:
if self.path.endswith('/searchemoji'): if self.path.endswith('/searchemoji'):
# show the search screen # show the search screen
msg = htmlSearchEmojiTextEntry(self.server.cssCache, msg = htmlSearchEmojiTextEntry(self.server.cssCache,
@ -11320,7 +11511,7 @@ class PubServer(BaseHTTPRequestHandler):
'individual post done', 'individual post done',
'post replies done') 'post replies done')
if self.path.endswith('/roles') and '/users/' in self.path: if self.path.endswith('/roles') and usersInPath:
if self._showRoles(authorized, if self._showRoles(authorized,
callingDomain, self.path, callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
@ -11340,7 +11531,7 @@ class PubServer(BaseHTTPRequestHandler):
'show roles done') 'show roles done')
# show skills on the profile page # show skills on the profile page
if self.path.endswith('/skills') and '/users/' in self.path: if self.path.endswith('/skills') and usersInPath:
if self._showSkills(authorized, if self._showSkills(authorized,
callingDomain, self.path, callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
@ -11361,7 +11552,7 @@ class PubServer(BaseHTTPRequestHandler):
# get an individual post from the path # get an individual post from the path
# /users/nickname/statuses/number # /users/nickname/statuses/number
if '/statuses/' in self.path and '/users/' in self.path: if '/statuses/' in self.path and usersInPath:
if self._showIndividualPost(authorized, if self._showIndividualPost(authorized,
callingDomain, self.path, callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
@ -11548,7 +11739,7 @@ class PubServer(BaseHTTPRequestHandler):
'show shares 2 done') 'show shares 2 done')
# block a domain from htmlAccountInfo # block a domain from htmlAccountInfo
if authorized and '/users/' in self.path and \ if authorized and usersInPath and \
'/accountinfo?blockdomain=' in self.path and \ '/accountinfo?blockdomain=' in self.path and \
'?handle=' in self.path: '?handle=' in self.path:
nickname = self.path.split('/users/')[1] nickname = self.path.split('/users/')[1]
@ -11584,7 +11775,7 @@ class PubServer(BaseHTTPRequestHandler):
return return
# unblock a domain from htmlAccountInfo # unblock a domain from htmlAccountInfo
if authorized and '/users/' in self.path and \ if authorized and usersInPath and \
'/accountinfo?unblockdomain=' in self.path and \ '/accountinfo?unblockdomain=' in self.path and \
'?handle=' in self.path: '?handle=' in self.path:
nickname = self.path.split('/users/')[1] nickname = self.path.split('/users/')[1]
@ -12898,8 +13089,12 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3) self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
usersInPath = False
if '/users/' in self.path:
usersInPath = True
# moderator action buttons # moderator action buttons
if authorized and '/users/' in self.path and \ if authorized and usersInPath and \
self.path.endswith('/moderationaction'): self.path.endswith('/moderationaction'):
self._moderatorActions(self.path, callingDomain, cookie, self._moderatorActions(self.path, callingDomain, cookie,
self.server.baseDir, self.server.baseDir,
@ -13139,7 +13334,7 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 15) self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 15)
if self.path.endswith('/outbox') or self.path.endswith('/shares'): if self.path.endswith('/outbox') or self.path.endswith('/shares'):
if '/users/' in self.path: if usersInPath:
if authorized: if authorized:
self.outboxAuthenticated = True self.outboxAuthenticated = True
pathUsersSection = self.path.split('/users/')[1] pathUsersSection = self.path.split('/users/')[1]
@ -13186,7 +13381,7 @@ class PubServer(BaseHTTPRequestHandler):
# receive images to the outbox # receive images to the outbox
if self.headers['Content-type'].startswith('image/') and \ if self.headers['Content-type'].startswith('image/') and \
'/users/' in self.path: usersInPath:
self._receiveImage(length, callingDomain, cookie, self._receiveImage(length, callingDomain, cookie,
authorized, self.path, authorized, self.path,
self.server.baseDir, self.server.baseDir,
@ -13362,7 +13557,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.debug: if self.server.debug:
print('DEBUG: POST saving to inbox queue') print('DEBUG: POST saving to inbox queue')
if '/users/' in self.path: if usersInPath:
pathUsersSection = self.path.split('/users/')[1] pathUsersSection = self.path.split('/users/')[1]
if '/' not in pathUsersSection: if '/' not in pathUsersSection:
if self.server.debug: if self.server.debug:

View File

@ -17,6 +17,9 @@
--main-bg-color-report: #221c27; --main-bg-color-report: #221c27;
--main-header-color-roles: #282237; --main-header-color-roles: #282237;
--main-fg-color: #dddddd; --main-fg-color: #dddddd;
--cw-color: #dddddd;
--cw-style: normal;
--cw-weight: bold;
--column-left-fg-color: #dddddd; --column-left-fg-color: #dddddd;
--column-right-fg-color: yellow; --column-right-fg-color: yellow;
--column-right-fg-color-voted-on: red; --column-right-fg-color-voted-on: red;
@ -178,6 +181,12 @@ body, html {
line-height: var(--line-spacing); line-height: var(--line-spacing);
} }
.cw {
font-style: var(--cw-style);
font-weight: var(--cw-weight);
color: var(--cw-color);
}
.leftColIcons { .leftColIcons {
width: 100%; width: 100%;
background-color: var(--column-left-color); background-color: var(--column-left-color);

View File

@ -10,6 +10,8 @@
--border-color: #505050; --border-color: #505050;
--font-size-header: 18px; --font-size-header: 18px;
--font-color-header: #ccc; --font-color-header: #ccc;
--cw-color: #dddddd;
--cw-style: normal;
--font-size: 40px; --font-size: 40px;
--font-size2: 24px; --font-size2: 24px;
--font-size3: 38px; --font-size3: 38px;
@ -100,6 +102,11 @@ a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
.cw {
font-style: var(--cw-style);
color: var(--cw-color);
}
.domainHistogramLeft { .domainHistogramLeft {
float: right; float: right;
} }

78
mastoapiv1.py 100644
View File

@ -0,0 +1,78 @@
__filename__ = "mastoapiv1.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from utils import loadJson
def getMastApiV1Id(path: str) -> int:
"""Extracts the mastodon Id number from the given path
"""
mastoId = None
idPath = '/api/v1/accounts/:'
if not path.startswith(idPath):
return None
mastoIdStr = path.replace(idPath, '')
if '/' in mastoIdStr:
mastoIdStr = mastoIdStr.split('/')[0]
if mastoIdStr.isdigit():
mastoId = int(mastoIdStr)
return mastoId
return None
def getMastoApiV1IdFromNickname(nickname: str) -> int:
"""Given an account nickname return the corresponding mastodon id
"""
return int.from_bytes(nickname.encode('utf-8'), 'little')
def _intToBytes(num: int) -> str:
if num == 0:
return b""
else:
return _intToBytes(num // 256) + bytes([num % 256])
def getNicknameFromMastoApiV1Id(mastoId: int) -> str:
"""Given the mastodon Id return the nickname
"""
nickname = _intToBytes(mastoId).decode()
return nickname[::-1]
def getMastoApiV1Account(baseDir: str, nickname: str, domain: str) -> {}:
"""See https://github.com/McKael/mastodon-documentation/
blob/master/Using-the-API/API.md#account
Authorization has already been performed
"""
accountFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '.json'
if not os.path.isfile(accountFilename):
return {}
accountJson = loadJson(accountFilename)
if not accountJson:
return {}
mastoAccountJson = {
"id": getMastoApiV1IdFromNickname(nickname),
"username": nickname,
"acct": nickname,
"display_name": accountJson['name'],
"locked": accountJson['manuallyApprovesFollowers'],
"created_at": "2016-10-05T10:30:00Z",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"note": accountJson['summary'],
"url": accountJson['id'],
"avatar": accountJson['icon']['url'],
"avatar_static": accountJson['icon']['url'],
"header": accountJson['image']['url'],
"header_static": accountJson['image']['url']
}
return mastoAccountJson

View File

@ -170,38 +170,33 @@ def getDefaultPersonContext() -> str:
"""Gets the default actor context """Gets the default actor context
""" """
return { return {
'Emoji': 'toot:Emoji',
'Hashtag': 'as:Hashtag',
'IdentityProof': 'toot:IdentityProof',
'PropertyValue': 'schema:PropertyValue',
'alsoKnownAs': {
'@id': 'as:alsoKnownAs', '@type': '@id'
},
'focalPoint': {
'@container': '@list', '@id': 'toot:focalPoint'
},
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
'movedTo': {
'@id': 'as:movedTo', '@type': '@id'
},
'schema': 'http://schema.org#',
'value': 'schema:value',
'Curve25519Key': 'toot:Curve25519Key', 'Curve25519Key': 'toot:Curve25519Key',
'Device': 'toot:Device', 'Device': 'toot:Device',
'Ed25519Key': 'toot:Ed25519Key', 'Ed25519Key': 'toot:Ed25519Key',
'Ed25519Signature': 'toot:Ed25519Signature', 'Ed25519Signature': 'toot:Ed25519Signature',
'EncryptedMessage': 'toot:EncryptedMessage', 'EncryptedMessage': 'toot:EncryptedMessage',
'identityKey': {'@id': 'toot:identityKey', '@type': '@id'}, 'IdentityProof': 'toot:IdentityProof',
'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'}, 'PropertyValue': 'schema:PropertyValue',
'messageFranking': 'toot:messageFranking', 'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
'publicKeyBase64': 'toot:publicKeyBase64', 'cipherText': 'toot:cipherText',
'claim': {'@id': 'toot:claim', '@type': '@id'},
'deviceId': 'toot:deviceId',
'devices': {'@id': 'toot:devices', '@type': '@id'},
'discoverable': 'toot:discoverable', 'discoverable': 'toot:discoverable',
'orgSchema': 'toot:orgSchema', 'featured': {'@id': 'toot:featured', '@type': '@id'},
'shares': 'toot:shares', 'featuredTags': {'@id': 'toot:featuredTags', '@type': '@id'},
'skills': 'toot:skills', 'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
'roles': 'toot:roles', 'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
'availability': 'toot:availability', 'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
'nomadicLocations': 'toot:nomadicLocations' 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
'messageFranking': 'toot:messageFranking',
'messageType': 'toot:messageType',
'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
'publicKeyBase64': 'toot:publicKeyBase64',
'schema': 'http://schema.org#',
'suspended': 'toot:suspended',
'toot': 'http://joinmastodon.org/ns#',
'value': 'schema:value'
} }
@ -262,17 +257,18 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
getDefaultPersonContext() getDefaultPersonContext()
], ],
'attachment': [],
'alsoKnownAs': [], 'alsoKnownAs': [],
'discoverable': False, 'attachment': [],
'devices': personId + '/collections/devices', 'devices': personId + '/collections/devices',
'endpoints': { 'endpoints': {
'id': personId+'/endpoints', 'id': personId + '/endpoints',
'sharedInbox': httpPrefix+'://'+domain+'/inbox', 'sharedInbox': httpPrefix+'://' + domain + '/inbox',
}, },
'followers': personId+'/followers', 'featured': personId + '/collections/featured',
'following': personId+'/following', 'featuredTags': personId + '/collections/tags',
'shares': personId+'/shares', 'followers': personId + '/followers',
'following': personId + '/following',
'shares': personId + '/shares',
'orgSchema': None, 'orgSchema': None,
'skills': {}, 'skills': {},
'roles': {}, 'roles': {},
@ -290,26 +286,19 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
}, },
'inbox': inboxStr, 'inbox': inboxStr,
'manuallyApprovesFollowers': approveFollowers, 'manuallyApprovesFollowers': approveFollowers,
'discoverable': False, 'discoverable': True,
'name': personName, 'name': personName,
'outbox': personId+'/outbox', 'outbox': personId + '/outbox',
'preferredUsername': personName, 'preferredUsername': personName,
'summary': '', 'summary': '',
'publicKey': { 'publicKey': {
'id': personId+'#main-key', 'id': personId + '#main-key',
'owner': personId, 'owner': personId,
'publicKeyPem': publicKeyPem 'publicKeyPem': publicKeyPem
}, },
'tag': [], 'tag': [],
'type': personType, 'type': personType,
'url': personUrl, 'url': personUrl
'nomadicLocations': [{
'id': personId,
'type': 'nomadicLocation',
'locationAddress': 'acct:' + nickname + '@' + domain,
'locationPrimary': True,
'locationDeleted': False
}]
} }
if nickname == 'inbox': if nickname == 'inbox':
@ -551,16 +540,6 @@ def personUpgradeActor(baseDir: str, personJson: {},
return return
if not personJson: if not personJson:
personJson = loadJson(filename) personJson = loadJson(filename)
if not personJson.get('nomadicLocations'):
personJson['nomadicLocations'] = [{
'id': personJson['id'],
'type': 'nomadicLocation',
'locationAddress':'acct:'+handle,
'locationPrimary':True,
'locationDeleted':False
}]
print('Nomadic locations added to to actor ' + handle)
updateActor = True
if updateActor: if updateActor:
saveJson(personJson, filename) saveJson(personJson, filename)

View File

@ -92,6 +92,8 @@ from newsdaemon import hashtagRuleTree
from newsdaemon import hashtagRuleResolve from newsdaemon import hashtagRuleResolve
from newswire import getNewswireTags from newswire import getNewswireTags
from newswire import parseFeedDate from newswire import parseFeedDate
from mastoapiv1 import getMastoApiV1IdFromNickname
from mastoapiv1 import getNicknameFromMastoApiV1Id
testServerAliceRunning = False testServerAliceRunning = False
testServerBobRunning = False testServerBobRunning = False
@ -3046,9 +3048,21 @@ def testLinksWithinPost() -> None:
assert postJsonObject['object']['content'] == content assert postJsonObject['object']['content'] == content
def testMastoApi():
print('testMastoApi')
nickname = 'ThisIsATestNickname'
mastoId = getMastoApiV1IdFromNickname(nickname)
assert(mastoId)
nickname2 = getNicknameFromMastoApiV1Id(mastoId)
if nickname2 != nickname:
print(nickname + ' != ' + nickname2)
assert nickname2 == nickname
def runAllTests(): def runAllTests():
print('Running tests...') print('Running tests...')
testFunctions() testFunctions()
testMastoApi()
testLinksWithinPost() testLinksWithinPost()
testReplyToPublicPost() testReplyToPublicPost()
testGetMentionedPeople() testGetMentionedPeople()

View File

@ -51,6 +51,7 @@
"main-bg-color-reply": "white", "main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0", "main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"cw-color": "#2d2c37",
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"login-fg-color": "white", "login-fg-color": "white",
"options-fg-color": "lightgrey", "options-fg-color": "lightgrey",

View File

@ -17,6 +17,7 @@
"main-bg-color-reply": "#030202", "main-bg-color-reply": "#030202",
"main-bg-color-report": "#050202", "main-bg-color-report": "#050202",
"main-header-color-roles": "#1f192d", "main-header-color-roles": "#1f192d",
"cw-color": "#00ff00",
"main-fg-color": "#00ff00", "main-fg-color": "#00ff00",
"login-fg-color": "#00ff00", "login-fg-color": "#00ff00",
"options-fg-color": "#00ff00", "options-fg-color": "#00ff00",

View File

@ -34,6 +34,7 @@
"title-color": "white", "title-color": "white",
"main-visited-color": "#e1c4bc", "main-visited-color": "#e1c4bc",
"options-main-visited-color": "#e1c4bc", "options-main-visited-color": "#e1c4bc",
"cw-color": "white",
"main-fg-color": "white", "main-fg-color": "white",
"options-fg-color": "white", "options-fg-color": "white",
"column-left-fg-color": "white", "column-left-fg-color": "white",

View File

@ -44,6 +44,7 @@
"options-main-link-color-hover": "#d09338", "options-main-link-color-hover": "#d09338",
"main-visited-color": "#ffb900", "main-visited-color": "#ffb900",
"options-main-visited-color": "#ffb900", "options-main-visited-color": "#ffb900",
"cw-color": "white",
"main-fg-color": "white", "main-fg-color": "white",
"login-fg-color": "white", "login-fg-color": "white",
"options-fg-color": "white", "options-fg-color": "white",

View File

@ -99,6 +99,7 @@
"main-bg-color-reply": "white", "main-bg-color-reply": "white",
"main-bg-color-report": "white", "main-bg-color-report": "white",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"cw-color": "black",
"main-fg-color": "black", "main-fg-color": "black",
"login-fg-color": "black", "login-fg-color": "black",
"options-fg-color": "black", "options-fg-color": "black",

View File

@ -22,6 +22,7 @@
"main-bg-color-report": "#9fb42b", "main-bg-color-report": "#9fb42b",
"main-bg-color-dm": "#5fb42b", "main-bg-color-dm": "#5fb42b",
"main-header-color-roles": "#9fb42b", "main-header-color-roles": "#9fb42b",
"cw-color": "#33390d",
"main-fg-color": "#33390d", "main-fg-color": "#33390d",
"login-fg-color": "#33390d", "login-fg-color": "#33390d",
"options-fg-color": "#33390d", "options-fg-color": "#33390d",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,4 +1,8 @@
{ {
"button-selected": "#999",
"button-background": "#bbbbbb",
"button-background-hover": "#999",
"column-left-header-background": "#bbbbbb",
"newswire-publish-icon": "True", "newswire-publish-icon": "True",
"full-width-timeline-buttons": "False", "full-width-timeline-buttons": "False",
"icons-as-buttons": "False", "icons-as-buttons": "False",
@ -32,6 +36,7 @@
"main-bg-color-reply": "white", "main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0", "main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"cw-color": "#777",
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"login-fg-color": "#2d2c37", "login-fg-color": "#2d2c37",
"options-fg-color": "#2d2c37", "options-fg-color": "#2d2c37",

View File

@ -33,6 +33,7 @@
"main-link-color-hover": "#d09338", "main-link-color-hover": "#d09338",
"options-main-link-color": "#6481f5", "options-main-link-color": "#6481f5",
"options-main-link-color-hover": "#d09338", "options-main-link-color-hover": "#d09338",
"cw-color": "#0481f5",
"main-fg-color": "#0481f5", "main-fg-color": "#0481f5",
"login-fg-color": "#0481f5", "login-fg-color": "#0481f5",
"options-fg-color": "#0481f5", "options-fg-color": "#0481f5",

View File

@ -23,6 +23,7 @@
"main-bg-color-reply": "#1a142d", "main-bg-color-reply": "#1a142d",
"main-bg-color-report": "#12152d", "main-bg-color-report": "#12152d",
"main-header-color-roles": "#1f192d", "main-header-color-roles": "#1f192d",
"cw-color": "#f98bb0",
"main-fg-color": "#f98bb0", "main-fg-color": "#f98bb0",
"login-fg-color": "#f98bb0", "login-fg-color": "#f98bb0",
"options-fg-color": "#f98bb0", "options-fg-color": "#f98bb0",

View File

@ -54,6 +54,7 @@
"main-link-color-hover": "#46eed5", "main-link-color-hover": "#46eed5",
"options-main-link-color": "#05b9ec", "options-main-link-color": "#05b9ec",
"options-main-link-color-hover": "#46eed5", "options-main-link-color-hover": "#46eed5",
"cw-color": "white",
"main-fg-color": "white", "main-fg-color": "white",
"login-fg-color": "white", "login-fg-color": "white",
"options-fg-color": "white", "options-fg-color": "white",

View File

@ -40,6 +40,7 @@
"main-bg-color-reply": "white", "main-bg-color-reply": "white",
"main-bg-color-report": "white", "main-bg-color-report": "white",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"cw-color": "#2d2c37",
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"login-fg-color": "#2d2c37", "login-fg-color": "#2d2c37",
"options-fg-color": "#2d2c37", "options-fg-color": "#2d2c37",

View File

@ -33,6 +33,7 @@
"title-color": "#ffc4bc", "title-color": "#ffc4bc",
"main-visited-color": "#e1c4bc", "main-visited-color": "#e1c4bc",
"options-main-visited-color": "#e1c4bc", "options-main-visited-color": "#e1c4bc",
"cw-color": "#ffc4bc",
"main-fg-color": "#ffc4bc", "main-fg-color": "#ffc4bc",
"login-fg-color": "#ffc4bc", "login-fg-color": "#ffc4bc",
"options-fg-color": "#ffc4bc", "options-fg-color": "#ffc4bc",

View File

@ -1,5 +1,6 @@
{ {
"dropdown-bg-color-hover": "#463b35", "dropdown-bg-color-hover": "#463b35",
"cw-color": "#d5c7b7",
"main-fg-color": "#d5c7b7", "main-fg-color": "#d5c7b7",
"column-left-fg-color": "#d5c7b7", "column-left-fg-color": "#d5c7b7",
"button-text": "#d5c7b7", "button-text": "#d5c7b7",

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "يرسل المنشورات إلى الحسابات التالية", "Sends out posts to the following accounts": "يرسل المنشورات إلى الحسابات التالية",
"Word frequencies": "ترددات الكلمات", "Word frequencies": "ترددات الكلمات",
"New account": "حساب جديد", "New account": "حساب جديد",
"Moved to new account address": "انتقل إلى عنوان الحساب الجديد" "Moved to new account address": "انتقل إلى عنوان الحساب الجديد",
"Yet another Epicyon Instance": "مثال آخر Epicyon",
"Other accounts": "حسابات أخرى"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Envia publicacions als comptes següents", "Sends out posts to the following accounts": "Envia publicacions als comptes següents",
"Word frequencies": "Freqüències de paraules", "Word frequencies": "Freqüències de paraules",
"New account": "Compte nou", "New account": "Compte nou",
"Moved to new account address": "S'ha mogut a l'adreça del compte nova" "Moved to new account address": "S'ha mogut a l'adreça del compte nova",
"Yet another Epicyon Instance": "Encara una altra instància Epicyon",
"Other accounts": "Altres comptes"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Yn anfon postiadau i'r cyfrifon canlynol", "Sends out posts to the following accounts": "Yn anfon postiadau i'r cyfrifon canlynol",
"Word frequencies": "Amleddau geiriau", "Word frequencies": "Amleddau geiriau",
"New account": "Cyfrif newydd", "New account": "Cyfrif newydd",
"Moved to new account address": "Wedi'i symud i gyfeiriad cyfrif newydd" "Moved to new account address": "Wedi'i symud i gyfeiriad cyfrif newydd",
"Yet another Epicyon Instance": "Digwyddiad Epicyon arall",
"Other accounts": "Cyfrifon eraill"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Sendet Beiträge an die folgenden Konten", "Sends out posts to the following accounts": "Sendet Beiträge an die folgenden Konten",
"Word frequencies": "Worthäufigkeiten", "Word frequencies": "Worthäufigkeiten",
"New account": "Neues Konto", "New account": "Neues Konto",
"Moved to new account address": "An neue Kontoadresse verschoben" "Moved to new account address": "An neue Kontoadresse verschoben",
"Yet another Epicyon Instance": "Noch eine Epicyon-Instanz",
"Other accounts": "Andere Konten"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Sends out posts to the following accounts", "Sends out posts to the following accounts": "Sends out posts to the following accounts",
"Word frequencies": "Word frequencies", "Word frequencies": "Word frequencies",
"New account": "New account", "New account": "New account",
"Moved to new account address": "Moved to new account address" "Moved to new account address": "Moved to new account address",
"Yet another Epicyon Instance": "Yet another Epicyon Instance",
"Other accounts": "Other accounts"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Envía publicaciones a las siguientes cuentas", "Sends out posts to the following accounts": "Envía publicaciones a las siguientes cuentas",
"Word frequencies": "Frecuencias de palabras", "Word frequencies": "Frecuencias de palabras",
"New account": "Nueva cuenta", "New account": "Nueva cuenta",
"Moved to new account address": "Movido a la nueva dirección de la cuenta" "Moved to new account address": "Movido a la nueva dirección de la cuenta",
"Yet another Epicyon Instance": "Otra instancia más de Epicyon",
"Other accounts": "Otras cuentas"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Envoie des messages aux comptes suivants", "Sends out posts to the following accounts": "Envoie des messages aux comptes suivants",
"Word frequencies": "Fréquences des mots", "Word frequencies": "Fréquences des mots",
"New account": "Nouveau compte", "New account": "Nouveau compte",
"Moved to new account address": "Déplacé vers une nouvelle adresse de compte" "Moved to new account address": "Déplacé vers une nouvelle adresse de compte",
"Yet another Epicyon Instance": "Encore une autre instance Epicyon",
"Other accounts": "Autres comptes"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Seoltar poist chuig na cuntais seo a leanas", "Sends out posts to the following accounts": "Seoltar poist chuig na cuntais seo a leanas",
"Word frequencies": "Minicíochtaí focal", "Word frequencies": "Minicíochtaí focal",
"New account": "Cuntas nua", "New account": "Cuntas nua",
"Moved to new account address": "Ar athraíodh a ionad go seoladh cuntas nua" "Moved to new account address": "Ar athraíodh a ionad go seoladh cuntas nua",
"Yet another Epicyon Instance": "Institiúid Epicyon eile fós",
"Other accounts": "Cuntais eile"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "निम्नलिखित खातों में पोस्ट भेजता है", "Sends out posts to the following accounts": "निम्नलिखित खातों में पोस्ट भेजता है",
"Word frequencies": "शब्द आवृत्तियों", "Word frequencies": "शब्द आवृत्तियों",
"New account": "नया खाता", "New account": "नया खाता",
"Moved to new account address": "नए खाते के पते पर ले जाया गया" "Moved to new account address": "नए खाते के पते पर ले जाया गया",
"Yet another Epicyon Instance": "फिर भी एक और एपिकॉन उदाहरण",
"Other accounts": "अन्य खाते"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Invia messaggi ai seguenti account", "Sends out posts to the following accounts": "Invia messaggi ai seguenti account",
"Word frequencies": "Frequenze di parole", "Word frequencies": "Frequenze di parole",
"New account": "Nuovo account", "New account": "Nuovo account",
"Moved to new account address": "Spostato al nuovo indirizzo dell'account" "Moved to new account address": "Spostato al nuovo indirizzo dell'account",
"Yet another Epicyon Instance": "Ancora un'altra istanza di Epicyon",
"Other accounts": "Altri account"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "以下のアカウントに投稿を送信します", "Sends out posts to the following accounts": "以下のアカウントに投稿を送信します",
"Word frequencies": "単語の頻度", "Word frequencies": "単語の頻度",
"New account": "新しいアカウント", "New account": "新しいアカウント",
"Moved to new account address": "新しいアカウントアドレスに移動しました" "Moved to new account address": "新しいアカウントアドレスに移動しました",
"Yet another Epicyon Instance": "さらに別のエピキオンインスタンス",
"Other accounts": "その他のアカウント"
} }

View File

@ -354,5 +354,7 @@
"Sends out posts to the following accounts": "Sends out posts to the following accounts", "Sends out posts to the following accounts": "Sends out posts to the following accounts",
"Word frequencies": "Word frequencies", "Word frequencies": "Word frequencies",
"New account": "New account", "New account": "New account",
"Moved to new account address": "Moved to new account address" "Moved to new account address": "Moved to new account address",
"Yet another Epicyon Instance": "Yet another Epicyon Instance",
"Other accounts": "Other accounts"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Envia postagens para as seguintes contas", "Sends out posts to the following accounts": "Envia postagens para as seguintes contas",
"Word frequencies": "Frequências de palavras", "Word frequencies": "Frequências de palavras",
"New account": "Nova conta", "New account": "Nova conta",
"Moved to new account address": "Movido para o novo endereço da conta" "Moved to new account address": "Movido para o novo endereço da conta",
"Yet another Epicyon Instance": "Mais uma instância do Epicyon",
"Other accounts": "Outras contas"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "Отправляет сообщения на следующие аккаунты", "Sends out posts to the following accounts": "Отправляет сообщения на следующие аккаунты",
"Word frequencies": "Частоты слов", "Word frequencies": "Частоты слов",
"New account": "Новый аккаунт", "New account": "Новый аккаунт",
"Moved to new account address": "Перемещен на новый адрес учетной записи" "Moved to new account address": "Перемещен на новый адрес учетной записи",
"Yet another Epicyon Instance": "Еще один экземпляр Эпикиона",
"Other accounts": "Другие аккаунты"
} }

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "将帖子发送到以下帐户", "Sends out posts to the following accounts": "将帖子发送到以下帐户",
"Word frequencies": "词频", "Word frequencies": "词频",
"New account": "新账户", "New account": "新账户",
"Moved to new account address": "移至新帐户地址" "Moved to new account address": "移至新帐户地址",
"Yet another Epicyon Instance": "另一个Epicyon实例",
"Other accounts": "其他账户"
} }

View File

@ -48,7 +48,8 @@ def htmlPersonOptions(defaultTimeline: str,
dormantMonths: int, dormantMonths: int,
backToPath: str, backToPath: str,
lockedAccount: bool, lockedAccount: bool,
movedTo: str) -> str: movedTo: str,
alsoKnownAs: []) -> str:
"""Show options for a person: view/follow/block/report """Show options for a person: view/follow/block/report
""" """
optionsDomain, optionsPort = getDomainFromActor(optionsActor) optionsDomain, optionsPort = getDomainFromActor(optionsActor)
@ -143,6 +144,24 @@ def htmlPersonOptions(defaultTimeline: str,
' <p class="optionsText">' + \ ' <p class="optionsText">' + \
translate['New account'] + \ translate['New account'] + \
': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n' ': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
elif alsoKnownAs:
optionsStr += \
' <p class="optionsText">' + \
translate['Other accounts'] + ': '
if isinstance(alsoKnownAs, list):
ctr = 0
for altActor in alsoKnownAs:
if ctr > 0:
optionsStr += ' '
ctr += 1
altDomain, altPort = getDomainFromActor(altActor)
optionsStr += \
'<a href="' + altActor + '">' + altDomain + '</a>'
elif isinstance(alsoKnownAs, str):
altDomain, altPort = getDomainFromActor(alsoKnownAs)
optionsStr += '<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
optionsStr += '</p>\n'
if emailAddress: if emailAddress:
optionsStr += \ optionsStr += \
'<p class="imText">' + translate['Email'] + \ '<p class="imText">' + translate['Email'] + \

View File

@ -1521,7 +1521,8 @@ def individualPostAsHtml(allowDownloads: bool,
addEmojiToDisplayName(baseDir, httpPrefix, addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain, nickname, domain,
cwStr, False) cwStr, False)
contentStr += '<b>' + cwStr + '</b>\n ' contentStr += \
'<label class="cw">' + cwStr + '</label>\n '
if isModerationPost: if isModerationPost:
containerClass = 'container report' containerClass = 'container report'
# get the content warning text # get the content warning text

View File

@ -229,6 +229,10 @@ def htmlProfileAfterSearch(cssCache: {},
if profileJson['image'].get('url'): if profileJson['image'].get('url'):
imageUrl = profileJson['image']['url'] imageUrl = profileJson['image']['url']
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
profileStr = \ profileStr = \
_getProfileHeaderAfterSearch(baseDir, _getProfileHeaderAfterSearch(baseDir,
nickname, defaultTimeline, nickname, defaultTimeline,
@ -238,7 +242,7 @@ def htmlProfileAfterSearch(cssCache: {},
displayName, followsYou, displayName, followsYou,
profileDescriptionShort, profileDescriptionShort,
avatarUrl, imageUrl, avatarUrl, imageUrl,
movedTo) movedTo, alsoKnownAs)
domainFull = getFullDomain(domain, port) domainFull = getFullDomain(domain, port)
@ -306,7 +310,8 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
avatarDescription: str, avatarDescription: str,
profileDescriptionShort: str, profileDescriptionShort: str,
loginButton: str, avatarUrl: str, loginButton: str, avatarUrl: str,
theme: str, movedTo: str) -> str: theme: str, movedTo: str,
alsoKnownAs: []) -> str:
"""The header of the profile screen, containing background """The header of the profile screen, containing background
image and avatar image and avatar
""" """
@ -335,6 +340,23 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
' <p>' + translate['New account'] + ': ' + \ ' <p>' + translate['New account'] + ': ' + \
'<a href="' + movedTo + '">@' + \ '<a href="' + movedTo + '">@' + \
newNickname + '@' + newDomainFull + '</a><br>\n' newNickname + '@' + newDomainFull + '</a><br>\n'
elif alsoKnownAs:
htmlStr += \
' <p>' + translate['Other accounts'] + ': '
if isinstance(alsoKnownAs, list):
ctr = 0
for altActor in alsoKnownAs:
if ctr > 0:
htmlStr += ' '
ctr += 1
altDomain, altPort = getDomainFromActor(altActor)
htmlStr += \
'<a href="' + altActor + '">' + altDomain + '</a>'
elif isinstance(alsoKnownAs, str):
altDomain, altPort = getDomainFromActor(alsoKnownAs)
htmlStr += '<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
htmlStr += '</p>\n'
htmlStr += \ htmlStr += \
' <a href="/users/' + nickname + \ ' <a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \ '/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
@ -357,7 +379,8 @@ def _getProfileHeaderAfterSearch(baseDir: str,
followsYou: bool, followsYou: bool,
profileDescriptionShort: str, profileDescriptionShort: str,
avatarUrl: str, imageUrl: str, avatarUrl: str, imageUrl: str,
movedTo: str) -> str: movedTo: str,
alsoKnownAs: []) -> str:
"""The header of a searched for handle, containing background """The header of a searched for handle, containing background
image and avatar image and avatar
""" """
@ -388,6 +411,23 @@ def _getProfileHeaderAfterSearch(baseDir: str,
newHandle = newNickname + '@' + newDomainFull newHandle = newNickname + '@' + newDomainFull
htmlStr += ' <p>' + translate['New account'] + \ htmlStr += ' <p>' + translate['New account'] + \
': < a href="' + movedTo + '">@' + newHandle + '</a></p>\n' ': < a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
elif alsoKnownAs:
htmlStr += \
' <p>' + translate['Other accounts'] + ': '
if isinstance(alsoKnownAs, list):
ctr = 0
for altActor in alsoKnownAs:
if ctr > 0:
htmlStr += ' '
ctr += 1
altDomain, altPort = getDomainFromActor(altActor)
htmlStr += \
'<a href="' + altActor + '">' + altDomain + '</a>'
elif isinstance(alsoKnownAs, str):
altDomain, altPort = getDomainFromActor(alsoKnownAs)
htmlStr += '<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
htmlStr += '</p>\n'
htmlStr += ' <p>' + profileDescriptionShort + '</p>\n' htmlStr += ' <p>' + profileDescriptionShort + '</p>\n'
htmlStr += ' </figcaption>\n' htmlStr += ' </figcaption>\n'
@ -616,6 +656,10 @@ def htmlProfile(rssIconAtTop: bool,
if profileJson.get('movedTo'): if profileJson.get('movedTo'):
movedTo = profileJson['movedTo'] movedTo = profileJson['movedTo']
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
avatarUrl = profileJson['icon']['url'] avatarUrl = profileJson['icon']['url']
profileHeaderStr = \ profileHeaderStr = \
_getProfileHeader(baseDir, nickname, domain, _getProfileHeader(baseDir, nickname, domain,
@ -624,7 +668,7 @@ def htmlProfile(rssIconAtTop: bool,
avatarDescription, avatarDescription,
profileDescriptionShort, profileDescriptionShort,
loginButton, avatarUrl, theme, loginButton, avatarUrl, theme,
movedTo) movedTo, alsoKnownAs)
profileStr = profileHeaderStr + donateSection profileStr = profileHeaderStr + donateSection
profileStr += '<div class="container" id="buttonheader">\n' profileStr += '<div class="container" id="buttonheader">\n'
@ -1280,6 +1324,22 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
' <textarea id="message" name="bio" style="height:200px">' + \ ' <textarea id="message" name="bio" style="height:200px">' + \
bioStr + '</textarea>\n' bioStr + '</textarea>\n'
alsoKnownAsStr = ''
if actorJson.get('alsoKnownAs'):
alsoKnownAs = actorJson['alsoKnownAs']
ctr = 0
for altActor in alsoKnownAs:
if ctr > 0:
alsoKnownAsStr += ', '
ctr += 1
alsoKnownAsStr += altActor
editProfileForm += '<label class="labels">' + \
translate['Other accounts'] + ':</label><br>\n'
editProfileForm += \
' <input type="text" placeholder="https://..." ' + \
'name="alsoKnownAs" value="' + alsoKnownAsStr + '">\n'
editProfileForm += '<label class="labels">' + \ editProfileForm += '<label class="labels">' + \
translate['Moved to new account address'] + ':</label><br>\n' translate['Moved to new account address'] + ':</label><br>\n'
editProfileForm += \ editProfileForm += \

View File

@ -167,8 +167,8 @@ def getContentWarningButton(postID: str, translate: {},
content: str) -> str: content: str) -> str:
"""Returns the markup for a content warning button """Returns the markup for a content warning button
""" """
return ' <details><summary><b>' + \ return ' <details><summary class="cw">' + \
translate['SHOW MORE'] + '</b></summary>' + \ translate['SHOW MORE'] + '</summary>' + \
'<div id="' + postID + '">' + content + \ '<div id="' + postID + '">' + content + \
'</div></details>\n' '</div></details>\n'