epicyon/person.py

690 lines
26 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "person.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2019-08-29 13:35:29 +00:00
__version__ = "1.0.0"
2019-06-28 18:55:29 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
import commentjson
import os
import fileinput
2019-07-12 14:31:56 +00:00
import subprocess
2019-08-13 11:59:38 +00:00
import shutil
from pprint import pprint
2019-07-12 14:31:56 +00:00
from pathlib import Path
2019-06-28 18:55:29 +00:00
from Crypto.PublicKey import RSA
2019-07-12 13:51:04 +00:00
from shutil import copyfile
2019-06-28 18:55:29 +00:00
from webfinger import createWebfingerEndpoint
from webfinger import storeWebfingerEndpoint
2019-08-25 16:09:56 +00:00
from posts import createDMTimeline
2019-09-23 19:53:18 +00:00
from posts import createRepliesTimeline
2019-07-25 16:50:48 +00:00
from posts import createInbox
2019-06-29 14:35:26 +00:00
from posts import createOutbox
2019-08-12 13:22:17 +00:00
from posts import createModeration
from auth import storeBasicCredentials
2019-08-13 11:59:38 +00:00
from auth import removePassword
2019-07-18 15:09:23 +00:00
from roles import setRole
from media import removeMetaData
2019-07-27 22:48:34 +00:00
from utils import validNickname
2019-08-08 11:24:26 +00:00
from utils import noOfAccounts
2019-08-13 13:58:48 +00:00
from auth import createPassword
2019-08-08 10:50:58 +00:00
from config import setConfigParam
from config import getConfigParam
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-12 13:51:04 +00:00
def setProfileImage(baseDir: str,httpPrefix :str,nickname: str,domain: str, \
port :int,imageFilename: str,imageType :str,resolution :str) -> bool:
2019-07-12 13:51:04 +00:00
"""Saves the given image file as an avatar or background
image for the given person
"""
2019-07-12 14:31:56 +00:00
imageFilename=imageFilename.replace('\n','')
2019-07-12 13:51:04 +00:00
if not (imageFilename.endswith('.png') or \
imageFilename.endswith('.jpg') or \
imageFilename.endswith('.jpeg') or \
imageFilename.endswith('.gif')):
2019-07-12 14:31:56 +00:00
print('Profile image must be png, jpg or gif format')
return False
2019-07-12 13:51:04 +00:00
2019-07-12 14:31:56 +00:00
if imageFilename.startswith('~/'):
imageFilename=imageFilename.replace('~/',str(Path.home())+'/')
if ':' in domain:
domain=domain.split(':')[0]
fullDomain=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
fullDomain=domain+':'+str(port)
2019-07-12 13:51:04 +00:00
handle=nickname.lower()+'@'+domain.lower()
personFilename=baseDir+'/accounts/'+handle+'.json'
if not os.path.isfile(personFilename):
2019-07-12 14:31:56 +00:00
print('person definition not found: '+personFilename)
return False
2019-07-12 13:51:04 +00:00
if not os.path.isdir(baseDir+'/accounts/'+handle):
2019-07-12 14:31:56 +00:00
print('Account not found: '+baseDir+'/accounts/'+handle)
return False
2019-07-12 13:51:04 +00:00
iconFilenameBase='icon'
if imageType=='avatar' or imageType=='icon':
iconFilenameBase='icon'
else:
iconFilenameBase='image'
mediaType='image/png'
iconFilename=iconFilenameBase+'.png'
if imageFilename.endswith('.jpg') or \
imageFilename.endswith('.jpeg'):
mediaType='image/jpeg'
iconFilename=iconFilenameBase+'.jpg'
if imageFilename.endswith('.gif'):
mediaType='image/gif'
iconFilename=iconFilenameBase+'.gif'
profileFilename=baseDir+'/accounts/'+handle+'/'+iconFilename
with open(personFilename, 'r') as fp:
personJson=commentjson.load(fp)
personJson[iconFilenameBase]['mediaType']=mediaType
2019-07-12 14:31:56 +00:00
personJson[iconFilenameBase]['url']=httpPrefix+'://'+fullDomain+'/users/'+nickname+'/'+iconFilename
2019-07-12 13:51:04 +00:00
with open(personFilename, 'w') as fp:
commentjson.dump(personJson, fp, indent=4, sort_keys=False)
cmd = '/usr/bin/convert '+imageFilename+' -size '+resolution+' -quality 50 '+profileFilename
2019-07-12 14:31:56 +00:00
subprocess.call(cmd, shell=True)
removeMetaData(profileFilename,profileFilename)
2019-07-12 14:31:56 +00:00
return True
return False
2019-07-12 13:51:04 +00:00
def setOrganizationScheme(baseDir: str,nickname: str,domain: str, \
schema: str) -> bool:
"""Set the organization schema within which a person exists
This will define how roles, skills and availability are assembled
into organizations
"""
# avoid giant strings
if len(schema)>256:
return False
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return False
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
actorJson['orgSchema']=schema
with open(actorFilename, 'w') as fp:
commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
return True
def accountExists(baseDir: str,nickname: str,domain: str) -> bool:
"""Returns true if the given account exists
"""
if ':' in domain:
domain=domain.split(':')[0]
return os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain)
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-19 14:03:34 +00:00
storeWebfingerEndpoint(nickname,domain,port,baseDir,webfingerEndpoint)
2019-06-30 18:23:18 +00:00
2019-07-03 09:40:27 +00:00
handle=nickname.lower()+'@'+domain.lower()
originalDomain=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
2019-06-30 18:23:18 +00:00
personType='Person'
approveFollowers=False
personName=nickname
personId=httpPrefix+'://'+domain+'/users/'+nickname
inboxStr=personId+'/inbox'
2019-08-26 15:20:14 +00:00
personUrl=httpPrefix+'://'+domain+'/@'+personName
if nickname=='inbox':
# shared inbox
inboxStr=httpPrefix+'://'+domain+'/actor/inbox'
personId=httpPrefix+'://'+domain+'/actor'
2019-08-26 15:20:14 +00:00
personUrl=httpPrefix+'://'+domain+'/about/more?instance_actor=true'
personName=originalDomain
approveFollowers=True
personType='Application'
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'},
'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
'schema': 'http://schema.org#',
'value': 'schema:value'}],
'attachment': [],
2019-07-03 20:52:25 +00:00
'endpoints': {
'id': personId+'/endpoints',
2019-07-03 20:52:25 +00:00
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
},
2019-07-05 22:17:06 +00:00
'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
'followers': personId+'/followers',
'following': personId+'/following',
'shares': personId+'/shares',
'orgSchema': None,
2019-07-14 12:08:18 +00:00
'skills': {},
'roles': {},
2019-07-14 13:30:59 +00:00
'availability': None,
2019-06-28 18:55:29 +00:00
'icon': {'mediaType': 'image/png',
'type': 'Image',
'url': personId+'/avatar.png'},
'id': personId,
2019-06-28 18:55:29 +00:00
'image': {'mediaType': 'image/png',
'type': 'Image',
'url': personId+'/image.png'},
'inbox': inboxStr,
'manuallyApprovesFollowers': approveFollowers,
'name': personName,
'outbox': personId+'/outbox',
'preferredUsername': personName,
2019-07-31 12:44:08 +00:00
'summary': '',
2019-08-07 19:40:12 +00:00
'publicKey': {
'id': personId+'#main-key',
'owner': personId,
2019-08-07 19:40:12 +00:00
'publicKeyPem': publicKeyPem
},
'tag': [],
'type': personType,
2019-08-26 15:20:14 +00:00
'url': personUrl
2019-06-28 18:55:29 +00:00
}
if nickname=='inbox':
# fields not needed by the shared inbox
del newPerson['outbox']
del newPerson['icon']
del newPerson['image']
del newPerson['skills']
del newPerson['shares']
del newPerson['roles']
del newPerson['tag']
del newPerson['availability']
del newPerson['followers']
del newPerson['following']
del newPerson['attachment']
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-07-11 12:29:31 +00:00
if not os.path.isdir(baseDir+peopleSubdir+'/'+handle+'/inbox'):
os.mkdir(baseDir+peopleSubdir+'/'+handle+'/inbox')
if not os.path.isdir(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'):
os.mkdir(baseDir+peopleSubdir+'/'+handle+'/queue')
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)
2019-08-22 14:43:43 +00:00
# save to cache
if not os.path.isdir(baseDir+'/cache'):
os.mkdir(baseDir+'/cache')
if not os.path.isdir(baseDir+'/cache/actors'):
os.mkdir(baseDir+'/cache/actors')
cacheFilename=baseDir+'/cache/actors/'+newPerson['id'].replace('/','#')+'.json'
with open(cacheFilename, 'w') as fp:
commentjson.dump(newPerson, fp, indent=4, sort_keys=False)
2019-06-28 18:55:29 +00:00
# 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)
if password:
storeBasicCredentials(baseDir,nickname,password)
2019-06-28 18:55:29 +00:00
return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
2019-08-08 13:38:33 +00:00
def registerAccount(baseDir: str,httpPrefix: str,domain: str,port: int, \
nickname: str,password: str) -> bool:
"""Registers a new account from the web interface
"""
if accountExists(baseDir,nickname,domain):
return False
2019-08-23 13:47:29 +00:00
if not validNickname(domain,nickname):
2019-08-08 13:38:33 +00:00
print('REGISTER: Nickname '+nickname+' is invalid')
return False
if len(password)<8:
print('REGISTER: Password should be at least 8 characters')
return False
privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint= \
createPerson(baseDir,nickname,domain,port, \
httpPrefix,True,password)
if privateKeyPem:
return True
return False
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
"""
2019-08-23 13:47:29 +00:00
if not validNickname(domain,nickname):
2019-07-05 11:27:18 +00:00
return None,None,None,None
2019-08-08 10:50:58 +00:00
2019-08-09 09:14:31 +00:00
# If a config.json file doesn't exist then don't decrement
# remaining registrations counter
2019-08-09 09:13:08 +00:00
remainingConfigExists=getConfigParam(baseDir,'registrationsRemaining')
if remainingConfigExists:
registrationsRemaining=int(remainingConfigExists)
2019-08-09 09:11:42 +00:00
if registrationsRemaining<=0:
return None,None,None,None
2019-08-08 10:50:58 +00:00
2019-07-18 13:10:26 +00:00
privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint = \
createPersonBase(baseDir,nickname,domain,port,httpPrefix,saveToFile,password)
if noOfAccounts(baseDir)==1:
2019-07-18 14:02:21 +00:00
#print(nickname+' becomes the instance admin and a moderator')
2019-07-18 13:22:55 +00:00
setRole(baseDir,nickname,domain,'instance','admin')
setRole(baseDir,nickname,domain,'instance','moderator')
2019-07-18 13:57:39 +00:00
setRole(baseDir,nickname,domain,'instance','delegator')
2019-08-10 15:07:02 +00:00
setConfigParam(baseDir,'admin',nickname)
2019-07-23 12:33:09 +00:00
2019-08-09 09:46:33 +00:00
if not os.path.isdir(baseDir+'/accounts'):
os.mkdir(baseDir+'/accounts')
2019-07-23 12:33:09 +00:00
if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
os.mkdir(baseDir+'/accounts/'+nickname+'@'+domain)
2019-07-21 12:29:36 +00:00
if os.path.isfile(baseDir+'/img/default-avatar.png'):
copyfile(baseDir+'/img/default-avatar.png',baseDir+'/accounts/'+nickname+'@'+domain+'/avatar.png')
theme=getConfigParam(baseDir,'theme')
defaultProfileImageFilename=baseDir+'/img/image.png'
if theme:
if os.path.isfile(baseDir+'/img/image_'+theme+'.png'):
defaultBannerFilename=baseDir+'/img/image_'+theme+'.png'
if os.path.isfile(defaultProfileImageFilename):
copyfile(defaultProfileImageFilename,baseDir+'/accounts/'+nickname+'@'+domain+'/image.png')
defaultBannerFilename=baseDir+'/img/banner.png'
if theme:
if os.path.isfile(baseDir+'/img/banner_'+theme+'.png'):
defaultBannerFilename=baseDir+'/img/banner_'+theme+'.png'
if os.path.isfile(defaultBannerFilename):
copyfile(defaultBannerFilename,baseDir+'/accounts/'+nickname+'@'+domain+'/banner.png')
2019-08-09 09:13:08 +00:00
if remainingConfigExists:
2019-08-09 09:11:42 +00:00
registrationsRemaining-=1
setConfigParam(baseDir,'registrationsRemaining',str(registrationsRemaining))
2019-07-18 13:10:26 +00:00
return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
2019-07-05 11:27:18 +00:00
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)
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,None)
2019-06-28 18:55:29 +00:00
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-08-05 15:52:18 +00:00
# is this a shared inbox lookup?
isSharedInbox=False
2019-08-05 15:22:59 +00:00
if path=='/inbox' or path=='/users/inbox' or path=='/sharedInbox':
2019-08-23 13:47:29 +00:00
# shared inbox actor on @domain@domain
path='/users/'+domain
isSharedInbox=True
2019-08-05 15:22:59 +00:00
else:
notPersonLookup=['/inbox','/outbox','/outboxarchive', \
'/followers','/following','/featured', \
'.png','.jpg','.gif','.mpv']
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-08-23 13:47:29 +00:00
if not isSharedInbox and not validNickname(domain,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-08-05 15:22:59 +00:00
handle=nickname+'@'+domain
filename=baseDir+'/accounts/'+handle+'.json'
2019-06-28 18:55:29 +00:00
if not os.path.isfile(filename):
return None
personJson={"user": "unknown"}
2019-08-05 21:20:44 +00:00
try:
with open(filename, 'r') as fp:
personJson=commentjson.load(fp)
except:
print('WARN: Failed to load actor '+filename)
return None
2019-06-28 18:55:29 +00:00
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, \
authorized: bool,ocapAlways: bool) -> []:
2019-08-12 13:22:17 +00:00
"""Obtain the inbox/outbox/moderation feed for the given person
2019-06-29 14:35:26 +00:00
"""
2019-08-25 16:09:56 +00:00
if boxname!='inbox' and boxname!='dm' and \
2019-09-23 19:53:18 +00:00
boxname!='replies' and \
2019-08-25 16:09:56 +00:00
boxname!='outbox' and boxname!='moderation':
2019-07-04 16:24:23 +00:00
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-08-23 13:47:29 +00:00
if not validNickname(domain,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,ocapAlways,pageNumber)
2019-08-25 16:09:56 +00:00
if boxname=='dm':
return createDMTimeline(baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-09-23 19:53:18 +00:00
elif boxname=='replies':
return createRepliesTimeline(baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-08-12 13:22:17 +00:00
elif boxname=='outbox':
return createOutbox(baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,authorized,pageNumber)
elif boxname=='moderation':
return createModeration(baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,authorized,pageNumber)
return None
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,ocapAlways: bool) -> []:
2019-07-04 16:24:23 +00:00
"""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
2019-08-23 13:47:29 +00:00
if not validNickname(domain,nickname):
2019-07-04 16:24:23 +00:00
return None
return createInbox(baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-07-04 16:24:23 +00:00
def setDisplayNickname(baseDir: str,nickname: str, domain: str, \
displayName: str) -> bool:
if len(displayName)>32:
2019-06-28 18:55:29 +00:00
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
personJson['name']=displayName
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
2019-07-31 12:44:08 +00:00
if not personJson.get('summary'):
2019-06-28 20:00:25 +00:00
return False
2019-07-31 12:44:08 +00:00
personJson['summary']=bio
2019-06-28 20:00:25 +00:00
with open(filename, 'w') as fp:
commentjson.dump(personJson, fp, indent=4, sort_keys=False)
return True
2019-08-13 09:24:55 +00:00
def isSuspended(baseDir: str,nickname: str) -> bool:
"""Returns true if the given nickname is suspended
"""
adminNickname=getConfigParam(baseDir,'admin')
if nickname==adminNickname:
return False
suspendedFilename=baseDir+'/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
lines = f.readlines()
suspendedFile=open(suspendedFilename,"w+")
for suspended in lines:
if suspended.strip('\n')==nickname:
return True
return False
def unsuspendAccount(baseDir: str,nickname: str) -> None:
"""Removes an account suspention
"""
suspendedFilename=baseDir+'/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
lines = f.readlines()
suspendedFile=open(suspendedFilename,"w+")
for suspended in lines:
if suspended.strip('\n')!=nickname:
suspendedFile.write(suspended)
suspendedFile.close()
def suspendAccount(baseDir: str,nickname: str,salts: {}) -> None:
"""Suspends the given account
This also changes the salt used by the authentication token
so that the person can't continue to use the system without
going through the login screen
"""
# Don't suspend the admin
adminNickname=getConfigParam(baseDir,'admin')
if nickname==adminNickname:
return
# Don't suspend moderators
moderatorsFile=baseDir+'/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
lines = f.readlines()
for moderator in lines:
if moderator.strip('\n')==nickname:
return
suspendedFilename=baseDir+'/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
lines = f.readlines()
for suspended in lines:
if suspended.strip('\n')==nickname:
return
suspendedFile=open(suspendedFilename,'a+')
if suspendedFile:
suspendedFile.write(nickname+'\n')
suspendedFile.close()
salts[nickname]=createPassword(32)
else:
suspendedFile=open(suspendedFilename,'w+')
if suspendedFile:
suspendedFile.write(nickname+'\n')
suspendedFile.close()
salts[nickname]=createPassword(32)
2019-08-13 11:59:38 +00:00
def canRemovePost(baseDir: str,nickname: str,domain: str,port: int,postId: str) -> bool:
"""Returns true if the given post can be removed
"""
if '/statuses/' not in postId:
return False
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
# is the post by the admin?
adminNickname=getConfigParam(baseDir,'admin')
if domainFull+'/users/'+adminNickname+'/' in postId:
return False
# is the post by a moderator?
moderatorsFile=baseDir+'/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
lines = f.readlines()
for moderator in lines:
if domainFull+'/users/'+moderator.strip('\n')+'/' in postId:
return False
return True
2019-08-13 12:14:11 +00:00
def removeTagsForNickname(baseDir: str,nickname: str,domain: str,port: int) -> None:
"""Removes tags for a nickname
"""
if not os.path.isdir(baseDir+'/tags'):
return
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-08-13 12:14:11 +00:00
matchStr=domainFull+'/users/'+nickname+'/'
directory = os.fsencode(baseDir+'/tags/')
for f in os.listdir(directory):
filename = os.fsdecode(f)
if not filename.endswith(".txt"):
continue
tagFilename=os.path.join(baseDir+'/accounts/',filename)
if matchStr not in open(tagFilename).read():
continue
with open(tagFilename, "r") as f:
lines = f.readlines()
tagFile=open(tagFilename,"w+")
if tagFile:
for tagline in lines:
if matchStr not in tagline:
tagFile.write(tagline)
tagFile.close()
def removeAccount(baseDir: str,nickname: str,domain: str,port: int) -> bool:
2019-08-13 11:59:38 +00:00
"""Removes an account
2019-08-13 13:58:48 +00:00
"""
2019-08-13 12:00:17 +00:00
# Don't remove the admin
2019-08-13 11:59:38 +00:00
adminNickname=getConfigParam(baseDir,'admin')
if nickname==adminNickname:
return False
2019-08-13 12:00:17 +00:00
# Don't remove moderators
2019-08-13 11:59:38 +00:00
moderatorsFile=baseDir+'/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
lines = f.readlines()
for moderator in lines:
if moderator.strip('\n')==nickname:
return False
unsuspendAccount(baseDir,nickname)
handle=nickname+'@'+domain
removePassword(baseDir,nickname)
2019-08-13 12:14:11 +00:00
removeTagsForNickname(baseDir,nickname,domain,port)
2019-08-13 11:59:38 +00:00
if os.path.isdir(baseDir+'/accounts/'+handle):
shutil.rmtree(baseDir+'/accounts/'+handle)
if os.path.isfile(baseDir+'/accounts/'+handle+'.json'):
os.remove(baseDir+'/accounts/'+handle+'.json')
if os.path.isfile(baseDir+'/wfendpoints/'+handle+'.json'):
os.remove(baseDir+'/wfendpoints/'+handle+'.json')
if os.path.isfile(baseDir+'/keys/private/'+handle+'.key'):
os.remove(baseDir+'/keys/private/'+handle+'.key')
if os.path.isfile(baseDir+'/keys/public/'+handle+'.pem'):
os.remove(baseDir+'/keys/public/'+handle+'.pem')
if os.path.isdir(baseDir+'/sharefiles/'+nickname):
shutil.rmtree(baseDir+'/sharefiles/'+nickname)
return True