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

merge-requests/30/head
Bob Mottram 2021-01-12 23:15:58 +00:00
commit e03b0b8908
57 changed files with 1400 additions and 415 deletions

17
blog.py
View File

@ -14,6 +14,7 @@ from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from webapp_utils import getPostAttachmentsAsHtml
from webapp_media import addEmbeddedElements
from utils import getConfigParam
from utils import getFullDomain
from utils import getMediaFormats
from utils import getNicknameFromActor
@ -386,7 +387,9 @@ def htmlBlogPost(authorized: bool,
cssFilename = baseDir + '/epicyon-blog.css'
if os.path.isfile(baseDir + '/blog.css'):
cssFilename = baseDir + '/blog.css'
blogStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
blogStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
_htmlBlogRemoveCwButton(blogStr, translate)
blogStr += _htmlBlogPostContent(authorized, baseDir,
@ -433,7 +436,9 @@ def htmlBlogPage(authorized: bool, session,
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
blogStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
blogStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
_htmlBlogRemoveCwButton(blogStr, translate)
blogsIndex = baseDir + '/accounts/' + \
@ -653,7 +658,9 @@ def htmlBlogView(authorized: bool,
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
blogStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
blogStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
if _noOfBlogAccounts(baseDir) <= 1:
nickname = _singleBlogAccountNickname(baseDir)
@ -755,7 +762,9 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
dateAndLocation += '<input type="text" name="location">'
dateAndLocation += '</div>'
editBlogForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
editBlogForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
editBlogForm += \
'<form enctype="multipart/form-data" method="POST" ' + \

View File

@ -903,6 +903,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
'png': 'image/png',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'webp': 'image/webp',
'avif': 'image/avif',
'mp4': 'video/mp4',

View File

@ -11,7 +11,9 @@ validContexts = (
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/identity/v1",
"https://w3id.org/security/v1",
"https://raitisoja.com/apschema/v1.21"
"*/apschema/v1.9",
"*/apschema/v1.21",
"*/litepub-0.1.jsonld"
)
@ -25,21 +27,79 @@ def hasValidContext(postJsonObject: {}) -> bool:
if not isinstance(url, str):
continue
if url not in validContexts:
print('Invalid @context: ' + url)
return False
wildcardFound = False
for c in validContexts:
if c.startswith('*'):
c = c.replace('*', '')
if url.endswith(c):
wildcardFound = True
break
if not wildcardFound:
print('Unrecognized @context: ' + url)
return False
elif isinstance(postJsonObject['@context'], str):
url = postJsonObject['@context']
if url not in validContexts:
print('Invalid @context: ' + url)
return False
wildcardFound = False
for c in validContexts:
if c.startswith('*'):
c = c.replace('*', '')
if url.endswith(c):
wildcardFound = True
break
if not wildcardFound:
print('Unrecognized @context: ' + url)
return False
else:
# not a list or string
return False
return True
def getApschemaV1_9() -> {}:
# https://domain/apschema/v1.9
return {
"@context": {
"zot": "https://hub.disroot.org/apschema#",
"id": "@id",
"type": "@type",
"commentPolicy": "as:commentPolicy",
"meData": "zot:meData",
"meDataType": "zot:meDataType",
"meEncoding": "zot:meEncoding",
"meAlgorithm": "zot:meAlgorithm",
"meCreator": "zot:meCreator",
"meSignatureValue": "zot:meSignatureValue",
"locationAddress": "zot:locationAddress",
"locationPrimary": "zot:locationPrimary",
"locationDeleted": "zot:locationDeleted",
"nomadicLocation": "zot:nomadicLocation",
"nomadicHubs": "zot:nomadicHubs",
"emojiReaction": "zot:emojiReaction",
"expires": "zot:expires",
"directMessage": "zot:directMessage",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"magicEnv": {
"@id": "zot:magicEnv",
"@type": "@id"
},
"nomadicLocations": {
"@id": "zot:nomadicLocations",
"@type": "@id"
},
"ostatus": "http://ostatus.org#",
"conversation": "ostatus:conversation",
"diaspora": "https://diasporafoundation.org/ns/",
"guid": "diaspora:guid",
"Hashtag": "as:Hashtag"
}
}
def getApschemaV1_21() -> {}:
# https://raitisoja.com/apschema/v1.21
# https://domain/apschema/v1.21
return {
"@context": {
"zot": "https://raitisoja.com/apschema#",
@ -69,6 +129,51 @@ def getApschemaV1_21() -> {}:
}
def getLitepubV0_1() -> {}:
# https://domain/schemas/litepub-0.1.jsonld
return {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Emoji": "toot:Emoji",
"Hashtag": "as:Hashtag",
"PropertyValue": "schema:PropertyValue",
"atomUri": "ostatus:atomUri",
"conversation": {
"@id": "ostatus:conversation",
"@type": "@id"
},
"discoverable": "toot:discoverable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"capabilities": "litepub:capabilities",
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
"invisible": "litepub:invisible",
"directMessage": "litepub:directMessage",
"listMessage": {
"@id": "litepub:listMessage",
"@type": "@id"
},
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
},
"EmojiReact": "litepub:EmojiReact",
"ChatMessage": "litepub:ChatMessage",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
}
}
]
}
def getV1SecuritySchema() -> {}:
# https://w3id.org/security/v1
return {

View File

@ -177,6 +177,7 @@ from shares import addShare
from shares import removeShare
from shares import expireShares
from categories import setHashtagCategory
from utils import isPublicPost
from utils import getLockedAccount
from utils import hasUsersPath
from utils import getFullDomain
@ -247,6 +248,7 @@ from newsdaemon import runNewswireDaemon
from filters import isFiltered
from filters import addGlobalFilter
from filters import removeGlobalFilter
from context import hasValidContext
import os
@ -265,7 +267,7 @@ maxPostsInNewsFeed = 10
maxPostsInRSSFeed = 10
# number of follows/followers per page
followsPerPage = 12
followsPerPage = 6
# number of item shares per page
sharesPerPage = 12
@ -288,6 +290,7 @@ class PubServer(BaseHTTPRequestHandler):
if path.endswith('.png') or \
path.endswith('.jpg') or \
path.endswith('.gif') or \
path.endswith('.svg') or \
path.endswith('.avif') or \
path.endswith('.webp'):
return True
@ -1039,6 +1042,14 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return 2
# check that the incoming message has a fully recognized
# linked data context
if not hasValidContext(messageJson):
print('Message arriving at inbox queue has no valid context')
self._400()
self.server.POSTbusy = False
return 3
# check for blocked domains so that they can be rejected early
messageDomain = None
if messageJson.get('actor'):
@ -1049,6 +1060,11 @@ class PubServer(BaseHTTPRequestHandler):
self._400()
self.server.POSTbusy = False
return 3
else:
print('Message arriving at inbox queue has no actor')
self._400()
self.server.POSTbusy = False
return 3
# if the inbox queue is full then return a busy code
if len(self.server.inboxQueue) >= self.server.maxQueueLength:
@ -2564,6 +2580,17 @@ class PubServer(BaseHTTPRequestHandler):
elif ('@' in searchStr or
('://' in searchStr and
hasUsersPath(searchStr))):
if searchStr.endswith(':') or \
searchStr.endswith(';') or \
searchStr.endswith('.'):
if callingDomain.endswith('.onion') and onionDomain:
actorStr = 'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and i2pDomain):
actorStr = 'http://' + i2pDomain + usersPath
self._redirect_headers(actorStr + '/search',
cookie, callingDomain)
self.server.POSTbusy = False
return
# profile search
nickname = getNicknameFromActor(actorStr)
if not self.server.session:
@ -2579,8 +2606,7 @@ class PubServer(BaseHTTPRequestHandler):
profilePathStr = path.replace('/searchhandle', '')
# are we already following the searched for handle?
if isFollowingActor(baseDir, nickname, domain,
searchStr):
if isFollowingActor(baseDir, nickname, domain, searchStr):
if not hasUsersPath(searchStr):
searchNickname = getNicknameFromActor(searchStr)
searchDomain, searchPort = \
@ -2840,6 +2866,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFilename = mediaFilenameBase + '.jpg'
if self.headers['Content-type'].endswith('gif'):
mediaFilename = mediaFilenameBase + '.gif'
if self.headers['Content-type'].endswith('svg+xml'):
mediaFilename = mediaFilenameBase + '.svg'
if self.headers['Content-type'].endswith('webp'):
mediaFilename = mediaFilenameBase + '.webp'
if self.headers['Content-type'].endswith('avif'):
@ -4196,6 +4224,21 @@ class PubServer(BaseHTTPRequestHandler):
setDonationUrl(actorJson, '')
actorChanged = True
# account moved to new address
movedTo = ''
if actorJson.get('movedTo'):
movedTo = actorJson['movedTo']
if fields.get('movedTo'):
if fields['movedTo'] != movedTo and \
'://' in fields['movedTo'] and \
'.' in fields['movedTo']:
actorJson['movedTo'] = movedTo
actorChanged = True
else:
if movedTo:
del actorJson['movedTo']
actorChanged = True
# change instance title
if fields.get('instanceTitle'):
currInstanceTitle = \
@ -5236,11 +5279,14 @@ class PubServer(BaseHTTPRequestHandler):
ssbAddress = None
emailAddress = None
lockedAccount = False
movedTo = ''
actorJson = getPersonFromCache(baseDir,
optionsActor,
self.server.personCache,
True)
if actorJson:
if actorJson.get('movedTo'):
movedTo = actorJson['movedTo']
lockedAccount = getLockedAccount(actorJson)
donateUrl = getDonationUrl(actorJson)
xmppAddress = getXmppAddress(actorJson)
@ -5271,7 +5317,8 @@ class PubServer(BaseHTTPRequestHandler):
emailAddress,
self.server.dormantMonths,
backToPath,
lockedAccount).encode('utf-8')
lockedAccount,
movedTo).encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
cookie, callingDomain)
@ -5351,6 +5398,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaImageType = 'webp'
elif emojiFilename.endswith('.avif'):
mediaImageType = 'avif'
elif emojiFilename.endswith('.svg'):
mediaImageType = 'svg+xml'
else:
mediaImageType = 'gif'
with open(emojiFilename, 'rb') as avFile:
@ -6996,6 +7045,10 @@ class PubServer(BaseHTTPRequestHandler):
# more social graph info
if not authorized:
pjo = postJsonObject
if not isPublicPost(pjo):
self._404()
self.server.GETbusy = False
return True
self._removePostInteractions(pjo)
if self._requestHTTP():
recentPostsCache = \
@ -7113,6 +7166,10 @@ class PubServer(BaseHTTPRequestHandler):
# graph info
if not authorized:
pjo = postJsonObject
if not isPublicPost(pjo):
self._404()
self.server.GETbusy = False
return True
self._removePostInteractions(pjo)
if self._requestHTTP():
@ -9293,6 +9350,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaFileType = 'webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'avif'
elif mediaFilename.endswith('.svg'):
mediaFileType = 'svg+xml'
else:
mediaFileType = 'gif'
with open(mediaFilename, 'rb') as avFile:
@ -9351,6 +9410,8 @@ class PubServer(BaseHTTPRequestHandler):
mediaImageType = 'gif'
elif avatarFile.endswith('.avif'):
mediaImageType = 'avif'
elif avatarFile.endswith('.svg'):
mediaImageType = 'svg+xml'
else:
mediaImageType = 'webp'
with open(avatarFilename, 'rb') as avFile:
@ -10277,6 +10338,7 @@ class PubServer(BaseHTTPRequestHandler):
# image on login screen or qrcode
if self.path == '/login.png' or \
self.path == '/login.gif' or \
self.path == '/login.svg' or \
self.path == '/login.webp' or \
self.path == '/login.avif' or \
self.path == '/login.jpeg' or \
@ -11877,6 +11939,7 @@ class PubServer(BaseHTTPRequestHandler):
filename.endswith('.jpg') or \
filename.endswith('.webp') or \
filename.endswith('.avif') or \
filename.endswith('.svg') or \
filename.endswith('.gif'):
postImageFilename = filename.replace('.temp', '')
print('Removing metadata from ' + postImageFilename)

View File

@ -6,523 +6,541 @@
<title>sport</title>
<description>billiard darts swim motorsport snooker marathon hockey diving baseball Millwall sailing athletics skating skiing sport football</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>events</title>
<description>neverforget award OONIbday waybackwednesday notifications throwbackthursday adventskalender live Day deepthoughts screenshotsaturday thursdaythoughts humanrightsday followfriday afediversechristmas wednesdaymotivation showerthoughts anarchymonday 100DaysToOffload ff holiday christmas week concert festival screenshottuesday dontstarve onthisday livestream sunday screenshotsunday liverpool adayinthelife day ccc InternationalCheetahDay interestingtimes christmaslights meetup</description>
<description>neverforget award OONIbday waybackwednesday notifications throwbackthursday adventskalender live Day deepthoughts thingaday screenshotsaturday thursdaythoughts beethoven250thbirthday humanrightsday followfriday afediversechristmas wednesdaymotivation showerthoughts beethoven anarchymonday 100DaysToOffload ff holiday christmas week concert festival FridayFolklore screenshottuesday dontstarve onthisday livestream BowieDay sunday screenshotsunday liverpool adayinthelife day ccc InternationalCheetahDay interestingtimes christmaslights meetup</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>gafam</title>
<description>zuckerberg caringissharing ads apple antitrust SpringerEnteignen GoogleDown AppleSearch bankruptBezos youtube ffs facebook interoperability amazon boycottinstagram amazonring googleplus degooglisation siri Facebook LeiharbeitAbschaffen advertising adtech fuckgoogle microsoft dtm twitter caffeine skype chrome hildebrandt youtubedl degoogled youtubers google sharingiscaring gis dt dotcoms deleteyoutube Instagram fascistbook FuckGoogle degoogle fuschia ungoogled ring affordances googledown gafam inspiring fuckoffgoogle deletefacebook fuckoffgoogleandco office365 instagram MatrixEffect playstore bigtech</description>
<description>zuckerberg caringissharing ads apple antitrust SpringerEnteignen ABoringDystopia GoogleDown AppleSearch bankruptBezos youtube ffs facebook interoperability amazon boycottinstagram amazonring Gafam googleplus degooglisation siri Facebook LeiharbeitAbschaffen advertising monopolies adtech fuckgoogle plottertwitter microsoft dtm twitter caffeine skype chrome hildebrandt youtubedl degoogled youtubers google sharingiscaring gis walledgarden dt dotcoms deleteyoutube Instagram fascistbook FuckGoogle degoogle fuschia appleiie ungoogled ring stopgoogle affordances googledown gafam inspiring fuckoffgoogle deletefacebook fuckoffgoogleandco office365 instagram MatrixEffect playstore bigtech whatsapp deleteamazon</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>activitypub</title>
<description>followerpower FederatedSocialMedia Fediverse activitypub activertypub pleroma losttoot PeerTube gofed fediblock lazyfedi federation instances fedilab pixiv mastotips mastodev mastotip friendica hiveway misskey siskin followers fediart Pixelfed contentwarnings pixelfed fediverseplaysjackbox fedidb block FediMemories Feditip Fediseminar onlyfedi socialcg monal tusky peertubers imagedescription feditips fedizens Mastodon following epicyon peertubeadmin mastomagic dev fediadmin pixeldev fosstodon instanceblock mastodonmonday isolategab fedireads PeertubeMastodonHost Bookwyrm federated socialhome fedivers MastodonMondays fediverse imagedescriptions mastoadmin smithereen mastodon fedi fediplay peertube lab mobilizon gemifedi</description>
<description>followerpower FederatedSocialMedia Fediverse activitypub activertypub pleroma losttoot PeerTube gofed fediblock lazyfedi federation instances fedilab pixiv mastotips mastodev mastotip friendica hiveway misskey siskin followers fediart blocking Pixelfed contentwarnings pixelfed fediverseplaysjackbox mapeocolaborativo fedidb block FediMemories Feditip Fediseminar onlyfedi socialcg monal tusky peertubers imagedescription feditips fedizens Mastodon following epicyon development peertubeadmin collaboration mastomagic dev fediadmin pixeldev fosstodon instanceblock mastodonmonday isolategab fedireads PeertubeMastodonHost Bookwyrm federated socialhome greenfediverse microblocks fedivers MastodonMondays fediverse imagedescriptions mastoadmin smithereen blabber mastodon fedi fediplay peertube developer lab mobilizon gemifedi</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>programming</title>
<description>Easer cpp report programming css objects Python FrancisBacon2020 mixers webdev gui release ada schutzstreifen rustlang ocaml program request_reaction uptronics solidarity hypocritcal profiles typescript forums vscode publiccode FreeSoftware vieprivée early adventofcode scripting warn spyware git solid trevornoah zinccoop tailwindcss raku fedidev c sourcecode publiekecode misc framaforms WendyLPatrick grep django gmic sackthelot gitportal relevance_P1Y kingparrot Leiharbeit programmer haskell Tarifvertrag unicode frgmntscnr github digitalmarketsact openrc tuskydev threema algorithms lisp forge pleaseshare HirsuteHippo resnetting fourtwenty libraries drivers freecode javascript fragment cpm code elisp patterns html terminal rust sauerkraut request spiritbomb r dramasystem go esbuild documentary golang clojurescript ruby contractpatch computers racket python indiedev kabelfernsehen alternatives OpenSource Scheibenwischer</description>
<description>Easer cpp report programming css objects Python FrancisBacon2020 mixers webdev gui release ada schutzstreifen rustlang ocaml program request_reaction uptronics hypocritcal profiles typescript forums vscode publiccode computerscience vieprivée early adventofcode cgit scripting warn spyware git solid trevornoah zinccoop tailwindcss raku fedidev c sourcecode publiekecode misc framaforms WendyLPatrick grep django gmic sackthelot gitportal gitlab relevance_P1Y kingparrot Leiharbeit programmer haskell OpenSourceHardware Tarifvertrag unicode frgmntscnr github digitalmarketsact freecodecamp openrc tuskydev threema algorithms lisp digitaldefenders forge pleaseshare HirsuteHippo resnetting fourtwenty libraries drivers freecode javascript fragment cpm code elisp patterns eq html terminal rust sauerkraut request spiritbomb r dramasystem go esbuild documentary golang clojurescript ruby contractpatch computers racket bugreport python indiedev kabelfernsehen alternatives OpenSource Scheibenwischer</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>nature</title>
<description>hiking wat StormBella morning trees light birds nature frogs sunrise coldwater inaturalist forest morningcrew australianwildlife capybara natur amphibians</description>
<description>hiking wat StormBella morning trees lichen light birds nature frogs sunrise moutains coldwater inaturalist forest morningcrew australianwildlife capybara enlightenment natur deforestation morningwalk amphibians</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>writing</title>
<description>blog tootfic authors poem magazine smallstories blogging smallpoems blogs interactivestorytelling WriteFreely storytelling goodreads creativewriting journal poetry</description>
<description>blog tootfic authors poem magazine smallstories blogging smallpoems writing blogs noblogo microfiction interactivestorytelling westernjournal quote WriteFreely storytelling goodreads creativewriting journal poetry</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>hardware</title>
<description>plugandplay PersonalComputer cyberdeck PineCUBE keyboards screenless modem analogcomputing TrueDelta keyboard ArmWorkstation daretocare printmaker cybredeck laptop solarpunk recycling lenovo fairelectronics fuse ibm 3dprinting MechcanicalKeyboards openhardware raspberrypi barcode pinetime pinebookpro PinebookPro 3dprint arm paperComputer amd openpower devopa thinkpad print electronic</description>
<description>plugandplay purism opennic PersonalComputer cyberdeck PineCUBE keyboards screenless pinebook modem analogcomputing TrueDelta keyboard ArmWorkstation daretocare laptops printmaker cybredeck computing laptop solarpunk recycling lenovo fairelectronics fuse ibm 3dprinting MechcanicalKeyboards hardware retrohardware openhardware raspberrypi barcode pinetime pinebookpro PinebookPro 3dprint arm paperComputer amd openpower devopa thinkpad raspberrypi4 print electronic</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>places</title>
<description>lapaz luanda asunción nouakchott conakry kyiv moscow saipan gibraltar dublin catalunya dannibleibt avarua hargeisa delhi niamey chișinău colombo brasília phnompenh mbabane belgrade belmopan pyongyang hannover ulaanbaatar oranjestad gaborone seattle ndjamena raw singapore kingedwardpoint abidjan nuuk pretoria papeete malé zagreb gitega abudhabi flyingfishcove castries georgetown hagåtña borikua basseterre hamburg kinshasa suva valparaíso athens roseau baku charlotteamalie antananarivo domi pristina santiago sukhumi berlin uptronicsberlin funafuti libreville hanoi philipsburg tehran banjul prague andorralavella daw yerevan portauprince dakar paramaribo tifariti capetown tirana klima ankara ipswich managua lisbon bishkek amsterdam portonovo santodomingo bangkok bucharest kathmandu aden madrid sanjuan vienna kingston kabul damascus stockholm douglas willemstad thehague panamacity beirut amman newdelhi tórshavn nouméa oslo alofi gustavia paris video cockburntown ottawa stepanakert portofspain fsberlin honiara asmara florida nicosia helsinki taipei tegucigalpa tokyo tashkent larochelle MadeInEU sarajevo algiers nairobi muscat monaco riyadh lusaka wellington bissau juba mariehamn majuro buenosaires ngerulmud dhaka guatemalacity washington vatican kuwaitcity londonboaters bern mexicocity bratislava bridgetown delhipolice tunis manila stanley matautu copenhagen barcelona lomé budapest ouagadougou mogadishu freetown victoria brazzaville portmoresby ashgabat kampala elaaiún vilnius bloemfontein sucre london marseille pagopago bradesestate oakland vaduz addis nürnberg naypyidaw CassetteNavigation khartoum baghdad bandar moroni lehavre portvila kingstown ChrisCrawford reykjavík manama accra windhoek nukualofa ciutatvella tbilisi canberra quito maputo cetinje putrajaya ramallah bogotá dodoma harare havana warsaw münster valletta localberlin ljubljana bamako kualalumpur podgorica rabat cotonou plymouth seoul Portland dushanbe bangui aotearoa westisland tskhinvali palikir caracas jamestown rome munich ass freestuffberlin sãotomé jakarta daressalaam sansalvador apia essex yaren cairo jerusalem brussels kigali southtarawa beijing minsk montevideo vientiane maseru hamilton doha tripoli celtic portlouis lima adamstown deventer abuja lilongwe nassau lobamba heathrow nyc strawberry montreal dili riga assembly lesbos monrovia nursultan gab sanjosé marigot islamabad malabo tallinn sahara thimphu yaoundé praia bujumbura sofia skopje</description>
<description>lapaz luanda asunción nouakchott conakry kyiv moscow saipan gibraltar dublin KlimaGerechtigkeit catalunya dannibleibt avarua hargeisa delhi niamey chișinău colombo brasília phnompenh mbabane danni belgrade belmopan pyongyang hannover ulaanbaatar oranjestad gaborone seattle ndjamena raw singapore kingedwardpoint abidjan nuuk pretoria papeete malé zagreb gitega abudhabi flyingfishcove castries georgetown hagåtña cassette borikua basseterre hamburg kinshasa suva valparaíso athens roseau baku charlotteamalie antananarivo domi pristina videocalls santiago sukhumi berlin uptronicsberlin funafuti libreville stopchasseacourre puertorico hanoi philipsburg tehran banjul prague andorralavella daw yerevan portauprince dakar paramaribo tifariti capetown tirana klima ankara ipswich managua lisbon bishkek amsterdam portonovo santodomingo bangkok bucharest kathmandu aden madrid sanjuan vienna kingston kabul damascus stockholm douglas willemstad thehague panamacity RassismusTötet beirut amman newdelhi tórshavn nouméa oslo alofi gustavia paris video cockburntown ottawa classical stepanakert portofspain klimakrise fsberlin honiara asmara florida nicosia helsinki taipei tegucigalpa tokyo tashkent larochelle MadeInEU sarajevo algiers KlimaKrise nairobi muscat monaco riyadh lusaka wellington bissau juba mariehamn klimaatcrisis majuro buenosaires ngerulmud dhaka guatemalacity washington vatican kuwaitcity londonboaters bern mexicocity bratislava bridgetown delhipolice tunis manila stanley matautu copenhagen barcelona lomé budapest ouagadougou mogadishu freetown victoria brazzaville portmoresby ashgabat kampala elaaiún vilnius bloemfontein sucre london marseille pagopago bradesestate oakland vaduz addis nürnberg naypyidaw CassetteNavigation khartoum baghdad bandar moroni lehavre portvila kingstown ChrisCrawford reykjavík manama accra windhoek nukualofa ciutatvella tbilisi canberra quito maputo cetinje putrajaya ramallah bogotá dodoma harare havana warsaw münster valletta localberlin ljubljana bamako kualalumpur podgorica rabat cotonou plymouth seoul Portland dushanbe bangui aotearoa westisland tskhinvali palikir caracas jamestown rome munich ass freestuffberlin sãotomé jakarta daressalaam sansalvador apia essex yaren cairo jerusalem brussels kigali southtarawa beijing minsk montevideo vientiane maseru hamilton doha tripoli celtic portlouis lima adamstown deventer weimar abuja lilongwe nassau lobamba heathrow nyc strawberry montreal dili riga assembly lesbos monrovia nursultan gab sanjosé klimaatrechtvaardigheid marigot islamabad malabo tallinn sahara thimphu yaoundé praia bujumbura washingtondc sofia skopje</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>music</title>
<description>musicprodution punk ourbeats indiemusic streetpunk bandcamp musicians jamendo ipod skinheadmusic rap mp3 indie Music EnvoieStopHashtagAu81212 thecure vaporwave dubstep synthwave oi rave freemusic nowplaying hiphop experimentalmusic fedimusic soundcloud frankiegoestohollywood dj newwave dorkwave producing musicproduction funkwhale retrosynth NowPlaying libremusicproduction MusicAdvent coinkydink arianagrande synth music darkwave metal fediversemusic cyberpunkmusic BandcampFriday</description>
<description>musicprodution punk ourbeats indiemusic streetpunk bikepunks bandcamp musicians jamendo ipod skinheadmusic rap mp3 indie Music EnvoieStopHashtagAu81212 thecure vaporwave dubstep synthwave bootstrap oi rave freemusic nowplaying hiphop experimentalmusic spotify liberapay fedimusic musicbrainz soundcloud frankiegoestohollywood ccmusic typographie dj newwave dorkwave producing musicproduction lastfm funkwhale punkwear retrosynth NowPlaying libremusicproduction MusicAdvent coinkydink arianagrande synth music np darkwave mastomusic grapheneos metal fediversemusic cyberpunkmusic BandcampFriday</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>politics</title>
<description>TakeOurPowerBack cia community wageslavery immigration dissent liberation fascism techtuesday skyofmywindow freedomofspeech rojava humanrights leftists Socialism ukpol FreeKeithLamar copwatch capitalismkills petition BorisJohnson freedom abolitionnow anarchism DefundThePolice technews smalltech oilwars kommunismus bjp ThirdRunway hierarchy election sky_of_my_window generalstrike antipolitics digitalfreedom mayday hatespeech fascists lowtech a11y burntheprisons cyberlaw peerproduction corporations iww commons corporatewatch wageslave uspol frontex communism RemoveThePolice Immigration neoliberalism socialecology MutualAid capitalism technology prisons wealth conspiracytheories corporatecrime communist KeirStarmer anarchismus politics inclusivity brightgreen anarchisme DominicCummings nzpol Bookchin ClemencyNow brexit totalitarianism privatisation TyskySour Labour freethemall green BAME decolonizeyourmind privilege AbolishPrisonsAbolishPolice surfaceworldblows ecofascism SocietalChange facialrecognition corruption anarchy propaganda decolonization digitalrights feminism polizei neo xp 18Source radicaltech redandanarchistskinheads PritiPatel latestagecapitalism racist MexicanRevolution elections RussellMaroonShoatz white prisoners warrants policebrutality borisjohnson Anarchist press mutuality whitehouse freedomofexpression censorship decolonize emmet decenterwhiteness Biden ChineseAppBan cooperative modi law deathtoamerica manipulation firetotheprisons britpol Capitalism surveillancecapitalism leftist Revolution ukpolitics JeremyCorbyn blacklivesmatter FreeAlabamaMovement rentstrike dsa techno migration mutualaid multipleexposure AbolishPrison fascist socialcoop anarchistprisoners polizeiproblem uselection IDPol Slavetrade met ourstreets refugees acab freewestpapua tech</description>
<description>TakeOurPowerBack trump cia community wageslavery immigration dissent liberation fascism techtuesday skyofmywindow techthursday freedomofspeech fascisme rojava humanrights leftists Socialism ukpol FreeKeithLamar Antifascisme copwatch capitalismkills petition BorisJohnson freedom abolitionnow anarchism DefundThePolice technews polizeigewalt smalltech antifascists oilwars kommunismus bjp ThirdRunway hierarchy election republicans solidarity techwear sky_of_my_window generalstrike antipolitics digitalfreedom mayday hatespeech fascists lowtech a11y burntheprisons cyberlaw peerproduction corporations iww freeassange commons corporatewatch wageslave uspol frontex communism RemoveThePolice Immigration neoliberalism socialecology MutualAid capitalism technology prisons wealth conspiracytheories corporatecrime communist KeirStarmer taoteching anarchismus politics inclusivity HeroesResist brightgreen anarchisme DominicCummings nzpol Bookchin ClemencyNow brexit totalitarianism privatisation TyskySour Labour freethemall green BAME decolonizeyourmind privilege antikapitalisme AbolishPrisonsAbolishPolice surfaceworldblows ecofascism SocietalChange facialrecognition corruption anarchy Feminism propaganda endsars decolonization digitalrights feminism polizei neo xp 18Source censorshipBook radicaltech conspiracy redandanarchistskinheads radicaldemocracy PritiPatel latestagecapitalism racist MexicanRevolution elections RussellMaroonShoatz commonspub white prisoners warrants policebrutality borisjohnson Anarchist press mutuality whitehouse freedomofexpression censorship decolonize emmet decenterwhiteness Biden ChineseAppBan cooperative modi law deathtoamerica manipulation firetotheprisons britpol Capitalism surveillancecapitalism leftist Revolution ukpolitics JeremyCorbyn blacklivesmatter FreeAlabamaMovement rentstrike dsa techno migration mutualaid multipleexposure AbolishPrison anarchists fascist socialcoop anarchistprisoners polizeiproblem uselection IDPol Antifa Slavetrade met ourstreets freespeech refugees acab ecology SurveillanceCapitalism freewestpapua sunnytech tech</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>food</title>
<description>vitamind cake margarine dessert salsa caviar theexpanse cookery pietons food skillet liquor milk bolognese recipe foodporn yeast plate waffle biscuit glaze omelette filet pastry wine hamburger juice Amazfish sourdough nuts gras toast broth batter foodie ketchup seasoning mayo soup pan voc imateapot teamcapy mayonnaise vegan dish avocado spice bakery cooking yogurt spotify crumble cider butter cook pottery cobbler steak pizza soda fedikitchen aroma oil flour cream nutella pie cuisine tartar tea marinade mushroom entree bread salad beans fresh syrup fermentation mushrooms cookie curd soysauce pudding beer baking fish foodwaste wheat pot TeamFerment stew chocolate paste wok recipes olive burger candy kitchen coffee bagel taste meat noodle raclette caramel rice eggs grill poutine lard croissant pasta foods cheese oregano drink muffin foie sauce soy vore cocoa sandwich mousse chili vinegar</description>
<description>vitamind cake margarine zwartepiet dessert salsa caviar theexpanse BellaSpielt cookery pietons food skillet spiel liquor milk bolognese recipe foodporn yeast drinking plate waffle biscuit glaze omelette filet pastry wine hamburger juice Amazfish sourdough nuts gras toast broth batter foodie spiele ketchup divoc seasoning mayo soup pan voc imateapot teamcapy mayonnaise vegan dish avocado spice bakery cooking yogurt crumble cider butter mastokitchen cook pottery mastocook cobbler steak pizza soda fedikitchen aroma oil angelfish flour cream nutella pie cuisine tartar tea marinade mushroom entree lfi bread salad beans fresh syrup fermentation mushrooms cookie wordstoliveby curd soysauce pudding beer baking fish foodwaste wheat pot TeamFerment stew chocolate paste wok recipes olive burger candy kitchen coffee bagel taste SpieleWinter2020 meat noodle raclette caramel rice eggs grill poutine lard croissant pasta foods cheese oregano drink muffin foie sauce soy vore pandemie cocoa sandwich mousse chili vinegar</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>farming</title>
<description>johndeere</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>countries</title>
<description>romania burma lithuania solomon chile Instanz fiji tajikistan benin paraguay eeuu senegal ukraine italy brunei nicaragua guyana Pflanzenbestimmung euphoria zambia iceland morocco netherlands swaziland bosnian solo suriname elsalvador russia samoa european czech belarus hayabusa2 kyrgyzstan uk abuse translation sanmarino catalonia panama japan buyused venezuela gambia freeNukem kuwait barbados papua greece switzerland uae nigeria usa angola honduras djibouti laos sierraleone cambodia ych vietnam neofeud seychelles marshall kazakhstan estonia tonga stlucia burundi bangladesh egypt mali congo us jordan speedrun grenada israel algeria ghana bosnia russian industrial eritrea bhutan hungary saudi slovenia tig bahamas australia kiribati togo koreanorth poland malawi capeverde run armenia american hautrauswasgeht bahrain mozambique beleuchtung southsudan syria micronesia maldives iran indigenous sweden ethiopia cuba liberia canada burkina somalia Chile scotland aur vaticancity easttimor austria turkey yemen Bolivia denmark trunk madagascar finland philippines ivorycoast haiti ecuador Portugal azerbaijan gasuk spain albania afghanistan europe mauritania dominica thailand belize westpapuauprising macedonia montenegro qatar mongolia costarica boatingeurope birdsofkenya latvia uzbekistan kabelaufklärung ireland iraq malaysia mexico mauritius oman chad nz georgia zimbabwe france serbia lesotho oddmuse tunisia argentina cameroon namibia sudan indonesia colombia tuvalu britainology beckychambers turkmenistan tanzania germany neuhier norway comoros auteursrecht guatemala Thailand kosovo andorra wales servus pakistan belgium china antigua life koreasouth newzealand einzelfall rwanda luxembourg libya italyisntreal nauru Anarchismus moldova palau taiwan kenya trinidad eu botswana CuriosidadesVariadas jamaica vanuatu cyprus aminus3 malta niger westpapua busse unitedstates myanmar saintvincent guinea nepal peru uganda uruguay india lebanon neurodiversity southafrica croatia europeanunion bolivia chinese dominican srilanka bulgaria slovakia speedrunning gabon psychedelicart stkitts liechtenstein brazil shutdowncanada</description>
<description>romania burma lithuania solomon chile Instanz fiji tajikistan benin paraguay eeuu senegal ukraine italy brunei nicaragua guyana Pflanzenbestimmung euphoria zambia iceland morocco netherlands swaziland bosnian solo suriname winningatlife elsalvador russia samoa european czech belarus hayabusa2 kyrgyzstan uk abuse translation sanmarino catalonia panama japan buyused venezuela gambia freeNukem kuwait barbados papua greece switzerland uae nigeria usa angola honduras djibouti laos sierraleone cambodia ych vietnam neofeud seychelles marshall kazakhstan estonia tonga stlucia burundi bangladesh egypt mali congo us jordan speedrun grenada israel psychic algeria ghana bosnia translations russian industrial eritrea bhutan ios hungary saudi slovenia tig czechosvlovakia bahamas australia kiribati togo koreanorth poland Überbevölkerung malawi capeverde run armenia american hautrauswasgeht bahrain mozambique beleuchtung southsudan syria micronesia maldives iran indigenous sweden ethiopia cuba liberia canada burkina somalia Chile scotland aur vaticancity easttimor austria turkey yemen Bolivia denmark trunk madagascar finland philippines ivorycoast haiti ecuador Portugal azerbaijan gasuk spain albania afghanistan europe mauritania dominica thailand belize westpapuauprising macedonia montenegro qatar mongolia costarica boatingeurope birdsofkenya latvia uzbekistan kabelaufklärung ireland iraq malaysia mexico mauritius oman chad nz georgia zimbabwe france serbia lesotho oddmuse tunisia argentina czechia cameroon namibia sudan indonesia colombia tuvalu britainology beckychambers turkmenistan tanzania germany neuhier norway comoros auteursrecht guatemala Thailand kosovo andorra wales servus pakistan belgium china antigua life koreasouth newzealand einzelfall rwanda luxembourg libya italyisntreal nauru Anarchismus moldova palau taiwan kenya trinidad eu botswana CuriosidadesVariadas jamaica vanuatu cyprus aminus3 malta polychromatic niger westpapua busse unitedstates myanmar saintvincent guinea nepal peru uganda uruguay india lebanon neurodiversity southafrica croatia europeanunion bolivia chinese dominican srilanka bulgaria slovakia speedrunning gabon psychedelicart stkitts liechtenstein brazil shutdowncanada</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>cycling</title>
<description>bicycle cycling bike thingsonbikes Snowbike cyclist</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>phones</title>
<description>mobileapp pine fdroid plasmamobile android phones smartphone iOS14 linuxphones QWERTYphones BriarProject librem5 pinephone mobile fairphone ubuntutouch Android ubports osmand vodafone iphones postmarketos iOS microg mobileKüfA</description>
<description>mobileapp pine fdroid plasmamobile android linuxmobile phones smartphone iOS14 linuxphones mobilelinux QWERTYphones siskinim Smartphones plasma phosh BriarProject librem5 osm pinetab pinephone mobile pine64 fairphone ubuntutouch Android ubports osmand vodafone linuxonmobile iphones postmarketos iOS microg mobileKüfA</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>security</title>
<description>signalboost encrypt letsencrypt autoritäreretatismus omemo password cryptography solarwinds communityalgorithmictrust infosec gchq IHaveSomethingToHide IronySec cryptowars supplychainattacks UseAMaskUseTor cyberattack security tor e2e bruceschneier vpn openssh openssl e2ee ed25519 encryption ssh misshaialert crypto giftofencryption malware opsec keepass torsocks nsa protonvpn yubikey nitrokey openpgp castor9 gpgtools gpg fotopiastory cybersecurity CryptoWars signal noscript trust cryptocurrency cryptomator openvpn datasecurity encryptiost securitynow tracking cloudflare</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>science</title>
<description>math womeninstem supercollider nextgeneration dna archaeologist dawkins graphTheory psychology biology generation gene paleontology</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>pandemic</title>
<description>covid19 corona Coronavirus CoronaWarnApp facemasks vaccines vaccine pandemic contacttracing tier4 covid coronavirus masks virus Lockdown rna codid19 COVID19 YesWeWork ContactTracing COVID</description>
<description>covid19 corona psmeandmywholefamilycaughtcovidfromwork Coronavirus CoronaWarnApp facemasks vaccines vaccine pandemic contacttracing tier4 covid coronavirus masks virus Lockdown rna codid19 COVID19 YesWeWork ContactTracing COVID</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>software</title>
<description>app freedombox windows libre nginx Framasoft invidious drm publicdomain kubernetes fossmendations jami FuckOffZoom quicksy free docker freesoftware gimp foss matrix thefreethoughtproject nextcloud wechat openscad TabOrder ikiwiki Linux rocketchat outreachy lyft nitter discord opensource diaspora yunohost littlebigdetails cabal conferencing libreboot accessibility devops owncast emacs freiesoftware email chatapps floss plugins deltachat application uifail FOSS bittorrent vlc zoom tiling gpl FriendofGNOME usability obnam snap cryptpad software OwnStream zrythm mumble grsync telegram containers blockchain irssi mutt design gameoftrees backup rotonde GNU thunderbird sysadmin apps licensing screenreaders profanity ffmpeg lemmy OSM distributedledger win10 element nativeApp jitsi wordpress ux rsync libreoffice dino plugin OCUPACAOCARLOSMARIGHELLA whatsapp openoffice</description>
<description>app freedombox windows libre nginx Framasoft invidious drm publicdomain kubernetes fossmendations jami FuckOffZoom quicksy whiteboard free docker freesoftware gimp foss matrix thefreethoughtproject filesystems nextcloud wechat openscad TabOrder ikiwiki Linux FreeSoftware rocketchat outreachy lyft nitter discord opensource diaspora yunohost littlebigdetails cabal conferencing libreboot accessibility devops owncast emacs freiesoftware writefreely email chatapps HappyNewYear floss plugins deltachat application uifail FOSS bittorrent vlc zoom tiling gpl FriendofGNOME usability obnam snap cryptpad software OwnStream upstream slack zrythm gnu mumble grsync freecad telegram containers blockchain irssi mcclim mutt design gameoftrees backup rotonde freetube GNU thunderbird sysadmin parler apps chat licensing screenreaders LINMOBapps profanity Tankklappe ffmpeg fossandcrafts lemmy OSM agpl distributedledger ghostscript win10 element chatty nativeApp jitsi wordpress ux rsync libreoffice dino plugin OCUPACAOCARLOSMARIGHELLA openoffice</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>security</title>
<description>encrypt omemo password cryptography solarwinds communityalgorithmictrust infosec gchq IHaveSomethingToHide IronySec cryptowars supplychainattacks UseAMaskUseTor cyberattack security tor e2e bruceschneier vpn openssh openssl e2ee ed25519 encryption ssh misshaialert crypto giftofencryption malware opsec keepass torsocks nsa protonvpn yubikey nitrokey openpgp castor9 gpgtools gpg fotopiastory cybersecurity CryptoWars signal noscript np trust cryptocurrency cryptomator openvpn datasecurity tracking cloudflare</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>gardening</title>
<description>sporespondence blockade inde independant deno cabbage bundeswehr onions bordeaux datenschleuder florespondence garden thyme DailyFlowers permaculture papuamerdeka flowers gardening de devilslettuce fahrräder golden</description>
<description>sporespondence blockade inde independant deno cabbage bundeswehr onions bordeaux datenschleuder florespondence garden thyme DailyFlowers acu permaculture papuamerdeka flowers gardening de devilslettuce fahrräder golden</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>conferences</title>
<description>debconf talk fossdem FreedomBoxSummit apconf2020 schmoocon summit confidenceTricks minidebconf rc3worldleaks emacsconf ox defcon flossevent conf rC3 rC3World conference flossconf apconf rC3one C3 config</description>
<description>debconf talk fossdem FreedomBoxSummit apconf2020 schmoocon summit confidenceTricks minidebconf rc3worldleaks emacsconf MCH2021 ox defcon flossevent conf rC3 rC3World conference flossconf apconf rC3one C3 config</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>cats</title>
<description>Cat dailycatpic dxp DailyCatVid katze CatsOfMastodon Leopard catbellies LapCats</description>
<description>Cat dailycatpic dxp DailyCatVid dx katze CatsOfMastodon Leopard catbellies LapCats</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>employment</title>
<description>InterviewQuestions mywork reproductivework bullshitjobs antiwork kreaturworks worklog hire hirefedi carework nowhiring work letthenetwork jobs</description>
<description>InterviewQuestions mywork reproductivework bullshitjobs antiwork kreaturworks worklog hire hirefedi carework nowhiring work letthenetwork jobs sexworker</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>radio</title>
<description>cbradio hamr whydopeopledoshitlikethis amateurradio radiohost oshw localization vantascape vantaradio ca radio healthcare listening hamradio FreeAllPoliticalPrisoners card10 radiobroadcasting 3dcad radioshow local noshame osh hackerpublicradio california listeningtonow radiobroadcast spazradio anonradio io</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>years</title>
<description>newyearsresolutions Year2020 year 1yrago newyear happynewyear 5yrsago newyearseve</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>linux</title>
<description>osdev opensuse linuxisnotanos elementaryos cli kde Debian11 slackware mobian openwrt distros nixos nix DebianBullseye shareyourdesktop wireguard linuxaudio nixpkgs gtk debian trisquel gnome linuxposting showyourdesktop windowmanager desktop ubuntu xubuntu unix fedora centos gentoo usergroup systemd linuxgaming Debian distro destinationlinux qubesos i3wm haiku linuxisnotaplatform linux EMMS netbsd termux btrfs reproduciblebuilds artix gtk4 archlinux rhel debianinstaller linuxisajoke</description>
<description>osdev opensuse linuxisnotanos elementaryos cli kde Debian11 slackware mobian openwrt distros nixos nix DebianBullseye shareyourdesktop wireguard linuxaudio nixpkgs gtk debian trisquel gnome linuxposting showyourdesktop windowmanager desktop ubuntu gnulinux justlinuxthings xubuntu unix fedora centos gentoo usergroup systemd linuxgaming Debian distro destinationlinux gtk3 qubesos i3wm kubuntu haiku linuxisnotaplatform linux EMMS netbsd termux btrfs reproduciblebuilds artix gtk4 archlinuxarm archlinux rhel debianinstaller linuxisajoke</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>photos</title>
<description>nikon photography photo photogrpahy tokyocameraclub photos photoshop camera myphoto picture streetphotography</description>
<description>nikon 90mm photography photo photogrpahy tokyocameraclub photos photoshop camera cameras myphoto picture streetphotography</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>crafts</title>
<description>topic_imadethis textile upholstery dust3d hackers hackerspaces sanding sundiy knitting hack biohacking wip jewelry diy upcycling woodworking origami makers quilting hacker quilt 3dmodel woodwork ceramics embroidery</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>pets</title>
<description>catpics catofmastodon mastodogs catbehaviour Coolcats dogsofmastodon gentrification cats kittens pet dog caturday catsofmastodon cute catstodon dogs mastocats cat catcontent</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>news</title>
<description>news Wikileaks newsletter rt bbc doubledownnews journalism SkyNews</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>games</title>
<description>minecraft tetris99 TerraNil runequest boardgames computergames gamedesign chess nintendoswitch mud indiegame game 0ad ttrpg gamedev guildwars2 TetrisGore gaming nintendo Gamesphere rpg tetris dosgaming DnD cyber2077 cyberpunk2077 FreeNukum neopets minetest guildwars dnd games</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>climate</title>
<description>energy renewables clouds renewableenergy amp climateemergency climate windenergy coal globalwarming climatechange weather climatecamp windpower science fossilfuels sky climatescience climatecrisis</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>internet</title>
<description>i2p spam firefox redecentralize wikipedia rtmp decentralization decentralize w3c torrent data sitejs internetarchaeology WordPress self contentmoderation distributed router dataretention selfhosting communityhosting icann discourse PeerToPeer dns openstandards nojs oauth hypercore CDNsAreEvil protonmail standards yourdataisyourdata internetfreedom gemini webui SmallWeb xmpp semanticweb socialnetwork content ntp socialnetworks proton icmp videocast jabber decentralized wiki ssb darknet cookies darkweb netcat Reddit server browser cloudy p2p social antisocial www ilovewikipedia web WebsiteStatus twitch 9front theserverroom socialmedia domain rss ipns mozilla voicemail mail i2pd ipfs internetradio browsers decentralizeit netscape openculture cyberspace offthegrid cloud internet decentralisation internetarchive js dark openweb onlineharms dot ftp internetshutdowns fixtheweb socialweb</description>
<description>immersiveweb dotcons i2p spam firefox redecentralize decentral wikipedia rtmp decentralization inclusiónsocial decentralize w3c torrent data sitejs publicserviceinternet internetarchaeology WordPress darkages self contentmoderation distributed router dataretention bigdata selfhosting communityhosting icann hosting discourse weblate PeerToPeer dns openstandards nojs oauth hypercore CDNsAreEvil protonmail standards yourdataisyourdata internetfreedom gemini webui SmallWeb distributedcoop xmpp semanticweb socialnetwork selfie content domains ntp socialnetworks Meme proton disco icmp videocast jabber webbrowsers decentralized wiki ssb darknet cookies darkweb netcat darktable Reddit server browser cloudy IPFS p2p social antisocial www ilovewikipedia web WebsiteStatus netshutdowns twitch 9front theserverroom socialmedia domain OpenStreetMap filesharing rss ipns mozilla voicemail mail i2pd ipfs internetradio browsers decentralizeit netscape openculture cyberspace messaging offthegrid cloud internet decentralisation serverMeddling internetarchive godot js dark openweb onlineharms dot thepiratebay ftp internetshutdowns fixtheweb socialweb mozillahubs</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>crafts</title>
<description>topic_imadethis hackerexchange textile upholstery hackgregator dust3d hackers hackerspaces sanding sundiy knitting hack biohacking wip jewelry diy upcycling woodworking origami makers quilting hacker quilt 3dmodel woodwork ceramics embroidery</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>pets</title>
<description>catpics catofmastodon reEducationCamp mastodogs catbehaviour Coolcats dogsofmastodon gentrification fostercats cats kittens pet dog caturday catsofmastodon cute catstodon dogs mastocats notpixiethecat londoninnercitykitties cat catcontent</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>art</title>
<description>paperart Linke water urban glassart artvsartist2020 abstract bsd earthship dccomics circuitsculpture watercolor memes autisticartist barrigòtic art open krita urbanart queerart deviantart adultcolouring collage jordanlynngribbleart openai harmreductionart openmoko wallpaper agriculture streetart coverart stickers fiberart MastoArt particl ParticlV3 culture opencl fiberarts polArt ink painting opencoop digitalart comic artwork openbsd mandala xkcd comics santa mastoart illustration artopencall gnuimagemanipulationprogram os wireart cartoon webcomic furryart DisabledArtist openstreetmap sticker artbreeder arttherapy TattoosOfTheFediverse artvsartist abstractart sculpture artist meme cultureshipnames concretepoetry artwithopensource opencallforartists commissionsopen peppertop visionaryart blackartist zines zine furry genart pixelart alisajart WaterDrinkers opencollective openrailwaymap JuliaHartleyBrewer artistsOfMastodon</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>news</title>
<description>news Wikileaks newsletter newsflash rt bbc doubledownnews journalism SkyNews</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>games</title>
<description>minecraft tetris99 TerraNil runequest boardgames computergames fucknintendo gameassets FediDesign gamedesign chess nintendoswitch mud indiegame game 0ad gameart opengameart sign ttrpg gamedev guildwars2 TetrisGore gaming gameing nintendo Gamesphere rpg tetris dosgaming DnD cyber2077 tarot cyberpunk2077 gamesforcats FreeNukum supermariomaker2 neopets minetest guildwars dnd games</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>legal</title>
<description>eek hfgkarlsruhe amro SpreekJeUitBekenKleur GameSphere OnlineHarmsBill laipower gdpr intros Anticritique learning energyflow rms digitalservicesact geekproblem dmca</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>climate</title>
<description>energy renewables clouds renewableenergy amp climateemergency climate windenergy coal sciencefiction skypack globalwarming climatechange weather climatecamp windpower pollution science fossilfuels sky climatescience climateaction climatecrisis</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>retro</title>
<description>A500 atarist commodore teletext floppy 8bit atari trs80 floppydisk retrocomputing C64 plan9 80s microcomputing omm retrogaming z80 8bitdo retro commissions amiga bbcmicro microcomputer bbsing</description>
<description>A500 atarist commodore teletext floppy 8bit atari trs80 floppydisk retrocomputing C64 plan9 80s microcomputing omm retrogaming z80 8bitdo retro retropie commissions amiga bbcmicro microcomputer bbsing</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>indymedia</title>
<description>visionontv globleIMC indymediaback pga indymedia hs2IMC indymediaIMC network roadsIMC omn tv roadstonowhereIMC UKIMC 4opens openmedianetwork</description>
<description>visionontv tredtionalmedia globleIMC indymediaback pga indymedia hs2IMC indymediaIMC network roadsIMC omn tv roadstonowhereIMC UKIMC 4opens openmedianetwork</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>media</title>
<description>livestreaming mainstreaming stream streaming weAreAllCrazy maiabeyrouti submedia theatlantic traditionalmedia videos railroads taina ai realmedia media</description>
<description>livestreaming mainstreaming stream trad streaming weAreAllCrazy maiabeyrouti sustainability diymedia submedia theatlantic traditionalmedia videos wikimedia railroads taina ai realmedia media independentmedia</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>activism</title>
<description>protestor grassroot g20 riseup sflc DanniVive reuse fsfe softwarefreedom ann activist xr directaction eff openrightsgroup protest JeffreySDukes actiondirecte kroymann HS2 ngo MarcWittmann fsf StopHS2 grassroots BLM changeisinyourhands conservancy JefferySaunders Kolektiva XR freeolabini announcement isolateByoblu annieleonard</description>
<description>protestor grassroot FreeLibreOpen g20 bekannt riseup sflc DanniVive reuse fsfe softwarefreedom ann activist xr SustainableUserFreedom directaction eff change openrightsgroup protest JeffreySDukes actiondirecte kroymann HS2 ngo MarcWittmann fsf StopHS2 grassroots antireport ClimateJustice BLM changeisinyourhands conservancy JefferySaunders Kolektiva XR freeolabini announcement isolateByoblu annieleonard</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>questions</title>
<description>askmastodon askfedi question askmasto askfediverse ask askfosstodon</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>birds</title>
<description>RainbowBeeEater bird</description>
<description>RainbowBeeEater pigeonlover bird</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>ethics</title>
<description>digitalethics ethics ethicallicense ethical</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>disability</title>
<description>ableism disabled</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>economics</title>
<description>bitcoin theWorkshop feministeconomics WealthConcentration valuesovereignty funding value shop crowdfund startups HenryGeorge crowdfunding limitstogrowth micropatronage monetize smallbusiness GitPay gdp limits</description>
<description>bitcoin theWorkshop feministeconomics WealthConcentration disabilitycrowdfund coops valuesovereignty funding platformcoop workercoops economics value shop crowdfund RIPpla startups HenryGeorge crowdfunding limitstogrowth micropatronage lgbtcrowdfund monetize smallbusiness pla GitPay gdp coop smallbusinesses infoshop limits</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>art</title>
<description>Linke urban glassart artvsartist2020 watercolor autisticartist barrigòtic art open krita urbanart queerart deviantart adultcolouring collage harmreductionart wallpaper streetart coverart fiberart MastoArt culture polArt ink painting opencoop digitalart comic artwork openbsd mandala xkcd comics santa mastoart illustration artopencall gnuimagemanipulationprogram os wireart cartoon webcomic furryart sticker artbreeder arttherapy TattoosOfTheFediverse artvsartist sculpture artist meme cultureshipnames concretepoetry artwithopensource opencallforartists commissionsopen peppertop blackartist zines zine furry opencollective JuliaHartleyBrewer artistsOfMastodon</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>podcasts</title>
<description>podcasting IntergalacticWasabiHour podcast tilde til tilderadio podcasts tildeverse smallisbeautiful tilvids</description>
<description>podcasting IntergalacticWasabiHour podcast tilde til tilderadio tildes podcasts tildeverse smallisbeautiful fertilizers tilvids</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>years</title>
<description>Year2020 year 1yrago 5yrsago</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>culture</title>
<description>etiquette</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>funding</title>
<description>donate disabledcrowdfund fundraiser patreon</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>identity</title>
<description>boomer</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>political</title>
<description>copservation linguisticProgramming</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>fashion</title>
<description>brasil fashionistas fashionesta bras fashion socks patches</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>month</title>
<description>april july march october november august june december september may feburary january month</description>
<description>april july march chapril october november august june december september may feburary january month</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>funding</title>
<description>disabledcrowdfund patreon</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>books</title>
<description>justhollythings earthsea ebooks book amreading bookwyrm bookreview theLibrary wayfarers books ebook epub cookbook</description>
<description>justhollythings earthsea ebooks book amreading failbook bookwyrm bookreview theLibrary wayfarers fakebook books bookreviews ebook epub cookbook</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>comedy</title>
<description>laugh humour satire irony standup funny humor</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>techbros</title>
<description>einfachredeneben hackernews red reddit</description>
<description>einfachredeneben redhat hackernews red reddit</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>health</title>
<description>medical burnout cannabis medicine treatment EmotionalFirstAid maryjane autistic health meds marijuana</description>
<description>medical burnout cannabis medicine treatment EmotionalFirstAid maryjane autistic neurodivergent health meds marijuana mentalhealth</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>facts</title>
<description>funfact didyouknow lifehack</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>ai</title>
<description>machinelearning</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>seasons</title>
<description>spring autumn winter summer solstice wintersolstice</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>gender</title>
<description>transwomen transcrowdfund womensart female nonbinary trans transphobia women estradiol queer genderQuiz woman transrights</description>
<description>transwomen transcrowdfund womensart female nonbinary trans transpositivity transphobia women estradiol queer genderQuiz genderqueerpositivity woman transrights</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>philosophy</title>
<description>minimalism maximalist maximalism stoic postmodernism minimalist</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>history</title>
<description>history anarchisthistory</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>fiction</title>
<description>cyberpunk thehobbit fiction microfiction genrefiction</description>
<description>cyberpunk thehobbit fiction genrefiction</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>legal</title>
<description>hfgkarlsruhe amro GameSphere OnlineHarmsBill laipower gdpr intros Anticritique learning energyflow digitalservicesact geekproblem dmca</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>introductions</title>
<description>newhere firsttoot recommends Introduction Introductions introduction intro introductions</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>audio</title>
<description>audioproduction audiofeedback audio</description>
<description>audioproduction audi audiofeedback audio</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>bots</title>
<description>bot</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>scifi</title>
<description>startrek starwars babylon5</description>
<description>startrekdiscovery startrek starwars babylon5</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>bots</title>
<description>bot humanrobotinteraction</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>religion</title>
<description>neopagan pagan catholic</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>help</title>
<description>mastohelp helpful help</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>obituaries</title>
<description>rip</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>astronomy</title>
<description>amateurastronomy astronomy space jupiter BackYardAstronomy moon saturn milkyway</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>photography</title>
<description>landscapephotography</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>privacy</title>
<description>privacypolicy surveillancetech privacymatters surveillance dataprivacy privacy WhatsappPrivacy</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>moderation</title>
<description>fedblock</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>languages</title>
<description>lojban gaelic</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>environment</title>
<description>s climatechaos</description>
<link/>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>election</title>
<description>voted vote</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>#music</title>
<description>trance</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>facts</title>
<description>didyouknow lifehack</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>radio</title>
<description>radiohost vantascape vantaradio ca radio healthcare listening hamradio FreeAllPoliticalPrisoners card10 radiobroadcasting 3dcad radioshow local california listeningtonow radiobroadcast spazradio anonradio io</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>licenses</title>
<description>copyright creative common creativecommons</description>
<description>copyright creative creativetoots common creativecommons</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>education</title>
<description>education teach tutorial</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>privacy</title>
<description>surveillancetech privacymatters surveillance dataprivacy privacy</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>microcontroller</title>
<description>microcontroller arduino</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>people</title>
<description>monbiot aldoushuxley relationships AskVanta</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>scotland</title>
<description>glasgow highlands edinburgh loch</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>entertainment</title>
<description>watching Thundercat thisisthetypeofmemethatilikecauseitcontainsreptiles entertainment me meow un themandalorian</description>
<description>watching Thundercat thisisthetypeofmemethatilikecauseitcontainsreptiles entertainment me meow un nowwatching themandalorian</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>#software</title>
<description>flatpak</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>microcontrollers</title>
<description>esp8266 esp32</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>help</title>
<description>helpful help</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>war</title>
<description>weapons</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
</item>
<item>
<title>philosophy</title>
<description>stoic postmodernism</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>france</title>
<description>Macronavirus</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>travel</title>
<description>travel taxi</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
<item>
<title>environment</title>
<description>climatechaos</description>
<title>architecture</title>
<description>concrete</description>
<link/>
<pubDate>Tue, 29 Dec 2020 20:59:38 UT</pubDate>
<pubDate>Sun, 10 Jan 2021 20:09:37 UT</pubDate>
</item>
</channel>
</rss>

