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

merge-requests/30/head
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 webfingerLookup
from webfinger import webfingerUpdate
from mastoapiv1 import getMastoApiV1Account
from mastoapiv1 import getMastApiV1Id
from mastoapiv1 import getNicknameFromMastoApiV1Id
from metadata import metaDataInstance
from metadata import metaDataNodeInfo
from pgp import getEmailAddress
@ -526,7 +529,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain)
self.send_header('WWW-Authenticate',
'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()
def _logout_headers(self, fileFormat: str, length: int,
@ -538,7 +543,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain)
self.send_header('WWW-Authenticate',
'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()
def _logout_redirect(self, redirect: str, cookie: str,
@ -553,7 +560,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Host', callingDomain)
self.send_header('InstanceID', self.server.instanceId)
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()
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('Host', callingDomain)
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('Referrer-Policy', 'origin')
self.send_header('Accept-Ranges', 'none')
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('InstanceID', self.server.instanceId)
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()
def _httpReturnCode(self, httpCode: int, httpDescription: str,
@ -677,7 +690,9 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('Content-Type', 'text/html; charset=utf-8')
msgLenStr = str(len(msg))
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()
if not self._write(msg):
print('Error when showing ' + str(httpCode))
@ -769,26 +784,101 @@ class PubServer(BaseHTTPRequestHandler):
return True
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
of returning an empty result to sites like
https://mastopeek.app-dist.eu
"""
if not self.path.startswith('/api/v1/'):
if not path.startswith('/api/v1/'):
return False
if self.server.debug:
print('DEBUG: mastodon api ' + self.path)
print('mastodon api v1: ' + 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')
if adminNickname and self.path == '/api/v1/instance':
if adminNickname and path == '/api/v1/instance':
instanceDescriptionShort = \
getConfigParam(self.server.baseDir,
'instanceDescriptionShort')
instanceDescriptionShort = 'Yet another Epicyon Instance'
if not instanceDescriptionShort:
instanceDescriptionShort = \
self.server.translate['Yet another Epicyon Instance']
instanceDescription = getConfigParam(self.server.baseDir,
'instanceDescription')
instanceTitle = getConfigParam(self.server.baseDir,
'instanceTitle')
instanceJson = \
sendJson = \
metaDataInstance(instanceTitle,
instanceDescriptionShort,
instanceDescription,
@ -800,29 +890,21 @@ class PubServer(BaseHTTPRequestHandler):
self.server.registration,
self.server.systemLanguage,
self.server.projectVersion)
msg = json.dumps(instanceJson).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 metadata sent')
return True
if self.path.startswith('/api/v1/instance/peers'):
sendJsonStr = 'masto API instance metadata sent'
elif path.startswith('/api/v1/instance/peers'):
# This is just a dummy result.
# Showing the full list of peers would have privacy implications.
# 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
# information about the interests of a small number of accounts
msg = json.dumps(['mastodon.social',
self.server.domainFull]).encode('utf-8')
sendJson = ['mastodon.social', self.server.domainFull]
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)
if self._hasAccept(callingDomain):
if 'application/ld+json' in self.headers['Accept']:
@ -835,28 +917,22 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers('application/ld+json', msglen,
None, callingDomain)
self._write(msg)
print('instance peers metadata sent')
return True
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')
if sendJsonStr:
print(sendJsonStr)
return True
# no api endpoints were matched
self._404()
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:
if not self.path.startswith('/nodeinfo/2.0'):
return False
@ -3872,7 +3948,7 @@ class PubServer(BaseHTTPRequestHandler):
if not actorJson.get('discoverable'):
# discoverable in profile directory
# which isn't implemented in Epicyon
actorJson['discoverable'] = False
actorJson['discoverable'] = True
actorChanged = True
if not actorJson['@context'][2].get('orgSchema'):
actorJson['@context'][2]['orgSchema'] = \
@ -3890,10 +3966,6 @@ class PubServer(BaseHTTPRequestHandler):
if not actorJson['@context'][2].get('availability'):
actorJson['@context'][2]['availaibility'] = \
'toot:availability'
if not actorJson['@context'][2].get('nomadicLocations'):
actorJson['@context'][2]['nomadicLocations'] = \
'toot:nomadicLocations'
actorChanged = True
if actorJson.get('capabilityAcquisitionEndpoint'):
del actorJson['capabilityAcquisitionEndpoint']
actorChanged = True
@ -4239,6 +4311,35 @@ class PubServer(BaseHTTPRequestHandler):
del actorJson['movedTo']
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
if fields.get('instanceTitle'):
currInstanceTitle = \
@ -4718,6 +4819,14 @@ class PubServer(BaseHTTPRequestHandler):
'https://w3id.org/security/v1',
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)
saveJson(actorJson, actorFilename)
webfingerUpdate(baseDir,
@ -4920,7 +5029,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(faviconFilename,
favType,
favBinary, None,
callingDomain)
self.server.domainFull)
self._write(favBinary)
if debug:
print('Sent favicon from cache: ' + callingDomain)
@ -4932,7 +5041,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(faviconFilename,
favType,
favBinary, None,
callingDomain)
self.server.domainFull)
self._write(favBinary)
self.server.iconsCache[favFilename] = favBinary
if self.server.debug:
@ -4971,7 +5080,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(fontFilename,
fontType,
fontBinary, None,
callingDomain)
self.server.domainFull)
self._write(fontBinary)
if debug:
print('font sent from cache: ' +
@ -4987,7 +5096,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(fontFilename,
fontType,
fontBinary, None,
callingDomain)
self.server.domainFull)
self._write(fontBinary)
self.server.fontsCache[fontStr] = fontBinary
if debug:
@ -5279,6 +5388,7 @@ class PubServer(BaseHTTPRequestHandler):
ssbAddress = None
emailAddress = None
lockedAccount = False
alsoKnownAs = None
movedTo = ''
actorJson = getPersonFromCache(baseDir,
optionsActor,
@ -5299,6 +5409,8 @@ class PubServer(BaseHTTPRequestHandler):
emailAddress = getEmailAddress(actorJson)
PGPpubKey = getPGPpubKey(actorJson)
PGPfingerprint = getPGPfingerprint(actorJson)
if actorJson.get('alsoKnownAs'):
alsoKnownAs = actorJson['alsoKnownAs']
msg = htmlPersonOptions(self.server.defaultTimeline,
self.server.cssCache,
self.server.translate,
@ -5318,7 +5430,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.dormantMonths,
backToPath,
lockedAccount,
movedTo).encode('utf-8')
movedTo, alsoKnownAs).encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
cookie, callingDomain)
@ -5367,7 +5479,7 @@ class PubServer(BaseHTTPRequestHandler):
mediaBinary = avFile.read()
self._set_headers_etag(mediaFilename, mediaFileType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show emoji done',
@ -5407,7 +5519,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(emojiFilename,
'image/' + mediaImageType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'background shown done',
@ -5443,7 +5555,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename,
mimeTypeStr,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
return
else:
@ -5454,7 +5566,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename,
mimeType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self.server.iconsCache[mediaStr] = mediaBinary
self._benchmarkGETtimings(GETstartTime, GETtimings,
@ -5480,7 +5592,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename,
mimeType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
@ -8943,6 +9055,62 @@ class PubServer(BaseHTTPRequestHandler):
return True
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,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
@ -8954,61 +9122,61 @@ class PubServer(BaseHTTPRequestHandler):
"""Shows the profile for a person
"""
# look up a person
getPerson = personLookup(domain, path, baseDir)
if getPerson:
if self._requestHTTP():
actorJson = personLookup(domain, path, baseDir)
if not actorJson:
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:
print('Starting new session during person lookup')
self.server.session = createSession(proxyType)
if not self.server.session:
print('ERROR: GET failed to create session ' +
'during person lookup')
self._404()
self.server.GETbusy = False
return True
msg = \
htmlProfile(self.server.rssIconAtTop,
self.server.cssCache,
self.server.iconsAsButtons,
self.server.defaultTimeline,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate,
self.server.projectVersion,
baseDir,
httpPrefix,
authorized,
getPerson, 'posts',
self.server.session,
self.server.cachedWebfingers,
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
self.server.themeName,
self.server.dormantMonths,
self.server.peertubeInstances,
None, None).encode('utf-8')
print('ERROR: GET failed to create session ' +
'during person lookup')
self._404()
self.server.GETbusy = False
return True
msg = \
htmlProfile(self.server.rssIconAtTop,
self.server.cssCache,
self.server.iconsAsButtons,
self.server.defaultTimeline,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate,
self.server.projectVersion,
baseDir,
httpPrefix,
authorized,
actorJson, 'posts',
self.server.session,
self.server.cachedWebfingers,
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
self.server.themeName,
self.server.dormantMonths,
self.server.peertubeInstances,
None, None).encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
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)
self._set_headers('text/html', msglen,
self._set_headers('application/ld+json', msglen,
cookie, callingDomain)
self._write(msg)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show profile 4 done',
'show profile posts')
else:
if self._fetchAuthenticated():
msg = json.dumps(getPerson,
ensure_ascii=False).encode('utf-8')
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
self._404()
self.server.GETbusy = False
return True
def _showBlogPage(self, authorized: bool,
callingDomain: str, path: str,
@ -9189,7 +9357,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(qrFilename)
self._set_headers_etag(qrFilename, mimeType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'login screen logo done',
@ -9228,7 +9396,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done',
@ -9270,7 +9438,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done',
@ -9315,7 +9483,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(bgFilename,
'image/' + ext,
bgBinary, None,
callingDomain)
self.server.domainFull)
self._write(bgBinary)
self._benchmarkGETtimings(GETstartTime,
GETtimings,
@ -9359,7 +9527,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(mediaFilename,
'image/' + mediaFileType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show media done',
@ -9419,7 +9587,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(avatarFilename,
'image/' + mediaImageType,
mediaBinary, None,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
@ -9722,14 +9890,6 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings,
'start', '_nodeinfo[callingDomain]')
# minimal mastodon api
if self._mastoApi(callingDomain):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'_nodeinfo[callingDomain]',
'_mastoApi[callingDomain]')
if self.path == '/logout':
if not self.server.newsInstance:
msg = \
@ -9822,6 +9982,19 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings,
'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:
print('Starting new session during GET')
self.server.session = createSession(self.server.proxyType)
@ -9880,7 +10053,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.path == '/sharedInbox' or \
self.path == '/users/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 not self.server.enableSharedInbox:
self._503()
@ -9958,6 +10131,24 @@ class PubServer(BaseHTTPRequestHandler):
self.server.debug)
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,
'sharedInbox enabled', 'rss3 done')
@ -10021,7 +10212,7 @@ class PubServer(BaseHTTPRequestHandler):
# list of registered devices for e2ee
# 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'):
nickname = self.path.split('/users/')
if '/' in nickname:
@ -10047,7 +10238,7 @@ class PubServer(BaseHTTPRequestHandler):
'blog view 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
if '?options=' in self.path:
self._showPersonOptions(callingDomain, self.path,
@ -10165,7 +10356,7 @@ class PubServer(BaseHTTPRequestHandler):
'terms of service done')
# 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'):
nickname = getNicknameFromActor(self.path)
followingFilename = \
@ -10282,7 +10473,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename, mimeType,
mediaBinary, cookie,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'profile.css done',
@ -10322,7 +10513,7 @@ class PubServer(BaseHTTPRequestHandler):
mimeType = mediaFileMimeType(screenFilename)
self._set_headers_etag(screenFilename, mimeType,
mediaBinary, cookie,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'manifest logo done',
@ -10368,7 +10559,7 @@ class PubServer(BaseHTTPRequestHandler):
self._set_headers_etag(iconFilename,
mimeTypeStr,
mediaBinary, cookie,
callingDomain)
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show screenshot done',
@ -10382,7 +10573,7 @@ class PubServer(BaseHTTPRequestHandler):
'login screen logo done')
# QR code for account handle
if '/users/' in self.path and \
if usersInPath and \
self.path.endswith('/qrcode.png'):
if self._showQRcode(callingDomain, self.path,
self.server.baseDir,
@ -10396,7 +10587,7 @@ class PubServer(BaseHTTPRequestHandler):
'account qrcode done')
# search screen banner image
if '/users/' in self.path:
if usersInPath:
if self.path.endswith('/search_banner.png'):
if self._searchScreenBanner(callingDomain, self.path,
self.server.baseDir,
@ -10485,7 +10676,7 @@ class PubServer(BaseHTTPRequestHandler):
# cached avatar images
# Note that this comes before the busy flag to avoid conflicts
if self.path.startswith('/avatars/'):
self._showCachedAvatar(callingDomain, self.path,
self._showCachedAvatar(self.server.domainFull, self.path,
self.server.baseDir,
GETstartTime, GETtimings)
return
@ -10696,7 +10887,7 @@ class PubServer(BaseHTTPRequestHandler):
'hashtag search done')
# 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 \
authorized:
nickname = self.path.split('/users/')[1]
@ -10716,7 +10907,7 @@ class PubServer(BaseHTTPRequestHandler):
# search for a fediverse address, shared item or emoji
# 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 \
'/search?' in self.path:
if '?' in self.path:
@ -10760,7 +10951,7 @@ class PubServer(BaseHTTPRequestHandler):
'search screen shown done')
# Show the calendar for a user
if htmlGET and '/users/' in self.path:
if htmlGET and usersInPath:
if '/calendar' in self.path:
# show the calendar screen
msg = htmlCalendar(self.server.cssCache,
@ -10782,7 +10973,7 @@ class PubServer(BaseHTTPRequestHandler):
'calendar shown done')
# Show confirmation for deleting a calendar event
if htmlGET and '/users/' in self.path:
if htmlGET and usersInPath:
if '/eventdelete' in self.path and \
'?time=' in self.path and \
'?id=' in self.path:
@ -10802,7 +10993,7 @@ class PubServer(BaseHTTPRequestHandler):
'calendar delete shown done')
# search for emoji by name
if htmlGET and '/users/' in self.path:
if htmlGET and usersInPath:
if self.path.endswith('/searchemoji'):
# show the search screen
msg = htmlSearchEmojiTextEntry(self.server.cssCache,
@ -11320,7 +11511,7 @@ class PubServer(BaseHTTPRequestHandler):
'individual post 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,
callingDomain, self.path,
self.server.baseDir,
@ -11340,7 +11531,7 @@ class PubServer(BaseHTTPRequestHandler):
'show roles done')
# 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,
callingDomain, self.path,
self.server.baseDir,
@ -11361,7 +11552,7 @@ class PubServer(BaseHTTPRequestHandler):
# get an individual post from the path
# /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,
callingDomain, self.path,
self.server.baseDir,
@ -11548,7 +11739,7 @@ class PubServer(BaseHTTPRequestHandler):
'show shares 2 done')
# block a domain from htmlAccountInfo
if authorized and '/users/' in self.path and \
if authorized and usersInPath and \
'/accountinfo?blockdomain=' in self.path and \
'?handle=' in self.path:
nickname = self.path.split('/users/')[1]
@ -11584,7 +11775,7 @@ class PubServer(BaseHTTPRequestHandler):
return
# unblock a domain from htmlAccountInfo
if authorized and '/users/' in self.path and \
if authorized and usersInPath and \
'/accountinfo?unblockdomain=' in self.path and \
'?handle=' in self.path:
nickname = self.path.split('/users/')[1]
@ -12898,8 +13089,12 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
usersInPath = False
if '/users/' in self.path:
usersInPath = True
# moderator action buttons
if authorized and '/users/' in self.path and \
if authorized and usersInPath and \
self.path.endswith('/moderationaction'):
self._moderatorActions(self.path, callingDomain, cookie,
self.server.baseDir,
@ -13139,7 +13334,7 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 15)
if self.path.endswith('/outbox') or self.path.endswith('/shares'):
if '/users/' in self.path:
if usersInPath:
if authorized:
self.outboxAuthenticated = True
pathUsersSection = self.path.split('/users/')[1]
@ -13186,7 +13381,7 @@ class PubServer(BaseHTTPRequestHandler):
# receive images to the outbox
if self.headers['Content-type'].startswith('image/') and \
'/users/' in self.path:
usersInPath:
self._receiveImage(length, callingDomain, cookie,
authorized, self.path,
self.server.baseDir,
@ -13362,7 +13557,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.debug:
print('DEBUG: POST saving to inbox queue')
if '/users/' in self.path:
if usersInPath:
pathUsersSection = self.path.split('/users/')[1]
if '/' not in pathUsersSection:
if self.server.debug:

View File

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

View File

@ -10,6 +10,8 @@
--border-color: #505050;
--font-size-header: 18px;
--font-color-header: #ccc;
--cw-color: #dddddd;
--cw-style: normal;
--font-size: 40px;
--font-size2: 24px;
--font-size3: 38px;
@ -100,6 +102,11 @@ a:focus {
border: 2px solid var(--focus-color);
}
.cw {
font-style: var(--cw-style);
color: var(--cw-color);
}
.domainHistogramLeft {
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
"""
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',
'Device': 'toot:Device',
'Ed25519Key': 'toot:Ed25519Key',
'Ed25519Signature': 'toot:Ed25519Signature',
'EncryptedMessage': 'toot:EncryptedMessage',
'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
'messageFranking': 'toot:messageFranking',
'publicKeyBase64': 'toot:publicKeyBase64',
'IdentityProof': 'toot:IdentityProof',
'PropertyValue': 'schema:PropertyValue',
'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
'cipherText': 'toot:cipherText',
'claim': {'@id': 'toot:claim', '@type': '@id'},
'deviceId': 'toot:deviceId',
'devices': {'@id': 'toot:devices', '@type': '@id'},
'discoverable': 'toot:discoverable',
'orgSchema': 'toot:orgSchema',
'shares': 'toot:shares',
'skills': 'toot:skills',
'roles': 'toot:roles',
'availability': 'toot:availability',
'nomadicLocations': 'toot:nomadicLocations'
'featured': {'@id': 'toot:featured', '@type': '@id'},
'featuredTags': {'@id': 'toot:featuredTags', '@type': '@id'},
'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
'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',
getDefaultPersonContext()
],
'attachment': [],
'alsoKnownAs': [],
'discoverable': False,
'attachment': [],
'devices': personId + '/collections/devices',
'endpoints': {
'id': personId+'/endpoints',
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
'id': personId + '/endpoints',
'sharedInbox': httpPrefix+'://' + domain + '/inbox',
},
'followers': personId+'/followers',
'following': personId+'/following',
'shares': personId+'/shares',
'featured': personId + '/collections/featured',
'featuredTags': personId + '/collections/tags',
'followers': personId + '/followers',
'following': personId + '/following',
'shares': personId + '/shares',
'orgSchema': None,
'skills': {},
'roles': {},
@ -290,26 +286,19 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
},
'inbox': inboxStr,
'manuallyApprovesFollowers': approveFollowers,
'discoverable': False,
'discoverable': True,
'name': personName,
'outbox': personId+'/outbox',
'outbox': personId + '/outbox',
'preferredUsername': personName,
'summary': '',
'publicKey': {
'id': personId+'#main-key',
'id': personId + '#main-key',
'owner': personId,
'publicKeyPem': publicKeyPem
},
'tag': [],
'type': personType,
'url': personUrl,
'nomadicLocations': [{
'id': personId,
'type': 'nomadicLocation',
'locationAddress': 'acct:' + nickname + '@' + domain,
'locationPrimary': True,
'locationDeleted': False
}]
'url': personUrl
}
if nickname == 'inbox':
@ -551,16 +540,6 @@ def personUpgradeActor(baseDir: str, personJson: {},
return
if not personJson:
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:
saveJson(personJson, filename)

View File

@ -92,6 +92,8 @@ from newsdaemon import hashtagRuleTree
from newsdaemon import hashtagRuleResolve
from newswire import getNewswireTags
from newswire import parseFeedDate
from mastoapiv1 import getMastoApiV1IdFromNickname
from mastoapiv1 import getNicknameFromMastoApiV1Id
testServerAliceRunning = False
testServerBobRunning = False
@ -3046,9 +3048,21 @@ def testLinksWithinPost() -> None:
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():
print('Running tests...')
testFunctions()
testMastoApi()
testLinksWithinPost()
testReplyToPublicPost()
testGetMentionedPeople()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@
"main-bg-color-report": "#9fb42b",
"main-bg-color-dm": "#5fb42b",
"main-header-color-roles": "#9fb42b",
"cw-color": "#33390d",
"main-fg-color": "#33390d",
"login-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",
"full-width-timeline-buttons": "False",
"icons-as-buttons": "False",
@ -32,6 +36,7 @@
"main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0",
"cw-color": "#777",
"main-fg-color": "#2d2c37",
"login-fg-color": "#2d2c37",
"options-fg-color": "#2d2c37",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -358,5 +358,7 @@
"Sends out posts to the following accounts": "يرسل المنشورات إلى الحسابات التالية",
"Word frequencies": "ترددات الكلمات",
"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",
"Word frequencies": "Freqüències de paraules",
"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",
"Word frequencies": "Amleddau geiriau",
"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",
"Word frequencies": "Worthäufigkeiten",
"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",
"Word frequencies": "Word frequencies",
"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",
"Word frequencies": "Frecuencias de palabras",
"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",
"Word frequencies": "Fréquences des mots",
"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",
"Word frequencies": "Minicíochtaí focal",
"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": "निम्नलिखित खातों में पोस्ट भेजता है",
"Word frequencies": "शब्द आवृत्तियों",
"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",
"Word frequencies": "Frequenze di parole",
"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": "以下のアカウントに投稿を送信します",
"Word frequencies": "単語の頻度",
"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",
"Word frequencies": "Word frequencies",
"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",
"Word frequencies": "Frequências de palavras",
"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": "Отправляет сообщения на следующие аккаунты",
"Word frequencies": "Частоты слов",
"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": "将帖子发送到以下帐户",
"Word frequencies": "词频",
"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,
backToPath: str,
lockedAccount: bool,
movedTo: str) -> str:
movedTo: str,
alsoKnownAs: []) -> str:
"""Show options for a person: view/follow/block/report
"""
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
@ -143,6 +144,24 @@ def htmlPersonOptions(defaultTimeline: str,
' <p class="optionsText">' + \
translate['New account'] + \
': <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:
optionsStr += \
'<p class="imText">' + translate['Email'] + \

View File

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

View File

@ -229,6 +229,10 @@ def htmlProfileAfterSearch(cssCache: {},
if profileJson['image'].get('url'):
imageUrl = profileJson['image']['url']
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
profileStr = \
_getProfileHeaderAfterSearch(baseDir,
nickname, defaultTimeline,
@ -238,7 +242,7 @@ def htmlProfileAfterSearch(cssCache: {},
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl,
movedTo)
movedTo, alsoKnownAs)
domainFull = getFullDomain(domain, port)
@ -306,7 +310,8 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
avatarDescription: str,
profileDescriptionShort: str,
loginButton: str, avatarUrl: str,
theme: str, movedTo: str) -> str:
theme: str, movedTo: str,
alsoKnownAs: []) -> str:
"""The header of the profile screen, containing background
image and avatar
"""
@ -335,6 +340,23 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
' <p>' + translate['New account'] + ': ' + \
'<a href="' + movedTo + '">@' + \
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 += \
' <a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
@ -357,7 +379,8 @@ def _getProfileHeaderAfterSearch(baseDir: str,
followsYou: bool,
profileDescriptionShort: str,
avatarUrl: str, imageUrl: str,
movedTo: str) -> str:
movedTo: str,
alsoKnownAs: []) -> str:
"""The header of a searched for handle, containing background
image and avatar
"""
@ -388,6 +411,23 @@ def _getProfileHeaderAfterSearch(baseDir: str,
newHandle = newNickname + '@' + newDomainFull
htmlStr += ' <p>' + translate['New account'] + \
': < 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 += ' </figcaption>\n'
@ -616,6 +656,10 @@ def htmlProfile(rssIconAtTop: bool,
if profileJson.get('movedTo'):
movedTo = profileJson['movedTo']
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
avatarUrl = profileJson['icon']['url']
profileHeaderStr = \
_getProfileHeader(baseDir, nickname, domain,
@ -624,7 +668,7 @@ def htmlProfile(rssIconAtTop: bool,
avatarDescription,
profileDescriptionShort,
loginButton, avatarUrl, theme,
movedTo)
movedTo, alsoKnownAs)
profileStr = profileHeaderStr + donateSection
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">' + \
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">' + \
translate['Moved to new account address'] + ':</label><br>\n'
editProfileForm += \

View File

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