forked from indymedia/epicyon
				
			
		
			
				
	
	
		
			676 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			676 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
| __filename__ = "person.py"
 | |
| __author__ = "Bob Mottram"
 | |
| __license__ = "AGPL3+"
 | |
| __version__ = "1.0.0"
 | |
| __maintainer__ = "Bob Mottram"
 | |
| __email__ = "bob@freedombone.net"
 | |
| __status__ = "Production"
 | |
| 
 | |
| import json
 | |
| import commentjson
 | |
| import os
 | |
| import fileinput
 | |
| import subprocess
 | |
| import shutil
 | |
| from pprint import pprint
 | |
| from pathlib import Path
 | |
| from Crypto.PublicKey import RSA
 | |
| from shutil import copyfile
 | |
| from webfinger import createWebfingerEndpoint
 | |
| from webfinger import storeWebfingerEndpoint
 | |
| from posts import createDMTimeline
 | |
| from posts import createInbox
 | |
| from posts import createOutbox
 | |
| from posts import createModeration
 | |
| from auth import storeBasicCredentials
 | |
| from auth import removePassword
 | |
| from roles import setRole
 | |
| from media import removeMetaData
 | |
| from utils import validNickname
 | |
| from utils import noOfAccounts
 | |
| from auth import createPassword
 | |
| from config import setConfigParam
 | |
| from config import getConfigParam
 | |
| 
 | |
| 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
 | |
| 
 | |
| def setProfileImage(baseDir: str,httpPrefix :str,nickname: str,domain: str, \
 | |
|                     port :int,imageFilename: str,imageType :str,resolution :str) -> bool:
 | |
|     """Saves the given image file as an avatar or background
 | |
|     image for the given person
 | |
|     """
 | |
|     imageFilename=imageFilename.replace('\n','')
 | |
|     if not (imageFilename.endswith('.png') or \
 | |
|             imageFilename.endswith('.jpg') or \
 | |
|             imageFilename.endswith('.jpeg') or \
 | |
|             imageFilename.endswith('.gif')):
 | |
|         print('Profile image must be png, jpg or gif format')
 | |
|         return False
 | |
| 
 | |
|     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)
 | |
| 
 | |
|     handle=nickname.lower()+'@'+domain.lower()
 | |
|     personFilename=baseDir+'/accounts/'+handle+'.json'
 | |
|     if not os.path.isfile(personFilename):
 | |
|         print('person definition not found: '+personFilename)
 | |
|         return False
 | |
|     if not os.path.isdir(baseDir+'/accounts/'+handle):
 | |
|         print('Account not found: '+baseDir+'/accounts/'+handle)
 | |
|         return False
 | |
| 
 | |
|     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
 | |
|         personJson[iconFilenameBase]['url']=httpPrefix+'://'+fullDomain+'/users/'+nickname+'/'+iconFilename
 | |
|         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
 | |
|         subprocess.call(cmd, shell=True)
 | |
|         removeMetaData(profileFilename,profileFilename)
 | |
|         return True
 | |
|     return False
 | |
| 
 | |
| 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)
 | |
| 
 | |