View File

@ -136,6 +136,7 @@
--publish-button-bottom-offset: 10px;
--banner-height: 15vh;
--banner-height-mobile: 10vh;
--post-separator-background: transparent;
--post-separator-margin-top: 0;
--post-separator-margin-bottom: 0;
--post-separator-width: 95%;
@ -186,7 +187,7 @@ body, html {
}
.postSeparatorImage img {
background-color: transparent;
background-color: var(--post-separator-background);
padding-top: var(--post-separator-margin-top);
padding-bottom: var(--post-separator-margin-bottom);
width: var(--post-separator-width);
@ -1124,6 +1125,7 @@ div.container {
filter: brightness(var(--icon-brightness-change));
}
.col-right img.rightColEdit {
float: right;
background: transparent;
width: var(--column-right-icon-size);
}

View File

@ -15,6 +15,7 @@ from person import deactivateAccount
from skills import setSkillLevel
from roles import setRole
from webfinger import webfingerHandle
from posts import downloadFollowCollection
from posts import getPublicPostDomains
from posts import getPublicPostDomainsBlocked
from posts import sendBlockViaServer
@ -73,6 +74,7 @@ from shares import addShare
from theme import setTheme
from announce import sendAnnounceViaServer
from socnet import instancesGraph
from migrate import migrateAccounts
import argparse
@ -156,6 +158,10 @@ parser.add_argument('--maxFollowers',
default=2000,
help='Maximum number of followers per account. ' +
'Zero for no limit.')
parser.add_argument('--followers',
dest='followers', type=str,
default='',
help='Show list of followers for the given actor')
parser.add_argument('--postcache', dest='maxRecentPosts', type=int,
default=512,
help='The maximum number of recent posts to store in RAM')
@ -321,6 +327,9 @@ parser.add_argument("--i2p", type=str2bool, nargs='?',
parser.add_argument("--tor", type=str2bool, nargs='?',
const=True, default=False,
help="Route via Tor")
parser.add_argument("--migrations", type=str2bool, nargs='?',
const=True, default=False,
help="Migrate moved accounts")
parser.add_argument("--tests", type=str2bool, nargs='?',
const=True, default=False,
help="Run unit tests")
@ -555,12 +564,14 @@ if args.postDomains:
args.port = 80
elif args.gnunet:
proxyType = 'gnunet'
wordFrequency = {}
domainList = []
domainList = getPublicPostDomains(None,
baseDir, nickname, domain,
proxyType, args.port,
httpPrefix, debug,
__version__, domainList)
__version__,
wordFrequency, domainList)
for postDomain in domainList:
print(postDomain)
sys.exit()
@ -593,12 +604,14 @@ if args.postDomainsBlocked:
args.port = 80
elif args.gnunet:
proxyType = 'gnunet'
wordFrequency = {}
domainList = []
domainList = getPublicPostDomainsBlocked(None,
baseDir, nickname, domain,
proxyType, args.port,
httpPrefix, debug,
__version__, domainList)
__version__,
wordFrequency, domainList)
for postDomain in domainList:
print(postDomain)
sys.exit()
@ -1304,6 +1317,33 @@ if args.hyper:
if args.i2p:
httpPrefix = 'http'
if args.migrations:
cachedWebfingers = {}
if args.http or domain.endswith('.onion'):
httpPrefix = 'http'
port = 80
proxyType = 'tor'
elif domain.endswith('.i2p'):
httpPrefix = 'http'
port = 80
proxyType = 'i2p'
elif args.gnunet:
httpPrefix = 'gnunet'
port = 80
proxyType = 'gnunet'
else:
httpPrefix = 'https'
port = 443
session = createSession(proxyType)
ctr = migrateAccounts(baseDir, session,
httpPrefix, cachedWebfingers,
True)
if ctr == 0:
print('No followed accounts have moved')
else:
print(str(ctr) + ' followed accounts were migrated')
sys.exit()
if args.actor:
originalActor = args.actor
if '/@' in args.actor or \
@ -1433,6 +1473,122 @@ if args.actor:
print('Failed to get ' + personUrl)
sys.exit()
if args.followers:
originalActor = args.followers
if '/@' in args.followers or \
'/users/' in args.followers or \
args.followers.startswith('http') or \
args.followers.startswith('dat'):
# format: https://domain/@nick
prefixes = getProtocolPrefixes()
for prefix in prefixes:
args.followers = args.followers.replace(prefix, '')
args.followers = args.followers.replace('/@', '/users/')
if not hasUsersPath(args.followers):
print('Expected actor format: ' +
'https://domain/@nick or https://domain/users/nick')
sys.exit()
if '/users/' in args.followers:
nickname = args.followers.split('/users/')[1]
nickname = nickname.replace('\n', '').replace('\r', '')
domain = args.followers.split('/users/')[0]
elif '/profile/' in args.followers:
nickname = args.followers.split('/profile/')[1]
nickname = nickname.replace('\n', '').replace('\r', '')
domain = args.followers.split('/profile/')[0]
elif '/channel/' in args.followers:
nickname = args.followers.split('/channel/')[1]
nickname = nickname.replace('\n', '').replace('\r', '')
domain = args.followers.split('/channel/')[0]
elif '/accounts/' in args.followers:
nickname = args.followers.split('/accounts/')[1]
nickname = nickname.replace('\n', '').replace('\r', '')
domain = args.followers.split('/accounts/')[0]
else:
# format: @nick@domain
if '@' not in args.followers:
print('Syntax: --actor nickname@domain')
sys.exit()
if args.followers.startswith('@'):
args.followers = args.followers[1:]
if '@' not in args.followers:
print('Syntax: --actor nickname@domain')
sys.exit()
nickname = args.followers.split('@')[0]
domain = args.followers.split('@')[1]
domain = domain.replace('\n', '').replace('\r', '')
cachedWebfingers = {}
if args.http or domain.endswith('.onion'):
httpPrefix = 'http'
port = 80
proxyType = 'tor'
elif domain.endswith('.i2p'):
httpPrefix = 'http'
port = 80
proxyType = 'i2p'
elif args.gnunet:
httpPrefix = 'gnunet'
port = 80
proxyType = 'gnunet'
else:
httpPrefix = 'https'
port = 443
session = createSession(proxyType)
if nickname == 'inbox':
nickname = domain
handle = nickname + '@' + domain
wfRequest = webfingerHandle(session, handle,
httpPrefix, cachedWebfingers,
None, __version__)
if not wfRequest:
print('Unable to webfinger ' + handle)
sys.exit()
if not isinstance(wfRequest, dict):
print('Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
sys.exit()
personUrl = None
if wfRequest.get('errors'):
print('wfRequest error: ' + str(wfRequest['errors']))
if hasUsersPath(args.followers):
personUrl = originalActor
else:
sys.exit()
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
}
if not personUrl:
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:
# try single user instance
personUrl = httpPrefix + '://' + domain
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
}
if '/channel/' in personUrl or '/accounts/' in personUrl:
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
}
followersList = \
downloadFollowCollection('followers', session,
httpPrefix, personUrl, 1, 3)
if followersList:
for actor in followersList:
print(actor)
sys.exit()
if args.addaccount:
if '@' in args.addaccount:
nickname = args.addaccount.split('@')[0]

