mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
commit
f799517268
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
19
announce.py
19
announce.py
|
|
@ -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,12 +391,12 @@ 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,
|
||||
personCache,
|
||||
projectVersion, httpPrefix,
|
||||
fromNickname, fromDomain,
|
||||
postToBox)
|
||||
sharedInbox, avatarUrl,
|
||||
displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||
personCache,
|
||||
projectVersion, httpPrefix,
|
||||
fromNickname, fromDomain,
|
||||
postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
12
blog.py
|
|
@ -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>'
|
||||
|
||||
|
|
|
|||
14
bookmarks.py
14
bookmarks.py
|
|
@ -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)
|
||||
|
|
|
|||
283
capabilities.py
283
capabilities.py
|
|
@ -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
|
||||
209
daemon.py
209
daemon.py
|
|
@ -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,12 +5591,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
path,
|
||||
httpPrefix,
|
||||
maxPostsInFeed, 'inbox',
|
||||
authorized,
|
||||
ocapAlways)
|
||||
authorized)
|
||||
if inboxFeed:
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show status done',
|
||||
'show inbox json')
|
||||
if GETstartTime:
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show status done',
|
||||
'show inbox json')
|
||||
if self._requestHTTP():
|
||||
nickname = path.replace('/users/', '')
|
||||
nickname = nickname.replace('/inbox', '')
|
||||
|
|
@ -5566,12 +5619,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
path + '?page=1',
|
||||
httpPrefix,
|
||||
maxPostsInFeed, 'inbox',
|
||||
authorized,
|
||||
ocapAlways)
|
||||
self._benchmarkGETtimings(GETstartTime,
|
||||
GETtimings,
|
||||
'show status done',
|
||||
'show inbox page')
|
||||
authorized)
|
||||
if GETstartTime:
|
||||
self._benchmarkGETtimings(GETstartTime,
|
||||
GETtimings,
|
||||
'show status done',
|
||||
'show inbox page')
|
||||
msg = htmlInbox(defaultTimeline,
|
||||
recentPostsCache,
|
||||
maxRecentPosts,
|
||||
|
|
@ -5590,16 +5643,21 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
projectVersion,
|
||||
self._isMinimal(nickname),
|
||||
YTReplacementDomain)
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show status done',
|
||||
'show inbox html')
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('text/html', len(msg),
|
||||
cookie, callingDomain)
|
||||
self._write(msg)
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show status done',
|
||||
'show inbox')
|
||||
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')
|
||||
else:
|
||||
# don't need authenticated fetch here because
|
||||
# there is already the authorization check
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
121
epicyon.py
121
epicyon.py
|
|
@ -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,
|
||||
|
|
|
|||
97
follow.py
97
follow.py
|
|
@ -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,10 +1003,11 @@ 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,
|
||||
fromDomain, postToBox)
|
||||
avatarUrl, displayName) = getPersonBox(baseDir, session,
|
||||
wfRequest, personCache,
|
||||
projectVersion, httpPrefix,
|
||||
fromNickname,
|
||||
fromDomain, postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
|
|
@ -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,35 +1079,7 @@ 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
|
||||
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 |
248
inbox.py
248
inbox.py
|
|
@ -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,24 +451,7 @@ 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
|
||||
recipientsDict[handle] = None
|
||||
else:
|
||||
if debug:
|
||||
print('DEBUG: ' + baseDir + '/accounts/' +
|
||||
|
|
@ -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,
|
||||
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: [],
|
||||
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
|
||||
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: [], 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 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,61 +2816,26 @@ 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,
|
||||
maxRecentPosts,
|
||||
session, keyId, handle,
|
||||
queueJson['post'],
|
||||
baseDir, httpPrefix,
|
||||
sendThreads, postLog,
|
||||
cachedWebfingers,
|
||||
personCache, queue,
|
||||
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,
|
||||
queueFilename, destination,
|
||||
maxReplies, allowDeletion,
|
||||
maxMentions, maxEmoji,
|
||||
translate, unitTest,
|
||||
YTReplacementDomain)
|
||||
if debug:
|
||||
pprint(queueJson['post'])
|
||||
print('No capability list within post')
|
||||
print('ocapAlways: ' + str(ocapAlways))
|
||||
inboxAfterInitial(recentPostsCache,
|
||||
maxRecentPosts,
|
||||
session, keyId, handle,
|
||||
queueJson['post'],
|
||||
baseDir, httpPrefix,
|
||||
sendThreads, postLog,
|
||||
cachedWebfingers,
|
||||
personCache, queue,
|
||||
domain,
|
||||
onionDomain, i2pDomain,
|
||||
port, proxyType,
|
||||
federationList,
|
||||
debug,
|
||||
queueFilename, destination,
|
||||
maxReplies, allowDeletion,
|
||||
maxMentions, maxEmoji,
|
||||
translate, unitTest,
|
||||
YTReplacementDomain)
|
||||
if debug:
|
||||
pprint(queueJson['post'])
|
||||
|
||||
print('Queue: Queue post accepted')
|
||||
if os.path.isfile(queueFilename):
|
||||
|
|
|
|||
16
like.py
16
like.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
180
ocaps.md
|
|
@ -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.
|
||||
|
|
@ -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: {},
|
||||
|
|
|
|||
36
person.py
36
person.py
|
|
@ -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,
|
||||
|
|
|
|||
315
posts.py
315
posts.py
|
|
@ -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,25 +1975,19 @@ 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))
|
||||
if inboxUrl:
|
||||
if inboxUrl.endswith('/actor/inbox'):
|
||||
inboxUrl = sharedInboxUrl
|
||||
print("inboxUrl: " + str(inboxUrl))
|
||||
print("toPersonId: " + str(toPersonId))
|
||||
print("sharedInboxUrl: " + str(sharedInboxUrl))
|
||||
if inboxUrl:
|
||||
if inboxUrl.endswith('/actor/inbox'):
|
||||
inboxUrl = sharedInboxUrl
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
|
|
@ -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,32 +2750,8 @@ 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
|
||||
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,13 +3173,14 @@ 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
|
||||
"""
|
||||
session = createSession(proxyType)
|
||||
if not session:
|
||||
session = createSession(proxyType)
|
||||
if not session:
|
||||
return domainList
|
||||
personCache = {}
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
3
roles.py
3
roles.py
|
|
@ -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)
|
||||
|
|
|
|||
37
session.py
37
session.py
|
|
@ -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,16 +93,13 @@ 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):
|
||||
print('postJson: ' + inboxUrl + ' not permitted')
|
||||
return None
|
||||
# check that we are posting to a permitted domain
|
||||
if not urlPermitted(inboxUrl, federationList):
|
||||
print('postJson: ' + inboxUrl + ' not permitted')
|
||||
return None
|
||||
|
||||
try:
|
||||
postResult = \
|
||||
|
|
@ -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,16 +169,13 @@ 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):
|
||||
print('postJson: ' + inboxUrl + ' not permitted')
|
||||
return None
|
||||
# check that we are posting to a permitted domain
|
||||
if not urlPermitted(inboxUrl, federationList):
|
||||
print('postJson: ' + inboxUrl + ' not permitted')
|
||||
return None
|
||||
|
||||
if not (attachImageFilename.endswith('.jpg') or
|
||||
attachImageFilename.endswith('.jpeg') or
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
44
tests.py
44
tests.py
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -286,5 +286,6 @@
|
|||
"Don't show the Like button": "لا تظهر زر أعجبني",
|
||||
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
|
||||
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
|
||||
"Indymedia": "Indymedia"
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag محظور"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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é"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,5 +286,6 @@
|
|||
"Don't show the Like button": "लाइक बटन न दिखाएं",
|
||||
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
|
||||
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
|
||||
"Indymedia": "Indymedia"
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "हैशटैग अवरुद्ध"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,5 +286,6 @@
|
|||
"Don't show the Like button": "「いいね!」ボタンを表示しない",
|
||||
"Autogenerated Hashtags": "自動生成されたハッシュタグ",
|
||||
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
|
||||
"Indymedia": "Indymedia"
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "ハッシュタグがブロックされました"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,5 +286,6 @@
|
|||
"Don't show the Like button": "Не показывать кнопку \"Нравится\"",
|
||||
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
|
||||
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
|
||||
"Indymedia": "Indymedia"
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Хештег заблокирован"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,5 +286,6 @@
|
|||
"Don't show the Like button": "不显示“赞”按钮",
|
||||
"Autogenerated Hashtags": "自动生成的标签",
|
||||
"Autogenerated Content Warnings": "自动生成的内容警告",
|
||||
"Indymedia": "Indymedia"
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "标签被阻止"
|
||||
}
|
||||
|
|
|
|||
53
utils.py
53
utils.py
|
|
@ -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',
|
||||
|
|
|
|||
163
webinterface.py
163
webinterface.py
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue