diff --git a/daemon.py b/daemon.py index 83320fc8b..ff72046bb 100644 --- a/daemon.py +++ b/daemon.py @@ -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: diff --git a/epicyon-profile.css b/epicyon-profile.css index 135030495..b4f1e85f3 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -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); diff --git a/epicyon-search.css b/epicyon-search.css index 65980b641..9f61bcd1f 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -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; } diff --git a/mastoapiv1.py b/mastoapiv1.py new file mode 100644 index 000000000..dd2ff9ffc --- /dev/null +++ b/mastoapiv1.py @@ -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 diff --git a/person.py b/person.py index d4194e936..95c6f9bca 100644 --- a/person.py +++ b/person.py @@ -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) diff --git a/tests.py b/tests.py index b9121b1ef..2bebb1f2c 100644 --- a/tests.py +++ b/tests.py @@ -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() diff --git a/theme/debian/theme.json b/theme/debian/theme.json index 126109ff0..225bf948c 100644 --- a/theme/debian/theme.json +++ b/theme/debian/theme.json @@ -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", diff --git a/theme/hacker/theme.json b/theme/hacker/theme.json index bda1565e9..a33b48b7b 100644 --- a/theme/hacker/theme.json +++ b/theme/hacker/theme.json @@ -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", diff --git a/theme/henge/theme.json b/theme/henge/theme.json index b8685b1ef..0048d7b0d 100644 --- a/theme/henge/theme.json +++ b/theme/henge/theme.json @@ -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", diff --git a/theme/indymediaclassic/theme.json b/theme/indymediaclassic/theme.json index 32621e0a3..80d9c6eff 100644 --- a/theme/indymediaclassic/theme.json +++ b/theme/indymediaclassic/theme.json @@ -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", diff --git a/theme/indymediamodern/theme.json b/theme/indymediamodern/theme.json index aab14209e..91859ddaa 100644 --- a/theme/indymediamodern/theme.json +++ b/theme/indymediamodern/theme.json @@ -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", diff --git a/theme/lcd/theme.json b/theme/lcd/theme.json index f0062ce12..8f80f567e 100644 --- a/theme/lcd/theme.json +++ b/theme/lcd/theme.json @@ -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", diff --git a/theme/light/icons/add.png b/theme/light/icons/add.png index 02a632a51..8f69825dc 100644 Binary files a/theme/light/icons/add.png and b/theme/light/icons/add.png differ diff --git a/theme/light/icons/bookmark.png b/theme/light/icons/bookmark.png index 793ffb3aa..f3bb262aa 100644 Binary files a/theme/light/icons/bookmark.png and b/theme/light/icons/bookmark.png differ diff --git a/theme/light/icons/bookmark_inactive.png b/theme/light/icons/bookmark_inactive.png index 698025b0e..0a6e6926e 100644 Binary files a/theme/light/icons/bookmark_inactive.png and b/theme/light/icons/bookmark_inactive.png differ diff --git a/theme/light/icons/calendar.png b/theme/light/icons/calendar.png index 6d5789c3a..3d4eadcc2 100644 Binary files a/theme/light/icons/calendar.png and b/theme/light/icons/calendar.png differ diff --git a/theme/light/icons/calendar_notify.png b/theme/light/icons/calendar_notify.png index c364c3109..8887b3caa 100644 Binary files a/theme/light/icons/calendar_notify.png and b/theme/light/icons/calendar_notify.png differ diff --git a/theme/light/icons/categoriesrss.png b/theme/light/icons/categoriesrss.png index 219adb9bc..d567267b5 100644 Binary files a/theme/light/icons/categoriesrss.png and b/theme/light/icons/categoriesrss.png differ diff --git a/theme/light/icons/delete.png b/theme/light/icons/delete.png index 9904774e3..18bab9e1b 100644 Binary files a/theme/light/icons/delete.png and b/theme/light/icons/delete.png differ diff --git a/theme/light/icons/dm.png b/theme/light/icons/dm.png index c0493f82e..0aa915932 100644 Binary files a/theme/light/icons/dm.png and b/theme/light/icons/dm.png differ diff --git a/theme/light/icons/download.png b/theme/light/icons/download.png index 3a9605ab7..a8f5ec177 100644 Binary files a/theme/light/icons/download.png and b/theme/light/icons/download.png differ diff --git a/theme/light/icons/edit.png b/theme/light/icons/edit.png index c07dc2dec..c1c869620 100644 Binary files a/theme/light/icons/edit.png and b/theme/light/icons/edit.png differ diff --git a/theme/light/icons/edit_notify.png b/theme/light/icons/edit_notify.png index 9b063fafb..f991f2204 100644 Binary files a/theme/light/icons/edit_notify.png and b/theme/light/icons/edit_notify.png differ diff --git a/theme/light/icons/favicon.ico b/theme/light/icons/favicon.ico index c7cb1bbbe..48cd73fb9 100644 Binary files a/theme/light/icons/favicon.ico and b/theme/light/icons/favicon.ico differ diff --git a/theme/light/icons/like.png b/theme/light/icons/like.png index 843103d05..d43305da8 100644 Binary files a/theme/light/icons/like.png and b/theme/light/icons/like.png differ diff --git a/theme/light/icons/like_inactive.png b/theme/light/icons/like_inactive.png index 352e949b9..69fc7ce27 100644 Binary files a/theme/light/icons/like_inactive.png and b/theme/light/icons/like_inactive.png differ diff --git a/theme/light/icons/links.png b/theme/light/icons/links.png index 4ef5fe6a4..0a18048fc 100644 Binary files a/theme/light/icons/links.png and b/theme/light/icons/links.png differ diff --git a/theme/light/icons/logorss.png b/theme/light/icons/logorss.png index c5fad44cb..62c9a2fe0 100644 Binary files a/theme/light/icons/logorss.png and b/theme/light/icons/logorss.png differ diff --git a/theme/light/icons/logout.png b/theme/light/icons/logout.png index 6c52946df..4be0b4d09 100644 Binary files a/theme/light/icons/logout.png and b/theme/light/icons/logout.png differ diff --git a/theme/light/icons/mute.png b/theme/light/icons/mute.png index 5fe808e0f..7da81922e 100644 Binary files a/theme/light/icons/mute.png and b/theme/light/icons/mute.png differ diff --git a/theme/light/icons/new.png b/theme/light/icons/new.png index c6c834eb8..59fa6daec 100644 Binary files a/theme/light/icons/new.png and b/theme/light/icons/new.png differ diff --git a/theme/light/icons/newpost.png b/theme/light/icons/newpost.png index 3bc33deb8..62437c094 100644 Binary files a/theme/light/icons/newpost.png and b/theme/light/icons/newpost.png differ diff --git a/theme/light/icons/newswire.png b/theme/light/icons/newswire.png index f3521130d..99a3ad1a3 100644 Binary files a/theme/light/icons/newswire.png and b/theme/light/icons/newswire.png differ diff --git a/theme/light/icons/pagedown.png b/theme/light/icons/pagedown.png index da7b236d8..225e446ca 100644 Binary files a/theme/light/icons/pagedown.png and b/theme/light/icons/pagedown.png differ diff --git a/theme/light/icons/pageup.png b/theme/light/icons/pageup.png index 8adf6a8b8..710146863 100644 Binary files a/theme/light/icons/pageup.png and b/theme/light/icons/pageup.png differ diff --git a/theme/light/icons/person.png b/theme/light/icons/person.png index 0241ae867..9fc872a23 100644 Binary files a/theme/light/icons/person.png and b/theme/light/icons/person.png differ diff --git a/theme/light/icons/prev.png b/theme/light/icons/prev.png index daa14c763..9582a2e9c 100644 Binary files a/theme/light/icons/prev.png and b/theme/light/icons/prev.png differ diff --git a/theme/light/icons/publish.png b/theme/light/icons/publish.png index 0fe148eea..5f6e45a14 100644 Binary files a/theme/light/icons/publish.png and b/theme/light/icons/publish.png differ diff --git a/theme/light/icons/repeat.png b/theme/light/icons/repeat.png index e2daf8082..4d04917cb 100644 Binary files a/theme/light/icons/repeat.png and b/theme/light/icons/repeat.png differ diff --git a/theme/light/icons/repeat_inactive.png b/theme/light/icons/repeat_inactive.png index 59ec9d794..0ed102052 100644 Binary files a/theme/light/icons/repeat_inactive.png and b/theme/light/icons/repeat_inactive.png differ diff --git a/theme/light/icons/reply.png b/theme/light/icons/reply.png index f869a975e..c03c41349 100644 Binary files a/theme/light/icons/reply.png and b/theme/light/icons/reply.png differ diff --git a/theme/light/icons/scope_blog.png b/theme/light/icons/scope_blog.png index e3cdb1b81..cc063d0fb 100644 Binary files a/theme/light/icons/scope_blog.png and b/theme/light/icons/scope_blog.png differ diff --git a/theme/light/icons/scope_dm.png b/theme/light/icons/scope_dm.png index 7c485959c..ad82f305b 100644 Binary files a/theme/light/icons/scope_dm.png and b/theme/light/icons/scope_dm.png differ diff --git a/theme/light/icons/scope_event.png b/theme/light/icons/scope_event.png index 6d5789c3a..26f7d58ac 100644 Binary files a/theme/light/icons/scope_event.png and b/theme/light/icons/scope_event.png differ diff --git a/theme/light/icons/scope_followers.png b/theme/light/icons/scope_followers.png index 2e420954c..efd3353e6 100644 Binary files a/theme/light/icons/scope_followers.png and b/theme/light/icons/scope_followers.png differ diff --git a/theme/light/icons/scope_public.png b/theme/light/icons/scope_public.png index 7f8633ff0..917fb082c 100644 Binary files a/theme/light/icons/scope_public.png and b/theme/light/icons/scope_public.png differ diff --git a/theme/light/icons/scope_question.png b/theme/light/icons/scope_question.png index a811b21c6..4c93ebcbf 100644 Binary files a/theme/light/icons/scope_question.png and b/theme/light/icons/scope_question.png differ diff --git a/theme/light/icons/scope_reminder.png b/theme/light/icons/scope_reminder.png index 809376840..12d055dee 100644 Binary files a/theme/light/icons/scope_reminder.png and b/theme/light/icons/scope_reminder.png differ diff --git a/theme/light/icons/scope_report.png b/theme/light/icons/scope_report.png index 7fbd60b74..5f8a74935 100644 Binary files a/theme/light/icons/scope_report.png and b/theme/light/icons/scope_report.png differ diff --git a/theme/light/icons/scope_share.png b/theme/light/icons/scope_share.png index 07fe95502..1cdd0b2a6 100644 Binary files a/theme/light/icons/scope_share.png and b/theme/light/icons/scope_share.png differ diff --git a/theme/light/icons/scope_unlisted.png b/theme/light/icons/scope_unlisted.png index b3ce02e69..ca84e23c3 100644 Binary files a/theme/light/icons/scope_unlisted.png and b/theme/light/icons/scope_unlisted.png differ diff --git a/theme/light/icons/search.png b/theme/light/icons/search.png index 6d3b05c83..48b7c304b 100644 Binary files a/theme/light/icons/search.png and b/theme/light/icons/search.png differ diff --git a/theme/light/icons/showhide.png b/theme/light/icons/showhide.png index 32be2848c..b7a0cceb7 100644 Binary files a/theme/light/icons/showhide.png and b/theme/light/icons/showhide.png differ diff --git a/theme/light/icons/unmute.png b/theme/light/icons/unmute.png index eb9af71d2..ebacfdb20 100644 Binary files a/theme/light/icons/unmute.png and b/theme/light/icons/unmute.png differ diff --git a/theme/light/right_col_image.png b/theme/light/right_col_image.png index 952430a14..a6dd6a496 100644 Binary files a/theme/light/right_col_image.png and b/theme/light/right_col_image.png differ diff --git a/theme/light/theme.json b/theme/light/theme.json index c6a212772..586a04bcd 100644 --- a/theme/light/theme.json +++ b/theme/light/theme.json @@ -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", diff --git a/theme/night/theme.json b/theme/night/theme.json index 8e93fa9d7..1762cfd2a 100644 --- a/theme/night/theme.json +++ b/theme/night/theme.json @@ -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", diff --git a/theme/purple/theme.json b/theme/purple/theme.json index 710fd3b04..42ce3939e 100644 --- a/theme/purple/theme.json +++ b/theme/purple/theme.json @@ -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", diff --git a/theme/rc3/theme.json b/theme/rc3/theme.json index da23786e1..4eb466d6f 100644 --- a/theme/rc3/theme.json +++ b/theme/rc3/theme.json @@ -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", diff --git a/theme/solidaric/theme.json b/theme/solidaric/theme.json index 3031117e1..bc6c5ff23 100644 --- a/theme/solidaric/theme.json +++ b/theme/solidaric/theme.json @@ -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", diff --git a/theme/starlight/theme.json b/theme/starlight/theme.json index e05fca0a7..1be4c4002 100644 --- a/theme/starlight/theme.json +++ b/theme/starlight/theme.json @@ -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", diff --git a/theme/zen/theme.json b/theme/zen/theme.json index 1635106c7..03b743247 100644 --- a/theme/zen/theme.json +++ b/theme/zen/theme.json @@ -1,5 +1,6 @@ { "dropdown-bg-color-hover": "#463b35", + "cw-color": "#d5c7b7", "main-fg-color": "#d5c7b7", "column-left-fg-color": "#d5c7b7", "button-text": "#d5c7b7", diff --git a/translations/ar.json b/translations/ar.json index f23323482..b9b556ef1 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -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": "حسابات أخرى" } diff --git a/translations/ca.json b/translations/ca.json index 38bfea5f3..a286abdcf 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -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" } diff --git a/translations/cy.json b/translations/cy.json index 700bf49bf..8f98bf4d2 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -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" } diff --git a/translations/de.json b/translations/de.json index 301b4c5db..4ab19cfd8 100644 --- a/translations/de.json +++ b/translations/de.json @@ -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" } diff --git a/translations/en.json b/translations/en.json index 4b799853d..68e7e7a8b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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" } diff --git a/translations/es.json b/translations/es.json index 6acba3b63..fbc8c6784 100644 --- a/translations/es.json +++ b/translations/es.json @@ -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" } diff --git a/translations/fr.json b/translations/fr.json index 2d7d83555..6dd5a7b87 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -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" } diff --git a/translations/ga.json b/translations/ga.json index e6fb16db6..dc4da496e 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -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" } diff --git a/translations/hi.json b/translations/hi.json index 7df3b17c9..60cdc8a3c 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -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": "अन्य खाते" } diff --git a/translations/it.json b/translations/it.json index 2e4fb153f..7d4f2f6c8 100644 --- a/translations/it.json +++ b/translations/it.json @@ -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" } diff --git a/translations/ja.json b/translations/ja.json index 5c33aad20..340a00812 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -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": "その他のアカウント" } diff --git a/translations/oc.json b/translations/oc.json index d5c98e96b..f5d0c1174 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -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" } diff --git a/translations/pt.json b/translations/pt.json index d71505127..c09154982 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -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" } diff --git a/translations/ru.json b/translations/ru.json index 4e60087d5..037195a08 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -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": "Другие аккаунты" } diff --git a/translations/zh.json b/translations/zh.json index f4d2a77c4..b4a95082f 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -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": "其他账户" } diff --git a/webapp_person_options.py b/webapp_person_options.py index 807465b95..69153af86 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -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, '

