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.
|
Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend.
|
||||||
|
|
||||||
[Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Object Capabilities](ocaps.md) - [Code of Conduct](code-of-conduct.md)
|
[Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Code of Conduct](code-of-conduct.md)
|
||||||
|
|
||||||
Matrix room: **#epicyon:matrix.freedombone.net**
|
Matrix room: **#epicyon:matrix.freedombone.net**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* http signatures and basic auth
|
* http signatures and basic auth
|
||||||
* Compatible with http (onion addresses), https and dat
|
* Compatible with http (onion addresses), https and dat
|
||||||
* Minimal dependencies.
|
* Minimal dependencies.
|
||||||
* Capabilities based security
|
|
||||||
* Support image blurhashes
|
* Support image blurhashes
|
||||||
* Data minimization principle. Configurable post expiry time
|
* Data minimization principle. Configurable post expiry time
|
||||||
* Likes and repeats only visible to authorized viewers
|
* Likes and repeats only visible to authorized viewers
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ __email__ = "bob@freedombone.net"
|
||||||
__status__ = "Production"
|
__status__ = "Production"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from capabilities import capabilitiesAccept
|
|
||||||
from capabilities import capabilitiesGrantedSave
|
|
||||||
from utils import urlPermitted
|
from utils import urlPermitted
|
||||||
from utils import getDomainFromActor
|
from utils import getDomainFromActor
|
||||||
from utils import getNicknameFromActor
|
from utils import getNicknameFromActor
|
||||||
|
|
@ -19,7 +17,7 @@ from utils import followPerson
|
||||||
def createAcceptReject(baseDir: str, federationList: [],
|
def createAcceptReject(baseDir: str, federationList: [],
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str, port: int,
|
||||||
toUrl: str, ccUrl: str, httpPrefix: str,
|
toUrl: str, ccUrl: str, httpPrefix: str,
|
||||||
objectJson: {}, ocapJson, acceptType: str) -> {}:
|
objectJson: {}, acceptType: str) -> {}:
|
||||||
"""Accepts or rejects something (eg. a follow request or offer)
|
"""Accepts or rejects something (eg. a follow request or offer)
|
||||||
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
|
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
|
||||||
and ccUrl might be a specific person favorited or repeated and
|
and ccUrl might be a specific person favorited or repeated and
|
||||||
|
|
@ -29,7 +27,7 @@ def createAcceptReject(baseDir: str, federationList: [],
|
||||||
if not objectJson.get('actor'):
|
if not objectJson.get('actor'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not urlPermitted(objectJson['actor'], federationList, "inbox:write"):
|
if not urlPermitted(objectJson['actor'], federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if port:
|
if port:
|
||||||
|
|
@ -48,25 +46,17 @@ def createAcceptReject(baseDir: str, federationList: [],
|
||||||
if ccUrl:
|
if ccUrl:
|
||||||
if len(ccUrl) > 0:
|
if len(ccUrl) > 0:
|
||||||
newAccept['cc'] = [ccUrl]
|
newAccept['cc'] = [ccUrl]
|
||||||
# attach capabilities for follow accept
|
|
||||||
if ocapJson:
|
|
||||||
newAccept['capabilities'] = ocapJson
|
|
||||||
return newAccept
|
return newAccept
|
||||||
|
|
||||||
|
|
||||||
def createAccept(baseDir: str, federationList: [],
|
def createAccept(baseDir: str, federationList: [],
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str, port: int,
|
||||||
toUrl: str, ccUrl: str, httpPrefix: str,
|
toUrl: str, ccUrl: str, httpPrefix: str,
|
||||||
objectJson: {},
|
objectJson: {}) -> {}:
|
||||||
acceptedCaps=["inbox:write", "objects:read"]) -> {}:
|
|
||||||
# create capabilities accept
|
|
||||||
ocapNew = capabilitiesAccept(baseDir, httpPrefix,
|
|
||||||
nickname, domain, port,
|
|
||||||
toUrl, True, acceptedCaps)
|
|
||||||
return createAcceptReject(baseDir, federationList,
|
return createAcceptReject(baseDir, federationList,
|
||||||
nickname, domain, port,
|
nickname, domain, port,
|
||||||
toUrl, ccUrl, httpPrefix,
|
toUrl, ccUrl, httpPrefix,
|
||||||
objectJson, ocapNew, 'Accept')
|
objectJson, 'Accept')
|
||||||
|
|
||||||
|
|
||||||
def createReject(baseDir: str, federationList: [],
|
def createReject(baseDir: str, federationList: [],
|
||||||
|
|
@ -154,13 +144,6 @@ def acceptFollow(baseDir: str, domain: str, messageJson: {},
|
||||||
if acceptedPort:
|
if acceptedPort:
|
||||||
acceptedDomainFull = acceptedDomain + ':' + str(acceptedPort)
|
acceptedDomainFull = acceptedDomain + ':' + str(acceptedPort)
|
||||||
|
|
||||||
# are capabilities attached? If so then store them
|
|
||||||
if messageJson.get('capabilities'):
|
|
||||||
if isinstance(messageJson['capabilities'], dict):
|
|
||||||
capabilitiesGrantedSave(baseDir,
|
|
||||||
nickname, acceptedDomainFull,
|
|
||||||
messageJson['capabilities'])
|
|
||||||
|
|
||||||
# has this person already been unfollowed?
|
# has this person already been unfollowed?
|
||||||
unfollowedFilename = baseDir + '/accounts/' + \
|
unfollowedFilename = baseDir + '/accounts/' + \
|
||||||
nickname + '@' + acceptedDomainFull + '/unfollowed.txt'
|
nickname + '@' + acceptedDomainFull + '/unfollowed.txt'
|
||||||
|
|
|
||||||
11
announce.py
11
announce.py
|
|
@ -108,7 +108,7 @@ def createAnnounce(session, baseDir: str, federationList: [],
|
||||||
followers url objectUrl is typically the url of the message,
|
followers url objectUrl is typically the url of the message,
|
||||||
corresponding to url or atomUri in createPostBase
|
corresponding to url or atomUri in createPostBase
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if ':' in domain:
|
if ':' in domain:
|
||||||
|
|
@ -231,7 +231,7 @@ def undoAnnounce(session, baseDir: str, federationList: [],
|
||||||
objectUrl is typically the url of the message which was repeated,
|
objectUrl is typically the url of the message which was repeated,
|
||||||
corresponding to url or atomUri in createPostBase
|
corresponding to url or atomUri in createPostBase
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if ':' in domain:
|
if ':' in domain:
|
||||||
|
|
@ -391,8 +391,8 @@ def sendAnnounceViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey, fromPersonId,
|
(inboxUrl, pubKeyId, pubKey, fromPersonId,
|
||||||
sharedInbox, capabilityAcquisition,
|
sharedInbox, avatarUrl,
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
fromNickname, fromDomain,
|
fromNickname, fromDomain,
|
||||||
|
|
@ -414,8 +414,7 @@ def sendAnnounceViaServer(baseDir: str, session,
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newAnnounceJson, [], inboxUrl,
|
postResult = postJson(session, newAnnounceJson, [], inboxUrl, headers)
|
||||||
headers, "inbox:write")
|
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: Announce not posted')
|
print('WARN: Announce not posted')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,6 @@ def sendAvailabilityViaServer(baseDir: str, session,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, nickname,
|
httpPrefix, nickname,
|
||||||
|
|
@ -146,7 +145,7 @@ def sendAvailabilityViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newAvailabilityJson, [],
|
postResult = postJson(session, newAvailabilityJson, [],
|
||||||
inboxUrl, headers, "inbox:write")
|
inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: failed to post availability')
|
print('WARN: failed to post availability')
|
||||||
|
|
||||||
|
|
|
||||||
12
blog.py
12
blog.py
|
|
@ -282,7 +282,8 @@ def htmlBlogPostRSS2(authorized: bool,
|
||||||
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
|
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
|
||||||
if not restrictToDomain or \
|
if not restrictToDomain or \
|
||||||
(restrictToDomain and '/' + domain in messageLink):
|
(restrictToDomain and '/' + domain in messageLink):
|
||||||
if postJsonObject['object'].get('summary'):
|
if postJsonObject['object'].get('summary') and \
|
||||||
|
postJsonObject['object'].get('published'):
|
||||||
published = postJsonObject['object']['published']
|
published = postJsonObject['object']['published']
|
||||||
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
|
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
titleStr = postJsonObject['object']['summary']
|
titleStr = postJsonObject['object']['summary']
|
||||||
|
|
@ -307,7 +308,8 @@ def htmlBlogPostRSS3(authorized: bool,
|
||||||
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
|
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
|
||||||
if not restrictToDomain or \
|
if not restrictToDomain or \
|
||||||
(restrictToDomain and '/' + domain in messageLink):
|
(restrictToDomain and '/' + domain in messageLink):
|
||||||
if postJsonObject['object'].get('summary'):
|
if postJsonObject['object'].get('summary') and \
|
||||||
|
postJsonObject['object'].get('published'):
|
||||||
published = postJsonObject['object']['published']
|
published = postJsonObject['object']['published']
|
||||||
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
|
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
titleStr = postJsonObject['object']['summary']
|
titleStr = postJsonObject['object']['summary']
|
||||||
|
|
@ -358,13 +360,15 @@ def htmlBlogPost(authorized: bool,
|
||||||
|
|
||||||
blogStr += '<a href="' + httpPrefix + '://' + \
|
blogStr += '<a href="' + httpPrefix + '://' + \
|
||||||
domainFull + '/blog/' + nickname + '/rss.xml">'
|
domainFull + '/blog/' + nickname + '/rss.xml">'
|
||||||
blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \
|
blogStr += '<img style="width:3%;min-width:50px" ' + \
|
||||||
|
'loading="lazy" alt="RSS 2.0" ' + \
|
||||||
'title="RSS 2.0" src="/' + \
|
'title="RSS 2.0" src="/' + \
|
||||||
iconsDir + '/rss.png" /></a>'
|
iconsDir + '/rss.png" /></a>'
|
||||||
|
|
||||||
blogStr += '<a href="' + httpPrefix + '://' + \
|
blogStr += '<a href="' + httpPrefix + '://' + \
|
||||||
domainFull + '/blog/' + nickname + '/rss.txt">'
|
domainFull + '/blog/' + nickname + '/rss.txt">'
|
||||||
blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
|
blogStr += '<img style="width:3%;min-width:50px" ' + \
|
||||||
|
'loading="lazy" alt="RSS 3.0" ' + \
|
||||||
'title="RSS 3.0" src="/' + \
|
'title="RSS 3.0" src="/' + \
|
||||||
iconsDir + '/rss3.png" /></a>'
|
iconsDir + '/rss3.png" /></a>'
|
||||||
|
|
||||||
|
|
|
||||||
14
bookmarks.py
14
bookmarks.py
|
|
@ -234,7 +234,7 @@ def bookmark(recentPostsCache: {},
|
||||||
'to' might be a specific person (actor) whose post was bookmarked
|
'to' might be a specific person (actor) whose post was bookmarked
|
||||||
object is typically the url of the message which was bookmarked
|
object is typically the url of the message which was bookmarked
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fullDomain = domain
|
fullDomain = domain
|
||||||
|
|
@ -330,7 +330,7 @@ def undoBookmark(recentPostsCache: {},
|
||||||
'to' might be a specific person (actor) whose post was bookmarked
|
'to' might be a specific person (actor) whose post was bookmarked
|
||||||
object is typically the url of the message which was bookmarked
|
object is typically the url of the message which was bookmarked
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fullDomain = domain
|
fullDomain = domain
|
||||||
|
|
@ -457,8 +457,7 @@ def sendBookmarkViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
@ -480,7 +479,7 @@ def sendBookmarkViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newBookmarkJson, [],
|
postResult = postJson(session, newBookmarkJson, [],
|
||||||
inboxUrl, headers, "inbox:write")
|
inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
@ -539,8 +538,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
@ -562,7 +560,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newUndoBookmarkJson, [],
|
postResult = postJson(session, newUndoBookmarkJson, [],
|
||||||
inboxUrl, headers, "inbox:write")
|
inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
|
||||||
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
|
|
||||||
175
daemon.py
175
daemon.py
|
|
@ -148,6 +148,7 @@ from webinterface import htmlTermsOfService
|
||||||
from webinterface import htmlSkillsSearch
|
from webinterface import htmlSkillsSearch
|
||||||
from webinterface import htmlHistorySearch
|
from webinterface import htmlHistorySearch
|
||||||
from webinterface import htmlHashtagSearch
|
from webinterface import htmlHashtagSearch
|
||||||
|
from webinterface import rssHashtagSearch
|
||||||
from webinterface import htmlModerationInfo
|
from webinterface import htmlModerationInfo
|
||||||
from webinterface import htmlSearchSharedItems
|
from webinterface import htmlSearchSharedItems
|
||||||
from webinterface import htmlHashtagBlocked
|
from webinterface import htmlHashtagBlocked
|
||||||
|
|
@ -443,7 +444,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
'failed to obtain keyId from signature')
|
'failed to obtain keyId from signature')
|
||||||
return False
|
return False
|
||||||
# is the keyId (actor) valid?
|
# is the keyId (actor) valid?
|
||||||
if not urlPermitted(keyId, self.server.federationList, "inbox:read"):
|
if not urlPermitted(keyId, self.server.federationList):
|
||||||
if self.server.debug:
|
if self.server.debug:
|
||||||
print('Authorized fetch failed: ' + keyId +
|
print('Authorized fetch failed: ' + keyId +
|
||||||
' is not permitted')
|
' is not permitted')
|
||||||
|
|
@ -920,6 +921,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if postToNickname:
|
if postToNickname:
|
||||||
print('Posting to nickname ' + postToNickname)
|
print('Posting to nickname ' + postToNickname)
|
||||||
self.postToNickname = postToNickname
|
self.postToNickname = postToNickname
|
||||||
|
|
||||||
return postMessageToOutbox(messageJson, self.postToNickname,
|
return postMessageToOutbox(messageJson, self.postToNickname,
|
||||||
self.server, self.server.baseDir,
|
self.server, self.server.baseDir,
|
||||||
self.server.httpPrefix,
|
self.server.httpPrefix,
|
||||||
|
|
@ -4047,7 +4049,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if '?page=' in hashtag:
|
if '?page=' in hashtag:
|
||||||
hashtag = hashtag.split('?page=')[0]
|
hashtag = hashtag.split('?page=')[0]
|
||||||
if isBlockedHashtag(baseDir, hashtag):
|
if isBlockedHashtag(baseDir, hashtag):
|
||||||
msg = htmlHashtagBlocked(baseDir).encode('utf-8')
|
msg = htmlHashtagBlocked(baseDir,
|
||||||
|
self.server.translate).encode('utf-8')
|
||||||
self._login_headers('text/html', len(msg), callingDomain)
|
self._login_headers('text/html', len(msg), callingDomain)
|
||||||
self._write(msg)
|
self._write(msg)
|
||||||
self.server.GETbusy = False
|
self.server.GETbusy = False
|
||||||
|
|
@ -4093,6 +4096,60 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
'login shown done',
|
'login shown done',
|
||||||
'hashtag search')
|
'hashtag search')
|
||||||
|
|
||||||
|
def _hashtagSearchRSS2(self, callingDomain: str,
|
||||||
|
path: str, cookie: str,
|
||||||
|
baseDir: str, httpPrefix: str,
|
||||||
|
domain: str, domainFull: str, port: int,
|
||||||
|
onionDomain: str, i2pDomain: str,
|
||||||
|
GETstartTime, GETtimings: {}):
|
||||||
|
"""Return an RSS 2 feed for a hashtag
|
||||||
|
"""
|
||||||
|
hashtag = path.split('/tags/rss2/')[1]
|
||||||
|
if isBlockedHashtag(baseDir, hashtag):
|
||||||
|
self._400()
|
||||||
|
self.server.GETbusy = False
|
||||||
|
return
|
||||||
|
nickname = None
|
||||||
|
if '/users/' in path:
|
||||||
|
actor = \
|
||||||
|
httpPrefix + '://' + domainFull + path
|
||||||
|
nickname = \
|
||||||
|
getNicknameFromActor(actor)
|
||||||
|
hashtagStr = \
|
||||||
|
rssHashtagSearch(nickname,
|
||||||
|
domain, port,
|
||||||
|
self.server.recentPostsCache,
|
||||||
|
self.server.maxRecentPosts,
|
||||||
|
self.server.translate,
|
||||||
|
baseDir, hashtag,
|
||||||
|
maxPostsInFeed, self.server.session,
|
||||||
|
self.server.cachedWebfingers,
|
||||||
|
self.server.personCache,
|
||||||
|
httpPrefix,
|
||||||
|
self.server.projectVersion,
|
||||||
|
self.server.YTReplacementDomain)
|
||||||
|
if hashtagStr:
|
||||||
|
msg = hashtagStr.encode('utf-8')
|
||||||
|
self._set_headers('text/xml', len(msg),
|
||||||
|
cookie, callingDomain)
|
||||||
|
self._write(msg)
|
||||||
|
else:
|
||||||
|
originPathStr = path.split('/tags/rss2/')[0]
|
||||||
|
originPathStrAbsolute = \
|
||||||
|
httpPrefix + '://' + domainFull + originPathStr
|
||||||
|
if callingDomain.endswith('.onion') and onionDomain:
|
||||||
|
originPathStrAbsolute = \
|
||||||
|
'http://' + onionDomain + originPathStr
|
||||||
|
elif (callingDomain.endswith('.i2p') and onionDomain):
|
||||||
|
originPathStrAbsolute = \
|
||||||
|
'http://' + i2pDomain + originPathStr
|
||||||
|
self._redirect_headers(originPathStrAbsolute + '/search',
|
||||||
|
cookie, callingDomain)
|
||||||
|
self.server.GETbusy = False
|
||||||
|
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||||
|
'login shown done',
|
||||||
|
'hashtag rss feed')
|
||||||
|
|
||||||
def _announceButton(self, callingDomain: str, path: str,
|
def _announceButton(self, callingDomain: str, path: str,
|
||||||
baseDir: str,
|
baseDir: str,
|
||||||
cookie: str, proxyType: str,
|
cookie: str, proxyType: str,
|
||||||
|
|
@ -4309,7 +4366,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.postLog,
|
self.server.postLog,
|
||||||
self.server.cachedWebfingers,
|
self.server.cachedWebfingers,
|
||||||
self.server.personCache,
|
self.server.personCache,
|
||||||
self.server.acceptedCaps,
|
|
||||||
debug,
|
debug,
|
||||||
self.server.projectVersion)
|
self.server.projectVersion)
|
||||||
originPathStrAbsolute = \
|
originPathStrAbsolute = \
|
||||||
|
|
@ -5178,7 +5234,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.translate,
|
self.server.translate,
|
||||||
self.server.projectVersion,
|
self.server.projectVersion,
|
||||||
baseDir, httpPrefix, True,
|
baseDir, httpPrefix, True,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'roles',
|
getPerson, 'roles',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
cachedWebfingers,
|
cachedWebfingers,
|
||||||
|
|
@ -5249,7 +5304,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.translate,
|
self.server.translate,
|
||||||
self.server.projectVersion,
|
self.server.projectVersion,
|
||||||
baseDir, httpPrefix, True,
|
baseDir, httpPrefix, True,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'skills',
|
getPerson, 'skills',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
cachedWebfingers,
|
cachedWebfingers,
|
||||||
|
|
@ -5516,7 +5570,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
proxyType: str, cookie: str,
|
proxyType: str, cookie: str,
|
||||||
debug: str,
|
debug: str,
|
||||||
recentPostsCache: {}, session,
|
recentPostsCache: {}, session,
|
||||||
ocapAlways: bool,
|
|
||||||
defaultTimeline: str,
|
defaultTimeline: str,
|
||||||
maxRecentPosts: int,
|
maxRecentPosts: int,
|
||||||
translate: {},
|
translate: {},
|
||||||
|
|
@ -5538,9 +5591,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'inbox',
|
maxPostsInFeed, 'inbox',
|
||||||
authorized,
|
authorized)
|
||||||
ocapAlways)
|
|
||||||
if inboxFeed:
|
if inboxFeed:
|
||||||
|
if GETstartTime:
|
||||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||||
'show status done',
|
'show status done',
|
||||||
'show inbox json')
|
'show inbox json')
|
||||||
|
|
@ -5566,8 +5619,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'inbox',
|
maxPostsInFeed, 'inbox',
|
||||||
authorized,
|
authorized)
|
||||||
ocapAlways)
|
if GETstartTime:
|
||||||
self._benchmarkGETtimings(GETstartTime,
|
self._benchmarkGETtimings(GETstartTime,
|
||||||
GETtimings,
|
GETtimings,
|
||||||
'show status done',
|
'show status done',
|
||||||
|
|
@ -5590,13 +5643,18 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
projectVersion,
|
projectVersion,
|
||||||
self._isMinimal(nickname),
|
self._isMinimal(nickname),
|
||||||
YTReplacementDomain)
|
YTReplacementDomain)
|
||||||
|
if GETstartTime:
|
||||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||||
'show status done',
|
'show status done',
|
||||||
'show inbox html')
|
'show inbox html')
|
||||||
|
|
||||||
|
if msg:
|
||||||
msg = msg.encode('utf-8')
|
msg = msg.encode('utf-8')
|
||||||
self._set_headers('text/html', len(msg),
|
self._set_headers('text/html', len(msg),
|
||||||
cookie, callingDomain)
|
cookie, callingDomain)
|
||||||
self._write(msg)
|
self._write(msg)
|
||||||
|
|
||||||
|
if GETstartTime:
|
||||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||||
'show status done',
|
'show status done',
|
||||||
'show inbox')
|
'show inbox')
|
||||||
|
|
@ -5647,8 +5705,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'dm',
|
maxPostsInFeed, 'dm',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
if inboxDMFeed:
|
if inboxDMFeed:
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
nickname = path.replace('/users/', '')
|
nickname = path.replace('/users/', '')
|
||||||
|
|
@ -5672,8 +5729,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'dm',
|
maxPostsInFeed, 'dm',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
msg = \
|
msg = \
|
||||||
htmlInboxDMs(self.server.defaultTimeline,
|
htmlInboxDMs(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -5748,7 +5804,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'tlreplies',
|
maxPostsInFeed, 'tlreplies',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
if not inboxRepliesFeed:
|
if not inboxRepliesFeed:
|
||||||
inboxRepliesFeed = []
|
inboxRepliesFeed = []
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
|
|
@ -5773,7 +5829,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'tlreplies',
|
maxPostsInFeed, 'tlreplies',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
msg = \
|
msg = \
|
||||||
htmlInboxReplies(self.server.defaultTimeline,
|
htmlInboxReplies(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -5848,7 +5904,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInMediaFeed, 'tlmedia',
|
maxPostsInMediaFeed, 'tlmedia',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
if not inboxMediaFeed:
|
if not inboxMediaFeed:
|
||||||
inboxMediaFeed = []
|
inboxMediaFeed = []
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
|
|
@ -5873,7 +5929,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInMediaFeed, 'tlmedia',
|
maxPostsInMediaFeed, 'tlmedia',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
msg = \
|
msg = \
|
||||||
htmlInboxMedia(self.server.defaultTimeline,
|
htmlInboxMedia(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -5948,7 +6004,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInBlogsFeed, 'tlblogs',
|
maxPostsInBlogsFeed, 'tlblogs',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
if not inboxBlogsFeed:
|
if not inboxBlogsFeed:
|
||||||
inboxBlogsFeed = []
|
inboxBlogsFeed = []
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
|
|
@ -5973,7 +6029,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInBlogsFeed, 'tlblogs',
|
maxPostsInBlogsFeed, 'tlblogs',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
msg = \
|
msg = \
|
||||||
htmlInboxBlogs(self.server.defaultTimeline,
|
htmlInboxBlogs(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -6106,7 +6162,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'tlbookmarks',
|
maxPostsInFeed, 'tlbookmarks',
|
||||||
authorized, self.server.ocapAlways)
|
authorized)
|
||||||
if bookmarksFeed:
|
if bookmarksFeed:
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
nickname = path.replace('/users/', '')
|
nickname = path.replace('/users/', '')
|
||||||
|
|
@ -6132,8 +6188,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed,
|
maxPostsInFeed,
|
||||||
'tlbookmarks',
|
'tlbookmarks',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
msg = \
|
msg = \
|
||||||
htmlBookmarks(self.server.defaultTimeline,
|
htmlBookmarks(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -6210,7 +6265,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'tlevents',
|
maxPostsInFeed, 'tlevents',
|
||||||
authorized, self.server.ocapAlways)
|
authorized)
|
||||||
print('eventsFeed: ' + str(eventsFeed))
|
print('eventsFeed: ' + str(eventsFeed))
|
||||||
if eventsFeed:
|
if eventsFeed:
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
|
|
@ -6236,8 +6291,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed,
|
maxPostsInFeed,
|
||||||
'tlevents',
|
'tlevents',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
msg = \
|
msg = \
|
||||||
htmlEvents(self.server.defaultTimeline,
|
htmlEvents(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -6306,8 +6360,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
port, path,
|
port, path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'outbox',
|
maxPostsInFeed, 'outbox',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
if outboxFeed:
|
if outboxFeed:
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
nickname = \
|
nickname = \
|
||||||
|
|
@ -6331,8 +6384,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'outbox',
|
maxPostsInFeed, 'outbox',
|
||||||
authorized,
|
authorized)
|
||||||
self.server.ocapAlways)
|
|
||||||
msg = \
|
msg = \
|
||||||
htmlOutbox(self.server.defaultTimeline,
|
htmlOutbox(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -6394,7 +6446,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path,
|
path,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'moderation',
|
maxPostsInFeed, 'moderation',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
if moderationFeed:
|
if moderationFeed:
|
||||||
if self._requestHTTP():
|
if self._requestHTTP():
|
||||||
nickname = path.replace('/users/', '')
|
nickname = path.replace('/users/', '')
|
||||||
|
|
@ -6418,7 +6470,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
path + '?page=1',
|
path + '?page=1',
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
maxPostsInFeed, 'moderation',
|
maxPostsInFeed, 'moderation',
|
||||||
True, self.server.ocapAlways)
|
True)
|
||||||
msg = \
|
msg = \
|
||||||
htmlModeration(self.server.defaultTimeline,
|
htmlModeration(self.server.defaultTimeline,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
|
|
@ -6521,7 +6573,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.projectVersion,
|
self.server.projectVersion,
|
||||||
baseDir, httpPrefix,
|
baseDir, httpPrefix,
|
||||||
authorized,
|
authorized,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'shares',
|
getPerson, 'shares',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
self.server.cachedWebfingers,
|
self.server.cachedWebfingers,
|
||||||
|
|
@ -6608,7 +6659,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.projectVersion,
|
self.server.projectVersion,
|
||||||
baseDir, httpPrefix,
|
baseDir, httpPrefix,
|
||||||
authorized,
|
authorized,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'following',
|
getPerson, 'following',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
self.server.cachedWebfingers,
|
self.server.cachedWebfingers,
|
||||||
|
|
@ -6695,7 +6745,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
baseDir,
|
baseDir,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
authorized,
|
authorized,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'followers',
|
getPerson, 'followers',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
self.server.cachedWebfingers,
|
self.server.cachedWebfingers,
|
||||||
|
|
@ -6757,7 +6806,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
baseDir,
|
baseDir,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
authorized,
|
authorized,
|
||||||
self.server.ocapAlways,
|
|
||||||
getPerson, 'posts',
|
getPerson, 'posts',
|
||||||
self.server.session,
|
self.server.session,
|
||||||
self.server.cachedWebfingers,
|
self.server.cachedWebfingers,
|
||||||
|
|
@ -8068,6 +8116,18 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
# hashtag search
|
# hashtag search
|
||||||
if self.path.startswith('/tags/') or \
|
if self.path.startswith('/tags/') or \
|
||||||
(authorized and '/tags/' in self.path):
|
(authorized and '/tags/' in self.path):
|
||||||
|
if self.path.startswith('/tags/rss2/'):
|
||||||
|
self._hashtagSearchRSS2(callingDomain,
|
||||||
|
self.path, cookie,
|
||||||
|
self.server.baseDir,
|
||||||
|
self.server.httpPrefix,
|
||||||
|
self.server.domain,
|
||||||
|
self.server.domainFull,
|
||||||
|
self.server.port,
|
||||||
|
self.server.onionDomain,
|
||||||
|
self.server.i2pDomain,
|
||||||
|
GETstartTime, GETtimings)
|
||||||
|
return
|
||||||
self._hashtagSearch(callingDomain,
|
self._hashtagSearch(callingDomain,
|
||||||
self.path, cookie,
|
self.path, cookie,
|
||||||
self.server.baseDir,
|
self.server.baseDir,
|
||||||
|
|
@ -8673,7 +8733,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
cookie, self.server.debug,
|
cookie, self.server.debug,
|
||||||
self.server.recentPostsCache,
|
self.server.recentPostsCache,
|
||||||
self.server.session,
|
self.server.session,
|
||||||
self.server.ocapAlways,
|
|
||||||
self.server.defaultTimeline,
|
self.server.defaultTimeline,
|
||||||
self.server.maxRecentPosts,
|
self.server.maxRecentPosts,
|
||||||
self.server.translate,
|
self.server.translate,
|
||||||
|
|
@ -9064,7 +9123,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
etag, callingDomain)
|
etag, callingDomain)
|
||||||
|
|
||||||
def _receiveNewPostProcess(self, postType: str, path: str, headers: {},
|
def _receiveNewPostProcess(self, postType: str, path: str, headers: {},
|
||||||
length: int, postBytes, boundary: str) -> int:
|
length: int, postBytes, boundary: str,
|
||||||
|
callingDomain: str, cookie: str,
|
||||||
|
authorized: bool) -> int:
|
||||||
# Note: this needs to happen synchronously
|
# Note: this needs to happen synchronously
|
||||||
# 0=this is not a new post
|
# 0=this is not a new post
|
||||||
# 1=new post success
|
# 1=new post success
|
||||||
|
|
@ -9205,6 +9266,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
privateEvent = False
|
privateEvent = False
|
||||||
else:
|
else:
|
||||||
privateEvent = True
|
privateEvent = True
|
||||||
|
|
||||||
if postType == 'newpost':
|
if postType == 'newpost':
|
||||||
messageJson = \
|
messageJson = \
|
||||||
createPublicPost(self.server.baseDir,
|
createPublicPost(self.server.baseDir,
|
||||||
|
|
@ -9224,6 +9286,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if messageJson:
|
if messageJson:
|
||||||
if fields['schedulePost']:
|
if fields['schedulePost']:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if self._postToOutbox(messageJson, __version__, nickname):
|
if self._postToOutbox(messageJson, __version__, nickname):
|
||||||
populateReplies(self.server.baseDir,
|
populateReplies(self.server.baseDir,
|
||||||
self.server.httpPrefix,
|
self.server.httpPrefix,
|
||||||
|
|
@ -9481,8 +9544,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if messageJson:
|
if messageJson:
|
||||||
if fields['schedulePost']:
|
if fields['schedulePost']:
|
||||||
return 1
|
return 1
|
||||||
# if self.server.debug:
|
print('Sending new DM to ' +
|
||||||
print('DEBUG: new DM to ' +
|
|
||||||
str(messageJson['object']['to']))
|
str(messageJson['object']['to']))
|
||||||
if self._postToOutbox(messageJson, __version__, nickname):
|
if self._postToOutbox(messageJson, __version__, nickname):
|
||||||
populateReplies(self.server.baseDir,
|
populateReplies(self.server.baseDir,
|
||||||
|
|
@ -9618,7 +9680,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return 1
|
return 1
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
def _receiveNewPost(self, postType: str, path: str) -> int:
|
def _receiveNewPost(self, postType: str, path: str,
|
||||||
|
callingDomain: str, cookie: str,
|
||||||
|
authorized: bool) -> int:
|
||||||
"""A new post has been created
|
"""A new post has been created
|
||||||
This creates a thread to send the new post
|
This creates a thread to send the new post
|
||||||
"""
|
"""
|
||||||
|
|
@ -9719,7 +9783,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
print('Creating new post from: ' + newPostThreadName)
|
print('Creating new post from: ' + newPostThreadName)
|
||||||
self._receiveNewPostProcess(postType,
|
self._receiveNewPostProcess(postType,
|
||||||
path, headers, length,
|
path, headers, length,
|
||||||
postBytes, boundary)
|
postBytes, boundary,
|
||||||
|
callingDomain, cookie,
|
||||||
|
authorized)
|
||||||
return pageNumber
|
return pageNumber
|
||||||
|
|
||||||
def _cryptoAPIreadHandle(self):
|
def _cryptoAPIreadHandle(self):
|
||||||
|
|
@ -10179,7 +10245,10 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
elif currPostType == 'newevent':
|
elif currPostType == 'newevent':
|
||||||
postRedirect = 'tlevents'
|
postRedirect = 'tlevents'
|
||||||
|
|
||||||
pageNumber = self._receiveNewPost(currPostType, self.path)
|
pageNumber = \
|
||||||
|
self._receiveNewPost(currPostType, self.path,
|
||||||
|
callingDomain, cookie,
|
||||||
|
authorized)
|
||||||
if pageNumber:
|
if pageNumber:
|
||||||
nickname = self.path.split('/users/')[1]
|
nickname = self.path.split('/users/')[1]
|
||||||
if '/' in nickname:
|
if '/' in nickname:
|
||||||
|
|
@ -10232,7 +10301,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.path.endswith('/inbox') or
|
self.path.endswith('/inbox') or
|
||||||
self.path.endswith('/shares') or
|
self.path.endswith('/shares') or
|
||||||
self.path.endswith('/moderationaction') or
|
self.path.endswith('/moderationaction') or
|
||||||
self.path.endswith('/caps/new') or
|
|
||||||
self.path == '/sharedInbox'):
|
self.path == '/sharedInbox'):
|
||||||
print('Attempt to POST to invalid path ' + self.path)
|
print('Attempt to POST to invalid path ' + self.path)
|
||||||
self._400()
|
self._400()
|
||||||
|
|
@ -10554,8 +10622,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
||||||
port=80, proxyPort=80, httpPrefix='https',
|
port=80, proxyPort=80, httpPrefix='https',
|
||||||
fedList=[], maxMentions=10, maxEmoji=10,
|
fedList=[], maxMentions=10, maxEmoji=10,
|
||||||
authenticatedFetch=False,
|
authenticatedFetch=False,
|
||||||
noreply=False, nolike=False, nopics=False,
|
|
||||||
noannounce=False, cw=False, ocapAlways=False,
|
|
||||||
proxyType=None, maxReplies=64,
|
proxyType=None, maxReplies=64,
|
||||||
domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
|
domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
|
||||||
allowDeletion=False, debug=False, unitTest=False,
|
allowDeletion=False, debug=False, unitTest=False,
|
||||||
|
|
@ -10687,7 +10753,6 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
||||||
httpd.sendThreads = sendThreads
|
httpd.sendThreads = sendThreads
|
||||||
httpd.postLog = []
|
httpd.postLog = []
|
||||||
httpd.maxQueueLength = 64
|
httpd.maxQueueLength = 64
|
||||||
httpd.ocapAlways = ocapAlways
|
|
||||||
httpd.allowDeletion = allowDeletion
|
httpd.allowDeletion = allowDeletion
|
||||||
httpd.lastLoginTime = 0
|
httpd.lastLoginTime = 0
|
||||||
httpd.maxReplies = maxReplies
|
httpd.maxReplies = maxReplies
|
||||||
|
|
@ -10695,19 +10760,8 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
||||||
httpd.tokensLookup = {}
|
httpd.tokensLookup = {}
|
||||||
loadTokens(baseDir, httpd.tokens, httpd.tokensLookup)
|
loadTokens(baseDir, httpd.tokens, httpd.tokensLookup)
|
||||||
httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch
|
httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch
|
||||||
httpd.acceptedCaps = ["inbox:write", "objects:read"]
|
|
||||||
# contains threads used to send posts to followers
|
# contains threads used to send posts to followers
|
||||||
httpd.followersThreads = []
|
httpd.followersThreads = []
|
||||||
if noreply:
|
|
||||||
httpd.acceptedCaps.append('inbox:noreply')
|
|
||||||
if nolike:
|
|
||||||
httpd.acceptedCaps.append('inbox:nolike')
|
|
||||||
if nopics:
|
|
||||||
httpd.acceptedCaps.append('inbox:nopics')
|
|
||||||
if noannounce:
|
|
||||||
httpd.acceptedCaps.append('inbox:noannounce')
|
|
||||||
if cw:
|
|
||||||
httpd.acceptedCaps.append('inbox:cw')
|
|
||||||
|
|
||||||
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
|
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
|
||||||
print('Creating shared inbox: inbox@' + domain)
|
print('Creating shared inbox: inbox@' + domain)
|
||||||
|
|
@ -10778,12 +10832,11 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
||||||
httpd.personCache, httpd.inboxQueue,
|
httpd.personCache, httpd.inboxQueue,
|
||||||
domain, onionDomain, i2pDomain, port, proxyType,
|
domain, onionDomain, i2pDomain, port, proxyType,
|
||||||
httpd.federationList,
|
httpd.federationList,
|
||||||
httpd.ocapAlways, maxReplies,
|
maxReplies,
|
||||||
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
||||||
allowDeletion, debug, maxMentions, maxEmoji,
|
allowDeletion, debug, maxMentions, maxEmoji,
|
||||||
httpd.translate, unitTest,
|
httpd.translate, unitTest,
|
||||||
httpd.YTReplacementDomain,
|
httpd.YTReplacementDomain), daemon=True)
|
||||||
httpd.acceptedCaps), daemon=True)
|
|
||||||
print('Creating scheduled post thread')
|
print('Creating scheduled post thread')
|
||||||
httpd.thrPostSchedule = \
|
httpd.thrPostSchedule = \
|
||||||
threadWithTrace(target=runPostSchedule,
|
threadWithTrace(target=runPostSchedule,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ def createDelete(session, baseDir: str, federationList: [],
|
||||||
objectUrl is typically the url of the message, corresponding to url
|
objectUrl is typically the url of the message, corresponding to url
|
||||||
or atomUri in createPostBase
|
or atomUri in createPostBase
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if ':' in domain:
|
if ':' in domain:
|
||||||
|
|
@ -137,8 +137,7 @@ def sendDeleteViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
@ -160,7 +159,7 @@ def sendDeleteViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, newDeleteJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, newDeleteJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
|
||||||
121
epicyon.py
121
epicyon.py
|
|
@ -16,6 +16,7 @@ from skills import setSkillLevel
|
||||||
from roles import setRole
|
from roles import setRole
|
||||||
from webfinger import webfingerHandle
|
from webfinger import webfingerHandle
|
||||||
from posts import getPublicPostDomains
|
from posts import getPublicPostDomains
|
||||||
|
from posts import getPublicPostDomainsBlocked
|
||||||
from posts import sendBlockViaServer
|
from posts import sendBlockViaServer
|
||||||
from posts import sendUndoBlockViaServer
|
from posts import sendUndoBlockViaServer
|
||||||
from posts import createPublicPost
|
from posts import createPublicPost
|
||||||
|
|
@ -24,6 +25,7 @@ from posts import archivePosts
|
||||||
from posts import sendPostViaServer
|
from posts import sendPostViaServer
|
||||||
from posts import getPublicPostsOfPerson
|
from posts import getPublicPostsOfPerson
|
||||||
from posts import getUserUrl
|
from posts import getUserUrl
|
||||||
|
from posts import checkDomains
|
||||||
from session import createSession
|
from session import createSession
|
||||||
from session import getJson
|
from session import getJson
|
||||||
from filters import addFilter
|
from filters import addFilter
|
||||||
|
|
@ -71,7 +73,9 @@ from socnet import instancesGraph
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
def str2bool(v):
|
def str2bool(v) -> bool:
|
||||||
|
"""Returns true if the given value is a boolean
|
||||||
|
"""
|
||||||
if isinstance(v, bool):
|
if isinstance(v, bool):
|
||||||
return v
|
return v
|
||||||
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
||||||
|
|
@ -155,6 +159,14 @@ parser.add_argument('--postDomains', dest='postDomains', type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Show domains referenced in public '
|
help='Show domains referenced in public '
|
||||||
'posts for the given handle')
|
'posts for the given handle')
|
||||||
|
parser.add_argument('--postDomainsBlocked', dest='postDomainsBlocked',
|
||||||
|
type=str, default=None,
|
||||||
|
help='Show blocked domains referenced in public '
|
||||||
|
'posts for the given handle')
|
||||||
|
parser.add_argument('--checkDomains', dest='checkDomains', type=str,
|
||||||
|
default=None,
|
||||||
|
help='Check domains of non-mutual followers for '
|
||||||
|
'domains which are globally blocked by this instance')
|
||||||
parser.add_argument('--socnet', dest='socnet', type=str,
|
parser.add_argument('--socnet', dest='socnet', type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Show dot diagram for social network '
|
help='Show dot diagram for social network '
|
||||||
|
|
@ -218,26 +230,6 @@ parser.add_argument("--testsnetwork", type=str2bool, nargs='?',
|
||||||
parser.add_argument("--testdata", type=str2bool, nargs='?',
|
parser.add_argument("--testdata", type=str2bool, nargs='?',
|
||||||
const=True, default=False,
|
const=True, default=False,
|
||||||
help="Generate some data for testing purposes")
|
help="Generate some data for testing purposes")
|
||||||
parser.add_argument("--ocap", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Always strictly enforce object capabilities")
|
|
||||||
parser.add_argument("--noreply", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Default capabilities don't allow replies on posts")
|
|
||||||
parser.add_argument("--nolike", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Default capabilities don't allow " +
|
|
||||||
"likes/favourites on posts")
|
|
||||||
parser.add_argument("--nopics", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Default capabilities don't allow attached pictures")
|
|
||||||
parser.add_argument("--noannounce", "--norepeat", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Default capabilities don't allow announce/repeat")
|
|
||||||
parser.add_argument("--cw", type=str2bool, nargs='?',
|
|
||||||
const=True, default=False,
|
|
||||||
help="Default capabilities don't allow posts " +
|
|
||||||
"without content warnings")
|
|
||||||
parser.add_argument('--icon', '--avatar', dest='avatar', type=str,
|
parser.add_argument('--icon', '--avatar', dest='avatar', type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Set the avatar filename for an account')
|
help='Set the avatar filename for an account')
|
||||||
|
|
@ -465,7 +457,8 @@ if args.postDomains:
|
||||||
elif args.gnunet:
|
elif args.gnunet:
|
||||||
proxyType = 'gnunet'
|
proxyType = 'gnunet'
|
||||||
domainList = []
|
domainList = []
|
||||||
domainList = getPublicPostDomains(baseDir, nickname, domain,
|
domainList = getPublicPostDomains(None,
|
||||||
|
baseDir, nickname, domain,
|
||||||
proxyType, args.port,
|
proxyType, args.port,
|
||||||
httpPrefix, debug,
|
httpPrefix, debug,
|
||||||
__version__, domainList)
|
__version__, domainList)
|
||||||
|
|
@ -473,6 +466,83 @@ if args.postDomains:
|
||||||
print(postDomain)
|
print(postDomain)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
if args.postDomainsBlocked:
|
||||||
|
# Domains which were referenced in public posts by a
|
||||||
|
# given handle but which are globally blocked on this instance
|
||||||
|
if '@' not in args.postDomainsBlocked:
|
||||||
|
if '/users/' in args.postDomainsBlocked:
|
||||||
|
postsNickname = getNicknameFromActor(args.posts)
|
||||||
|
postsDomain, postsPort = getDomainFromActor(args.posts)
|
||||||
|
args.postDomainsBlocked = postsNickname + '@' + postsDomain
|
||||||
|
if postsPort:
|
||||||
|
if postsPort != 80 and postsPort != 443:
|
||||||
|
args.postDomainsBlocked += ':' + str(postsPort)
|
||||||
|
else:
|
||||||
|
print('Syntax: --postDomainsBlocked nickname@domain')
|
||||||
|
sys.exit()
|
||||||
|
if not args.http:
|
||||||
|
args.port = 443
|
||||||
|
nickname = args.postDomainsBlocked.split('@')[0]
|
||||||
|
domain = args.postDomainsBlocked.split('@')[1]
|
||||||
|
proxyType = None
|
||||||
|
if args.tor or domain.endswith('.onion'):
|
||||||
|
proxyType = 'tor'
|
||||||
|
if domain.endswith('.onion'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.i2p or domain.endswith('.i2p'):
|
||||||
|
proxyType = 'i2p'
|
||||||
|
if domain.endswith('.i2p'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.gnunet:
|
||||||
|
proxyType = 'gnunet'
|
||||||
|
domainList = []
|
||||||
|
domainList = getPublicPostDomainsBlocked(None,
|
||||||
|
baseDir, nickname, domain,
|
||||||
|
proxyType, args.port,
|
||||||
|
httpPrefix, debug,
|
||||||
|
__version__, domainList)
|
||||||
|
for postDomain in domainList:
|
||||||
|
print(postDomain)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if args.checkDomains:
|
||||||
|
# Domains which were referenced in public posts by a
|
||||||
|
# given handle but which are globally blocked on this instance
|
||||||
|
if '@' not in args.checkDomains:
|
||||||
|
if '/users/' in args.checkDomains:
|
||||||
|
postsNickname = getNicknameFromActor(args.posts)
|
||||||
|
postsDomain, postsPort = getDomainFromActor(args.posts)
|
||||||
|
args.checkDomains = postsNickname + '@' + postsDomain
|
||||||
|
if postsPort:
|
||||||
|
if postsPort != 80 and postsPort != 443:
|
||||||
|
args.checkDomains += ':' + str(postsPort)
|
||||||
|
else:
|
||||||
|
print('Syntax: --checkDomains nickname@domain')
|
||||||
|
sys.exit()
|
||||||
|
if not args.http:
|
||||||
|
args.port = 443
|
||||||
|
nickname = args.checkDomains.split('@')[0]
|
||||||
|
domain = args.checkDomains.split('@')[1]
|
||||||
|
proxyType = None
|
||||||
|
if args.tor or domain.endswith('.onion'):
|
||||||
|
proxyType = 'tor'
|
||||||
|
if domain.endswith('.onion'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.i2p or domain.endswith('.i2p'):
|
||||||
|
proxyType = 'i2p'
|
||||||
|
if domain.endswith('.i2p'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.gnunet:
|
||||||
|
proxyType = 'gnunet'
|
||||||
|
maxBlockedDomains = 0
|
||||||
|
checkDomains(None,
|
||||||
|
baseDir, nickname, domain,
|
||||||
|
proxyType, args.port,
|
||||||
|
httpPrefix, debug,
|
||||||
|
__version__,
|
||||||
|
maxBlockedDomains, False)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
if args.socnet:
|
if args.socnet:
|
||||||
if ',' not in args.socnet:
|
if ',' not in args.socnet:
|
||||||
print('Syntax: '
|
print('Syntax: '
|
||||||
|
|
@ -718,7 +788,6 @@ if args.approve:
|
||||||
postLog = []
|
postLog = []
|
||||||
cachedWebfingers = {}
|
cachedWebfingers = {}
|
||||||
personCache = {}
|
personCache = {}
|
||||||
acceptedCaps = []
|
|
||||||
manualApproveFollowRequest(session, baseDir,
|
manualApproveFollowRequest(session, baseDir,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
args.nickname, domain, port,
|
args.nickname, domain, port,
|
||||||
|
|
@ -726,7 +795,6 @@ if args.approve:
|
||||||
federationList,
|
federationList,
|
||||||
sendThreads, postLog,
|
sendThreads, postLog,
|
||||||
cachedWebfingers, personCache,
|
cachedWebfingers, personCache,
|
||||||
acceptedCaps,
|
|
||||||
debug, __version__)
|
debug, __version__)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
@ -1111,9 +1179,6 @@ if args.port:
|
||||||
if args.proxyPort:
|
if args.proxyPort:
|
||||||
proxyPort = args.proxyPort
|
proxyPort = args.proxyPort
|
||||||
setConfigParam(baseDir, 'proxyPort', proxyPort)
|
setConfigParam(baseDir, 'proxyPort', proxyPort)
|
||||||
ocapAlways = False
|
|
||||||
if args.ocap:
|
|
||||||
ocapAlways = args.ocap
|
|
||||||
if args.gnunet:
|
if args.gnunet:
|
||||||
httpPrefix = 'gnunet'
|
httpPrefix = 'gnunet'
|
||||||
if args.dat:
|
if args.dat:
|
||||||
|
|
@ -1830,8 +1895,6 @@ if __name__ == "__main__":
|
||||||
port, proxyPort, httpPrefix,
|
port, proxyPort, httpPrefix,
|
||||||
federationList, args.maxMentions,
|
federationList, args.maxMentions,
|
||||||
args.maxEmoji, args.authenticatedFetch,
|
args.maxEmoji, args.authenticatedFetch,
|
||||||
args.noreply, args.nolike, args.nopics,
|
|
||||||
args.noannounce, args.cw, ocapAlways,
|
|
||||||
proxyType, args.maxReplies,
|
proxyType, args.maxReplies,
|
||||||
args.domainMaxPostsPerDay,
|
args.domainMaxPostsPerDay,
|
||||||
args.accountMaxPostsPerDay,
|
args.accountMaxPostsPerDay,
|
||||||
|
|
|
||||||
93
follow.py
93
follow.py
|
|
@ -8,6 +8,7 @@ __status__ = "Production"
|
||||||
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
import os
|
import os
|
||||||
|
from utils import getFollowersList
|
||||||
from utils import validNickname
|
from utils import validNickname
|
||||||
from utils import domainPermitted
|
from utils import domainPermitted
|
||||||
from utils import getDomainFromActor
|
from utils import getDomainFromActor
|
||||||
|
|
@ -112,15 +113,14 @@ def isFollowingActor(baseDir: str,
|
||||||
|
|
||||||
|
|
||||||
def getMutualsOfPerson(baseDir: str,
|
def getMutualsOfPerson(baseDir: str,
|
||||||
nickname: str, domain: str,
|
nickname: str, domain: str) -> []:
|
||||||
followFile='following.txt') -> []:
|
|
||||||
"""Returns the mutuals of a person
|
"""Returns the mutuals of a person
|
||||||
i.e. accounts which they follow and which also follow back
|
i.e. accounts which they follow and which also follow back
|
||||||
"""
|
"""
|
||||||
followers = \
|
followers = \
|
||||||
getFollowersOfPerson(baseDir, nickname, domain, 'followers')
|
getFollowersList(baseDir, nickname, domain, 'followers.txt')
|
||||||
following = \
|
following = \
|
||||||
getFollowersOfPerson(baseDir, nickname, domain, 'following')
|
getFollowersList(baseDir, nickname, domain, 'following.txt')
|
||||||
mutuals = []
|
mutuals = []
|
||||||
for handle in following:
|
for handle in following:
|
||||||
if handle in followers:
|
if handle in followers:
|
||||||
|
|
@ -128,36 +128,6 @@ def getMutualsOfPerson(baseDir: str,
|
||||||
return mutuals
|
return mutuals
|
||||||
|
|
||||||
|
|
||||||
def getFollowersOfPerson(baseDir: str,
|
|
||||||
nickname: str, domain: str,
|
|
||||||
followFile='following.txt') -> []:
|
|
||||||
"""Returns a list containing the followers of the given person
|
|
||||||
Used by the shared inbox to know who to send incoming mail to
|
|
||||||
"""
|
|
||||||
followers = []
|
|
||||||
if ':' in domain:
|
|
||||||
domain = domain.split(':')[0]
|
|
||||||
handle = nickname + '@' + domain
|
|
||||||
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
|
||||||
return followers
|
|
||||||
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
|
|
||||||
for account in dirs:
|
|
||||||
filename = os.path.join(subdir, account) + '/' + followFile
|
|
||||||
if account == handle or account.startswith('inbox@'):
|
|
||||||
continue
|
|
||||||
if not os.path.isfile(filename):
|
|
||||||
continue
|
|
||||||
with open(filename, 'r') as followingfile:
|
|
||||||
for followingHandle in followingfile:
|
|
||||||
followingHandle2 = followingHandle.replace('\n', '')
|
|
||||||
followingHandle2 = followingHandle2.replace('\r', '')
|
|
||||||
if followingHandle2 == handle:
|
|
||||||
if account not in followers:
|
|
||||||
followers.append(account)
|
|
||||||
break
|
|
||||||
return followers
|
|
||||||
|
|
||||||
|
|
||||||
def followerOfPerson(baseDir: str, nickname: str, domain: str,
|
def followerOfPerson(baseDir: str, nickname: str, domain: str,
|
||||||
followerNickname: str, followerDomain: str,
|
followerNickname: str, followerDomain: str,
|
||||||
federationList: [], debug: bool) -> bool:
|
federationList: [], debug: bool) -> bool:
|
||||||
|
|
@ -543,8 +513,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
|
||||||
port: int, sendThreads: [], postLog: [],
|
port: int, sendThreads: [], postLog: [],
|
||||||
cachedWebfingers: {}, personCache: {},
|
cachedWebfingers: {}, personCache: {},
|
||||||
messageJson: {}, federationList: [],
|
messageJson: {}, federationList: [],
|
||||||
debug: bool, projectVersion: str,
|
debug: bool, projectVersion: str) -> bool:
|
||||||
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
|
|
||||||
"""Receives a follow request within the POST section of HTTPServer
|
"""Receives a follow request within the POST section of HTTPServer
|
||||||
"""
|
"""
|
||||||
if not messageJson['type'].startswith('Follow'):
|
if not messageJson['type'].startswith('Follow'):
|
||||||
|
|
@ -685,8 +654,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
|
||||||
nicknameToFollow, domainToFollow, port,
|
nicknameToFollow, domainToFollow, port,
|
||||||
nickname, domain, fromPort,
|
nickname, domain, fromPort,
|
||||||
messageJson['actor'], federationList,
|
messageJson['actor'], federationList,
|
||||||
messageJson, acceptedCaps,
|
messageJson, sendThreads, postLog,
|
||||||
sendThreads, postLog,
|
|
||||||
cachedWebfingers, personCache,
|
cachedWebfingers, personCache,
|
||||||
debug, projectVersion, True)
|
debug, projectVersion, True)
|
||||||
|
|
||||||
|
|
@ -696,8 +664,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
|
||||||
port: int,
|
port: int,
|
||||||
nickname: str, domain: str, fromPort: int,
|
nickname: str, domain: str, fromPort: int,
|
||||||
personUrl: str, federationList: [],
|
personUrl: str, federationList: [],
|
||||||
followJson: {}, acceptedCaps: [],
|
followJson: {}, sendThreads: [], postLog: [],
|
||||||
sendThreads: [], postLog: [],
|
|
||||||
cachedWebfingers: {}, personCache: {},
|
cachedWebfingers: {}, personCache: {},
|
||||||
debug: bool, projectVersion: str,
|
debug: bool, projectVersion: str,
|
||||||
removeFollowActivity: bool):
|
removeFollowActivity: bool):
|
||||||
|
|
@ -715,7 +682,7 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
|
||||||
acceptJson = createAccept(baseDir, federationList,
|
acceptJson = createAccept(baseDir, federationList,
|
||||||
nicknameToFollow, domainToFollow, port,
|
nicknameToFollow, domainToFollow, port,
|
||||||
personUrl, '', httpPrefix,
|
personUrl, '', httpPrefix,
|
||||||
followJson, acceptedCaps)
|
followJson)
|
||||||
if debug:
|
if debug:
|
||||||
pprint(acceptJson)
|
pprint(acceptJson)
|
||||||
print('DEBUG: sending follow Accept from ' +
|
print('DEBUG: sending follow Accept from ' +
|
||||||
|
|
@ -938,8 +905,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
@ -961,7 +927,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, newFollowJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, newFollowJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
@ -1037,9 +1003,10 @@ def sendUnfollowRequestViaServer(baseDir: str, session,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition, avatarUrl,
|
avatarUrl, displayName) = getPersonBox(baseDir, session,
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix,
|
||||||
|
fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
||||||
if not inboxUrl:
|
if not inboxUrl:
|
||||||
|
|
@ -1059,7 +1026,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, unfollowJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, unfollowJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
@ -1075,14 +1042,12 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
|
||||||
"""In a shared inbox if we receive a post we know who it's from
|
"""In a shared inbox if we receive a post we know who it's from
|
||||||
and if it's addressed to followers then we need to get a list of those.
|
and if it's addressed to followers then we need to get a list of those.
|
||||||
This returns a list of account handles which follow the given actor
|
This returns a list of account handles which follow the given actor
|
||||||
and also the corresponding capability id if it exists
|
|
||||||
"""
|
"""
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: getting followers of ' + actor)
|
print('DEBUG: getting followers of ' + actor)
|
||||||
recipientsDict = {}
|
recipientsDict = {}
|
||||||
if ':' not in actor:
|
if ':' not in actor:
|
||||||
return recipientsDict
|
return recipientsDict
|
||||||
httpPrefix = actor.split(':')[0]
|
|
||||||
nickname = getNicknameFromActor(actor)
|
nickname = getNicknameFromActor(actor)
|
||||||
if not nickname:
|
if not nickname:
|
||||||
if debug:
|
if debug:
|
||||||
|
|
@ -1114,34 +1079,6 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: ' + account +
|
print('DEBUG: ' + account +
|
||||||
' follows ' + actorHandle)
|
' follows ' + actorHandle)
|
||||||
ocapFilename = baseDir + '/accounts/' + \
|
|
||||||
account + '/ocap/accept/' + httpPrefix + \
|
|
||||||
':##' + domain + ':' + str(port) + \
|
|
||||||
'#users#' + nickname + '.json'
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: checking capabilities of' + account)
|
|
||||||
if os.path.isfile(ocapFilename):
|
|
||||||
ocapJson = loadJson(ocapFilename)
|
|
||||||
if ocapJson:
|
|
||||||
if ocapJson.get('id'):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: ' +
|
|
||||||
'capabilities id found for ' +
|
|
||||||
account)
|
|
||||||
|
|
||||||
recipientsDict[account] = ocapJson['id']
|
|
||||||
else:
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: ' +
|
|
||||||
'capabilities has no ' +
|
|
||||||
'id attribute')
|
|
||||||
recipientsDict[account] = None
|
|
||||||
else:
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: ' +
|
|
||||||
'No capabilities file found for ' +
|
|
||||||
account + ' granted by ' + actorHandle)
|
|
||||||
print(ocapFilename)
|
|
||||||
recipientsDict[account] = None
|
recipientsDict[account] = None
|
||||||
return recipientsDict
|
return recipientsDict
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 2.8 KiB |
192
inbox.py
192
inbox.py
|
|
@ -40,9 +40,6 @@ from pprint import pprint
|
||||||
from cache import getPersonFromCache
|
from cache import getPersonFromCache
|
||||||
from cache import storePersonInCache
|
from cache import storePersonInCache
|
||||||
from acceptreject import receiveAcceptReject
|
from acceptreject import receiveAcceptReject
|
||||||
from capabilities import getOcapFilename
|
|
||||||
from capabilities import CapablePost
|
|
||||||
from capabilities import capabilitiesReceiveUpdate
|
|
||||||
from bookmarks import updateBookmarksCollection
|
from bookmarks import updateBookmarksCollection
|
||||||
from bookmarks import undoBookmarksCollectionEntry
|
from bookmarks import undoBookmarksCollectionEntry
|
||||||
from blocking import isBlocked
|
from blocking import isBlocked
|
||||||
|
|
@ -268,7 +265,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
|
||||||
if domain in actor:
|
if domain in actor:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not urlPermitted(actor, federationList, "inbox:write"):
|
if not urlPermitted(actor, federationList):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce')
|
alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce')
|
||||||
|
|
@ -281,7 +278,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
|
||||||
inReplyTo = messageJson['object']['inReplyTo']
|
inReplyTo = messageJson['object']['inReplyTo']
|
||||||
if not isinstance(inReplyTo, str):
|
if not isinstance(inReplyTo, str):
|
||||||
return False
|
return False
|
||||||
if not urlPermitted(inReplyTo, federationList, "inbox:write"):
|
if not urlPermitted(inReplyTo, federationList):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
@ -437,81 +434,12 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def inboxCheckCapabilities(baseDir: str, nickname: str, domain: str,
|
|
||||||
actor: str, queueFilename: str, queue: [],
|
|
||||||
queueJson: {}, capabilityId: str,
|
|
||||||
debug: bool) -> bool:
|
|
||||||
if nickname == 'inbox':
|
|
||||||
return True
|
|
||||||
|
|
||||||
ocapFilename = \
|
|
||||||
getOcapFilename(baseDir,
|
|
||||||
queueJson['nickname'], queueJson['domain'],
|
|
||||||
actor, 'accept')
|
|
||||||
if not ocapFilename:
|
|
||||||
return False
|
|
||||||
if not os.path.isfile(ocapFilename):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: capabilities for ' +
|
|
||||||
actor + ' do not exist')
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
oc = loadJson(ocapFilename)
|
|
||||||
if not oc:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not oc.get('id'):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: capabilities for ' + actor + ' do not contain an id')
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if oc['id'] != capabilityId:
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: capability id mismatch')
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not oc.get('capability'):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: missing capability list')
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not CapablePost(queueJson['post'], oc['capability'], debug):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: insufficient capabilities to write to inbox from ' +
|
|
||||||
actor)
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: object capabilities check success')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
|
def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
|
||||||
recipientsDict: {},
|
recipientsDict: {},
|
||||||
domainMatch: str, domain: str,
|
domainMatch: str, domain: str,
|
||||||
actor: str, debug: bool) -> bool:
|
actor: str, debug: bool) -> bool:
|
||||||
"""Given a list of post recipients (toList) from 'to' or 'cc' parameters
|
"""Given a list of post recipients (toList) from 'to' or 'cc' parameters
|
||||||
populate a recipientsDict with the handle and capabilities id for each
|
populate a recipientsDict with the handle for each
|
||||||
"""
|
"""
|
||||||
followerRecipients = False
|
followerRecipients = False
|
||||||
for recipient in toList:
|
for recipient in toList:
|
||||||
|
|
@ -523,23 +451,6 @@ def inboxPostRecipientsAdd(baseDir: str, httpPrefix: str, toList: [],
|
||||||
nickname = recipient.split(domainMatch)[1]
|
nickname = recipient.split(domainMatch)[1]
|
||||||
handle = nickname+'@'+domain
|
handle = nickname+'@'+domain
|
||||||
if os.path.isdir(baseDir + '/accounts/' + handle):
|
if os.path.isdir(baseDir + '/accounts/' + handle):
|
||||||
# are capabilities granted for this account to the
|
|
||||||
# sender (actor) of the post?
|
|
||||||
ocapFilename = \
|
|
||||||
baseDir + '/accounts/' + handle + \
|
|
||||||
'/ocap/accept/' + actor.replace('/', '#') + '.json'
|
|
||||||
if os.path.isfile(ocapFilename):
|
|
||||||
# read the granted capabilities and obtain the id
|
|
||||||
ocapJson = loadJson(ocapFilename)
|
|
||||||
if ocapJson:
|
|
||||||
if ocapJson.get('id'):
|
|
||||||
# append with the capabilities id
|
|
||||||
recipientsDict[handle] = ocapJson['id']
|
|
||||||
else:
|
|
||||||
recipientsDict[handle] = None
|
|
||||||
else:
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: ' + ocapFilename + ' not found')
|
|
||||||
recipientsDict[handle] = None
|
recipientsDict[handle] = None
|
||||||
else:
|
else:
|
||||||
if debug:
|
if debug:
|
||||||
|
|
@ -741,8 +652,7 @@ def receiveUndo(session, baseDir: str, httpPrefix: str,
|
||||||
port: int, sendThreads: [], postLog: [],
|
port: int, sendThreads: [], postLog: [],
|
||||||
cachedWebfingers: {}, personCache: {},
|
cachedWebfingers: {}, personCache: {},
|
||||||
messageJson: {}, federationList: [],
|
messageJson: {}, federationList: [],
|
||||||
debug: bool,
|
debug: bool) -> bool:
|
||||||
acceptedCaps=["inbox:write", "objects:read"]) -> bool:
|
|
||||||
"""Receives an undo request within the POST section of HTTPServer
|
"""Receives an undo request within the POST section of HTTPServer
|
||||||
"""
|
"""
|
||||||
if not messageJson['type'].startswith('Undo'):
|
if not messageJson['type'].startswith('Undo'):
|
||||||
|
|
@ -1005,24 +915,6 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str,
|
||||||
print('DEBUG: Profile update was received for ' +
|
print('DEBUG: Profile update was received for ' +
|
||||||
messageJson['object']['url'])
|
messageJson['object']['url'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if messageJson['object'].get('capability') and \
|
|
||||||
messageJson['object'].get('scope'):
|
|
||||||
nickname = getNicknameFromActor(messageJson['object']['scope'])
|
|
||||||
if nickname:
|
|
||||||
domain, tempPort = \
|
|
||||||
getDomainFromActor(messageJson['object']['scope'])
|
|
||||||
|
|
||||||
if messageJson['object']['type'] == 'Capability':
|
|
||||||
capability = messageJson['object']['capability']
|
|
||||||
if capabilitiesReceiveUpdate(baseDir, nickname, domain, port,
|
|
||||||
messageJson['actor'],
|
|
||||||
messageJson['object']['id'],
|
|
||||||
capability,
|
|
||||||
debug):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: An update was received')
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2124,20 +2016,19 @@ def inboxUpdateIndex(boxname: str, baseDir: str, handle: str,
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
session, keyId: str, handle: str, messageJson: {},
|
session, keyId: str, handle: str, messageJson: {},
|
||||||
baseDir: str, httpPrefix: str, sendThreads: [],
|
baseDir: str, httpPrefix: str, sendThreads: [],
|
||||||
postLog: [], cachedWebfingers: {}, personCache: {},
|
postLog: [], cachedWebfingers: {}, personCache: {},
|
||||||
queue: [], domain: str,
|
queue: [], domain: str,
|
||||||
onionDomain: str, i2pDomain: str,
|
onionDomain: str, i2pDomain: str,
|
||||||
port: int, proxyType: str,
|
port: int, proxyType: str,
|
||||||
federationList: [], ocapAlways: bool, debug: bool,
|
federationList: [], debug: bool,
|
||||||
acceptedCaps: [],
|
|
||||||
queueFilename: str, destinationFilename: str,
|
queueFilename: str, destinationFilename: str,
|
||||||
maxReplies: int, allowDeletion: bool,
|
maxReplies: int, allowDeletion: bool,
|
||||||
maxMentions: int, maxEmoji: int, translate: {},
|
maxMentions: int, maxEmoji: int, translate: {},
|
||||||
unitTest: bool, YTReplacementDomain: str) -> bool:
|
unitTest: bool, YTReplacementDomain: str) -> bool:
|
||||||
""" Anything which needs to be done after capabilities checks have passed
|
""" Anything which needs to be done after initial checks have passed
|
||||||
"""
|
"""
|
||||||
actor = keyId
|
actor = keyId
|
||||||
if '#' in actor:
|
if '#' in actor:
|
||||||
|
|
@ -2247,7 +2138,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: object capabilities passed')
|
print('DEBUG: initial checks passed')
|
||||||
print('copy queue file from ' + queueFilename +
|
print('copy queue file from ' + queueFilename +
|
||||||
' to ' + destinationFilename)
|
' to ' + destinationFilename)
|
||||||
|
|
||||||
|
|
@ -2526,13 +2417,11 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
cachedWebfingers: {}, personCache: {}, queue: [],
|
cachedWebfingers: {}, personCache: {}, queue: [],
|
||||||
domain: str,
|
domain: str,
|
||||||
onionDomain: str, i2pDomain: str, port: int, proxyType: str,
|
onionDomain: str, i2pDomain: str, port: int, proxyType: str,
|
||||||
federationList: [],
|
federationList: [], maxReplies: int,
|
||||||
ocapAlways: bool, maxReplies: int,
|
|
||||||
domainMaxPostsPerDay: int, accountMaxPostsPerDay: int,
|
domainMaxPostsPerDay: int, accountMaxPostsPerDay: int,
|
||||||
allowDeletion: bool, debug: bool, maxMentions: int,
|
allowDeletion: bool, debug: bool, maxMentions: int,
|
||||||
maxEmoji: int, translate: {}, unitTest: bool,
|
maxEmoji: int, translate: {}, unitTest: bool,
|
||||||
YTReplacementDomain: str,
|
YTReplacementDomain: str) -> None:
|
||||||
acceptedCaps=["inbox:write", "objects:read"]) -> None:
|
|
||||||
"""Processes received items and moves them to the appropriate
|
"""Processes received items and moves them to the appropriate
|
||||||
directories
|
directories
|
||||||
"""
|
"""
|
||||||
|
|
@ -2801,8 +2690,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
personCache,
|
personCache,
|
||||||
queueJson['post'],
|
queueJson['post'],
|
||||||
federationList,
|
federationList,
|
||||||
debug,
|
debug):
|
||||||
acceptedCaps=["inbox:write", "objects:read"]):
|
|
||||||
print('Queue: Undo accepted from ' + keyId)
|
print('Queue: Undo accepted from ' + keyId)
|
||||||
if os.path.isfile(queueFilename):
|
if os.path.isfile(queueFilename):
|
||||||
os.remove(queueFilename)
|
os.remove(queueFilename)
|
||||||
|
|
@ -2819,9 +2707,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
personCache,
|
personCache,
|
||||||
queueJson['post'],
|
queueJson['post'],
|
||||||
federationList,
|
federationList,
|
||||||
debug, projectVersion,
|
debug, projectVersion):
|
||||||
acceptedCaps=["inbox:write",
|
|
||||||
"objects:read"]):
|
|
||||||
if os.path.isfile(queueFilename):
|
if os.path.isfile(queueFilename):
|
||||||
os.remove(queueFilename)
|
os.remove(queueFilename)
|
||||||
if len(queue) > 0:
|
if len(queue) > 0:
|
||||||
|
|
@ -2917,22 +2803,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
pprint(recipientsDictFollowers)
|
pprint(recipientsDictFollowers)
|
||||||
print('*************************************')
|
print('*************************************')
|
||||||
|
|
||||||
if queueJson['post'].get('capability'):
|
|
||||||
if not isinstance(queueJson['post']['capability'], list):
|
|
||||||
print('Queue: capability on post should be a list')
|
|
||||||
if os.path.isfile(queueFilename):
|
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Copy any posts addressed to followers into the shared inbox
|
# Copy any posts addressed to followers into the shared inbox
|
||||||
# this avoid copying file multiple times to potentially many
|
# this avoid copying file multiple times to potentially many
|
||||||
# individual inboxes
|
# individual inboxes
|
||||||
# This obviously bypasses object capabilities and so
|
|
||||||
# any checking will needs to be handled at the time when inbox
|
|
||||||
# GET happens on individual accounts.
|
|
||||||
# See posts.py/createBoxBase
|
|
||||||
if len(recipientsDictFollowers) > 0:
|
if len(recipientsDictFollowers) > 0:
|
||||||
sharedInboxPostFilename = \
|
sharedInboxPostFilename = \
|
||||||
queueJson['destination'].replace(inboxHandle, inboxHandle)
|
queueJson['destination'].replace(inboxHandle, inboxHandle)
|
||||||
|
|
@ -2943,16 +2816,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
for handle, capsId in recipientsDict.items():
|
for handle, capsId in recipientsDict.items():
|
||||||
destination = \
|
destination = \
|
||||||
queueJson['destination'].replace(inboxHandle, handle)
|
queueJson['destination'].replace(inboxHandle, handle)
|
||||||
# check that capabilities are accepted
|
inboxAfterInitial(recentPostsCache,
|
||||||
if queueJson['post'].get('capability'):
|
|
||||||
capabilityIdList = queueJson['post']['capability']
|
|
||||||
# does the capability id list within the post
|
|
||||||
# contain the id of the recipient with this handle?
|
|
||||||
# Here the capability id begins with the handle,
|
|
||||||
# so this could also be matched separately, but it's
|
|
||||||
# probably not necessary
|
|
||||||
if capsId in capabilityIdList:
|
|
||||||
inboxAfterCapabilities(recentPostsCache,
|
|
||||||
maxRecentPosts,
|
maxRecentPosts,
|
||||||
session, keyId, handle,
|
session, keyId, handle,
|
||||||
queueJson['post'],
|
queueJson['post'],
|
||||||
|
|
@ -2963,32 +2827,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
domain,
|
domain,
|
||||||
onionDomain, i2pDomain,
|
onionDomain, i2pDomain,
|
||||||
port, proxyType,
|
port, proxyType,
|
||||||
federationList, ocapAlways,
|
federationList,
|
||||||
debug, acceptedCaps,
|
debug,
|
||||||
queueFilename, destination,
|
|
||||||
maxReplies, allowDeletion,
|
|
||||||
maxMentions, maxEmoji,
|
|
||||||
translate, unitTest,
|
|
||||||
YTReplacementDomain)
|
|
||||||
else:
|
|
||||||
print('Queue: object capabilities check has failed')
|
|
||||||
if debug:
|
|
||||||
pprint(queueJson['post'])
|
|
||||||
else:
|
|
||||||
if not ocapAlways:
|
|
||||||
inboxAfterCapabilities(recentPostsCache,
|
|
||||||
maxRecentPosts,
|
|
||||||
session, keyId, handle,
|
|
||||||
queueJson['post'],
|
|
||||||
baseDir, httpPrefix,
|
|
||||||
sendThreads, postLog,
|
|
||||||
cachedWebfingers,
|
|
||||||
personCache, queue,
|
|
||||||
domain,
|
|
||||||
onionDomain, i2pDomain,
|
|
||||||
port, proxyType,
|
|
||||||
federationList, ocapAlways,
|
|
||||||
debug, acceptedCaps,
|
|
||||||
queueFilename, destination,
|
queueFilename, destination,
|
||||||
maxReplies, allowDeletion,
|
maxReplies, allowDeletion,
|
||||||
maxMentions, maxEmoji,
|
maxMentions, maxEmoji,
|
||||||
|
|
@ -2996,8 +2836,6 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
YTReplacementDomain)
|
YTReplacementDomain)
|
||||||
if debug:
|
if debug:
|
||||||
pprint(queueJson['post'])
|
pprint(queueJson['post'])
|
||||||
print('No capability list within post')
|
|
||||||
print('ocapAlways: ' + str(ocapAlways))
|
|
||||||
|
|
||||||
print('Queue: Queue post accepted')
|
print('Queue: Queue post accepted')
|
||||||
if os.path.isfile(queueFilename):
|
if os.path.isfile(queueFilename):
|
||||||
|
|
|
||||||
16
like.py
16
like.py
|
|
@ -63,7 +63,7 @@ def like(recentPostsCache: {},
|
||||||
'to' might be a specific person (actor) whose post was liked
|
'to' might be a specific person (actor) whose post was liked
|
||||||
object is typically the url of the message which was liked
|
object is typically the url of the message which was liked
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fullDomain = domain
|
fullDomain = domain
|
||||||
|
|
@ -162,7 +162,7 @@ def undolike(recentPostsCache: {},
|
||||||
'to' might be a specific person (actor) whose post was liked
|
'to' might be a specific person (actor) whose post was liked
|
||||||
object is typically the url of the message which was liked
|
object is typically the url of the message which was liked
|
||||||
"""
|
"""
|
||||||
if not urlPermitted(objectUrl, federationList, "inbox:write"):
|
if not urlPermitted(objectUrl, federationList):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fullDomain = domain
|
fullDomain = domain
|
||||||
|
|
@ -267,8 +267,7 @@ def sendLikeViaServer(baseDir: str, session,
|
||||||
postToBox = 'outbox'
|
postToBox = 'outbox'
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey, fromPersonId,
|
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
|
||||||
sharedInbox, capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
@ -291,8 +290,7 @@ def sendLikeViaServer(baseDir: str, session,
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newLikeJson, [], inboxUrl,
|
postResult = postJson(session, newLikeJson, [], inboxUrl, headers)
|
||||||
headers, "inbox:write")
|
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: POST announce failed for c2s to ' + inboxUrl)
|
print('WARN: POST announce failed for c2s to ' + inboxUrl)
|
||||||
return 5
|
return 5
|
||||||
|
|
@ -352,8 +350,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
|
||||||
postToBox = 'outbox'
|
postToBox = 'outbox'
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey, fromPersonId,
|
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox,
|
||||||
sharedInbox, capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, fromNickname,
|
httpPrefix, fromNickname,
|
||||||
|
|
@ -375,8 +372,7 @@ def sendUndoLikeViaServer(baseDir: str, session,
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newUndoLikeJson, [], inboxUrl,
|
postResult = postJson(session, newUndoLikeJson, [], inboxUrl, headers)
|
||||||
headers, "inbox:write")
|
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: POST announce failed for c2s to ' + inboxUrl)
|
print('WARN: POST announce failed for c2s to ' + inboxUrl)
|
||||||
return 5
|
return 5
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ def manualApproveFollowRequest(session, baseDir: str,
|
||||||
federationList: [],
|
federationList: [],
|
||||||
sendThreads: [], postLog: [],
|
sendThreads: [], postLog: [],
|
||||||
cachedWebfingers: {}, personCache: {},
|
cachedWebfingers: {}, personCache: {},
|
||||||
acceptedCaps: [],
|
|
||||||
debug: bool,
|
debug: bool,
|
||||||
projectVersion: str) -> None:
|
projectVersion: str) -> None:
|
||||||
"""Manually approve a follow request
|
"""Manually approve a follow request
|
||||||
|
|
@ -142,7 +141,7 @@ def manualApproveFollowRequest(session, baseDir: str,
|
||||||
approvePort,
|
approvePort,
|
||||||
followJson['actor'],
|
followJson['actor'],
|
||||||
federationList,
|
federationList,
|
||||||
followJson, acceptedCaps,
|
followJson,
|
||||||
sendThreads, postLog,
|
sendThreads, postLog,
|
||||||
cachedWebfingers, personCache,
|
cachedWebfingers, personCache,
|
||||||
debug, projectVersion, False)
|
debug, projectVersion, False)
|
||||||
|
|
|
||||||
180
ocaps.md
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,
|
def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
||||||
server, baseDir: str, httpPrefix: str,
|
server, baseDir: str, httpPrefix: str,
|
||||||
domain: str, domainFull: str,
|
domain: str, domainFull: str,
|
||||||
onionDomain: str, i2pDomain: str,
|
onionDomain: str, i2pDomain: str, port: int,
|
||||||
port: int,
|
|
||||||
recentPostsCache: {}, followersThreads: [],
|
recentPostsCache: {}, followersThreads: [],
|
||||||
federationList: [], sendThreads: [],
|
federationList: [], sendThreads: [],
|
||||||
postLog: [], cachedWebfingers: {},
|
postLog: [], cachedWebfingers: {},
|
||||||
|
|
|
||||||
36
person.py
36
person.py
|
|
@ -259,7 +259,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
'id': personId+'/endpoints',
|
'id': personId+'/endpoints',
|
||||||
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
||||||
},
|
},
|
||||||
'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
|
|
||||||
'followers': personId+'/followers',
|
'followers': personId+'/followers',
|
||||||
'following': personId+'/following',
|
'following': personId+'/following',
|
||||||
'shares': personId+'/shares',
|
'shares': personId+'/shares',
|
||||||
|
|
@ -327,8 +326,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
if not os.path.isdir(baseDir + peopleSubdir + '/' +
|
if not os.path.isdir(baseDir + peopleSubdir + '/' +
|
||||||
handle + '/outbox'):
|
handle + '/outbox'):
|
||||||
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox')
|
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox')
|
||||||
if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/ocap'):
|
|
||||||
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/ocap')
|
|
||||||
if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'):
|
if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'):
|
||||||
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
|
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
|
||||||
filename = baseDir + peopleSubdir + '/' + handle + '.json'
|
filename = baseDir + peopleSubdir + '/' + handle + '.json'
|
||||||
|
|
@ -506,15 +503,6 @@ def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
True, True, None)
|
True, True, None)
|
||||||
|
|
||||||
|
|
||||||
def createCapabilitiesInbox(baseDir: str, nickname: str,
|
|
||||||
domain: str, port: int,
|
|
||||||
httpPrefix: str) -> (str, str, {}, {}):
|
|
||||||
"""Generates the capabilities inbox to sign requests
|
|
||||||
"""
|
|
||||||
return createPersonBase(baseDir, nickname, domain, port,
|
|
||||||
httpPrefix, True, True, None)
|
|
||||||
|
|
||||||
|
|
||||||
def personUpgradeActor(baseDir: str, personJson: {},
|
def personUpgradeActor(baseDir: str, personJson: {},
|
||||||
handle: str, filename: str) -> None:
|
handle: str, filename: str) -> None:
|
||||||
"""Alter the actor to add any new properties
|
"""Alter the actor to add any new properties
|
||||||
|
|
@ -598,7 +586,7 @@ def personLookup(domain: str, path: str, baseDir: str) -> {}:
|
||||||
def personBoxJson(recentPostsCache: {},
|
def personBoxJson(recentPostsCache: {},
|
||||||
session, baseDir: str, domain: str, port: int, path: str,
|
session, baseDir: str, domain: str, port: int, path: str,
|
||||||
httpPrefix: str, noOfItems: int, boxname: str,
|
httpPrefix: str, noOfItems: int, boxname: str,
|
||||||
authorized: bool, ocapAlways: bool) -> {}:
|
authorized: bool) -> {}:
|
||||||
"""Obtain the inbox/outbox/moderation feed for the given person
|
"""Obtain the inbox/outbox/moderation feed for the given person
|
||||||
"""
|
"""
|
||||||
if boxname != 'inbox' and boxname != 'dm' and \
|
if boxname != 'inbox' and boxname != 'dm' and \
|
||||||
|
|
@ -644,38 +632,36 @@ def personBoxJson(recentPostsCache: {},
|
||||||
return createInbox(recentPostsCache,
|
return createInbox(recentPostsCache,
|
||||||
session, baseDir, nickname, domain, port,
|
session, baseDir, nickname, domain, port,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways, pageNumber)
|
noOfItems, headerOnly, pageNumber)
|
||||||
elif boxname == 'dm':
|
elif boxname == 'dm':
|
||||||
return createDMTimeline(recentPostsCache,
|
return createDMTimeline(recentPostsCache,
|
||||||
session, baseDir, nickname, domain, port,
|
session, baseDir, nickname, domain, port,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways, pageNumber)
|
noOfItems, headerOnly, pageNumber)
|
||||||
elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
|
elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
|
||||||
return createBookmarksTimeline(session, baseDir, nickname, domain,
|
return createBookmarksTimeline(session, baseDir, nickname, domain,
|
||||||
port, httpPrefix,
|
port, httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways,
|
noOfItems, headerOnly,
|
||||||
pageNumber)
|
pageNumber)
|
||||||
elif boxname == 'tlevents':
|
elif boxname == 'tlevents':
|
||||||
return createEventsTimeline(recentPostsCache,
|
return createEventsTimeline(recentPostsCache,
|
||||||
session, baseDir, nickname, domain,
|
session, baseDir, nickname, domain,
|
||||||
port, httpPrefix,
|
port, httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways,
|
noOfItems, headerOnly,
|
||||||
pageNumber)
|
pageNumber)
|
||||||
elif boxname == 'tlreplies':
|
elif boxname == 'tlreplies':
|
||||||
return createRepliesTimeline(recentPostsCache,
|
return createRepliesTimeline(recentPostsCache,
|
||||||
session, baseDir, nickname, domain,
|
session, baseDir, nickname, domain,
|
||||||
port, httpPrefix,
|
port, httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways,
|
noOfItems, headerOnly,
|
||||||
pageNumber)
|
pageNumber)
|
||||||
elif boxname == 'tlmedia':
|
elif boxname == 'tlmedia':
|
||||||
return createMediaTimeline(session, baseDir, nickname, domain, port,
|
return createMediaTimeline(session, baseDir, nickname, domain, port,
|
||||||
httpPrefix,
|
httpPrefix, noOfItems, headerOnly,
|
||||||
noOfItems, headerOnly, ocapAlways,
|
|
||||||
pageNumber)
|
pageNumber)
|
||||||
elif boxname == 'tlblogs':
|
elif boxname == 'tlblogs':
|
||||||
return createBlogsTimeline(session, baseDir, nickname, domain, port,
|
return createBlogsTimeline(session, baseDir, nickname, domain, port,
|
||||||
httpPrefix,
|
httpPrefix, noOfItems, headerOnly,
|
||||||
noOfItems, headerOnly, ocapAlways,
|
|
||||||
pageNumber)
|
pageNumber)
|
||||||
elif boxname == 'outbox':
|
elif boxname == 'outbox':
|
||||||
return createOutbox(session, baseDir, nickname, domain, port,
|
return createOutbox(session, baseDir, nickname, domain, port,
|
||||||
|
|
@ -685,14 +671,14 @@ def personBoxJson(recentPostsCache: {},
|
||||||
elif boxname == 'moderation':
|
elif boxname == 'moderation':
|
||||||
return createModeration(baseDir, nickname, domain, port,
|
return createModeration(baseDir, nickname, domain, port,
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
noOfItems, headerOnly, authorized,
|
noOfItems, headerOnly,
|
||||||
pageNumber)
|
pageNumber)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def personInboxJson(recentPostsCache: {},
|
def personInboxJson(recentPostsCache: {},
|
||||||
baseDir: str, domain: str, port: int, path: str,
|
baseDir: str, domain: str, port: int, path: str,
|
||||||
httpPrefix: str, noOfItems: int, ocapAlways: bool) -> []:
|
httpPrefix: str, noOfItems: int) -> []:
|
||||||
"""Obtain the inbox feed for the given person
|
"""Obtain the inbox feed for the given person
|
||||||
Authentication is expected to have already happened
|
Authentication is expected to have already happened
|
||||||
"""
|
"""
|
||||||
|
|
@ -729,7 +715,7 @@ def personInboxJson(recentPostsCache: {},
|
||||||
return None
|
return None
|
||||||
return createInbox(recentPostsCache, baseDir, nickname,
|
return createInbox(recentPostsCache, baseDir, nickname,
|
||||||
domain, port, httpPrefix,
|
domain, port, httpPrefix,
|
||||||
noOfItems, headerOnly, ocapAlways, pageNumber)
|
noOfItems, headerOnly, pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def setDisplayNickname(baseDir: str, nickname: str, domain: str,
|
def setDisplayNickname(baseDir: str, nickname: str, domain: str,
|
||||||
|
|
|
||||||
297
posts.py
297
posts.py
|
|
@ -14,6 +14,7 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import random
|
||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
@ -29,6 +30,8 @@ from session import postJsonString
|
||||||
from session import postImage
|
from session import postImage
|
||||||
from webfinger import webfingerHandle
|
from webfinger import webfingerHandle
|
||||||
from httpsig import createSignedHeader
|
from httpsig import createSignedHeader
|
||||||
|
from utils import getFollowersList
|
||||||
|
from utils import isEvil
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
from utils import siteIsActive
|
from utils import siteIsActive
|
||||||
from utils import getCachedPostFilename
|
from utils import getCachedPostFilename
|
||||||
|
|
@ -42,8 +45,6 @@ from utils import validNickname
|
||||||
from utils import locatePost
|
from utils import locatePost
|
||||||
from utils import loadJson
|
from utils import loadJson
|
||||||
from utils import saveJson
|
from utils import saveJson
|
||||||
from capabilities import getOcapFilename
|
|
||||||
from capabilities import capabilitiesUpdate
|
|
||||||
from media import attachMedia
|
from media import attachMedia
|
||||||
from media import replaceYouTube
|
from media import replaceYouTube
|
||||||
from content import removeHtml
|
from content import removeHtml
|
||||||
|
|
@ -207,7 +208,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
|
||||||
else:
|
else:
|
||||||
personUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
personUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
||||||
if not personUrl:
|
if not personUrl:
|
||||||
return None, None, None, None, None, None, None, None
|
return None, None, None, None, None, None, None
|
||||||
personJson = \
|
personJson = \
|
||||||
getPersonFromCache(baseDir, personUrl, personCache, True)
|
getPersonFromCache(baseDir, personUrl, personCache, True)
|
||||||
if not personJson:
|
if not personJson:
|
||||||
|
|
@ -225,7 +226,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
|
||||||
projectVersion, httpPrefix, domain)
|
projectVersion, httpPrefix, domain)
|
||||||
if not personJson:
|
if not personJson:
|
||||||
print('Unable to get actor')
|
print('Unable to get actor')
|
||||||
return None, None, None, None, None, None, None, None
|
return None, None, None, None, None, None, None
|
||||||
boxJson = None
|
boxJson = None
|
||||||
if not personJson.get(boxName):
|
if not personJson.get(boxName):
|
||||||
if personJson.get('endpoints'):
|
if personJson.get('endpoints'):
|
||||||
|
|
@ -235,7 +236,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
|
||||||
boxJson = personJson[boxName]
|
boxJson = personJson[boxName]
|
||||||
|
|
||||||
if not boxJson:
|
if not boxJson:
|
||||||
return None, None, None, None, None, None, None, None
|
return None, None, None, None, None, None, None
|
||||||
|
|
||||||
personId = None
|
personId = None
|
||||||
if personJson.get('id'):
|
if personJson.get('id'):
|
||||||
|
|
@ -254,9 +255,6 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
|
||||||
if personJson.get('endpoints'):
|
if personJson.get('endpoints'):
|
||||||
if personJson['endpoints'].get('sharedInbox'):
|
if personJson['endpoints'].get('sharedInbox'):
|
||||||
sharedInbox = personJson['endpoints']['sharedInbox']
|
sharedInbox = personJson['endpoints']['sharedInbox']
|
||||||
capabilityAcquisition = None
|
|
||||||
if personJson.get('capabilityAcquisitionEndpoint'):
|
|
||||||
capabilityAcquisition = personJson['capabilityAcquisitionEndpoint']
|
|
||||||
avatarUrl = None
|
avatarUrl = None
|
||||||
if personJson.get('icon'):
|
if personJson.get('icon'):
|
||||||
if personJson['icon'].get('url'):
|
if personJson['icon'].get('url'):
|
||||||
|
|
@ -268,7 +266,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {},
|
||||||
storePersonInCache(baseDir, personUrl, personJson, personCache, True)
|
storePersonInCache(baseDir, personUrl, personJson, personCache, True)
|
||||||
|
|
||||||
return boxJson, pubKeyId, pubKey, personId, sharedInbox, \
|
return boxJson, pubKeyId, pubKey, personId, sharedInbox, \
|
||||||
capabilityAcquisition, avatarUrl, displayName
|
avatarUrl, displayName
|
||||||
|
|
||||||
|
|
||||||
def getPosts(session, outboxUrl: str, maxPosts: int,
|
def getPosts(session, outboxUrl: str, maxPosts: int,
|
||||||
|
|
@ -890,24 +888,12 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
if not clientToServer:
|
if not clientToServer:
|
||||||
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
||||||
|
|
||||||
# if capabilities have been granted for this actor
|
|
||||||
# then get the corresponding id
|
|
||||||
capabilityIdList = []
|
|
||||||
ocapFilename = getOcapFilename(baseDir, nickname, domain,
|
|
||||||
toUrl, 'granted')
|
|
||||||
if ocapFilename:
|
|
||||||
if os.path.isfile(ocapFilename):
|
|
||||||
oc = loadJson(ocapFilename)
|
|
||||||
if oc:
|
|
||||||
if oc.get('id'):
|
|
||||||
capabilityIdList = [oc['id']]
|
|
||||||
idStr = \
|
idStr = \
|
||||||
httpPrefix + '://' + domain + '/users/' + nickname + \
|
httpPrefix + '://' + domain + '/users/' + nickname + \
|
||||||
'/statuses/' + statusNumber + '/replies'
|
'/statuses/' + statusNumber + '/replies'
|
||||||
newPost = {
|
newPost = {
|
||||||
'@context': postContext,
|
'@context': postContext,
|
||||||
'id': newPostId + '/activity',
|
'id': newPostId + '/activity',
|
||||||
'capability': capabilityIdList,
|
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': actorUrl,
|
'actor': actorUrl,
|
||||||
'published': published,
|
'published': published,
|
||||||
|
|
@ -1072,11 +1058,9 @@ def outboxMessageCreateWrap(httpPrefix: str,
|
||||||
cc = []
|
cc = []
|
||||||
if messageJson.get('cc'):
|
if messageJson.get('cc'):
|
||||||
cc = messageJson['cc']
|
cc = messageJson['cc']
|
||||||
capabilityUrl = []
|
|
||||||
newPost = {
|
newPost = {
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
'id': newPostId + '/activity',
|
'id': newPostId + '/activity',
|
||||||
'capability': capabilityUrl,
|
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': httpPrefix + '://' + domain + '/users/' + nickname,
|
'actor': httpPrefix + '://' + domain + '/users/' + nickname,
|
||||||
'published': published,
|
'published': published,
|
||||||
|
|
@ -1580,7 +1564,7 @@ def threadSendPost(session, postJsonStr: str, federationList: [],
|
||||||
postResult, unauthorized = \
|
postResult, unauthorized = \
|
||||||
postJsonString(session, postJsonStr, federationList,
|
postJsonString(session, postJsonStr, federationList,
|
||||||
inboxUrl, signatureHeaderJson,
|
inboxUrl, signatureHeaderJson,
|
||||||
"inbox:write", debug)
|
debug)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('ERROR: postJsonString failed ' + str(e))
|
print('ERROR: postJsonString failed ' + str(e))
|
||||||
if unauthorized:
|
if unauthorized:
|
||||||
|
|
@ -1665,26 +1649,18 @@ def sendPost(projectVersion: str,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
toPersonId, sharedInbox,
|
toPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
nickname, domain, postToBox)
|
nickname, domain, postToBox)
|
||||||
|
|
||||||
# If there are more than one followers on the target domain
|
|
||||||
# then send to the shared inbox indead of the individual inbox
|
|
||||||
if nickname == 'capabilities':
|
|
||||||
inboxUrl = capabilityAcquisition
|
|
||||||
if not capabilityAcquisition:
|
|
||||||
return 2
|
|
||||||
|
|
||||||
if not inboxUrl:
|
if not inboxUrl:
|
||||||
return 3
|
return 3
|
||||||
if not pubKey:
|
if not pubKey:
|
||||||
return 4
|
return 4
|
||||||
if not toPersonId:
|
if not toPersonId:
|
||||||
return 5
|
return 5
|
||||||
# sharedInbox and capabilities are optional
|
# sharedInbox is optional
|
||||||
|
|
||||||
postJsonObject = \
|
postJsonObject = \
|
||||||
createPostBase(baseDir, nickname, domain, port,
|
createPostBase(baseDir, nickname, domain, port,
|
||||||
|
|
@ -1790,7 +1766,6 @@ def sendPostViaServer(projectVersion: str,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
@ -1856,7 +1831,7 @@ def sendPostViaServer(projectVersion: str,
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postImage(session, attachImageFilename, [],
|
postImage(session, attachImageFilename, [],
|
||||||
inboxUrl, headers, "inbox:write")
|
inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: Failed to upload image')
|
print('DEBUG: Failed to upload image')
|
||||||
|
|
@ -1869,7 +1844,7 @@ def sendPostViaServer(projectVersion: str,
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJsonString(session, json.dumps(postJsonObject), [],
|
postJsonString(session, json.dumps(postJsonObject), [],
|
||||||
inboxUrl, headers, "inbox:write", debug)
|
inboxUrl, headers, debug)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST failed for c2s to '+inboxUrl)
|
print('DEBUG: POST failed for c2s to '+inboxUrl)
|
||||||
|
|
@ -2000,19 +1975,13 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
|
||||||
else:
|
else:
|
||||||
postToBox = 'outbox'
|
postToBox = 'outbox'
|
||||||
|
|
||||||
# get the actor inbox/outbox/capabilities for the To handle
|
# get the actor inbox/outbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl,
|
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest,
|
displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
nickname, domain, postToBox)
|
nickname, domain, postToBox)
|
||||||
|
|
||||||
if nickname == 'capabilities':
|
|
||||||
inboxUrl = capabilityAcquisition
|
|
||||||
if not capabilityAcquisition:
|
|
||||||
return 2
|
|
||||||
else:
|
|
||||||
print("inboxUrl: " + str(inboxUrl))
|
print("inboxUrl: " + str(inboxUrl))
|
||||||
print("toPersonId: " + str(toPersonId))
|
print("toPersonId: " + str(toPersonId))
|
||||||
print("sharedInboxUrl: " + str(sharedInboxUrl))
|
print("sharedInboxUrl: " + str(sharedInboxUrl))
|
||||||
|
|
@ -2036,7 +2005,7 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: missing personId')
|
print('DEBUG: missing personId')
|
||||||
return 5
|
return 5
|
||||||
# sharedInbox and capabilities are optional
|
# sharedInbox is optional
|
||||||
|
|
||||||
# get the senders private key
|
# get the senders private key
|
||||||
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug)
|
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug)
|
||||||
|
|
@ -2470,75 +2439,69 @@ def sendToFollowersThread(session, baseDir: str,
|
||||||
def createInbox(recentPostsCache: {},
|
def createInbox(recentPostsCache: {},
|
||||||
session, baseDir: str, nickname: str, domain: str, port: int,
|
session, baseDir: str, nickname: str, domain: str, port: int,
|
||||||
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
|
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
|
||||||
ocapAlways: bool, pageNumber=None) -> {}:
|
pageNumber=None) -> {}:
|
||||||
return createBoxIndexed(recentPostsCache,
|
return createBoxIndexed(recentPostsCache,
|
||||||
session, baseDir, 'inbox',
|
session, baseDir, 'inbox',
|
||||||
nickname, domain, port, httpPrefix,
|
nickname, domain, port, httpPrefix,
|
||||||
itemsPerPage, headerOnly, True,
|
itemsPerPage, headerOnly, True,
|
||||||
ocapAlways, pageNumber)
|
pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
|
def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed({}, session, baseDir, 'tlbookmarks',
|
return createBoxIndexed({}, session, baseDir, 'tlbookmarks',
|
||||||
nickname, domain,
|
nickname, domain,
|
||||||
port, httpPrefix, itemsPerPage, headerOnly,
|
port, httpPrefix, itemsPerPage, headerOnly,
|
||||||
True, ocapAlways, pageNumber)
|
True, pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createEventsTimeline(recentPostsCache: {},
|
def createEventsTimeline(recentPostsCache: {},
|
||||||
session, baseDir: str, nickname: str, domain: str,
|
session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents',
|
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents',
|
||||||
nickname, domain,
|
nickname, domain,
|
||||||
port, httpPrefix, itemsPerPage, headerOnly,
|
port, httpPrefix, itemsPerPage, headerOnly,
|
||||||
True, ocapAlways, pageNumber)
|
True, pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createDMTimeline(recentPostsCache: {},
|
def createDMTimeline(recentPostsCache: {},
|
||||||
session, baseDir: str, nickname: str, domain: str,
|
session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed(recentPostsCache,
|
return createBoxIndexed(recentPostsCache,
|
||||||
session, baseDir, 'dm', nickname,
|
session, baseDir, 'dm', nickname,
|
||||||
domain, port, httpPrefix, itemsPerPage,
|
domain, port, httpPrefix, itemsPerPage,
|
||||||
headerOnly, True, ocapAlways, pageNumber)
|
headerOnly, True, pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createRepliesTimeline(recentPostsCache: {},
|
def createRepliesTimeline(recentPostsCache: {},
|
||||||
session, baseDir: str, nickname: str, domain: str,
|
session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
|
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
|
||||||
nickname, domain, port, httpPrefix,
|
nickname, domain, port, httpPrefix,
|
||||||
itemsPerPage, headerOnly, True,
|
itemsPerPage, headerOnly, True,
|
||||||
ocapAlways, pageNumber)
|
pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
|
def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname,
|
return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname,
|
||||||
domain, port, httpPrefix,
|
domain, port, httpPrefix,
|
||||||
itemsPerPage, headerOnly, True,
|
itemsPerPage, headerOnly, True,
|
||||||
ocapAlways, pageNumber)
|
pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
|
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
|
||||||
port: int, httpPrefix: str, itemsPerPage: int,
|
port: int, httpPrefix: str, itemsPerPage: int,
|
||||||
headerOnly: bool, ocapAlways: bool,
|
headerOnly: bool, pageNumber=None) -> {}:
|
||||||
pageNumber=None) -> {}:
|
|
||||||
return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname,
|
return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname,
|
||||||
domain, port, httpPrefix,
|
domain, port, httpPrefix,
|
||||||
itemsPerPage, headerOnly, True,
|
itemsPerPage, headerOnly, True,
|
||||||
ocapAlways, pageNumber)
|
pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createOutbox(session, baseDir: str, nickname: str, domain: str,
|
def createOutbox(session, baseDir: str, nickname: str, domain: str,
|
||||||
|
|
@ -2548,12 +2511,12 @@ def createOutbox(session, baseDir: str, nickname: str, domain: str,
|
||||||
return createBoxIndexed({}, session, baseDir, 'outbox',
|
return createBoxIndexed({}, session, baseDir, 'outbox',
|
||||||
nickname, domain, port, httpPrefix,
|
nickname, domain, port, httpPrefix,
|
||||||
itemsPerPage, headerOnly, authorized,
|
itemsPerPage, headerOnly, authorized,
|
||||||
False, pageNumber)
|
pageNumber)
|
||||||
|
|
||||||
|
|
||||||
def createModeration(baseDir: str, nickname: str, domain: str, port: int,
|
def createModeration(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
|
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
|
||||||
ocapAlways: bool, pageNumber=None) -> {}:
|
pageNumber=None) -> {}:
|
||||||
boxDir = createPersonDir(nickname, domain, baseDir, 'inbox')
|
boxDir = createPersonDir(nickname, domain, baseDir, 'inbox')
|
||||||
boxname = 'moderation'
|
boxname = 'moderation'
|
||||||
|
|
||||||
|
|
@ -2751,8 +2714,7 @@ def createBoxIndex(boxDir: str, postsInBoxDict: {}) -> int:
|
||||||
|
|
||||||
def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
|
def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
|
||||||
postsInBoxDict: {}, postsCtr: int,
|
postsInBoxDict: {}, postsCtr: int,
|
||||||
nickname: str, domain: str,
|
nickname: str, domain: str) -> int:
|
||||||
ocapAlways: bool) -> int:
|
|
||||||
""" Creates an index for the given shared inbox
|
""" Creates an index for the given shared inbox
|
||||||
"""
|
"""
|
||||||
handle = nickname + '@' + domain
|
handle = nickname + '@' + domain
|
||||||
|
|
@ -2788,30 +2750,6 @@ def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
|
||||||
if actorNickname + '@' + actorDomain not in followingHandles:
|
if actorNickname + '@' + actorDomain not in followingHandles:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ocapAlways:
|
|
||||||
capsList = None
|
|
||||||
# Note: should this be in the Create or the object of a post?
|
|
||||||
if postJsonObject.get('capability'):
|
|
||||||
if isinstance(postJsonObject['capability'], list):
|
|
||||||
capsList = postJsonObject['capability']
|
|
||||||
|
|
||||||
# Have capabilities been granted for the sender?
|
|
||||||
ocapFilename = \
|
|
||||||
baseDir + '/accounts/' + handle + '/ocap/granted/' + \
|
|
||||||
postJsonObject['actor'].replace('/', '#') + '.json'
|
|
||||||
if not os.path.isfile(ocapFilename):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# read the capabilities id
|
|
||||||
ocapJson = loadJson(ocapFilename, 0)
|
|
||||||
if not ocapJson:
|
|
||||||
print('WARN: json load exception createSharedInboxIndex')
|
|
||||||
else:
|
|
||||||
if ocapJson.get('id'):
|
|
||||||
if ocapJson['id'] in capsList:
|
|
||||||
postsInBoxDict[statusNumber] = sharedInboxFilename
|
|
||||||
postsCtr += 1
|
|
||||||
else:
|
|
||||||
postsInBoxDict[statusNumber] = sharedInboxFilename
|
postsInBoxDict[statusNumber] = sharedInboxFilename
|
||||||
postsCtr += 1
|
postsCtr += 1
|
||||||
return postsCtr
|
return postsCtr
|
||||||
|
|
@ -2866,7 +2804,7 @@ def createBoxIndexed(recentPostsCache: {},
|
||||||
session, baseDir: str, boxname: str,
|
session, baseDir: str, boxname: str,
|
||||||
nickname: str, domain: str, port: int, httpPrefix: str,
|
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||||
itemsPerPage: int, headerOnly: bool, authorized: bool,
|
itemsPerPage: int, headerOnly: bool, authorized: bool,
|
||||||
ocapAlways: bool, pageNumber=None) -> {}:
|
pageNumber=None) -> {}:
|
||||||
"""Constructs the box feed for a person with the given nickname
|
"""Constructs the box feed for a person with the given nickname
|
||||||
"""
|
"""
|
||||||
if not authorized or not pageNumber:
|
if not authorized or not pageNumber:
|
||||||
|
|
@ -3005,10 +2943,6 @@ def createBoxIndexed(recentPostsCache: {},
|
||||||
except BaseException:
|
except BaseException:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# remove any capability so that it's not displayed
|
|
||||||
if p.get('capability'):
|
|
||||||
del p['capability']
|
|
||||||
|
|
||||||
# Don't show likes, replies or shares (announces) to
|
# Don't show likes, replies or shares (announces) to
|
||||||
# unauthorized viewers
|
# unauthorized viewers
|
||||||
if not authorized:
|
if not authorized:
|
||||||
|
|
@ -3226,7 +3160,6 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
|
||||||
|
|
||||||
(personUrl, pubKeyId, pubKey,
|
(personUrl, pubKeyId, pubKey,
|
||||||
personId, shaedInbox,
|
personId, shaedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
@ -3240,12 +3173,13 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
|
||||||
projectVersion, httpPrefix, domain)
|
projectVersion, httpPrefix, domain)
|
||||||
|
|
||||||
|
|
||||||
def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
|
def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str,
|
||||||
proxyType: str, port: int, httpPrefix: str,
|
proxyType: str, port: int, httpPrefix: str,
|
||||||
debug: bool, projectVersion: str,
|
debug: bool, projectVersion: str,
|
||||||
domainList=[]) -> []:
|
domainList=[]) -> []:
|
||||||
""" Returns a list of domains referenced within public posts
|
""" Returns a list of domains referenced within public posts
|
||||||
"""
|
"""
|
||||||
|
if not session:
|
||||||
session = createSession(proxyType)
|
session = createSession(proxyType)
|
||||||
if not session:
|
if not session:
|
||||||
return domainList
|
return domainList
|
||||||
|
|
@ -3270,8 +3204,7 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
|
||||||
return domainList
|
return domainList
|
||||||
|
|
||||||
(personUrl, pubKeyId, pubKey,
|
(personUrl, pubKeyId, pubKey,
|
||||||
personId, shaedInbox,
|
personId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
@ -3288,43 +3221,125 @@ def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
|
||||||
return postDomains
|
return postDomains
|
||||||
|
|
||||||
|
|
||||||
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str,
|
def getPublicPostDomainsBlocked(session, baseDir: str,
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str,
|
||||||
followerUrl, updateCaps: [],
|
proxyType: str, port: int, httpPrefix: str,
|
||||||
sendThreads: [], postLog: [],
|
debug: bool, projectVersion: str,
|
||||||
cachedWebfingers: {}, personCache: {},
|
domainList=[]) -> []:
|
||||||
federationList: [], debug: bool,
|
""" Returns a list of domains referenced within public posts which
|
||||||
projectVersion: str) -> int:
|
are globally blocked on this instance
|
||||||
"""When the capabilities for a follower are changed this
|
|
||||||
sends out an update. followerUrl is the actor of the follower.
|
|
||||||
"""
|
"""
|
||||||
updateJson = \
|
postDomains = \
|
||||||
capabilitiesUpdate(baseDir, httpPrefix,
|
getPublicPostDomains(session, baseDir, nickname, domain,
|
||||||
nickname, domain, port,
|
proxyType, port, httpPrefix,
|
||||||
followerUrl, updateCaps)
|
debug, projectVersion,
|
||||||
|
domainList)
|
||||||
|
if not postDomains:
|
||||||
|
return []
|
||||||
|
|
||||||
if not updateJson:
|
blockingFilename = baseDir + '/accounts/blocking.txt'
|
||||||
return 1
|
if not os.path.isfile(blockingFilename):
|
||||||
|
return []
|
||||||
|
|
||||||
if debug:
|
# read the blocked domains as a single string
|
||||||
pprint(updateJson)
|
blockedStr = ''
|
||||||
print('DEBUG: sending capabilities update from ' +
|
with open(blockingFilename, 'r') as fp:
|
||||||
nickname + '@' + domain + ' port ' + str(port) +
|
blockedStr = fp.read()
|
||||||
' to ' + followerUrl)
|
|
||||||
|
|
||||||
clientToServer = False
|
blockedDomains = []
|
||||||
followerNickname = getNicknameFromActor(followerUrl)
|
for domainName in postDomains:
|
||||||
if not followerNickname:
|
if '@' not in domainName:
|
||||||
print('WARN: unable to find nickname in ' + followerUrl)
|
continue
|
||||||
return 1
|
# get the domain after the @
|
||||||
followerDomain, followerPort = getDomainFromActor(followerUrl)
|
domainName = domainName.split('@')[1].strip()
|
||||||
return sendSignedJson(updateJson, session, baseDir,
|
if isEvil(domainName):
|
||||||
nickname, domain, port,
|
blockedDomains.append(domainName)
|
||||||
followerNickname, followerDomain, followerPort, '',
|
continue
|
||||||
httpPrefix, True, clientToServer,
|
if domainName in blockedStr:
|
||||||
federationList,
|
blockedDomains.append(domainName)
|
||||||
sendThreads, postLog, cachedWebfingers,
|
|
||||||
personCache, debug, projectVersion)
|
return blockedDomains
|
||||||
|
|
||||||
|
|
||||||
|
def getNonMutualsOfPerson(baseDir: str,
|
||||||
|
nickname: str, domain: str) -> []:
|
||||||
|
"""Returns the followers who are not mutuals of a person
|
||||||
|
i.e. accounts which follow you but you don't follow them
|
||||||
|
"""
|
||||||
|
followers = \
|
||||||
|
getFollowersList(baseDir, nickname, domain, 'followers.txt')
|
||||||
|
following = \
|
||||||
|
getFollowersList(baseDir, nickname, domain, 'following.txt')
|
||||||
|
nonMutuals = []
|
||||||
|
for handle in followers:
|
||||||
|
if handle not in following:
|
||||||
|
nonMutuals.append(handle)
|
||||||
|
return nonMutuals
|
||||||
|
|
||||||
|
|
||||||
|
def checkDomains(session, baseDir: str,
|
||||||
|
nickname: str, domain: str,
|
||||||
|
proxyType: str, port: int, httpPrefix: str,
|
||||||
|
debug: bool, projectVersion: str,
|
||||||
|
maxBlockedDomains: int, singleCheck: bool):
|
||||||
|
"""Checks follower accounts for references to globally blocked domains
|
||||||
|
"""
|
||||||
|
nonMutuals = getNonMutualsOfPerson(baseDir, nickname, domain)
|
||||||
|
if not nonMutuals:
|
||||||
|
print('No non-mutual followers were found')
|
||||||
|
return
|
||||||
|
followerWarningFilename = baseDir + '/accounts/followerWarnings.txt'
|
||||||
|
updateFollowerWarnings = False
|
||||||
|
followerWarningStr = ''
|
||||||
|
if os.path.isfile(followerWarningFilename):
|
||||||
|
with open(followerWarningFilename, 'r') as fp:
|
||||||
|
followerWarningStr = fp.read()
|
||||||
|
|
||||||
|
if singleCheck:
|
||||||
|
# checks a single random non-mutual
|
||||||
|
index = random.randrange(0, len(nonMutuals))
|
||||||
|
handle = nonMutuals[index]
|
||||||
|
if '@' in handle:
|
||||||
|
nonMutualNickname = handle.split('@')[0]
|
||||||
|
nonMutualDomain = handle.split('@')[1].strip()
|
||||||
|
blockedDomains = \
|
||||||
|
getPublicPostDomainsBlocked(session, baseDir,
|
||||||
|
nonMutualNickname,
|
||||||
|
nonMutualDomain,
|
||||||
|
proxyType, port, httpPrefix,
|
||||||
|
debug, projectVersion, [])
|
||||||
|
if blockedDomains:
|
||||||
|
if len(blockedDomains) > maxBlockedDomains:
|
||||||
|
followerWarningStr += handle + '\n'
|
||||||
|
updateFollowerWarnings = True
|
||||||
|
else:
|
||||||
|
# checks all non-mutuals
|
||||||
|
for handle in nonMutuals:
|
||||||
|
if '@' not in handle:
|
||||||
|
continue
|
||||||
|
if handle in followerWarningStr:
|
||||||
|
continue
|
||||||
|
nonMutualNickname = handle.split('@')[0]
|
||||||
|
nonMutualDomain = handle.split('@')[1].strip()
|
||||||
|
blockedDomains = \
|
||||||
|
getPublicPostDomainsBlocked(session, baseDir,
|
||||||
|
nonMutualNickname,
|
||||||
|
nonMutualDomain,
|
||||||
|
proxyType, port, httpPrefix,
|
||||||
|
debug, projectVersion, [])
|
||||||
|
if blockedDomains:
|
||||||
|
print(handle)
|
||||||
|
for d in blockedDomains:
|
||||||
|
print(' ' + d)
|
||||||
|
if len(blockedDomains) > maxBlockedDomains:
|
||||||
|
followerWarningStr += handle + '\n'
|
||||||
|
updateFollowerWarnings = True
|
||||||
|
|
||||||
|
if updateFollowerWarnings and followerWarningStr:
|
||||||
|
with open(followerWarningFilename, 'w+') as fp:
|
||||||
|
fp.write(followerWarningStr)
|
||||||
|
if not singleCheck:
|
||||||
|
print(followerWarningStr)
|
||||||
|
|
||||||
|
|
||||||
def populateRepliesJson(baseDir: str, nickname: str, domain: str,
|
def populateRepliesJson(baseDir: str, nickname: str, domain: str,
|
||||||
|
|
@ -3692,8 +3707,7 @@ def sendBlockViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest,
|
displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
|
|
@ -3715,8 +3729,7 @@ def sendBlockViaServer(baseDir: str, session,
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newBlockJson, [], inboxUrl,
|
postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
|
||||||
headers, "inbox:write")
|
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: Unable to post block')
|
print('WARN: Unable to post block')
|
||||||
|
|
||||||
|
|
@ -3781,8 +3794,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
|
||||||
|
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox, avatarUrl,
|
||||||
capabilityAcquisition, avatarUrl,
|
|
||||||
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
|
||||||
projectVersion, httpPrefix, fromNickname,
|
projectVersion, httpPrefix, fromNickname,
|
||||||
fromDomain, postToBox)
|
fromDomain, postToBox)
|
||||||
|
|
@ -3803,8 +3815,7 @@ def sendUndoBlockViaServer(baseDir: str, session,
|
||||||
'Content-type': 'application/json',
|
'Content-type': 'application/json',
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = postJson(session, newBlockJson, [], inboxUrl,
|
postResult = postJson(session, newBlockJson, [], inboxUrl, headers)
|
||||||
headers, "inbox:write")
|
|
||||||
if not postResult:
|
if not postResult:
|
||||||
print('WARN: Unable to post block')
|
print('WARN: Unable to post block')
|
||||||
|
|
||||||
|
|
|
||||||
3
roles.py
3
roles.py
|
|
@ -291,7 +291,6 @@ def sendRoleViaServer(baseDir: str, session,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session,
|
avatarUrl, displayName) = getPersonBox(baseDir, session,
|
||||||
wfRequest, personCache,
|
wfRequest, personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
@ -315,7 +314,7 @@ def sendRoleViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, newRoleJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, newRoleJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
||||||
|
|
|
||||||
25
session.py
25
session.py
|
|
@ -49,7 +49,7 @@ def createSession(proxyType: str):
|
||||||
session.proxies = {}
|
session.proxies = {}
|
||||||
session.proxies['http'] = 'socks5h://localhost:7777'
|
session.proxies['http'] = 'socks5h://localhost:7777'
|
||||||
session.proxies['https'] = 'socks5h://localhost:7777'
|
session.proxies['https'] = 'socks5h://localhost:7777'
|
||||||
print('New session created with proxy ' + str(proxyType))
|
# print('New session created with proxy ' + str(proxyType))
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,14 +93,11 @@ def getJson(session, url: str, headers: {}, params: {},
|
||||||
|
|
||||||
|
|
||||||
def postJson(session, postJsonObject: {}, federationList: [],
|
def postJson(session, postJsonObject: {}, federationList: [],
|
||||||
inboxUrl: str, headers: {}, capability: str) -> str:
|
inboxUrl: str, headers: {}) -> str:
|
||||||
"""Post a json message to the inbox of another person
|
"""Post a json message to the inbox of another person
|
||||||
Supplying a capability, such as "inbox:write"
|
|
||||||
"""
|
"""
|
||||||
# always allow capability requests
|
|
||||||
if not capability.startswith('cap'):
|
|
||||||
# check that we are posting to a permitted domain
|
# check that we are posting to a permitted domain
|
||||||
if not urlPermitted(inboxUrl, federationList, capability):
|
if not urlPermitted(inboxUrl, federationList):
|
||||||
print('postJson: ' + inboxUrl + ' not permitted')
|
print('postJson: ' + inboxUrl + ' not permitted')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -132,22 +129,13 @@ def postJsonString(session, postJsonStr: str,
|
||||||
federationList: [],
|
federationList: [],
|
||||||
inboxUrl: str,
|
inboxUrl: str,
|
||||||
headers: {},
|
headers: {},
|
||||||
capability: str,
|
|
||||||
debug: bool) -> (bool, bool):
|
debug: bool) -> (bool, bool):
|
||||||
"""Post a json message string to the inbox of another person
|
"""Post a json message string to the inbox of another person
|
||||||
Supplying a capability, such as "inbox:write"
|
|
||||||
The second boolean returned is true if the send is unauthorized
|
The second boolean returned is true if the send is unauthorized
|
||||||
NOTE: Here we post a string rather than the original json so that
|
NOTE: Here we post a string rather than the original json so that
|
||||||
conversions between string and json format don't invalidate
|
conversions between string and json format don't invalidate
|
||||||
the message body digest of http signatures
|
the message body digest of http signatures
|
||||||
"""
|
"""
|
||||||
# always allow capability requests
|
|
||||||
if not capability.startswith('cap'):
|
|
||||||
# check that we are posting to a permitted domain
|
|
||||||
if not urlPermitted(inboxUrl, federationList, capability):
|
|
||||||
print('postJson: ' + inboxUrl + ' not permitted by capabilities')
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
postResult = \
|
postResult = \
|
||||||
session.post(url=inboxUrl, data=postJsonStr, headers=headers)
|
session.post(url=inboxUrl, data=postJsonStr, headers=headers)
|
||||||
|
|
@ -181,14 +169,11 @@ def postJsonString(session, postJsonStr: str,
|
||||||
|
|
||||||
|
|
||||||
def postImage(session, attachImageFilename: str, federationList: [],
|
def postImage(session, attachImageFilename: str, federationList: [],
|
||||||
inboxUrl: str, headers: {}, capability: str) -> str:
|
inboxUrl: str, headers: {}) -> str:
|
||||||
"""Post an image to the inbox of another person or outbox via c2s
|
"""Post an image to the inbox of another person or outbox via c2s
|
||||||
Supplying a capability, such as "inbox:write"
|
|
||||||
"""
|
"""
|
||||||
# always allow capability requests
|
|
||||||
if not capability.startswith('cap'):
|
|
||||||
# check that we are posting to a permitted domain
|
# check that we are posting to a permitted domain
|
||||||
if not urlPermitted(inboxUrl, federationList, capability):
|
if not urlPermitted(inboxUrl, federationList):
|
||||||
print('postJson: ' + inboxUrl + ' not permitted')
|
print('postJson: ' + inboxUrl + ' not permitted')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,6 @@ def sendShareViaServer(baseDir, session,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, fromNickname,
|
httpPrefix, fromNickname,
|
||||||
|
|
@ -405,7 +404,7 @@ def sendShareViaServer(baseDir, session,
|
||||||
postResult = \
|
postResult = \
|
||||||
postImage(session, imageFilename, [],
|
postImage(session, imageFilename, [],
|
||||||
inboxUrl.replace('/' + postToBox, '/shares'),
|
inboxUrl.replace('/' + postToBox, '/shares'),
|
||||||
headers, "inbox:write")
|
headers)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'host': fromDomain,
|
'host': fromDomain,
|
||||||
|
|
@ -413,7 +412,7 @@ def sendShareViaServer(baseDir, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, newShareJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, newShareJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
@ -483,7 +482,6 @@ def sendUndoShareViaServer(baseDir: str, session,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, fromNickname,
|
httpPrefix, fromNickname,
|
||||||
|
|
@ -506,7 +504,7 @@ def sendUndoShareViaServer(baseDir: str, session,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, undoShareJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, undoShareJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,6 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str,
|
||||||
# get the actor inbox for the To handle
|
# get the actor inbox for the To handle
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, nickname, domain,
|
httpPrefix, nickname, domain,
|
||||||
|
|
@ -175,7 +174,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str,
|
||||||
'Authorization': authHeader
|
'Authorization': authHeader
|
||||||
}
|
}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session, newSkillJson, [], inboxUrl, headers, "inbox:write")
|
postJson(session, newSkillJson, [], inboxUrl, headers)
|
||||||
if not postResult:
|
if not postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ def instancesGraph(baseDir: str, handles: str,
|
||||||
|
|
||||||
(personUrl, pubKeyId, pubKey,
|
(personUrl, pubKeyId, pubKey,
|
||||||
personId, shaedInbox,
|
personId, shaedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
|
||||||
44
tests.py
44
tests.py
|
|
@ -42,10 +42,10 @@ from utils import copytree
|
||||||
from utils import loadJson
|
from utils import loadJson
|
||||||
from utils import saveJson
|
from utils import saveJson
|
||||||
from utils import getStatusNumber
|
from utils import getStatusNumber
|
||||||
|
from utils import getFollowersOfPerson
|
||||||
from follow import followerOfPerson
|
from follow import followerOfPerson
|
||||||
from follow import unfollowPerson
|
from follow import unfollowPerson
|
||||||
from follow import unfollowerOfPerson
|
from follow import unfollowerOfPerson
|
||||||
from follow import getFollowersOfPerson
|
|
||||||
from follow import sendFollowRequest
|
from follow import sendFollowRequest
|
||||||
from person import createPerson
|
from person import createPerson
|
||||||
from person import setDisplayNickname
|
from person import setDisplayNickname
|
||||||
|
|
@ -239,7 +239,7 @@ def testThreads():
|
||||||
def createServerAlice(path: str, domain: str, port: int,
|
def createServerAlice(path: str, domain: str, port: int,
|
||||||
bobAddress: str, federationList: [],
|
bobAddress: str, federationList: [],
|
||||||
hasFollows: bool, hasPosts: bool,
|
hasFollows: bool, hasPosts: bool,
|
||||||
ocapAlways: bool, sendThreads: []):
|
sendThreads: []):
|
||||||
print('Creating test server: Alice on port ' + str(port))
|
print('Creating test server: Alice on port ' + str(port))
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
@ -249,11 +249,6 @@ def createServerAlice(path: str, domain: str, port: int,
|
||||||
httpPrefix = 'http'
|
httpPrefix = 'http'
|
||||||
proxyType = None
|
proxyType = None
|
||||||
password = 'alicepass'
|
password = 'alicepass'
|
||||||
noreply = False
|
|
||||||
nolike = False
|
|
||||||
nopics = False
|
|
||||||
noannounce = False
|
|
||||||
cw = False
|
|
||||||
useBlurhash = True
|
useBlurhash = True
|
||||||
maxReplies = 64
|
maxReplies = 64
|
||||||
domainMaxPostsPerDay = 1000
|
domainMaxPostsPerDay = 1000
|
||||||
|
|
@ -296,7 +291,6 @@ def createServerAlice(path: str, domain: str, port: int,
|
||||||
"instanceId", False, path, domain,
|
"instanceId", False, path, domain,
|
||||||
onionDomain, i2pDomain, None, port, port,
|
onionDomain, i2pDomain, None, port, port,
|
||||||
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
||||||
noreply, nolike, nopics, noannounce, cw, ocapAlways,
|
|
||||||
proxyType, maxReplies,
|
proxyType, maxReplies,
|
||||||
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
||||||
allowDeletion, True, True, False, sendThreads, False,
|
allowDeletion, True, True, False, sendThreads, False,
|
||||||
|
|
@ -306,7 +300,7 @@ def createServerAlice(path: str, domain: str, port: int,
|
||||||
def createServerBob(path: str, domain: str, port: int,
|
def createServerBob(path: str, domain: str, port: int,
|
||||||
aliceAddress: str, federationList: [],
|
aliceAddress: str, federationList: [],
|
||||||
hasFollows: bool, hasPosts: bool,
|
hasFollows: bool, hasPosts: bool,
|
||||||
ocapAlways: bool, sendThreads: []):
|
sendThreads: []):
|
||||||
print('Creating test server: Bob on port ' + str(port))
|
print('Creating test server: Bob on port ' + str(port))
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
@ -317,11 +311,6 @@ def createServerBob(path: str, domain: str, port: int,
|
||||||
proxyType = None
|
proxyType = None
|
||||||
clientToServer = False
|
clientToServer = False
|
||||||
password = 'bobpass'
|
password = 'bobpass'
|
||||||
noreply = False
|
|
||||||
nolike = False
|
|
||||||
nopics = False
|
|
||||||
noannounce = False
|
|
||||||
cw = False
|
|
||||||
useBlurhash = False
|
useBlurhash = False
|
||||||
maxReplies = 64
|
maxReplies = 64
|
||||||
domainMaxPostsPerDay = 1000
|
domainMaxPostsPerDay = 1000
|
||||||
|
|
@ -364,7 +353,6 @@ def createServerBob(path: str, domain: str, port: int,
|
||||||
"instanceId", False, path, domain,
|
"instanceId", False, path, domain,
|
||||||
onionDomain, i2pDomain, None, port, port,
|
onionDomain, i2pDomain, None, port, port,
|
||||||
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
||||||
noreply, nolike, nopics, noannounce, cw, ocapAlways,
|
|
||||||
proxyType, maxReplies,
|
proxyType, maxReplies,
|
||||||
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
domainMaxPostsPerDay, accountMaxPostsPerDay,
|
||||||
allowDeletion, True, True, False, sendThreads, False,
|
allowDeletion, True, True, False, sendThreads, False,
|
||||||
|
|
@ -373,7 +361,7 @@ def createServerBob(path: str, domain: str, port: int,
|
||||||
|
|
||||||
def createServerEve(path: str, domain: str, port: int, federationList: [],
|
def createServerEve(path: str, domain: str, port: int, federationList: [],
|
||||||
hasFollows: bool, hasPosts: bool,
|
hasFollows: bool, hasPosts: bool,
|
||||||
ocapAlways: bool, sendThreads: []):
|
sendThreads: []):
|
||||||
print('Creating test server: Eve on port ' + str(port))
|
print('Creating test server: Eve on port ' + str(port))
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
@ -383,11 +371,6 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
|
||||||
httpPrefix = 'http'
|
httpPrefix = 'http'
|
||||||
proxyType = None
|
proxyType = None
|
||||||
password = 'evepass'
|
password = 'evepass'
|
||||||
noreply = False
|
|
||||||
nolike = False
|
|
||||||
nopics = False
|
|
||||||
noannounce = False
|
|
||||||
cw = False
|
|
||||||
maxReplies = 64
|
maxReplies = 64
|
||||||
allowDeletion = True
|
allowDeletion = True
|
||||||
privateKeyPem, publicKeyPem, person, wfEndpoint = \
|
privateKeyPem, publicKeyPem, person, wfEndpoint = \
|
||||||
|
|
@ -406,7 +389,6 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
|
||||||
"instanceId", False, path, domain,
|
"instanceId", False, path, domain,
|
||||||
onionDomain, i2pDomain, None, port, port,
|
onionDomain, i2pDomain, None, port, port,
|
||||||
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
httpPrefix, federationList, maxMentions, maxEmoji, False,
|
||||||
noreply, nolike, nopics, noannounce, cw, ocapAlways,
|
|
||||||
proxyType, maxReplies, allowDeletion, True, True, False,
|
proxyType, maxReplies, allowDeletion, True, True, False,
|
||||||
sendThreads, False, False)
|
sendThreads, False, False)
|
||||||
|
|
||||||
|
|
@ -427,8 +409,6 @@ def testPostMessageBetweenServers():
|
||||||
shutil.rmtree(baseDir + '/.tests')
|
shutil.rmtree(baseDir + '/.tests')
|
||||||
os.mkdir(baseDir + '/.tests')
|
os.mkdir(baseDir + '/.tests')
|
||||||
|
|
||||||
ocapAlways = False
|
|
||||||
|
|
||||||
# create the servers
|
# create the servers
|
||||||
aliceDir = baseDir + '/.tests/alice'
|
aliceDir = baseDir + '/.tests/alice'
|
||||||
aliceDomain = '127.0.0.50'
|
aliceDomain = '127.0.0.50'
|
||||||
|
|
@ -454,7 +434,7 @@ def testPostMessageBetweenServers():
|
||||||
threadWithTrace(target=createServerAlice,
|
threadWithTrace(target=createServerAlice,
|
||||||
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, aliceSendThreads),
|
aliceSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
global thrBob
|
global thrBob
|
||||||
|
|
@ -468,7 +448,7 @@ def testPostMessageBetweenServers():
|
||||||
threadWithTrace(target=createServerBob,
|
threadWithTrace(target=createServerBob,
|
||||||
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, bobSendThreads),
|
bobSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
thrAlice.start()
|
thrAlice.start()
|
||||||
|
|
@ -687,8 +667,6 @@ def testFollowBetweenServers():
|
||||||
shutil.rmtree(baseDir + '/.tests')
|
shutil.rmtree(baseDir + '/.tests')
|
||||||
os.mkdir(baseDir + '/.tests')
|
os.mkdir(baseDir + '/.tests')
|
||||||
|
|
||||||
ocapAlways = False
|
|
||||||
|
|
||||||
# create the servers
|
# create the servers
|
||||||
aliceDir = baseDir + '/.tests/alice'
|
aliceDir = baseDir + '/.tests/alice'
|
||||||
aliceDomain = '127.0.0.47'
|
aliceDomain = '127.0.0.47'
|
||||||
|
|
@ -713,7 +691,7 @@ def testFollowBetweenServers():
|
||||||
threadWithTrace(target=createServerAlice,
|
threadWithTrace(target=createServerAlice,
|
||||||
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, aliceSendThreads),
|
aliceSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
global thrBob
|
global thrBob
|
||||||
|
|
@ -727,7 +705,7 @@ def testFollowBetweenServers():
|
||||||
threadWithTrace(target=createServerBob,
|
threadWithTrace(target=createServerBob,
|
||||||
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, bobSendThreads),
|
bobSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
thrAlice.start()
|
thrAlice.start()
|
||||||
|
|
@ -1246,8 +1224,6 @@ def testClientToServer():
|
||||||
shutil.rmtree(baseDir + '/.tests')
|
shutil.rmtree(baseDir + '/.tests')
|
||||||
os.mkdir(baseDir + '/.tests')
|
os.mkdir(baseDir + '/.tests')
|
||||||
|
|
||||||
ocapAlways = False
|
|
||||||
|
|
||||||
# create the servers
|
# create the servers
|
||||||
aliceDir = baseDir + '/.tests/alice'
|
aliceDir = baseDir + '/.tests/alice'
|
||||||
aliceDomain = '127.0.0.42'
|
aliceDomain = '127.0.0.42'
|
||||||
|
|
@ -1272,7 +1248,7 @@ def testClientToServer():
|
||||||
threadWithTrace(target=createServerAlice,
|
threadWithTrace(target=createServerAlice,
|
||||||
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
args=(aliceDir, aliceDomain, alicePort, bobAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, aliceSendThreads),
|
aliceSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
global thrBob
|
global thrBob
|
||||||
|
|
@ -1286,7 +1262,7 @@ def testClientToServer():
|
||||||
threadWithTrace(target=createServerBob,
|
threadWithTrace(target=createServerBob,
|
||||||
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
args=(bobDir, bobDomain, bobPort, aliceAddress,
|
||||||
federationList, False, False,
|
federationList, False, False,
|
||||||
ocapAlways, bobSendThreads),
|
bobSendThreads),
|
||||||
daemon=True)
|
daemon=True)
|
||||||
|
|
||||||
thrAlice.start()
|
thrAlice.start()
|
||||||
|
|
|
||||||
|
|
@ -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 محظور"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "No mostreu el botó M'agrada",
|
"Don't show the Like button": "No mostreu el botó M'agrada",
|
||||||
"Autogenerated Hashtags": "Hashtags autogenerats",
|
"Autogenerated Hashtags": "Hashtags autogenerats",
|
||||||
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
|
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag bloquejat"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Peidiwch â dangos y botwm Hoffi",
|
"Don't show the Like button": "Peidiwch â dangos y botwm Hoffi",
|
||||||
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
|
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
|
||||||
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
|
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag wedi'i Blocio"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an",
|
"Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an",
|
||||||
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
|
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
|
||||||
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
|
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag blockiert"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Don't show the Like button",
|
"Don't show the Like button": "Don't show the Like button",
|
||||||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag Blocked"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "No mostrar el botón Me gusta",
|
"Don't show the Like button": "No mostrar el botón Me gusta",
|
||||||
"Autogenerated Hashtags": "Hashtags autogenerados",
|
"Autogenerated Hashtags": "Hashtags autogenerados",
|
||||||
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
|
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag bloqueada"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Ne pas afficher le bouton J'aime",
|
"Don't show the Like button": "Ne pas afficher le bouton J'aime",
|
||||||
"Autogenerated Hashtags": "Hashtags générés automatiquement",
|
"Autogenerated Hashtags": "Hashtags générés automatiquement",
|
||||||
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
|
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag bloqué"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Ná taispeáin an cnaipe Cosúil",
|
"Don't show the Like button": "Ná taispeáin an cnaipe Cosúil",
|
||||||
"Autogenerated Hashtags": "Hashtags uathghinte",
|
"Autogenerated Hashtags": "Hashtags uathghinte",
|
||||||
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
|
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag Blocáilte"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "हैशटैग अवरुद्ध"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Non mostrare il pulsante Mi piace",
|
"Don't show the Like button": "Non mostrare il pulsante Mi piace",
|
||||||
"Autogenerated Hashtags": "Hashtag generati automaticamente",
|
"Autogenerated Hashtags": "Hashtag generati automaticamente",
|
||||||
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
|
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag bloccato"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "ハッシュタグがブロックされました"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,5 +282,6 @@
|
||||||
"Don't show the Like button": "Don't show the Like button",
|
"Don't show the Like button": "Don't show the Like button",
|
||||||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag Blocked"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,6 @@
|
||||||
"Don't show the Like button": "Não mostrar o botão Curtir",
|
"Don't show the Like button": "Não mostrar o botão Curtir",
|
||||||
"Autogenerated Hashtags": "Hashtags autogeradas",
|
"Autogenerated Hashtags": "Hashtags autogeradas",
|
||||||
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
|
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
|
||||||
"Indymedia": "Indymedia"
|
"Indymedia": "Indymedia",
|
||||||
|
"Hashtag Blocked": "Hashtag bloqueada"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "Хештег заблокирован"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "标签被阻止"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
utils.py
53
utils.py
|
|
@ -19,6 +19,55 @@ from calendar import monthrange
|
||||||
from followingCalendar import addPersonToCalendar
|
from followingCalendar import addPersonToCalendar
|
||||||
|
|
||||||
|
|
||||||
|
def getFollowersList(baseDir: str,
|
||||||
|
nickname: str, domain: str,
|
||||||
|
followFile='following.txt') -> []:
|
||||||
|
"""Returns a list of followers for the given account
|
||||||
|
"""
|
||||||
|
filename = \
|
||||||
|
baseDir + '/accounts/' + nickname + '@' + domain + '/' + followFile
|
||||||
|
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
return []
|
||||||
|
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for i in range(len(lines)):
|
||||||
|
lines[i] = lines[i].strip()
|
||||||
|
return lines
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def getFollowersOfPerson(baseDir: str,
|
||||||
|
nickname: str, domain: str,
|
||||||
|
followFile='following.txt') -> []:
|
||||||
|
"""Returns a list containing the followers of the given person
|
||||||
|
Used by the shared inbox to know who to send incoming mail to
|
||||||
|
"""
|
||||||
|
followers = []
|
||||||
|
if ':' in domain:
|
||||||
|
domain = domain.split(':')[0]
|
||||||
|
handle = nickname + '@' + domain
|
||||||
|
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
||||||
|
return followers
|
||||||
|
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
|
||||||
|
for account in dirs:
|
||||||
|
filename = os.path.join(subdir, account) + '/' + followFile
|
||||||
|
if account == handle or account.startswith('inbox@'):
|
||||||
|
continue
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
continue
|
||||||
|
with open(filename, 'r') as followingfile:
|
||||||
|
for followingHandle in followingfile:
|
||||||
|
followingHandle2 = followingHandle.replace('\n', '')
|
||||||
|
followingHandle2 = followingHandle2.replace('\r', '')
|
||||||
|
if followingHandle2 == handle:
|
||||||
|
if account not in followers:
|
||||||
|
followers.append(account)
|
||||||
|
break
|
||||||
|
return followers
|
||||||
|
|
||||||
|
|
||||||
def removeIdEnding(idStr: str) -> str:
|
def removeIdEnding(idStr: str) -> str:
|
||||||
"""Removes endings such as /activity and /undo
|
"""Removes endings such as /activity and /undo
|
||||||
"""
|
"""
|
||||||
|
|
@ -193,7 +242,7 @@ def domainPermitted(domain: str, federationList: []):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def urlPermitted(url: str, federationList: [], capability: str):
|
def urlPermitted(url: str, federationList: []):
|
||||||
if isEvil(url):
|
if isEvil(url):
|
||||||
return False
|
return False
|
||||||
if not federationList:
|
if not federationList:
|
||||||
|
|
@ -620,7 +669,7 @@ def validNickname(domain: str, nickname: str) -> bool:
|
||||||
return False
|
return False
|
||||||
reservedNames = ('inbox', 'dm', 'outbox', 'following',
|
reservedNames = ('inbox', 'dm', 'outbox', 'following',
|
||||||
'public', 'followers',
|
'public', 'followers',
|
||||||
'channel', 'capabilities', 'calendar',
|
'channel', 'calendar',
|
||||||
'tlreplies', 'tlmedia', 'tlblogs',
|
'tlreplies', 'tlmedia', 'tlblogs',
|
||||||
'tlevents',
|
'tlevents',
|
||||||
'moderation', 'activity', 'undo',
|
'moderation', 'activity', 'undo',
|
||||||
|
|
|
||||||
163
webinterface.py
163
webinterface.py
|
|
@ -766,6 +766,14 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
|
||||||
hashtagSearchForm += '<center>\n' + \
|
hashtagSearchForm += '<center>\n' + \
|
||||||
'<h1>#' + hashtag + '</h1>\n' + '</center>\n'
|
'<h1>#' + hashtag + '</h1>\n' + '</center>\n'
|
||||||
|
|
||||||
|
# RSS link for hashtag feed
|
||||||
|
hashtagSearchForm += '<center><a href="/tags/rss2/' + hashtag + '">'
|
||||||
|
hashtagSearchForm += \
|
||||||
|
'<img style="width:3%;min-width:50px" ' + \
|
||||||
|
'loading="lazy" alt="RSS 2.0" ' + \
|
||||||
|
'title="RSS 2.0" src="/' + \
|
||||||
|
iconsDir + '/rss.png" /></a></center>'
|
||||||
|
|
||||||
if startIndex > 0:
|
if startIndex > 0:
|
||||||
# previous page link
|
# previous page link
|
||||||
hashtagSearchForm += \
|
hashtagSearchForm += \
|
||||||
|
|
@ -787,7 +795,7 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
|
||||||
else:
|
else:
|
||||||
postFields = postId.split(' ')
|
postFields = postId.split(' ')
|
||||||
if len(postFields) != 3:
|
if len(postFields) != 3:
|
||||||
index = +1
|
index += 1
|
||||||
continue
|
continue
|
||||||
nickname = postFields[1]
|
nickname = postFields[1]
|
||||||
postId = postFields[2]
|
postId = postFields[2]
|
||||||
|
|
@ -833,6 +841,133 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
|
||||||
return hashtagSearchForm
|
return hashtagSearchForm
|
||||||
|
|
||||||
|
|
||||||
|
def rss2TagHeader(hashtag: str, httpPrefix: str, domainFull: str) -> str:
|
||||||
|
rssStr = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
|
||||||
|
rssStr += "<rss version=\"2.0\">"
|
||||||
|
rssStr += '<channel>'
|
||||||
|
rssStr += ' <title>#' + hashtag + '</title>'
|
||||||
|
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
|
||||||
|
'/tags/rss2/' + hashtag + '</link>'
|
||||||
|
return rssStr
|
||||||
|
|
||||||
|
|
||||||
|
def rss2TagFooter() -> str:
|
||||||
|
rssStr = '</channel>'
|
||||||
|
rssStr += '</rss>'
|
||||||
|
return rssStr
|
||||||
|
|
||||||
|
|
||||||
|
def rssHashtagSearch(nickname: str, domain: str, port: int,
|
||||||
|
recentPostsCache: {}, maxRecentPosts: int,
|
||||||
|
translate: {},
|
||||||
|
baseDir: str, hashtag: str,
|
||||||
|
postsPerPage: int,
|
||||||
|
session, wfRequest: {}, personCache: {},
|
||||||
|
httpPrefix: str, projectVersion: str,
|
||||||
|
YTReplacementDomain: str) -> str:
|
||||||
|
"""Show an rss feed for a hashtag
|
||||||
|
"""
|
||||||
|
if hashtag.startswith('#'):
|
||||||
|
hashtag = hashtag[1:]
|
||||||
|
hashtag = urllib.parse.unquote(hashtag)
|
||||||
|
hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt'
|
||||||
|
if not os.path.isfile(hashtagIndexFile):
|
||||||
|
if hashtag != hashtag.lower():
|
||||||
|
hashtag = hashtag.lower()
|
||||||
|
hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt'
|
||||||
|
if not os.path.isfile(hashtagIndexFile):
|
||||||
|
print('WARN: hashtag file not found ' + hashtagIndexFile)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# check that the directory for the nickname exists
|
||||||
|
if nickname:
|
||||||
|
if not os.path.isdir(baseDir + '/accounts/' +
|
||||||
|
nickname + '@' + domain):
|
||||||
|
nickname = None
|
||||||
|
|
||||||
|
# read the index
|
||||||
|
lines = []
|
||||||
|
with open(hashtagIndexFile, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
|
||||||
|
domainFull = domain
|
||||||
|
if port:
|
||||||
|
if port != 80 and port != 443:
|
||||||
|
domainFull = domain + ':' + str(port)
|
||||||
|
|
||||||
|
maxFeedLength = 10
|
||||||
|
hashtagFeed = \
|
||||||
|
rss2TagHeader(hashtag, httpPrefix, domainFull)
|
||||||
|
for index in range(len(lines)):
|
||||||
|
postId = lines[index].strip('\n').strip('\r')
|
||||||
|
if ' ' not in postId:
|
||||||
|
nickname = getNicknameFromActor(postId)
|
||||||
|
if not nickname:
|
||||||
|
index += 1
|
||||||
|
if index >= maxFeedLength:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
postFields = postId.split(' ')
|
||||||
|
if len(postFields) != 3:
|
||||||
|
index += 1
|
||||||
|
if index >= maxFeedLength:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
nickname = postFields[1]
|
||||||
|
postId = postFields[2]
|
||||||
|
postFilename = locatePost(baseDir, nickname, domain, postId)
|
||||||
|
if not postFilename:
|
||||||
|
index += 1
|
||||||
|
if index >= maxFeedLength:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
postJsonObject = loadJson(postFilename)
|
||||||
|
if postJsonObject:
|
||||||
|
if not isPublicPost(postJsonObject):
|
||||||
|
index += 1
|
||||||
|
if index >= maxFeedLength:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
# add to feed
|
||||||
|
if postJsonObject['object'].get('content') and \
|
||||||
|
postJsonObject['object'].get('attributedTo') and \
|
||||||
|
postJsonObject['object'].get('published'):
|
||||||
|
published = postJsonObject['object']['published']
|
||||||
|
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")
|
||||||
|
hashtagFeed += ' <item>'
|
||||||
|
hashtagFeed += \
|
||||||
|
' <author>' + \
|
||||||
|
postJsonObject['object']['attributedTo'] + \
|
||||||
|
'</author>'
|
||||||
|
if postJsonObject['object'].get('summary'):
|
||||||
|
hashtagFeed += \
|
||||||
|
' <title>' + \
|
||||||
|
postJsonObject['object']['summary'] + \
|
||||||
|
'</title>'
|
||||||
|
hashtagFeed += \
|
||||||
|
' <description><![CDATA[' + \
|
||||||
|
postJsonObject['object']['content'] + \
|
||||||
|
']]></description>'
|
||||||
|
hashtagFeed += \
|
||||||
|
' <pubDate>' + rssDateStr + '</pubDate>'
|
||||||
|
if postJsonObject['object'].get('attachment'):
|
||||||
|
for attach in postJsonObject['object']['attachment']:
|
||||||
|
if not attach.get('url'):
|
||||||
|
continue
|
||||||
|
hashtagFeed += \
|
||||||
|
' <link>' + attach['url'] + '</link>'
|
||||||
|
hashtagFeed += ' </item>'
|
||||||
|
index += 1
|
||||||
|
if index >= maxFeedLength:
|
||||||
|
break
|
||||||
|
|
||||||
|
return hashtagFeed + rss2TagFooter()
|
||||||
|
|
||||||
|
|
||||||
def htmlSkillsSearch(translate: {}, baseDir: str,
|
def htmlSkillsSearch(translate: {}, baseDir: str,
|
||||||
httpPrefix: str,
|
httpPrefix: str,
|
||||||
skillsearch: str, instanceOnly: bool,
|
skillsearch: str, instanceOnly: bool,
|
||||||
|
|
@ -1849,7 +1984,7 @@ def htmlAbout(baseDir: str, httpPrefix: str,
|
||||||
return aboutForm
|
return aboutForm
|
||||||
|
|
||||||
|
|
||||||
def htmlHashtagBlocked(baseDir: str) -> str:
|
def htmlHashtagBlocked(baseDir: str, translate: {}) -> str:
|
||||||
"""Show the screen for a blocked hashtag
|
"""Show the screen for a blocked hashtag
|
||||||
"""
|
"""
|
||||||
blockedHashtagForm = ''
|
blockedHashtagForm = ''
|
||||||
|
|
@ -1860,9 +1995,12 @@ def htmlHashtagBlocked(baseDir: str) -> str:
|
||||||
blockedHashtagCSS = cssFile.read()
|
blockedHashtagCSS = cssFile.read()
|
||||||
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
|
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
|
||||||
blockedHashtagForm += '<div><center>\n'
|
blockedHashtagForm += '<div><center>\n'
|
||||||
blockedHashtagForm += ' <p class="screentitle">Hashtag Blocked</p>\n'
|
|
||||||
blockedHashtagForm += \
|
blockedHashtagForm += \
|
||||||
' <p>See <a href="/terms">Terms of Service</a></p>\n'
|
' <p class="screentitle">' + \
|
||||||
|
translate['Hashtag Blocked'] + '</p>\n'
|
||||||
|
blockedHashtagForm += \
|
||||||
|
' <p>See <a href="/terms">' + \
|
||||||
|
translate['Terms of Service'] + '</a></p>\n'
|
||||||
blockedHashtagForm += '</center></div>\n'
|
blockedHashtagForm += '</center></div>\n'
|
||||||
blockedHashtagForm += htmlFooter()
|
blockedHashtagForm += htmlFooter()
|
||||||
return blockedHashtagForm
|
return blockedHashtagForm
|
||||||
|
|
@ -2515,7 +2653,7 @@ def htmlFooter() -> str:
|
||||||
def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
|
def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
translate: {},
|
translate: {},
|
||||||
baseDir: str, httpPrefix: str,
|
baseDir: str, httpPrefix: str,
|
||||||
authorized: bool, ocapAlways: bool,
|
authorized: bool,
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str, port: int,
|
||||||
session, wfRequest: {}, personCache: {},
|
session, wfRequest: {}, personCache: {},
|
||||||
projectVersion: str,
|
projectVersion: str,
|
||||||
|
|
@ -2536,8 +2674,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
str(currPage),
|
str(currPage),
|
||||||
httpPrefix,
|
httpPrefix,
|
||||||
10, 'outbox',
|
10, 'outbox',
|
||||||
authorized,
|
authorized)
|
||||||
ocapAlways)
|
|
||||||
if not outboxFeed:
|
if not outboxFeed:
|
||||||
break
|
break
|
||||||
if len(outboxFeed['orderedItems']) == 0:
|
if len(outboxFeed['orderedItems']) == 0:
|
||||||
|
|
@ -2565,7 +2702,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
|
|
||||||
|
|
||||||
def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
|
def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
|
||||||
authorized: bool, ocapAlways: bool,
|
authorized: bool,
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str, port: int,
|
||||||
session, wfRequest: {}, personCache: {},
|
session, wfRequest: {}, personCache: {},
|
||||||
followingJson: {}, projectVersion: str,
|
followingJson: {}, projectVersion: str,
|
||||||
|
|
@ -2795,7 +2932,7 @@ def htmlProfile(defaultTimeline: str,
|
||||||
recentPostsCache: {}, maxRecentPosts: int,
|
recentPostsCache: {}, maxRecentPosts: int,
|
||||||
translate: {}, projectVersion: str,
|
translate: {}, projectVersion: str,
|
||||||
baseDir: str, httpPrefix: str, authorized: bool,
|
baseDir: str, httpPrefix: str, authorized: bool,
|
||||||
ocapAlways: bool, profileJson: {}, selected: str,
|
profileJson: {}, selected: str,
|
||||||
session, wfRequest: {}, personCache: {},
|
session, wfRequest: {}, personCache: {},
|
||||||
YTReplacementDomain: str,
|
YTReplacementDomain: str,
|
||||||
extraJson=None,
|
extraJson=None,
|
||||||
|
|
@ -3055,14 +3192,14 @@ def htmlProfile(defaultTimeline: str,
|
||||||
htmlProfilePosts(recentPostsCache, maxRecentPosts,
|
htmlProfilePosts(recentPostsCache, maxRecentPosts,
|
||||||
translate,
|
translate,
|
||||||
baseDir, httpPrefix, authorized,
|
baseDir, httpPrefix, authorized,
|
||||||
ocapAlways, nickname, domain, port,
|
nickname, domain, port,
|
||||||
session, wfRequest, personCache,
|
session, wfRequest, personCache,
|
||||||
projectVersion,
|
projectVersion,
|
||||||
YTReplacementDomain) + licenseStr
|
YTReplacementDomain) + licenseStr
|
||||||
if selected == 'following':
|
if selected == 'following':
|
||||||
profileStr += \
|
profileStr += \
|
||||||
htmlProfileFollowing(translate, baseDir, httpPrefix,
|
htmlProfileFollowing(translate, baseDir, httpPrefix,
|
||||||
authorized, ocapAlways, nickname,
|
authorized, nickname,
|
||||||
domain, port, session,
|
domain, port, session,
|
||||||
wfRequest, personCache, extraJson,
|
wfRequest, personCache, extraJson,
|
||||||
projectVersion, ["unfollow"], selected,
|
projectVersion, ["unfollow"], selected,
|
||||||
|
|
@ -3070,7 +3207,7 @@ def htmlProfile(defaultTimeline: str,
|
||||||
if selected == 'followers':
|
if selected == 'followers':
|
||||||
profileStr += \
|
profileStr += \
|
||||||
htmlProfileFollowing(translate, baseDir, httpPrefix,
|
htmlProfileFollowing(translate, baseDir, httpPrefix,
|
||||||
authorized, ocapAlways, nickname,
|
authorized, nickname,
|
||||||
domain, port, session,
|
domain, port, session,
|
||||||
wfRequest, personCache, extraJson,
|
wfRequest, personCache, extraJson,
|
||||||
projectVersion, ["block"],
|
projectVersion, ["block"],
|
||||||
|
|
@ -3112,7 +3249,6 @@ def individualFollowAsHtml(translate: {},
|
||||||
if domain not in followUrl:
|
if domain not in followUrl:
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache, projectVersion,
|
personCache, projectVersion,
|
||||||
httpPrefix, nickname,
|
httpPrefix, nickname,
|
||||||
|
|
@ -3966,7 +4102,6 @@ def individualPostAsHtml(allowDownloads: bool,
|
||||||
if fullDomain not in postActor:
|
if fullDomain not in postActor:
|
||||||
(inboxUrl, pubKeyId, pubKey,
|
(inboxUrl, pubKeyId, pubKey,
|
||||||
fromPersonId, sharedInbox,
|
fromPersonId, sharedInbox,
|
||||||
capabilityAcquisition,
|
|
||||||
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
|
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
personCache,
|
personCache,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue