epicyon/person.py

913 lines
35 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "person.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2019-12-14 10:52:19 +00:00
__version__ = "1.1.0"
2019-06-28 18:55:29 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
2019-10-11 18:03:58 +00:00
import time
2019-06-28 18:55:29 +00:00
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 random import randint
2019-07-12 14:31:56 +00:00
from pathlib import Path
2020-03-04 09:59:08 +00:00
try:
from Cryptodome.PublicKey import RSA
except ImportError:
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-09-28 11:40:42 +00:00
from posts import createMediaTimeline
2020-02-24 14:39:25 +00:00
from posts import createBlogsTimeline
2019-11-17 14:01:49 +00:00
from posts import createBookmarksTimeline
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-10-22 11:55:06 +00:00
from utils import loadJson
from utils import saveJson
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
2019-10-22 11:55:06 +00:00
personJson=loadJson(personFilename)
2019-09-30 22:39:02 +00:00
if personJson:
2019-07-12 13:51:04 +00:00
personJson[iconFilenameBase]['mediaType']=mediaType
2019-07-12 14:31:56 +00:00
personJson[iconFilenameBase]['url']=httpPrefix+'://'+fullDomain+'/users/'+nickname+'/'+iconFilename
2019-10-22 11:55:06 +00:00
saveJson(personJson,personFilename)
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
2019-09-30 22:39:02 +00:00
2019-10-22 11:55:06 +00:00
actorJson=loadJson(actorFilename)
2019-09-30 22:39:02 +00:00
if actorJson:
actorJson['orgSchema']=schema
2019-10-22 11:55:06 +00:00
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]
2019-11-05 12:29:53 +00:00
return os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain) or \
os.path.isdir(baseDir+'/deactivated/'+nickname+'@'+domain)
def randomizeActorImages(personJson: {}) -> None:
"""Randomizes the filenames for avatar image and background
This causes other instances to update their cached avatar image
"""
personId=personJson['id']
2020-01-19 23:39:00 +00:00
lastPartOfFilename=personJson['icon']['url'].split('/')[-1]
existingExtension=lastPartOfFilename.split('.')[1]
personJson['icon']['url']=personId+'/avatar'+str(randint(10000000000000,99999999999999))+'.'+existingExtension
lastPartOfFilename=personJson['image']['url'].split('/')[-1]
existingExtension=lastPartOfFilename.split('.')[1]
personJson['image']['url']=personId+'/image'+str(randint(10000000000000,99999999999999))+'.'+existingExtension
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-11-06 22:02:08 +00:00
'alsoKnownAs': [],
'discoverable': False,
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'+str(randint(10000000000000,99999999999999))+'.png'},
'id': personId,
2019-06-28 18:55:29 +00:00
'image': {'mediaType': 'image/png',
'type': 'Image',
'url': personId+'/image'+str(randint(10000000000000,99999999999999))+'.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,
2020-01-19 20:19:56 +00:00
'url': personUrl,
'nomadicLocations': [{
'id': personId,
'type': 'nomadicLocation',
'locationAddress':'acct:'+nickname+'@'+domain,
'locationPrimary':True,
'locationDeleted':False
}]
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'
2019-10-22 11:55:06 +00:00
saveJson(newPerson,filename)
2019-06-28 18:55:29 +00:00
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'
2019-10-22 11:55:06 +00:00
saveJson(newPerson,cacheFilename)
2019-08-22 14:43:43 +00:00
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-10-04 12:39:46 +00:00
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
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)
2020-01-19 20:29:39 +00:00
2020-01-19 20:58:50 +00:00
def personUpgradeActor(baseDir: str,personJson: {},handle: str,filename: str) -> None:
2020-01-19 20:29:39 +00:00
"""Alter the actor to add any new properties
2020-01-19 20:43:03 +00:00
"""
2020-01-19 20:58:50 +00:00
updateActor=False
2020-01-19 20:43:03 +00:00
if not os.path.isfile(filename):
print('WARN: actor file not found '+filename)
return
2020-01-19 20:42:03 +00:00
if not personJson:
personJson=loadJson(filename)
2020-01-19 20:29:39 +00:00
if not personJson.get('nomadicLocations'):
personJson['nomadicLocations']=[{
'id': personJson['id'],
'type': 'nomadicLocation',
'locationAddress':'acct:'+handle,
'locationPrimary':True,
'locationDeleted':False
}]
print('Nomadic locations added to to actor '+handle)
2020-01-19 20:58:50 +00:00
updateActor=True
if updateActor:
saveJson(personJson,filename)
# also update the actor within the cache
actorCacheFilename= \
baseDir+'/accounts/cache/actors/'+ \
personJson['id'].replace('/','#')+'.json'
if os.path.isfile(actorCacheFilename):
saveJson(personJson,actorCacheFilename)
# update domain/@nickname in actors cache
actorCacheFilename= \
baseDir+'/accounts/cache/actors/'+ \
personJson['id'].replace('/users/','/@').replace('/','#')+'.json'
if os.path.isfile(actorCacheFilename):
saveJson(personJson,actorCacheFilename)
2020-01-19 20:29:39 +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
2019-10-22 11:55:06 +00:00
personJson=loadJson(filename)
2020-01-19 20:58:50 +00:00
personUpgradeActor(baseDir,personJson,handle,filename)
2019-10-22 11:55:06 +00:00
#if not personJson:
# personJson={"user": "unknown"}
2019-06-28 18:55:29 +00:00
return personJson
2019-06-29 14:35:26 +00:00
def personBoxJson(recentPostsCache: {}, \
session,baseDir: str,domain: str,port: int,path: str, \
httpPrefix: str,noOfItems: int,boxname: str, \
2020-02-25 13:35:41 +00:00
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-28 11:40:42 +00:00
boxname!='tlreplies' and boxname!='tlmedia' and \
2020-02-24 14:39:25 +00:00
boxname!='tlblogs' and \
2019-11-17 14:01:49 +00:00
boxname!='outbox' and boxname!='moderation' and \
boxname!='tlbookmarks':
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(recentPostsCache, \
session,baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-11-17 14:01:49 +00:00
elif boxname=='dm':
2019-09-28 16:21:43 +00:00
return createDMTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
2019-08-25 16:09:56 +00:00
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-11-17 14:01:49 +00:00
elif boxname=='tlbookmarks':
return createBookmarksTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-09-23 20:43:18 +00:00
elif boxname=='tlreplies':
2019-09-28 16:21:43 +00:00
return createRepliesTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
2019-09-23 19:53:18 +00:00
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-09-28 11:40:42 +00:00
elif boxname=='tlmedia':
2019-09-28 16:21:43 +00:00
return createMediaTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
2020-02-24 14:39:25 +00:00
noOfItems,headerOnly,ocapAlways,pageNumber)
elif boxname=='tlblogs':
return createBlogsTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
2019-09-28 11:40:42 +00:00
noOfItems,headerOnly,ocapAlways,pageNumber)
2019-08-12 13:22:17 +00:00
elif boxname=='outbox':
2019-09-28 16:21:43 +00:00
return createOutbox(session,baseDir,nickname,domain,port,httpPrefix, \
2019-08-12 13:22:17 +00:00
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
def personInboxJson(recentPostsCache: {}, \
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(recentPostsCache,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
2019-09-30 22:39:02 +00:00
2019-10-22 11:55:06 +00:00
personJson=loadJson(filename)
2019-06-28 18:55:29 +00:00
if not personJson:
return False
personJson['name']=displayName
2019-10-22 11:55:06 +00:00
saveJson(personJson,filename)
2019-06-28 18:55:29 +00:00
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
2019-09-30 22:39:02 +00:00
2019-10-22 11:55:06 +00:00
personJson=loadJson(filename)
2019-06-28 20:00:25 +00:00
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-09-30 22:39:02 +00:00
2019-10-22 11:55:06 +00:00
saveJson(personJson,filename)
2019-06-28 20:00:25 +00:00
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,domain: str) -> None:
2019-08-13 09:24:55 +00:00
"""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)
2019-08-13 09:24:55 +00:00
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()
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/')
2019-09-27 12:09:04 +00:00
for f in os.scandir(directory):
f=f.name
2019-08-13 12:14:11 +00:00
filename = os.fsdecode(f)
if not filename.endswith(".txt"):
continue
2019-11-27 09:56:14 +00:00
tagFilename=os.path.join(directory,filename)
2019-11-27 09:51:59 +00:00
if not os.path.isfile(tagFilename):
continue
2019-08-13 12:14:11 +00:00
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-11-05 10:28:46 +00:00
if os.path.isdir(baseDir+'/deactivated/'+handle):
shutil.rmtree(baseDir+'/deactivated/'+handle)
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)
2019-11-05 12:13:41 +00:00
if os.path.isfile(baseDir+'/wfdeactivated/'+handle+'.json'):
2019-11-05 12:12:30 +00:00
os.remove(baseDir+'/wfdeactivated/'+handle+'.json')
if os.path.isdir(baseDir+'/sharefilesdeactivated/'+nickname):
shutil.rmtree(baseDir+'/sharefilesdeactivated/'+nickname)
2019-08-13 11:59:38 +00:00
return True
2019-11-05 10:37:37 +00:00
def deactivateAccount(baseDir: str,nickname: str,domain: str) -> bool:
"""Makes an account temporarily unavailable
"""
2019-11-05 12:07:18 +00:00
handle=nickname+'@'+domain
accountDir=baseDir+'/accounts/'+handle
if not os.path.isdir(accountDir):
2019-11-05 10:37:37 +00:00
return False
deactivatedDir=baseDir+'/deactivated'
if not os.path.isdir(deactivatedDir):
os.mkdir(deactivatedDir)
2019-11-05 12:07:18 +00:00
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)
2019-11-05 10:37:37 +00:00
return os.path.isdir(deactivatedDir+'/'+nickname+'@'+domain)
def activateAccount(baseDir: str,nickname: str,domain: str) -> None:
"""Makes a deactivated account available
"""
2019-11-05 12:07:18 +00:00
handle=nickname+'@'+domain
deactivatedDir=baseDir+'/deactivated'
2019-11-05 12:07:18 +00:00
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)
2019-11-06 11:39:41 +00:00
2019-11-06 11:59:13 +00:00
def isPersonSnoozed(baseDir: str,nickname: str,domain: str,snoozeActor: str) -> bool:
2019-11-06 11:39:41 +00:00
"""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'
2019-11-06 11:57:43 +00:00
if os.path.isfile(snoozedFilename):
if snoozeActor+' ' in open(snoozedFilename).read():
return
2019-11-06 11:39:41 +00:00
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()