epicyon/announce.py

430 lines
16 KiB
Python

__filename__ = "announce.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "ActivityPub"
from utils import hasGroupType
from utils import removeDomainPort
from utils import hasObjectDict
from utils import removeIdEnding
from utils import hasUsersPath
from utils import getFullDomain
from utils import getStatusNumber
from utils import createOutboxDir
from utils import urlPermitted
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
from utils import saveJson
from utils import undoAnnounceCollectionEntry
from utils import updateAnnounceCollection
from utils import localActorUrl
from utils import replaceUsersWithAt
from posts import sendSignedJson
from posts import getPersonBox
from session import postJson
from webfinger import webfingerHandle
from auth import createBasicAuthHeader
def isSelfAnnounce(postJsonObject: {}) -> bool:
"""Is the given post a self announce?
"""
if not postJsonObject.get('actor'):
return False
if not postJsonObject.get('type'):
return False
if postJsonObject['type'] != 'Announce':
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['actor'], str):
return False
if not isinstance(postJsonObject['object'], str):
return False
return postJsonObject['actor'] in postJsonObject['object']
def outboxAnnounce(recentPostsCache: {},
baseDir: str, messageJson: {}, debug: bool) -> bool:
""" Adds or removes announce entries from the shares collection
within a given post
"""
if not messageJson.get('actor'):
return False
if not isinstance(messageJson['actor'], str):
return False
if not messageJson.get('type'):
return False
if not messageJson.get('object'):
return False
if messageJson['type'] == 'Announce':
if not isinstance(messageJson['object'], str):
return False
if isSelfAnnounce(messageJson):
return False
nickname = getNicknameFromActor(messageJson['actor'])
if not nickname:
print('WARN: no nickname found in ' + messageJson['actor'])
return False
domain, port = getDomainFromActor(messageJson['actor'])
postFilename = locatePost(baseDir, nickname, domain,
messageJson['object'])
if postFilename:
updateAnnounceCollection(recentPostsCache, baseDir, postFilename,
messageJson['actor'],
nickname, domain, debug)
return True
elif messageJson['type'] == 'Undo':
if not hasObjectDict(messageJson):
return False
if not messageJson['object'].get('type'):
return False
if messageJson['object']['type'] == 'Announce':
if not isinstance(messageJson['object']['object'], str):
return False
nickname = getNicknameFromActor(messageJson['actor'])
if not nickname:
print('WARN: no nickname found in ' + messageJson['actor'])
return False
domain, port = getDomainFromActor(messageJson['actor'])
postFilename = locatePost(baseDir, nickname, domain,
messageJson['object']['object'])
if postFilename:
undoAnnounceCollectionEntry(recentPostsCache,
baseDir, postFilename,
messageJson['actor'],
domain, debug)
return True
return False
def announcedByPerson(isAnnounced: bool, postActor: str,
nickname: str, domainFull: str) -> bool:
"""Returns True if the given post is announced by the given person
"""
if not postActor:
return False
if isAnnounced and \
postActor.endswith(domainFull + '/users/' + nickname):
return True
return False
def createAnnounce(session, baseDir: str, federationList: [],
nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str,
objectUrl: str, saveToFile: bool,
clientToServer: bool,
sendThreads: [], postLog: [],
personCache: {}, cachedWebfingers: {},
debug: bool, projectVersion: str,
signingPrivateKeyPem: str) -> {}:
"""Creates an announce message
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
and ccUrl might be a specific person favorited or repeated and the
followers url objectUrl is typically the url of the message,
corresponding to url or atomUri in createPostBase
"""
if not urlPermitted(objectUrl, federationList):
return None
domain = removeDomainPort(domain)
fullDomain = getFullDomain(domain, port)
statusNumber, published = getStatusNumber()
newAnnounceId = httpPrefix + '://' + fullDomain + \
'/users/' + nickname + '/statuses/' + statusNumber
atomUriStr = localActorUrl(httpPrefix, nickname, fullDomain) + \
'/statuses/' + statusNumber
newAnnounce = {
"@context": "https://www.w3.org/ns/activitystreams",
'actor': localActorUrl(httpPrefix, nickname, fullDomain),
'atomUri': atomUriStr,
'cc': [],
'id': newAnnounceId + '/activity',
'object': objectUrl,
'published': published,
'to': [toUrl],
'type': 'Announce'
}
if ccUrl:
if len(ccUrl) > 0:
newAnnounce['cc'] = [ccUrl]
if saveToFile:
outboxDir = createOutboxDir(nickname, domain, baseDir)
filename = outboxDir + '/' + newAnnounceId.replace('/', '#') + '.json'
saveJson(newAnnounce, filename)
announceNickname = None
announceDomain = None
announcePort = None
groupAccount = False
if hasUsersPath(objectUrl):
announceNickname = getNicknameFromActor(objectUrl)
announceDomain, announcePort = getDomainFromActor(objectUrl)
if '/' + str(announceNickname) + '/' in objectUrl:
announceActor = \
objectUrl.split('/' + announceNickname + '/')[0] + \
'/' + announceNickname
if hasGroupType(baseDir, announceActor, personCache):
groupAccount = True
if announceNickname and announceDomain:
sendSignedJson(newAnnounce, session, baseDir,
nickname, domain, port,
announceNickname, announceDomain, announcePort, None,
httpPrefix, True, clientToServer, federationList,
sendThreads, postLog, cachedWebfingers, personCache,
debug, projectVersion, None, groupAccount,
signingPrivateKeyPem)
return newAnnounce
def announcePublic(session, baseDir: str, federationList: [],
nickname: str, domain: str, port: int, httpPrefix: str,
objectUrl: str, clientToServer: bool,
sendThreads: [], postLog: [],
personCache: {}, cachedWebfingers: {},
debug: bool, projectVersion: str,
signingPrivateKeyPem: str) -> {}:
"""Makes a public announcement
"""
fromDomain = getFullDomain(domain, port)
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
ccUrl = localActorUrl(httpPrefix, nickname, fromDomain) + '/followers'
return createAnnounce(session, baseDir, federationList,
nickname, domain, port,
toUrl, ccUrl, httpPrefix,
objectUrl, True, clientToServer,
sendThreads, postLog,
personCache, cachedWebfingers,
debug, projectVersion,
signingPrivateKeyPem)
def sendAnnounceViaServer(baseDir: str, session,
fromNickname: str, password: str,
fromDomain: str, fromPort: int,
httpPrefix: str, repeatObjectUrl: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str,
signingPrivateKeyPem: str) -> {}:
"""Creates an announce message via c2s
"""
if not session:
print('WARN: No session for sendAnnounceViaServer')
return 6
fromDomainFull = getFullDomain(fromDomain, fromPort)
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
actorStr = localActorUrl(httpPrefix, fromNickname, fromDomainFull)
ccUrl = actorStr + '/followers'
statusNumber, published = getStatusNumber()
newAnnounceId = actorStr + '/statuses/' + statusNumber
newAnnounceJson = {
"@context": "https://www.w3.org/ns/activitystreams",
'actor': actorStr,
'atomUri': newAnnounceId,
'cc': [ccUrl],
'id': newAnnounceId + '/activity',
'object': repeatObjectUrl,
'published': published,
'to': [toUrl],
'type': 'Announce'
}
handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
fromDomain, projectVersion, debug, False,
signingPrivateKeyPem)
if not wfRequest:
if debug:
print('DEBUG: announce webfinger failed for ' + handle)
return 1
if not isinstance(wfRequest, dict):
print('WARN: announce webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return 1
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, avatarUrl,
displayName) = getPersonBox(signingPrivateKeyPem,
baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
fromNickname, fromDomain,
postToBox, 73528)
if not inboxUrl:
if debug:
print('DEBUG: announce no ' + postToBox +
' was found for ' + handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: announce no actor was found for ' + handle)
return 4
authHeader = createBasicAuthHeader(fromNickname, password)
headers = {
'host': fromDomain,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(httpPrefix, fromDomainFull,
session, newAnnounceJson, [], inboxUrl,
headers, 3, True)
if not postResult:
print('WARN: announce not posted')
if debug:
print('DEBUG: c2s POST announce success')
return newAnnounceJson
def sendUndoAnnounceViaServer(baseDir: str, session,
undoPostJsonObject: {},
nickname: str, password: str,
domain: str, port: int,
httpPrefix: str, repeatObjectUrl: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str,
signingPrivateKeyPem: str) -> {}:
"""Undo an announce message via c2s
"""
if not session:
print('WARN: No session for sendUndoAnnounceViaServer')
return 6
domainFull = getFullDomain(domain, port)
actor = localActorUrl(httpPrefix, nickname, domainFull)
handle = replaceUsersWithAt(actor)
statusNumber, published = getStatusNumber()
unAnnounceJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': actor + '/statuses/' + str(statusNumber) + '/undo',
'type': 'Undo',
'actor': actor,
'object': undoPostJsonObject['object']
}
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
domain, projectVersion, debug, False,
signingPrivateKeyPem)
if not wfRequest:
if debug:
print('DEBUG: undo announce webfinger failed for ' + handle)
return 1
if not isinstance(wfRequest, dict):
print('WARN: undo announce webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return 1
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey, fromPersonId,
sharedInbox, avatarUrl,
displayName) = getPersonBox(signingPrivateKeyPem,
baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain,
postToBox, 73528)
if not inboxUrl:
if debug:
print('DEBUG: undo announce no ' + postToBox +
' was found for ' + handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: undo announce no actor was found for ' + handle)
return 4
authHeader = createBasicAuthHeader(nickname, password)
headers = {
'host': domain,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(httpPrefix, domainFull,
session, unAnnounceJson, [], inboxUrl,
headers, 3, True)
if not postResult:
print('WARN: undo announce not posted')
if debug:
print('DEBUG: c2s POST undo announce success')
return unAnnounceJson
def outboxUndoAnnounce(recentPostsCache: {},
baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
messageJson: {}, debug: bool) -> None:
""" When an undo announce is received by the outbox from c2s
"""
if not messageJson.get('type'):
return
if not messageJson['type'] == 'Undo':
return
if not hasObjectDict(messageJson):
if debug:
print('DEBUG: undo like object is not dict')
return
if not messageJson['object'].get('type'):
if debug:
print('DEBUG: undo like - no type')
return
if not messageJson['object']['type'] == 'Announce':
if debug:
print('DEBUG: not a undo announce')
return
if not messageJson['object'].get('object'):
if debug:
print('DEBUG: no object in undo announce')
return
if not isinstance(messageJson['object']['object'], str):
if debug:
print('DEBUG: undo announce object is not string')
return
if debug:
print('DEBUG: c2s undo announce request arrived in outbox')
messageId = removeIdEnding(messageJson['object']['object'])
domain = removeDomainPort(domain)
postFilename = locatePost(baseDir, nickname, domain, messageId)
if not postFilename:
if debug:
print('DEBUG: c2s undo announce post not found in inbox or outbox')
print(messageId)
return True
undoAnnounceCollectionEntry(recentPostsCache, baseDir, postFilename,
messageJson['actor'], domain, debug)
if debug:
print('DEBUG: post undo announce via c2s - ' + postFilename)