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
``` 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())
if currSessionTime-self.server.sessionLastUpdate>600:
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)
pubKey=getPersonPubKey(self.server.session,personUrl,self.server.personCache)
if not pubKey:
@ -230,6 +230,12 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy=False
return
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')
pprint(messageJson)
# 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.cachedWebfingers={}
httpd.useTor=useTor
httpd.session = createSession(useTor)
httpd.session = createSession(domain,port,useTor)
httpd.sessionLastUpdate=int(time.time())
httpd.lastGET=0
httpd.lastPOST=0

View File

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

View File

@ -26,6 +26,8 @@ from session import getJson
from session import postJson
from webfinger import webfingerHandle
from httpsig import createSignedHeader
from utils import getStatusNumber
from utils import createOutboxDir
try:
from BeautifulSoup import BeautifulSoup
except ImportError:
@ -113,6 +115,7 @@ def getPersonPubKey(session,personUrl: str,personCache: {}) -> str:
return None
personJson = getPersonFromCache(personUrl,personCache)
if not personJson:
print('************Obtaining public key for '+personUrl)
personJson = getJson(session,personUrl,asHeader,None)
pubKey=None
if personJson.get('publicKey'):
@ -122,14 +125,15 @@ def getPersonPubKey(session,personUrl: str,personCache: {}) -> str:
storePersonInCache(personUrl,personJson,personCache)
return pubKey
def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji: int,maxAttachments: int,federationList: [],personCache: {}) -> {}:
userPosts={}
feedUrl,pubKeyId,pubKey,personId = getPersonBox(session,wfRequest,personCache,'outbox')
if not feedUrl:
return userPosts
def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int,maxEmoji: int,maxAttachments: int,federationList: [],personCache: {}) -> {}:
personPosts={}
if not outboxUrl:
return personPosts
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
i = 0
for item in parseUserFeed(session,feedUrl,asHeader):
for item in parseUserFeed(session,outboxUrl,asHeader):
pprint(item)
if not item.get('type'):
continue
if item['type'] != 'Create':
@ -137,7 +141,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if not item.get('object'):
continue
published = item['object']['published']
if not userPosts.get(published):
if not personPosts.get(published):
content = item['object']['content']
mentions=[]
@ -195,7 +199,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if item['object'].get('sensitive'):
sensitive = item['object']['sensitive']
userPosts[published] = {
personPosts[published] = {
"sensitive": sensitive,
"inreplyto": inReplyTo,
"summary": summary,
@ -211,18 +215,7 @@ def getUserPosts(session,wfRequest: {},maxPosts: int,maxMentions: int,maxEmoji:
if i == maxPosts:
break
return userPosts
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
return personPosts
def createOutboxArchive(username: str,domain: str,baseDir: str) -> str:
"""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)
except Exception as 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) -> {}:
"""Creates a message

View File

@ -7,12 +7,16 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import requests
from requests_toolbelt.adapters.source import SourceAddressAdapter
import json
baseDirectory=None
def createSession(onionRoute: bool):
def createSession(domain: str, port: int, onionRoute: bool):
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:
session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:9050'

View File

@ -109,8 +109,8 @@ def createServerAlice(path: str,domain: str,port: int,federationList: []):
useTor=False
privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True)
deleteAllPosts(username,domain,path)
followPerson(path,username,domain,'bob','127.0.10.2:61936',federationList)
followerOfPerson(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.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, "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)
@ -130,8 +130,8 @@ def createServerBob(path: str,domain: str,port: int,federationList: []):
useTor=False
privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,username,domain,port,https,True)
deleteAllPosts(username,domain,path)
followPerson(path,username,domain,'alice','127.0.10.1:61935',federationList)
followerOfPerson(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.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, "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)
@ -150,7 +150,7 @@ def testPostMessageBetweenServers():
https=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()
if not os.path.isdir(baseDir+'/.tests'):
@ -158,12 +158,12 @@ def testPostMessageBetweenServers():
# create the servers
aliceDir=baseDir+'/.tests/alice'
aliceDomain='127.0.10.1'
aliceDomain='127.0.0.50'
alicePort=61935
thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList),daemon=True)
bobDir=baseDir+'/.tests/bob'
bobDomain='127.0.10.2'
bobDomain='127.0.0.100'
bobPort=61936
thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList),daemon=True)
@ -180,7 +180,7 @@ def testPostMessageBetweenServers():
print('Alice sends to Bob')
os.chdir(aliceDir)
sessionAlice = createSession(useTor)
sessionAlice = createSession(aliceDomain,alicePort,useTor)
inReplyTo=None
inReplyToAtomUri=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