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

main
Bob Mottram 2020-08-18 21:17:27 +01:00
commit 2b8d34d9e6
51 changed files with 813 additions and 273 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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

247
daemon.py
View File

@ -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:

View File

@ -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:

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 + '"'

View File

@ -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:

View File

@ -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?
"""

BIN
img/logo128.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
img/logo144.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/logo152.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
img/logo192.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
img/logo256.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/logo512.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
img/logo72.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
img/logo96.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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",

View File

@ -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": "هذه معلقة حاليا",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "ये फिलहाल निलंबित हैं",

View File

@ -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",

View File

@ -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": "これらは現在一時停止中です",

View File

@ -98,7 +98,7 @@
"About this Instance": "A prepaus daquesta 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 daquesta instància",
"Go Back": "Tornar",
"Go Back": "",
"Stop blocking": "Quitar de blocar",
"View": "Veire",
"Options for": "Opcions per",

View File

@ -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",

View File

@ -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": "В настоящее время они приостановлены",

View File

@ -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": "这是一个机器人帐户",

View File

@ -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:

View File

@ -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 += '<center><h1>#' + hashtag + '</h1></center>'
if nickname:
hashtagSearchForm += '<center>\n' + \
'<h1><a href="/users/' + nickname + '/search">#' + \
hashtag + '</a></h1>\n' + '</center>\n'
else:
hashtagSearchForm += '<center>\n' + \
'<h1>#' + hashtag + '</h1>\n' + '</center>\n'
if startIndex > 0:
# previous page link
@ -758,7 +764,7 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"></a></center>'
'"></a></center>\n'
index = startIndex
while index <= endIndex:
postId = lines[index].strip('\n').strip('\r')
@ -1306,240 +1312,242 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
editProfileForm = htmlHeader(cssFilename, editProfileCSS)
editProfileForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/profiledata">'
editProfileForm += ' <div class="vertical-center">'
'accept-charset="UTF-8" action="' + path + '/profiledata">\n'
editProfileForm += ' <div class="vertical-center">\n'
editProfileForm += \
' <p class="new-post-text">' + translate['Profile for'] + \
' ' + nickname + '@' + domainFull + '</p>'
editProfileForm += ' <div class="container">'
editProfileForm += \
' <input type="submit" name="submitProfile" value="' + \
translate['Submit'] + '">'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
translate['Cancel'] + '</button></a>'
editProfileForm += ' </div>'
translate['Go Back'] + '</button></a>\n'
editProfileForm += \
' <input type="submit" name="submitProfile" value="' + \
translate['Submit'] + '">\n'
editProfileForm += ' </div>\n'
if scheduledPostsExist(baseDir, nickname, domain):
editProfileForm += ' <div class="container">'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeScheduledPosts"> ' + \
translate['Remove scheduled posts'] + '<br>'
editProfileForm += ' </div>'
translate['Remove scheduled posts'] + '<br>\n'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">'
editProfileForm += ' <div class="container">\n'
editProfileForm += ' <label class="labels">' + \
translate['Nickname'] + '</label>'
translate['Nickname'] + '</label>\n'
editProfileForm += \
' <input type="text" name="displayNickname" value="' + \
displayNickname + '"><br>'
displayNickname + '"><br>\n'
editProfileForm += \
' <label class="labels">' + translate['Your bio'] + '</label>'
' <label class="labels">' + translate['Your bio'] + '</label>\n'
editProfileForm += \
' <textarea id="message" name="bio" style="height:200px">' + \
bioStr + '</textarea>'
bioStr + '</textarea>\n'
editProfileForm += '<label class="labels">' + \
translate['Donations link'] + '</label><br>'
translate['Donations link'] + '</label><br>\n'
editProfileForm += \
' <input type="text" placeholder="https://..." ' + \
'name="donateUrl" value="' + donateUrl + '">'
'name="donateUrl" value="' + donateUrl + '">\n'
editProfileForm += \
'<label class="labels">' + translate['XMPP'] + '</label><br>'
'<label class="labels">' + translate['XMPP'] + '</label><br>\n'
editProfileForm += \
' <input type="text" name="xmppAddress" value="' + \
xmppAddress + '">'
xmppAddress + '">\n'
editProfileForm += '<label class="labels">' + \
translate['Matrix'] + '</label><br>'
translate['Matrix'] + '</label><br>\n'
editProfileForm += \
' <input type="text" name="matrixAddress" value="' + \
matrixAddress+'">'
matrixAddress+'">\n'
editProfileForm += '<label class="labels">SSB</label><br>'
editProfileForm += '<label class="labels">SSB</label><br>\n'
editProfileForm += \
' <input type="text" name="ssbAddress" value="' + \
ssbAddress + '">'
ssbAddress + '">\n'
editProfileForm += '<label class="labels">Blog</label><br>'
editProfileForm += '<label class="labels">Blog</label><br>\n'
editProfileForm += \
' <input type="text" name="blogAddress" value="' + \
blogAddress + '">'
blogAddress + '">\n'
editProfileForm += '<label class="labels">Tox</label><br>'
editProfileForm += '<label class="labels">Tox</label><br>\n'
editProfileForm += \
' <input type="text" name="toxAddress" value="' + \
toxAddress + '">'
toxAddress + '">\n'
editProfileForm += '<label class="labels">' + \
translate['Email'] + '</label><br>'
translate['Email'] + '</label><br>\n'
editProfileForm += \
' <input type="text" name="email" value="' + emailAddress + '">'
' <input type="text" name="email" value="' + emailAddress + '">\n'
editProfileForm += \
'<label class="labels">' + \
translate['PGP Fingerprint'] + '</label><br>'
translate['PGP Fingerprint'] + '</label><br>\n'
editProfileForm += \
' <input type="text" name="openpgp" value="' + \
PGPfingerprint + '">'
PGPfingerprint + '">\n'
editProfileForm += \
'<label class="labels">' + translate['PGP'] + '</label><br>'
'<label class="labels">' + translate['PGP'] + '</label><br>\n'
editProfileForm += \
' <textarea id="message" placeholder=' + \
'"-----BEGIN PGP PUBLIC KEY BLOCK-----" name="pgp" ' + \
'style="height:100px">' + PGPpubKey + '</textarea>'
'style="height:100px">' + PGPpubKey + '</textarea>\n'
editProfileForm += '<a href="/users/' + nickname + \
'/followingaccounts"><label class="labels">' + \
translate['Following'] + '</label></a><br>'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
translate['Following'] + '</label></a><br>\n'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
idx = 'The files attached below should be no larger than ' + \
'10MB in total uploaded at once.'
editProfileForm += \
' <label class="labels">' + translate[idx] + '</label><br><br>'
' <label class="labels">' + translate[idx] + '</label><br><br>\n'
editProfileForm += \
' <label class="labels">' + translate['Avatar image'] + '</label>'
' <label class="labels">' + translate['Avatar image'] + \
'</label>\n'
editProfileForm += \
' <input type="file" id="avatar" name="avatar"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += \
' <br><label class="labels">' + \
translate['Background image'] + '</label>'
translate['Background image'] + '</label>\n'
editProfileForm += ' <input type="file" id="image" name="image"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' <br><label class="labels">' + \
translate['Timeline banner image'] + '</label>'
translate['Timeline banner image'] + '</label>\n'
editProfileForm += ' <input type="file" id="banner" name="banner"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' <br><label class="labels">' + \
translate['Search banner image'] + '</label>'
translate['Search banner image'] + '</label>\n'
editProfileForm += ' <input type="file" id="search_banner" '
editProfileForm += 'name="search_banner"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
'<label class="labels">' + translate['Change Password'] + \
'</label><br>'
editProfileForm += ' <input type="text" name="password" value=""><br>'
'</label><br>\n'
editProfileForm += ' <input type="text" name="password" ' + \
'value=""><br>\n'
editProfileForm += \
'<label class="labels">' + translate['Confirm Password'] + \
'</label><br>'
'</label><br>\n'
editProfileForm += \
' <input type="text" name="passwordconfirm" value="">'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
' <input type="text" name="passwordconfirm" value="">\n'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="approveFollowers" ' + manuallyApprovesFollowers + \
'> ' + translate['Approve follower requests'] + '<br>'
'> ' + translate['Approve follower requests'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="isBot" ' + \
isBot + '> ' + translate['This is a bot account'] + '<br>'
isBot + '> ' + translate['This is a bot account'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="isGroup" ' + isGroup + '> ' + \
translate['This is a group account'] + '<br>'
translate['This is a group account'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="followDMs" ' + followDMs + '> ' + \
translate['Only people I follow can send me DMs'] + '<br>'
translate['Only people I follow can send me DMs'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeTwitter" ' + removeTwitter + '> ' + \
translate['Remove Twitter posts'] + '<br>'
translate['Remove Twitter posts'] + '<br>\n'
if path.startswith('/users/' + adminNickname + '/'):
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="mediaInstance" ' + mediaInstanceStr + '> ' + \
translate['This is a media instance'] + '<br>'
translate['This is a media instance'] + '<br>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Filtered words'] + '</label></b>'
translate['Filtered words'] + '</label></b>\n'
editProfileForm += ' <br><label class="labels">' + \
translate['One per line'] + '</label>'
translate['One per line'] + '</label>\n'
editProfileForm += ' <textarea id="message" ' + \
'name="filteredWords" style="height:200px">' + \
filterStr + '</textarea>'
filterStr + '</textarea>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Word Replacements'] + '</label></b>'
editProfileForm += ' <br><label class="labels">A -> B</label>'
translate['Word Replacements'] + '</label></b>\n'
editProfileForm += ' <br><label class="labels">A -> B</label>\n'
editProfileForm += \
' <textarea id="message" name="switchWords" ' + \
'style="height:200px">' + switchStr + '</textarea>'
'style="height:200px">' + switchStr + '</textarea>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Blocked accounts'] + '</label></b>'
translate['Blocked accounts'] + '</label></b>\n'
idx = 'Blocked accounts, one per line, in the form ' + \
'nickname@domain or *@blockeddomain'
editProfileForm += \
' <br><label class="labels">' + translate[idx] + '</label>'
' <br><label class="labels">' + translate[idx] + '</label>\n'
editProfileForm += \
' <textarea id="message" name="blocked" style="height:200px">' + \
blockedStr + '</textarea>'
blockedStr + '</textarea>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Federation list'] + '</label></b>'
translate['Federation list'] + '</label></b>\n'
idx = 'Federate only with a defined set of instances. ' + \
'One domain name per line.'
editProfileForm += \
' <br><label class="labels">' + \
translate[idx] + '</label>'
translate[idx] + '</label>\n'
editProfileForm += \
' <textarea id="message" name="allowedInstances" ' + \
'style="height:200px">' + allowedInstancesStr + '</textarea>'
'style="height:200px">' + allowedInstancesStr + '</textarea>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Git Projects'] + '</label></b>'
translate['Git Projects'] + '</label></b>\n'
idx = 'List of project names that you wish to receive git patches for'
editProfileForm += \
' <br><label class="labels">' + \
translate[idx] + '</label>'
translate[idx] + '</label>\n'
editProfileForm += \
' <textarea id="message" name="gitProjects" ' + \
'style="height:100px">' + gitProjectsStr + '</textarea>'
'style="height:100px">' + gitProjectsStr + '</textarea>\n'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['YouTube Replacement Domain'] + '</label></b>'
translate['YouTube Replacement Domain'] + '</label></b>\n'
YTReplacementDomain = getConfigParam(baseDir, "youtubedomain")
if not YTReplacementDomain:
YTReplacementDomain = ''
editProfileForm += \
' <input type="text" name="ytdomain" value="' + \
YTReplacementDomain + '">'
YTReplacementDomain + '">\n'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <b><label class="labels">' + \
translate['Skills'] + '</label></b><br>'
translate['Skills'] + '</label></b><br>\n'
idx = 'If you want to participate within organizations then you ' + \
'can indicate some skills that you have and approximate ' + \
'proficiency levels. This helps organizers to construct ' + \
'teams with an appropriate combination of skills.'
editProfileForm += ' <label class="labels">' + \
translate[idx] + '</label>'
translate[idx] + '</label>\n'
editProfileForm += skillsStr + themesDropdown + moderatorsStr
editProfileForm += ' </div>' + instanceStr
editProfileForm += ' <div class="container">'
editProfileForm += ' </div>\n' + instanceStr
editProfileForm += ' <div class="container">\n'
editProfileForm += ' <b><label class="labels">' + \
translate['Danger Zone'] + '</label></b><br>'
translate['Danger Zone'] + '</label></b><br>\n'
editProfileForm += \
' <input type="checkbox" class=dangercheckbox" ' + \
'name="deactivateThisAccount"> ' + \
translate['Deactivate this account'] + '<br>'
editProfileForm += ' </div>'
editProfileForm += ' </div>'
editProfileForm += '</form>'
translate['Deactivate this account'] + '<br>\n'
editProfileForm += ' </div>\n'
editProfileForm += ' </div>\n'
editProfileForm += '</form>\n'
editProfileForm += htmlFooter()
return editProfileForm
@ -2206,7 +2214,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
newPostForm += \
' <a href="' + pathBase + \
'/inbox"><button class="cancelbtn">' + \
translate['Cancel'] + '</button></a>\n'
translate['Go Back'] + '</button></a>\n'
newPostForm += \
' <input type="submit" name="submitPost" value="' + \
translate['Submit'] + '">\n'
@ -2289,6 +2297,8 @@ def htmlHeader(cssFilename: str, css: str, lang='en') -> str:
htmlStr += ' <link rel="preload" as="font" type="' + \
fontFormat + '" href="' + fontName + '" crossorigin>\n'
htmlStr += ' <style>\n' + css + '</style>\n'
htmlStr += ' <link rel="manifest" href="/manifest.json">\n'
htmlStr += ' <meta name="theme-color" content="grey">\n'
htmlStr += ' </head>\n'
htmlStr += ' <body>\n'
return htmlStr
@ -2671,7 +2681,7 @@ def htmlProfile(defaultTimeline: str,
ssbAddress + '</label></p>\n'
if toxAddress:
donateSection += \
'<p>Tox: <label class="ssbaddr">' + \
'<p>Tox: <label class="toxaddr">' + \
toxAddress + '</label></p>\n'
if PGPfingerprint:
donateSection += \
@ -2791,30 +2801,32 @@ def htmlProfile(defaultTimeline: str,
profileStr = \
linkToTimelineStart + profileHeaderStr + \
linkToTimelineEnd + donateSection
profileStr += '<div class="container">\n'
profileStr += '<div class="container" id="buttonheader">\n'
profileStr += ' <center>'
profileStr += \
' <a href="' + usersPath + '"><button class="' + postsButton + \
'"><span>' + translate['Posts'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/following"><button class="' + \
followingButton + '"><span>' + translate['Following'] + \
' <a href="' + usersPath + '#buttonheader"><button class="' + \
postsButton + '"><span>' + translate['Posts'] + \
' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/followers"><button class="' + \
followersButton + '"><span>' + translate['Followers'] + \
' <a href="' + usersPath + '/following#buttonheader">' + \
'<button class="' + followingButton + '"><span>' + \
translate['Following'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/followers#buttonheader">' + \
'<button class="' + followersButton + \
'"><span>' + translate['Followers'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/roles#buttonheader">' + \
'<button class="' + rolesButton + '"><span>' + translate['Roles'] + \
' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/roles"><button class="' + \
rolesButton + '"><span>' + translate['Roles'] + ' </span></button></a>'
' <a href="' + usersPath + '/skills#buttonheader">' + \
'<button class="' + skillsButton + '"><span>' + \
translate['Skills'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/skills"><button class="' + \
skillsButton + '"><span>' + translate['Skills'] + \
' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/shares"><button class="' + \
sharesButton + '"><span>' + translate['Shares'] + \
' </span></button></a>'
' <a href="' + usersPath + '/shares#buttonheader">' + \
'<button class="' + sharesButton + '"><span>' + \
translate['Shares'] + ' </span></button></a>'
profileStr += editProfileStr + logoutStr
profileStr += ' </center>'
profileStr += '</div>'
@ -3708,7 +3720,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain,
displayName, False)
avatarLink = ' <a href="' + postActor + '">'
avatarLink = ' <a class="imageAnchor" href="' + postActor + '">'
avatarLink += \
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>'
@ -3716,14 +3728,15 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
if showAvatarOptions and \
fullDomain + '/users/' + nickname not in postActor:
avatarLink = \
' <a href="/users/' + nickname + '?options=' + postActor + \
' <a class="imageAnchor" href="/users/' + \
nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">\n'
avatarLink += \
' <img loading="lazy" title="' + \
translate['Show options for this person'] + \
'" src="' + avatarUrl + '" ' + avatarPosition + '/></a>\n'
avatarImageInPost = \
' <div class="timeline-avatar">' + avatarLink + '</div>\n'
' <div class="timeline-avatar">' + avatarLink.strip() + '</div>\n'
# don't create new html within the bookmarks timeline
# it should already have been created for the inbox
@ -3788,7 +3801,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain,
displayName, False)
titleStr += \
'<a href="/users/' + nickname + '?options=' + postActor + \
'<a class="imageAnchor" href="/users/' + \
nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
'">' + displayName + '</a>\n'
else:
@ -3802,7 +3816,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
# pprint(postJsonObject)
print('ERROR: no actorDomain')
titleStr += \
'<a href="/users/' + nickname + '?options=' + postActor + \
'<a class="imageAnchor" href="/users/' + \
nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
'">@' + actorNickname + '@' + actorDomain + '</a>\n'
@ -3833,19 +3848,20 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
replyStr = ''
if isPublicRepeat:
replyStr += \
'<a href="/users/' + nickname + '?replyto=' + replyToLink + \
'<a class="imageAnchor" href="/users/' + \
nickname + '?replyto=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">\n'
else:
if isDM(postJsonObject):
replyStr += \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?replydm=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">\n'
else:
replyStr += \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?replyfollowers=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">\n'
@ -3861,7 +3877,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
if isBlogPost(postJsonObject):
if '/statuses/' in postJsonObject['object']['id']:
editStr += \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" href="/users/' + nickname + \
'/tlblogs?editblogpost=' + \
postJsonObject['object']['id'].split('/statuses/')[1] + \
'?actor=' + actorNickname + \
@ -3885,7 +3901,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
announceLink = 'unrepeatprivate'
announceTitle = translate['Undo the repeat']
announceStr = \
'<a href="/users/' + nickname + '?' + announceLink + \
'<a class="imageAnchor" href="/users/' + \
nickname + '?' + announceLink + \
'=' + postJsonObject['object']['id'] + pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
'?bm=' + timelinePostBookmark + \
@ -3913,7 +3930,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
likeLink = 'unlike'
likeTitle = translate['Undo the like']
likeStr = \
'<a href="/users/' + nickname + '?' + \
'<a class="imageAnchor" href="/users/' + nickname + '?' + \
likeLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
@ -3935,7 +3952,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
bookmarkLink = 'unbookmark'
bookmarkTitle = translate['Undo the bookmark']
bookmarkStr = \
'<a href="/users/' + nickname + '?' + \
'<a class="imageAnchor" href="/users/' + nickname + '?' + \
bookmarkLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
@ -3955,7 +3972,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
messageId.startswith(postActor))):
if '/users/' + nickname + '/' in messageId:
deleteStr = \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?delete=' + messageId + pageNumberParam + \
'" title="' + translate['Delete this post'] + '">\n'
deleteStr += \
@ -3965,7 +3982,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
else:
if not isMuted:
muteStr = \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?mute=' + messageId + pageNumberParam + '?tl=' + boxName + \
'?bm=' + timelinePostBookmark + \
'" title="' + translate['Mute this post'] + '">\n'
@ -3976,7 +3993,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
'" src="/' + iconsDir + '/mute.png"/></a>\n'
else:
muteStr = \
'<a href="/users/' + nickname + '?unmute=' + messageId + \
'<a class="imageAnchor" href="/users/' + \
nickname + '?unmute=' + messageId + \
pageNumberParam + '?tl=' + boxName + '?bm=' + \
timelinePostBookmark + '" title="' + \
translate['Undo mute'] + '">\n'
@ -4034,7 +4052,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
idx = 'Show options for this person'
replyAvatarImageInPost = \
'<div class="timeline-avatar-reply">\n' \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" ' + \
'href="/users/' + nickname + \
'?options=' + \
announceActor + ';' + str(pageNumber) + \
';' + announceAvatarUrl + \
@ -4128,7 +4147,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
'<div class=' + \
'"timeline-avatar-reply">\n'
replyAvatarImageInPost += \
'<a href="/users/' + nickname + \
'<a class="imageAnchor" ' + \
'href="/users/' + nickname + \
'?options=' + replyActor + \
';' + str(pageNumber) + ';' + \
replyAvatarUrl + \
@ -4190,7 +4210,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
attachmentStr, galleryStr = \
getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
isMuted, avatarLink,
isMuted, avatarLink.strip(),
replyStr, announceStr, likeStr,
bookmarkStr, deleteStr, muteStr)
@ -4530,7 +4550,8 @@ def htmlTimeline(defaultTimeline: str,
# show follow approvals icon
followApprovals = \
'<a href="' + usersPath + \
'/followers"><img loading="lazy" ' + \
'/followers#buttonheader">' + \
'<img loading="lazy" ' + \
'class="timelineicon" alt="' + \
translate['Approve follow requests'] + \
'" title="' + translate['Approve follow requests'] + \
@ -4566,7 +4587,7 @@ def htmlTimeline(defaultTimeline: str,
if boxName != 'tlblogs':
if not manuallyApproveFollowers:
newPostButtonStr = \
'<a href="' + usersPath + \
'<a class="imageAnchor" href="' + usersPath + \
'/newpost"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="| ' + \
@ -4574,7 +4595,7 @@ def htmlTimeline(defaultTimeline: str,
'" class="timelineicon"/></a>\n'
else:
newPostButtonStr = \
'<a href="' + usersPath + \
'<a class="imageAnchor" href="' + usersPath + \
'/newfollowers"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + \
@ -4582,7 +4603,7 @@ def htmlTimeline(defaultTimeline: str,
'" class="timelineicon"/></a>\n'
else:
newPostButtonStr = \
'<a href="' + usersPath + \
'<a class="imageAnchor" href="' + usersPath + \
'/newblog"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="| ' + \
@ -4590,7 +4611,7 @@ def htmlTimeline(defaultTimeline: str,
'" class="timelineicon"/></a>\n'
else:
newPostButtonStr = \
'<a href="' + usersPath + \
'<a class="imageAnchor" href="' + usersPath + \
'/newdm"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new DM'] + \
@ -4684,7 +4705,7 @@ def htmlTimeline(defaultTimeline: str,
sharesButtonStr + bookmarksButtonStr + \
moderationButtonStr + newPostButtonStr
tlStr += \
' <a href="' + usersPath + \
' <a class="imageAnchor" href="' + usersPath + \
'/search"><img loading="lazy" src="/' + \
iconsDir + '/search.png" title="' + \
translate['Search and follow'] + '" alt="| ' + \
@ -4695,13 +4716,13 @@ def htmlTimeline(defaultTimeline: str,
# indicate that the calendar icon is highlighted
calendarAltText = '*' + calendarAltText + '*'
tlStr += \
' <a href="' + usersPath + calendarPath + \
' <a class="imageAnchor" href="' + usersPath + calendarPath + \
'"><img loading="lazy" src="/' + iconsDir + '/' + \
calendarImage + '" title="' + translate['Calendar'] + \
'" alt="| ' + calendarAltText + '" class="timelineicon"/></a>\n'
tlStr += \
' <a href="' + usersPath + '/minimal' + \
' <a class="imageAnchor" href="' + usersPath + '/minimal' + \
'"><img loading="lazy" src="/' + iconsDir + \
'/showhide.png" title="' + translate['Show/Hide Buttons'] + \
'" alt="| ' + translate['Show/Hide Buttons'] + \
@ -5622,7 +5643,8 @@ def htmlPersonOptions(translate: {}, baseDir: str,
translate['Petname'] + ': ' + \
'<input type="text" name="optionpetname" value="' + \
petname + '">\n' \
'<button type="submit" class="button" name="submitPetname">' + \
'<button type="submit" class="buttonsmall" ' + \
'name="submitPetname">' + \
translate['Submit'] + '</button><br>\n'
if isFollowingActor(baseDir, nickname, domain, optionsActor):
@ -5632,7 +5654,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
'<input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar" checked> ' + \
translate['Receive calendar events from this account'] + \
'<button type="submit" class="button" ' + \
'<button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
translate['Submit'] + '</button><br>\n'
else:
@ -5640,11 +5662,14 @@ def htmlPersonOptions(translate: {}, baseDir: str,
'<input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar"> ' + \
translate['Receive calendar events from this account'] + \
'<button type="submit" class="button" ' + \
'<button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
translate['Submit'] + '</button><br>\n'
optionsStr += optionsLinkStr
optionsStr += \
' <a href="/"><button type="button" class="button" ' + \
'name="submitBack">' + translate['Go Back'] + '</button></a>\n'
optionsStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>\n'
@ -5675,7 +5700,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
optionsStr += \
' <br><br>' + translate['Notes'] + ': \n'
optionsStr += '<button type="submit" class="button" ' + \
optionsStr += '<button type="submit" class="buttonsmall" ' + \
'name="submitPersonNotes">' + \
translate['Submit'] + '</button><br>\n'
optionsStr += \
@ -6244,10 +6269,10 @@ def htmlSearch(translate: {},
followStr += \
' <input type="hidden" name="actor" value="' + actor + '">\n'
followStr += ' <input type="text" name="searchtext" autofocus><br>\n'
followStr += ' <a href="/"><button type="button" class="button" ' + \
'name="submitBack">' + translate['Go Back'] + '</button></a>\n'
followStr += ' <button type="submit" class="button" ' + \
'name="submitSearch">' + translate['Submit'] + '</button>\n'
followStr += ' <button type="submit" class="button" ' + \
'name="submitBack">' + translate['Go Back'] + '</button>\n'
followStr += ' </form>\n'
followStr += ' <p class="hashtagswarm">' + \
htmlHashTagSwarm(baseDir, actor) + '</p>\n'
@ -6269,6 +6294,7 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
"""Show a profile page after a search for a fediverse address
"""
if '/users/' in profileHandle or \
'/accounts/' in profileHandle or \
'/channel/' in profileHandle or \
'/profile/' in profileHandle or \
'/@' in profileHandle:
@ -6440,15 +6466,15 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
profileStr += \
' <input type="hidden" name="actor" value="' + \
personUrl + '">\n'
profileStr += \
' <a href="' + backUrl + '"><button class="button">' + \
translate['Go Back'] + '</button></a>\n'
profileStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Follow'] + '</button>\n'
profileStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>\n'
profileStr += \
' <a href="' + backUrl + '"><button class="button">' + \
translate['Go Back'] + '</button></a>\n'
profileStr += ' </center>\n'
profileStr += ' </form>\n'
profileStr += '</div>\n'