forked from indymedia/epicyon
874 lines
33 KiB
Python
874 lines
33 KiB
Python
__filename__ = "person.py"
|
|
__author__ = "Bob Mottram"
|
|
__license__ = "AGPL3+"
|
|
__version__ = "1.1.0"
|
|
__maintainer__ = "Bob Mottram"
|
|
__email__ = "bob@freedombone.net"
|
|
__status__ = "Production"
|
|
|
|
import json
|
|
import time
|
|
import os
|
|
import fileinput
|
|
import subprocess
|
|
import shutil
|
|
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 createRepliesTimeline
|
|
from posts import createMediaTimeline
|
|
from posts import createBookmarksTimeline
|
|
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 utils import loadJson
|
|
from utils import saveJson
|
|
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
|
|
|
|
personJson=loadJson(personFilename)
|
|
if personJson:
|
|
personJson[iconFilenameBase]['mediaType']=mediaType
|
|
personJson[iconFilenameBase]['url']=httpPrefix+'://'+fullDomain+'/users/'+nickname+'/'+iconFilename
|
|
saveJson(personJson,personFilename)
|
|
|
|
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
|
|
|
|
actorJson=loadJson(actorFilename)
|
|
if actorJson:
|
|
actorJson['orgSchema']=schema
|
|
saveJson(actorJson,actorFilename)
|
|
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) or \
|
|
os.path.isdir(baseDir+'/deactivated/'+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': [],
|
|
'alsoKnownAs': [],
|
|
'discoverable': False,
|
|
'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,
|
|
'nomadicLocations': [{
|
|
'id': personId,
|
|
'type': 'nomadicLocation',
|
|
'locationAddress':'acct:'+nickname+'@'+domain,
|
|
'locationPrimary':True,
|
|
'locationDeleted':False
|
|
}]
|
|
}
|
|
|
|
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'
|
|
saveJson(newPerson,filename)
|
|
|
|
# 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'
|
|
saveJson(newPerson,cacheFilename)
|
|
|
|
# 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 createGroup(baseDir: str,nickname: str,domain: str,port: int, \
|
|
httpPrefix: str, saveToFile: bool,password=None) -> (str,str,{},{}):
|
|
"""Returns a group
|
|
"""
|
|
privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint= \
|
|
createPerson(baseDir,nickname,domain,port, \
|
|
httpPrefix,saveToFile,password)
|
|
newPerson['type']='Group'
|
|
return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
|
|
|
|
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')
|
|
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')
|
|
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 personUpgradeActor(personJson: {},handle: str,filename: str) -> None:
|
|
"""Alter the actor to add any new properties
|
|
"""
|
|
if not os.path.isfile(filename):
|
|
print('WARN: actor file not found '+filename)
|
|
return
|
|
if not personJson:
|
|
personJson=loadJson(filename)
|
|
if not personJson.get('nomadicLocations'):
|
|
personJson['nomadicLocations']=[{
|
|
'id': personJson['id'],
|
|
'type': 'nomadicLocation',
|
|
'locationAddress':'acct:'+handle,
|
|
'locationPrimary':True,
|
|
'locationDeleted':False
|
|
}]
|
|
saveJson(personJson,filename)
|
|
print('Nomadic locations added to to actor '+handle)
|
|
|
|
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=loadJson(filename)
|
|
personUpgradeActor(personJson,handle,filename)
|
|
#if not personJson:
|
|
# personJson={"user": "unknown"}
|
|
return personJson
|
|
|
|
def personBoxJson(recentPostsCache: {}, \
|
|
session,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!='tlreplies' and boxname!='tlmedia' and \
|
|
boxname!='outbox' and boxname!='moderation' and \
|
|
boxname!='tlbookmarks':
|
|
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(recentPostsCache, \
|
|
session,baseDir,nickname,domain,port,httpPrefix, \
|
|
noOfItems,headerOnly,ocapAlways,pageNumber)
|
|
elif boxname=='dm':
|
|
return createDMTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
|
|
noOfItems,headerOnly,ocapAlways,pageNumber)
|
|
elif boxname=='tlbookmarks':
|
|
return createBookmarksTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
|
|
noOfItems,headerOnly,ocapAlways,pageNumber)
|
|
elif boxname=='tlreplies':
|
|
return createRepliesTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
|
|
noOfItems,headerOnly,ocapAlways,pageNumber)
|
|
elif boxname=='tlmedia':
|
|
return createMediaTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
|
|
noOfItems,headerOnly,ocapAlways,pageNumber)
|
|
elif boxname=='outbox':
|
|
return createOutbox(session,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(recentPostsCache: {}, \
|
|
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(recentPostsCache,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=loadJson(filename)
|
|
if not personJson:
|
|
return False
|
|
personJson['name']=displayName
|
|
saveJson(personJson,filename)
|
|
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=loadJson(filename)
|
|
if not personJson:
|
|
return False
|
|
if not personJson.get('summary'):
|
|
return False
|
|
personJson['summary']=bio
|
|
|
|
saveJson(personJson,filename)
|
|
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,domain: str) -> None:
|
|
"""Suspends the given account
|
|
"""
|
|
# 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
|
|
|
|
saltFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/.salt'
|
|
if os.path.isfile(saltFilename):
|
|
os.remove(saltFilename)
|
|
tokenFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/.token'
|
|
if os.path.isfile(tokenFilename):
|
|
os.remove(tokenFilename)
|
|
|
|
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()
|
|
else:
|
|
suspendedFile=open(suspendedFilename,'w+')
|
|
if suspendedFile:
|
|
suspendedFile.write(nickname+'\n')
|
|
suspendedFile.close()
|
|
|
|
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.scandir(directory):
|
|
f=f.name
|
|
filename = os.fsdecode(f)
|
|
if not filename.endswith(".txt"):
|
|
continue
|
|
tagFilename=os.path.join(directory,filename)
|
|
if not os.path.isfile(tagFilename):
|
|
continue
|
|
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+'/deactivated/'+handle):
|
|
shutil.rmtree(baseDir+'/deactivated/'+handle)
|
|
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)
|
|
if os.path.isfile(baseDir+'/wfdeactivated/'+handle+'.json'):
|
|
os.remove(baseDir+'/wfdeactivated/'+handle+'.json')
|
|
if os.path.isdir(baseDir+'/sharefilesdeactivated/'+nickname):
|
|
shutil.rmtree(baseDir+'/sharefilesdeactivated/'+nickname)
|
|
return True
|
|
|
|
def deactivateAccount(baseDir: str,nickname: str,domain: str) -> bool:
|
|
"""Makes an account temporarily unavailable
|
|
"""
|
|
handle=nickname+'@'+domain
|
|
|
|
accountDir=baseDir+'/accounts/'+handle
|
|
if not os.path.isdir(accountDir):
|
|
return False
|
|
deactivatedDir=baseDir+'/deactivated'
|
|
if not os.path.isdir(deactivatedDir):
|
|
os.mkdir(deactivatedDir)
|
|
shutil.move(accountDir,deactivatedDir+'/'+handle)
|
|
|
|
if os.path.isfile(baseDir+'/wfendpoints/'+handle+'.json'):
|
|
deactivatedWebfingerDir=baseDir+'/wfdeactivated'
|
|
if not os.path.isdir(deactivatedWebfingerDir):
|
|
os.mkdir(deactivatedWebfingerDir)
|
|
shutil.move(baseDir+'/wfendpoints/'+handle+'.json',deactivatedWebfingerDir+'/'+handle+'.json')
|
|
|
|
if os.path.isdir(baseDir+'/sharefiles/'+nickname):
|
|
deactivatedSharefilesDir=baseDir+'/sharefilesdeactivated'
|
|
if not os.path.isdir(deactivatedSharefilesDir):
|
|
os.mkdir(deactivatedSharefilesDir)
|
|
shutil.move(baseDir+'/sharefiles/'+nickname,deactivatedSharefilesDir+'/'+nickname)
|
|
return os.path.isdir(deactivatedDir+'/'+nickname+'@'+domain)
|
|
|
|
def activateAccount(baseDir: str,nickname: str,domain: str) -> None:
|
|
"""Makes a deactivated account available
|
|
"""
|
|
handle=nickname+'@'+domain
|
|
|
|
deactivatedDir=baseDir+'/deactivated'
|
|
deactivatedAccountDir=deactivatedDir+'/'+handle
|
|
if os.path.isdir(deactivatedAccountDir):
|
|
accountDir=baseDir+'/accounts/'+handle
|
|
if not os.path.isdir(accountDir):
|
|
shutil.move(deactivatedAccountDir,accountDir)
|
|
|
|
deactivatedWebfingerDir=baseDir+'/wfdeactivated'
|
|
if os.path.isfile(deactivatedWebfingerDir+'/'+handle+'.json'):
|
|
shutil.move(deactivatedWebfingerDir+'/'+handle+'.json',baseDir+'/wfendpoints/'+handle+'.json')
|
|
|
|
deactivatedSharefilesDir=baseDir+'/sharefilesdeactivated'
|
|
if os.path.isdir(deactivatedSharefilesDir+'/'+nickname):
|
|
if not os.path.isdir(baseDir+'/sharefiles/'+nickname):
|
|
shutil.move(deactivatedSharefilesDir+'/'+nickname,baseDir+'/sharefiles/'+nickname)
|
|
|
|
def isPersonSnoozed(baseDir: str,nickname: str,domain: str,snoozeActor: str) -> bool:
|
|
"""Returns true if the given actor is snoozed
|
|
"""
|
|
snoozedFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/snoozed.txt'
|
|
if not os.path.isfile(snoozedFilename):
|
|
return False
|
|
if snoozeActor+' ' not in open(snoozedFilename).read():
|
|
return False
|
|
# remove the snooze entry if it has timed out
|
|
replaceStr=None
|
|
with open(snoozedFilename, 'r') as snoozedFile:
|
|
for line in snoozedFile:
|
|
# is this the entry for the actor?
|
|
if line.startswith(snoozeActor+' '):
|
|
snoozedTimeStr=line.split(' ')[1].replace('\n','')
|
|
# is there a time appended?
|
|
if snoozedTimeStr.isdigit():
|
|
snoozedTime=int(snoozedTimeStr)
|
|
currTime=int(time.time())
|
|
# has the snooze timed out?
|
|
if int(currTime-snoozedTime)>60*60*24:
|
|
replaceStr=line
|
|
else:
|
|
replaceStr=line
|
|
break
|
|
if replaceStr:
|
|
content=None
|
|
with open(snoozedFilename, 'r') as snoozedFile:
|
|
content=snoozedFile.read().replace(replaceStr,'')
|
|
if content:
|
|
writeSnoozedFile=open(snoozedFilename, 'w')
|
|
if writeSnoozedFile:
|
|
writeSnoozedFile.write(content)
|
|
writeSnoozedFile.close()
|
|
|
|
if snoozeActor+' ' in open(snoozedFilename).read():
|
|
return True
|
|
return False
|
|
|
|
def personSnooze(baseDir: str,nickname: str,domain: str,snoozeActor: str) -> None:
|
|
"""Temporarily ignores the given actor
|
|
"""
|
|
accountDir=baseDir+'/accounts/'+nickname+'@'+domain
|
|
if not os.path.isdir(accountDir):
|
|
print('ERROR: unknown account '+accountDir)
|
|
return
|
|
snoozedFilename=accountDir+'/snoozed.txt'
|
|
if os.path.isfile(snoozedFilename):
|
|
if snoozeActor+' ' in open(snoozedFilename).read():
|
|
return
|
|
snoozedFile=open(snoozedFilename, "a+")
|
|
if snoozedFile:
|
|
snoozedFile.write(snoozeActor+' '+str(int(time.time()))+'\n')
|
|
snoozedFile.close()
|
|
|
|
def personUnsnooze(baseDir: str,nickname: str,domain: str,snoozeActor: str) -> None:
|
|
"""Undoes a temporarily ignore of the given actor
|
|
"""
|
|
accountDir=baseDir+'/accounts/'+nickname+'@'+domain
|
|
if not os.path.isdir(accountDir):
|
|
print('ERROR: unknown account '+accountDir)
|
|
return
|
|
snoozedFilename=accountDir+'/snoozed.txt'
|
|
if not os.path.isfile(snoozedFilename):
|
|
return
|
|
if snoozeActor+' ' not in open(snoozedFilename).read():
|
|
return
|
|
replaceStr=None
|
|
with open(snoozedFilename, 'r') as snoozedFile:
|
|
for line in snoozedFile:
|
|
if line.startswith(snoozeActor+' '):
|
|
replaceStr=line
|
|
break
|
|
if replaceStr:
|
|
content=None
|
|
with open(snoozedFilename, 'r') as snoozedFile:
|
|
content=snoozedFile.read().replace(replaceStr,'')
|
|
if content:
|
|
writeSnoozedFile=open(snoozedFilename, 'w')
|
|
if writeSnoozedFile:
|
|
writeSnoozedFile.write(content)
|
|
writeSnoozedFile.close()
|