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

main
Bob Mottram 2020-09-30 09:41:45 +01:00
commit f799517268
44 changed files with 753 additions and 1198 deletions

View File

@ -8,7 +8,7 @@ Add issues on https://gitlab.com/bashrc2/epicyon/-/issues
Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend. Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend.
[Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Object Capabilities](ocaps.md) - [Code of Conduct](code-of-conduct.md) [Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Code of Conduct](code-of-conduct.md)
Matrix room: **#epicyon:matrix.freedombone.net** Matrix room: **#epicyon:matrix.freedombone.net**

View File

@ -15,7 +15,6 @@
* http signatures and basic auth * http signatures and basic auth
* Compatible with http (onion addresses), https and dat * Compatible with http (onion addresses), https and dat
* Minimal dependencies. * Minimal dependencies.
* Capabilities based security
* Support image blurhashes * Support image blurhashes
* Data minimization principle. Configurable post expiry time * Data minimization principle. Configurable post expiry time
* Likes and repeats only visible to authorized viewers * Likes and repeats only visible to authorized viewers

View File

@ -7,8 +7,6 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import os import os
from capabilities import capabilitiesAccept
from capabilities import capabilitiesGrantedSave
from utils import urlPermitted from utils import urlPermitted
from utils import getDomainFromActor from utils import getDomainFromActor
from utils import getNicknameFromActor from utils import getNicknameFromActor
@ -19,7 +17,7 @@ from utils import followPerson
def createAcceptReject(baseDir: str, federationList: [], def createAcceptReject(baseDir: str, federationList: [],
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str, toUrl: str, ccUrl: str, httpPrefix: str,
objectJson: {}, ocapJson, acceptType: str) -> {}: objectJson: {}, acceptType: str) -> {}:
"""Accepts or rejects something (eg. a follow request or offer) """Accepts or rejects something (eg. a follow request or offer)
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
and ccUrl might be a specific person favorited or repeated and and ccUrl might be a specific person favorited or repeated and
@ -29,7 +27,7 @@ def createAcceptReject(baseDir: str, federationList: [],
if not objectJson.get('actor'): if not objectJson.get('actor'):
return None return None
if not urlPermitted(objectJson['actor'], federationList, "inbox:write"): if not urlPermitted(objectJson['actor'], federationList):
return None return None
if port: if port:
@ -48,25 +46,17 @@ def createAcceptReject(baseDir: str, federationList: [],
if ccUrl: if ccUrl:
if len(ccUrl) > 0: if len(ccUrl) > 0:
newAccept['cc'] = [ccUrl] newAccept['cc'] = [ccUrl]
# attach capabilities for follow accept
if ocapJson:
newAccept['capabilities'] = ocapJson
return newAccept return newAccept
def createAccept(baseDir: str, federationList: [], def createAccept(baseDir: str, federationList: [],
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str, toUrl: str, ccUrl: str, httpPrefix: str,
objectJson: {}, objectJson: {}) -> {}:
acceptedCaps=["inbox:write", "objects:read"]) -> {}:
# create capabilities accept
ocapNew = capabilitiesAccept(baseDir, httpPrefix,
nickname, domain, port,
toUrl, True, acceptedCaps)
return createAcceptReject(baseDir, federationList, return createAcceptReject(baseDir, federationList,
nickname, domain, port, nickname, domain, port,
toUrl, ccUrl, httpPrefix, toUrl, ccUrl, httpPrefix,
objectJson, ocapNew, 'Accept') objectJson, 'Accept')
def createReject(baseDir: str, federationList: [], def createReject(baseDir: str, federationList: [],
@ -154,13 +144,6 @@ def acceptFollow(baseDir: str, domain: str, messageJson: {},
if acceptedPort: if acceptedPort:
acceptedDomainFull = acceptedDomain + ':' + str(acceptedPort) acceptedDomainFull = acceptedDomain + ':' + str(acceptedPort)
# are capabilities attached? If so then store them
if messageJson.get('capabilities'):
if isinstance(messageJson['capabilities'], dict):
capabilitiesGrantedSave(baseDir,
nickname, acceptedDomainFull,
messageJson['capabilities'])
# has this person already been unfollowed? # has this person already been unfollowed?
unfollowedFilename = baseDir + '/accounts/' + \ unfollowedFilename = baseDir + '/accounts/' + \
nickname + '@' + acceptedDomainFull + '/unfollowed.txt' nickname + '@' + acceptedDomainFull + '/unfollowed.txt'

View File

@ -108,7 +108,7 @@ def createAnnounce(session, baseDir: str, federationList: [],
followers url objectUrl is typically the url of the message, followers url objectUrl is typically the url of the message,
corresponding to url or atomUri in createPostBase corresponding to url or atomUri in createPostBase
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
if ':' in domain: if ':' in domain:
@ -231,7 +231,7 @@ def undoAnnounce(session, baseDir: str, federationList: [],
objectUrl is typically the url of the message which was repeated, objectUrl is typically the url of the message which was repeated,
corresponding to url or atomUri in createPostBase corresponding to url or atomUri in createPostBase
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
if ':' in domain: if ':' in domain:
@ -391,8 +391,8 @@ def sendAnnounceViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId, (inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, capabilityAcquisition, sharedInbox, avatarUrl,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
fromNickname, fromDomain, fromNickname, fromDomain,
@ -414,8 +414,7 @@ def sendAnnounceViaServer(baseDir: str, session,
'Content-type': 'application/json', 'Content-type': 'application/json',
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newAnnounceJson, [], inboxUrl, postResult = postJson(session, newAnnounceJson, [], inboxUrl, headers)
headers, "inbox:write")
if not postResult: if not postResult:
print('WARN: Announce not posted') print('WARN: Announce not posted')

View File

@ -123,7 +123,6 @@ def sendAvailabilityViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, nickname, httpPrefix, nickname,
@ -146,7 +145,7 @@ def sendAvailabilityViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newAvailabilityJson, [], postResult = postJson(session, newAvailabilityJson, [],
inboxUrl, headers, "inbox:write") inboxUrl, headers)
if not postResult: if not postResult:
print('WARN: failed to post availability') print('WARN: failed to post availability')

12
blog.py
View File

@ -282,7 +282,8 @@ def htmlBlogPostRSS2(authorized: bool,
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/') messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
if not restrictToDomain or \ if not restrictToDomain or \
(restrictToDomain and '/' + domain in messageLink): (restrictToDomain and '/' + domain in messageLink):
if postJsonObject['object'].get('summary'): if postJsonObject['object'].get('summary') and \
postJsonObject['object'].get('published'):
published = postJsonObject['object']['published'] published = postJsonObject['object']['published']
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
titleStr = postJsonObject['object']['summary'] titleStr = postJsonObject['object']['summary']
@ -307,7 +308,8 @@ def htmlBlogPostRSS3(authorized: bool,
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/') messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
if not restrictToDomain or \ if not restrictToDomain or \
(restrictToDomain and '/' + domain in messageLink): (restrictToDomain and '/' + domain in messageLink):
if postJsonObject['object'].get('summary'): if postJsonObject['object'].get('summary') and \
postJsonObject['object'].get('published'):
published = postJsonObject['object']['published'] published = postJsonObject['object']['published']
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
titleStr = postJsonObject['object']['summary'] titleStr = postJsonObject['object']['summary']
@ -358,13 +360,15 @@ def htmlBlogPost(authorized: bool,
blogStr += '<a href="' + httpPrefix + '://' + \ blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.xml">' domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \ blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \ 'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a>' iconsDir + '/rss.png" /></a>'
blogStr += '<a href="' + httpPrefix + '://' + \ blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.txt">' domainFull + '/blog/' + nickname + '/rss.txt">'
blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \ blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 3.0" ' + \
'title="RSS 3.0" src="/' + \ 'title="RSS 3.0" src="/' + \
iconsDir + '/rss3.png" /></a>' iconsDir + '/rss3.png" /></a>'

View File

@ -234,7 +234,7 @@ def bookmark(recentPostsCache: {},
'to' might be a specific person (actor) whose post was bookmarked 'to' might be a specific person (actor) whose post was bookmarked
object is typically the url of the message which was bookmarked object is typically the url of the message which was bookmarked
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
fullDomain = domain fullDomain = domain
@ -330,7 +330,7 @@ def undoBookmark(recentPostsCache: {},
'to' might be a specific person (actor) whose post was bookmarked 'to' might be a specific person (actor) whose post was bookmarked
object is typically the url of the message which was bookmarked object is typically the url of the message which was bookmarked
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
fullDomain = domain fullDomain = domain
@ -457,8 +457,7 @@ def sendBookmarkViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
@ -480,7 +479,7 @@ def sendBookmarkViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newBookmarkJson, [], postResult = postJson(session, newBookmarkJson, [],
inboxUrl, headers, "inbox:write") inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
@ -539,8 +538,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
@ -562,7 +560,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newUndoBookmarkJson, [], postResult = postJson(session, newUndoBookmarkJson, [],
inboxUrl, headers, "inbox:write") inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)

View File

@ -1,283 +0,0 @@
__filename__ = "capabilities.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from auth import createPassword
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import loadJson
from utils import saveJson
def getOcapFilename(baseDir: str,
nickname: str, domain: str,
actor: str, subdir: str) -> str:
"""Returns the filename for a particular capability accepted or granted
Also creates directories as needed
"""
if not actor:
return None
if ':' in domain:
domain = domain.split(':')[0]
if not os.path.isdir(baseDir + '/accounts'):
os.mkdir(baseDir + '/accounts')
ocDir = baseDir + '/accounts/' + nickname + '@' + domain
if not os.path.isdir(ocDir):
os.mkdir(ocDir)
ocDir = baseDir + '/accounts/' + nickname + '@' + domain + '/ocap'
if not os.path.isdir(ocDir):
os.mkdir(ocDir)
ocDir = baseDir + '/accounts/' + \
nickname + '@' + domain + '/ocap/' + subdir
if not os.path.isdir(ocDir):
os.mkdir(ocDir)
return baseDir + '/accounts/' + \
nickname + '@' + domain + '/ocap/' + \
subdir + '/' + actor.replace('/', '#') + '.json'
def CapablePost(postJson: {}, capabilityList: [], debug: bool) -> bool:
"""Determines whether a post arriving in the inbox
should be accepted accoring to the list of capabilities
"""
if postJson.get('type'):
# No announces/repeats
if postJson['type'] == 'Announce':
if 'inbox:noannounce' in capabilityList:
if debug:
print('DEBUG: ' +
'inbox post rejected because inbox:noannounce')
return False
# No likes
if postJson['type'] == 'Like':
if 'inbox:nolike' in capabilityList:
if debug:
print('DEBUG: ' +
'inbox post rejected because inbox:nolike')
return False
if postJson['type'] == 'Create':
if postJson.get('object'):
# Does this have a reply?
if postJson['object'].get('inReplyTo'):
if postJson['object']['inReplyTo']:
if 'inbox:noreply' in capabilityList:
if debug:
print('DEBUG: ' +
'inbox post rejected because ' +
'inbox:noreply')
return False
# are content warnings enforced?
if postJson['object'].get('sensitive'):
if not postJson['object']['sensitive']:
if 'inbox:cw' in capabilityList:
if debug:
print('DEBUG: ' +
'inbox post rejected because inbox:cw')
return False
# content warning must have non-zero summary
if postJson['object'].get('summary'):
if len(postJson['object']['summary']) < 2:
if 'inbox:cw' in capabilityList:
if debug:
print('DEBUG: ' +
'inbox post rejected because ' +
'inbox:cw, summary missing')
return False
if 'inbox:write' in capabilityList:
return True
return True
def capabilitiesRequest(baseDir: str, httpPrefix: str, domain: str,
requestedActor: str, requestedDomain: str,
requestedCaps=["inbox:write", "objects:read"]) -> {}:
# This is sent to the capabilities endpoint /caps/new
# which could be instance wide or for a particular person
# This could also be added to a follow activity
ocapId = createPassword(32)
ocapRequest = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": httpPrefix + "://" + requestedDomain + "/caps/request/" + ocapId,
"type": "Request",
"capability": requestedCaps,
"actor": requestedActor
}
return ocapRequest
def capabilitiesAccept(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
acceptedActor: str, saveToFile: bool,
acceptedCaps=["inbox:write", "objects:read"]) -> {}:
# This gets returned to capabilities requester
# This could also be added to a follow Accept activity
# reject excessively long actors
if len(acceptedActor) > 256:
return None
fullDomain = domain
if port:
if port != 80 and port != 443:
if ':' not in domain:
fullDomain = domain + ':' + str(port)
# make directories to store capabilities
ocapFilename = \
getOcapFilename(baseDir, nickname, fullDomain, acceptedActor, 'accept')
if not ocapFilename:
return None
ocapAccept = None
# if the capability already exists then load it from file
if os.path.isfile(ocapFilename):
ocapAccept = loadJson(ocapFilename)
# otherwise create a new capability
if not ocapAccept:
acceptedActorNickname = getNicknameFromActor(acceptedActor)
if not acceptedActorNickname:
print('WARN: unable to find nickname in ' + acceptedActor)
return None
acceptedActorDomain, acceptedActorPort = \
getDomainFromActor(acceptedActor)
if acceptedActorPort:
ocapId = acceptedActorNickname + '@' + acceptedActorDomain + \
':' + str(acceptedActorPort) + '#'+createPassword(32)
else:
ocapId = acceptedActorNickname + '@' + acceptedActorDomain + \
'#' + createPassword(32)
ocapAccept = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": httpPrefix + "://" + fullDomain + "/caps/" + ocapId,
"type": "Capability",
"capability": acceptedCaps,
"scope": acceptedActor,
"actor": httpPrefix + "://" + fullDomain
}
if nickname:
ocapAccept['actor'] = \
httpPrefix + "://" + fullDomain + '/users/' + nickname
if saveToFile:
saveJson(ocapAccept, ocapFilename)
return ocapAccept
def capabilitiesGrantedSave(baseDir: str,
nickname: str, domain: str, ocap: {}) -> bool:
"""A capabilities accept is received, so stor it for
reference when sending to the actor
"""
if not ocap.get('actor'):
return False
ocapFilename = \
getOcapFilename(baseDir, nickname, domain, ocap['actor'], 'granted')
if not ocapFilename:
return False
saveJson(ocap, ocapFilename)
return True
def capabilitiesUpdate(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
updateActor: str,
updateCaps: []) -> {}:
"""Used to sends an update for a change of object capabilities
Note that the capability id gets changed with a new random token
so that the old capabilities can't continue to be used
"""
# reject excessively long actors
if len(updateActor) > 256:
return None
fullDomain = domain
if port:
if port != 80 and port != 443:
if ':' not in domain:
fullDomain = domain + ':' + str(port)
# Get the filename of the capability
ocapFilename = \
getOcapFilename(baseDir, nickname, fullDomain, updateActor, 'accept')
if not ocapFilename:
return None
# The capability should already exist for it to be updated
if not os.path.isfile(ocapFilename):
return None
# create an update activity
ocapUpdate = {
"@context": "https://www.w3.org/ns/activitystreams",
'type': 'Update',
'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
'to': [updateActor],
'cc': [],
'object': {}
}
# read the existing capability
ocapJson = loadJson(ocapFilename)
# set the new capabilities list. eg. ["inbox:write","objects:read"]
ocapJson['capability'] = updateCaps
# change the id, so that the old capabilities can't continue to be used
updateActorNickname = getNicknameFromActor(updateActor)
if not updateActorNickname:
print('WARN: unable to find nickname in ' + updateActor)
return None
updateActorDomain, updateActorPort = getDomainFromActor(updateActor)
if updateActorPort:
ocapId = updateActorNickname + '@' + updateActorDomain + \
':' + str(updateActorPort) + '#' + createPassword(32)
else:
ocapId = updateActorNickname + '@' + updateActorDomain + \
'#' + createPassword(32)
ocapJson['id'] = httpPrefix + "://" + fullDomain + "/caps/" + ocapId
ocapUpdate['object'] = ocapJson
# save it again
saveJson(ocapJson, ocapFilename)
return ocapUpdate
def capabilitiesReceiveUpdate(baseDir: str,
nickname: str, domain: str, port: int,
actor: str,
newCapabilitiesId: str,
capabilityList: [], debug: bool) -> bool:
"""An update for a capability or the given actor has arrived
"""
ocapFilename = \
getOcapFilename(baseDir, nickname, domain, actor, 'granted')
if not ocapFilename:
return False
if not os.path.isfile(ocapFilename):
if debug:
print('DEBUG: capabilities file not found during update')
print(ocapFilename)
return False
ocapJson = loadJson(ocapFilename)
if ocapJson:
ocapJson['id'] = newCapabilitiesId
ocapJson['capability'] = capabilityList
return saveJson(ocapJson, ocapFilename)
return False

175
daemon.py
View File

@ -148,6 +148,7 @@ from webinterface import htmlTermsOfService
from webinterface import htmlSkillsSearch from webinterface import htmlSkillsSearch
from webinterface import htmlHistorySearch from webinterface import htmlHistorySearch
from webinterface import htmlHashtagSearch from webinterface import htmlHashtagSearch
from webinterface import rssHashtagSearch
from webinterface import htmlModerationInfo from webinterface import htmlModerationInfo
from webinterface import htmlSearchSharedItems from webinterface import htmlSearchSharedItems
from webinterface import htmlHashtagBlocked from webinterface import htmlHashtagBlocked
@ -443,7 +444,7 @@ class PubServer(BaseHTTPRequestHandler):
'failed to obtain keyId from signature') 'failed to obtain keyId from signature')
return False return False
# is the keyId (actor) valid? # is the keyId (actor) valid?
if not urlPermitted(keyId, self.server.federationList, "inbox:read"): if not urlPermitted(keyId, self.server.federationList):
if self.server.debug: if self.server.debug:
print('Authorized fetch failed: ' + keyId + print('Authorized fetch failed: ' + keyId +
' is not permitted') ' is not permitted')
@ -920,6 +921,7 @@ class PubServer(BaseHTTPRequestHandler):
if postToNickname: if postToNickname:
print('Posting to nickname ' + postToNickname) print('Posting to nickname ' + postToNickname)
self.postToNickname = postToNickname self.postToNickname = postToNickname
return postMessageToOutbox(messageJson, self.postToNickname, return postMessageToOutbox(messageJson, self.postToNickname,
self.server, self.server.baseDir, self.server, self.server.baseDir,
self.server.httpPrefix, self.server.httpPrefix,
@ -4047,7 +4049,8 @@ class PubServer(BaseHTTPRequestHandler):
if '?page=' in hashtag: if '?page=' in hashtag:
hashtag = hashtag.split('?page=')[0] hashtag = hashtag.split('?page=')[0]
if isBlockedHashtag(baseDir, hashtag): if isBlockedHashtag(baseDir, hashtag):
msg = htmlHashtagBlocked(baseDir).encode('utf-8') msg = htmlHashtagBlocked(baseDir,
self.server.translate).encode('utf-8')
self._login_headers('text/html', len(msg), callingDomain) self._login_headers('text/html', len(msg), callingDomain)
self._write(msg) self._write(msg)
self.server.GETbusy = False self.server.GETbusy = False
@ -4093,6 +4096,60 @@ class PubServer(BaseHTTPRequestHandler):
'login shown done', 'login shown done',
'hashtag search') 'hashtag search')
def _hashtagSearchRSS2(self, callingDomain: str,
path: str, cookie: str,
baseDir: str, httpPrefix: str,
domain: str, domainFull: str, port: int,
onionDomain: str, i2pDomain: str,
GETstartTime, GETtimings: {}):
"""Return an RSS 2 feed for a hashtag
"""
hashtag = path.split('/tags/rss2/')[1]
if isBlockedHashtag(baseDir, hashtag):
self._400()
self.server.GETbusy = False
return
nickname = None
if '/users/' in path:
actor = \
httpPrefix + '://' + domainFull + path
nickname = \
getNicknameFromActor(actor)
hashtagStr = \
rssHashtagSearch(nickname,
domain, port,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate,
baseDir, hashtag,
maxPostsInFeed, self.server.session,
self.server.cachedWebfingers,
self.server.personCache,
httpPrefix,
self.server.projectVersion,
self.server.YTReplacementDomain)
if hashtagStr:
msg = hashtagStr.encode('utf-8')
self._set_headers('text/xml', len(msg),
cookie, callingDomain)
self._write(msg)
else:
originPathStr = path.split('/tags/rss2/')[0]
originPathStrAbsolute = \
httpPrefix + '://' + domainFull + originPathStr
if callingDomain.endswith('.onion') and onionDomain:
originPathStrAbsolute = \
'http://' + onionDomain + originPathStr
elif (callingDomain.endswith('.i2p') and onionDomain):
originPathStrAbsolute = \
'http://' + i2pDomain + originPathStr
self._redirect_headers(originPathStrAbsolute + '/search',
cookie, callingDomain)
self.server.GETbusy = False
self._benchmarkGETtimings(GETstartTime, GETtimings,
'login shown done',
'hashtag rss feed')
def _announceButton(self, callingDomain: str, path: str, def _announceButton(self, callingDomain: str, path: str,
baseDir: str, baseDir: str,
cookie: str, proxyType: str, cookie: str, proxyType: str,
@ -4309,7 +4366,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.postLog, self.server.postLog,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.acceptedCaps,
debug, debug,
self.server.projectVersion) self.server.projectVersion)
originPathStrAbsolute = \ originPathStrAbsolute = \
@ -5178,7 +5234,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.translate, self.server.translate,
self.server.projectVersion, self.server.projectVersion,
baseDir, httpPrefix, True, baseDir, httpPrefix, True,
self.server.ocapAlways,
getPerson, 'roles', getPerson, 'roles',
self.server.session, self.server.session,
cachedWebfingers, cachedWebfingers,
@ -5249,7 +5304,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.translate, self.server.translate,
self.server.projectVersion, self.server.projectVersion,
baseDir, httpPrefix, True, baseDir, httpPrefix, True,
self.server.ocapAlways,
getPerson, 'skills', getPerson, 'skills',
self.server.session, self.server.session,
cachedWebfingers, cachedWebfingers,
@ -5516,7 +5570,6 @@ class PubServer(BaseHTTPRequestHandler):
proxyType: str, cookie: str, proxyType: str, cookie: str,
debug: str, debug: str,
recentPostsCache: {}, session, recentPostsCache: {}, session,
ocapAlways: bool,
defaultTimeline: str, defaultTimeline: str,
maxRecentPosts: int, maxRecentPosts: int,
translate: {}, translate: {},
@ -5538,9 +5591,9 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'inbox', maxPostsInFeed, 'inbox',
authorized, authorized)
ocapAlways)
if inboxFeed: if inboxFeed:
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done', 'show status done',
'show inbox json') 'show inbox json')
@ -5566,8 +5619,8 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInFeed, 'inbox', maxPostsInFeed, 'inbox',
authorized, authorized)
ocapAlways) if GETstartTime:
self._benchmarkGETtimings(GETstartTime, self._benchmarkGETtimings(GETstartTime,
GETtimings, GETtimings,
'show status done', 'show status done',
@ -5590,13 +5643,18 @@ class PubServer(BaseHTTPRequestHandler):
projectVersion, projectVersion,
self._isMinimal(nickname), self._isMinimal(nickname),
YTReplacementDomain) YTReplacementDomain)
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done', 'show status done',
'show inbox html') 'show inbox html')
if msg:
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', len(msg), self._set_headers('text/html', len(msg),
cookie, callingDomain) cookie, callingDomain)
self._write(msg) self._write(msg)
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done', 'show status done',
'show inbox') 'show inbox')
@ -5647,8 +5705,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'dm', maxPostsInFeed, 'dm',
authorized, authorized)
self.server.ocapAlways)
if inboxDMFeed: if inboxDMFeed:
if self._requestHTTP(): if self._requestHTTP():
nickname = path.replace('/users/', '') nickname = path.replace('/users/', '')
@ -5672,8 +5729,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInFeed, 'dm', maxPostsInFeed, 'dm',
authorized, authorized)
self.server.ocapAlways)
msg = \ msg = \
htmlInboxDMs(self.server.defaultTimeline, htmlInboxDMs(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -5748,7 +5804,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'tlreplies', maxPostsInFeed, 'tlreplies',
True, self.server.ocapAlways) True)
if not inboxRepliesFeed: if not inboxRepliesFeed:
inboxRepliesFeed = [] inboxRepliesFeed = []
if self._requestHTTP(): if self._requestHTTP():
@ -5773,7 +5829,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInFeed, 'tlreplies', maxPostsInFeed, 'tlreplies',
True, self.server.ocapAlways) True)
msg = \ msg = \
htmlInboxReplies(self.server.defaultTimeline, htmlInboxReplies(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -5848,7 +5904,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInMediaFeed, 'tlmedia', maxPostsInMediaFeed, 'tlmedia',
True, self.server.ocapAlways) True)
if not inboxMediaFeed: if not inboxMediaFeed:
inboxMediaFeed = [] inboxMediaFeed = []
if self._requestHTTP(): if self._requestHTTP():
@ -5873,7 +5929,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInMediaFeed, 'tlmedia', maxPostsInMediaFeed, 'tlmedia',
True, self.server.ocapAlways) True)
msg = \ msg = \
htmlInboxMedia(self.server.defaultTimeline, htmlInboxMedia(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -5948,7 +6004,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInBlogsFeed, 'tlblogs', maxPostsInBlogsFeed, 'tlblogs',
True, self.server.ocapAlways) True)
if not inboxBlogsFeed: if not inboxBlogsFeed:
inboxBlogsFeed = [] inboxBlogsFeed = []
if self._requestHTTP(): if self._requestHTTP():
@ -5973,7 +6029,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInBlogsFeed, 'tlblogs', maxPostsInBlogsFeed, 'tlblogs',
True, self.server.ocapAlways) True)
msg = \ msg = \
htmlInboxBlogs(self.server.defaultTimeline, htmlInboxBlogs(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -6106,7 +6162,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'tlbookmarks', maxPostsInFeed, 'tlbookmarks',
authorized, self.server.ocapAlways) authorized)
if bookmarksFeed: if bookmarksFeed:
if self._requestHTTP(): if self._requestHTTP():
nickname = path.replace('/users/', '') nickname = path.replace('/users/', '')
@ -6132,8 +6188,7 @@ class PubServer(BaseHTTPRequestHandler):
httpPrefix, httpPrefix,
maxPostsInFeed, maxPostsInFeed,
'tlbookmarks', 'tlbookmarks',
authorized, authorized)
self.server.ocapAlways)
msg = \ msg = \
htmlBookmarks(self.server.defaultTimeline, htmlBookmarks(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -6210,7 +6265,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'tlevents', maxPostsInFeed, 'tlevents',
authorized, self.server.ocapAlways) authorized)
print('eventsFeed: ' + str(eventsFeed)) print('eventsFeed: ' + str(eventsFeed))
if eventsFeed: if eventsFeed:
if self._requestHTTP(): if self._requestHTTP():
@ -6236,8 +6291,7 @@ class PubServer(BaseHTTPRequestHandler):
httpPrefix, httpPrefix,
maxPostsInFeed, maxPostsInFeed,
'tlevents', 'tlevents',
authorized, authorized)
self.server.ocapAlways)
msg = \ msg = \
htmlEvents(self.server.defaultTimeline, htmlEvents(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -6306,8 +6360,7 @@ class PubServer(BaseHTTPRequestHandler):
port, path, port, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'outbox', maxPostsInFeed, 'outbox',
authorized, authorized)
self.server.ocapAlways)
if outboxFeed: if outboxFeed:
if self._requestHTTP(): if self._requestHTTP():
nickname = \ nickname = \
@ -6331,8 +6384,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInFeed, 'outbox', maxPostsInFeed, 'outbox',
authorized, authorized)
self.server.ocapAlways)
msg = \ msg = \
htmlOutbox(self.server.defaultTimeline, htmlOutbox(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -6394,7 +6446,7 @@ class PubServer(BaseHTTPRequestHandler):
path, path,
httpPrefix, httpPrefix,
maxPostsInFeed, 'moderation', maxPostsInFeed, 'moderation',
True, self.server.ocapAlways) True)
if moderationFeed: if moderationFeed:
if self._requestHTTP(): if self._requestHTTP():
nickname = path.replace('/users/', '') nickname = path.replace('/users/', '')
@ -6418,7 +6470,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1', path + '?page=1',
httpPrefix, httpPrefix,
maxPostsInFeed, 'moderation', maxPostsInFeed, 'moderation',
True, self.server.ocapAlways) True)
msg = \ msg = \
htmlModeration(self.server.defaultTimeline, htmlModeration(self.server.defaultTimeline,
self.server.recentPostsCache, self.server.recentPostsCache,
@ -6521,7 +6573,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.projectVersion, self.server.projectVersion,
baseDir, httpPrefix, baseDir, httpPrefix,
authorized, authorized,
self.server.ocapAlways,
getPerson, 'shares', getPerson, 'shares',
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
@ -6608,7 +6659,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.projectVersion, self.server.projectVersion,
baseDir, httpPrefix, baseDir, httpPrefix,
authorized, authorized,
self.server.ocapAlways,
getPerson, 'following', getPerson, 'following',
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
@ -6695,7 +6745,6 @@ class PubServer(BaseHTTPRequestHandler):
baseDir, baseDir,
httpPrefix, httpPrefix,
authorized, authorized,
self.server.ocapAlways,
getPerson, 'followers', getPerson, 'followers',
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
@ -6757,7 +6806,6 @@ class PubServer(BaseHTTPRequestHandler):
baseDir, baseDir,
httpPrefix, httpPrefix,
authorized, authorized,
self.server.ocapAlways,
getPerson, 'posts', getPerson, 'posts',
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
@ -8068,6 +8116,18 @@ class PubServer(BaseHTTPRequestHandler):
# hashtag search # hashtag search
if self.path.startswith('/tags/') or \ if self.path.startswith('/tags/') or \
(authorized and '/tags/' in self.path): (authorized and '/tags/' in self.path):
if self.path.startswith('/tags/rss2/'):
self._hashtagSearchRSS2(callingDomain,
self.path, cookie,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.domainFull,
self.server.port,
self.server.onionDomain,
self.server.i2pDomain,
GETstartTime, GETtimings)
return
self._hashtagSearch(callingDomain, self._hashtagSearch(callingDomain,
self.path, cookie, self.path, cookie,
self.server.baseDir, self.server.baseDir,
@ -8673,7 +8733,6 @@ class PubServer(BaseHTTPRequestHandler):
cookie, self.server.debug, cookie, self.server.debug,
self.server.recentPostsCache, self.server.recentPostsCache,
self.server.session, self.server.session,
self.server.ocapAlways,
self.server.defaultTimeline, self.server.defaultTimeline,
self.server.maxRecentPosts, self.server.maxRecentPosts,
self.server.translate, self.server.translate,
@ -9064,7 +9123,9 @@ class PubServer(BaseHTTPRequestHandler):
etag, callingDomain) etag, callingDomain)
def _receiveNewPostProcess(self, postType: str, path: str, headers: {}, def _receiveNewPostProcess(self, postType: str, path: str, headers: {},
length: int, postBytes, boundary: str) -> int: length: int, postBytes, boundary: str,
callingDomain: str, cookie: str,
authorized: bool) -> int:
# Note: this needs to happen synchronously # Note: this needs to happen synchronously
# 0=this is not a new post # 0=this is not a new post
# 1=new post success # 1=new post success
@ -9205,6 +9266,7 @@ class PubServer(BaseHTTPRequestHandler):
privateEvent = False privateEvent = False
else: else:
privateEvent = True privateEvent = True
if postType == 'newpost': if postType == 'newpost':
messageJson = \ messageJson = \
createPublicPost(self.server.baseDir, createPublicPost(self.server.baseDir,
@ -9224,6 +9286,7 @@ class PubServer(BaseHTTPRequestHandler):
if messageJson: if messageJson:
if fields['schedulePost']: if fields['schedulePost']:
return 1 return 1
if self._postToOutbox(messageJson, __version__, nickname): if self._postToOutbox(messageJson, __version__, nickname):
populateReplies(self.server.baseDir, populateReplies(self.server.baseDir,
self.server.httpPrefix, self.server.httpPrefix,
@ -9481,8 +9544,7 @@ class PubServer(BaseHTTPRequestHandler):
if messageJson: if messageJson:
if fields['schedulePost']: if fields['schedulePost']:
return 1 return 1
# if self.server.debug: print('Sending new DM to ' +
print('DEBUG: new DM to ' +
str(messageJson['object']['to'])) str(messageJson['object']['to']))
if self._postToOutbox(messageJson, __version__, nickname): if self._postToOutbox(messageJson, __version__, nickname):
populateReplies(self.server.baseDir, populateReplies(self.server.baseDir,
@ -9618,7 +9680,9 @@ class PubServer(BaseHTTPRequestHandler):
return 1 return 1
return -1 return -1
def _receiveNewPost(self, postType: str, path: str) -> int: def _receiveNewPost(self, postType: str, path: str,
callingDomain: str, cookie: str,
authorized: bool) -> int:
"""A new post has been created """A new post has been created
This creates a thread to send the new post This creates a thread to send the new post
""" """
@ -9719,7 +9783,9 @@ class PubServer(BaseHTTPRequestHandler):
print('Creating new post from: ' + newPostThreadName) print('Creating new post from: ' + newPostThreadName)
self._receiveNewPostProcess(postType, self._receiveNewPostProcess(postType,
path, headers, length, path, headers, length,
postBytes, boundary) postBytes, boundary,
callingDomain, cookie,
authorized)
return pageNumber return pageNumber
def _cryptoAPIreadHandle(self): def _cryptoAPIreadHandle(self):
@ -10179,7 +10245,10 @@ class PubServer(BaseHTTPRequestHandler):
elif currPostType == 'newevent': elif currPostType == 'newevent':
postRedirect = 'tlevents' postRedirect = 'tlevents'
pageNumber = self._receiveNewPost(currPostType, self.path) pageNumber = \
self._receiveNewPost(currPostType, self.path,
callingDomain, cookie,
authorized)
if pageNumber: if pageNumber:
nickname = self.path.split('/users/')[1] nickname = self.path.split('/users/')[1]
if '/' in nickname: if '/' in nickname:
@ -10232,7 +10301,6 @@ class PubServer(BaseHTTPRequestHandler):
self.path.endswith('/inbox') or self.path.endswith('/inbox') or
self.path.endswith('/shares') or self.path.endswith('/shares') or
self.path.endswith('/moderationaction') or self.path.endswith('/moderationaction') or
self.path.endswith('/caps/new') or
self.path == '/sharedInbox'): self.path == '/sharedInbox'):
print('Attempt to POST to invalid path ' + self.path) print('Attempt to POST to invalid path ' + self.path)
self._400() self._400()
@ -10554,8 +10622,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
port=80, proxyPort=80, httpPrefix='https', port=80, proxyPort=80, httpPrefix='https',
fedList=[], maxMentions=10, maxEmoji=10, fedList=[], maxMentions=10, maxEmoji=10,
authenticatedFetch=False, authenticatedFetch=False,
noreply=False, nolike=False, nopics=False,
noannounce=False, cw=False, ocapAlways=False,
proxyType=None, maxReplies=64, proxyType=None, maxReplies=64,
domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864, domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
allowDeletion=False, debug=False, unitTest=False, allowDeletion=False, debug=False, unitTest=False,
@ -10687,7 +10753,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.sendThreads = sendThreads httpd.sendThreads = sendThreads
httpd.postLog = [] httpd.postLog = []
httpd.maxQueueLength = 64 httpd.maxQueueLength = 64
httpd.ocapAlways = ocapAlways
httpd.allowDeletion = allowDeletion httpd.allowDeletion = allowDeletion
httpd.lastLoginTime = 0 httpd.lastLoginTime = 0
httpd.maxReplies = maxReplies httpd.maxReplies = maxReplies
@ -10695,19 +10760,8 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.tokensLookup = {} httpd.tokensLookup = {}
loadTokens(baseDir, httpd.tokens, httpd.tokensLookup) loadTokens(baseDir, httpd.tokens, httpd.tokensLookup)
httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch
httpd.acceptedCaps = ["inbox:write", "objects:read"]
# contains threads used to send posts to followers # contains threads used to send posts to followers
httpd.followersThreads = [] httpd.followersThreads = []
if noreply:
httpd.acceptedCaps.append('inbox:noreply')
if nolike:
httpd.acceptedCaps.append('inbox:nolike')
if nopics:
httpd.acceptedCaps.append('inbox:nopics')
if noannounce:
httpd.acceptedCaps.append('inbox:noannounce')
if cw:
httpd.acceptedCaps.append('inbox:cw')
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain): if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
print('Creating shared inbox: inbox@' + domain) print('Creating shared inbox: inbox@' + domain)
@ -10778,12 +10832,11 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.personCache, httpd.inboxQueue, httpd.personCache, httpd.inboxQueue,
domain, onionDomain, i2pDomain, port, proxyType, domain, onionDomain, i2pDomain, port, proxyType,
httpd.federationList, httpd.federationList,
httpd.ocapAlways, maxReplies, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, debug, maxMentions, maxEmoji, allowDeletion, debug, maxMentions, maxEmoji,
httpd.translate, unitTest, httpd.translate, unitTest,
httpd.YTReplacementDomain, httpd.YTReplacementDomain), daemon=True)
httpd.acceptedCaps), daemon=True)
print('Creating scheduled post thread') print('Creating scheduled post thread')
httpd.thrPostSchedule = \ httpd.thrPostSchedule = \
threadWithTrace(target=runPostSchedule, threadWithTrace(target=runPostSchedule,

View File

@ -34,7 +34,7 @@ def createDelete(session, baseDir: str, federationList: [],
objectUrl is typically the url of the message, corresponding to url objectUrl is typically the url of the message, corresponding to url
or atomUri in createPostBase or atomUri in createPostBase
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
if ':' in domain: if ':' in domain:
@ -137,8 +137,7 @@ def sendDeleteViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
@ -160,7 +159,7 @@ def sendDeleteViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, newDeleteJson, [], inboxUrl, headers, "inbox:write") postJson(session, newDeleteJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)

View File

@ -16,6 +16,7 @@ from skills import setSkillLevel
from roles import setRole from roles import setRole
from webfinger import webfingerHandle from webfinger import webfingerHandle
from posts import getPublicPostDomains from posts import getPublicPostDomains
from posts import getPublicPostDomainsBlocked
from posts import sendBlockViaServer from posts import sendBlockViaServer
from posts import sendUndoBlockViaServer from posts import sendUndoBlockViaServer
from posts import createPublicPost from posts import createPublicPost
@ -24,6 +25,7 @@ from posts import archivePosts
from posts import sendPostViaServer from posts import sendPostViaServer
from posts import getPublicPostsOfPerson from posts import getPublicPostsOfPerson
from posts import getUserUrl from posts import getUserUrl
from posts import checkDomains
from session import createSession from session import createSession
from session import getJson from session import getJson
from filters import addFilter from filters import addFilter
@ -71,7 +73,9 @@ from socnet import instancesGraph
import argparse import argparse
def str2bool(v): def str2bool(v) -> bool:
"""Returns true if the given value is a boolean
"""
if isinstance(v, bool): if isinstance(v, bool):
return v return v
if v.lower() in ('yes', 'true', 't', 'y', '1'): if v.lower() in ('yes', 'true', 't', 'y', '1'):
@ -155,6 +159,14 @@ parser.add_argument('--postDomains', dest='postDomains', type=str,
default=None, default=None,
help='Show domains referenced in public ' help='Show domains referenced in public '
'posts for the given handle') 'posts for the given handle')
parser.add_argument('--postDomainsBlocked', dest='postDomainsBlocked',
type=str, default=None,
help='Show blocked domains referenced in public '
'posts for the given handle')
parser.add_argument('--checkDomains', dest='checkDomains', type=str,
default=None,
help='Check domains of non-mutual followers for '
'domains which are globally blocked by this instance')
parser.add_argument('--socnet', dest='socnet', type=str, parser.add_argument('--socnet', dest='socnet', type=str,
default=None, default=None,
help='Show dot diagram for social network ' help='Show dot diagram for social network '
@ -218,26 +230,6 @@ parser.add_argument("--testsnetwork", type=str2bool, nargs='?',
parser.add_argument("--testdata", type=str2bool, nargs='?', parser.add_argument("--testdata", type=str2bool, nargs='?',
const=True, default=False, const=True, default=False,
help="Generate some data for testing purposes") help="Generate some data for testing purposes")
parser.add_argument("--ocap", type=str2bool, nargs='?',
const=True, default=False,
help="Always strictly enforce object capabilities")
parser.add_argument("--noreply", type=str2bool, nargs='?',
const=True, default=False,
help="Default capabilities don't allow replies on posts")
parser.add_argument("--nolike", type=str2bool, nargs='?',
const=True, default=False,
help="Default capabilities don't allow " +
"likes/favourites on posts")
parser.add_argument("--nopics", type=str2bool, nargs='?',
const=True, default=False,
help="Default capabilities don't allow attached pictures")
parser.add_argument("--noannounce", "--norepeat", type=str2bool, nargs='?',
const=True, default=False,
help="Default capabilities don't allow announce/repeat")
parser.add_argument("--cw", type=str2bool, nargs='?',
const=True, default=False,
help="Default capabilities don't allow posts " +
"without content warnings")
parser.add_argument('--icon', '--avatar', dest='avatar', type=str, parser.add_argument('--icon', '--avatar', dest='avatar', type=str,
default=None, default=None,
help='Set the avatar filename for an account') help='Set the avatar filename for an account')
@ -465,7 +457,8 @@ if args.postDomains:
elif args.gnunet: elif args.gnunet:
proxyType = 'gnunet' proxyType = 'gnunet'
domainList = [] domainList = []
domainList = getPublicPostDomains(baseDir, nickname, domain, domainList = getPublicPostDomains(None,
baseDir, nickname, domain,
proxyType, args.port, proxyType, args.port,
httpPrefix, debug, httpPrefix, debug,
__version__, domainList) __version__, domainList)
@ -473,6 +466,83 @@ if args.postDomains:
print(postDomain) print(postDomain)
sys.exit() sys.exit()
if args.postDomainsBlocked:
# Domains which were referenced in public posts by a
# given handle but which are globally blocked on this instance
if '@' not in args.postDomainsBlocked:
if '/users/' in args.postDomainsBlocked:
postsNickname = getNicknameFromActor(args.posts)
postsDomain, postsPort = getDomainFromActor(args.posts)
args.postDomainsBlocked = postsNickname + '@' + postsDomain
if postsPort:
if postsPort != 80 and postsPort != 443:
args.postDomainsBlocked += ':' + str(postsPort)
else:
print('Syntax: --postDomainsBlocked nickname@domain')
sys.exit()
if not args.http:
args.port = 443
nickname = args.postDomainsBlocked.split('@')[0]
domain = args.postDomainsBlocked.split('@')[1]
proxyType = None
if args.tor or domain.endswith('.onion'):
proxyType = 'tor'
if domain.endswith('.onion'):
args.port = 80
elif args.i2p or domain.endswith('.i2p'):
proxyType = 'i2p'
if domain.endswith('.i2p'):
args.port = 80
elif args.gnunet:
proxyType = 'gnunet'
domainList = []
domainList = getPublicPostDomainsBlocked(None,
baseDir, nickname, domain,
proxyType, args.port,
httpPrefix, debug,
__version__, domainList)
for postDomain in domainList:
print(postDomain)
sys.exit()
if args.checkDomains:
# Domains which were referenced in public posts by a
# given handle but which are globally blocked on this instance
if '@' not in args.checkDomains:
if '/users/' in args.checkDomains:
postsNickname = getNicknameFromActor(args.posts)
postsDomain, postsPort = getDomainFromActor(args.posts)
args.checkDomains = postsNickname + '@' + postsDomain
if postsPort:
if postsPort != 80 and postsPort != 443:
args.checkDomains += ':' + str(postsPort)
else:
print('Syntax: --checkDomains nickname@domain')
sys.exit()
if not args.http:
args.port = 443
nickname = args.checkDomains.split('@')[0]
domain = args.checkDomains.split('@')[1]
proxyType = None
if args.tor or domain.endswith('.onion'):
proxyType = 'tor'
if domain.endswith('.onion'):
args.port = 80
elif args.i2p or domain.endswith('.i2p'):
proxyType = 'i2p'
if domain.endswith('.i2p'):
args.port = 80
elif args.gnunet:
proxyType = 'gnunet'
maxBlockedDomains = 0
checkDomains(None,
baseDir, nickname, domain,
proxyType, args.port,
httpPrefix, debug,
__version__,
maxBlockedDomains, False)
sys.exit()
if args.socnet: if args.socnet:
if ',' not in args.socnet: if ',' not in args.socnet:
print('Syntax: ' print('Syntax: '
@ -718,7 +788,6 @@ if args.approve:
postLog = [] postLog = []
cachedWebfingers = {} cachedWebfingers = {}
personCache = {} personCache = {}
acceptedCaps = []
manualApproveFollowRequest(session, baseDir, manualApproveFollowRequest(session, baseDir,
httpPrefix, httpPrefix,
args.nickname, domain, port, args.nickname, domain, port,
@ -726,7 +795,6 @@ if args.approve:
federationList, federationList,
sendThreads, postLog, sendThreads, postLog,
cachedWebfingers, personCache, cachedWebfingers, personCache,
acceptedCaps,
debug, __version__) debug, __version__)
sys.exit() sys.exit()
@ -1111,9 +1179,6 @@ if args.port:
if args.proxyPort: if args.proxyPort:
proxyPort = args.proxyPort proxyPort = args.proxyPort
setConfigParam(baseDir, 'proxyPort', proxyPort) setConfigParam(baseDir, 'proxyPort', proxyPort)
ocapAlways = False
if args.ocap:
ocapAlways = args.ocap
if args.gnunet: if args.gnunet:
httpPrefix = 'gnunet' httpPrefix = 'gnunet'
if args.dat: if args.dat:
@ -1830,8 +1895,6 @@ if __name__ == "__main__":
port, proxyPort, httpPrefix, port, proxyPort, httpPrefix,
federationList, args.maxMentions, federationList, args.maxMentions,
args.maxEmoji, args.authenticatedFetch, args.maxEmoji, args.authenticatedFetch,
args.noreply, args.nolike, args.nopics,
args.noannounce, args.cw, ocapAlways,
proxyType, args.maxReplies, proxyType, args.maxReplies,
args.domainMaxPostsPerDay, args.domainMaxPostsPerDay,
args.accountMaxPostsPerDay, args.accountMaxPostsPerDay,

View File

@ -8,6 +8,7 @@ __status__ = "Production"
from pprint import pprint from pprint import pprint
import os import os
from utils import getFollowersList
from utils import validNickname from utils import validNickname
from utils import domainPermitted from utils import domainPermitted
from utils import getDomainFromActor from utils import getDomainFromActor
@ -112,15 +113,14 @@ def isFollowingActor(baseDir: str,
def getMutualsOfPerson(baseDir: str, def getMutualsOfPerson(baseDir: str,
nickname: str, domain: str, nickname: str, domain: str) -> []:
followFile='following.txt') -> []:
"""Returns the mutuals of a person """Returns the mutuals of a person
i.e. accounts which they follow and which also follow back i.e. accounts which they follow and which also follow back
""" """
followers = \ followers = \
getFollowersOfPerson(baseDir, nickname, domain, 'followers') getFollowersList(baseDir, nickname, domain, 'followers.txt')
following = \ following = \
getFollowersOfPerson(baseDir, nickname, domain, 'following') getFollowersList(baseDir, nickname, domain, 'following.txt')
mutuals = [] mutuals = []
for handle in following: for handle in following:
if handle in followers: if handle in followers:
@ -128,36 +128,6 @@ def getMutualsOfPerson(baseDir: str,
return mutuals return mutuals
def getFollowersOfPerson(baseDir: str,
nickname: str, domain: str,
followFile='following.txt') -> []:
"""Returns a list containing the followers of the given person
Used by the shared inbox to know who to send incoming mail to
"""
followers = []
if ':' in domain:
domain = domain.split(':')[0]
handle = nickname + '@' + domain
if not os.path.isdir(baseDir + '/accounts/' + handle):
return followers
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for account in dirs:
filename = os.path.join(subdir, account) + '/' + followFile
if account == handle or account.startswith('inbox@'):
continue
if not os.path.isfile(filename):
continue
with open(filename, 'r') as followingfile:
for followingHandle in followingfile:
followingHandle2 = followingHandle.replace('\n', '')
followingHandle2 = followingHandle2.replace('\r', '')
if followingHandle2 == handle:
if account not in followers:
followers.append(account)
break
return followers
def followerOfPerson(baseDir: str, nickname: str, domain: str, def followerOfPerson(baseDir: str, nickname: str, domain: str,
followerNickname: str, followerDomain: str, followerNickname: str, followerDomain: str,
federationList: [], debug: bool) -> bool: federationList: [], debug: bool) -> bool:
@ -543,8 +513,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [], port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [], messageJson: {}, federationList: [],
debug: bool, projectVersion: str, debug: bool, projectVersion: str) -> bool:
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
"""Receives a follow request within the POST section of HTTPServer """Receives a follow request within the POST section of HTTPServer
""" """
if not messageJson['type'].startswith('Follow'): if not messageJson['type'].startswith('Follow'):
@ -685,8 +654,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
nicknameToFollow, domainToFollow, port, nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort, nickname, domain, fromPort,
messageJson['actor'], federationList, messageJson['actor'], federationList,
messageJson, acceptedCaps, messageJson, sendThreads, postLog,
sendThreads, postLog,
cachedWebfingers, personCache, cachedWebfingers, personCache,
debug, projectVersion, True) debug, projectVersion, True)
@ -696,8 +664,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
port: int, port: int,
nickname: str, domain: str, fromPort: int, nickname: str, domain: str, fromPort: int,
personUrl: str, federationList: [], personUrl: str, federationList: [],
followJson: {}, acceptedCaps: [], followJson: {}, sendThreads: [], postLog: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str, debug: bool, projectVersion: str,
removeFollowActivity: bool): removeFollowActivity: bool):
@ -715,7 +682,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
acceptJson = createAccept(baseDir, federationList, acceptJson = createAccept(baseDir, federationList,
nicknameToFollow, domainToFollow, port, nicknameToFollow, domainToFollow, port,
personUrl, '', httpPrefix, personUrl, '', httpPrefix,
followJson, acceptedCaps) followJson)
if debug: if debug:
pprint(acceptJson) pprint(acceptJson)
print('DEBUG: sending follow Accept from ' + print('DEBUG: sending follow Accept from ' +
@ -938,8 +905,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
@ -961,7 +927,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, newFollowJson, [], inboxUrl, headers, "inbox:write") postJson(session, newFollowJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
@ -1037,9 +1003,10 @@ def sendUnfollowRequestViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl, avatarUrl, displayName) = getPersonBox(baseDir, session,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix,
fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
if not inboxUrl: if not inboxUrl:
@ -1059,7 +1026,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, unfollowJson, [], inboxUrl, headers, "inbox:write") postJson(session, unfollowJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
@ -1075,14 +1042,12 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
"""In a shared inbox if we receive a post we know who it's from """In a shared inbox if we receive a post we know who it's from
and if it's addressed to followers then we need to get a list of those. and if it's addressed to followers then we need to get a list of those.
This returns a list of account handles which follow the given actor This returns a list of account handles which follow the given actor
and also the corresponding capability id if it exists
""" """
if debug: if debug:
print('DEBUG: getting followers of ' + actor) print('DEBUG: getting followers of ' + actor)
recipientsDict = {} recipientsDict = {}
if ':' not in actor: if ':' not in actor:
return recipientsDict return recipientsDict
httpPrefix = actor.split(':')[0]
nickname = getNicknameFromActor(actor) nickname = getNicknameFromActor(actor)
if not nickname: if not nickname:
if debug: if debug:
@ -1114,34 +1079,6 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
if debug: if debug:
print('DEBUG: ' + account + print('DEBUG: ' + account +
' follows ' + actorHandle) ' follows ' + actorHandle)
ocapFilename = baseDir + '/accounts/' + \
account + '/ocap/accept/' + httpPrefix + \
':##' + domain + ':' + str(port) + \
'#users#' + nickname + '.json'
if debug:
print('DEBUG: checking capabilities of' + account)
if os.path.isfile(ocapFilename):
ocapJson = loadJson(ocapFilename)
if ocapJson:
if ocapJson.get('id'):
if debug:
print('DEBUG: ' +
'capabilities id found for ' +
account)
recipientsDict[account] = ocapJson['id']
else:
if debug:
print('DEBUG: ' +
'capabilities has no ' +
'id attribute')
recipientsDict[account] = None
else:
if debug:
print('DEBUG: ' +
'No capabilities file found for ' +
account + ' granted by ' + actorHandle)
print(ocapFilename)
recipientsDict[account] = None recipientsDict[account] = None
return recipientsDict return recipientsDict

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

192
inbox.py
View File

@ -40,9 +40,6 @@ from pprint import pprint
from cache import getPersonFromCache from cache import getPersonFromCache
from cache import storePersonInCache from cache import storePersonInCache
from acceptreject import receiveAcceptReject from acceptreject import receiveAcceptReject
from capabilities import getOcapFilename
from capabilities import CapablePost
from capabilities import capabilitiesReceiveUpdate
from bookmarks import updateBookmarksCollection from bookmarks import updateBookmarksCollection
from bookmarks import undoBookmarksCollectionEntry from bookmarks import undoBookmarksCollectionEntry
from blocking import isBlocked from blocking import isBlocked
@ -268,7 +265,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
if domain in actor: if domain in actor:
return True return True
if not urlPermitted(actor, federationList, "inbox:write"): if not urlPermitted(actor, federationList):
return False return False
alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce') alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce')
@ -281,7 +278,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
inReplyTo = messageJson['object']['inReplyTo'] inReplyTo = messageJson['object']['inReplyTo']
if not isinstance(inReplyTo, str): if not isinstance(inReplyTo, str):
return False return False
if not urlPermitted(inReplyTo, federationList, "inbox:write"): if not urlPermitted(inReplyTo, federationList):
return False return False
return True return True
@ -437,81 +434,12 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
return filename return filename
def inboxCheckCapabilities(baseDir: str, nickname: str, domain: str,
actor: str, queueFilename: str, queue: [],
queueJson: {}, capabilityId: str,
debug: bool) -> bool:
if nickname == 'inbox':
return True
ocapFilename = \
getOcapFilename(baseDir,
queueJson['nickname'], queueJson['domain'],
actor, 'accept')
if not ocapFilename:
return False
if not os.path.isfile(ocapFilename):
if debug:
print('DEBUG: capabilities for ' +
actor + ' do not exist')
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
return False
oc = loadJson(ocapFilename)
if not oc:
return False
if not oc.get('id'):
if debug:
print('DEBUG: capabilities for ' + actor + ' do not contain an id')
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
return False
if oc['id'] != capabilityId:
if debug:
print('DEBUG: capability id mismatch')
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
return False
if not oc.get('capability'):
if debug:
print('DEBUG: missing capability list')
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
return False
if not CapablePost(queueJson['post'], oc['capability'], debug):
if debug:
print('DEBUG: insufficient capabilities to write to inbox from ' +
actor)
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
return False
if debug:
print('DEBUG: object capabilities check success')
return True
def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [], def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
recipientsDict: {}, recipientsDict: {},
domainMatch: str, domain: str, domainMatch: str, domain: str,
actor: str, debug: bool) -> bool: actor: str, debug: bool) -> bool:
"""Given a list of post recipients (toList) from 'to' or 'cc' parameters """Given a list of post recipients (toList) from 'to' or 'cc' parameters
populate a recipientsDict with the handle and capabilities id for each populate a recipientsDict with the handle for each
""" """
followerRecipients = False followerRecipients = False
for recipient in toList: for recipient in toList:
@ -523,23 +451,6 @@ def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
nickname = recipient.split(domainMatch)[1] nickname = recipient.split(domainMatch)[1]
handle = nickname+'@'+domain handle = nickname+'@'+domain
if os.path.isdir(baseDir + '/accounts/' + handle): if os.path.isdir(baseDir + '/accounts/' + handle):
# are capabilities granted for this account to the
# sender (actor) of the post?
ocapFilename = \
baseDir + '/accounts/' + handle + \
'/ocap/accept/' + actor.replace('/', '#') + '.json'
if os.path.isfile(ocapFilename):
# read the granted capabilities and obtain the id
ocapJson = loadJson(ocapFilename)
if ocapJson:
if ocapJson.get('id'):
# append with the capabilities id
recipientsDict[handle] = ocapJson['id']
else:
recipientsDict[handle] = None
else:
if debug:
print('DEBUG: ' + ocapFilename + ' not found')
recipientsDict[handle] = None recipientsDict[handle] = None
else: else:
if debug: if debug:
@ -741,8 +652,7 @@ def receiveUndo(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [], port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [], messageJson: {}, federationList: [],
debug: bool, debug: bool) -> bool:
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
"""Receives an undo request within the POST section of HTTPServer """Receives an undo request within the POST section of HTTPServer
""" """
if not messageJson['type'].startswith('Undo'): if not messageJson['type'].startswith('Undo'):
@ -1005,24 +915,6 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str,
print('DEBUG: Profile update was received for ' + print('DEBUG: Profile update was received for ' +
messageJson['object']['url']) messageJson['object']['url'])
return True return True
if messageJson['object'].get('capability') and \
messageJson['object'].get('scope'):
nickname = getNicknameFromActor(messageJson['object']['scope'])
if nickname:
domain, tempPort = \
getDomainFromActor(messageJson['object']['scope'])
if messageJson['object']['type'] == 'Capability':
capability = messageJson['object']['capability']
if capabilitiesReceiveUpdate(baseDir, nickname, domain, port,
messageJson['actor'],
messageJson['object']['id'],
capability,
debug):
if debug:
print('DEBUG: An update was received')
return True
return False return False
@ -2124,20 +2016,19 @@ def inboxUpdateIndex(boxname: str, baseDir: str, handle: str,
return False return False
def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int, def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
session, keyId: str, handle: str, messageJson: {}, session, keyId: str, handle: str, messageJson: {},
baseDir: str, httpPrefix: str, sendThreads: [], baseDir: str, httpPrefix: str, sendThreads: [],
postLog: [], cachedWebfingers: {}, personCache: {}, postLog: [], cachedWebfingers: {}, personCache: {},
queue: [], domain: str, queue: [], domain: str,
onionDomain: str, i2pDomain: str, onionDomain: str, i2pDomain: str,
port: int, proxyType: str, port: int, proxyType: str,
federationList: [], ocapAlways: bool, debug: bool, federationList: [], debug: bool,
acceptedCaps: [],
queueFilename: str, destinationFilename: str, queueFilename: str, destinationFilename: str,
maxReplies: int, allowDeletion: bool, maxReplies: int, allowDeletion: bool,
maxMentions: int, maxEmoji: int, translate: {}, maxMentions: int, maxEmoji: int, translate: {},
unitTest: bool, YTReplacementDomain: str) -> bool: unitTest: bool, YTReplacementDomain: str) -> bool:
""" Anything which needs to be done after capabilities checks have passed """ Anything which needs to be done after initial checks have passed
""" """
actor = keyId actor = keyId
if '#' in actor: if '#' in actor:
@ -2247,7 +2138,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
return False return False
if debug: if debug:
print('DEBUG: object capabilities passed') print('DEBUG: initial checks passed')
print('copy queue file from ' + queueFilename + print('copy queue file from ' + queueFilename +
' to ' + destinationFilename) ' to ' + destinationFilename)
@ -2526,13 +2417,11 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
cachedWebfingers: {}, personCache: {}, queue: [], cachedWebfingers: {}, personCache: {}, queue: [],
domain: str, domain: str,
onionDomain: str, i2pDomain: str, port: int, proxyType: str, onionDomain: str, i2pDomain: str, port: int, proxyType: str,
federationList: [], federationList: [], maxReplies: int,
ocapAlways: bool, maxReplies: int,
domainMaxPostsPerDay: int, accountMaxPostsPerDay: int, domainMaxPostsPerDay: int, accountMaxPostsPerDay: int,
allowDeletion: bool, debug: bool, maxMentions: int, allowDeletion: bool, debug: bool, maxMentions: int,
maxEmoji: int, translate: {}, unitTest: bool, maxEmoji: int, translate: {}, unitTest: bool,
YTReplacementDomain: str, YTReplacementDomain: str) -> None:
acceptedCaps=["inbox:write", "objects:read"]) -> None:
"""Processes received items and moves them to the appropriate """Processes received items and moves them to the appropriate
directories directories
""" """
@ -2801,8 +2690,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
personCache, personCache,
queueJson['post'], queueJson['post'],
federationList, federationList,
debug, debug):
acceptedCaps=["inbox:write", "objects:read"]):
print('Queue: Undo accepted from ' + keyId) print('Queue: Undo accepted from ' + keyId)
if os.path.isfile(queueFilename): if os.path.isfile(queueFilename):
os.remove(queueFilename) os.remove(queueFilename)
@ -2819,9 +2707,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
personCache, personCache,
queueJson['post'], queueJson['post'],
federationList, federationList,
debug, projectVersion, debug, projectVersion):
acceptedCaps=["inbox:write",
"objects:read"]):
if os.path.isfile(queueFilename): if os.path.isfile(queueFilename):
os.remove(queueFilename) os.remove(queueFilename)
if len(queue) > 0: if len(queue) > 0:
@ -2917,22 +2803,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
pprint(recipientsDictFollowers) pprint(recipientsDictFollowers)
print('*************************************') print('*************************************')
if queueJson['post'].get('capability'):
if not isinstance(queueJson['post']['capability'], list):
print('Queue: capability on post should be a list')
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
continue
# Copy any posts addressed to followers into the shared inbox # Copy any posts addressed to followers into the shared inbox
# this avoid copying file multiple times to potentially many # this avoid copying file multiple times to potentially many
# individual inboxes # individual inboxes
# This obviously bypasses object capabilities and so
# any checking will needs to be handled at the time when inbox
# GET happens on individual accounts.
# See posts.py/createBoxBase
if len(recipientsDictFollowers) > 0: if len(recipientsDictFollowers) > 0:
sharedInboxPostFilename = \ sharedInboxPostFilename = \
queueJson['destination'].replace(inboxHandle, inboxHandle) queueJson['destination'].replace(inboxHandle, inboxHandle)
@ -2943,16 +2816,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
for handle, capsId in recipientsDict.items(): for handle, capsId in recipientsDict.items():
destination = \ destination = \
queueJson['destination'].replace(inboxHandle, handle) queueJson['destination'].replace(inboxHandle, handle)
# check that capabilities are accepted inboxAfterInitial(recentPostsCache,
if queueJson['post'].get('capability'):
capabilityIdList = queueJson['post']['capability']
# does the capability id list within the post
# contain the id of the recipient with this handle?
# Here the capability id begins with the handle,
# so this could also be matched separately, but it's
# probably not necessary
if capsId in capabilityIdList:
inboxAfterCapabilities(recentPostsCache,
maxRecentPosts, maxRecentPosts,
session, keyId, handle, session, keyId, handle,
queueJson['post'], queueJson['post'],
@ -2963,32 +2827,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
domain, domain,
onionDomain, i2pDomain, onionDomain, i2pDomain,
port, proxyType, port, proxyType,
federationList, ocapAlways, federationList,
debug, acceptedCaps, debug,
queueFilename, destination,
maxReplies, allowDeletion,
maxMentions, maxEmoji,
translate, unitTest,
YTReplacementDomain)
else:
print('Queue: object capabilities check has failed')
if debug:
pprint(queueJson['post'])
else:
if not ocapAlways:
inboxAfterCapabilities(recentPostsCache,
maxRecentPosts,
session, keyId, handle,
queueJson['post'],
baseDir, httpPrefix,
sendThreads, postLog,
cachedWebfingers,
personCache, queue,
domain,
onionDomain, i2pDomain,
port, proxyType,
federationList, ocapAlways,
debug, acceptedCaps,
queueFilename, destination, queueFilename, destination,
maxReplies, allowDeletion, maxReplies, allowDeletion,
maxMentions, maxEmoji, maxMentions, maxEmoji,
@ -2996,8 +2836,6 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
YTReplacementDomain) YTReplacementDomain)
if debug: if debug:
pprint(queueJson['post']) pprint(queueJson['post'])
print('No capability list within post')
print('ocapAlways: ' + str(ocapAlways))
print('Queue: Queue post accepted') print('Queue: Queue post accepted')
if os.path.isfile(queueFilename): if os.path.isfile(queueFilename):