View File

@ -7,48 +7,195 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from utils import getNicknameFromActor
from utils import getDomainFromActor
from webfinger import webfingerHandle
from blocking import isBlocked
from session import getJson
from posts import getUserUrl
from follow import unfollowAccount
def _migrateFollows(followFilename: str, oldHandle: str,
newHandle: str) -> None:
"""Changes a handle within following or followers list
def _moveFollowingHandlesForAccount(baseDir: str, nickname: str, domain: str,
session,
httpPrefix: str, cachedWebfingers: {},
debug: bool) -> int:
"""Goes through all follows for an account and updates any that have moved
"""
if not os.path.isfile(followFilename):
return
if oldHandle not in open(followFilename).read():
return
followData = None
with open(followFilename, 'r') as followFile:
followData = followFile.read()
if not followData:
return
newFollowData = followData.replace(oldHandle, newHandle)
if followData == newFollowData:
return
with open(followFilename, 'w+') as followFile:
followFile.write(newFollowData)
ctr = 0
followingFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/following.txt'
if not os.path.isfile(followingFilename):
return ctr
with open(followingFilename, "r") as f:
followingHandles = f.readlines()
for followHandle in followingHandles:
followHandle = followHandle.strip("\n").strip("\r")
ctr += \
_updateMovedHandle(baseDir, nickname, domain,
followHandle, session,
httpPrefix, cachedWebfingers,
debug)
return ctr
def migrateAccount(baseDir: str, oldHandle: str, newHandle: str) -> None:
"""If a followed account changes then this modifies the
following and followers lists for each account accordingly
def _updateMovedHandle(baseDir: str, nickname: str, domain: str,
handle: str, session,
httpPrefix: str, cachedWebfingers: {},
debug: bool) -> int:
"""Check if an account has moved, and if so then alter following.txt
for each account.
Returns 1 if moved, 0 otherwise
"""
if oldHandle.startswith('@'):
oldHandle = oldHandle[1:]
if '@' not in oldHandle:
return
if newHandle.startswith('@'):
newHandle = newHandle[1:]
if '@' not in newHandle:
return
ctr = 0
if '@' not in handle:
return ctr
if len(handle) < 5:
return ctr
if handle.startswith('@'):
handle = handle[1:]
wfRequest = webfingerHandle(session, handle,
httpPrefix, cachedWebfingers,
None, __version__)
if not wfRequest:
print('updateMovedHandle unable to webfinger ' + handle)
return ctr
if not isinstance(wfRequest, dict):
print('updateMovedHandle webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return ctr
personUrl = None
if wfRequest.get('errors'):
print('wfRequest error: ' + str(wfRequest['errors']))
return ctr
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
}
if not personUrl:
personUrl = getUserUrl(wfRequest)
if not personUrl:
return ctr
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
}
personJson = \
getJson(session, personUrl, asHeader, None, __version__,
httpPrefix, None)
if not personJson:
return ctr
if not personJson.get('movedTo'):
return ctr
movedToUrl = personJson['movedTo']
if '://' not in movedToUrl:
return ctr
if '.' not in movedToUrl:
return ctr
movedToNickname = getNicknameFromActor(movedToUrl)
if not movedToNickname:
return ctr
movedToDomain, movedToPort = getDomainFromActor(movedToUrl)
if not movedToDomain:
return ctr
movedToDomainFull = movedToDomain
if movedToPort:
if movedToPort != 80 and movedToPort != 443:
movedToDomainFull = movedToDomain + ':' + str(movedToPort)
if isBlocked(baseDir, nickname, domain,
movedToNickname, movedToDomain):
# someone that you follow has moved to a blocked domain
# so just unfollow them
unfollowAccount(baseDir, nickname, domain,
movedToNickname, movedToDomainFull,
'following.txt', debug)
return ctr
followingFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/following.txt'
if os.path.isfile(followingFilename):
with open(followingFilename, "r") as f:
followingHandles = f.readlines()
movedToHandle = movedToNickname + '@' + movedToDomainFull
handleLower = handle.lower()
refollowFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/refollow.txt'
# unfollow the old handle
with open(followingFilename, 'w+') as f:
for followHandle in followingHandles:
if followHandle.strip("\n").strip("\r").lower() != \
handleLower:
f.write(followHandle)
else:
handleNickname = handle.split('@')[0]
handleDomain = handle.split('@')[1]
unfollowAccount(baseDir, nickname, domain,
handleNickname,
handleDomain,
'following.txt', debug)
ctr += 1
print('Unfollowed ' + handle + ' who has moved to ' +
movedToHandle)
# save the new handles to the refollow list
if os.path.isfile(refollowFilename):
with open(refollowFilename, 'a+') as f:
f.write(movedToHandle + '\n')
else:
with open(refollowFilename, 'w+') as f:
f.write(movedToHandle + '\n')
followersFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/followers.txt'
if os.path.isfile(followersFilename):
with open(followersFilename, "r") as f:
followerHandles = f.readlines()
handleLower = handle.lower()
# remove followers who have moved
with open(followersFilename, 'w+') as f:
for followerHandle in followerHandles:
if followerHandle.strip("\n").strip("\r").lower() != \
handleLower:
f.write(followerHandle)
else:
ctr += 1
print('Removed follower who has moved ' + handle)
return ctr
def migrateAccounts(baseDir: str, session,
httpPrefix: str, cachedWebfingers: {},
debug: bool) -> int:
"""If followed accounts change then this modifies the
following lists for each account accordingly.
Returns the number of accounts migrated
"""
# update followers and following lists for each account
ctr = 0
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for handle in dirs:
if '@' in handle:
accountDir = baseDir + '/accounts/' + handle
followFilename = accountDir + '/following.txt'
_migrateFollows(followFilename, oldHandle, newHandle)
followFilename = accountDir + '/followers.txt'
_migrateFollows(followFilename, oldHandle, newHandle)
if '@' not in handle:
continue
if handle.startswith('inbox@'):
continue
if handle.startswith('news@'):
continue
nickname = handle.split('@')[0]
domain = handle.split('@')[1]
ctr += \
_moveFollowingHandlesForAccount(baseDir, nickname, domain,
session, httpPrefix,
cachedWebfingers, debug)
break
return ctr

View File

@ -23,9 +23,9 @@ from newswire import getDictFromNewswire
# from posts import sendSignedJson
from posts import createNewsPost
from posts import archivePostsForPerson
from content import removeHtmlTag
from content import dangerousMarkup
from content import validHashTag
from utils import removeHtml
from utils import getFullDomain
from utils import loadJson
from utils import saveJson
@ -506,14 +506,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str,
rssDescription = ''
# get the rss description if it exists
rssDescription = _removeControlCharacters(item[4])
if rssDescription.startswith('<![CDATA['):
rssDescription = rssDescription.replace('<![CDATA[', '')
rssDescription = rssDescription.replace(']]>', '')
rssDescription = rssDescription.replace(']]', '')
if '&' in rssDescription:
rssDescription = html.unescape(rssDescription)
rssDescription = '<p>' + rssDescription + '<p>'
rssDescription = '<p>' + removeHtml(item[4]) + '<p>'
mirrored = item[7]
postUrl = url
@ -526,20 +519,9 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str,
postUrl += '/index.html'
# add the off-site link to the description
if rssDescription and \
not dangerousMarkup(rssDescription, allowLocalNetworkAccess):
rssDescription += \
'<br><a href="' + postUrl + '">' + \
translate['Read more...'] + '</a>'
else:
rssDescription = \
'<a href="' + postUrl + '">' + \
translate['Read more...'] + '</a>'
# remove image dimensions
if '<img' in rssDescription:
rssDescription = removeHtmlTag(rssDescription, 'width')
rssDescription = removeHtmlTag(rssDescription, 'height')
rssDescription += \
'<br><a href="' + postUrl + '">' + \
translate['Read more...'] + '</a>'
followersOnly = False
# NOTE: the id when the post is created will not be

View File

@ -304,13 +304,13 @@ def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
description = ''
if '<description>' in rssItem and '</description>' in rssItem:
description = rssItem.split('<description>')[1]
description = _removeCDATA(description.split('</description>')[0])
description = removeHtml(description.split('</description>')[0])
else:
if '<media:description>' in rssItem and \
'</media:description>' in rssItem:
description = rssItem.split('<media:description>')[1]
description = description.split('</media:description>')[0]
description = _removeCDATA(description)
description = removeHtml(description)
link = rssItem.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
@ -388,13 +388,13 @@ def _xml1StrToDict(baseDir: str, domain: str, xmlStr: str,
description = ''
if '<description>' in rssItem and '</description>' in rssItem:
description = rssItem.split('<description>')[1]
description = _removeCDATA(description.split('</description>')[0])
description = removeHtml(description.split('</description>')[0])
else:
if '<media:description>' in rssItem and \
'</media:description>' in rssItem:
description = rssItem.split('<media:description>')[1]
description = description.split('</media:description>')[0]
description = _removeCDATA(description)
description = removeHtml(description)
link = rssItem.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
@ -460,13 +460,13 @@ def _atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
description = ''
if '<summary>' in atomItem and '</summary>' in atomItem:
description = atomItem.split('<summary>')[1]
description = _removeCDATA(description.split('</summary>')[0])
description = removeHtml(description.split('</summary>')[0])
else:
if '<media:description>' in atomItem and \
'</media:description>' in atomItem:
description = atomItem.split('<media:description>')[1]
description = description.split('</media:description>')[0]
description = _removeCDATA(description)
description = removeHtml(description)
link = atomItem.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
@ -538,11 +538,11 @@ def _atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
'</media:description>' in atomItem:
description = atomItem.split('<media:description>')[1]
description = description.split('</media:description>')[0]
description = _removeCDATA(description)
description = removeHtml(description)
elif '<summary>' in atomItem and '</summary>' in atomItem:
description = atomItem.split('<summary>')[1]
description = description.split('</summary>')[0]
description = _removeCDATA(description)
description = removeHtml(description)
link = atomItem.split('<yt:videoId>')[1]
link = link.split('</yt:videoId>')[0]
link = 'https://www.youtube.com/watch?v=' + link.strip()
@ -692,7 +692,7 @@ def getRSSfromDict(baseDir: str, newswire: {},
continue
rssStr += '<item>\n'
rssStr += ' <title>' + fields[0] + '</title>\n'
description = _removeCDATA(firstParagraphFromString(fields[4]))
description = removeHtml(firstParagraphFromString(fields[4]))
rssStr += ' <description>' + description + '</description>\n'
url = fields[1]
if '://' not in url:
@ -812,7 +812,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str,
votes = loadJson(fullPostFilename + '.votes')
content = postJsonObject['object']['content']
description = firstParagraphFromString(content)
description = _removeCDATA(description)
description = removeHtml(description)
tagsFromPost = _getHashtagsFromPost(postJsonObject)
_addNewswireDictEntry(baseDir, domain,
newswire, published,

View File

@ -134,6 +134,7 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
extensions = {
"jpeg": "jpg",
"gif": "gif",
"svg": "svg",
"webp": "webp",
"avif": "avif",
"audio/mpeg": "mp3",

View File

@ -60,8 +60,9 @@ def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
if not (imageFilename.endswith('.png') or
imageFilename.endswith('.jpg') or
imageFilename.endswith('.jpeg') or
imageFilename.endswith('.svg') or
imageFilename.endswith('.gif')):
print('Profile image must be png, jpg or gif format')
print('Profile image must be png, jpg, gif or svg format')
return False
if imageFilename.startswith('~/'):
@ -95,6 +96,9 @@ def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
if imageFilename.endswith('.gif'):
mediaType = 'image/gif'
iconFilename = iconFilenameBase + '.gif'
if imageFilename.endswith('.svg'):
mediaType = 'image/svg+xml'
iconFilename = iconFilenameBase + '.svg'
profileFilename = baseDir + '/accounts/' + handle + '/' + iconFilename
personJson = loadJson(personFilename)
@ -591,7 +595,7 @@ def personLookup(domain: str, path: str, baseDir: str) -> {}:
else:
notPersonLookup = ('/inbox', '/outbox', '/outboxarchive',
'/followers', '/following', '/featured',
'.png', '.jpg', '.gif', '.mpv')
'.png', '.jpg', '.gif', '.svg', '.mpv')
for ending in notPersonLookup:
if path.endswith(ending):
return None

115
posts.py
View File

@ -30,6 +30,7 @@ from session import postJsonString
from session import postImage
from webfinger import webfingerHandle
from httpsig import createSignedHeader
from utils import isPublicPost
from utils import hasUsersPath
from utils import validPostDate
from utils import getFullDomain
@ -146,10 +147,10 @@ def _cleanHtml(rawHtml: str) -> str:
def getUserUrl(wfRequest: {}, sourceId=0) -> str:
"""Gets the actor url from a webfinger request
"""
# print('getUserUrl: ' + str(sourceId) + ' ' + str(wfRequest))
if not wfRequest.get('links'):
if sourceId == 72367:
print('getUserUrl failed to get display name for webfinger ' +
print('getUserUrl ' + str(sourceId) +
' failed to get display name for webfinger ' +
str(wfRequest))
else:
print('getUserUrl webfinger activity+json contains no links ' +
@ -283,6 +284,9 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
displayName = None
if personJson.get('name'):
displayName = removeHtml(personJson['name'])
# have they moved?
if personJson.get('movedTo'):
displayName += ''
storePersonInCache(baseDir, personUrl, personJson, personCache, True)
@ -468,6 +472,46 @@ def _getPosts(session, outboxUrl: str, maxPosts: int,
return personPosts
def _updateWordFrequency(content: str, wordFrequency: {}) -> None:
"""Creates a dictionary containing words and the number of times
that they appear
"""
plainText = removeHtml(content)
plainText = plainText.replace('.', ' ')
plainText = plainText.replace(';', ' ')
plainText = plainText.replace('?', ' ')
wordsList = plainText.split(' ')
commonWords = (
'that', 'some', 'about', 'then', 'they', 'were',
'also', 'from', 'with', 'this', 'have', 'more',
'need', 'here', 'would', 'these', 'into', 'very',
'well', 'when', 'what', 'your', 'there', 'which',
'even', 'there', 'such', 'just', 'those', 'only',
'will', 'much', 'than', 'them', 'each', 'goes',
'been', 'over', 'their', 'where', 'could', 'though'
)
for word in wordsList:
wordLen = len(word)
if wordLen < 3:
continue
if wordLen < 4:
if word.upper() != word:
continue
if '&' in word or \
'"' in word or \
'@' in word or \
'://' in word:
continue
if word.endswith(':'):
word = word.replace(':', '')
if word.lower() in commonWords:
continue
if wordFrequency.get(word):
wordFrequency[word] += 1
else:
wordFrequency[word] = 1
def getPostDomains(session, outboxUrl: str, maxPosts: int,
maxMentions: int,
maxEmoji: int, maxAttachments: int,
@ -475,7 +519,9 @@ def getPostDomains(session, outboxUrl: str, maxPosts: int,
personCache: {},
debug: bool,
projectVersion: str, httpPrefix: str,
domain: str, domainList=[]) -> []:
domain: str,
wordFrequency: {},
domainList=[]) -> []:
"""Returns a list of domains referenced within public posts
"""
if not outboxUrl:
@ -502,6 +548,9 @@ def getPostDomains(session, outboxUrl: str, maxPosts: int,
continue
if not isinstance(item['object'], dict):
continue
if item['object'].get('content'):
_updateWordFrequency(item['object']['content'],
wordFrequency)
if item['object'].get('inReplyTo'):
if isinstance(item['object']['inReplyTo'], str):
postDomain, postPort = \
@ -3091,7 +3140,7 @@ def _createBoxIndexed(recentPostsCache: {},
if not authorized:
if p.get('object'):
if isinstance(p['object'], dict):
if isDM(p):
if not isPublicPost(p):
continue
if p['object'].get('likes'):
p['likes'] = {'items': []}
@ -3333,7 +3382,7 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str,
domainList=[]) -> []:
wordFrequency: {}, domainList=[]) -> []:
""" Returns a list of domains referenced within public posts
"""
if not session:
@ -3370,14 +3419,50 @@ def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str,
getPostDomains(session, personUrl, 64, maxMentions, maxEmoji,
maxAttachments, federationList,
personCache, debug,
projectVersion, httpPrefix, domain, domainList)
projectVersion, httpPrefix, domain,
wordFrequency, domainList)
postDomains.sort()
return postDomains
def downloadFollowCollection(followType: str,
session, httpPrefix,
actor: str, pageNumber=1,
noOfPages=1) -> []:
"""Returns a list of following/followers for the given actor
by downloading the json for their following/followers collection
"""
prof = 'https://www.w3.org/ns/activitystreams'
if '/channel/' not in actor or '/accounts/' not in actor:
sessionHeaders = {
'Accept': 'application/activity+json; profile="' + prof + '"'
}
else:
sessionHeaders = {
'Accept': 'application/ld+json; profile="' + prof + '"'
}
result = []
for pageCtr in range(noOfPages):
url = actor + '/' + followType + '?page=' + str(pageNumber + pageCtr)
followersJson = \
getJson(session, url, sessionHeaders, None, __version__,
httpPrefix, None)
if followersJson:
if followersJson.get('orderedItems'):
for followerActor in followersJson['orderedItems']:
if followerActor not in result:
result.append(followerActor)
else:
break
else:
break
return result
def getPublicPostInfo(session, baseDir: str, nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str) -> []:
debug: bool, projectVersion: str,
wordFrequency: {}) -> []:
""" Returns a dict of domains referenced within public posts
"""
if not session:
@ -3415,7 +3500,8 @@ def getPublicPostInfo(session, baseDir: str, nickname: str, domain: str,
getPostDomains(session, personUrl, maxPosts, maxMentions, maxEmoji,
maxAttachments, federationList,
personCache, debug,
projectVersion, httpPrefix, domain, [])
projectVersion, httpPrefix, domain,
wordFrequency, [])
postDomains.sort()
domainsInfo = {}
for d in postDomains:
@ -3441,7 +3527,7 @@ def getPublicPostDomainsBlocked(session, baseDir: str,
nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str,
domainList=[]) -> []:
wordFrequency: {}, domainList=[]) -> []:
""" Returns a list of domains referenced within public posts which
are globally blocked on this instance
"""
@ -3449,7 +3535,7 @@ def getPublicPostDomainsBlocked(session, baseDir: str,
getPublicPostDomains(session, baseDir, nickname, domain,
proxyType, port, httpPrefix,
debug, projectVersion,
domainList)
wordFrequency, domainList)
if not postDomains:
return []
@ -3497,9 +3583,10 @@ def checkDomains(session, baseDir: str,
nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str,
maxBlockedDomains: int, singleCheck: bool):
maxBlockedDomains: int, singleCheck: bool) -> None:
"""Checks follower accounts for references to globally blocked domains
"""
wordFrequency = {}
nonMutuals = _getNonMutualsOfPerson(baseDir, nickname, domain)
if not nonMutuals:
print('No non-mutual followers were found')
@ -3523,7 +3610,8 @@ def checkDomains(session, baseDir: str,
nonMutualNickname,
nonMutualDomain,
proxyType, port, httpPrefix,
debug, projectVersion, [])
debug, projectVersion,
wordFrequency, [])
if blockedDomains:
if len(blockedDomains) > maxBlockedDomains:
followerWarningStr += handle + '\n'
@ -3542,7 +3630,8 @@ def checkDomains(session, baseDir: str,
nonMutualNickname,
nonMutualDomain,
proxyType, port, httpPrefix,
debug, projectVersion, [])
debug, projectVersion,
wordFrequency, [])
if blockedDomains:
print(handle)
for d in blockedDomains:

View File

@ -37,7 +37,9 @@ import traceback
from collections import deque, namedtuple
from numbers import Integral, Real
from context import getApschemaV1_9
from context import getApschemaV1_21
from context import getLitepubV0_1
from context import getV1Schema
from context import getV1SecuritySchema
from context import getActivitystreamsSchema
@ -397,13 +399,27 @@ def load_document(url):
'document': getActivitystreamsSchema()
}
return doc
elif url == 'https://raitisoja.com/apschema/v1.21':
elif url.endswith('/apschema/v1.9'):
doc = {
'contextUrl': None,
'documentUrl': url,
'document': getApschemaV1_9()
}
return doc
elif url.endswith('/apschema/v1.21'):
doc = {
'contextUrl': None,
'documentUrl': url,
'document': getApschemaV1_21()
}
return doc
elif url.endswith('/litepub-0.1.jsonld'):
doc = {
'contextUrl': None,
'documentUrl': url,
'document': getLitepubV0_1()
}
return doc
return None
except JsonLdError as e:
raise e

View File

@ -142,6 +142,10 @@ def runPostSchedule(baseDir: str, httpd, maxScheduledPosts: int):
for account in dirs:
if '@' not in account:
continue
if account.startswith('inbox@'):
continue
if account.startswith('news@'):
continue
# scheduled posts index for this account
scheduleIndexFilename = \
baseDir + '/accounts/' + account + '/schedule.index'

View File

@ -180,8 +180,9 @@ def postImage(session, attachImageFilename: str, federationList: [],
if not (attachImageFilename.endswith('.jpg') or
attachImageFilename.endswith('.jpeg') or
attachImageFilename.endswith('.png') or
attachImageFilename.endswith('.svg') or
attachImageFilename.endswith('.gif')):
print('Image must be png, jpg, or gif')
print('Image must be png, jpg, gif or svg')
return None
if not os.path.isfile(attachImageFilename):
print('Image not found: ' + attachImageFilename)
@ -191,6 +192,8 @@ def postImage(session, attachImageFilename: str, federationList: [],
contentType = 'image/png'
if attachImageFilename.endswith('.gif'):
contentType = 'image/gif'
if attachImageFilename.endswith('.svg'):
contentType = 'image/svg+xml'
headers['Content-type'] = contentType
with open(attachImageFilename, 'rb') as avFile:

View File

@ -67,11 +67,13 @@ def instancesGraph(baseDir: str, handles: str,
projectVersion, httpPrefix,
nickname, domain, 'outbox',
27261)
wordFrequency = {}
postDomains = \
getPostDomains(session, personUrl, 64, maxMentions, maxEmoji,
maxAttachments, federationList,
personCache, debug,
projectVersion, httpPrefix, domain, [])
projectVersion, httpPrefix, domain,
wordFrequency, [])
postDomains.sort()
for fedDomain in postDomains:
dotLineStr = ' "' + domain + '" -> "' + fedDomain + '";\n'

View File

@ -2799,7 +2799,6 @@ def testFunctions():
'threadSendPost',
'sendToFollowers',
'expireCache',
'migrateAccount',
'getMutualsOfPerson',
'runPostsQueue',
'runSharesExpire',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -17,7 +17,7 @@
"hashtag-size2": "30px",
"font-size-calendar-header": "2rem",
"font-size-calendar-cell": "2rem",
"time-vertical-align": "10px",
"time-vertical-align": "1.1%",
"publish-button-vertical-offset": "15px",
"vertical-between-posts": "0",
"vertical-between-posts-header": "0 0",
@ -34,12 +34,13 @@
"font-size-newswire": "12px",
"font-size-newswire-mobile": "30px",
"font-size-dropdown-header": "30px",
"post-separator-background": "#efefef",
"post-separator-margin-top": "1%",
"post-separator-margin-bottom": "1%",
"post-separator-width": "96%",
"post-separator-width": "100%",
"separator-width-left": "98%",
"separator-width-right": "99%",
"post-separator-height": "1px",
"post-separator-height": "0px",
"column-left-top-margin": "10px",
"column-left-border-width": "0px",
"column-right-border-width": "0px",

View File

@ -351,5 +351,12 @@
"Peertube Instances": "مثيلات Peertube",
"Show video previews for the following Peertube sites.": "إظهار معاينات الفيديو لمواقع Peertube التالية.",
"Follows you": "يتبعك",
"Verify all signatures": "تحقق من جميع التوقيعات"
"Verify all signatures": "تحقق من جميع التوقيعات",
"Blocked followers": "المتابعون المحظورون",
"Blocked following": "المتابعة المحظورة",
"Receives posts from the following accounts": "يتلقى المشاركات من الحسابات التالية",
"Sends out posts to the following accounts": "يرسل المنشورات إلى الحسابات التالية",
"Word frequencies": "ترددات الكلمات",
"New account": "حساب جديد",
"Moved to new account address": "انتقل إلى عنوان الحساب الجديد"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Instàncies de Peertube",
"Show video previews for the following Peertube sites.": "Mostra les previsualitzacions de vídeo dels següents llocs de Peertube.",
"Follows you": "Et segueix",
"Verify all signatures": "Verifiqueu totes les signatures"
"Verify all signatures": "Verifiqueu totes les signatures",
"Blocked followers": "Seguidors bloquejats",
"Blocked following": "Seguiment bloquejat",
"Receives posts from the following accounts": "Rep publicacions dels comptes següents",
"Sends out posts to the following accounts": "Envia publicacions als comptes següents",
"Word frequencies": "Freqüències de paraules",
"New account": "Compte nou",
"Moved to new account address": "S'ha mogut a l'adreça del compte nova"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Camau Peertube",
"Show video previews for the following Peertube sites.": "Dangos rhagolygon fideo ar gyfer y safleoedd Peertube canlynol.",
"Follows you": "Yn eich dilyn chi",
"Verify all signatures": "Gwirio pob llofnod"
"Verify all signatures": "Gwirio pob llofnod",
"Blocked followers": "Dilynwyr wedi'u blocio",
"Blocked following": "Wedi'i rwystro yn dilyn",
"Receives posts from the following accounts": "Yn derbyn swyddi o'r cyfrifon canlynol",
"Sends out posts to the following accounts": "Yn anfon postiadau i'r cyfrifon canlynol",
"Word frequencies": "Amleddau geiriau",
"New account": "Cyfrif newydd",
"Moved to new account address": "Wedi'i symud i gyfeiriad cyfrif newydd"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Peertube-Instanzen",
"Show video previews for the following Peertube sites.": "Zeigen Sie eine Videovorschau für die folgenden Peertube-Websites an.",
"Follows you": "Folgt dir",
"Verify all signatures": "Überprüfen Sie alle Signaturen"
"Verify all signatures": "Überprüfen Sie alle Signaturen",
"Blocked followers": "Blockierte Follower",
"Blocked following": "Folgendes blockiert",
"Receives posts from the following accounts": "Erhält Beiträge von folgenden Konten",
"Sends out posts to the following accounts": "Sendet Beiträge an die folgenden Konten",
"Word frequencies": "Worthäufigkeiten",
"New account": "Neues Konto",
"Moved to new account address": "An neue Kontoadresse verschoben"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Peertube Instances",
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites.",
"Follows you": "Follows you",
"Verify all signatures": "Verify all signatures"
"Verify all signatures": "Verify all signatures",
"Blocked followers": "Blocked followers",
"Blocked following": "Blocked following",
"Receives posts from the following accounts": "Receives posts from the following accounts",
"Sends out posts to the following accounts": "Sends out posts to the following accounts",
"Word frequencies": "Word frequencies",
"New account": "New account",
"Moved to new account address": "Moved to new account address"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Instancias de Peertube",
"Show video previews for the following Peertube sites.": "Muestre vistas previas de video para los siguientes sitios de Peertube.",
"Follows you": "Te sigue",
"Verify all signatures": "Verificar todas las firmas"
"Verify all signatures": "Verificar todas las firmas",
"Blocked followers": "Seguidores bloqueadas",
"Blocked following": "Seguimiento bloqueado",
"Receives posts from the following accounts": "Recibe publicaciones de las siguientes cuentas",
"Sends out posts to the following accounts": "Envía publicaciones a las siguientes cuentas",
"Word frequencies": "Frecuencias de palabras",
"New account": "Nueva cuenta",
"Moved to new account address": "Movido a la nueva dirección de la cuenta"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Instances Peertube",
"Show video previews for the following Peertube sites.": "Afficher des aperçus vidéo pour les sites Peertube suivants.",
"Follows you": "Vous suit",
"Verify all signatures": "Vérifier toutes les signatures"
"Verify all signatures": "Vérifier toutes les signatures",
"Blocked followers": "Abonnés bloqués",
"Blocked following": "Bloqué suite",
"Receives posts from the following accounts": "Reçoit les publications des comptes suivants",
"Sends out posts to the following accounts": "Envoie des messages aux comptes suivants",
"Word frequencies": "Fréquences des mots",
"New account": "Nouveau compte",
"Moved to new account address": "Déplacé vers une nouvelle adresse de compte"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Imeachtaí Peertube",
"Show video previews for the following Peertube sites.": "Taispeáin réamhamharcanna físe do na suíomhanna Peertube seo a leanas.",
"Follows you": "Leanann tú",
"Verify all signatures": "Fíoraigh gach síniú"
"Verify all signatures": "Fíoraigh gach síniú",
"Blocked followers": "Leanúna blocáilte",
"Blocked following": "Blocáilte ina dhiaidh",
"Receives posts from the following accounts": "Faigheann sé poist ó na cuntais seo a leanas",
"Sends out posts to the following accounts": "Seoltar poist chuig na cuntais seo a leanas",
"Word frequencies": "Minicíochtaí focal",
"New account": "Cuntas nua",
"Moved to new account address": "Ar athraíodh a ionad go seoladh cuntas nua"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Peertube उदाहरण",
"Show video previews for the following Peertube sites.": "निम्नलिखित Peertube साइटों के लिए वीडियो पूर्वावलोकन दिखाएं।",
"Follows you": "आपका पीछा करता है",
"Verify all signatures": "सभी हस्ताक्षर सत्यापित करें"
"Verify all signatures": "सभी हस्ताक्षर सत्यापित करें",
"Blocked followers": "अवरुद्ध अनुयायियों",
"Blocked following": "बाद में ब्लॉक किया गया",
"Receives posts from the following accounts": "निम्नलिखित खातों से पोस्ट प्राप्त करता है",
"Sends out posts to the following accounts": "निम्नलिखित खातों में पोस्ट भेजता है",
"Word frequencies": "शब्द आवृत्तियों",
"New account": "नया खाता",
"Moved to new account address": "नए खाते के पते पर ले जाया गया"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Istanze di Peertube",
"Show video previews for the following Peertube sites.": "Mostra le anteprime dei video per i seguenti siti Peertube.",
"Follows you": "Ti segue",
"Verify all signatures": "Verifica tutte le firme"
"Verify all signatures": "Verifica tutte le firme",
"Blocked followers": "Follower bloccati",
"Blocked following": "Seguito bloccato",
"Receives posts from the following accounts": "Riceve post dai seguenti account",
"Sends out posts to the following accounts": "Invia messaggi ai seguenti account",
"Word frequencies": "Frequenze di parole",
"New account": "Nuovo account",
"Moved to new account address": "Spostato al nuovo indirizzo dell'account"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Peertubeインスタンス",
"Show video previews for the following Peertube sites.": "次のPeertubeサイトのビデオプレビューを表示します。",
"Follows you": "あなたについていきます",
"Verify all signatures": "すべての署名を確認する"
"Verify all signatures": "すべての署名を確認する",
"Blocked followers": "ブロックされたフォロワー",
"Blocked following": "次のブロック",
"Receives posts from the following accounts": "以下のアカウントから投稿を受け取ります",
"Sends out posts to the following accounts": "以下のアカウントに投稿を送信します",
"Word frequencies": "単語の頻度",
"New account": "新しいアカウント",
"Moved to new account address": "新しいアカウントアドレスに移動しました"
}

View File

@ -347,5 +347,12 @@
"Peertube Instances": "Peertube Instances",
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites.",
"Follows you": "Follows you",
"Verify all signatures": "Verify all signatures"
"Verify all signatures": "Verify all signatures",
"Blocked followers": "Blocked followers",
"Blocked following": "Blocked following",
"Receives posts from the following accounts": "Receives posts from the following accounts",
"Sends out posts to the following accounts": "Sends out posts to the following accounts",
"Word frequencies": "Word frequencies",
"New account": "New account",
"Moved to new account address": "Moved to new account address"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Instâncias Peertube",
"Show video previews for the following Peertube sites.": "Mostrar visualizações de vídeo para os seguintes sites Peertube.",
"Follows you": "Segue você",
"Verify all signatures": "Verifique todas as assinaturas"
"Verify all signatures": "Verifique todas as assinaturas",
"Blocked followers": "Seguidores bloqueados",
"Blocked following": "Seguindo bloqueado",
"Receives posts from the following accounts": "Recebe postagens das seguintes contas",
"Sends out posts to the following accounts": "Envia postagens para as seguintes contas",
"Word frequencies": "Frequências de palavras",
"New account": "Nova conta",
"Moved to new account address": "Movido para o novo endereço da conta"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Экземпляры Peertube",
"Show video previews for the following Peertube sites.": "Показать превью видео для следующих сайтов Peertube.",
"Follows you": "Следует за вами",
"Verify all signatures": "Проверить все подписи"
"Verify all signatures": "Проверить все подписи",
"Blocked followers": "Заблокированные подписчики",
"Blocked following": "Заблокировано подписок",
"Receives posts from the following accounts": "Получает сообщения от следующих аккаунтов",
"Sends out posts to the following accounts": "Отправляет сообщения на следующие аккаунты",
"Word frequencies": "Частоты слов",
"New account": "Новый аккаунт",
"Moved to new account address": "Перемещен на новый адрес учетной записи"
}

View File

@ -351,5 +351,12 @@
"Peertube Instances": "Peertube实例",
"Show video previews for the following Peertube sites.": "显示以下Peertube网站的视频预览。",
"Follows you": "跟着你",
"Verify all signatures": "验证所有签名"
"Verify all signatures": "验证所有签名",
"Blocked followers": "被封锁的追随者",
"Blocked following": "被阻止",
"Receives posts from the following accounts": "从以下帐户接收帖子",
"Sends out posts to the following accounts": "将帖子发送到以下帐户",
"Word frequencies": "词频",
"New account": "新账户",
"Moved to new account address": "移至新帐户地址"
}

View File

@ -47,8 +47,12 @@ def validPostDate(published: str, maxAgeDays=7) -> bool:
daysDiff = datetime.datetime.utcnow() - baselineTime
nowDaysSinceEpoch = daysDiff.days
postTimeObject = \
datetime.datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
try:
postTimeObject = \
datetime.datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
except BaseException:
return False
daysDiff = postTimeObject - baselineTime
postDaysSinceEpoch = daysDiff.days
@ -129,7 +133,7 @@ def isEditor(baseDir: str, nickname: str) -> bool:
def getImageExtensions() -> []:
"""Returns a list of the possible image file extensions
"""
return ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif')
return ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg')
def getVideoExtensions() -> []:
@ -1721,6 +1725,7 @@ def mediaFileMimeType(filename: str) -> str:
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'webp': 'image/webp',
'avif': 'image/avif',
'mp3': 'audio/mpeg',

View File

@ -37,7 +37,9 @@ def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
aboutForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
aboutForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
aboutForm += '<div class="container">' + aboutText + '</div>'
if onionDomain:
aboutForm += \

View File

@ -10,6 +10,7 @@ import os
from datetime import datetime
from datetime import date
from shutil import copyfile
from utils import getConfigParam
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
@ -52,7 +53,9 @@ def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
deletePostStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
deletePostStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
deletePostStr += \
'<center><h1>' + postTime + ' ' + str(year) + '/' + \
str(monthNumber) + \
@ -109,7 +112,9 @@ def _htmlCalendarDay(cssCache: {}, translate: {},
if '/users/' in actor:
calActor = '/users/' + actor.split('/users/')[1]
calendarStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
calendarStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
calendarStr += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \
@ -290,7 +295,9 @@ def htmlCalendar(cssCache: {}, translate: {},
if '/users/' in actor:
calActor = '/users/' + actor.split('/users/')[1]
calendarStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
calendarStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
calendarStr += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \

View File

@ -260,7 +260,9 @@ def htmlLinksMobile(cssCache: {}, baseDir: str,
if ':' in domain:
domain = domain.split(':')[0]
htmlStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain, theme)
htmlStr += \
@ -321,7 +323,9 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str,
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain, theme)
editLinksForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
editLinksForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# top banner
editLinksForm += \

View File

@ -15,6 +15,7 @@ from utils import loadJson
from utils import votesOnNewswireItem
from utils import getNicknameFromActor
from utils import isEditor
from utils import getConfigParam
from posts import isModerator
from webapp_utils import getRightImageFile
from webapp_utils import htmlHeaderWithExternalStyle
@ -342,7 +343,9 @@ def htmlCitations(baseDir: str, nickname: str, domain: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
htmlStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# top banner
bannerFile, bannerFilename = \
@ -452,7 +455,9 @@ def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str,
showPublishButton = editor
htmlStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain, theme)
@ -515,7 +520,9 @@ def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str,
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain, theme)
editNewswireForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
editNewswireForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# top banner
editNewswireForm += \
@ -631,7 +638,9 @@ def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str,
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
editNewsPostForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
editNewsPostForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
editNewsPostForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/newseditdata">\n'

View File