| def createPersonBase(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
 | |
|     """
 | |
|     privateKeyPem,publicKeyPem=generateRSAKey()
 | |
|     webfingerEndpoint= \
 | |
|         createWebfingerEndpoint(nickname,domain,port,httpPrefix,publicKeyPem)
 | |
|     if saveToFile:
 | |
|         storeWebfingerEndpoint(nickname,domain,port,baseDir,webfingerEndpoint)
 | |
| 
 | |
|     handle=nickname.lower()+'@'+domain.lower()
 | |
|     originalDomain=domain
 | |
|     if port:
 | |
|         if port!=80 and port!=443:
 | |
|             if ':' not in domain:
 | |
|                 domain=domain+':'+str(port)
 | |
| 
 | |
|     personType='Person'
 | |
|     approveFollowers=False
 | |
|     personName=nickname
 | |
|     personId=httpPrefix+'://'+domain+'/users/'+nickname
 | |
|     inboxStr=personId+'/inbox'
 | |
|     personUrl=httpPrefix+'://'+domain+'/@'+personName
 | |
|     if nickname=='inbox':
 | |
|         # shared inbox
 | |
|         inboxStr=httpPrefix+'://'+domain+'/actor/inbox'
 | |
|         personId=httpPrefix+'://'+domain+'/actor'
 | |
|         personUrl=httpPrefix+'://'+domain+'/about/more?instance_actor=true'
 | |
|         personName=originalDomain
 | |
|         approveFollowers=True
 | |
|         personType='Application'
 | |
| 
 | |
|     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': [],
 | |
|                  'endpoints': {
 | |
|                      'id': personId+'/endpoints',
 | |
|                      'sharedInbox': httpPrefix+'://'+domain+'/inbox',
 | |
|                  },
 | |
|                  'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
 | |
|                  'followers': personId+'/followers',
 | |
|                  'following': personId+'/following',
 | |
|                  'shares': personId+'/shares',
 | |
|                  'orgSchema': None,
 | |
|                  'skills': {},
 | |
|                  'roles': {},
 | |
|                  'availability': None,
 | |
|                  'icon': {'mediaType': 'image/png',
 | |
|                           'type': 'Image',
 | |
|                           'url': personId+'/avatar.png'},
 | |
|                  'id': personId,
 | |
|                  'image': {'mediaType': 'image/png',
 | |
|                            'type': 'Image',
 | |
|                            'url': personId+'/image.png'},
 | |
|                  'inbox': inboxStr,
 | |
|                  'manuallyApprovesFollowers': approveFollowers,
 | |
|                  'name': personName,
 | |
|                  'outbox': personId+'/outbox',
 | |
|                  'preferredUsername': personName,
 | |
|                  'summary': '',
 | |
|                  'publicKey': {
 | |
|                      'id': personId+'#main-key',
 | |
|                      'owner': personId,
 | |
|                      'publicKeyPem': publicKeyPem
 | |
|                  },
 | |
|                  'tag': [],
 | |
|                  'type': personType,
 | |
|                  'url': personUrl
 | |
|     }
 | |
| 
 | |
|     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']
 | |
| 
 | |
|     if saveToFile:
 | |
|         # save person to file
 | |
|         peopleSubdir='/accounts'
 | |
|         if not os.path.isdir(baseDir+peopleSubdir):
 | |
|             os.mkdir(baseDir+peopleSubdir)
 | |
|         if not os.path.isdir(baseDir+peopleSubdir+'/'+handle):
 | |
|             os.mkdir(baseDir+peopleSubdir+'/'+handle)
 | |
|         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')
 | |
|         filename=baseDir+peopleSubdir+'/'+handle+'.json'
 | |
|         with open(filename, 'w') as fp:
 | |
|             commentjson.dump(newPerson, fp, indent=4, sort_keys=False)
 | |
| 
 | |
|         # 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)
 | |
| 
 | |
|         # 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)
 | |
| 
 | |
|     return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
 | |
| 
 | |
| 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
 | |
|     if not validNickname(domain,nickname):
 | |
|         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
 | |
| 
 | |
| 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(domain,nickname):
 | |
|        return None,None,None,None
 | |
| 
 | |
|     # If a config.json file doesn't exist then don't decrement
 | |
|     # remaining registrations counter
 | |
|     remainingConfigExists=getConfigParam(baseDir,'registrationsRemaining')
 | |
|     if remainingConfigExists:
 | |
|         registrationsRemaining=int(remainingConfigExists)
 | |
|         if registrationsRemaining<=0:
 | |
|             return None,None,None,None
 | |
| 
 | |
|     privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint = \
 | |
|         createPersonBase(baseDir,nickname,domain,port,httpPrefix,saveToFile,password)
 | |
|     if noOfAccounts(baseDir)==1:
 | |
|         #print(nickname+' becomes the instance admin and a moderator')
 | |
|         setRole(baseDir,nickname,domain,'instance','admin')
 | |
|         setRole(baseDir,nickname,domain,'instance','moderator')
 | |
|         setRole(baseDir,nickname,domain,'instance','delegator')
 | |
|         setConfigParam(baseDir,'admin',nickname)
 | |
| 
 | |
|     if not os.path.isdir(baseDir+'/accounts'):
 | |
|         os.mkdir(baseDir+'/accounts')
 | |
|     if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
 | |
|         os.mkdir(baseDir+'/accounts/'+nickname+'@'+domain)
 | |
|     
 | |
|     if os.path.isfile(baseDir+'/img/default-avatar.png'):
 | |
|         copyfile(baseDir+'/img/default-avatar.png',baseDir+'/accounts/'+nickname+'@'+domain+'/avatar.png')
 | |
|     if os.path.isfile(baseDir+'/img/image.png'):
 | |
|         copyfile(baseDir+'/img/image.png',baseDir+'/accounts/'+nickname+'@'+domain+'/image.png')
 | |
|     if os.path.isfile(baseDir+'/img/banner.png'):
 | |
|         copyfile(baseDir+'/img/banner.png',baseDir+'/accounts/'+nickname+'@'+domain+'/banner.png')
 | |
|     if remainingConfigExists:
 | |
|         registrationsRemaining-=1
 | |
|         setConfigParam(baseDir,'registrationsRemaining',str(registrationsRemaining))
 | |
|     return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
 | |
| 
 | |
| 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)
 | |
|     
 | |
| def personLookup(domain: str,path: str,baseDir: str) -> {}:
 | |
|     """Lookup the person for an given nickname
 | |
|     """
 | |
|     if path.endswith('#main-key'):
 | |
|         path=path.replace('#main-key','')
 | |
|     # is this a shared inbox lookup?
 | |
|     isSharedInbox=False
 | |
|     if path=='/inbox' or path=='/users/inbox' or path=='/sharedInbox':
 | |
|         # shared inbox actor on @domain@domain
 | |
|         path='/users/'+domain
 | |
|         isSharedInbox=True
 | |
|     else:
 | |
|         notPersonLookup=['/inbox','/outbox','/outboxarchive', \
 | |
|                          '/followers','/following','/featured', \
 | |
|                          '.png','.jpg','.gif','.mpv']
 | |
|         for ending in notPersonLookup:        
 | |
|             if path.endswith(ending):
 | |
|                 return None
 | |
|     nickname=None
 | |
|     if path.startswith('/users/'):
 | |
|         nickname=path.replace('/users/','',1)
 | |
|     if path.startswith('/@'):
 | |
|         nickname=path.replace('/@','',1)
 | |
|     if not nickname:
 | |
|         return None
 | |
|     if not isSharedInbox and not validNickname(domain,nickname):
 | |
|         return None
 | |
|     if ':' in domain:
 | |
|         domain=domain.split(':')[0]
 | |
|     handle=nickname+'@'+domain
 | |
|     filename=baseDir+'/accounts/'+handle+'.json'
 | |
|     if not os.path.isfile(filename):
 | |
|         return None
 | |
|     personJson={"user": "unknown"}
 | |
|     try:
 | |
|         with open(filename, 'r') as fp:
 | |
|             personJson=commentjson.load(fp)
 | |
|     except:
 | |
|         print('WARN: Failed to load actor '+filename)
 | |
|         return None
 | |
|     return personJson
 | |
| 
 | |
| def personBoxJson(baseDir: str,domain: str,port: int,path: str, \
 | |
|                   httpPrefix: str,noOfItems: int,boxname: str, \
 | |
|                   authorized: bool,ocapAlways: bool) -> []:
 | |
|     """Obtain the inbox/outbox/moderation feed for the given person
 | |
|     """
 | |
|     if boxname!='inbox' and boxname!='dm' and \
 | |
|        boxname!='outbox' and boxname!='moderation':
 | |
|         return None
 | |
| 
 | |
|     if not '/'+boxname 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('/'+boxname):
 | |
|         return None
 | |
|     nickname=None
 | |
|     if path.startswith('/users/'):
 | |
|         nickname=path.replace('/users/','',1).replace('/'+boxname,'')
 | |
|     if path.startswith('/@'):
 | |
|         nickname=path.replace('/@','',1).replace('/'+boxname,'')
 | |
|     if not nickname:
 | |
|         return None
 | |
|     if not validNickname(domain,nickname):
 | |
|         return None
 | |
|     if boxname=='inbox':
 | |
|         return createInbox(baseDir,nickname,domain,port,httpPrefix, \
 | |
|                            noOfItems,headerOnly,ocapAlways,pageNumber)
 | |
|     if boxname=='dm':
 | |
|         return createDMTimeline(baseDir,nickname,domain,port,httpPrefix, \
 | |
|                                 noOfItems,headerOnly,ocapAlways,pageNumber)
 | |
|     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
 | |
| 
 | |
| def personInboxJson(baseDir: str,domain: str,port: int,path: str, \
 | |
|                     httpPrefix: str,noOfItems: int,ocapAlways: bool) -> []:
 | |
|     """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(domain,nickname):
 | |
|         return None
 | |
|     return createInbox(baseDir,nickname,domain,port,httpPrefix, \
 | |
|                        noOfItems,headerOnly,ocapAlways,pageNumber)
 | |
| 
 | |
| def setDisplayNickname(baseDir: str,nickname: str, domain: str, \
 | |
|                        displayName: str) -> bool:
 | |
|     if len(displayName)>32:
 | |
|         return False
 | |
|     handle=nickname.lower()+'@'+domain.lower()
 | |
|     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
 | |
|     with open(filename, 'w') as fp:
 | |
|         commentjson.dump(personJson, fp, indent=4, sort_keys=False)
 | |
|     return True
 | |
| 
 | |
| def setBio(baseDir: str,nickname: str, domain: str, bio: str) -> bool:
 | |
|     if len(bio)>32:
 | |
|         return False
 | |
|     handle=nickname.lower()+'@'+domain.lower()
 | |
|     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('summary'):
 | |
|         return False
 | |
|     personJson['summary']=bio
 | |
|     with open(filename, 'w') as fp:
 | |
|         commentjson.dump(personJson, fp, indent=4, sort_keys=False)
 | |
|     return True
 | |
| 
 | |
| 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)            
 | |
| 
 | |
| 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
 | |
| 
 | |
| 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)
 | |
|     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:
 | |
|     """Removes an account
 | |
|     """    
 | |
|     # Don't remove the admin
 | |
|     adminNickname=getConfigParam(baseDir,'admin')
 | |
|     if nickname==adminNickname:
 | |
|         return False
 | |
| 
 | |
|     # Don't remove 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 False
 | |
| 
 | |
|     unsuspendAccount(baseDir,nickname)
 | |
|     handle=nickname+'@'+domain
 | |
|     removePassword(baseDir,nickname)
 | |
|     removeTagsForNickname(baseDir,nickname,domain,port)
 | |
|     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
 |