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.
[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**

View File

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

View File

@ -7,8 +7,6 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from capabilities import capabilitiesAccept
from capabilities import capabilitiesGrantedSave
from utils import urlPermitted
from utils import getDomainFromActor
from utils import getNicknameFromActor
@ -19,7 +17,7 @@ from utils import followPerson
def createAcceptReject(baseDir: str, federationList: [],
nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str,
objectJson: {}, ocapJson, acceptType: str) -> {}:
objectJson: {}, acceptType: str) -> {}:
"""Accepts or rejects something (eg. a follow request or offer)
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
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'):
return None
if not urlPermitted(objectJson['actor'], federationList, "inbox:write"):
if not urlPermitted(objectJson['actor'], federationList):
return None
if port:
@ -48,25 +46,17 @@ def createAcceptReject(baseDir: str, federationList: [],
if ccUrl:
if len(ccUrl) > 0:
newAccept['cc'] = [ccUrl]
# attach capabilities for follow accept
if ocapJson:
newAccept['capabilities'] = ocapJson
return newAccept
def createAccept(baseDir: str, federationList: [],
nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str,
objectJson: {},
acceptedCaps=["inbox:write", "objects:read"]) -> {}:
# create capabilities accept
ocapNew = capabilitiesAccept(baseDir, httpPrefix,
nickname, domain, port,
toUrl, True, acceptedCaps)
objectJson: {}) -> {}:
return createAcceptReject(baseDir, federationList,
nickname, domain, port,
toUrl, ccUrl, httpPrefix,
objectJson, ocapNew, 'Accept')
objectJson, 'Accept')
def createReject(baseDir: str, federationList: [],
@ -154,13 +144,6 @@ def acceptFollow(baseDir: str, domain: str, messageJson: {},
if 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?
unfollowedFilename = baseDir + '/accounts/' + \
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,
corresponding to url or atomUri in createPostBase
"""
if not urlPermitted(objectUrl, federationList, "inbox:write"):
if not urlPermitted(objectUrl, federationList):
return None
if ':' in domain:
@ -231,7 +231,7 @@ def undoAnnounce(session, baseDir: str, federationList: [],
objectUrl is typically the url of the message which was repeated,
corresponding to url or atomUri in createPostBase
"""
if not urlPermitted(objectUrl, federationList, "inbox:write"):
if not urlPermitted(objectUrl, federationList):
return None
if ':' in domain:
@ -391,8 +391,8 @@ def sendAnnounceViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
fromNickname, fromDomain,
@ -414,8 +414,7 @@ def sendAnnounceViaServer(baseDir: str, session,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newAnnounceJson, [], inboxUrl,
headers, "inbox:write")
postResult = postJson(session, newAnnounceJson, [], inboxUrl, headers)
if not postResult:
print('WARN: Announce not posted')

View File

@ -123,7 +123,6 @@ def sendAvailabilityViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion,
httpPrefix, nickname,
@ -146,7 +145,7 @@ def sendAvailabilityViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = postJson(session, newAvailabilityJson, [],
inboxUrl, headers, "inbox:write")
inboxUrl, headers)
if not postResult:
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/', '/')
if not restrictToDomain or \
(restrictToDomain and '/' + domain in messageLink):
if postJsonObject['object'].get('summary'):
if postJsonObject['object'].get('summary') and \
postJsonObject['object'].get('published'):
published = postJsonObject['object']['published']
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
titleStr = postJsonObject['object']['summary']
@ -307,7 +308,8 @@ def htmlBlogPostRSS3(authorized: bool,
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
if not restrictToDomain or \
(restrictToDomain and '/' + domain in messageLink):
if postJsonObject['object'].get('summary'):
if postJsonObject['object'].get('summary') and \
postJsonObject['object'].get('published'):
published = postJsonObject['object']['published']
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
titleStr = postJsonObject['object']['summary']
@ -358,13 +360,15 @@ def htmlBlogPost(authorized: bool,
blogStr += '<a href="' + httpPrefix + '://' + \
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="/' + \
iconsDir + '/rss.png" /></a>'
blogStr += '<a href="' + httpPrefix + '://' + \
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="/' + \
iconsDir + '/rss3.png" /></a>'

View File

@ -234,7 +234,7 @@ def bookmark(recentPostsCache: {},
'to' might be a specific person (actor) whose post 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
fullDomain = domain
@ -330,7 +330,7 @@ def undoBookmark(recentPostsCache: {},
'to' might be a specific person (actor) whose post 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
fullDomain = domain
@ -457,8 +457,7 @@ def sendBookmarkViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
@ -480,7 +479,7 @@ def sendBookmarkViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = postJson(session, newBookmarkJson, [],
inboxUrl, headers, "inbox:write")
inboxUrl, headers)
if not postResult:
if debug:
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
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
@ -562,7 +560,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = postJson(session, newUndoBookmarkJson, [],
inboxUrl, headers, "inbox:write")
inboxUrl, headers)
if not postResult:
if debug:
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 htmlHistorySearch
from webinterface import htmlHashtagSearch
from webinterface import rssHashtagSearch
from webinterface import htmlModerationInfo
from webinterface import htmlSearchSharedItems
from webinterface import htmlHashtagBlocked
@ -443,7 +444,7 @@ class PubServer(BaseHTTPRequestHandler):
'failed to obtain keyId from signature')
return False
# 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:
print('Authorized fetch failed: ' + keyId +
' is not permitted')
@ -920,6 +921,7 @@ class PubServer(BaseHTTPRequestHandler):
if postToNickname:
print('Posting to nickname ' + postToNickname)
self.postToNickname = postToNickname
return postMessageToOutbox(messageJson, self.postToNickname,
self.server, self.server.baseDir,
self.server.httpPrefix,
@ -4047,7 +4049,8 @@ class PubServer(BaseHTTPRequestHandler):
if '?page=' in hashtag:
hashtag = hashtag.split('?page=')[0]
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._write(msg)
self.server.GETbusy = False
@ -4093,6 +4096,60 @@ class PubServer(BaseHTTPRequestHandler):
'login shown done',
'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,
baseDir: str,
cookie: str, proxyType: str,
@ -4309,7 +4366,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.postLog,
self.server.cachedWebfingers,
self.server.personCache,
self.server.acceptedCaps,
debug,
self.server.projectVersion)
originPathStrAbsolute = \
@ -5178,7 +5234,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.translate,
self.server.projectVersion,
baseDir, httpPrefix, True,
self.server.ocapAlways,
getPerson, 'roles',
self.server.session,
cachedWebfingers,
@ -5249,7 +5304,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.translate,
self.server.projectVersion,
baseDir, httpPrefix, True,
self.server.ocapAlways,
getPerson, 'skills',
self.server.session,
cachedWebfingers,
@ -5516,7 +5570,6 @@ class PubServer(BaseHTTPRequestHandler):
proxyType: str, cookie: str,
debug: str,
recentPostsCache: {}, session,
ocapAlways: bool,
defaultTimeline: str,
maxRecentPosts: int,
translate: {},
@ -5538,9 +5591,9 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'inbox',
authorized,
ocapAlways)
authorized)
if inboxFeed:
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done',
'show inbox json')
@ -5566,8 +5619,8 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInFeed, 'inbox',
authorized,
ocapAlways)
authorized)
if GETstartTime:
self._benchmarkGETtimings(GETstartTime,
GETtimings,
'show status done',
@ -5590,13 +5643,18 @@ class PubServer(BaseHTTPRequestHandler):
projectVersion,
self._isMinimal(nickname),
YTReplacementDomain)
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done',
'show inbox html')
if msg:
msg = msg.encode('utf-8')
self._set_headers('text/html', len(msg),
cookie, callingDomain)
self._write(msg)
if GETstartTime:
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show status done',
'show inbox')
@ -5647,8 +5705,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'dm',
authorized,
self.server.ocapAlways)
authorized)
if inboxDMFeed:
if self._requestHTTP():
nickname = path.replace('/users/', '')
@ -5672,8 +5729,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInFeed, 'dm',
authorized,
self.server.ocapAlways)
authorized)
msg = \
htmlInboxDMs(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -5748,7 +5804,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'tlreplies',
True, self.server.ocapAlways)
True)
if not inboxRepliesFeed:
inboxRepliesFeed = []
if self._requestHTTP():
@ -5773,7 +5829,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInFeed, 'tlreplies',
True, self.server.ocapAlways)
True)
msg = \
htmlInboxReplies(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -5848,7 +5904,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInMediaFeed, 'tlmedia',
True, self.server.ocapAlways)
True)
if not inboxMediaFeed:
inboxMediaFeed = []
if self._requestHTTP():
@ -5873,7 +5929,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInMediaFeed, 'tlmedia',
True, self.server.ocapAlways)
True)
msg = \
htmlInboxMedia(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -5948,7 +6004,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInBlogsFeed, 'tlblogs',
True, self.server.ocapAlways)
True)
if not inboxBlogsFeed:
inboxBlogsFeed = []
if self._requestHTTP():
@ -5973,7 +6029,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInBlogsFeed, 'tlblogs',
True, self.server.ocapAlways)
True)
msg = \
htmlInboxBlogs(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -6106,7 +6162,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'tlbookmarks',
authorized, self.server.ocapAlways)
authorized)
if bookmarksFeed:
if self._requestHTTP():
nickname = path.replace('/users/', '')
@ -6132,8 +6188,7 @@ class PubServer(BaseHTTPRequestHandler):
httpPrefix,
maxPostsInFeed,
'tlbookmarks',
authorized,
self.server.ocapAlways)
authorized)
msg = \
htmlBookmarks(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -6210,7 +6265,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'tlevents',
authorized, self.server.ocapAlways)
authorized)
print('eventsFeed: ' + str(eventsFeed))
if eventsFeed:
if self._requestHTTP():
@ -6236,8 +6291,7 @@ class PubServer(BaseHTTPRequestHandler):
httpPrefix,
maxPostsInFeed,
'tlevents',
authorized,
self.server.ocapAlways)
authorized)
msg = \
htmlEvents(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -6306,8 +6360,7 @@ class PubServer(BaseHTTPRequestHandler):
port, path,
httpPrefix,
maxPostsInFeed, 'outbox',
authorized,
self.server.ocapAlways)
authorized)
if outboxFeed:
if self._requestHTTP():
nickname = \
@ -6331,8 +6384,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInFeed, 'outbox',
authorized,
self.server.ocapAlways)
authorized)
msg = \
htmlOutbox(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -6394,7 +6446,7 @@ class PubServer(BaseHTTPRequestHandler):
path,
httpPrefix,
maxPostsInFeed, 'moderation',
True, self.server.ocapAlways)
True)
if moderationFeed:
if self._requestHTTP():
nickname = path.replace('/users/', '')
@ -6418,7 +6470,7 @@ class PubServer(BaseHTTPRequestHandler):
path + '?page=1',
httpPrefix,
maxPostsInFeed, 'moderation',
True, self.server.ocapAlways)
True)
msg = \
htmlModeration(self.server.defaultTimeline,
self.server.recentPostsCache,
@ -6521,7 +6573,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.projectVersion,
baseDir, httpPrefix,
authorized,
self.server.ocapAlways,
getPerson, 'shares',
self.server.session,
self.server.cachedWebfingers,
@ -6608,7 +6659,6 @@ class PubServer(BaseHTTPRequestHandler):
self.server.projectVersion,
baseDir, httpPrefix,
authorized,
self.server.ocapAlways,
getPerson, 'following',
self.server.session,
self.server.cachedWebfingers,
@ -6695,7 +6745,6 @@ class PubServer(BaseHTTPRequestHandler):
baseDir,
httpPrefix,
authorized,
self.server.ocapAlways,
getPerson, 'followers',
self.server.session,
self.server.cachedWebfingers,
@ -6757,7 +6806,6 @@ class PubServer(BaseHTTPRequestHandler):
baseDir,
httpPrefix,
authorized,
self.server.ocapAlways,
getPerson, 'posts',
self.server.session,
self.server.cachedWebfingers,
@ -8068,6 +8116,18 @@ class PubServer(BaseHTTPRequestHandler):
# hashtag search
if self.path.startswith('/tags/') or \
(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.path, cookie,
self.server.baseDir,
@ -8673,7 +8733,6 @@ class PubServer(BaseHTTPRequestHandler):
cookie, self.server.debug,
self.server.recentPostsCache,
self.server.session,
self.server.ocapAlways,
self.server.defaultTimeline,
self.server.maxRecentPosts,
self.server.translate,
@ -9064,7 +9123,9 @@ class PubServer(BaseHTTPRequestHandler):
etag, callingDomain)
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
# 0=this is not a new post
# 1=new post success
@ -9205,6 +9266,7 @@ class PubServer(BaseHTTPRequestHandler):
privateEvent = False
else:
privateEvent = True
if postType == 'newpost':
messageJson = \
createPublicPost(self.server.baseDir,
@ -9224,6 +9286,7 @@ class PubServer(BaseHTTPRequestHandler):
if messageJson:
if fields['schedulePost']:
return 1
if self._postToOutbox(messageJson, __version__, nickname):
populateReplies(self.server.baseDir,
self.server.httpPrefix,
@ -9481,8 +9544,7 @@ class PubServer(BaseHTTPRequestHandler):
if messageJson:
if fields['schedulePost']:
return 1
# if self.server.debug:
print('DEBUG: new DM to ' +
print('Sending new DM to ' +
str(messageJson['object']['to']))
if self._postToOutbox(messageJson, __version__, nickname):
populateReplies(self.server.baseDir,
@ -9618,7 +9680,9 @@ class PubServer(BaseHTTPRequestHandler):
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
This creates a thread to send the new post
"""
@ -9719,7 +9783,9 @@ class PubServer(BaseHTTPRequestHandler):
print('Creating new post from: ' + newPostThreadName)
self._receiveNewPostProcess(postType,
path, headers, length,
postBytes, boundary)
postBytes, boundary,
callingDomain, cookie,
authorized)
return pageNumber
def _cryptoAPIreadHandle(self):
@ -10179,7 +10245,10 @@ class PubServer(BaseHTTPRequestHandler):
elif currPostType == 'newevent':
postRedirect = 'tlevents'
pageNumber = self._receiveNewPost(currPostType, self.path)
pageNumber = \
self._receiveNewPost(currPostType, self.path,
callingDomain, cookie,
authorized)
if pageNumber:
nickname = self.path.split('/users/')[1]
if '/' in nickname:
@ -10232,7 +10301,6 @@ class PubServer(BaseHTTPRequestHandler):
self.path.endswith('/inbox') or
self.path.endswith('/shares') or
self.path.endswith('/moderationaction') or
self.path.endswith('/caps/new') or
self.path == '/sharedInbox'):
print('Attempt to POST to invalid path ' + self.path)
self._400()
@ -10554,8 +10622,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
port=80, proxyPort=80, httpPrefix='https',
fedList=[], maxMentions=10, maxEmoji=10,
authenticatedFetch=False,
noreply=False, nolike=False, nopics=False,
noannounce=False, cw=False, ocapAlways=False,
proxyType=None, maxReplies=64,
domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
allowDeletion=False, debug=False, unitTest=False,
@ -10687,7 +10753,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.sendThreads = sendThreads
httpd.postLog = []
httpd.maxQueueLength = 64
httpd.ocapAlways = ocapAlways
httpd.allowDeletion = allowDeletion
httpd.lastLoginTime = 0
httpd.maxReplies = maxReplies
@ -10695,19 +10760,8 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.tokensLookup = {}
loadTokens(baseDir, httpd.tokens, httpd.tokensLookup)
httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch
httpd.acceptedCaps = ["inbox:write", "objects:read"]
# contains threads used to send posts to followers
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):
print('Creating shared inbox: inbox@' + domain)
@ -10778,12 +10832,11 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.personCache, httpd.inboxQueue,
domain, onionDomain, i2pDomain, port, proxyType,
httpd.federationList,
httpd.ocapAlways, maxReplies,
maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, debug, maxMentions, maxEmoji,
httpd.translate, unitTest,
httpd.YTReplacementDomain,
httpd.acceptedCaps), daemon=True)
httpd.YTReplacementDomain), daemon=True)
print('Creating scheduled post thread')
httpd.thrPostSchedule = \
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
or atomUri in createPostBase
"""
if not urlPermitted(objectUrl, federationList, "inbox:write"):
if not urlPermitted(objectUrl, federationList):
return None
if ':' in domain:
@ -137,8 +137,7 @@ def sendDeleteViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
@ -160,7 +159,7 @@ def sendDeleteViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = \
postJson(session, newDeleteJson, [], inboxUrl, headers, "inbox:write")
postJson(session, newDeleteJson, [], inboxUrl, headers)
if not postResult:
if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)

View File

@ -16,6 +16,7 @@ from skills import setSkillLevel
from roles import setRole
from webfinger import webfingerHandle
from posts import getPublicPostDomains
from posts import getPublicPostDomainsBlocked
from posts import sendBlockViaServer
from posts import sendUndoBlockViaServer
from posts import createPublicPost
@ -24,6 +25,7 @@ from posts import archivePosts
from posts import sendPostViaServer
from posts import getPublicPostsOfPerson
from posts import getUserUrl
from posts import checkDomains
from session import createSession
from session import getJson
from filters import addFilter
@ -71,7 +73,9 @@ from socnet import instancesGraph
import argparse
def str2bool(v):
def str2bool(v) -> bool:
"""Returns true if the given value is a boolean
"""
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
@ -155,6 +159,14 @@ parser.add_argument('--postDomains', dest='postDomains', type=str,
default=None,
help='Show domains referenced in public '
'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,
default=None,
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='?',
const=True, default=False,
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,
default=None,
help='Set the avatar filename for an account')
@ -465,7 +457,8 @@ if args.postDomains:
elif args.gnunet:
proxyType = 'gnunet'
domainList = []
domainList = getPublicPostDomains(baseDir, nickname, domain,
domainList = getPublicPostDomains(None,
baseDir, nickname, domain,
proxyType, args.port,
httpPrefix, debug,
__version__, domainList)
@ -473,6 +466,83 @@ if args.postDomains:
print(postDomain)
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 ',' not in args.socnet:
print('Syntax: '
@ -718,7 +788,6 @@ if args.approve:
postLog = []
cachedWebfingers = {}
personCache = {}
acceptedCaps = []
manualApproveFollowRequest(session, baseDir,
httpPrefix,
args.nickname, domain, port,
@ -726,7 +795,6 @@ if args.approve:
federationList,
sendThreads, postLog,
cachedWebfingers, personCache,
acceptedCaps,
debug, __version__)
sys.exit()
@ -1111,9 +1179,6 @@ if args.port:
if args.proxyPort:
proxyPort = args.proxyPort
setConfigParam(baseDir, 'proxyPort', proxyPort)
ocapAlways = False
if args.ocap:
ocapAlways = args.ocap
if args.gnunet:
httpPrefix = 'gnunet'
if args.dat:
@ -1830,8 +1895,6 @@ if __name__ == "__main__":
port, proxyPort, httpPrefix,
federationList, args.maxMentions,
args.maxEmoji, args.authenticatedFetch,
args.noreply, args.nolike, args.nopics,
args.noannounce, args.cw, ocapAlways,
proxyType, args.maxReplies,
args.domainMaxPostsPerDay,
args.accountMaxPostsPerDay,

View File

@ -8,6 +8,7 @@ __status__ = "Production"
from pprint import pprint
import os
from utils import getFollowersList
from utils import validNickname
from utils import domainPermitted
from utils import getDomainFromActor
@ -112,15 +113,14 @@ def isFollowingActor(baseDir: str,
def getMutualsOfPerson(baseDir: str,
nickname: str, domain: str,
followFile='following.txt') -> []:
nickname: str, domain: str) -> []:
"""Returns the mutuals of a person
i.e. accounts which they follow and which also follow back
"""
followers = \
getFollowersOfPerson(baseDir, nickname, domain, 'followers')
getFollowersList(baseDir, nickname, domain, 'followers.txt')
following = \
getFollowersOfPerson(baseDir, nickname, domain, 'following')
getFollowersList(baseDir, nickname, domain, 'following.txt')
mutuals = []
for handle in following:
if handle in followers:
@ -128,36 +128,6 @@ def getMutualsOfPerson(baseDir: str,
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,
followerNickname: str, followerDomain: str,
federationList: [], debug: bool) -> bool:
@ -543,8 +513,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [],
debug: bool, projectVersion: str,
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
debug: bool, projectVersion: str) -> bool:
"""Receives a follow request within the POST section of HTTPServer
"""
if not messageJson['type'].startswith('Follow'):
@ -685,8 +654,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson['actor'], federationList,
messageJson, acceptedCaps,
sendThreads, postLog,
messageJson, sendThreads, postLog,
cachedWebfingers, personCache,
debug, projectVersion, True)
@ -696,8 +664,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
port: int,
nickname: str, domain: str, fromPort: int,
personUrl: str, federationList: [],
followJson: {}, acceptedCaps: [],
sendThreads: [], postLog: [],
followJson: {}, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str,
removeFollowActivity: bool):
@ -715,7 +682,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
acceptJson = createAccept(baseDir, federationList,
nicknameToFollow, domainToFollow, port,
personUrl, '', httpPrefix,
followJson, acceptedCaps)
followJson)
if debug:
pprint(acceptJson)
print('DEBUG: sending follow Accept from ' +
@ -938,8 +905,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
@ -961,7 +927,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = \
postJson(session, newFollowJson, [], inboxUrl, headers, "inbox:write")
postJson(session, newFollowJson, [], inboxUrl, headers)
if not postResult:
if debug:
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
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
avatarUrl, displayName) = getPersonBox(baseDir, session,
wfRequest, personCache,
projectVersion, httpPrefix,
fromNickname,
fromDomain, postToBox)
if not inboxUrl:
@ -1059,7 +1026,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = \
postJson(session, unfollowJson, [], inboxUrl, headers, "inbox:write")
postJson(session, unfollowJson, [], inboxUrl, headers)
if not postResult:
if debug:
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
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
and also the corresponding capability id if it exists
"""
if debug:
print('DEBUG: getting followers of ' + actor)
recipientsDict = {}
if ':' not in actor:
return recipientsDict
httpPrefix = actor.split(':')[0]
nickname = getNicknameFromActor(actor)
if not nickname:
if debug:
@ -1114,34 +1079,6 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
if debug:
print('DEBUG: ' + account +
' 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
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 storePersonInCache
from acceptreject import receiveAcceptReject
from capabilities import getOcapFilename
from capabilities import CapablePost
from capabilities import capabilitiesReceiveUpdate
from bookmarks import updateBookmarksCollection
from bookmarks import undoBookmarksCollectionEntry
from blocking import isBlocked
@ -268,7 +265,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
if domain in actor:
return True
if not urlPermitted(actor, federationList, "inbox:write"):
if not urlPermitted(actor, federationList):
return False
alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce')
@ -281,7 +278,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
inReplyTo = messageJson['object']['inReplyTo']
if not isinstance(inReplyTo, str):
return False
if not urlPermitted(inReplyTo, federationList, "inbox:write"):
if not urlPermitted(inReplyTo, federationList):
return False
return True
@ -437,81 +434,12 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
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: [],
recipientsDict: {},
domainMatch: str, domain: str,
actor: str, debug: bool) -> bool:
"""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
for recipient in toList:
@ -523,23 +451,6 @@ def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
nickname = recipient.split(domainMatch)[1]
handle = nickname+'@'+domain
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
else:
if debug:
@ -741,8 +652,7 @@ def receiveUndo(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [],
debug: bool,
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
debug: bool) -> bool:
"""Receives an undo request within the POST section of HTTPServer
"""
if not messageJson['type'].startswith('Undo'):
@ -1005,24 +915,6 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str,
print('DEBUG: Profile update was received for ' +
messageJson['object']['url'])
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
@ -2124,20 +2016,19 @@ def inboxUpdateIndex(boxname: str, baseDir: str, handle: str,
return False
def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
session, keyId: str, handle: str, messageJson: {},
baseDir: str, httpPrefix: str, sendThreads: [],
postLog: [], cachedWebfingers: {}, personCache: {},
queue: [], domain: str,
onionDomain: str, i2pDomain: str,
port: int, proxyType: str,
federationList: [], ocapAlways: bool, debug: bool,
acceptedCaps: [],
federationList: [], debug: bool,
queueFilename: str, destinationFilename: str,
maxReplies: int, allowDeletion: bool,
maxMentions: int, maxEmoji: int, translate: {},
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
if '#' in actor:
@ -2247,7 +2138,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
return False
if debug:
print('DEBUG: object capabilities passed')
print('DEBUG: initial checks passed')
print('copy queue file from ' + queueFilename +
' to ' + destinationFilename)
@ -2526,13 +2417,11 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
cachedWebfingers: {}, personCache: {}, queue: [],
domain: str,
onionDomain: str, i2pDomain: str, port: int, proxyType: str,
federationList: [],
ocapAlways: bool, maxReplies: int,
federationList: [], maxReplies: int,
domainMaxPostsPerDay: int, accountMaxPostsPerDay: int,
allowDeletion: bool, debug: bool, maxMentions: int,
maxEmoji: int, translate: {}, unitTest: bool,
YTReplacementDomain: str,
acceptedCaps=["inbox:write", "objects:read"]) -> None:
YTReplacementDomain: str) -> None:
"""Processes received items and moves them to the appropriate
directories
"""
@ -2801,8 +2690,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
personCache,
queueJson['post'],
federationList,
debug,
acceptedCaps=["inbox:write", "objects:read"]):
debug):
print('Queue: Undo accepted from ' + keyId)
if os.path.isfile(queueFilename):
os.remove(queueFilename)
@ -2819,9 +2707,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
personCache,
queueJson['post'],
federationList,
debug, projectVersion,
acceptedCaps=["inbox:write",
"objects:read"]):
debug, projectVersion):
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
@ -2917,22 +2803,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
pprint(recipientsDictFollowers)
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
# this avoid copying file multiple times to potentially many
# 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:
sharedInboxPostFilename = \
queueJson['destination'].replace(inboxHandle, inboxHandle)
@ -2943,16 +2816,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
for handle, capsId in recipientsDict.items():
destination = \
queueJson['destination'].replace(inboxHandle, handle)
# check that capabilities are accepted
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,
inboxAfterInitial(recentPostsCache,
maxRecentPosts,
session, keyId, handle,
queueJson['post'],
@ -2963,32 +2827,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
domain,
onionDomain, i2pDomain,
port, proxyType,
federationList, ocapAlways,
debug, acceptedCaps,
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,
federationList,
debug,
queueFilename, destination,
maxReplies, allowDeletion,
maxMentions, maxEmoji,
@ -2996,8 +2836,6 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
YTReplacementDomain)
if debug:
pprint(queueJson['post'])
print('No capability list within post')
print('ocapAlways: ' + str(ocapAlways))
print('Queue: Queue post accepted')
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
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
fullDomain = domain
@ -162,7 +162,7 @@ def undolike(recentPostsCache: {},
'to' might be a specific person (actor) whose post 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
fullDomain = domain
@ -267,8 +267,7 @@ def sendLikeViaServer(baseDir: str, session,
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, capabilityAcquisition,
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
@ -291,8 +290,7 @@ def sendLikeViaServer(baseDir: str, session,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newLikeJson, [], inboxUrl,
headers, "inbox:write")
postResult = postJson(session, newLikeJson, [], inboxUrl, headers)
if not postResult:
print('WARN: POST announce failed for c2s to ' + inboxUrl)
return 5
@ -352,8 +350,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, capabilityAcquisition,
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion,
httpPrefix, fromNickname,
@ -375,8 +372,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newUndoLikeJson, [], inboxUrl,
headers, "inbox:write")
postResult = postJson(session, newUndoLikeJson, [], inboxUrl, headers)
if not postResult:
print('WARN: POST announce failed for c2s to ' + inboxUrl)
return 5

View File

@ -85,7 +85,6 @@ def manualApproveFollowRequest(session, baseDir: str,
federationList: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
acceptedCaps: [],
debug: bool,
projectVersion: str) -> None:
"""Manually approve a follow request
@ -142,7 +141,7 @@ def manualApproveFollowRequest(session, baseDir: str,
approvePort,
followJson['actor'],
federationList,
followJson, acceptedCaps,
followJson,
sendThreads, postLog,
cachedWebfingers, personCache,
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,
server, baseDir: str, httpPrefix: str,
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str,
port: int,
onionDomain: str, i2pDomain: str, port: int,
recentPostsCache: {}, followersThreads: [],
federationList: [], sendThreads: [],
postLog: [], cachedWebfingers: {},

View File

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

297
posts.py
View File

@ -14,6 +14,7 @@ import shutil
import sys
import time
import uuid
import random
from socket import error as SocketError
from time import gmtime, strftime
from collections import OrderedDict
@ -29,6 +30,8 @@ from session import postJsonString
from session import postImage
from webfinger import webfingerHandle
from httpsig import createSignedHeader
from utils import getFollowersList
from utils import isEvil
from utils import removeIdEnding
from utils import siteIsActive
from utils import getCachedPostFilename
@ -42,8 +45,6 @@ from utils import validNickname
from utils import locatePost
from utils import loadJson
from utils import saveJson
from capabilities import getOcapFilename
from capabilities import capabilitiesUpdate
from media import attachMedia
from media import replaceYouTube
from content import removeHtml
@ -207,7 +208,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
else:
personUrl = httpPrefix + '://' + domain + '/users/' + nickname
if not personUrl:
return None, None, None, None, None, None, None, None
return None, None, None, None, None, None, None
personJson = \
getPersonFromCache(baseDir, personUrl, personCache, True)
if not personJson:
@ -225,7 +226,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
projectVersion, httpPrefix, domain)
if not personJson:
print('Unable to get actor')
return None, None, None, None, None, None, None, None
return None, None, None, None, None, None, None
boxJson = None
if not personJson.get(boxName):
if personJson.get('endpoints'):
@ -235,7 +236,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
boxJson = personJson[boxName]
if not boxJson:
return None, None, None, None, None, None, None, None
return None, None, None, None, None, None, None
personId = None
if personJson.get('id'):
@ -254,9 +255,6 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
if personJson.get('endpoints'):
if personJson['endpoints'].get('sharedInbox'):
sharedInbox = personJson['endpoints']['sharedInbox']
capabilityAcquisition = None
if personJson.get('capabilityAcquisitionEndpoint'):
capabilityAcquisition = personJson['capabilityAcquisitionEndpoint']
avatarUrl = None
if personJson.get('icon'):
if personJson['icon'].get('url'):
@ -268,7 +266,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
storePersonInCache(baseDir, personUrl, personJson, personCache, True)
return boxJson, pubKeyId, pubKey, personId, sharedInbox, \
capabilityAcquisition, avatarUrl, displayName
avatarUrl, displayName
def getPosts(session, outboxUrl: str, maxPosts: int,
@ -890,24 +888,12 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
if not clientToServer:
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 = \
httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber + '/replies'
newPost = {
'@context': postContext,
'id': newPostId + '/activity',
'capability': capabilityIdList,
'type': 'Create',
'actor': actorUrl,
'published': published,
@ -1072,11 +1058,9 @@ def outboxMessageCreateWrap(httpPrefix: str,
cc = []
if messageJson.get('cc'):
cc = messageJson['cc']
capabilityUrl = []
newPost = {
"@context": "https://www.w3.org/ns/activitystreams",
'id': newPostId + '/activity',
'capability': capabilityUrl,
'type': 'Create',
'actor': httpPrefix + '://' + domain + '/users/' + nickname,
'published': published,
@ -1580,7 +1564,7 @@ def threadSendPost(session, postJsonStr: str, federationList: [],
postResult, unauthorized = \
postJsonString(session, postJsonStr, federationList,
inboxUrl, signatureHeaderJson,
"inbox:write", debug)
debug)
except Exception as e:
print('ERROR: postJsonString failed ' + str(e))
if unauthorized:
@ -1665,26 +1649,18 @@ def sendPost(projectVersion: str,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
toPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
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:
return 3
if not pubKey:
return 4
if not toPersonId:
return 5
# sharedInbox and capabilities are optional
# sharedInbox is optional
postJsonObject = \
createPostBase(baseDir, nickname, domain, port,
@ -1790,7 +1766,6 @@ def sendPostViaServer(projectVersion: str,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
@ -1856,7 +1831,7 @@ def sendPostViaServer(projectVersion: str,
}
postResult = \
postImage(session, attachImageFilename, [],
inboxUrl, headers, "inbox:write")
inboxUrl, headers)
if not postResult:
if debug:
print('DEBUG: Failed to upload image')
@ -1869,7 +1844,7 @@ def sendPostViaServer(projectVersion: str,
}
postResult = \
postJsonString(session, json.dumps(postJsonObject), [],
inboxUrl, headers, "inbox:write", debug)
inboxUrl, headers, debug)
if not postResult:
if debug:
print('DEBUG: POST failed for c2s to '+inboxUrl)
@ -2000,19 +1975,13 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
else:
postToBox = 'outbox'
# get the actor inbox/outbox/capabilities for the To handle
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl,
capabilityAcquisition, avatarUrl,
# get the actor inbox/outbox for the To handle
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, postToBox)
if nickname == 'capabilities':
inboxUrl = capabilityAcquisition
if not capabilityAcquisition:
return 2
else:
print("inboxUrl: " + str(inboxUrl))
print("toPersonId: " + str(toPersonId))
print("sharedInboxUrl: " + str(sharedInboxUrl))
@ -2036,7 +2005,7 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
if debug:
print('DEBUG: missing personId')
return 5
# sharedInbox and capabilities are optional
# sharedInbox is optional
# get the senders private key
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug)
@ -2470,75 +2439,69 @@ def sendToFollowersThread(session, baseDir: str,
def createInbox(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache,
session, baseDir, 'inbox',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
pageNumber)
def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlbookmarks',
nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber)
True, pageNumber)
def createEventsTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents',
nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber)
True, pageNumber)
def createDMTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache,
session, baseDir, 'dm', nickname,
domain, port, httpPrefix, itemsPerPage,
headerOnly, True, ocapAlways, pageNumber)
headerOnly, True, pageNumber)
def createRepliesTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
pageNumber)
def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname,
domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
pageNumber)
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname,
domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
pageNumber)
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',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, authorized,
False, pageNumber)
pageNumber)
def createModeration(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
boxDir = createPersonDir(nickname, domain, baseDir, 'inbox')
boxname = 'moderation'
@ -2751,8 +2714,7 @@ def createBoxIndex(boxDir: str, postsInBoxDict: {}) -> int:
def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
postsInBoxDict: {}, postsCtr: int,
nickname: str, domain: str,
ocapAlways: bool) -> int:
nickname: str, domain: str) -> int:
""" Creates an index for the given shared inbox
"""
handle = nickname + '@' + domain
@ -2788,30 +2750,6 @@ def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
if actorNickname + '@' + actorDomain not in followingHandles:
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
postsCtr += 1
return postsCtr
@ -2866,7 +2804,7 @@ def createBoxIndexed(recentPostsCache: {},
session, baseDir: str, boxname: str,
nickname: str, domain: str, port: int, httpPrefix: str,
itemsPerPage: int, headerOnly: bool, authorized: bool,
ocapAlways: bool, pageNumber=None) -> {}:
pageNumber=None) -> {}:
"""Constructs the box feed for a person with the given nickname
"""
if not authorized or not pageNumber:
@ -3005,10 +2943,6 @@ def createBoxIndexed(recentPostsCache: {},
except BaseException:
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
# unauthorized viewers
if not authorized:
@ -3226,7 +3160,6 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
(personUrl, pubKeyId, pubKey,
personId, shaedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
@ -3240,12 +3173,13 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
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,
debug: bool, projectVersion: str,
domainList=[]) -> []:
""" Returns a list of domains referenced within public posts
"""
if not session:
session = createSession(proxyType)
if not session:
return domainList
@ -3270,8 +3204,7 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
return domainList
(personUrl, pubKeyId, pubKey,
personId, shaedInbox,
capabilityAcquisition,
personId, sharedInbox,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
@ -3288,43 +3221,125 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
return postDomains
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
followerUrl, updateCaps: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
federationList: [], debug: bool,
projectVersion: str) -> int:
"""When the capabilities for a follower are changed this
sends out an update. followerUrl is the actor of the follower.
def getPublicPostDomainsBlocked(session, baseDir: str,
nickname: str, domain: str,
proxyType: str, port: int, httpPrefix: str,
debug: bool, projectVersion: str,
domainList=[]) -> []:
""" Returns a list of domains referenced within public posts which
are globally blocked on this instance
"""
updateJson = \
capabilitiesUpdate(baseDir, httpPrefix,
nickname, domain, port,
followerUrl, updateCaps)
postDomains = \
getPublicPostDomains(session, baseDir, nickname, domain,
proxyType, port, httpPrefix,
debug, projectVersion,
domainList)
if not postDomains:
return []
if not updateJson:
return 1
blockingFilename = baseDir + '/accounts/blocking.txt'
if not os.path.isfile(blockingFilename):
return []
if debug:
pprint(updateJson)
print('DEBUG: sending capabilities update from ' +
nickname + '@' + domain + ' port ' + str(port) +
' to ' + followerUrl)
# read the blocked domains as a single string
blockedStr = ''
with open(blockingFilename, 'r') as fp:
blockedStr = fp.read()
clientToServer = False
followerNickname = getNicknameFromActor(followerUrl)
if not followerNickname:
print('WARN: unable to find nickname in ' + followerUrl)
return 1
followerDomain, followerPort = getDomainFromActor(followerUrl)
return sendSignedJson(updateJson, session, baseDir,
nickname, domain, port,
followerNickname, followerDomain, followerPort, '',
httpPrefix, True, clientToServer,
federationList,
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion)
blockedDomains = []
for domainName in postDomains:
if '@' not in domainName:
continue
# get the domain after the @
domainName = domainName.split('@')[1].strip()
if isEvil(domainName):
blockedDomains.append(domainName)
continue
if domainName in blockedStr:
blockedDomains.append(domainName)
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,
@ -3692,8 +3707,7 @@ def sendBlockViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix, fromNickname,
@ -3715,8 +3729,7 @@ def sendBlockViaServer(baseDir: str, session,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newBlockJson, [], inboxUrl,
headers, "inbox:write")
postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
if not postResult:
print('WARN: Unable to post block')
@ -3781,8 +3794,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
@ -3803,8 +3815,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newBlockJson, [], inboxUrl,
headers, "inbox:write")
postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
if not postResult:
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
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session,
wfRequest, personCache,
projectVersion, httpPrefix,
@ -315,7 +314,7 @@ def sendRoleViaServer(baseDir: str, session,
'Authorization': authHeader
}
postResult = \
postJson(session, newRoleJson, [], inboxUrl, headers, "inbox:write")
postJson(session, newRoleJson, [], inboxUrl, headers)
if not postResult:
if debug:
print('DEBUG: POST announce failed for c2s to '+inboxUrl)

View File

@ -49,7 +49,7 @@ def createSession(proxyType: str):
session.proxies = {}
session.proxies['http'] = '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
@ -93,14 +93,11 @@ def getJson(session, url: str, headers: {}, params: {},
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
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
if not urlPermitted(inboxUrl, federationList, capability):
if not urlPermitted(inboxUrl, federationList):
print('postJson: ' + inboxUrl + ' not permitted')
return None
@ -132,22 +129,13 @@ def postJsonString(session, postJsonStr: str,
federationList: [],
inboxUrl: str,
headers: {},
capability: str,
debug: bool) -> (bool, bool):
"""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
NOTE: Here we post a string rather than the original json so that
conversions between string and json format don't invalidate
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:
postResult = \
session.post(url=inboxUrl, data=postJsonStr, headers=headers)
@ -181,14 +169,11 @@ def postJsonString(session, postJsonStr: str,
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
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
if not urlPermitted(inboxUrl, federationList, capability):
if not urlPermitted(inboxUrl, federationList):
print('postJson: ' + inboxUrl + ' not permitted')
return None

View File

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

View File

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

View File

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

View File

@ -286,5 +286,6 @@
"Don't show the Like button": "لا تظهر زر أعجبني",
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
"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",
"Autogenerated Hashtags": "Hashtags 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",
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
"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",
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
"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",
"Autogenerated Hashtags": "Autogenerated Hashtags",
"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",
"Autogenerated Hashtags": "Hashtags autogenerados",
"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",
"Autogenerated Hashtags": "Hashtags 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",
"Autogenerated Hashtags": "Hashtags 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": "लाइक बटन न दिखाएं",
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
"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",
"Autogenerated Hashtags": "Hashtag 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": "「いいね!」ボタンを表示しない",
"Autogenerated Hashtags": "自動生成されたハッシュタグ",
"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",
"Autogenerated Hashtags": "Autogenerated Hashtags",
"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",
"Autogenerated Hashtags": "Hashtags autogeradas",
"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": "Не показывать кнопку \"Нравится\"",
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
"Indymedia": "Indymedia"
"Indymedia": "Indymedia",
"Hashtag Blocked": "Хештег заблокирован"
}

View File

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

View File

@ -19,6 +19,55 @@ from calendar import monthrange
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:
"""Removes endings such as /activity and /undo
"""
@ -193,7 +242,7 @@ def domainPermitted(domain: str, federationList: []):
return False
def urlPermitted(url: str, federationList: [], capability: str):
def urlPermitted(url: str, federationList: []):
if isEvil(url):
return False
if not federationList:
@ -620,7 +669,7 @@ def validNickname(domain: str, nickname: str) -> bool:
return False
reservedNames = ('inbox', 'dm', 'outbox', 'following',
'public', 'followers',
'channel', 'capabilities', 'calendar',
'channel', 'calendar',
'tlreplies', 'tlmedia', 'tlblogs',
'tlevents',
'moderation', 'activity', 'undo',

View File

@ -766,6 +766,14 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
hashtagSearchForm += '<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:
# previous page link
hashtagSearchForm += \
@ -787,7 +795,7 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
else:
postFields = postId.split(' ')
if len(postFields) != 3:
index = +1
index += 1
continue
nickname = postFields[1]
postId = postFields[2]
@ -833,6 +841,133 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
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,
httpPrefix: str,
skillsearch: str, instanceOnly: bool,
@ -1849,7 +1984,7 @@ def htmlAbout(baseDir: str, httpPrefix: str,
return aboutForm
def htmlHashtagBlocked(baseDir: str) -> str:
def htmlHashtagBlocked(baseDir: str, translate: {}) -> str:
"""Show the screen for a blocked hashtag
"""
blockedHashtagForm = ''
@ -1860,9 +1995,12 @@ def htmlHashtagBlocked(baseDir: str) -> str:
blockedHashtagCSS = cssFile.read()
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
blockedHashtagForm += '<div><center>\n'
blockedHashtagForm += ' <p class="screentitle">Hashtag Blocked</p>\n'
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 += htmlFooter()
return blockedHashtagForm
@ -2515,7 +2653,7 @@ def htmlFooter() -> str:
def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, httpPrefix: str,
authorized: bool, ocapAlways: bool,
authorized: bool,
nickname: str, domain: str, port: int,
session, wfRequest: {}, personCache: {},
projectVersion: str,
@ -2536,8 +2674,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
str(currPage),
httpPrefix,
10, 'outbox',
authorized,
ocapAlways)
authorized)
if not outboxFeed:
break
if len(outboxFeed['orderedItems']) == 0:
@ -2565,7 +2702,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
authorized: bool, ocapAlways: bool,
authorized: bool,
nickname: str, domain: str, port: int,
session, wfRequest: {}, personCache: {},
followingJson: {}, projectVersion: str,
@ -2795,7 +2932,7 @@ def htmlProfile(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, projectVersion: str,
baseDir: str, httpPrefix: str, authorized: bool,
ocapAlways: bool, profileJson: {}, selected: str,
profileJson: {}, selected: str,
session, wfRequest: {}, personCache: {},
YTReplacementDomain: str,
extraJson=None,
@ -3055,14 +3192,14 @@ def htmlProfile(defaultTimeline: str,
htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate,
baseDir, httpPrefix, authorized,
ocapAlways, nickname, domain, port,
nickname, domain, port,
session, wfRequest, personCache,
projectVersion,
YTReplacementDomain) + licenseStr
if selected == 'following':
profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname,
authorized, nickname,
domain, port, session,
wfRequest, personCache, extraJson,
projectVersion, ["unfollow"], selected,
@ -3070,7 +3207,7 @@ def htmlProfile(defaultTimeline: str,
if selected == 'followers':
profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname,
authorized, nickname,
domain, port, session,
wfRequest, personCache, extraJson,
projectVersion, ["block"],
@ -3112,7 +3249,6 @@ def individualFollowAsHtml(translate: {},
if domain not in followUrl:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion,
httpPrefix, nickname,
@ -3966,7 +4102,6 @@ def individualPostAsHtml(allowDownloads: bool,
if fullDomain not in postActor:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,