diff --git a/acceptreject.py b/acceptreject.py index e7051f7dd..dcfdeb226 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -202,6 +202,7 @@ def receiveAcceptReject(session, baseDir: str, print('DEBUG: ' + messageJson['type'] + ' has no actor') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: diff --git a/announce.py b/announce.py index 4dc0f74e7..00804d2b5 100644 --- a/announce.py +++ b/announce.py @@ -81,6 +81,12 @@ def announcedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool: # not to be confused with shared items if not postJsonObject['object'].get('shares'): return False + if not isinstance(postJsonObject['object']['shares'], dict): + return False + if not postJsonObject['object']['shares'].get('items'): + return False + if not isinstance(postJsonObject['object']['shares']['items'], list): + return False actorMatch = domain + '/users/' + nickname for item in postJsonObject['object']['shares']['items']: if item['actor'].endswith(actorMatch): @@ -141,6 +147,7 @@ def createAnnounce(session, baseDir: str, federationList: [], announceDomain = None announcePort = None if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: announceNickname = getNicknameFromActor(objectUrl) @@ -257,6 +264,7 @@ def undoAnnounce(session, baseDir: str, federationList: [], announceDomain = None announcePort = None if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: announceNickname = getNicknameFromActor(objectUrl) diff --git a/auth.py b/auth.py index 8297aa816..d2aab5917 100644 --- a/auth.py +++ b/auth.py @@ -57,6 +57,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str, 'contain a space character') return False if '/users/' not in path and \ + '/accounts/' not in path and \ '/channel/' not in path and \ '/profile/' not in path: if debug: diff --git a/blocking.py b/blocking.py index 070b4c83e..0c57af788 100644 --- a/blocking.py +++ b/blocking.py @@ -220,6 +220,7 @@ def outboxBlock(baseDir: str, httpPrefix: str, print('DEBUG: c2s block object is not a status') return if '/users/' not in messageId and \ + '/accounts/' not in messageId and \ '/channel/' not in messageId and \ '/profile/' not in messageId: if debug: @@ -298,6 +299,7 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str, print('DEBUG: c2s undo block object is not a status') return if '/users/' not in messageId and \ + '/accounts/' not in messageId and \ '/channel/' not in messageId and \ '/profile/' not in messageId: if debug: diff --git a/bookmarks.py b/bookmarks.py index 22387d395..b7f1fa824 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -262,6 +262,7 @@ def bookmark(recentPostsCache: {}, bookmarkedPostDomain, bookmarkedPostPort = getDomainFromActor(acBm) else: if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: ou = objectUrl @@ -362,6 +363,7 @@ def undoBookmark(recentPostsCache: {}, bookmarkedPostDomain, bookmarkedPostPort = getDomainFromActor(acBm) else: if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: ou = objectUrl diff --git a/daemon.py b/daemon.py index 05d0bf911..072d637b0 100644 --- a/daemon.py +++ b/daemon.py @@ -1245,6 +1245,99 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 4) + # manifest for progressive web apps + if '/manifest.json' in self.path: + app1 = "https://f-droid.org/en/packages/eu.siacs.conversations" + app2 = "https://staging.f-droid.org/en/packages/im.vector.app" + manifest = { + "name": "Epicyon", + "short_name": "Epicyon", + "start_url": "/index.html", + "display": "standalone", + "background_color": "black", + "theme_color": "grey", + "orientation": "portrait-primary", + "categories": ["microblog", "fediverse", "activitypub"], + "screenshots": [ + { + "src": "/mobile.jpg", + "sizes": "418x851", + "type": "image/jpeg" + }, + { + "src": "/mobile_person.jpg", + "sizes": "429x860", + "type": "image/jpeg" + }, + { + "src": "/mobile_search.jpg", + "sizes": "422x861", + "type": "image/jpeg" + } + ], + "icons": [ + { + "src": "/logo72.png", + "type": "image/png", + "sizes": "72x72" + }, + { + "src": "/logo96.png", + "type": "image/png", + "sizes": "96x96" + }, + { + "src": "/logo128.png", + "type": "image/png", + "sizes": "128x128" + }, + { + "src": "/logo144.png", + "type": "image/png", + "sizes": "144x144" + }, + { + "src": "/logo152.png", + "type": "image/png", + "sizes": "152x152" + }, + { + "src": "/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/logo256.png", + "type": "image/png", + "sizes": "256x256" + }, + { + "src": "/logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "related_applications": [ + { + "platform": "fdroid", + "url": app1 + }, + { + "platform": "fdroid", + "url": app2 + } + ] + } + msg = json.dumps(manifest, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + if self.server.debug: + print('Sent manifest: ' + callingDomain) + return + # favicon image if 'favicon.ico' in self.path: favType = 'image/x-icon' @@ -1262,6 +1355,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.baseDir + '/img/icons/' + favFilename if self._etag_exists(faviconFilename): # The file has not changed + if self.server.debug: + print('favicon icon has not changed: ' + callingDomain) self._304() return if self.server.iconsCache.get(favFilename): @@ -1271,6 +1366,8 @@ class PubServer(BaseHTTPRequestHandler): favBinary, cookie, callingDomain) self._write(favBinary) + if self.server.debug: + print('Sent favicon from cache: ' + callingDomain) return else: if os.path.isfile(faviconFilename): @@ -1282,7 +1379,11 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) self._write(favBinary) self.server.iconsCache[favFilename] = favBinary + if self.server.debug: + print('Sent favicon from file: ' + callingDomain) return + if self.server.debug: + print('favicon not sent: ' + callingDomain) self._404() return @@ -1355,6 +1456,9 @@ class PubServer(BaseHTTPRequestHandler): fontBinary, cookie, callingDomain) self._write(fontBinary) + if self.server.debug: + print('font sent from cache: ' + + self.path + ' ' + callingDomain) return else: if os.path.isfile(fontFilename): @@ -1366,7 +1470,12 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) self._write(fontBinary) self.server.fontsCache[fontStr] = fontBinary + if self.server.debug: + print('font sent from file: ' + + self.path + ' ' + callingDomain) return + if self.server.debug: + print('font not found: ' + self.path + ' ' + callingDomain) self._404() return @@ -1421,9 +1530,15 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers('text/xml', len(msg), cookie, callingDomain) self._write(msg) + if self.server.debug: + print('Sent rss2 feed: ' + + self.path + ' ' + callingDomain) return - self._404() - return + if self.server.debug: + print('Failed to get rss2 feed: ' + + self.path + ' ' + callingDomain) + self._404() + return # RSS 3.0 if self.path.startswith('/blog/') and \ @@ -1459,9 +1574,15 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers('text/plain; charset=utf-8', len(msg), cookie, callingDomain) self._write(msg) + if self.server.debug: + print('Sent rss3 feed: ' + + self.path + ' ' + callingDomain) return - self._404() - return + if self.server.debug: + print('Failed to get rss3 feed: ' + + self.path + ' ' + callingDomain) + self._404() + return # show the main blog page if htmlGET and (self.path == '/blog' or @@ -1830,15 +1951,19 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 15) - # image on login screen or qrcode - if self.path == '/login.png' or \ - self.path == '/login.gif' or \ - self.path == '/login.webp' or \ - self.path == '/login.jpeg' or \ - self.path == '/login.jpg' or \ - self.path == '/qrcode.png': + # manifest images used to create a home screen icon + # when selecting "add to home screen" in browsers + # which support progressive web apps + if self.path == '/logo72.png' or \ + self.path == '/logo96.png' or \ + self.path == '/logo128.png' or \ + self.path == '/logo144.png' or \ + self.path == '/logo152.png' or \ + self.path == '/logo192.png' or \ + self.path == '/logo256.png' or \ + self.path == '/logo512.png': mediaFilename = \ - self.server.baseDir + '/accounts' + self.path + self.server.baseDir + '/img' + self.path if os.path.isfile(mediaFilename): if self._etag_exists(mediaFilename): # The file has not changed @@ -1866,6 +1991,75 @@ class PubServer(BaseHTTPRequestHandler): self._404() return + # manifest images used to show example screenshots + # for use by app stores + if self.path == '/screenshot1.jpg' or \ + self.path == '/screenshot2.jpg': + screenFilename = \ + self.server.baseDir + '/img' + self.path + if os.path.isfile(screenFilename): + if self._etag_exists(screenFilename): + # The file has not changed + self._304() + return + + tries = 0 + mediaBinary = None + while tries < 5: + try: + with open(screenFilename, 'rb') as avFile: + mediaBinary = avFile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + if mediaBinary: + self._set_headers_etag(screenFilename, + 'image/png', + mediaBinary, cookie, + callingDomain) + self._write(mediaBinary) + return + self._404() + return + + # image on login screen or qrcode + if self.path == '/login.png' or \ + self.path == '/login.gif' or \ + self.path == '/login.webp' or \ + self.path == '/login.jpeg' or \ + self.path == '/login.jpg' or \ + self.path == '/qrcode.png': + iconFilename = \ + self.server.baseDir + '/accounts' + self.path + if os.path.isfile(iconFilename): + if self._etag_exists(iconFilename): + # The file has not changed + self._304() + return + + tries = 0 + mediaBinary = None + while tries < 5: + try: + with open(iconFilename, 'rb') as avFile: + mediaBinary = avFile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + if mediaBinary: + self._set_headers_etag(iconFilename, + 'image/png', + mediaBinary, cookie, + callingDomain) + self._write(mediaBinary) + return + self._404() + return + self._benchmarkGETtimings(GETstartTime, GETtimings, 16) # QR code for account handle @@ -1875,11 +2069,11 @@ class PubServer(BaseHTTPRequestHandler): savePersonQrcode(self.server.baseDir, nickname, self.server.domain, self.server.port) - mediaFilename = \ + qrFilename = \ self.server.baseDir + '/accounts/' + \ nickname + '@' + self.server.domain + '/qrcode.png' - if os.path.isfile(mediaFilename): - if self._etag_exists(mediaFilename): + if os.path.isfile(qrFilename): + if self._etag_exists(qrFilename): # The file has not changed self._304() return @@ -1888,7 +2082,7 @@ class PubServer(BaseHTTPRequestHandler): mediaBinary = None while tries < 5: try: - with open(mediaFilename, 'rb') as avFile: + with open(qrFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as e: @@ -1896,7 +2090,7 @@ class PubServer(BaseHTTPRequestHandler): time.sleep(1) tries += 1 if mediaBinary: - self._set_headers_etag(mediaFilename, 'image/png', + self._set_headers_etag(qrFilename, 'image/png', mediaBinary, cookie, callingDomain) self._write(mediaBinary) @@ -1908,11 +2102,11 @@ class PubServer(BaseHTTPRequestHandler): if '/users/' in self.path and \ self.path.endswith('/search_banner.png'): nickname = getNicknameFromActor(self.path) - mediaFilename = \ + bannerFilename = \ self.server.baseDir + '/accounts/' + \ nickname + '@' + self.server.domain + '/search_banner.png' - if os.path.isfile(mediaFilename): - if self._etag_exists(mediaFilename): + if os.path.isfile(bannerFilename): + if self._etag_exists(bannerFilename): # The file has not changed self._304() return @@ -1921,7 +2115,7 @@ class PubServer(BaseHTTPRequestHandler): mediaBinary = None while tries < 5: try: - with open(mediaFilename, 'rb') as avFile: + with open(bannerFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as e: @@ -1929,7 +2123,7 @@ class PubServer(BaseHTTPRequestHandler): time.sleep(1) tries += 1 if mediaBinary: - self._set_headers_etag(mediaFilename, 'image/png', + self._set_headers_etag(bannerFilename, 'image/png', mediaBinary, cookie, callingDomain) self._write(mediaBinary) @@ -5914,7 +6108,7 @@ class PubServer(BaseHTTPRequestHandler): return self._400() elif path.startswith('/api/v1/crypto/keys/query'): - # given a handle (nickname@domain) return the devices + # given a handle (nickname@domain) return a list of the devices # registered to that handle if not self._cryptoAPIQuery(): self._400() @@ -7232,7 +7426,12 @@ class PubServer(BaseHTTPRequestHandler): self._write(msg) self.server.POSTbusy = False return - elif '@' in searchStr: + elif ('@' in searchStr or + ('://' in searchStr and + ('/users/' in searchStr or + '/profile/' in searchStr or + '/accounts/' in searchStr or + '/channel/' in searchStr))): # profile search nickname = getNicknameFromActor(actorStr) if not self.server.session: diff --git a/delete.py b/delete.py index e26002fee..f77585440 100644 --- a/delete.py +++ b/delete.py @@ -67,6 +67,7 @@ def createDelete(session, baseDir: str, federationList: [], deleteDomain = None deletePort = None if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: deleteNickname = getNicknameFromActor(objectUrl) @@ -262,6 +263,7 @@ def outboxDelete(baseDir: str, httpPrefix: str, print('DEBUG: c2s delete object is not a status') return if '/users/' not in messageId and \ + '/accounts/' not in messageId and \ '/channel/' not in messageId and \ '/profile/' not in messageId: if debug: diff --git a/epicyon-blog.css b/epicyon-blog.css index a2ceee5d8..46dfd30fc 100644 --- a/epicyon-blog.css +++ b/epicyon-blog.css @@ -42,6 +42,7 @@ --gallery-font-size-mobile: 35px; --button-corner-radius: 15px; --timeline-border-radius: 30px; + --focus-color: white; } @font-face { @@ -80,6 +81,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .cwText { display: none; } diff --git a/epicyon-calendar.css b/epicyon-calendar.css index cbf3471d3..9c11fd0cb 100644 --- a/epicyon-calendar.css +++ b/epicyon-calendar.css @@ -13,6 +13,7 @@ --event-foreground:white; --title-text: #282c37; --title-background: #ccc; + --focus-color: white; } @font-face { @@ -67,6 +68,10 @@ a:link { margin: -1rem; } +a:focus { + border: 2px solid var(--focus-color); +} + .calendar__day__header, .calendar__day__cell { border: 2px solid var(--lines-color); diff --git a/epicyon-follow.css b/epicyon-follow.css index 2f7ec22f1..ed7aca1ed 100644 --- a/epicyon-follow.css +++ b/epicyon-follow.css @@ -30,6 +30,7 @@ --follow-text-size1: 24px; --follow-text-size2: 40px; --follow-text-entry-width: 90%; + --focus-color: white; } @font-face { @@ -67,6 +68,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .searchBanner { background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("search_banner.png"); background-position: center; diff --git a/epicyon-login.css b/epicyon-login.css index d547f5821..1a02de7a6 100644 --- a/epicyon-login.css +++ b/epicyon-login.css @@ -19,6 +19,7 @@ --button-background: #999; --button-selected: #666; --form-border-radius: 30px; + --focus-color: white; } @font-face { @@ -63,6 +64,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + form { border: var(--border-width) solid var(--border-color); border-radius: var(--form-border-radius); diff --git a/epicyon-options.css b/epicyon-options.css index 3fe023a1f..c7f07905d 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -18,7 +18,9 @@ --text-entry-background: #111; --time-color: #aaa; --button-text: #FFFFFF; + --button-small-text: #FFFFFF; --button-background: #999; + --button-small-background: #999; --button-selected: #666; --hashtag-margin: 2%; --hashtag-vertical-spacing1: 50px; @@ -30,6 +32,7 @@ --follow-text-size1: 24px; --follow-text-size2: 40px; --follow-text-entry-width: 90%; + --focus-color: white; } @font-face { @@ -72,6 +75,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .follow { height: 100%; position: relative; @@ -112,13 +119,14 @@ a:link { width: 15%; } -textarea { - font-size: var(--font-size4); - width: 90%; - background-color: var(--text-entry-background); -} - @media screen and (min-width: 400px) { + textarea { + font-family: Arial, Helvetica, sans-serif; + font-size: var(--font-size4); + width: 90%; + background-color: var(--text-entry-background); + color: white; + } .followText { font-size: var(--follow-text-size1); } @@ -140,7 +148,22 @@ textarea { text-align: center; padding: 10px; font-size: 24px; - width: 20%; + width: 10ch; + max-width: 200px; + min-width: 100px; + cursor: pointer; + margin: 30px; + } + .buttonsmall { + border-radius: 4px; + background-color: var(--button-small-background); + font-family: Arial, Helvetica, sans-serif; + border: none; + color: var(--button-small-text); + text-align: center; + padding: 10px; + font-size: 24px; + width: 7ch; max-width: 200px; min-width: 100px; cursor: pointer; @@ -159,6 +182,13 @@ textarea { } @media screen and (max-width: 1000px) { + textarea { + font-family: Arial, Helvetica, sans-serif; + font-size: var(--font-size); + width: 90%; + background-color: var(--text-entry-background); + color: white; + } .followText { font-size: var(--follow-text-size2); } @@ -180,7 +210,22 @@ textarea { text-align: center; padding: 10px; font-size: 40px; - width: 20%; + width: 10ch; + max-width: 200px; + min-width: 100px; + cursor: pointer; + margin: 30px; + } + .buttonsmall { + border-radius: 4px; + background-color: var(--button-small-background); + font-family: Arial, Helvetica, sans-serif; + border: none; + color: var(--button-small-text); + text-align: center; + padding: 10px; + font-size: 40px; + width: 7ch; max-width: 200px; min-width: 100px; cursor: pointer; diff --git a/epicyon-profile.css b/epicyon-profile.css index 7cb709598..1c38bf5d3 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -26,6 +26,9 @@ --font-size4: 22px; --font-size5: 20px; --font-size-pgp-key: 16px; + --font-size-pgp-key2: 8px; + --font-size-tox: 16px; + --font-size-tox2: 8px; --text-entry-foreground: #ccc; --text-entry-background: #111; --time-color: #aaa; @@ -51,6 +54,7 @@ --timeline-border-radius: 30px; --icons-side: right; --title-color: #999; + --focus-color: white; } @font-face { @@ -73,6 +77,10 @@ body, html { font-size: var(--font-size); } +.imageAnchor:focus img{ + border: 2px solid var(--focus-color); +} + h1 { color: var(--title-color); } @@ -93,6 +101,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .timeline-banner { background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png"); height: 10%; @@ -116,11 +128,6 @@ a:link { float: right; } -.ssbaddr { - font-size: var(--font-size-pgp-key); - font-family: Arial, Helvetica, sans-serif; -} - .about { font-size: var(--font-size5); font-family: Arial, Helvetica, sans-serif; @@ -285,16 +292,19 @@ a:link { width: 90%; } +.message:focus{ + border: 2px solid var(--focus-color); +} + +.message:focus img{ + border: 2px solid var(--focus-color); +} + .gitpatch { width: 90%; font-family: 'monospace'; } -.container p.administeredby { - font-size: var(--font-size-header); - font-family: Arial, Helvetica, sans-serif; -} - .container::after { content: ""; clear: both; @@ -346,13 +356,6 @@ a:link { background: var(--link-bg-color); } -.pgp { - font-size: var(--font-size-pgp-key); - color: var(--main-link-color); - background: var(--link-bg-color); - font-family: 'monospace'; -} - .container img.announceOrReply { float: none; width: 30px; @@ -857,6 +860,24 @@ aside .toggle-inside li { } @media screen and (min-width: 400px) { + .container p.administeredby { + font-size: var(--font-size-header); + font-family: Arial, Helvetica, sans-serif; + } + .toxaddr { + font-size: var(--font-size-tox); + font-family: Arial, Helvetica, sans-serif; + } + .ssbaddr { + font-size: var(--font-size-pgp-key); + font-family: Arial, Helvetica, sans-serif; + } + .pgp { + font-size: var(--font-size-pgp-key); + color: var(--main-link-color); + background: var(--link-bg-color); + font-family: 'monospace'; + } body, html { font-size: var(--font-size4); font-family: Arial, Helvetica, sans-serif; @@ -1260,6 +1281,24 @@ aside .toggle-inside li { } @media screen and (max-width: 1000px) { + .container p.administeredby { + font-size: var(--font-size-tox2); + font-family: Arial, Helvetica, sans-serif; + } + .toxaddr { + font-size: var(--font-size-tox2); + font-family: Arial, Helvetica, sans-serif; + } + .ssbaddr { + font-size: var(--font-size-pgp-key2); + font-family: Arial, Helvetica, sans-serif; + } + .pgp { + font-size: var(--font-size-pgp-key2); + color: var(--main-link-color); + background: var(--link-bg-color); + font-family: 'monospace'; + } body, html { font-size: var(--font-size3); font-family: Arial, Helvetica, sans-serif; diff --git a/epicyon-search.css b/epicyon-search.css index 3df723300..ab4a91a19 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -30,6 +30,7 @@ --follow-text-size1: 24px; --follow-text-size2: 40px; --follow-text-entry-width: 90%; + --focus-color: white; } @font-face { @@ -67,6 +68,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .searchBanner { background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("search_banner.png"); background-position: center; diff --git a/epicyon-suspended.css b/epicyon-suspended.css index 7ace9ba3f..03ddc7dc2 100644 --- a/epicyon-suspended.css +++ b/epicyon-suspended.css @@ -19,6 +19,7 @@ --button-text: #FFFFFF; --button-background: #999; --button-selected: #666; + --focus-color: white; } @font-face { @@ -57,6 +58,10 @@ a:link { font-weight: bold; } +a:focus { + border: 2px solid var(--focus-color); +} + .screentitle { font-size: 30px; font-family: Arial, Helvetica, sans-serif; diff --git a/epicyon.py b/epicyon.py index 1ca654c63..c7809ecbd 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1130,6 +1130,7 @@ if args.actor: args.actor = args.actor.replace(prefix, '') args.actor = args.actor.replace('/@', '/users/') if '/users/' not in args.actor and \ + '/accounts/' not in args.actor and \ '/channel/' not in args.actor and \ '/profile/' not in args.actor: print('Expected actor format: ' + @@ -1143,10 +1144,14 @@ if args.actor: nickname = args.actor.split('/profile/')[1] nickname = nickname.replace('\n', '').replace('\r', '') domain = args.actor.split('/profile/')[0] - else: + elif '/channel/' in args.actor: nickname = args.actor.split('/channel/')[1] nickname = nickname.replace('\n', '').replace('\r', '') domain = args.actor.split('/channel/')[0] + elif '/accounts/' in args.actor: + nickname = args.actor.split('/accounts/')[1] + nickname = nickname.replace('\n', '').replace('\r', '') + domain = args.actor.split('/accounts/')[0] else: # format: @nick@domain if '@' not in args.actor: @@ -1198,6 +1203,7 @@ if args.actor: if wfRequest.get('errors'): print('wfRequest error: ' + str(wfRequest['errors'])) if '/users/' in args.actor or \ + '/accounts/' in args.actor or \ '/profile/' in args.actor or \ '/channel/' in args.actor: personUrl = originalActor @@ -1212,6 +1218,7 @@ if args.actor: personUrl = getUserUrl(wfRequest) if nickname == domain: personUrl = personUrl.replace('/users/', '/actor/') + personUrl = personUrl.replace('/accounts/', '/actor/') personUrl = personUrl.replace('/channel/', '/actor/') personUrl = personUrl.replace('/profile/', '/actor/') if not personUrl: @@ -1221,7 +1228,7 @@ if args.actor: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } - if '/channel/' in personUrl: + if '/channel/' in personUrl or '/accounts/' in personUrl: profileStr = 'https://www.w3.org/ns/activitystreams' asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' diff --git a/follow.py b/follow.py index 06a636554..3c6c6e1d0 100644 --- a/follow.py +++ b/follow.py @@ -555,6 +555,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, print('DEBUG: follow request has no actor') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -582,6 +583,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, if not messageJson.get('to'): messageJson['to'] = messageJson['object'] if '/users/' not in messageJson['object'] and \ + '/accounts/' not in messageJson['object'] and \ '/channel/' not in messageJson['object'] and \ '/profile/' not in messageJson['object']: if debug: diff --git a/happening.py b/happening.py index da9ddfb8d..438ffa3ad 100644 --- a/happening.py +++ b/happening.py @@ -7,14 +7,142 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +from uuid import UUID from datetime import datetime from utils import loadJson +from utils import saveJson from utils import locatePost from utils import daysInMonth from utils import mergeDicts +def validUuid(testUuid: str, version=4): + """Check if uuid_to_test is a valid UUID + """ + try: + uuid_obj = UUID(testUuid, version=version) + except ValueError: + return False + + return str(uuid_obj) == testUuid + + +def removeEventFromTimeline(eventId: str, tlEventsFilename: str) -> None: + """Removes the given event Id from the timeline + """ + if eventId + '\n' not in open(tlEventsFilename).read(): + return + with open(tlEventsFilename, 'r') as fp: + eventsTimeline = fp.read().replace(eventId + '\n', '') + try: + with open(tlEventsFilename, 'w+') as fp2: + fp2.write(eventsTimeline) + except BaseException: + print('ERROR: unable to save events timeline') + pass + + +def saveEvent(baseDir: str, handle: str, postId: str, + eventJson: {}) -> bool: + """Saves an event to the calendar and/or the events timeline + If an event has extra fields, as per Mobilizon, + Then it is saved as a separate entity and added to the + events timeline + """ + calendarPath = baseDir + '/accounts/' + handle + '/calendar' + if not os.path.isdir(calendarPath): + os.mkdir(calendarPath) + + # get the year, month and day from the event + eventTime = datetime.strptime(eventJson['startTime'], + "%Y-%m-%dT%H:%M:%S%z") + eventYear = int(eventTime.strftime("%Y")) + if eventYear < 2020 or eventYear >= 2100: + return False + eventMonthNumber = int(eventTime.strftime("%m")) + if eventMonthNumber < 1 or eventMonthNumber > 12: + return False + eventDayOfMonth = int(eventTime.strftime("%d")) + if eventDayOfMonth < 1 or eventDayOfMonth > 31: + return False + + if eventJson.get('name') and eventJson.get('actor') and \ + eventJson.get('uuid') and eventJson.get('content'): + if not validUuid(eventJson['uuid']): + return False + # if this is a full description of an event then save it + # as a separate json file + eventsPath = baseDir + '/accounts/' + handle + '/events' + if not os.path.isdir(eventsPath): + os.mkdir(eventsPath) + eventsYearPath = \ + baseDir + '/accounts/' + handle + '/events/' + str(eventYear) + if not os.path.isdir(eventsYearPath): + os.mkdir(eventsYearPath) + eventId = str(eventYear) + '-' + eventTime.strftime("%m") + '-' + \ + eventTime.strftime("%d") + '_' + eventJson['uuid'] + eventFilename = eventsYearPath + '/' + eventId + '.json' + + saveJson(eventJson, eventFilename) + # save to the events timeline + tlEventsFilename = baseDir + '/accounts/' + handle + '/events.txt' + + if os.path.isfile(tlEventsFilename): + removeEventFromTimeline(eventId, tlEventsFilename) + try: + with open(tlEventsFilename, 'r+') as tlEventsFile: + content = tlEventsFile.read() + tlEventsFile.seek(0, 0) + tlEventsFile.write(eventId + '\n' + content) + except Exception as e: + print('WARN: Failed to write entry to events file ' + + tlEventsFilename + ' ' + str(e)) + return False + else: + tlEventsFile = open(tlEventsFilename, 'w+') + tlEventsFile.write(eventId + '\n') + tlEventsFile.close() + + # create a directory for the calendar year + if not os.path.isdir(calendarPath + '/' + str(eventYear)): + os.mkdir(calendarPath + '/' + str(eventYear)) + + # calendar month file containing event post Ids + calendarFilename = calendarPath + '/' + str(eventYear) + \ + '/' + str(eventMonthNumber) + '.txt' + + # Does this event post already exist within the calendar month? + if os.path.isfile(calendarFilename): + if postId in open(calendarFilename).read(): + # Event post already exists + return False + + # append the post Id to the file for the calendar month + calendarFile = open(calendarFilename, 'a+') + if not calendarFile: + return False + calendarFile.write(postId + '\n') + calendarFile.close() + + # create a file which will trigger a notification that + # a new event has been added + calendarNotificationFilename = \ + baseDir + '/accounts/' + handle + '/.newCalendar' + calendarNotificationFile = \ + open(calendarNotificationFilename, 'w+') + if not calendarNotificationFile: + return False + calendarNotificationFile.write('/calendar?year=' + + str(eventYear) + + '?month=' + + str(eventMonthNumber) + + '?day=' + + str(eventDayOfMonth)) + calendarNotificationFile.close() + return True + + def isHappeningEvent(tag: {}) -> bool: """Is this tag an Event or Place ActivityStreams type? """ diff --git a/img/logo128.png b/img/logo128.png new file mode 100644 index 000000000..4a7683ca0 Binary files /dev/null and b/img/logo128.png differ diff --git a/img/logo144.png b/img/logo144.png new file mode 100644 index 000000000..c644defac Binary files /dev/null and b/img/logo144.png differ diff --git a/img/logo152.png b/img/logo152.png new file mode 100644 index 000000000..011235ee4 Binary files /dev/null and b/img/logo152.png differ diff --git a/img/logo192.png b/img/logo192.png new file mode 100644 index 000000000..ee9bf1e9b Binary files /dev/null and b/img/logo192.png differ diff --git a/img/logo256.png b/img/logo256.png new file mode 100644 index 000000000..c5c087e41 Binary files /dev/null and b/img/logo256.png differ diff --git a/img/logo512.png b/img/logo512.png new file mode 100644 index 000000000..10b0194e2 Binary files /dev/null and b/img/logo512.png differ diff --git a/img/logo72.png b/img/logo72.png new file mode 100644 index 000000000..3f13793c1 Binary files /dev/null and b/img/logo72.png differ diff --git a/img/logo96.png b/img/logo96.png new file mode 100644 index 000000000..98ef5f5c4 Binary files /dev/null and b/img/logo96.png differ diff --git a/img/mobile.jpg b/img/mobile.jpg index 2cdb0e31f..9cdc0781f 100644 Binary files a/img/mobile.jpg and b/img/mobile.jpg differ diff --git a/img/mobile_person.jpg b/img/mobile_person.jpg new file mode 100644 index 000000000..e8d69725a Binary files /dev/null and b/img/mobile_person.jpg differ diff --git a/img/mobile_search.jpg b/img/mobile_search.jpg new file mode 100644 index 000000000..30fab6598 Binary files /dev/null and b/img/mobile_search.jpg differ diff --git a/inbox.py b/inbox.py index 15bf74a25..692b89c17 100644 --- a/inbox.py +++ b/inbox.py @@ -64,6 +64,7 @@ from git import isGitPatch from git import receiveGitPatch from followingCalendar import receivingCalendarEvents from content import dangerousMarkup +from happening import saveEvent def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: @@ -660,6 +661,7 @@ def receiveUndoFollow(session, baseDir: str, httpPrefix: str, print('DEBUG: follow request has no actor within object') return False if '/users/' not in messageJson['object']['actor'] and \ + '/accounts/' not in messageJson['object']['actor'] and \ '/channel/' not in messageJson['object']['actor'] and \ '/profile/' not in messageJson['object']['actor']: if debug: @@ -734,6 +736,7 @@ def receiveUndo(session, baseDir: str, httpPrefix: str, print('DEBUG: follow request has no actor') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -791,11 +794,13 @@ def personReceiveUpdate(baseDir: str, if actor not in personJson['id']: actor = updateDomainFull + '/channel/' + updateNickname if actor not in personJson['id']: - if debug: - print('actor: ' + actor) - print('id: ' + personJson['id']) - print('DEBUG: Actor does not match id') - return False + actor = updateDomainFull + '/accounts/' + updateNickname + if actor not in personJson['id']: + if debug: + print('actor: ' + actor) + print('id: ' + personJson['id']) + print('DEBUG: Actor does not match id') + return False if updateDomainFull == domainFull: if debug: print('DEBUG: You can only receive actor updates ' + @@ -906,6 +911,7 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str, print('DEBUG: ' + messageJson['type'] + ' object has no type') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1007,6 +1013,7 @@ def receiveLike(recentPostsCache: {}, print('DEBUG: ' + messageJson['type'] + ' has no "to" list') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1075,6 +1082,7 @@ def receiveUndoLike(recentPostsCache: {}, ' like object is not a string') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1287,6 +1295,7 @@ def receiveDelete(session, handle: str, isGroup: bool, baseDir: str, print('DEBUG: ' + messageJson['type'] + ' has no "to" list') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1357,6 +1366,7 @@ def receiveAnnounce(recentPostsCache: {}, print('DEBUG: ' + messageJson['type'] + ' has no "to" list') return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1365,6 +1375,7 @@ def receiveAnnounce(recentPostsCache: {}, messageJson['type']) return False if '/users/' not in messageJson['object'] and \ + '/accounts/' not in messageJson['object'] and \ '/channel/' not in messageJson['object'] and \ '/profile/' not in messageJson['object']: if debug: @@ -1433,6 +1444,7 @@ def receiveAnnounce(recentPostsCache: {}, lookupActor = attrib if lookupActor: if '/users/' in lookupActor or \ + '/accounts/' in lookupActor or \ '/channel/' in lookupActor or \ '/profile/' in lookupActor: if '/statuses/' in lookupActor: @@ -1484,6 +1496,7 @@ def receiveUndoAnnounce(recentPostsCache: {}, if messageJson['object']['type'] != 'Announce': return False if '/users/' not in messageJson['actor'] and \ + '/accounts/' not in messageJson['actor'] and \ '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: @@ -1678,6 +1691,7 @@ def obtainAvatarForReplyPost(session, baseDir: str, httpPrefix: str, return if not ('/users/' in lookupActor or + '/accounts/' in lookupActor or '/channel/' in lookupActor or '/profile/' in lookupActor): return @@ -1957,10 +1971,6 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None: if not isinstance(postJsonObject['object']['tag'], list): return - calendarPath = baseDir + '/accounts/' + handle + '/calendar' - if not os.path.isdir(calendarPath): - os.mkdir(calendarPath) - actor = postJsonObject['actor'] actorNickname = getNicknameFromActor(actor) actorDomain, actorPort = getDomainFromActor(actor) @@ -1970,6 +1980,11 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None: handleNickname, handleDomain, actorNickname, actorDomain): return + + postId = \ + postJsonObject['id'].replace('/activity', '').replace('/', '#') + + # look for events within the tags list for tagDict in postJsonObject['object']['tag']: if not tagDict.get('type'): continue @@ -1977,38 +1992,7 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None: continue if not tagDict.get('startTime'): continue - # get the year and month from the event - eventTime = datetime.datetime.strptime(tagDict['startTime'], - "%Y-%m-%dT%H:%M:%S%z") - eventYear = int(eventTime.strftime("%Y")) - eventMonthNumber = int(eventTime.strftime("%m")) - eventDayOfMonth = int(eventTime.strftime("%d")) - - if not os.path.isdir(calendarPath + '/' + str(eventYear)): - os.mkdir(calendarPath + '/' + str(eventYear)) - calendarFilename = calendarPath + '/' + str(eventYear) + \ - '/' + str(eventMonthNumber) + '.txt' - postId = \ - postJsonObject['id'].replace('/activity', '').replace('/', '#') - if os.path.isfile(calendarFilename): - if postId in open(calendarFilename).read(): - return - calendarFile = open(calendarFilename, 'a+') - if calendarFile: - calendarFile.write(postId + '\n') - calendarFile.close() - calendarNotificationFilename = \ - baseDir + '/accounts/' + handle + '/.newCalendar' - calendarNotificationFile = \ - open(calendarNotificationFilename, 'w+') - if calendarNotificationFile: - calendarNotificationFile.write('/calendar?year=' + - str(eventYear) + - '?month=' + - str(eventMonthNumber) + - '?day=' + - str(eventDayOfMonth)) - calendarNotificationFile.close() + saveEvent(baseDir, handle, postId, tagDict) def inboxUpdateIndex(boxname: str, baseDir: str, handle: str, diff --git a/like.py b/like.py index dde8bd8bc..f3d127dcb 100644 --- a/like.py +++ b/like.py @@ -90,6 +90,7 @@ def like(recentPostsCache: {}, likedPostDomain, likedPostPort = getDomainFromActor(actorLiked) else: if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: likedPostNickname = getNicknameFromActor(objectUrl) @@ -193,6 +194,7 @@ def undolike(recentPostsCache: {}, likedPostDomain, likedPostPort = getDomainFromActor(actorLiked) else: if '/users/' in objectUrl or \ + '/accounts/' in objectUrl or \ '/channel/' in objectUrl or \ '/profile/' in objectUrl: likedPostNickname = getNicknameFromActor(objectUrl) diff --git a/posts.py b/posts.py index 1aa7f8b14..05b081e7f 100644 --- a/posts.py +++ b/posts.py @@ -136,6 +136,7 @@ def getUserUrl(wfRequest: {}) -> str: if link.get('type') and link.get('href'): if link['type'] == 'application/activity+json': if not ('/users/' in link['href'] or + '/accounts/' in link['href'] or '/profile/' in link['href'] or '/channel/' in link['href']): print('Webfinger activity+json contains ' + @@ -207,7 +208,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {}, return None, None, None, None, None, None, None, None personJson = getPersonFromCache(baseDir, personUrl, personCache) if not personJson: - if '/channel/' in personUrl: + if '/channel/' in personUrl or '/accounts/' in personUrl: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } @@ -3188,7 +3189,8 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, asHeader = { 'Accept': 'application/activity+json; profile="' + profileStr + '"' } - if '/channel/' in postJsonObject['actor']: + if '/channel/' in postJsonObject['actor'] or \ + '/accounts/' in postJsonObject['actor']: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } @@ -3238,6 +3240,7 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, rejectAnnounce(announceFilename) return None if '/users/' not in announcedJson['id'] and \ + '/accounts/' not in announcedJson['id'] and \ '/channel/' not in announcedJson['id'] and \ '/profile/' not in announcedJson['id']: rejectAnnounce(announceFilename) diff --git a/tests.py b/tests.py index 947d9d98c..f7a27aaa3 100644 --- a/tests.py +++ b/tests.py @@ -1585,6 +1585,30 @@ def testActorParsing(): nickname = getNicknameFromActor(actor) assert nickname == 'mynick' + actor = 'https://element/accounts/badger' + domain, port = getDomainFromActor(actor) + assert domain == 'element' + nickname = getNicknameFromActor(actor) + assert nickname == 'badger' + + actor = 'egg@chicken.com' + domain, port = getDomainFromActor(actor) + assert domain == 'chicken.com' + nickname = getNicknameFromActor(actor) + assert nickname == 'egg' + + actor = '@waffle@cardboard' + domain, port = getDomainFromActor(actor) + assert domain == 'cardboard' + nickname = getNicknameFromActor(actor) + assert nickname == 'waffle' + + actor = 'https://astral/channel/sky' + domain, port = getDomainFromActor(actor) + assert domain == 'astral' + nickname = getNicknameFromActor(actor) + assert nickname == 'sky' + actor = 'https://randomain/users/rando' domain, port = getDomainFromActor(actor) assert domain == 'randomain' diff --git a/theme.py b/theme.py index a8a85b5e6..8924232e5 100644 --- a/theme.py +++ b/theme.py @@ -277,6 +277,7 @@ def setThemeNight(baseDir: str): fontStr = \ "url('./fonts/solidaric.woff2') format('woff2')" themeParams = { + "focus-color": "blue", "font-size-button-mobile": "36px", "font-size": "32px", "font-size2": "26px", @@ -320,6 +321,7 @@ def setThemeStarlight(baseDir: str): removeTheme(baseDir) setThemeInConfig(baseDir, name) themeParams = { + "focus-color": "darkred", "font-size-button-mobile": "36px", "font-size": "32px", "font-size2": "26px", @@ -341,6 +343,7 @@ def setThemeStarlight(baseDir: str): "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background": "#69282c", + "button-small-background": "darkblue", "button-selected": "#a34046", "button-highlighted": "#12435f", "button-fg-highlighted": "white", @@ -501,7 +504,9 @@ def setThemeLCD(baseDir: str): "button-selected": "black", "button-highlighted": "green", "button-background": "#33390d", + "button-small-background": "#33390d", "button-text": "#9fb42b", + "button-small-text": "#9fb42b", "color: #FFFFFE;": "color: #9fb42b;", "calendar-bg-color": "#eee", "day-number": "#3f2145", @@ -568,7 +573,9 @@ def setThemePurple(baseDir: str): "main-visited-color": "#f93bb0", "button-selected": "#c042a0", "button-background": "#ff42a0", + "button-small-background": "#ff42a0", "button-text": "white", + "button-small-text": "white", "color: #FFFFFE;": "color: #1f152d;", "calendar-bg-color": "#eee", "lines-color": "#ff42a0", @@ -599,6 +606,7 @@ def setThemePurple(baseDir: str): def setThemeHacker(baseDir: str): name = 'hacker' themeParams = { + "focus-color": "green", "main-bg-color": "black", "link-bg-color": "black", "main-bg-color-dm": "#0b0a0a", @@ -612,7 +620,9 @@ def setThemeHacker(baseDir: str): "main-visited-color": "#3c8234", "button-selected": "#063200", "button-background": "#062200", + "button-small-background": "#062200", "button-text": "#00ff00", + "button-small-text": "#00ff00", "button-corner-radius": "4px", "timeline-border-radius": "4px", "*font-family": "'Bedstead'", @@ -646,6 +656,7 @@ def setThemeHacker(baseDir: str): def setThemeLight(baseDir: str): name = 'light' themeParams = { + "focus-color": "grey", "font-size-button-mobile": "36px", "font-size": "32px", "font-size2": "26px", @@ -700,6 +711,7 @@ def setThemeLight(baseDir: str): def setThemeSolidaric(baseDir: str): name = 'solidaric' themeParams = { + "focus-color": "grey", "font-size-button-mobile": "36px", "font-size": "32px", "font-size2": "26px", diff --git a/translations/ar.json b/translations/ar.json index b6762b3d2..b13483b47 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -91,7 +91,7 @@ "Stop blocking": "وقف الحظر", "Enter an emoji name to search for": "أدخل اسم رمز تعبيري للبحث عنه", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "أدخل عنوانًا أو عنصرًا مشتركًا أو! history أو #hashtag أو * مهارة أو: emoji: للبحث عنه", - "Go Back": "عد", + "Go Back": "◀", "Moderation Information": "معلومات الاعتدال", "Suspended accounts": "الحسابات المعلقه", "These are currently suspended": "هذه معلقة حاليا", diff --git a/translations/ca.json b/translations/ca.json index c4850e8ef..774fd067a 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -91,7 +91,7 @@ "Stop blocking": "Deixeu de bloquejar", "Enter an emoji name to search for": "Introduïu un nom emoji per cercar", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Introduïu una adreça, un element compartit, un historial!, #Hashtag, * skill o: emoji: per cercar", - "Go Back": "Torna", + "Go Back": "◀", "Moderation Information": "Informació de moderació", "Suspended accounts": "Comptes suspesos", "These are currently suspended": "Actualment estan suspeses", diff --git a/translations/cy.json b/translations/cy.json index afddb9c9f..28f8cdc38 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -91,7 +91,7 @@ "Stop blocking": "Stopiwch rwystro", "Enter an emoji name to search for": "Rhowch enw emoji i chwilio amdano", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Rhowch gyfeiriad, eitem a rennir ,! Hanes, #hashtag, * sgil neu: emoji: i chwilio amdano", - "Go Back": "Go Back", + "Go Back": "◀", "Moderation Information": "Gwybodaeth Cymedroli", "Suspended accounts": "Cyfrifon gohiriedig", "These are currently suspended": "Mae'r rhain wedi'u hatal ar hyn o bryd", diff --git a/translations/de.json b/translations/de.json index 232e65d8b..86f695959 100644 --- a/translations/de.json +++ b/translations/de.json @@ -91,7 +91,7 @@ "Stop blocking": "Sperre aufheben", "Enter an emoji name to search for": "Geben Sie einen Emojinamen ein, nach dem gesucht werden soll", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Geben Sie eine Adresse, ein freigegebenes Element ,! History, #hashtag, * Skill oder: emoji: ein, nach der gesucht werden soll", - "Go Back": "Zurück", + "Go Back": "◀", "Moderation Information": "Moderationsinformationen", "Suspended accounts": "Temporäre gesperrte Benutzer", "These are currently suspended": "Diese sind temporär gesperrt", diff --git a/translations/en.json b/translations/en.json index 1261381b6..4a559bb42 100644 --- a/translations/en.json +++ b/translations/en.json @@ -39,7 +39,7 @@ "Report": "Report", "Send to moderators": "Send to moderators", "Search for emoji": "Search for emoji", - "Cancel": "Cancel", + "Cancel": "✘", "Submit": "Submit", "Image description": "Image description", "Item image": "Item image", @@ -91,7 +91,7 @@ "Stop blocking": "Stop blocking", "Enter an emoji name to search for": "Enter an emoji name to search for", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for", - "Go Back": "Go Back", + "Go Back": "◀", "Moderation Information": "Moderation Information", "Suspended accounts": "Suspended accounts", "These are currently suspended": "These are currently suspended", diff --git a/translations/es.json b/translations/es.json index 333480767..e19e01a6b 100644 --- a/translations/es.json +++ b/translations/es.json @@ -91,7 +91,7 @@ "Stop blocking": "Dejar de bloquear", "Enter an emoji name to search for": "Ingrese un nombre de emoji para buscar", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Ingrese una dirección, elemento compartido,! Historial, #hashtag, * skill o: emoji: para buscar", - "Go Back": "Regresa", + "Go Back": "◀", "Moderation Information": "Información de moderación", "Suspended accounts": "Cuentas suspendidas", "These are currently suspended": "Actualmente están suspendidos", diff --git a/translations/fr.json b/translations/fr.json index f4b9dd0f4..17dafb27d 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -91,7 +91,7 @@ "Stop blocking": "Arrêtez le blocage", "Enter an emoji name to search for": "Entrez un nom emoji à rechercher", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Entrez une adresse, un élément partagé,! History, #hashtag, * skill ou: emoji: pour rechercher", - "Go Back": "Retourner", + "Go Back": "◀", "Moderation Information": "Informations de modération", "Suspended accounts": "Comptes suspendus", "These are currently suspended": "Ceux-ci sont actuellement suspendus", diff --git a/translations/ga.json b/translations/ga.json index a812ff8e3..b012a8098 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -91,7 +91,7 @@ "Stop blocking": "Stop blocáil", "Enter an emoji name to search for": "Cuir isteach ainm emoji chun cuardach a dhéanamh", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Iontráil seoladh, mír roinnte ,! Stair, #hashtag, * scil nó: emoji: chun cuardach a dhéanamh", - "Go Back": "Dul ar ais", + "Go Back": "◀", "Moderation Information": "Faisnéis Modhnóireachta", "Suspended accounts": "Cuntais ar fionraí", "These are currently suspended": "Tá siad seo ar fionraí faoi láthair", diff --git a/translations/hi.json b/translations/hi.json index 1b961804e..a05b9e387 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -91,7 +91,7 @@ "Stop blocking": "रोकना बंद करो", "Enter an emoji name to search for": "खोजने के लिए एक इमोजी नाम दर्ज करें", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "एक पता, साझा किया गया आइटम दर्ज करें; इतिहास, # अंश, * कौशल या: इमोजी: खोजने के लिए", - "Go Back": "वापस जाओ", + "Go Back": "◀", "Moderation Information": "मॉडरेशन जानकारी", "Suspended accounts": "निलंबित खाते", "These are currently suspended": "ये फिलहाल निलंबित हैं", diff --git a/translations/it.json b/translations/it.json index b7c9c0507..356112f64 100644 --- a/translations/it.json +++ b/translations/it.json @@ -91,7 +91,7 @@ "Stop blocking": "Smetti di bloccare", "Enter an emoji name to search for": "Inserisci un nome emoji da cercare", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Inserisci un indirizzo, un oggetto condiviso,! Storia, #hashtag, * abilità o: emoji: per cercare", - "Go Back": "Torna indietro", + "Go Back": "◀", "Moderation Information": "Informazioni sulla moderazione", "Suspended accounts": "Conti sospesi", "These are currently suspended": "Questi sono attualmente sospesi", diff --git a/translations/ja.json b/translations/ja.json index 562118dd5..89351b413 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -91,7 +91,7 @@ "Stop blocking": "ブロックを停止", "Enter an emoji name to search for": "検索する絵文字名を入力してください", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "検索するアドレス、共有アイテム、!history、#ハッシュタグ、* skillまたは:emoji:を入力してください", - "Go Back": "戻る", + "Go Back": "◀", "Moderation Information": "モデレーション情報", "Suspended accounts": "一時停止されたアカウント", "These are currently suspended": "これらは現在一時停止中です", diff --git a/translations/oc.json b/translations/oc.json index 42ca4e070..71eddf628 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -98,7 +98,7 @@ "About this Instance": "A prepaus d’aquesta instància", "Any blocks or suspensions made by moderators will be shown here.": "Tot blocatge o suspension realizada pels moderators son mostrats aquí.", "These are globally blocked for all accounts on this instance": "Aquí son los blocatges generals per totes los comptes d’aquesta instància", - "Go Back": "Tornar", + "Go Back": "◀", "Stop blocking": "Quitar de blocar", "View": "Veire", "Options for": "Opcions per", diff --git a/translations/pt.json b/translations/pt.json index 279e6c9ce..79c2a7cee 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -91,7 +91,7 @@ "Stop blocking": "Pare de bloquear", "Enter an emoji name to search for": "Digite um nome emoji para procurar", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Digite um endereço, item compartilhado,! History, #hashtag, * skill ou: emoji: para procurar", - "Go Back": "Volte", + "Go Back": "◀", "Moderation Information": "Informações sobre moderação", "Suspended accounts": "Contas suspensas", "These are currently suspended": "Estes estão atualmente suspensos", diff --git a/translations/ru.json b/translations/ru.json index a5471654a..e9befe81c 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -91,7 +91,7 @@ "Stop blocking": "Прекратить блокировку", "Enter an emoji name to search for": "Введите имя смайлика для поиска", "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Введите адрес, общий элемент,! History, #hashtag, * skill или: emoji: для поиска", - "Go Back": "Вернитесь назад", + "Go Back": "◀", "Moderation Information": "Модерация Информация", "Suspended accounts": "Приостановленные аккаунты", "These are currently suspended": "В настоящее время они приостановлены", diff --git a/translations/zh.json b/translations/zh.json index 99f982152..2afa7f0c0 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -111,6 +111,7 @@ "The files attached below should be no larger than 10MB in total uploaded at once.": "一次上传的文件总数不得超过10MB。", "Avatar image": "头像图片", "Background image": "背景图", + "Go Back": "◀", "Timeline banner image": "时间线横幅图片", "Approve follower requests": "批准关注者请求", "This is a bot account": "这是一个机器人帐户", diff --git a/utils.py b/utils.py index abdb67be7..d5601f651 100644 --- a/utils.py +++ b/utils.py @@ -215,6 +215,8 @@ def getDisplayName(baseDir: str, actor: str, personCache: {}) -> str: def getNicknameFromActor(actor: str) -> str: """Returns the nickname from an actor url """ + if actor.startswith('@'): + actor = actor[1:] if '/users/' not in actor: if '/profile/' in actor: nickStr = actor.split('/profile/')[1].replace('@', '') @@ -222,18 +224,27 @@ def getNicknameFromActor(actor: str) -> str: return nickStr else: return nickStr.split('/')[0] - if '/channel/' in actor: + elif '/channel/' in actor: nickStr = actor.split('/channel/')[1].replace('@', '') if '/' not in nickStr: return nickStr else: return nickStr.split('/')[0] - # https://domain/@nick - if '/@' in actor: + elif '/accounts/' in actor: + nickStr = actor.split('/accounts/')[1].replace('@', '') + if '/' not in nickStr: + return nickStr + else: + return nickStr.split('/')[0] + elif '/@' in actor: + # https://domain/@nick nickStr = actor.split('/@')[1] if '/' in nickStr: nickStr = nickStr.split('/')[0] return nickStr + elif '@' in actor: + nickStr = actor.split('@')[0] + return nickStr return None nickStr = actor.split('/users/')[1].replace('@', '') if '/' not in nickStr: @@ -245,28 +256,38 @@ def getNicknameFromActor(actor: str) -> str: def getDomainFromActor(actor: str) -> (str, int): """Returns the domain name from an actor url """ + if actor.startswith('@'): + actor = actor[1:] port = None prefixes = getProtocolPrefixes() if '/profile/' in actor: domain = actor.split('/profile/')[0] for prefix in prefixes: domain = domain.replace(prefix, '') + elif '/accounts/' in actor: + domain = actor.split('/accounts/')[0] + for prefix in prefixes: + domain = domain.replace(prefix, '') + elif '/channel/' in actor: + domain = actor.split('/channel/')[0] + for prefix in prefixes: + domain = domain.replace(prefix, '') + elif '/users/' in actor: + domain = actor.split('/users/')[0] + for prefix in prefixes: + domain = domain.replace(prefix, '') + elif '/@' in actor: + domain = actor.split('/@')[0] + for prefix in prefixes: + domain = domain.replace(prefix, '') + elif '@' in actor: + domain = actor.split('@')[1].strip() else: - if '/channel/' in actor: - domain = actor.split('/channel/')[0] - for prefix in prefixes: - domain = domain.replace(prefix, '') - else: - if '/users/' not in actor: - domain = actor - for prefix in prefixes: - domain = domain.replace(prefix, '') - if '/' in actor: - domain = domain.split('/')[0] - else: - domain = actor.split('/users/')[0] - for prefix in prefixes: - domain = domain.replace(prefix, '') + domain = actor + for prefix in prefixes: + domain = domain.replace(prefix, '') + if '/' in actor: + domain = domain.split('/')[0] if ':' in domain: portStr = domain.split(':')[1] if not portStr.isdigit(): @@ -576,12 +597,13 @@ def validNickname(domain: str, nickname: str) -> bool: if nickname == domain: return False reservedNames = ('inbox', 'dm', 'outbox', 'following', - 'public', 'followers', 'profile', + 'public', 'followers', 'channel', 'capabilities', 'calendar', 'tlreplies', 'tlmedia', 'tlblogs', 'moderation', 'activity', 'undo', 'reply', 'replies', 'question', 'like', 'likes', 'users', 'statuses', + 'accounts', 'channels', 'profile', 'updates', 'repeat', 'announce', 'shares', 'fonts', 'icons') if nickname in reservedNames: diff --git a/webinterface.py b/webinterface.py index 6f8030950..79205e8c6 100644 --- a/webinterface.py +++ b/webinterface.py @@ -252,7 +252,7 @@ def updateAvatarImageCache(session, baseDir: str, httpPrefix: str, print('Failed to download avatar image: ' + str(avatarUrl)) print(e) prof = 'https://www.w3.org/ns/activitystreams' - if '/channel/' not in actor: + if '/channel/' not in actor or '/accounts/' not in actor: sessionHeaders = { 'Accept': 'application/activity+json; profile="' + prof + '"' } @@ -747,7 +747,13 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int, # add the page title hashtagSearchForm = htmlHeader(cssFilename, hashtagSearchCSS) - hashtagSearchForm += '
Tox: