Announce function

master
Bob Mottram 2019-07-02 10:25:29 +01:00
parent e2de1d1b9c
commit f609e365da
8 changed files with 143 additions and 53 deletions

View File

@ -9,5 +9,5 @@ Also: https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tut
## Install ## Install
``` bash ``` bash
sudo pacman -S python-pysocks python-pycryptodome python-beautifulsoup4 sudo pacman -S python-pysocks python-pycryptodome python-beautifulsoup4 python-requests-toolbelt
``` ```

65
announce.py 100644
View File

@ -0,0 +1,65 @@
__filename__ = "announce.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "0.0.1"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
import commentjson
from utils import getStatusNumber
from utils import createOutboxDir
def createAnnounce(baseDir: str,username: str, domain: str, port: int,toUrl: str, ccUrl: str, https: bool, objectUrl: str, saveToFile: bool) -> {}:
"""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
"""
prefix='https'
if not https:
prefix='http'
if port!=80 and port!=443:
domain=domain+':'+str(port)
statusNumber,published = getStatusNumber()
newAnnounceId=prefix+'://'+domain+'/users/'+username+'/statuses/'+statusNumber
newAnnounce = {
'actor': prefix+'://'+domain+'/users/'+username,
'atomUri': prefix+'://'+domain+'/users/'+username+'/statuses/'+statusNumber,
'cc': [],
'id': newAnnounceId+'/activity',
'object': objectUrl,
'published': published,
'to': [toUrl],
'type': 'Announce'
}
if ccUrl:
if len(ccUrl)>0:
newAnnounce['cc']=ccUrl
if saveToFile:
if ':' in domain:
domain=domain.split(':')[0]
outboxDir = createOutboxDir(username,domain,baseDir)
filename=outboxDir+'/'+newAnnounceId.replace('/','#')+'.json'
with open(filename, 'w') as fp:
commentjson.dump(newAnnounce, fp, indent=4, sort_keys=False)
return newAnnounce
def announcePublic(baseDir: str,username: str, domain: str, port: int, https: bool, objectUrl: str, saveToFile: bool) -> {}:
"""Makes a public announcement
"""
prefix='https'
if not https:
prefix='http'
fromDomain=domain
if port!=80 and port!=443:
fromDomain=fromDomain+':'+str(port)
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
ccUrl = prefix + '://'+fromDomain+'/users/'+username+'/followers'
return createAnnounce(baseDir,username, domain, port,toUrl, ccUrl, https, objectUrl, saveToFile)

View File

@ -220,7 +220,7 @@ class PubServer(BaseHTTPRequestHandler):
currSessionTime=int(time.time()) currSessionTime=int(time.time())
if currSessionTime-self.server.sessionLastUpdate>600: if currSessionTime-self.server.sessionLastUpdate>600:
self.server.sessionLastUpdate=currSessionTime self.server.sessionLastUpdate=currSessionTime
self.server.session = createSession(self.server.useTor) self.server.session = createSession(self.server.domain,self.server.port,self.server.useTor)
print('**************** POST get public key of '+personUrl+' from '+self.server.baseDir) print('**************** POST get public key of '+personUrl+' from '+self.server.baseDir)
pubKey=getPersonPubKey(self.server.session,personUrl,self.server.personCache) pubKey=getPersonPubKey(self.server.session,personUrl,self.server.personCache)
if not pubKey: if not pubKey:
@ -230,6 +230,12 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy=False self.server.POSTbusy=False
return return
print('**************** POST check signature') print('**************** POST check signature')
if not verifyPostHeaders(self.server.https, pubKey, self.headers, '/inbox' ,False, json.dumps(messageJson)):
print('**************** POST signature verification failed')
self.send_response(401)
self.end_headers()
self.server.POSTbusy=False
return
print('**************** POST valid') print('**************** POST valid')
pprint(messageJson) pprint(messageJson)
# add a property to the object, just to mess with data # add a property to the object, just to mess with data
@ -261,7 +267,7 @@ def runDaemon(domain: str,port=80,https=True,fedList=[],useTor=False) -> None:
httpd.personCache={} httpd.personCache={}
httpd.cachedWebfingers={} httpd.cachedWebfingers={}
httpd.useTor=useTor httpd.useTor=useTor
httpd.session = createSession(useTor) httpd.session = createSession(domain,port,useTor)
httpd.sessionLastUpdate=int(time.time()) httpd.sessionLastUpdate=int(time.time())
httpd.lastGET=0 httpd.lastGET=0
httpd.lastPOST=0 httpd.lastPOST=0

View File

@ -10,7 +10,7 @@ from person import createPerson
from person import setPreferredUsername from person import setPreferredUsername
from person import setBio from person import setBio
from webfinger import webfingerHandle from webfinger import webfingerHandle
from posts import getUserPosts from posts import getPosts
from posts import createPublicPost from posts import createPublicPost
from posts import deleteAllPosts from posts import deleteAllPosts
from posts import createOutbox from posts import createOutbox
@ -35,6 +35,7 @@ from follow import unfollowPerson
from follow import unfollowerOfPerson from follow import unfollowerOfPerson
from tests import testPostMessageBetweenServers from tests import testPostMessageBetweenServers
from tests import runAllTests from tests import runAllTests
from announce import announcePublic
runAllTests() runAllTests()
@ -46,7 +47,7 @@ port=6227
https=False https=False
useTor=False useTor=False
baseDir=os.getcwd() baseDir=os.getcwd()
session = createSession(useTor) session = createSession(domain,port,useTor)
personCache={} personCache={}
cachedWebfingers={} cachedWebfingers={}
@ -84,11 +85,11 @@ setBio(baseDir,username,domain,'Some personal info')
#outboxJson=createOutbox(baseDir,username,domain,port,https,2,True,None) #outboxJson=createOutbox(baseDir,username,domain,port,https,2,True,None)
#pprint(outboxJson) #pprint(outboxJson)
testPostMessageBetweenServers() #testPostMessageBetweenServers()
#runDaemon(domain,port,https,federationList,useTor) #runDaemon(domain,port,https,federationList,useTor)
#testHttpsig() #testHttpsig()
sys.exit() #sys.exit()
#pprint(person) #pprint(person)
#print('\n') #print('\n')
@ -99,16 +100,16 @@ wfRequest = webfingerHandle(session,handle,True,cachedWebfingers)
if not wfRequest: if not wfRequest:
sys.exit() sys.exit()
personJson,pubKeyId,pubKey,personId=getPersonBox(session,wfRequest,personCache) personUrl,pubKeyId,pubKey,personId=getPersonBox(session,wfRequest,personCache,'outbox')
pprint(personJson) #pprint(personUrl)
sys.exit() #sys.exit()
wfResult = json.dumps(wfRequest, indent=4, sort_keys=True) wfResult = json.dumps(wfRequest, indent=4, sort_keys=True)
print(str(wfResult)) #print(str(wfResult))
sys.exit() #sys.exit()
maxMentions=10 maxMentions=10
maxEmoji=10 maxEmoji=10
maxAttachments=5 maxAttachments=5
userPosts = getUserPosts(session,wfRequest,2,maxMentions,maxEmoji,maxAttachments,federationList) userPosts = getPosts(session,personUrl,10,maxMentions,maxEmoji,maxAttachments,federationList,personCache)
#print(str(userPosts)) #print(str(userPosts))

View File

@ -26,6 +26,8 @@ from session import getJson
from session import postJson from session import postJson
from webfinger import webfingerHandle from webfinger import webfingerHandle
from httpsig import createSignedHeader from httpsig import createSignedHeader
from utils import getStatusNumber
from utils import createOutboxDir
try: try:
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
except ImportError: except ImportError:
@ -113,6 +115,7 @@ def getPersonPubKey(session,personUrl: str,personCache: {}) -> str:
return None return None
personJson = getPersonFromCache(personUrl,personCache) personJson = getPersonFromCache(personUrl,personCache)
if not personJson: if not personJson:
print('************Obtaining public key for '+personUrl)
personJson = getJson(session,personUrl,asHeader,None) personJson = getJson(session,personUrl,asHeader,None)
pubKey=None pubKey=None
if personJson.get('publicKey'): if personJson.get('publicKey'):
@ -122,14 +125,15 @@ def getPersonPubKey(session,personUrl: str,personCache: {}) -> str:
storePersonInCache(personUrl,personJson,personCache) storePersonInCache(personUrl,personJson,personCache)
return pubKey return pubKey
def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji: int,maxAttachments: int,federationList: [],personCache: {}) -> {}: def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int,maxEmoji: int,maxAttachments: int,federationList: [],personCache: {}) -> {}:
userPosts={} personPosts={}
feedUrl,pubKeyId,pubKey,personId = getPersonBox(session,wfRequest,personCache,'outbox') if not outboxUrl:
if not feedUrl: return personPosts
return userPosts
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
i = 0 i = 0
for item in parseUserFeed(session,feedUrl,asHeader): for item in parseUserFeed(session,outboxUrl,asHeader):
pprint(item)
if not item.get('type'): if not item.get('type'):
continue continue
if item['type'] != 'Create': if item['type'] != 'Create':
@ -137,7 +141,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if not item.get('object'): if not item.get('object'):
continue continue
published = item['object']['published'] published = item['object']['published']
if not userPosts.get(published): if not personPosts.get(published):
content = item['object']['content'] content = item['object']['content']
mentions=[] mentions=[]
@ -195,7 +199,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if item['object'].get('sensitive'): if item['object'].get('sensitive'):
sensitive = item['object']['sensitive'] sensitive = item['object']['sensitive']
userPosts[published] = { personPosts[published] = {
"sensitive": sensitive, "sensitive": sensitive,
"inreplyto": inReplyTo, "inreplyto": inReplyTo,
"summary": summary, "summary": summary,
@ -211,18 +215,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if i == maxPosts: if i == maxPosts:
break break
return userPosts return personPosts
def createOutboxDir(username: str,domain: str,baseDir: str) -> str:
"""Create an outbox for a person and returns the feed filename and directory
"""
handle=username.lower()+'@'+domain.lower()
if not os.path.isdir(baseDir+'/accounts/'+handle):
os.mkdir(baseDir+'/accounts/'+handle)
outboxDir=baseDir+'/accounts/'+handle+'/outbox'
if not os.path.isdir(outboxDir):
os.mkdir(outboxDir)
return outboxDir
def createOutboxArchive(username: str,domain: str,baseDir: str) -> str: def createOutboxArchive(username: str,domain: str,baseDir: str) -> str:
"""Creates an archive directory for outbox posts """Creates an archive directory for outbox posts
@ -247,17 +240,6 @@ def deleteAllPosts(username: str, domain: str,baseDir: str) -> None:
elif os.path.isdir(filePath): shutil.rmtree(filePath) elif os.path.isdir(filePath): shutil.rmtree(filePath)
except Exception as e: except Exception as e:
print(e) print(e)
def getStatusNumber() -> (str,str):
"""Returns the status number and published date
"""
currTime=datetime.datetime.utcnow()
daysSinceEpoch=(currTime - datetime.datetime(1970,1,1)).days
# status is the number of seconds since epoch
statusNumber=str(((daysSinceEpoch*24*60*60) + (currTime.hour*60*60) + (currTime.minute*60) + currTime.second)*1000000 + currTime.microsecond)
published=currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
conversationDate=currTime.strftime("%Y-%m-%d")
return statusNumber,published
def createPostBase(baseDir: str,username: str, domain: str, port: int,toUrl: str, ccUrl: str, https: bool, content: str, followersOnly: bool, saveToFile: bool, inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: def createPostBase(baseDir: str,username: str, domain: str, port: int,toUrl: str, ccUrl: str, https: bool, content: str, followersOnly: bool, saveToFile: bool, inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
"""Creates a message """Creates a message

View File

@ -7,12 +7,16 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import requests import requests
from requests_toolbelt.adapters.source import SourceAddressAdapter
import json import json
baseDirectory=None baseDirectory=None
def createSession(onionRoute: bool): def createSession(domain: str, port: int, onionRoute: bool):
session = requests.session() session = requests.session()
if domain.startswith('127.') or domain.startswith('192.') or domain.startswith('10.'):
session.mount('http://', SourceAddressAdapter(domain))
#session.mount('http://', SourceAddressAdapter((domain, port)))
if onionRoute: if onionRoute:
session.proxies = {} session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:9050' session.proxies['http'] = 'socks5h://localhost:9050'

View File

@ -109,8 +109,8 @@ def createServerAlice(path: str,domain: str,port: int,federationList: []):
useTor=False useTor=False
privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True) privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True)
deleteAllPosts(username,domain,path) deleteAllPosts(username,domain,path)
followPerson(path,username,domain,'bob','127.0.10.2:61936',federationList) followPerson(path,username,domain,'bob','127.0.0.100:61936',federationList)
followerOfPerson(path,username,domain,'bob','127.0.10.2:61936',federationList) followerOfPerson(path,username,domain,'bob','127.0.0.100:61936',federationList)
createPublicPost(path,username, domain, port,https, "No wise fish would go anywhere without a porpoise", False, True) createPublicPost(path,username, domain, port,https, "No wise fish would go anywhere without a porpoise", False, True)
createPublicPost(path,username, domain, port,https, "Curiouser and curiouser!", False, True) createPublicPost(path,username, domain, port,https, "Curiouser and curiouser!", False, True)
createPublicPost(path,username, domain, port,https, "In the gardens of memory, in the palace of dreams, that is where you and I shall meet", False, True) createPublicPost(path,username, domain, port,https, "In the gardens of memory, in the palace of dreams, that is where you and I shall meet", False, True)
@ -130,8 +130,8 @@ def createServerBob(path: str,domain: str,port: int,federationList: []):
useTor=False useTor=False
privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True) privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True)
deleteAllPosts(username,domain,path) deleteAllPosts(username,domain,path)
followPerson(path,username,domain,'alice','127.0.10.1:61935',federationList) followPerson(path,username,domain,'alice','127.0.0.50:61935',federationList)
followerOfPerson(path,username,domain,'alice','127.0.10.1:61935',federationList) followerOfPerson(path,username,domain,'alice','127.0.0.50:61935',federationList)
createPublicPost(path,username, domain, port,https, "It's your life, live it your way.", False, True) createPublicPost(path,username, domain, port,https, "It's your life, live it your way.", False, True)
createPublicPost(path,username, domain, port,https, "One of the things I've realised is that I am very simple", False, True) createPublicPost(path,username, domain, port,https, "One of the things I've realised is that I am very simple", False, True)
createPublicPost(path,username, domain, port,https, "Quantum physics is a bit of a passion of mine", False, True) createPublicPost(path,username, domain, port,https, "Quantum physics is a bit of a passion of mine", False, True)
@ -150,7 +150,7 @@ def testPostMessageBetweenServers():
https=False https=False
useTor=False useTor=False
federationList=['127.0.0.1','127.0.10.1','127.0.10.2'] federationList=['127.0.0.50','127.0.0.100']
baseDir=os.getcwd() baseDir=os.getcwd()
if not os.path.isdir(baseDir+'/.tests'): if not os.path.isdir(baseDir+'/.tests'):
@ -158,12 +158,12 @@ def testPostMessageBetweenServers():
# create the servers # create the servers
aliceDir=baseDir+'/.tests/alice' aliceDir=baseDir+'/.tests/alice'
aliceDomain='127.0.10.1' aliceDomain='127.0.0.50'
alicePort=61935 alicePort=61935
thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList),daemon=True) thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList),daemon=True)
bobDir=baseDir+'/.tests/bob' bobDir=baseDir+'/.tests/bob'
bobDomain='127.0.10.2' bobDomain='127.0.0.100'
bobPort=61936 bobPort=61936
thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList),daemon=True) thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList),daemon=True)
@ -180,7 +180,7 @@ def testPostMessageBetweenServers():
print('Alice sends to Bob') print('Alice sends to Bob')
os.chdir(aliceDir) os.chdir(aliceDir)
sessionAlice = createSession(useTor) sessionAlice = createSession(aliceDomain,alicePort,useTor)
inReplyTo=None inReplyTo=None
inReplyToAtomUri=None inReplyToAtomUri=None
subject=None subject=None

32
utils.py 100644
View File

@ -0,0 +1,32 @@
__filename__ = "utils.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "0.0.1"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
import datetime
def getStatusNumber() -> (str,str):
"""Returns the status number and published date
"""
currTime=datetime.datetime.utcnow()
daysSinceEpoch=(currTime - datetime.datetime(1970,1,1)).days
# status is the number of seconds since epoch
statusNumber=str(((daysSinceEpoch*24*60*60) + (currTime.hour*60*60) + (currTime.minute*60) + currTime.second)*1000000 + currTime.microsecond)
published=currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
conversationDate=currTime.strftime("%Y-%m-%d")
return statusNumber,published
def createOutboxDir(username: str,domain: str,baseDir: str) -> str:
"""Create an outbox for a person and returns the feed filename and directory
"""
handle=username.lower()+'@'+domain.lower()
if not os.path.isdir(baseDir+'/accounts/'+handle):
os.mkdir(baseDir+'/accounts/'+handle)
outboxDir=baseDir+'/accounts/'+handle+'/outbox'
if not os.path.isdir(outboxDir):
os.mkdir(outboxDir)
return outboxDir