16
like.py
View File

@ -63,7 +63,7 @@ def like(recentPostsCache: {},
'to' might be a specific person (actor) whose post was liked 'to' might be a specific person (actor) whose post was liked
object is typically the url of the message which was liked object is typically the url of the message which was liked
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
fullDomain = domain fullDomain = domain
@ -162,7 +162,7 @@ def undolike(recentPostsCache: {},
'to' might be a specific person (actor) whose post was liked 'to' might be a specific person (actor) whose post was liked
object is typically the url of the message which was liked object is typically the url of the message which was liked
""" """
if not urlPermitted(objectUrl, federationList, "inbox:write"): if not urlPermitted(objectUrl, federationList):
return None return None
fullDomain = domain fullDomain = domain
@ -267,8 +267,7 @@ def sendLikeViaServer(baseDir: str, session,
postToBox = 'outbox' postToBox = 'outbox'
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId, (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
sharedInbox, capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
@ -291,8 +290,7 @@ def sendLikeViaServer(baseDir: str, session,
'Content-type': 'application/json', 'Content-type': 'application/json',
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newLikeJson, [], inboxUrl, postResult = postJson(session, newLikeJson, [], inboxUrl, headers)
headers, "inbox:write")
if not postResult: if not postResult:
print('WARN: POST announce failed for c2s to ' + inboxUrl) print('WARN: POST announce failed for c2s to ' + inboxUrl)
return 5 return 5
@ -352,8 +350,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
postToBox = 'outbox' postToBox = 'outbox'
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId, (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
sharedInbox, capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, fromNickname, httpPrefix, fromNickname,
@ -375,8 +372,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
'Content-type': 'application/json', 'Content-type': 'application/json',
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newUndoLikeJson, [], inboxUrl, postResult = postJson(session, newUndoLikeJson, [], inboxUrl, headers)
headers, "inbox:write")
if not postResult: if not postResult:
print('WARN: POST announce failed for c2s to ' + inboxUrl) print('WARN: POST announce failed for c2s to ' + inboxUrl)
return 5 return 5

View File

@ -85,7 +85,6 @@ def manualApproveFollowRequest(session, baseDir: str,
federationList: [], federationList: [],
sendThreads: [], postLog: [], sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
acceptedCaps: [],
debug: bool, debug: bool,
projectVersion: str) -> None: projectVersion: str) -> None:
"""Manually approve a follow request """Manually approve a follow request
@ -142,7 +141,7 @@ def manualApproveFollowRequest(session, baseDir: str,
approvePort, approvePort,
followJson['actor'], followJson['actor'],
federationList, federationList,
followJson, acceptedCaps, followJson,
sendThreads, postLog, sendThreads, postLog,
cachedWebfingers, personCache, cachedWebfingers, personCache,
debug, projectVersion, False) debug, projectVersion, False)

180
ocaps.md
View File

@ -1,180 +0,0 @@
# Object Capabilities Prototype
This is one proposed way that OCAP could work.
## TL;DR
* Works from person to person, not instance to instance. Actor-oriented capabilities.
* Produces negligible additional network traffic, although see the proviso for shared inbox
* Doesn't require any additional encryption to be performed
* Works in the same way between people on different instances or the same instance
* People can alter what their followers can do on an individual basis
* Leverages the existing follow request mechanism
## Workflow
Default capabilities are initially set up when a follow request is made. The Accept activity sent back from a follow request can be received by any instance. A capabilities accept activity is attached to the follow accept.
``` text
Alice
|
V
Follow Request
|
V
Bob
|
V
Create/store default Capabilities
for Alice
|
V
Follow Accept + default Capabilities
|
V
Alice
|
V
Store Granted Capabilities
```
The default capabilities could be *any preferred policy* of the instance. They could be no capabilities at all, read only or full access to everything.
Example Follow request from **Alice** to **Bob**:
``` json
{'actor': 'http://alicedomain.net/users/alice',
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'id': 'http://alicedomain.net/users/alice/statuses/1562507338839876',
'object': 'http://bobdomain.net/users/bob',
'published': '2019-07-07T13:48:58Z',
'to': ['http://bobdomain.net/users/bob'],
'type': 'Follow'}
```
Follow Accept from **Bob** to **Alice** with attached capabilities.
``` json
{'actor': 'http://bobdomain.net/users/bob',
'capabilities': {'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'},
'cc': [],
'object': {'actor': 'http://alicedomain.net/users/alice',
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'id': 'http://alicedomain.net/users/alice/statuses/1562507338839876',
'object': 'http://bobdomain.net/users/bob',
'published': '2019-07-07T13:48:58Z',
'to': ['http://bobdomain.net/users/bob'],
'type': 'Follow'},
'to': ['http://alicedomain.net/users/alice'],
'type': 'Accept'}
```
When posts are subsequently sent from the following instance (server-to-server) they should have the corresponding capability id string attached within the Create wrapper. To handle the *shared inbox* scenario this should be a list rather than a single string. In the above example that would be *['http://bobdomain.net/caps/alice@alicedomain.net#rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ']*. It should contain a random token which is hard to guess by brute force methods.
NOTE: the token should be random and not a hash of anything. Making it a hash would give an adversary a much better chance of calculating it.
``` text
Alice
|
V
Send Post
Attach id from Stored Capabilities
granted by Bob
|
V
Bob
|
V
http signature check
|
V
Check Capability id matches
stored capabilities
|
V
Match stored capability scope
against actor on received post
|
V
Check that stored capability
contains inbox:write, etc
|
V
Any other checks
|
V
Accept incoming post
```
Subsequently **Bob** could change the stored capabilities for **Alice** in their database, giving the new object a different id. This could be sent back to **Alice** as an **Update** activity with attached capability.
Bob can send this to Alice, altering *capability* to now include *inbox:noreply*. Notice that the random token at the end of the *id* has changed, so that Alice can't continue to use the old capabilities.
``` json
{'actor': 'http://bobdomain.net/users/bob',
'cc': [],
'object': {'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read', 'inbox:noreply'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#53nwZhHipNFCNwrJ2sgE8GPx13SnV23X',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'},
'to': ['http://alicedomain.net/users/alice'],
'type': 'Update'}
```
Alice then receives this and updates her capabilities granted by Bob to:
``` json
{'actor': 'http://bobdomain.net/users/bob',
'capability': ['inbox:write', 'objects:read', 'inbox:noreply'],
'id': 'http://bobdomain.net/caps/alice@alicedomain.net#53nwZhHipNFCNwrJ2sgE8GPx13SnV23X',
'scope': 'http://alicedomain.net/users/alice',
'type': 'Capability'}
```
If she sets her system to somehow ignore the update then if capabilities are strictly enforced she will no longer be able to send messages to Bob's inbox.
Object capabilities can be strictly enforced by adding the **--ocap** option when running the server. The only activities which it is not enforced upon are **Follow** and **Accept**. Anyone can create a follow request or accept updated capabilities.
## Object capabilities in the shared inbox scenario
Shared inboxes are obviously essential for any kind of scalability, otherwise there would be vast amounts of duplicated messages being dumped onto the intertubes like a big truck.
With the shared inbox instead of sending from Alice to 500 of her fans on a different instance - repeatedly sending the same message to individual inboxes - a single message is sent to its shared inbox (which has its own special account called 'inbox') and it then decides how to distribute that. If a list of capability ids is attached to the message which gets sent to the shared inbox then the receiving server can use that.
When a post arrives in the shared inbox it is checked to see that at least one follower exists for it. If there are only a small number of followers then it is treated like a direct message and copied separately to individual account inboxes after capabilities checks. For larger numbers of followers the capabilities checks are done at the time when the inbox is fetched. This avoids a lot of duplicated storage of posts.
A potential down side is that for popular accounts with many followers the number of capabilities ids (one for each follower on the receiving server) on a post sent to the shared inbox could be large. However, in terms of bandwidth it may still not be very significant compared to heavyweight websites containing a lot of javascript.
## Some capabilities
*inbox:write* - follower can post anything to your inbox
*inbox:noreply* - follower can't reply to your posts
*inbox:nolike* - follower can't like your posts
*inbox:nopics* - follower can't post image links
*inbox:noannounce* - follower can't send repeats (announce activities) to your inbox
*inbox:cw* - follower can't post to your inbox unless they include a content warning
## Object capabilities adversaries
If **Eve** subsequently learns what the capabilities id is for **Alice** by somehow intercepting the traffic (eg. suppose she works for *Eveflare*) then she can't gain the capabilities of Alice due to the *scope* parameter against which the actors of incoming posts are checked.
**Eve** could create a post pretending to be from Alice's domain, but the http signature check would fail due to her not having Alice's keys.
The only scenarios in which Eve might triumph would be if she could also do DNS highjacking and:
* Bob isn't storing Alice's public key and looks it up repeatedly
* Alice and Bob's instances are foolishly configured to perform *blind key rotation* such that her being in the middle is indistinguishable from expected key changes
Even if Eve has an account on Alice's instance this won't help her very much unless she can get write access to the database.
Another scenario is that you grant capabilities to an account on a hostile instance. The hostile instance then shares the resulting token with all other accounts on it. Potentially those other accounts might be able to gain capabilities which they havn't been granted *but only if they also have identical signing keys*. Checking for public key duplication on the instance granting capabilities could mitigate this. At the point at which a capabilities request is made are there any other known accounts with the same public key? Since actors are public it would also be possible to automatically scan for the existence of instances with duplicated signing keys.

View File

@ -39,8 +39,7 @@ from shares import outboxUndoShareUpload
def postMessageToOutbox(messageJson: {}, postToNickname: str, def postMessageToOutbox(messageJson: {}, postToNickname: str,
server, baseDir: str, httpPrefix: str, server, baseDir: str, httpPrefix: str,
domain: str, domainFull: str, domain: str, domainFull: str,
onionDomain: str, i2pDomain: str, onionDomain: str, i2pDomain: str, port: int,
port: int,
recentPostsCache: {}, followersThreads: [], recentPostsCache: {}, followersThreads: [],
federationList: [], sendThreads: [], federationList: [], sendThreads: [],
postLog: [], cachedWebfingers: {}, postLog: [], cachedWebfingers: {},

View File

@ -259,7 +259,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
'id': personId+'/endpoints', 'id': personId+'/endpoints',
'sharedInbox': httpPrefix+'://'+domain+'/inbox', 'sharedInbox': httpPrefix+'://'+domain+'/inbox',
}, },
'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
'followers': personId+'/followers', 'followers': personId+'/followers',
'following': personId+'/following', 'following': personId+'/following',
'shares': personId+'/shares', 'shares': personId+'/shares',
@ -327,8 +326,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
if not os.path.isdir(baseDir + peopleSubdir + '/' + if not os.path.isdir(baseDir + peopleSubdir + '/' +
handle + '/outbox'): handle + '/outbox'):
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox') os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox')
if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/ocap'):
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/ocap')
if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'): if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'):
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue') os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
filename = baseDir + peopleSubdir + '/' + handle + '.json' filename = baseDir + peopleSubdir + '/' + handle + '.json'
@ -506,15 +503,6 @@ def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int,
True, True, None) True, True, None)
def createCapabilitiesInbox(baseDir: str, nickname: str,
domain: str, port: int,
httpPrefix: str) -> (str, str, {}, {}):
"""Generates the capabilities inbox to sign requests
"""
return createPersonBase(baseDir, nickname, domain, port,
httpPrefix, True, True, None)
def personUpgradeActor(baseDir: str, personJson: {}, def personUpgradeActor(baseDir: str, personJson: {},
handle: str, filename: str) -> None: handle: str, filename: str) -> None:
"""Alter the actor to add any new properties """Alter the actor to add any new properties
@ -598,7 +586,7 @@ def personLookup(domain: str, path: str, baseDir: str) -> {}:
def personBoxJson(recentPostsCache: {}, def personBoxJson(recentPostsCache: {},
session, baseDir: str, domain: str, port: int, path: str, session, baseDir: str, domain: str, port: int, path: str,
httpPrefix: str, noOfItems: int, boxname: str, httpPrefix: str, noOfItems: int, boxname: str,
authorized: bool, ocapAlways: bool) -> {}: authorized: bool) -> {}:
"""Obtain the inbox/outbox/moderation feed for the given person """Obtain the inbox/outbox/moderation feed for the given person
""" """
if boxname != 'inbox' and boxname != 'dm' and \ if boxname != 'inbox' and boxname != 'dm' and \
@ -644,38 +632,36 @@ def personBoxJson(recentPostsCache: {},
return createInbox(recentPostsCache, return createInbox(recentPostsCache,
session, baseDir, nickname, domain, port, session, baseDir, nickname, domain, port,
httpPrefix, httpPrefix,
noOfItems, headerOnly, ocapAlways, pageNumber) noOfItems, headerOnly, pageNumber)
elif boxname == 'dm': elif boxname == 'dm':
return createDMTimeline(recentPostsCache, return createDMTimeline(recentPostsCache,
session, baseDir, nickname, domain, port, session, baseDir, nickname, domain, port,
httpPrefix, httpPrefix,
noOfItems, headerOnly, ocapAlways, pageNumber) noOfItems, headerOnly, pageNumber)
elif boxname == 'tlbookmarks' or boxname == 'bookmarks': elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
return createBookmarksTimeline(session, baseDir, nickname, domain, return createBookmarksTimeline(session, baseDir, nickname, domain,
port, httpPrefix, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, noOfItems, headerOnly,
pageNumber) pageNumber)
elif boxname == 'tlevents': elif boxname == 'tlevents':
return createEventsTimeline(recentPostsCache, return createEventsTimeline(recentPostsCache,
session, baseDir, nickname, domain, session, baseDir, nickname, domain,
port, httpPrefix, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, noOfItems, headerOnly,
pageNumber) pageNumber)
elif boxname == 'tlreplies': elif boxname == 'tlreplies':
return createRepliesTimeline(recentPostsCache, return createRepliesTimeline(recentPostsCache,
session, baseDir, nickname, domain, session, baseDir, nickname, domain,
port, httpPrefix, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, noOfItems, headerOnly,
pageNumber) pageNumber)
elif boxname == 'tlmedia': elif boxname == 'tlmedia':
return createMediaTimeline(session, baseDir, nickname, domain, port, return createMediaTimeline(session, baseDir, nickname, domain, port,
httpPrefix, httpPrefix, noOfItems, headerOnly,
noOfItems, headerOnly, ocapAlways,
pageNumber) pageNumber)
elif boxname == 'tlblogs': elif boxname == 'tlblogs':
return createBlogsTimeline(session, baseDir, nickname, domain, port, return createBlogsTimeline(session, baseDir, nickname, domain, port,
httpPrefix, httpPrefix, noOfItems, headerOnly,
noOfItems, headerOnly, ocapAlways,
pageNumber) pageNumber)
elif boxname == 'outbox': elif boxname == 'outbox':
return createOutbox(session, baseDir, nickname, domain, port, return createOutbox(session, baseDir, nickname, domain, port,
@ -685,14 +671,14 @@ def personBoxJson(recentPostsCache: {},
elif boxname == 'moderation': elif boxname == 'moderation':
return createModeration(baseDir, nickname, domain, port, return createModeration(baseDir, nickname, domain, port,
httpPrefix, httpPrefix,
noOfItems, headerOnly, authorized, noOfItems, headerOnly,
pageNumber) pageNumber)
return None return None
def personInboxJson(recentPostsCache: {}, def personInboxJson(recentPostsCache: {},
baseDir: str, domain: str, port: int, path: str, baseDir: str, domain: str, port: int, path: str,
httpPrefix: str, noOfItems: int, ocapAlways: bool) -> []: httpPrefix: str, noOfItems: int) -> []:
"""Obtain the inbox feed for the given person """Obtain the inbox feed for the given person
Authentication is expected to have already happened Authentication is expected to have already happened
""" """
@ -729,7 +715,7 @@ def personInboxJson(recentPostsCache: {},
return None return None
return createInbox(recentPostsCache, baseDir, nickname, return createInbox(recentPostsCache, baseDir, nickname,
domain, port, httpPrefix, domain, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, pageNumber) noOfItems, headerOnly, pageNumber)
def setDisplayNickname(baseDir: str, nickname: str, domain: str, def setDisplayNickname(baseDir: str, nickname: str, domain: str,

297
posts.py
View File

@ -14,6 +14,7 @@ import shutil
import sys import sys
import time import time
import uuid import uuid
import random
from socket import error as SocketError from socket import error as SocketError
from time import gmtime, strftime from time import gmtime, strftime
from collections import OrderedDict from collections import OrderedDict
@ -29,6 +30,8 @@ from session import postJsonString
from session import postImage from session import postImage
from webfinger import webfingerHandle from webfinger import webfingerHandle
from httpsig import createSignedHeader from httpsig import createSignedHeader
from utils import getFollowersList
from utils import isEvil
from utils import removeIdEnding from utils import removeIdEnding
from utils import siteIsActive from utils import siteIsActive
from utils import getCachedPostFilename from utils import getCachedPostFilename
@ -42,8 +45,6 @@ from utils import validNickname
from utils import locatePost from utils import locatePost
from utils import loadJson from utils import loadJson
from utils import saveJson from utils import saveJson
from capabilities import getOcapFilename
from capabilities import capabilitiesUpdate
from media import attachMedia from media import attachMedia
from media import replaceYouTube from media import replaceYouTube
from content import removeHtml from content import removeHtml
@ -207,7 +208,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
else: else:
personUrl = httpPrefix + '://' + domain + '/users/' + nickname personUrl = httpPrefix + '://' + domain + '/users/' + nickname
if not personUrl: if not personUrl:
return None, None, None, None, None, None, None, None return None, None, None, None, None, None, None
personJson = \ personJson = \
getPersonFromCache(baseDir, personUrl, personCache, True) getPersonFromCache(baseDir, personUrl, personCache, True)
if not personJson: if not personJson:
@ -225,7 +226,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
projectVersion, httpPrefix, domain) projectVersion, httpPrefix, domain)
if not personJson: if not personJson:
print('Unable to get actor') print('Unable to get actor')
return None, None, None, None, None, None, None, None return None, None, None, None, None, None, None
boxJson = None boxJson = None
if not personJson.get(boxName): if not personJson.get(boxName):
if personJson.get('endpoints'): if personJson.get('endpoints'):
@ -235,7 +236,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
boxJson = personJson[boxName] boxJson = personJson[boxName]
if not boxJson: if not boxJson:
return None, None, None, None, None, None, None, None return None, None, None, None, None, None, None
personId = None personId = None
if personJson.get('id'): if personJson.get('id'):
@ -254,9 +255,6 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
if personJson.get('endpoints'): if personJson.get('endpoints'):
if personJson['endpoints'].get('sharedInbox'): if personJson['endpoints'].get('sharedInbox'):
sharedInbox = personJson['endpoints']['sharedInbox'] sharedInbox = personJson['endpoints']['sharedInbox']
capabilityAcquisition = None
if personJson.get('capabilityAcquisitionEndpoint'):
capabilityAcquisition = personJson['capabilityAcquisitionEndpoint']
avatarUrl = None avatarUrl = None
if personJson.get('icon'): if personJson.get('icon'):
if personJson['icon'].get('url'): if personJson['icon'].get('url'):
@ -268,7 +266,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
storePersonInCache(baseDir, personUrl, personJson, personCache, True) storePersonInCache(baseDir, personUrl, personJson, personCache, True)
return boxJson, pubKeyId, pubKey, personId, sharedInbox, \ return boxJson, pubKeyId, pubKey, personId, sharedInbox, \
capabilityAcquisition, avatarUrl, displayName avatarUrl, displayName
def getPosts(session, outboxUrl: str, maxPosts: int, def getPosts(session, outboxUrl: str, maxPosts: int,
@ -890,24 +888,12 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
if not clientToServer: if not clientToServer:
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
# if capabilities have been granted for this actor
# then get the corresponding id
capabilityIdList = []
ocapFilename = getOcapFilename(baseDir, nickname, domain,
toUrl, 'granted')
if ocapFilename:
if os.path.isfile(ocapFilename):
oc = loadJson(ocapFilename)
if oc:
if oc.get('id'):
capabilityIdList = [oc['id']]
idStr = \ idStr = \
httpPrefix + '://' + domain + '/users/' + nickname + \ httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber + '/replies' '/statuses/' + statusNumber + '/replies'
newPost = { newPost = {
'@context': postContext, '@context': postContext,
'id': newPostId + '/activity', 'id': newPostId + '/activity',
'capability': capabilityIdList,
'type': 'Create', 'type': 'Create',
'actor': actorUrl, 'actor': actorUrl,
'published': published, 'published': published,
@ -1072,11 +1058,9 @@ def outboxMessageCreateWrap(httpPrefix: str,
cc = [] cc = []
if messageJson.get('cc'): if messageJson.get('cc'):
cc = messageJson['cc'] cc = messageJson['cc']
capabilityUrl = []
newPost = { newPost = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
'id': newPostId + '/activity', 'id': newPostId + '/activity',
'capability': capabilityUrl,
'type': 'Create', 'type': 'Create',
'actor': httpPrefix + '://' + domain + '/users/' + nickname, 'actor': httpPrefix + '://' + domain + '/users/' + nickname,
'published': published, 'published': published,
@ -1580,7 +1564,7 @@ def threadSendPost(session, postJsonStr: str, federationList: [],
postResult, unauthorized = \ postResult, unauthorized = \
postJsonString(session, postJsonStr, federationList, postJsonString(session, postJsonStr, federationList,
inboxUrl, signatureHeaderJson, inboxUrl, signatureHeaderJson,
"inbox:write", debug) debug)
except Exception as e: except Exception as e:
print('ERROR: postJsonString failed ' + str(e)) print('ERROR: postJsonString failed ' + str(e))
if unauthorized: if unauthorized:
@ -1665,26 +1649,18 @@ def sendPost(projectVersion: str,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
toPersonId, sharedInbox, toPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
nickname, domain, postToBox) nickname, domain, postToBox)
# If there are more than one followers on the target domain
# then send to the shared inbox indead of the individual inbox
if nickname == 'capabilities':
inboxUrl = capabilityAcquisition
if not capabilityAcquisition:
return 2
if not inboxUrl: if not inboxUrl:
return 3 return 3
if not pubKey: if not pubKey:
return 4 return 4
if not toPersonId: if not toPersonId:
return 5 return 5
# sharedInbox and capabilities are optional # sharedInbox is optional
postJsonObject = \ postJsonObject = \
createPostBase(baseDir, nickname, domain, port, createPostBase(baseDir, nickname, domain, port,
@ -1790,7 +1766,6 @@ def sendPostViaServer(projectVersion: str,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
@ -1856,7 +1831,7 @@ def sendPostViaServer(projectVersion: str,
} }
postResult = \ postResult = \
postImage(session, attachImageFilename, [], postImage(session, attachImageFilename, [],
inboxUrl, headers, "inbox:write") inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: Failed to upload image') print('DEBUG: Failed to upload image')
@ -1869,7 +1844,7 @@ def sendPostViaServer(projectVersion: str,
} }
postResult = \ postResult = \
postJsonString(session, json.dumps(postJsonObject), [], postJsonString(session, json.dumps(postJsonObject), [],
inboxUrl, headers, "inbox:write", debug) inboxUrl, headers, debug)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST failed for c2s to '+inboxUrl) print('DEBUG: POST failed for c2s to '+inboxUrl)
@ -2000,19 +1975,13 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
else: else:
postToBox = 'outbox' postToBox = 'outbox'
# get the actor inbox/outbox/capabilities for the To handle # get the actor inbox/outbox for the To handle
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl, (inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
nickname, domain, postToBox) nickname, domain, postToBox)
if nickname == 'capabilities':
inboxUrl = capabilityAcquisition
if not capabilityAcquisition:
return 2
else:
print("inboxUrl: " + str(inboxUrl)) print("inboxUrl: " + str(inboxUrl))
print("toPersonId: " + str(toPersonId)) print("toPersonId: " + str(toPersonId))
print("sharedInboxUrl: " + str(sharedInboxUrl)) print("sharedInboxUrl: " + str(sharedInboxUrl))
@ -2036,7 +2005,7 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
if debug: if debug:
print('DEBUG: missing personId') print('DEBUG: missing personId')
return 5 return 5
# sharedInbox and capabilities are optional # sharedInbox is optional
# get the senders private key # get the senders private key
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug) privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug)
@ -2470,75 +2439,69 @@ def sendToFollowersThread(session, baseDir: str,
def createInbox(recentPostsCache: {}, def createInbox(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, port: int, session, baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool, httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}: pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, return createBoxIndexed(recentPostsCache,
session, baseDir, 'inbox', session, baseDir, 'inbox',
nickname, domain, port, httpPrefix, nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True, itemsPerPage, headerOnly, True,
ocapAlways, pageNumber) pageNumber)
def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str, def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlbookmarks', return createBoxIndexed({}, session, baseDir, 'tlbookmarks',
nickname, domain, nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly, port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber) True, pageNumber)
def createEventsTimeline(recentPostsCache: {}, def createEventsTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents', return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents',
nickname, domain, nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly, port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber) True, pageNumber)
def createDMTimeline(recentPostsCache: {}, def createDMTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, return createBoxIndexed(recentPostsCache,
session, baseDir, 'dm', nickname, session, baseDir, 'dm', nickname,
domain, port, httpPrefix, itemsPerPage, domain, port, httpPrefix, itemsPerPage,
headerOnly, True, ocapAlways, pageNumber) headerOnly, True, pageNumber)
def createRepliesTimeline(recentPostsCache: {}, def createRepliesTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies', return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
nickname, domain, port, httpPrefix, nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True, itemsPerPage, headerOnly, True,
ocapAlways, pageNumber) pageNumber)
def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str, def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname, return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname,
domain, port, httpPrefix, domain, port, httpPrefix,
itemsPerPage, headerOnly, True, itemsPerPage, headerOnly, True,
ocapAlways, pageNumber) pageNumber)
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str, def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname, return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname,
domain, port, httpPrefix, domain, port, httpPrefix,
itemsPerPage, headerOnly, True, itemsPerPage, headerOnly, True,
ocapAlways, pageNumber) pageNumber)
def createOutbox(session, baseDir: str, nickname: str, domain: str, def createOutbox(session, baseDir: str, nickname: str, domain: str,
@ -2548,12 +2511,12 @@ def createOutbox(session, baseDir: str, nickname: str, domain: str,
return createBoxIndexed({}, session, baseDir, 'outbox', return createBoxIndexed({}, session, baseDir, 'outbox',
nickname, domain, port, httpPrefix, nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, authorized, itemsPerPage, headerOnly, authorized,
False, pageNumber) pageNumber)
def createModeration(baseDir: str, nickname: str, domain: str, port: int, def createModeration(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool, httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}: pageNumber=None) -> {}:
boxDir = createPersonDir(nickname, domain, baseDir, 'inbox') boxDir = createPersonDir(nickname, domain, baseDir, 'inbox')
boxname = 'moderation' boxname = 'moderation'
@ -2751,8 +2714,7 @@ def createBoxIndex(boxDir: str, postsInBoxDict: {}) -> int:
def createSharedInboxIndex(baseDir: str, sharedBoxDir: str, def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
postsInBoxDict: {}, postsCtr: int, postsInBoxDict: {}, postsCtr: int,
nickname: str, domain: str, nickname: str, domain: str) -> int:
ocapAlways: bool) -> int:
""" Creates an index for the given shared inbox """ Creates an index for the given shared inbox
""" """
handle = nickname + '@' + domain handle = nickname + '@' + domain
@ -2788,30 +2750,6 @@ def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
if actorNickname + '@' + actorDomain not in followingHandles: if actorNickname + '@' + actorDomain not in followingHandles:
continue continue
if ocapAlways:
capsList = None
# Note: should this be in the Create or the object of a post?
if postJsonObject.get('capability'):
if isinstance(postJsonObject['capability'], list):
capsList = postJsonObject['capability']
# Have capabilities been granted for the sender?
ocapFilename = \
baseDir + '/accounts/' + handle + '/ocap/granted/' + \
postJsonObject['actor'].replace('/', '#') + '.json'
if not os.path.isfile(ocapFilename):
continue
# read the capabilities id
ocapJson = loadJson(ocapFilename, 0)
if not ocapJson:
print('WARN: json load exception createSharedInboxIndex')
else:
if ocapJson.get('id'):
if ocapJson['id'] in capsList:
postsInBoxDict[statusNumber] = sharedInboxFilename
postsCtr += 1
else:
postsInBoxDict[statusNumber] = sharedInboxFilename postsInBoxDict[statusNumber] = sharedInboxFilename
postsCtr += 1 postsCtr += 1
return postsCtr return postsCtr
@ -2866,7 +2804,7 @@ def createBoxIndexed(recentPostsCache: {},
session, baseDir: str, boxname: str, session, baseDir: str, boxname: str,
nickname: str, domain: str, port: int, httpPrefix: str, nickname: str, domain: str, port: int, httpPrefix: str,
itemsPerPage: int, headerOnly: bool, authorized: bool, itemsPerPage: int, headerOnly: bool, authorized: bool,
ocapAlways: bool, pageNumber=None) -> {}: pageNumber=None) -> {}:
"""Constructs the box feed for a person with the given nickname """Constructs the box feed for a person with the given nickname
""" """
if not authorized or not pageNumber: if not authorized or not pageNumber:
@ -3005,10 +2943,6 @@ def createBoxIndexed(recentPostsCache: {},
except BaseException: except BaseException:
continue continue
# remove any capability so that it's not displayed
if p.get('capability'):
del p['capability']
# Don't show likes, replies or shares (announces) to # Don't show likes, replies or shares (announces) to
# unauthorized viewers # unauthorized viewers
if not authorized: if not authorized:
@ -3226,7 +3160,6 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
(personUrl, pubKeyId, pubKey, (personUrl, pubKeyId, pubKey,
personId, shaedInbox, personId, shaedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
@ -3240,12 +3173,13 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
projectVersion, httpPrefix, domain) projectVersion, httpPrefix, domain)
def getPublicPostDomains(baseDir: str, nickname: str, domain: str, def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str, proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str, debug: bool, projectVersion: str,
domainList=[]) -> []: domainList=[]) -> []:
""" Returns a list of domains referenced within public posts """ Returns a list of domains referenced within public posts
""" """
if not session:
session = createSession(proxyType) session = createSession(proxyType)
if not session: if not session:
return domainList return domainList
@ -3270,8 +3204,7 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
return domainList return domainList
(personUrl, pubKeyId, pubKey, (personUrl, pubKeyId, pubKey,
personId, shaedInbox, personId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
@ -3288,43 +3221,125 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
return postDomains return postDomains
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str, def getPublicPostDomainsBlocked(session, baseDir: str,
nickname: str, domain: str, port: int, nickname: str, domain: str,
followerUrl, updateCaps: [], proxyType: str, port: int, httpPrefix: str,
sendThreads: [], postLog: [], debug: bool, projectVersion: str,
cachedWebfingers: {}, personCache: {}, domainList=[]) -> []:
federationList: [], debug: bool, """ Returns a list of domains referenced within public posts which
projectVersion: str) -> int: are globally blocked on this instance
"""When the capabilities for a follower are changed this
sends out an update. followerUrl is the actor of the follower.
""" """
updateJson = \ postDomains = \
capabilitiesUpdate(baseDir, httpPrefix, getPublicPostDomains(session, baseDir, nickname, domain,
nickname, domain, port, proxyType, port, httpPrefix,
followerUrl, updateCaps) debug, projectVersion,
domainList)
if not postDomains:
return []
if not updateJson: blockingFilename = baseDir + '/accounts/blocking.txt'
return 1 if not os.path.isfile(blockingFilename):
return []
if debug: # read the blocked domains as a single string
pprint(updateJson) blockedStr = ''
print('DEBUG: sending capabilities update from ' + with open(blockingFilename, 'r') as fp:
nickname + '@' + domain + ' port ' + str(port) + blockedStr = fp.read()
' to ' + followerUrl)
clientToServer = False blockedDomains = []
followerNickname = getNicknameFromActor(followerUrl) for domainName in postDomains:
if not followerNickname: if '@' not in domainName:
print('WARN: unable to find nickname in ' + followerUrl) continue
return 1 # get the domain after the @
followerDomain, followerPort = getDomainFromActor(followerUrl) domainName = domainName.split('@')[1].strip()
return sendSignedJson(updateJson, session, baseDir, if isEvil(domainName):
nickname, domain, port, blockedDomains.append(domainName)
followerNickname, followerDomain, followerPort, '', continue
httpPrefix, True, clientToServer, if domainName in blockedStr:
federationList, blockedDomains.append(domainName)
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion) return blockedDomains
def getNonMutualsOfPerson(baseDir: str,
nickname: str, domain: str) -> []:
"""Returns the followers who are not mutuals of a person
i.e. accounts which follow you but you don't follow them
"""
followers = \
getFollowersList(baseDir, nickname, domain, 'followers.txt')
following = \
getFollowersList(baseDir, nickname, domain, 'following.txt')
nonMutuals = []
for handle in followers:
if handle not in following:
nonMutuals.append(handle)
return nonMutuals
def checkDomains(session, baseDir: str,
nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str,
maxBlockedDomains: int, singleCheck: bool):
"""Checks follower accounts for references to globally blocked domains
"""
nonMutuals = getNonMutualsOfPerson(baseDir, nickname, domain)
if not nonMutuals:
print('No non-mutual followers were found')
return
followerWarningFilename = baseDir + '/accounts/followerWarnings.txt'
updateFollowerWarnings = False
followerWarningStr = ''
if os.path.isfile(followerWarningFilename):
with open(followerWarningFilename, 'r') as fp:
followerWarningStr = fp.read()
if singleCheck:
# checks a single random non-mutual
index = random.randrange(0, len(nonMutuals))
handle = nonMutuals[index]
if '@' in handle:
nonMutualNickname = handle.split('@')[0]
nonMutualDomain = handle.split('@')[1].strip()
blockedDomains = \
getPublicPostDomainsBlocked(session, baseDir,
nonMutualNickname,
nonMutualDomain,
proxyType, port, httpPrefix,
debug, projectVersion, [])
if blockedDomains:
if len(blockedDomains) > maxBlockedDomains:
followerWarningStr += handle + '\n'
updateFollowerWarnings = True
else:
# checks all non-mutuals
for handle in nonMutuals:
if '@' not in handle:
continue
if handle in followerWarningStr:
continue
nonMutualNickname = handle.split('@')[0]
nonMutualDomain = handle.split('@')[1].strip()
blockedDomains = \
getPublicPostDomainsBlocked(session, baseDir,
nonMutualNickname,
nonMutualDomain,
proxyType, port, httpPrefix,
debug, projectVersion, [])
if blockedDomains:
print(handle)
for d in blockedDomains:
print(' ' + d)
if len(blockedDomains) > maxBlockedDomains:
followerWarningStr += handle + '\n'
updateFollowerWarnings = True
if updateFollowerWarnings and followerWarningStr:
with open(followerWarningFilename, 'w+') as fp:
fp.write(followerWarningStr)
if not singleCheck:
print(followerWarningStr)
def populateRepliesJson(baseDir: str, nickname: str, domain: str, def populateRepliesJson(baseDir: str, nickname: str, domain: str,
@ -3692,8 +3707,7 @@ def sendBlockViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
@ -3715,8 +3729,7 @@ def sendBlockViaServer(baseDir: str, session,
'Content-type': 'application/json', 'Content-type': 'application/json',
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newBlockJson, [], inboxUrl, postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
headers, "inbox:write")
if not postResult: if not postResult:
print('WARN: Unable to post block') print('WARN: Unable to post block')
@ -3781,8 +3794,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox, avatarUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache, displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname, projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox) fromDomain, postToBox)
@ -3803,8 +3815,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
'Content-type': 'application/json', 'Content-type': 'application/json',
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = postJson(session, newBlockJson, [], inboxUrl, postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
headers, "inbox:write")
if not postResult: if not postResult:
print('WARN: Unable to post block') print('WARN: Unable to post block')

View File

@ -291,7 +291,6 @@ def sendRoleViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, avatarUrl, displayName) = getPersonBox(baseDir, session,
wfRequest, personCache, wfRequest, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,
@ -315,7 +314,7 @@ def sendRoleViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, newRoleJson, [], inboxUrl, headers, "inbox:write") postJson(session, newRoleJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to '+inboxUrl) print('DEBUG: POST announce failed for c2s to '+inboxUrl)

View File

@ -49,7 +49,7 @@ def createSession(proxyType: str):
session.proxies = {} session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:7777' session.proxies['http'] = 'socks5h://localhost:7777'
session.proxies['https'] = 'socks5h://localhost:7777' session.proxies['https'] = 'socks5h://localhost:7777'
print('New session created with proxy ' + str(proxyType)) # print('New session created with proxy ' + str(proxyType))
return session return session
@ -93,14 +93,11 @@ def getJson(session, url: str, headers: {}, params: {},
def postJson(session, postJsonObject: {}, federationList: [], def postJson(session, postJsonObject: {}, federationList: [],
inboxUrl: str, headers: {}, capability: str) -> str: inboxUrl: str, headers: {}) -> str:
"""Post a json message to the inbox of another person """Post a json message to the inbox of another person
Supplying a capability, such as "inbox:write"
""" """
# always allow capability requests
if not capability.startswith('cap'):
# check that we are posting to a permitted domain # check that we are posting to a permitted domain
if not urlPermitted(inboxUrl, federationList, capability): if not urlPermitted(inboxUrl, federationList):
print('postJson: ' + inboxUrl + ' not permitted') print('postJson: ' + inboxUrl + ' not permitted')
return None return None
@ -132,22 +129,13 @@ def postJsonString(session, postJsonStr: str,
federationList: [], federationList: [],
inboxUrl: str, inboxUrl: str,
headers: {}, headers: {},
capability: str,
debug: bool) -> (bool, bool): debug: bool) -> (bool, bool):
"""Post a json message string to the inbox of another person """Post a json message string to the inbox of another person
Supplying a capability, such as "inbox:write"
The second boolean returned is true if the send is unauthorized The second boolean returned is true if the send is unauthorized
NOTE: Here we post a string rather than the original json so that NOTE: Here we post a string rather than the original json so that
conversions between string and json format don't invalidate conversions between string and json format don't invalidate
the message body digest of http signatures the message body digest of http signatures
""" """
# always allow capability requests
if not capability.startswith('cap'):
# check that we are posting to a permitted domain
if not urlPermitted(inboxUrl, federationList, capability):
print('postJson: ' + inboxUrl + ' not permitted by capabilities')
return None, None
try: try:
postResult = \ postResult = \
session.post(url=inboxUrl, data=postJsonStr, headers=headers) session.post(url=inboxUrl, data=postJsonStr, headers=headers)
@ -181,14 +169,11 @@ def postJsonString(session, postJsonStr: str,
def postImage(session, attachImageFilename: str, federationList: [], def postImage(session, attachImageFilename: str, federationList: [],
inboxUrl: str, headers: {}, capability: str) -> str: inboxUrl: str, headers: {}) -> str:
"""Post an image to the inbox of another person or outbox via c2s """Post an image to the inbox of another person or outbox via c2s
Supplying a capability, such as "inbox:write"
""" """
# always allow capability requests
if not capability.startswith('cap'):
# check that we are posting to a permitted domain # check that we are posting to a permitted domain
if not urlPermitted(inboxUrl, federationList, capability): if not urlPermitted(inboxUrl, federationList):
print('postJson: ' + inboxUrl + ' not permitted') print('postJson: ' + inboxUrl + ' not permitted')
return None return None

View File

@ -380,7 +380,6 @@ def sendShareViaServer(baseDir, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, fromNickname, httpPrefix, fromNickname,
@ -405,7 +404,7 @@ def sendShareViaServer(baseDir, session,
postResult = \ postResult = \
postImage(session, imageFilename, [], postImage(session, imageFilename, [],
inboxUrl.replace('/' + postToBox, '/shares'), inboxUrl.replace('/' + postToBox, '/shares'),
headers, "inbox:write") headers)
headers = { headers = {
'host': fromDomain, 'host': fromDomain,
@ -413,7 +412,7 @@ def sendShareViaServer(baseDir, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, newShareJson, [], inboxUrl, headers, "inbox:write") postJson(session, newShareJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
@ -483,7 +482,6 @@ def sendUndoShareViaServer(baseDir: str, session,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, fromNickname, httpPrefix, fromNickname,
@ -506,7 +504,7 @@ def sendUndoShareViaServer(baseDir: str, session,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, undoShareJson, [], inboxUrl, headers, "inbox:write") postJson(session, undoShareJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)

View File

@ -152,7 +152,6 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str,
# get the actor inbox for the To handle # get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, nickname, domain, httpPrefix, nickname, domain,
@ -175,7 +174,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str,
'Authorization': authHeader 'Authorization': authHeader
} }
postResult = \ postResult = \
postJson(session, newSkillJson, [], inboxUrl, headers, "inbox:write") postJson(session, newSkillJson, [], inboxUrl, headers)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl) print('DEBUG: POST announce failed for c2s to ' + inboxUrl)

View File

@ -65,7 +65,6 @@ def instancesGraph(baseDir: str, handles: str,
(personUrl, pubKeyId, pubKey, (personUrl, pubKeyId, pubKey,
personId, shaedInbox, personId, shaedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,

View File

@ -42,10 +42,10 @@ from utils import copytree
from utils import loadJson from utils import loadJson
from utils import saveJson from utils import saveJson
from utils import getStatusNumber from utils import getStatusNumber
from utils import getFollowersOfPerson
from follow import followerOfPerson from follow import followerOfPerson
from follow import unfollowPerson from follow import unfollowPerson
from follow import unfollowerOfPerson from follow import unfollowerOfPerson
from follow import getFollowersOfPerson
from follow import sendFollowRequest from follow import sendFollowRequest
from person import createPerson from person import createPerson
from person import setDisplayNickname from person import setDisplayNickname
@ -239,7 +239,7 @@ def testThreads():
def createServerAlice(path: str, domain: str, port: int, def createServerAlice(path: str, domain: str, port: int,
bobAddress: str, federationList: [], bobAddress: str, federationList: [],
hasFollows: bool, hasPosts: bool, hasFollows: bool, hasPosts: bool,
ocapAlways: bool, sendThreads: []): sendThreads: []):
print('Creating test server: Alice on port ' + str(port)) print('Creating test server: Alice on port ' + str(port))
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -249,11 +249,6 @@ def createServerAlice(path: str, domain: str, port: int,
httpPrefix = 'http' httpPrefix = 'http'
proxyType = None proxyType = None
password = 'alicepass' password = 'alicepass'
noreply = False
nolike = False
nopics = False
noannounce = False
cw = False
useBlurhash = True useBlurhash = True
maxReplies = 64 maxReplies = 64
domainMaxPostsPerDay = 1000 domainMaxPostsPerDay = 1000
@ -296,7 +291,6 @@ def createServerAlice(path: str, domain: str, port: int,
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, proxyType, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, True, True, False, sendThreads, False, allowDeletion, True, True, False, sendThreads, False,
@ -306,7 +300,7 @@ def createServerAlice(path: str, domain: str, port: int,
def createServerBob(path: str, domain: str, port: int, def createServerBob(path: str, domain: str, port: int,
aliceAddress: str, federationList: [], aliceAddress: str, federationList: [],
hasFollows: bool, hasPosts: bool, hasFollows: bool, hasPosts: bool,
ocapAlways: bool, sendThreads: []): sendThreads: []):
print('Creating test server: Bob on port ' + str(port)) print('Creating test server: Bob on port ' + str(port))
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -317,11 +311,6 @@ def createServerBob(path: str, domain: str, port: int,
proxyType = None proxyType = None
clientToServer = False clientToServer = False
password = 'bobpass' password = 'bobpass'
noreply = False
nolike = False
nopics = False
noannounce = False
cw = False
useBlurhash = False useBlurhash = False
maxReplies = 64 maxReplies = 64
domainMaxPostsPerDay = 1000 domainMaxPostsPerDay = 1000
@ -364,7 +353,6 @@ def createServerBob(path: str, domain: str, port: int,
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, proxyType, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, True, True, False, sendThreads, False, allowDeletion, True, True, False, sendThreads, False,
@ -373,7 +361,7 @@ def createServerBob(path: str, domain: str, port: int,
def createServerEve(path: str, domain: str, port: int, federationList: [], def createServerEve(path: str, domain: str, port: int, federationList: [],
hasFollows: bool, hasPosts: bool, hasFollows: bool, hasPosts: bool,
ocapAlways: bool, sendThreads: []): sendThreads: []):
print('Creating test server: Eve on port ' + str(port)) print('Creating test server: Eve on port ' + str(port))
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -383,11 +371,6 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
httpPrefix = 'http' httpPrefix = 'http'
proxyType = None proxyType = None
password = 'evepass' password = 'evepass'
noreply = False
nolike = False
nopics = False
noannounce = False
cw = False
maxReplies = 64 maxReplies = 64
allowDeletion = True allowDeletion = True
privateKeyPem, publicKeyPem, person, wfEndpoint = \ privateKeyPem, publicKeyPem, person, wfEndpoint = \
@ -406,7 +389,6 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, allowDeletion, True, True, False, proxyType, maxReplies, allowDeletion, True, True, False,
sendThreads, False, False) sendThreads, False, False)
@ -427,8 +409,6 @@ def testPostMessageBetweenServers():
shutil.rmtree(baseDir + '/.tests') shutil.rmtree(baseDir + '/.tests')
os.mkdir(baseDir + '/.tests') os.mkdir(baseDir + '/.tests')
ocapAlways = False
# create the servers # create the servers
aliceDir = baseDir + '/.tests/alice' aliceDir = baseDir + '/.tests/alice'
aliceDomain = '127.0.0.50' aliceDomain = '127.0.0.50'
@ -454,7 +434,7 @@ def testPostMessageBetweenServers():
threadWithTrace(target=createServerAlice, threadWithTrace(target=createServerAlice,
args=(aliceDir, aliceDomain, alicePort, bobAddress, args=(aliceDir, aliceDomain, alicePort, bobAddress,
federationList, False, False, federationList, False, False,
ocapAlways, aliceSendThreads), aliceSendThreads),
daemon=True) daemon=True)
global thrBob global thrBob
@ -468,7 +448,7 @@ def testPostMessageBetweenServers():
threadWithTrace(target=createServerBob, threadWithTrace(target=createServerBob,
args=(bobDir, bobDomain, bobPort, aliceAddress, args=(bobDir, bobDomain, bobPort, aliceAddress,
federationList, False, False, federationList, False, False,
ocapAlways, bobSendThreads), bobSendThreads),
daemon=True) daemon=True)
thrAlice.start() thrAlice.start()
@ -687,8 +667,6 @@ def testFollowBetweenServers():
shutil.rmtree(baseDir + '/.tests') shutil.rmtree(baseDir + '/.tests')
os.mkdir(baseDir + '/.tests') os.mkdir(baseDir + '/.tests')
ocapAlways = False
# create the servers # create the servers
aliceDir = baseDir + '/.tests/alice' aliceDir = baseDir + '/.tests/alice'
aliceDomain = '127.0.0.47' aliceDomain = '127.0.0.47'
@ -713,7 +691,7 @@ def testFollowBetweenServers():
threadWithTrace(target=createServerAlice, threadWithTrace(target=createServerAlice,
args=(aliceDir, aliceDomain, alicePort, bobAddress, args=(aliceDir, aliceDomain, alicePort, bobAddress,
federationList, False, False, federationList, False, False,
ocapAlways, aliceSendThreads), aliceSendThreads),
daemon=True) daemon=True)
global thrBob global thrBob
@ -727,7 +705,7 @@ def testFollowBetweenServers():
threadWithTrace(target=createServerBob, threadWithTrace(target=createServerBob,
args=(bobDir, bobDomain, bobPort, aliceAddress, args=(bobDir, bobDomain, bobPort, aliceAddress,
federationList, False, False, federationList, False, False,
ocapAlways, bobSendThreads), bobSendThreads),
daemon=True) daemon=True)
thrAlice.start() thrAlice.start()
@ -1246,8 +1224,6 @@ def testClientToServer():
shutil.rmtree(baseDir + '/.tests') shutil.rmtree(baseDir + '/.tests')
os.mkdir(baseDir + '/.tests') os.mkdir(baseDir + '/.tests')
ocapAlways = False
# create the servers # create the servers
aliceDir = baseDir + '/.tests/alice' aliceDir = baseDir + '/.tests/alice'
aliceDomain = '127.0.0.42' aliceDomain = '127.0.0.42'
@ -1272,7 +1248,7 @@ def testClientToServer():
threadWithTrace(target=createServerAlice, threadWithTrace(target=createServerAlice,
args=(aliceDir, aliceDomain, alicePort, bobAddress, args=(aliceDir, aliceDomain, alicePort, bobAddress,
federationList, False, False, federationList, False, False,
ocapAlways, aliceSendThreads), aliceSendThreads),
daemon=True) daemon=True)
global thrBob global thrBob
@ -1286,7 +1262,7 @@ def testClientToServer():
threadWithTrace(target=createServerBob, threadWithTrace(target=createServerBob,
args=(bobDir, bobDomain, bobPort, aliceAddress, args=(bobDir, bobDomain, bobPort, aliceAddress,
federationList, False, False, federationList, False, False,
ocapAlways, bobSendThreads), bobSendThreads),
daemon=True) daemon=True)
thrAlice.start() thrAlice.start()

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "لا تظهر زر أعجبني", "Don't show the Like button": "لا تظهر زر أعجبني",
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا", "Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا", "Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag محظور"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "No mostreu el botó M'agrada", "Don't show the Like button": "No mostreu el botó M'agrada",
"Autogenerated Hashtags": "Hashtags autogenerats", "Autogenerated Hashtags": "Hashtags autogenerats",
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats", "Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloquejat"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Peidiwch â dangos y botwm Hoffi", "Don't show the Like button": "Peidiwch â dangos y botwm Hoffi",
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig", "Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated", "Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag wedi'i Blocio"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an", "Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an",
"Autogenerated Hashtags": "Automatisch generierte Hashtags", "Autogenerated Hashtags": "Automatisch generierte Hashtags",
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten", "Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag blockiert"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Don't show the Like button", "Don't show the Like button": "Don't show the Like button",
"Autogenerated Hashtags": "Autogenerated Hashtags", "Autogenerated Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings", "Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocked"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "No mostrar el botón Me gusta", "Don't show the Like button": "No mostrar el botón Me gusta",
"Autogenerated Hashtags": "Hashtags autogenerados", "Autogenerated Hashtags": "Hashtags autogenerados",
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente", "Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqueada"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Ne pas afficher le bouton J'aime", "Don't show the Like button": "Ne pas afficher le bouton J'aime",
"Autogenerated Hashtags": "Hashtags générés automatiquement", "Autogenerated Hashtags": "Hashtags générés automatiquement",
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement", "Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqué"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Ná taispeáin an cnaipe Cosúil", "Don't show the Like button": "Ná taispeáin an cnaipe Cosúil",
"Autogenerated Hashtags": "Hashtags uathghinte", "Autogenerated Hashtags": "Hashtags uathghinte",
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte", "Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocáilte"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "लाइक बटन न दिखाएं", "Don't show the Like button": "लाइक बटन न दिखाएं",
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग", "Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी", "Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "हैशटैग अवरुद्ध"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Non mostrare il pulsante Mi piace", "Don't show the Like button": "Non mostrare il pulsante Mi piace",
"Autogenerated Hashtags": "Hashtag generati automaticamente", "Autogenerated Hashtags": "Hashtag generati automaticamente",
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente", "Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloccato"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "「いいね!」ボタンを表示しない", "Don't show the Like button": "「いいね!」ボタンを表示しない",
"Autogenerated Hashtags": "自動生成されたハッシュタグ", "Autogenerated Hashtags": "自動生成されたハッシュタグ",
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告", "Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "ハッシュタグがブロックされました"
} }

View File

@ -282,5 +282,6 @@
"Don't show the Like button": "Don't show the Like button", "Don't show the Like button": "Don't show the Like button",
"Autogenerated Hashtags": "Autogenerated Hashtags", "Autogenerated Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings", "Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocked"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Não mostrar o botão Curtir", "Don't show the Like button": "Não mostrar o botão Curtir",
"Autogenerated Hashtags": "Hashtags autogeradas", "Autogenerated Hashtags": "Hashtags autogeradas",
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente", "Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqueada"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "Не показывать кнопку \"Нравится\"", "Don't show the Like button": "Не показывать кнопку \"Нравится\"",
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги", "Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании", "Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "Хештег заблокирован"
} }

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "不显示“赞”按钮", "Don't show the Like button": "不显示“赞”按钮",
"Autogenerated Hashtags": "自动生成的标签", "Autogenerated Hashtags": "自动生成的标签",
"Autogenerated Content Warnings": "自动生成的内容警告", "Autogenerated Content Warnings": "自动生成的内容警告",
"Indymedia": "Indymedia" "Indymedia": "Indymedia",
"Hashtag Blocked": "标签被阻止"
} }

View File

@ -19,6 +19,55 @@ from calendar import monthrange
from followingCalendar import addPersonToCalendar from followingCalendar import addPersonToCalendar
def getFollowersList(baseDir: str,
nickname: str, domain: str,
followFile='following.txt') -> []:
"""Returns a list of followers for the given account
"""
filename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/' + followFile
if not os.path.isfile(filename):
return []
with open(filename, "r") as f:
lines = f.readlines()
for i in range(len(lines)):
lines[i] = lines[i].strip()
return lines
return []
def getFollowersOfPerson(baseDir: str,
nickname: str, domain: str,
followFile='following.txt') -> []:
"""Returns a list containing the followers of the given person
Used by the shared inbox to know who to send incoming mail to
"""
followers = []
if ':' in domain:
domain = domain.split(':')[0]
handle = nickname + '@' + domain
if not os.path.isdir(baseDir + '/accounts/' + handle):
return followers
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for account in dirs:
filename = os.path.join(subdir, account) + '/' + followFile
if account == handle or account.startswith('inbox@'):
continue
if not os.path.isfile(filename):
continue
with open(filename, 'r') as followingfile:
for followingHandle in followingfile:
followingHandle2 = followingHandle.replace('\n', '')
followingHandle2 = followingHandle2.replace('\r', '')
if followingHandle2 == handle:
if account not in followers:
followers.append(account)
break
return followers
def removeIdEnding(idStr: str) -> str: def removeIdEnding(idStr: str) -> str:
"""Removes endings such as /activity and /undo """Removes endings such as /activity and /undo
""" """
@ -193,7 +242,7 @@ def domainPermitted(domain: str, federationList: []):
return False return False
def urlPermitted(url: str, federationList: [], capability: str): def urlPermitted(url: str, federationList: []):
if isEvil(url): if isEvil(url):
return False return False
if not federationList: if not federationList:
@ -620,7 +669,7 @@ def validNickname(domain: str, nickname: str) -> bool:
return False return False
reservedNames = ('inbox', 'dm', 'outbox', 'following', reservedNames = ('inbox', 'dm', 'outbox', 'following',
'public', 'followers', 'public', 'followers',
'channel', 'capabilities', 'calendar', 'channel', 'calendar',
'tlreplies', 'tlmedia', 'tlblogs', 'tlreplies', 'tlmedia', 'tlblogs',
'tlevents', 'tlevents',
'moderation', 'activity', 'undo', 'moderation', 'activity', 'undo',

View File

@ -766,6 +766,14 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
hashtagSearchForm += '<center>\n' + \ hashtagSearchForm += '<center>\n' + \
'<h1>#' + hashtag + '</h1>\n' + '</center>\n' '<h1>#' + hashtag + '</h1>\n' + '</center>\n'
# RSS link for hashtag feed
hashtagSearchForm += '<center><a href="/tags/rss2/' + hashtag + '">'
hashtagSearchForm += \
'<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a></center>'
if startIndex > 0: if startIndex > 0:
# previous page link # previous page link
hashtagSearchForm += \ hashtagSearchForm += \
@ -787,7 +795,7 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
else: else:
postFields = postId.split(' ') postFields = postId.split(' ')
if len(postFields) != 3: if len(postFields) != 3:
index = +1 index += 1
continue continue
nickname = postFields[1] nickname = postFields[1]
postId = postFields[2] postId = postFields[2]
@ -833,6 +841,133 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
return hashtagSearchForm return hashtagSearchForm
def rss2TagHeader(hashtag: str, httpPrefix: str, domainFull: str) -> str:
rssStr = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
rssStr += "<rss version=\"2.0\">"
rssStr += '<channel>'
rssStr += ' <title>#' + hashtag + '</title>'
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/tags/rss2/' + hashtag + '</link>'
return rssStr
def rss2TagFooter() -> str:
rssStr = '</channel>'
rssStr += '</rss>'
return rssStr
def rssHashtagSearch(nickname: str, domain: str, port: int,
recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, hashtag: str,
postsPerPage: int,
session, wfRequest: {}, personCache: {},
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show an rss feed for a hashtag
"""
if hashtag.startswith('#'):
hashtag = hashtag[1:]
hashtag = urllib.parse.unquote(hashtag)
hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt'
if not os.path.isfile(hashtagIndexFile):
if hashtag != hashtag.lower():
hashtag = hashtag.lower()
hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt'
if not os.path.isfile(hashtagIndexFile):
print('WARN: hashtag file not found ' + hashtagIndexFile)
return None
# check that the directory for the nickname exists
if nickname:
if not os.path.isdir(baseDir + '/accounts/' +
nickname + '@' + domain):
nickname = None
# read the index
lines = []
with open(hashtagIndexFile, "r") as f:
lines = f.readlines()
if not lines:
return None
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
maxFeedLength = 10
hashtagFeed = \
rss2TagHeader(hashtag, httpPrefix, domainFull)
for index in range(len(lines)):
postId = lines[index].strip('\n').strip('\r')
if ' ' not in postId:
nickname = getNicknameFromActor(postId)
if not nickname:
index += 1
if index >= maxFeedLength:
break
continue
else:
postFields = postId.split(' ')
if len(postFields) != 3:
index += 1
if index >= maxFeedLength:
break
continue
nickname = postFields[1]
postId = postFields[2]
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
index += 1
if index >= maxFeedLength:
break
continue
postJsonObject = loadJson(postFilename)
if postJsonObject:
if not isPublicPost(postJsonObject):
index += 1
if index >= maxFeedLength:
break
continue
# add to feed
if postJsonObject['object'].get('content') and \
postJsonObject['object'].get('attributedTo') and \
postJsonObject['object'].get('published'):
published = postJsonObject['object']['published']
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")
hashtagFeed += ' <item>'
hashtagFeed += \
' <author>' + \
postJsonObject['object']['attributedTo'] + \
'</author>'
if postJsonObject['object'].get('summary'):
hashtagFeed += \
' <title>' + \
postJsonObject['object']['summary'] + \
'</title>'
hashtagFeed += \
' <description><![CDATA[' + \
postJsonObject['object']['content'] + \
']]></description>'
hashtagFeed += \
' <pubDate>' + rssDateStr + '</pubDate>'
if postJsonObject['object'].get('attachment'):
for attach in postJsonObject['object']['attachment']:
if not attach.get('url'):
continue
hashtagFeed += \
' <link>' + attach['url'] + '</link>'
hashtagFeed += ' </item>'
index += 1
if index >= maxFeedLength:
break
return hashtagFeed + rss2TagFooter()
def htmlSkillsSearch(translate: {}, baseDir: str, def htmlSkillsSearch(translate: {}, baseDir: str,
httpPrefix: str, httpPrefix: str,
skillsearch: str, instanceOnly: bool, skillsearch: str, instanceOnly: bool,
@ -1849,7 +1984,7 @@ def htmlAbout(baseDir: str, httpPrefix: str,
return aboutForm return aboutForm
def htmlHashtagBlocked(baseDir: str) -> str: def htmlHashtagBlocked(baseDir: str, translate: {}) -> str:
"""Show the screen for a blocked hashtag """Show the screen for a blocked hashtag
""" """
blockedHashtagForm = '' blockedHashtagForm = ''
@ -1860,9 +1995,12 @@ def htmlHashtagBlocked(baseDir: str) -> str:
blockedHashtagCSS = cssFile.read() blockedHashtagCSS = cssFile.read()
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS) blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
blockedHashtagForm += '<div><center>\n' blockedHashtagForm += '<div><center>\n'
blockedHashtagForm += ' <p class="screentitle">Hashtag Blocked</p>\n'
blockedHashtagForm += \ blockedHashtagForm += \
' <p>See <a href="/terms">Terms of Service</a></p>\n' ' <p class="screentitle">' + \
translate['Hashtag Blocked'] + '</p>\n'
blockedHashtagForm += \
' <p>See <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>\n'
blockedHashtagForm += '</center></div>\n' blockedHashtagForm += '</center></div>\n'
blockedHashtagForm += htmlFooter() blockedHashtagForm += htmlFooter()
return blockedHashtagForm return blockedHashtagForm
@ -2515,7 +2653,7 @@ def htmlFooter() -> str:
def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
translate: {}, translate: {},
baseDir: str, httpPrefix: str, baseDir: str, httpPrefix: str,
authorized: bool, ocapAlways: bool, authorized: bool,
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
projectVersion: str, projectVersion: str,
@ -2536,8 +2674,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
str(currPage), str(currPage),
httpPrefix, httpPrefix,
10, 'outbox', 10, 'outbox',
authorized, authorized)
ocapAlways)
if not outboxFeed: if not outboxFeed:
break break
if len(outboxFeed['orderedItems']) == 0: if len(outboxFeed['orderedItems']) == 0:
@ -2565,7 +2702,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
authorized: bool, ocapAlways: bool, authorized: bool,
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
followingJson: {}, projectVersion: str, followingJson: {}, projectVersion: str,
@ -2795,7 +2932,7 @@ def htmlProfile(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int, recentPostsCache: {}, maxRecentPosts: int,
translate: {}, projectVersion: str, translate: {}, projectVersion: str,
baseDir: str, httpPrefix: str, authorized: bool, baseDir: str, httpPrefix: str, authorized: bool,
ocapAlways: bool, profileJson: {}, selected: str, profileJson: {}, selected: str,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
YTReplacementDomain: str, YTReplacementDomain: str,
extraJson=None, extraJson=None,
@ -3055,14 +3192,14 @@ def htmlProfile(defaultTimeline: str,
htmlProfilePosts(recentPostsCache, maxRecentPosts, htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate, translate,
baseDir, httpPrefix, authorized, baseDir, httpPrefix, authorized,
ocapAlways, nickname, domain, port, nickname, domain, port,
session, wfRequest, personCache, session, wfRequest, personCache,
projectVersion, projectVersion,
YTReplacementDomain) + licenseStr YTReplacementDomain) + licenseStr
if selected == 'following': if selected == 'following':
profileStr += \ profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix, htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname, authorized, nickname,
domain, port, session, domain, port, session,
wfRequest, personCache, extraJson, wfRequest, personCache, extraJson,
projectVersion, ["unfollow"], selected, projectVersion, ["unfollow"], selected,
@ -3070,7 +3207,7 @@ def htmlProfile(defaultTimeline: str,
if selected == 'followers': if selected == 'followers':
profileStr += \ profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix, htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname, authorized, nickname,
domain, port, session, domain, port, session,
wfRequest, personCache, extraJson, wfRequest, personCache, extraJson,
projectVersion, ["block"], projectVersion, ["block"],
@ -3112,7 +3249,6 @@ def individualFollowAsHtml(translate: {},
if domain not in followUrl: if domain not in followUrl:
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion, personCache, projectVersion,
httpPrefix, nickname, httpPrefix, nickname,
@ -3966,7 +4102,6 @@ def individualPostAsHtml(allowDownloads: bool,
if fullDomain not in postActor: if fullDomain not in postActor:
(inboxUrl, pubKeyId, pubKey, (inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, personCache,
projectVersion, httpPrefix, projectVersion, httpPrefix,