@ -13,6 +13,7 @@ from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
from utils import loadJson
from utils import getConfigParam
from webapp_utils import getAltPath
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
@ -57,7 +58,9 @@ def htmlConfirmDelete(cssCache: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
deletePostStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
deletePostStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
deletePostStr += \
individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
translate, pageNumber,
@ -130,7 +133,9 @@ def htmlConfirmRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
sharesStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
sharesStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
sharesStr += '<div class="follow">\n'
sharesStr += ' <div class="followAvatar">\n'
sharesStr += ' <center>\n'
@ -177,7 +182,9 @@ def htmlConfirmFollow(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
followStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
followStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
followStr += '<div class="follow">\n'
followStr += ' <div class="followAvatar">\n'
followStr += ' <center>\n'
@ -221,7 +228,9 @@ def htmlConfirmUnfollow(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
followStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
followStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
followStr += '<div class="follow">\n'
followStr += ' <div class="followAvatar">\n'
followStr += ' <center>\n'
@ -266,7 +275,9 @@ def htmlConfirmUnblock(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
blockStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
blockStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
blockStr += '<div class="block">\n'
blockStr += ' <div class="blockAvatar">\n'
blockStr += ' <center>\n'

View File

@ -12,6 +12,7 @@ from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import getImageFormats
from utils import getMediaFormats
from utils import getConfigParam
from webapp_utils import getBannerFile
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
@ -555,7 +556,9 @@ def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {},
dateAndLocation += '<input type="text" name="category">\n'
dateAndLocation += '</div>\n'
newPostForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
newPostForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
newPostForm += \
'<header>\n' + \

View File

@ -9,6 +9,7 @@ __status__ = "Production"
import os
from utils import isSystemAccount
from utils import getDomainFromActor
from utils import getConfigParam
from person import personBoxJson
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
@ -170,7 +171,9 @@ def htmlFrontScreen(rssIconAtTop: bool,
profileFooterStr += ' </tbody>\n'
profileFooterStr += '</table>\n'
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
profileStr = \
htmlHeaderWithExternalStyle(cssFilename) + \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
profileStr + profileFooterStr + htmlFooter()
return profileStr

View File

@ -10,6 +10,7 @@ import os
from shutil import copyfile
from datetime import datetime
from utils import getNicknameFromActor
from utils import getConfigParam
from categories import getHashtagCategories
from categories import getHashtagCategory
from webapp_utils import getSearchBannerFile
@ -246,7 +247,9 @@ def htmlSearchHashtagCategory(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/search.css'):
cssFilename = baseDir + '/search.css'
htmlStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# show a banner above the search box
searchBannerFile, searchBannerFilename = \

View File

@ -61,6 +61,9 @@ def htmlLogin(cssCache: {}, translate: {},
elif os.path.isfile(baseDir + '/accounts/login.gif'):
loginImage = 'login.gif'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.svg'):
loginImage = 'login.svg'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.webp'):
loginImage = 'login.webp'
loginImageFilename = baseDir + '/accounts/' + loginImage
@ -129,7 +132,9 @@ def htmlLogin(cssCache: {}, translate: {},
if not autocomplete:
autocompleteStr = 'autocomplete="off" value=""'
loginForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
loginForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
loginForm += '<br>\n'
loginForm += '<form method="POST" action="/login">\n'
loginForm += ' <div class="imgcontainer">\n'

View File

@ -7,10 +7,13 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from utils import getFullDomain
from utils import isEditor
from utils import loadJson
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import getConfigParam
from posts import downloadFollowCollection
from posts import getPublicPostInfo
from posts import isModerator
from webapp_timeline import htmlTimeline
@ -19,6 +22,8 @@ from webapp_utils import getContentWarningButton
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from blocking import isBlockedDomain
from blocking import isBlocked
from session import createSession
def htmlModeration(cssCache: {}, defaultTimeline: str,
@ -70,31 +75,65 @@ def htmlAccountInfo(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
infoForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
searchNickname = getNicknameFromActor(searchHandle)
searchDomain, searchPort = getDomainFromActor(searchHandle)
searchHandle = searchNickname + '@' + searchDomain
searchActor = \
httpPrefix + '://' + searchDomain + '/users/' + searchNickname
infoForm += \
'<center><h1><a href="/users/' + nickname + '/moderation">' + \
translate['Account Information'] + ':</a> <a href="' + \
httpPrefix + '://' + searchDomain + '/users/' + searchNickname + \
'">' + searchHandle + '</a></h1><br>'
translate['Account Information'] + ':</a> <a href="' + searchActor + \
'">' + searchHandle + '</a></h1><br>\n'
infoForm += translate[msgStr1] + '</center><br><br>'
infoForm += translate[msgStr1] + '</center><br><br>\n'
proxyType = 'tor'
if not os.path.isfile('/usr/bin/tor'):
proxyType = None
if domain.endswith('.i2p'):
proxyType = None
domainDict = getPublicPostInfo(None,
session = createSession(proxyType)
wordFrequency = {}
domainDict = getPublicPostInfo(session,
baseDir, searchNickname, searchDomain,
proxyType, searchPort,
httpPrefix, debug,
__version__)
infoForm += '<div class="accountInfoDomains">'
__version__, wordFrequency)
# get a list of any blocked followers
followersList = \
downloadFollowCollection('followers', session,
httpPrefix, searchActor, 1, 5)
blockedFollowers = []
for followerActor in followersList:
followerNickname = getNicknameFromActor(followerActor)
followerDomain, followerPort = getDomainFromActor(followerActor)
followerDomainFull = getFullDomain(followerDomain, followerPort)
if isBlocked(baseDir, nickname, domain,
followerNickname, followerDomainFull):
blockedFollowers.append(followerActor)
# get a list of any blocked following
followingList = \
downloadFollowCollection('following', session,
httpPrefix, searchActor, 1, 5)
blockedFollowing = []
for followingActor in followingList:
followingNickname = getNicknameFromActor(followingActor)
followingDomain, followingPort = getDomainFromActor(followingActor)
followingDomainFull = getFullDomain(followingDomain, followingPort)
if isBlocked(baseDir, nickname, domain,
followingNickname, followingDomainFull):
blockedFollowing.append(followingActor)
infoForm += '<div class="accountInfoDomains">\n'
usersPath = '/users/' + nickname + '/accountinfo'
ctr = 1
for postDomain, blockedPostUrls in domainDict.items():
@ -122,7 +161,7 @@ def htmlAccountInfo(cssCache: {}, translate: {},
'?handle=' + searchHandle + '">'
infoForm += '<button class="buttonhighlighted"><span>' + \
translate['Unblock'] + '</span></button></a> ' + \
blockedPostsHtml
blockedPostsHtml + '\n'
else:
infoForm += \
'<a href="' + usersPath + '?blockdomain=' + postDomain + \
@ -130,10 +169,71 @@ def htmlAccountInfo(cssCache: {}, translate: {},
if postDomain != domain:
infoForm += '<button class="button"><span>' + \
translate['Block'] + '</span></button>'
infoForm += '</a>'
infoForm += '<br>'
infoForm += '</a>\n'
infoForm += '<br>\n'
infoForm += '</div>\n'
if blockedFollowing:
blockedFollowing.sort()
infoForm += '<div class="accountInfoDomains">\n'
infoForm += '<h1>' + translate['Blocked following'] + '</h1>\n'
infoForm += \
'<p>' + \
translate['Receives posts from the following accounts'] + \
':</p>\n'
for actor in blockedFollowing:
followingNickname = getNicknameFromActor(actor)
followingDomain, followingPort = getDomainFromActor(actor)
followingDomainFull = \
getFullDomain(followingDomain, followingPort)
infoForm += '<a href="' + actor + '">' + \
followingNickname + '@' + followingDomainFull + \
'</a><br><br>\n'
infoForm += '</div>\n'
if blockedFollowers:
blockedFollowers.sort()
infoForm += '<div class="accountInfoDomains">\n'
infoForm += '<h1>' + translate['Blocked followers'] + '</h1>\n'
infoForm += \
'<p>' + \
translate['Sends out posts to the following accounts'] + \
':</p>\n'
for actor in blockedFollowers:
followerNickname = getNicknameFromActor(actor)
followerDomain, followerPort = getDomainFromActor(actor)
followerDomainFull = getFullDomain(followerDomain, followerPort)
infoForm += '<a href="' + actor + '">' + \
followerNickname + '@' + followerDomainFull + '</a><br><br>\n'
infoForm += '</div>\n'
if wordFrequency:
maxCount = 1
for word, count in wordFrequency.items():
if count > maxCount:
maxCount = count
minimumWordCount = int(maxCount / 2)
if minimumWordCount >= 3:
infoForm += '<div class="accountInfoDomains">\n'
infoForm += '<h1>' + translate['Word frequencies'] + '</h1>\n'
wordSwarm = ''
ctr = 0
for word, count in wordFrequency.items():
if count >= minimumWordCount:
if ctr > 0:
wordSwarm += ' '
if count < maxCount - int(maxCount / 4):
wordSwarm += word
else:
if count != maxCount:
wordSwarm += '<b>' + word + '</b>'
else:
wordSwarm += '<b><i>' + word + '</i></b>'
ctr += 1
infoForm += wordSwarm
infoForm += '</div>\n'
infoForm += '</div>'
infoForm += htmlFooter()
return infoForm
@ -151,7 +251,9 @@ def htmlModerationInfo(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
infoForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
infoForm += \
'<center><h1><a href="/users/' + nickname + '/moderation">' + \

View File

@ -47,7 +47,8 @@ def htmlPersonOptions(defaultTimeline: str,
emailAddress: str,
dormantMonths: int,
backToPath: str,
lockedAccount: bool) -> str:
lockedAccount: bool,
movedTo: str) -> str:
"""Show options for a person: view/follow/block/report
"""
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
@ -109,7 +110,9 @@ def htmlPersonOptions(defaultTimeline: str,
'"><button class="button" name="submitDonate">' + \
translate['Donate'] + '</button></a>\n'
optionsStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
optionsStr += '<br><br>\n'
optionsStr += '<div class="options">\n'
optionsStr += ' <div class="optionsAvatar">\n'
@ -121,6 +124,8 @@ def htmlPersonOptions(defaultTimeline: str,
handleShown = handle
if lockedAccount:
handleShown += '🔒'
if movedTo:
handleShown += ''
if dormant:
handleShown += ' 💤'
optionsStr += \
@ -129,6 +134,15 @@ def htmlPersonOptions(defaultTimeline: str,
if followsYou:
optionsStr += \
' <p class="optionsText">' + translate['Follows you'] + '</p>\n'
if movedTo:
newNickname = getNicknameFromActor(movedTo)
newDomain, newPort = getDomainFromActor(movedTo)
if newNickname and newDomain:
newHandle = newNickname + '@' + newDomain
optionsStr += \
' <p class="optionsText">' + \
translate['New account'] + \
': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
if emailAddress:
optionsStr += \
'<p class="imText">' + translate['Email'] + \

View File

@ -22,6 +22,7 @@ from posts import getPersonBox
from posts import isDM
from posts import downloadAnnounce
from posts import populateRepliesJson
from utils import getConfigParam
from utils import getFullDomain
from utils import isEditor
from utils import locatePost
@ -61,6 +62,7 @@ from webapp_utils import getBrokenLinkSubstitute
from webapp_media import addEmbeddedElements
from webapp_question import insertQuestion
from devices import E2EEdecryptMessageFromDevice
from webfinger import webfingerHandle
def _logPostTiming(enableTimingLog: bool, postStartTime, debugId: str) -> None:
@ -1153,14 +1155,29 @@ def individualPostAsHtml(allowDownloads: bool,
# get the display name
if domainFull not in postActor:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session,
cachedWebfingers,
personCache,
projectVersion, httpPrefix,
nickname, domain, 'outbox',
72367)
# lookup the correct webfinger for the postActor
postActorNickname = getNicknameFromActor(postActor)
postActorDomain, postActorPort = getDomainFromActor(postActor)
postActorDomainFull = getFullDomain(postActorDomain, postActorPort)
postActorHandle = postActorNickname + '@' + postActorDomainFull
postActorWf = \
webfingerHandle(session, postActorHandle, httpPrefix,
cachedWebfingers,
domain, __version__)
avatarUrl2 = None
displayName = None
if postActorWf:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session,
postActorWf,
personCache,
projectVersion,
httpPrefix,
nickname, domain,
'outbox', 72367)
_logPostTiming(enableTimingLog, postStartTime, '6')
if avatarUrl2:
@ -1689,7 +1706,10 @@ def htmlIndividualPost(cssCache: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
return htmlHeaderWithExternalStyle(cssFilename) + postStr + htmlFooter()
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
postStr + htmlFooter()
def htmlPostReplies(cssCache: {},
@ -1724,4 +1744,7 @@ def htmlPostReplies(cssCache: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
return htmlHeaderWithExternalStyle(cssFilename) + repliesStr + htmlFooter()
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
repliesStr + htmlFooter()

View File

@ -170,6 +170,10 @@ def htmlProfileAfterSearch(cssCache: {},
lockedAccount = getLockedAccount(profileJson)
if lockedAccount:
displayName += '🔒'
movedTo = ''
if profileJson.get('movedTo'):
movedTo = profileJson['movedTo']
displayName += ''
followsYou = \
isFollowerOfPerson(baseDir,
@ -233,7 +237,8 @@ def htmlProfileAfterSearch(cssCache: {},
translate,
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl)
avatarUrl, imageUrl,
movedTo)
domainFull = getFullDomain(domain, port)
@ -288,7 +293,10 @@ def htmlProfileAfterSearch(cssCache: {},
if i >= 20:
break
return htmlHeaderWithExternalStyle(cssFilename) + profileStr + htmlFooter()
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
profileStr + htmlFooter()
def _getProfileHeader(baseDir: str, nickname: str, domain: str,
@ -298,7 +306,7 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
avatarDescription: str,
profileDescriptionShort: str,
loginButton: str, avatarUrl: str,
theme: str) -> str:
theme: str, movedTo: str) -> str:
"""The header of the profile screen, containing background
image and avatar
"""
@ -318,6 +326,15 @@ def _getProfileHeader(baseDir: str, nickname: str, domain: str,
htmlStr += ' <h1>' + displayName + '</h1>\n'
htmlStr += \
' <p><b>@' + nickname + '@' + domainFull + '</b><br>\n'
if movedTo:
newNickname = getNicknameFromActor(movedTo)
newDomain, newPort = getDomainFromActor(movedTo)
newDomainFull = getFullDomain(newDomain, newPort)
if newNickname and newDomain:
htmlStr += \
' <p>' + translate['New account'] + ': ' + \
'<a href="' + movedTo + '">@' + \
newNickname + '@' + newDomainFull + '</a><br>\n'
htmlStr += \
' <a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
@ -339,7 +356,8 @@ def _getProfileHeaderAfterSearch(baseDir: str,
displayName: str,
followsYou: bool,
profileDescriptionShort: str,
avatarUrl: str, imageUrl: str) -> str:
avatarUrl: str, imageUrl: str,
movedTo: str) -> str:
"""The header of a searched for handle, containing background
image and avatar
"""
@ -362,6 +380,15 @@ def _getProfileHeaderAfterSearch(baseDir: str,
' <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
if followsYou:
htmlStr += ' <p><b>' + translate['Follows you'] + '</b></p>\n'
if movedTo:
newNickname = getNicknameFromActor(movedTo)
newDomain, newPort = getDomainFromActor(movedTo)
newDomainFull = getFullDomain(newDomain, newPort)
if newNickname and newDomain:
newHandle = newNickname + '@' + newDomainFull
htmlStr += ' <p>' + translate['New account'] + \
': < a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
htmlStr += ' <p>' + profileDescriptionShort + '</p>\n'
htmlStr += ' </figcaption>\n'
htmlStr += ' </figure>\n\n'
@ -585,6 +612,10 @@ def htmlProfile(rssIconAtTop: bool,
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
movedTo = ''
if profileJson.get('movedTo'):
movedTo = profileJson['movedTo']
avatarUrl = profileJson['icon']['url']
profileHeaderStr = \
_getProfileHeader(baseDir, nickname, domain,
@ -592,7 +623,8 @@ def htmlProfile(rssIconAtTop: bool,
defaultTimeline, displayName,
avatarDescription,
profileDescriptionShort,
loginButton, avatarUrl, theme)
loginButton, avatarUrl, theme,
movedTo)
profileStr = profileHeaderStr + donateSection
profileStr += '<div class="container" id="buttonheader">\n'
@ -680,8 +712,10 @@ def htmlProfile(rssIconAtTop: bool,
nickname, domainFull,
extraJson) + licenseStr
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
profileStr = \
htmlHeaderWithExternalStyle(cssFilename) + \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
profileStr + htmlFooter()
return profileStr
@ -898,8 +932,11 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
toxAddress = ''
briarAddress = ''
manuallyApprovesFollowers = ''
movedTo = ''
actorJson = loadJson(actorFilename)
if actorJson:
if actorJson.get('movedTo'):
movedTo = actorJson['movedTo']
donateUrl = getDonationUrl(actorJson)
xmppAddress = getXmppAddress(actorJson)
matrixAddress = getMatrixAddress(actorJson)
@ -1196,7 +1233,9 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
'style="height:200px">' + peertubeInstancesStr + \
'</textarea>\n'
editProfileForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
editProfileForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# top banner
editProfileForm += \
@ -1240,6 +1279,12 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
editProfileForm += \
' <textarea id="message" name="bio" style="height:200px">' + \
bioStr + '</textarea>\n'
editProfileForm += '<label class="labels">' + \
translate['Moved to new account address'] + ':</label><br>\n'
editProfileForm += \
' <input type="text" placeholder="https://..." ' + \
'name="movedTo" value="' + movedTo + '">\n'
editProfileForm += '<label class="labels">' + \
translate['Donations link'] + '</label><br>\n'
editProfileForm += \
@ -1521,26 +1566,35 @@ def _individualFollowAsHtml(translate: {},
buttons=[]) -> str:
"""An individual follow entry on the profile screen
"""
nickname = getNicknameFromActor(followUrl)
domain, port = getDomainFromActor(followUrl)
titleStr = '@' + nickname + '@' + domain
if dormant:
titleStr += ' 💤'
followUrlNickname = getNicknameFromActor(followUrl)
followUrlDomain, followUrlPort = getDomainFromActor(followUrl)
followUrlDomainFull = getFullDomain(followUrlDomain, followUrlPort)
titleStr = '@' + followUrlNickname + '@' + followUrlDomainFull
avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True)
if not avatarUrl:
avatarUrl = followUrl + '/avatar.png'
if domain not in followUrl:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session,
cachedWebfingers,
personCache, projectVersion,
httpPrefix, nickname,
domain, 'outbox', 43036)
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
titleStr = displayName + ' ' + titleStr
# lookup the correct webfinger for the followUrl
followUrlHandle = followUrlNickname + '@' + followUrlDomainFull
followUrlWf = \
webfingerHandle(session, followUrlHandle, httpPrefix,
cachedWebfingers,
domain, __version__)
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session,
followUrlWf,
personCache, projectVersion,
httpPrefix, followUrlNickname,
domain, 'outbox', 43036)
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
titleStr = displayName
if dormant:
titleStr += ' 💤'
buttonsStr = ''
if authorized:

View File

@ -10,6 +10,7 @@ import os
from shutil import copyfile
import urllib.parse
from datetime import datetime
from utils import getConfigParam
from utils import getFullDomain
from utils import isEditor
from utils import loadJson
@ -50,7 +51,9 @@ def htmlSearchEmoji(cssCache: {}, translate: {},
emojiLookupFilename = baseDir + '/emoji/emoji.json'
# create header
emojiForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
emojiForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
emojiForm += '<center><h1>' + \
translate['Emoji Search'] + \
'</h1></center>'
@ -110,8 +113,10 @@ def htmlSearchSharedItems(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
sharedItemsForm = \
htmlHeaderWithExternalStyle(cssFilename)
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
sharedItemsForm += \
'<center><h1>' + \
'<a href="' + actor + '/search">' + \
@ -285,7 +290,9 @@ def htmlSearchEmojiTextEntry(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
emojiStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
emojiStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
emojiStr += '<div class="follow">\n'
emojiStr += ' <div class="followAvatar">\n'
emojiStr += ' <center>\n'
@ -325,7 +332,9 @@ def htmlSearch(cssCache: {}, translate: {},
if os.path.isfile(baseDir + '/search.css'):
cssFilename = baseDir + '/search.css'
followStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
followStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# show a banner above the search box
searchBannerFile, searchBannerFilename = \
@ -459,7 +468,9 @@ def htmlSkillsSearch(actor: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
skillSearchForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
skillSearchForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
skillSearchForm += \
'<center><h1><a href = "' + actor + '/search">' + \
translate['Skills search'] + ': ' + \
@ -525,8 +536,10 @@ def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
historySearchForm = \
htmlHeaderWithExternalStyle(cssFilename)
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
# add the page title
domainFull = getFullDomain(domain, port)
@ -650,8 +663,10 @@ def htmlHashtagSearch(cssCache: {},
endIndex = noOfLines - 1
# add the page title
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
hashtagSearchForm = \
htmlHeaderWithExternalStyle(cssFilename)
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
if nickname:
hashtagSearchForm += '<center>\n' + \
'<h1><a href="/users/' + nickname + '/search">#' + \

View File

@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from utils import getConfigParam
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
@ -19,7 +20,9 @@ def htmlSuspended(cssCache: {}, baseDir: str) -> str:
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
suspendedForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
suspendedForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
suspendedForm += '<div><center>\n'
suspendedForm += ' <p class="screentitle">Account Suspended</p>\n'
suspendedForm += ' <p>See <a href="/terms">Terms of Service</a></p>\n'

View File

@ -8,6 +8,7 @@ __status__ = "Production"
import os
import time
from utils import getConfigParam
from utils import getFullDomain
from utils import isEditor
from utils import removeIdEnding
@ -255,7 +256,9 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
eventsButton + '"><span>' + translate['Events'] + \
'</span></button></a>'
tlStr = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
tlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
_logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '4')

View File

@ -37,7 +37,9 @@ def htmlTermsOfService(cssCache: {}, baseDir: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
TOSForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
TOSForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
TOSForm += '<div class="container">' + TOSText + '</div>\n'
if adminNickname:
adminActor = httpPrefix + '://' + domainFull + \

View File

@ -42,7 +42,10 @@ def htmlFollowingList(cssCache: {}, baseDir: str,
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
followingListHtml = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
followingListHtml = htmlHeaderWithExternalStyle(cssFilename,
instanceTitle)
for followingAddress in followingList:
if followingAddress:
followingListHtml += \
@ -61,7 +64,10 @@ def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str:
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
blockedHashtagForm = htmlHeaderWithExternalStyle(cssFilename)
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
blockedHashtagForm = htmlHeaderWithExternalStyle(cssFilename,
instanceTitle)
blockedHashtagForm += '<div><center>\n'
blockedHashtagForm += \
' <p class="screentitle">' + \
@ -289,6 +295,7 @@ def updateAvatarImageCache(session, baseDir: str, httpPrefix: str,
'jpg': 'jpeg',
'jpeg': 'jpeg',
'gif': 'gif',
'svg': 'svg+xml',
'webp': 'webp',
'avif': 'avif'
}
@ -521,7 +528,8 @@ def getRightImageFile(baseDir: str,
nickname, domain, theme)
def htmlHeaderWithExternalStyle(cssFilename: str, lang='en') -> str:
def htmlHeaderWithExternalStyle(cssFilename: str, instanceTitle: str,
lang='en') -> str:
htmlStr = '<!DOCTYPE html>\n'
htmlStr += '<html lang="' + lang + '">\n'
htmlStr += ' <head>\n'
@ -530,7 +538,7 @@ def htmlHeaderWithExternalStyle(cssFilename: str, lang='en') -> str:
htmlStr += ' <link rel="stylesheet" href="' + cssFile + '">\n'
htmlStr += ' <link rel="manifest" href="/manifest.json">\n'
htmlStr += ' <meta name="theme-color" content="grey">\n'
htmlStr += ' <title>Epicyon</title>\n'
htmlStr += ' <title>' + instanceTitle + '</title>\n'
htmlStr += ' </head>\n'
htmlStr += ' <body>\n'
return htmlStr
@ -646,12 +654,14 @@ def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
mediaType == 'image/jpeg' or \
mediaType == 'image/webp' or \
mediaType == 'image/avif' or \
mediaType == 'image/svg+xml' or \
mediaType == 'image/gif':
if attach['url'].endswith('.png') or \
attach['url'].endswith('.jpg') or \
attach['url'].endswith('.jpeg') or \
attach['url'].endswith('.webp') or \
attach['url'].endswith('.avif') or \
attach['url'].endswith('.svg') or \
attach['url'].endswith('.gif'):
if attachmentCtr > 0:
attachmentStr += '<br>'

View File

@ -352,4 +352,4 @@ def webfingerUpdate(baseDir: str, nickname: str, domain: str,
if _webfingerUpdateFromProfile(wfJson, actorJson):
if saveJson(wfJson, filename):
cachedWebfingers[handle] = wfJson
storeWebfingerInCache(handle, wfJson, cachedWebfingers)