2019-06-28 18:55:29 +00:00
|
|
|
__filename__ = "person.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__license__ = "AGPL3+"
|
|
|
|
__version__ = "0.0.1"
|
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
__status__ = "Production"
|
|
|
|
|
|
|
|
import json
|
|
|
|
import commentjson
|
|
|
|
import os
|
|
|
|
import fileinput
|
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
from webfinger import createWebfingerEndpoint
|
|
|
|
from webfinger import storeWebfingerEndpoint
|
2019-06-29 14:35:26 +00:00
|
|
|
from posts import createOutbox
|
2019-07-03 19:10:24 +00:00
|
|
|
from auth import storeBasicCredentials
|
2019-06-28 18:55:29 +00:00
|
|
|
|
|
|
|
def generateRSAKey() -> (str,str):
|
|
|
|
key = RSA.generate(2048)
|
|
|
|
privateKeyPem = key.exportKey("PEM").decode("utf-8")
|
|
|
|
publicKeyPem = key.publickey().exportKey("PEM").decode("utf-8")
|
|
|
|
return privateKeyPem,publicKeyPem
|
|
|
|
|
2019-07-05 11:27:18 +00:00
|
|
|
def createPersonBase(baseDir: str,nickname: str,domain: str,port: int, \
|
|
|
|
httpPrefix: str, saveToFile: bool,password=None) -> (str,str,{},{}):
|
2019-06-28 18:55:29 +00:00
|
|
|
"""Returns the private key, public key, actor and webfinger endpoint
|
|
|
|
"""
|
|
|
|
privateKeyPem,publicKeyPem=generateRSAKey()
|
2019-07-02 20:54:22 +00:00
|
|
|
webfingerEndpoint= \
|
2019-07-03 19:00:03 +00:00
|
|
|
createWebfingerEndpoint(nickname,domain,port,httpPrefix,publicKeyPem)
|
2019-06-28 18:55:29 +00:00
|
|
|
if saveToFile:
|
2019-07-03 09:40:27 +00:00
|
|
|
storeWebfingerEndpoint(nickname,domain,baseDir,webfingerEndpoint)
|
2019-06-30 18:23:18 +00:00
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
handle=nickname.lower()+'@'+domain.lower()
|
2019-06-30 18:23:18 +00:00
|
|
|
if port!=80 and port!=443:
|
|
|
|
domain=domain+':'+str(port)
|
|
|
|
|
2019-06-28 18:55:29 +00:00
|
|
|
newPerson = {'@context': ['https://www.w3.org/ns/activitystreams',
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
{'Emoji': 'toot:Emoji',
|
|
|
|
'Hashtag': 'as:Hashtag',
|
|
|
|
'IdentityProof': 'toot:IdentityProof',
|
|
|
|
'PropertyValue': 'schema:PropertyValue',
|
|
|
|
'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
|
|
|
|
'featured': {'@id': 'toot:featured', '@type': '@id'},
|
|
|
|
'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
|
|
|
|
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
|
|
|
|
'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
|
|
|
|
'schema': 'http://schema.org#',
|
|
|
|
'toot': 'http://joinmastodon.org/ns#',
|
|
|
|
'value': 'schema:value'}],
|
|
|
|
'attachment': [],
|
2019-07-03 20:52:25 +00:00
|
|
|
'endpoints': {
|
2019-07-04 08:56:15 +00:00
|
|
|
'id': httpPrefix+'://'+domain+'/users/'+nickname+'/endpoints',
|
2019-07-03 20:52:25 +00:00
|
|
|
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
|
|
|
'uploadMedia': httpPrefix+'://'+domain+'/users/'+nickname+'/endpoints/uploadMedia'
|
|
|
|
},
|
2019-07-03 19:00:03 +00:00
|
|
|
'featured': httpPrefix+'://'+domain+'/users/'+nickname+'/collections/featured',
|
|
|
|
'followers': httpPrefix+'://'+domain+'/users/'+nickname+'/followers',
|
|
|
|
'following': httpPrefix+'://'+domain+'/users/'+nickname+'/following',
|
2019-06-28 18:55:29 +00:00
|
|
|
'icon': {'mediaType': 'image/png',
|
|
|
|
'type': 'Image',
|
2019-07-03 19:00:03 +00:00
|
|
|
'url': httpPrefix+'://'+domain+'/users/'+nickname+'_icon.png'},
|
|
|
|
'id': httpPrefix+'://'+domain+'/users/'+nickname,
|
2019-06-28 18:55:29 +00:00
|
|
|
'image': {'mediaType': 'image/png',
|
|
|
|
'type': 'Image',
|
2019-07-03 19:00:03 +00:00
|
|
|
'url': httpPrefix+'://'+domain+'/users/'+nickname+'.png'},
|
|
|
|
'inbox': httpPrefix+'://'+domain+'/users/'+nickname+'/inbox',
|
2019-06-28 20:02:04 +00:00
|
|
|
'manuallyApprovesFollowers': False,
|
2019-07-03 09:40:27 +00:00
|
|
|
'name': nickname,
|
2019-07-03 19:00:03 +00:00
|
|
|
'outbox': httpPrefix+'://'+domain+'/users/'+nickname+'/outbox',
|
2019-07-03 09:47:09 +00:00
|
|
|
'preferredUsername': ''+nickname,
|
2019-07-04 14:36:29 +00:00
|
|
|
'publicKey': {'id': httpPrefix+'://'+domain+'/users/'+nickname+'#main-key',
|
2019-07-03 19:00:03 +00:00
|
|
|
'owner': httpPrefix+'://'+domain+'/users/'+nickname,
|
2019-06-28 18:55:29 +00:00
|
|
|
'publicKeyPem': publicKeyPem,
|
|
|
|
'summary': '',
|
|
|
|
'tag': [],
|
|
|
|
'type': 'Person',
|
2019-07-03 19:00:03 +00:00
|
|
|
'url': httpPrefix+'://'+domain+'/@'+nickname}
|
2019-06-28 18:55:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if saveToFile:
|
|
|
|
# save person to file
|
|
|
|
peopleSubdir='/accounts'
|
|
|
|
if not os.path.isdir(baseDir+peopleSubdir):
|
|
|
|
os.mkdir(baseDir+peopleSubdir)
|
2019-07-05 09:20:54 +00:00
|
|
|
if not os.path.isdir(baseDir+peopleSubdir+'/'+handle):
|
|
|
|
os.mkdir(baseDir+peopleSubdir+'/'+handle)
|
2019-06-28 18:55:29 +00:00
|
|
|
filename=baseDir+peopleSubdir+'/'+handle+'.json'
|
|
|
|
with open(filename, 'w') as fp:
|
|
|
|
commentjson.dump(newPerson, fp, indent=4, sort_keys=False)
|
|
|
|
|
|
|
|
# save the private key
|
|
|
|
privateKeysSubdir='/keys/private'
|
|
|
|
if not os.path.isdir(baseDir+'/keys'):
|
|
|
|
os.mkdir(baseDir+'/keys')
|
|
|
|
if not os.path.isdir(baseDir+privateKeysSubdir):
|
|
|
|
os.mkdir(baseDir+privateKeysSubdir)
|
|
|
|
filename=baseDir+privateKeysSubdir+'/'+handle+'.key'
|
|
|
|
with open(filename, "w") as text_file:
|
|
|
|
print(privateKeyPem, file=text_file)
|
|
|
|
|
|
|
|
# save the public key
|
|
|
|
publicKeysSubdir='/keys/public'
|
|
|
|
if not os.path.isdir(baseDir+publicKeysSubdir):
|
|
|
|
os.mkdir(baseDir+publicKeysSubdir)
|
|
|
|
filename=baseDir+publicKeysSubdir+'/'+handle+'.pem'
|
|
|
|
with open(filename, "w") as text_file:
|
|
|
|
print(publicKeyPem, file=text_file)
|
|
|
|
|
2019-07-03 19:10:24 +00:00
|
|
|
if password:
|
|
|
|
storeBasicCredentials(baseDir,nickname,password)
|
|
|
|
|
2019-06-28 18:55:29 +00:00
|
|
|
return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
|
|
|
|
|
2019-07-05 11:27:18 +00:00
|
|
|
def createPerson(baseDir: str,nickname: str,domain: str,port: int, \
|
|
|
|
httpPrefix: str, saveToFile: bool,password=None) -> (str,str,{},{}):
|
|
|
|
"""Returns the private key, public key, actor and webfinger endpoint
|
|
|
|
"""
|
|
|
|
if not validNickname(nickname):
|
|
|
|
return None,None,None,None
|
|
|
|
return createPersonBase(baseDir,nickname,domain,port,httpPrefix,saveToFile,password)
|
|
|
|
|
|
|
|
def createSharedInbox(baseDir: str,nickname: str,domain: str,port: int, \
|
|
|
|
httpPrefix: str) -> (str,str,{},{}):
|
|
|
|
"""Generates the shared inbox
|
|
|
|
"""
|
|
|
|
return createPersonBase(baseDir,nickname,domain,port,httpPrefix,True,None)
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
def validNickname(nickname: str) -> bool:
|
2019-06-28 18:55:29 +00:00
|
|
|
forbiddenChars=['.',' ','/','?',':',';','@']
|
|
|
|
for c in forbiddenChars:
|
2019-07-03 09:40:27 +00:00
|
|
|
if c in nickname:
|
2019-06-28 18:55:29 +00:00
|
|
|
return False
|
2019-07-05 11:27:18 +00:00
|
|
|
reservedNames=['inbox','outbox','following','followers','sharedinbox']
|
2019-07-04 18:26:37 +00:00
|
|
|
if nickname in reservedNames:
|
|
|
|
return False
|
2019-06-28 18:55:29 +00:00
|
|
|
return True
|
|
|
|
|
2019-06-30 22:56:37 +00:00
|
|
|
def personLookup(domain: str,path: str,baseDir: str) -> {}:
|
2019-07-03 09:40:27 +00:00
|
|
|
"""Lookup the person for an given nickname
|
2019-06-28 18:55:29 +00:00
|
|
|
"""
|
2019-07-04 18:26:37 +00:00
|
|
|
if path.endswith('#main-key'):
|
|
|
|
path=path.replace('#main-key','')
|
2019-07-02 20:54:22 +00:00
|
|
|
notPersonLookup=['/inbox','/outbox','/outboxarchive', \
|
|
|
|
'/followers','/following','/featured', \
|
2019-07-04 14:36:29 +00:00
|
|
|
'.png','.jpg','.gif','.mpv']
|
2019-06-28 18:55:29 +00:00
|
|
|
for ending in notPersonLookup:
|
|
|
|
if path.endswith(ending):
|
|
|
|
return None
|
2019-07-03 09:40:27 +00:00
|
|
|
nickname=None
|
2019-06-28 18:55:29 +00:00
|
|
|
if path.startswith('/users/'):
|
2019-07-03 09:40:27 +00:00
|
|
|
nickname=path.replace('/users/','',1)
|
2019-06-28 18:55:29 +00:00
|
|
|
if path.startswith('/@'):
|
2019-07-03 09:40:27 +00:00
|
|
|
nickname=path.replace('/@','',1)
|
|
|
|
if not nickname:
|
2019-06-28 18:55:29 +00:00
|
|
|
return None
|
2019-07-03 09:40:27 +00:00
|
|
|
if not validNickname(nickname):
|
2019-06-28 18:55:29 +00:00
|
|
|
return None
|
2019-07-01 21:01:43 +00:00
|
|
|
if ':' in domain:
|
|
|
|
domain=domain.split(':')[0]
|
2019-07-03 09:40:27 +00:00
|
|
|
handle=nickname.lower()+'@'+domain.lower()
|
2019-06-28 18:55:29 +00:00
|
|
|
filename=baseDir+'/accounts/'+handle.lower()+'.json'
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
return None
|
|
|
|
personJson={"user": "unknown"}
|
|
|
|
with open(filename, 'r') as fp:
|
|
|
|
personJson=commentjson.load(fp)
|
|
|
|
return personJson
|
2019-06-29 14:35:26 +00:00
|
|
|
|
2019-07-04 16:24:23 +00:00
|
|
|
def personBoxJson(baseDir: str,domain: str,port: int,path: str, \
|
|
|
|
httpPrefix: str,noOfItems: int,boxname: str) -> []:
|
|
|
|
"""Obtain the inbox/outbox feed for the given person
|
2019-06-29 14:35:26 +00:00
|
|
|
"""
|
2019-07-04 16:24:23 +00:00
|
|
|
if boxname!='inbox' and boxname!='outbox':
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not '/'+boxname in path:
|
2019-06-29 16:47:37 +00:00
|
|
|
return None
|
|
|
|
|
2019-06-29 17:12:26 +00:00
|
|
|
# Only show the header by default
|
|
|
|
headerOnly=True
|
|
|
|
|
2019-06-29 16:47:37 +00:00
|
|
|
# handle page numbers
|
2019-06-29 17:12:26 +00:00
|
|
|
pageNumber=None
|
2019-06-29 16:47:37 +00:00
|
|
|
if '?page=' in path:
|
|
|
|
pageNumber=path.split('?page=')[1]
|
|
|
|
if pageNumber=='true':
|
|
|
|
pageNumber=1
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
pageNumber=int(pageNumber)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
path=path.split('?page=')[0]
|
2019-06-29 17:12:26 +00:00
|
|
|
headerOnly=False
|
2019-06-29 16:47:37 +00:00
|
|
|
|
2019-07-04 16:24:23 +00:00
|
|
|
if not path.endswith('/'+boxname):
|
2019-06-29 15:18:35 +00:00
|
|
|
return None
|
2019-07-03 09:40:27 +00:00
|
|
|
nickname=None
|
2019-06-29 14:35:26 +00:00
|
|
|
if path.startswith('/users/'):
|
2019-07-04 16:24:23 +00:00
|
|
|
nickname=path.replace('/users/','',1).replace('/'+boxname,'')
|
2019-06-29 14:35:26 +00:00
|
|
|
if path.startswith('/@'):
|
2019-07-04 16:24:23 +00:00
|
|
|
nickname=path.replace('/@','',1).replace('/'+boxname,'')
|
2019-07-03 09:40:27 +00:00
|
|
|
if not nickname:
|
2019-06-29 15:18:35 +00:00
|
|
|
return None
|
2019-07-03 09:40:27 +00:00
|
|
|
if not validNickname(nickname):
|
2019-06-29 15:18:35 +00:00
|
|
|
return None
|
2019-07-04 16:24:23 +00:00
|
|
|
if boxname=='inbox':
|
|
|
|
return createInbox(baseDir,nickname,domain,port,httpPrefix, \
|
|
|
|
noOfItems,headerOnly,pageNumber)
|
2019-07-03 19:00:03 +00:00
|
|
|
return createOutbox(baseDir,nickname,domain,port,httpPrefix, \
|
2019-07-02 20:54:22 +00:00
|
|
|
noOfItems,headerOnly,pageNumber)
|
2019-06-29 14:35:26 +00:00
|
|
|
|
2019-07-04 16:24:23 +00:00
|
|
|
def personInboxJson(baseDir: str,domain: str,port: int,path: str, \
|
|
|
|
httpPrefix: str,noOfItems: int) -> []:
|
|
|
|
"""Obtain the inbox feed for the given person
|
|
|
|
Authentication is expected to have already happened
|
|
|
|
"""
|
|
|
|
if not '/inbox' in path:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Only show the header by default
|
|
|
|
headerOnly=True
|
|
|
|
|
|
|
|
# handle page numbers
|
|
|
|
pageNumber=None
|
|
|
|
if '?page=' in path:
|
|
|
|
pageNumber=path.split('?page=')[1]
|
|
|
|
if pageNumber=='true':
|
|
|
|
pageNumber=1
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
pageNumber=int(pageNumber)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
path=path.split('?page=')[0]
|
|
|
|
headerOnly=False
|
|
|
|
|
|
|
|
if not path.endswith('/inbox'):
|
|
|
|
return None
|
|
|
|
nickname=None
|
|
|
|
if path.startswith('/users/'):
|
|
|
|
nickname=path.replace('/users/','',1).replace('/inbox','')
|
|
|
|
if path.startswith('/@'):
|
|
|
|
nickname=path.replace('/@','',1).replace('/inbox','')
|
|
|
|
if not nickname:
|
|
|
|
return None
|
|
|
|
if not validNickname(nickname):
|
|
|
|
return None
|
|
|
|
return createInbox(baseDir,nickname,domain,port,httpPrefix, \
|
|
|
|
noOfItems,headerOnly,pageNumber)
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
def setPreferredNickname(baseDir: str,nickname: str, domain: str, \
|
2019-07-02 20:54:22 +00:00
|
|
|
preferredName: str) -> bool:
|
2019-06-28 18:55:29 +00:00
|
|
|
if len(preferredName)>32:
|
|
|
|
return False
|
2019-07-03 09:40:27 +00:00
|
|
|
handle=nickname.lower()+'@'+domain.lower()
|
2019-06-28 18:55:29 +00:00
|
|
|
filename=baseDir+'/accounts/'+handle.lower()+'.json'
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
return False
|
|
|
|
personJson=None
|
|
|
|
with open(filename, 'r') as fp:
|
|
|
|
personJson=commentjson.load(fp)
|
|
|
|
if not personJson:
|
|
|
|
return False
|
2019-07-03 09:47:09 +00:00
|
|
|
personJson['preferredUsername']=preferredName
|
2019-06-28 18:55:29 +00:00
|
|
|
with open(filename, 'w') as fp:
|
|
|
|
commentjson.dump(personJson, fp, indent=4, sort_keys=False)
|
|
|
|
return True
|
2019-06-28 20:00:25 +00:00
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
def setBio(baseDir: str,nickname: str, domain: str, bio: str) -> bool:
|
2019-06-28 20:00:25 +00:00
|
|
|
if len(bio)>32:
|
|
|
|
return False
|
2019-07-03 09:40:27 +00:00
|
|
|
handle=nickname.lower()+'@'+domain.lower()
|
2019-06-28 20:00:25 +00:00
|
|
|
filename=baseDir+'/accounts/'+handle.lower()+'.json'
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
return False
|
|
|
|
personJson=None
|
|
|
|
with open(filename, 'r') as fp:
|
|
|
|
personJson=commentjson.load(fp)
|
|
|
|
if not personJson:
|
|
|
|
return False
|
|
|
|
if not personJson.get('publicKey'):
|
|
|
|
return False
|
|
|
|
personJson['publicKey']['summary']=bio
|
|
|
|
with open(filename, 'w') as fp:
|
|
|
|
commentjson.dump(personJson, fp, indent=4, sort_keys=False)
|
|
|
|
return True
|