' + \ translate['New account'] + \ ': @' + newHandle + '

\n' + elif alsoKnownAs: + optionsStr += \ + '

' + \ + translate['Other accounts'] + ': ' + + if isinstance(alsoKnownAs, list): + ctr = 0 + for altActor in alsoKnownAs: + if ctr > 0: + optionsStr += ' ' + ctr += 1 + altDomain, altPort = getDomainFromActor(altActor) + optionsStr += \ + '' + altDomain + '' + elif isinstance(alsoKnownAs, str): + altDomain, altPort = getDomainFromActor(alsoKnownAs) + optionsStr += '' + altDomain + '' + optionsStr += '

\n' if emailAddress: optionsStr += \ '

' + translate['Email'] + \ diff --git a/webapp_post.py b/webapp_post.py index ff1bfea8d..89ebbf613 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -1521,7 +1521,8 @@ def individualPostAsHtml(allowDownloads: bool, addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, cwStr, False) - contentStr += '' + cwStr + '\n ' + contentStr += \ + '\n ' if isModerationPost: containerClass = 'container report' # get the content warning text diff --git a/webapp_profile.py b/webapp_profile.py index 31c3b32c4..2fa59c458 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -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, '

' + translate['New account'] + ': ' + \ '@' + \ newNickname + '@' + newDomainFull + '
\n' + elif alsoKnownAs: + htmlStr += \ + '

' + translate['Other accounts'] + ': ' + + if isinstance(alsoKnownAs, list): + ctr = 0 + for altActor in alsoKnownAs: + if ctr > 0: + htmlStr += ' ' + ctr += 1 + altDomain, altPort = getDomainFromActor(altActor) + htmlStr += \ + '' + altDomain + '' + elif isinstance(alsoKnownAs, str): + altDomain, altPort = getDomainFromActor(alsoKnownAs) + htmlStr += '' + altDomain + '' + htmlStr += '

\n' htmlStr += \ ' @' + newHandle + '

\n' + elif alsoKnownAs: + htmlStr += \ + '

' + translate['Other accounts'] + ': ' + + if isinstance(alsoKnownAs, list): + ctr = 0 + for altActor in alsoKnownAs: + if ctr > 0: + htmlStr += ' ' + ctr += 1 + altDomain, altPort = getDomainFromActor(altActor) + htmlStr += \ + '' + altDomain + '' + elif isinstance(alsoKnownAs, str): + altDomain, altPort = getDomainFromActor(alsoKnownAs) + htmlStr += '' + altDomain + '' + htmlStr += '

\n' htmlStr += '

' + profileDescriptionShort + '

\n' htmlStr += ' \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 += '
\n' @@ -1280,6 +1324,22 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, ' \n' + alsoKnownAsStr = '' + if actorJson.get('alsoKnownAs'): + alsoKnownAs = actorJson['alsoKnownAs'] + ctr = 0 + for altActor in alsoKnownAs: + if ctr > 0: + alsoKnownAsStr += ', ' + ctr += 1 + alsoKnownAsStr += altActor + + editProfileForm += '
\n' + editProfileForm += \ + ' \n' + editProfileForm += '
\n' editProfileForm += \ diff --git a/webapp_utils.py b/webapp_utils.py index 2de80bee8..fc830f51d 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -167,8 +167,8 @@ def getContentWarningButton(postID: str, translate: {}, content: str) -> str: """Returns the markup for a content warning button """ - return '
' + \ - translate['SHOW MORE'] + '' + \ + return '
' + \ + translate['SHOW MORE'] + '' + \ '
' + content + \ '
\n'