epicyon/person.py

1397 lines
51 KiB
Python
Raw Normal View History

2020-04-03 18:12:08 +00:00
__filename__ = "person.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-04-03 18:12:08 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "ActivityPub"
2020-04-03 18:12:08 +00:00
2019-10-11 18:03:58 +00:00
import time
2019-06-28 18:55:29 +00:00
import os
2019-07-12 14:31:56 +00:00
import subprocess
2019-08-13 11:59:38 +00:00
import shutil
import pyqrcode
from random import randint
2019-07-12 14:31:56 +00:00
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
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
2021-03-11 18:15:04 +00:00
from posts import getUserUrl
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-10-07 09:10:42 +00:00
from posts import createNewsTimeline
2020-02-24 14:39:25 +00:00
from posts import createBlogsTimeline
2020-11-27 12:29:20 +00:00
from posts import createFeaturesTimeline
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
2021-05-16 15:10:39 +00:00
from roles import setRolesFromList
from roles import getActorRolesList
2021-05-09 12:17:55 +00:00
from media import processMetaData
from utils import removeLineEndings
2021-06-26 14:21:24 +00:00
from utils import removeDomainPort
2021-05-08 17:13:46 +00:00
from utils import getStatusNumber
2020-12-16 11:04:46 +00:00
from utils import getFullDomain
2019-07-27 22:48:34 +00:00
from utils import validNickname
2019-10-22 11:55:06 +00:00
from utils import loadJson
from utils import saveJson
2020-10-06 08:58:44 +00:00
from utils import setConfigParam
from utils import getConfigParam
from utils import refreshNewswire
2021-03-11 18:15:04 +00:00
from utils import getProtocolPrefixes
from utils import hasUsersPath
2021-06-25 14:33:16 +00:00
from utils import getImageExtensions
2021-07-04 11:15:26 +00:00
from utils import isImageFile
2021-07-13 21:59:53 +00:00
from utils import acctDir
from utils import getUserPaths
2021-07-30 13:00:23 +00:00
from utils import getGroupPaths
2021-03-11 18:15:04 +00:00
from session import createSession
from session import getJson
from webfinger import webfingerHandle
from pprint import pprint
2021-06-25 14:33:16 +00:00
from cache import getPersonFromCache
2019-06-28 18:55:29 +00:00
2020-04-03 18:12:08 +00:00
def generateRSAKey() -> (str, str):
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
privateKeyPem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
pubkey = key.public_key()
publicKeyPem = pubkey.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
privateKeyPem = privateKeyPem.decode("utf-8")
publicKeyPem = publicKeyPem.decode("utf-8")
2020-04-03 18:12:08 +00:00
return privateKeyPem, publicKeyPem
def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
port: int, imageFilename: str, imageType: str,
2021-05-09 19:11:05 +00:00
resolution: str, city: 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
"""
2020-05-22 11:32:38 +00:00
imageFilename = imageFilename.replace('\n', '').replace('\r', '')
2021-07-04 11:15:26 +00:00
if not isImageFile(imageFilename):
2021-01-11 22:27:57 +00:00
print('Profile image must be png, jpg, gif or svg format')
2019-07-12 14:31:56 +00:00
return False
2019-07-12 13:51:04 +00:00
2019-07-12 14:31:56 +00:00
if imageFilename.startswith('~/'):
2020-04-03 18:12:08 +00:00
imageFilename = imageFilename.replace('~/', str(Path.home()) + '/')
2019-07-12 14:31:56 +00:00
domain = removeDomainPort(domain)
2020-12-16 11:04:46 +00:00
fullDomain = getFullDomain(domain, port)
2019-07-12 13:51:04 +00:00
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
2020-04-03 18:12:08 +00:00
personFilename = baseDir + '/accounts/' + handle + '.json'
2019-07-12 13:51:04 +00:00
if not os.path.isfile(personFilename):
2020-04-03 18:12:08 +00:00
print('person definition not found: ' + personFilename)
2019-07-12 14:31:56 +00:00
return False
2020-04-03 18:12:08 +00:00
if not os.path.isdir(baseDir + '/accounts/' + handle):
print('Account not found: ' + baseDir + '/accounts/' + handle)
2019-07-12 14:31:56 +00:00
return False
2019-07-12 13:51:04 +00:00
2020-04-03 18:12:08 +00:00
iconFilenameBase = 'icon'
if imageType == 'avatar' or imageType == 'icon':
iconFilenameBase = 'icon'
2019-07-12 13:51:04 +00:00
else:
2020-04-03 18:12:08 +00:00
iconFilenameBase = 'image'
2020-03-22 21:16:02 +00:00
2020-04-03 18:12:08 +00:00
mediaType = 'image/png'
iconFilename = iconFilenameBase + '.png'
2019-07-12 13:51:04 +00:00
if imageFilename.endswith('.jpg') or \
imageFilename.endswith('.jpeg'):
2020-04-03 18:12:08 +00:00
mediaType = 'image/jpeg'
iconFilename = iconFilenameBase + '.jpg'
2021-07-04 11:39:13 +00:00
elif imageFilename.endswith('.gif'):
2020-04-03 18:12:08 +00:00
mediaType = 'image/gif'
iconFilename = iconFilenameBase + '.gif'
2021-07-04 11:39:13 +00:00
elif imageFilename.endswith('.webp'):
mediaType = 'image/webp'
iconFilename = iconFilenameBase + '.webp'
elif imageFilename.endswith('.avif'):
mediaType = 'image/avif'
iconFilename = iconFilenameBase + '.avif'
elif imageFilename.endswith('.svg'):
2021-01-11 22:27:57 +00:00
mediaType = 'image/svg+xml'
iconFilename = iconFilenameBase + '.svg'
2020-04-03 18:12:08 +00:00
profileFilename = baseDir + '/accounts/' + handle + '/' + iconFilename
2019-07-12 13:51:04 +00:00
2020-04-03 18:12:08 +00:00
personJson = loadJson(personFilename)
2019-09-30 22:39:02 +00:00
if personJson:
2020-04-03 18:12:08 +00:00
personJson[iconFilenameBase]['mediaType'] = mediaType
personJson[iconFilenameBase]['url'] = \
httpPrefix + '://' + fullDomain + '/users/' + \
2021-06-22 12:42:52 +00:00
nickname + '/' + iconFilename
2020-04-03 18:12:08 +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)
2021-05-10 10:46:45 +00:00
processMetaData(baseDir, nickname, domain,
2021-05-09 19:11:05 +00:00
profileFilename, profileFilename, city)
2019-07-12 14:31:56 +00:00
return True
return False
2019-07-12 13:51:04 +00:00
2020-04-03 18:12:08 +00:00
def _accountExists(baseDir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account exists
"""
domain = removeDomainPort(domain)
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
return os.path.isdir(accountDir) or \
2020-04-03 18:12:08 +00:00
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
"""
2020-04-03 18:12:08 +00:00
personId = personJson['id']
lastPartOfFilename = personJson['icon']['url'].split('/')[-1]
existingExtension = lastPartOfFilename.split('.')[1]
2020-07-08 15:09:27 +00:00
# NOTE: these files don't need to have cryptographically
# secure names
2020-07-08 15:17:00 +00:00
randStr = str(randint(10000000000000, 99999999999999)) # nosec
baseUrl = personId.split('/users/')[0]
nickname = personJson['preferredUsername']
2020-04-03 18:12:08 +00:00
personJson['icon']['url'] = \
baseUrl + '/accounts/avatars/' + nickname + \
'/avatar' + randStr + '.' + existingExtension
2020-04-03 18:12:08 +00:00
lastPartOfFilename = personJson['image']['url'].split('/')[-1]
existingExtension = lastPartOfFilename.split('.')[1]
2020-07-08 15:17:00 +00:00
randStr = str(randint(10000000000000, 99999999999999)) # nosec
2020-04-03 18:12:08 +00:00
personJson['image']['url'] = \
baseUrl + '/accounts/headers/' + nickname + \
'/image' + randStr + '.' + existingExtension
2020-04-03 18:12:08 +00:00
2020-08-05 10:28:54 +00:00
def getDefaultPersonContext() -> str:
2020-08-05 10:33:47 +00:00
"""Gets the default actor context
"""
2020-08-05 10:28:54 +00:00
return {
'Curve25519Key': 'toot:Curve25519Key',
'Device': 'toot:Device',
'Ed25519Key': 'toot:Ed25519Key',
'Ed25519Signature': 'toot:Ed25519Signature',
'EncryptedMessage': 'toot:EncryptedMessage',
'IdentityProof': 'toot:IdentityProof',
'PropertyValue': 'schema:PropertyValue',
'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
'cipherText': 'toot:cipherText',
'claim': {'@id': 'toot:claim', '@type': '@id'},
'deviceId': 'toot:deviceId',
'devices': {'@id': 'toot:devices', '@type': '@id'},
'discoverable': 'toot:discoverable',
'featured': {'@id': 'toot:featured', '@type': '@id'},
'featuredTags': {'@id': 'toot:featuredTags', '@type': '@id'},
2020-08-05 10:28:54 +00:00
'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
2020-08-05 10:28:54 +00:00
'messageFranking': 'toot:messageFranking',
'messageType': 'toot:messageType',
'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
2020-12-17 22:59:11 +00:00
'publicKeyBase64': 'toot:publicKeyBase64',
'schema': 'http://schema.org#',
'suspended': 'toot:suspended',
'toot': 'http://joinmastodon.org/ns#',
2021-05-13 11:14:14 +00:00
'value': 'schema:value',
2021-05-16 10:42:52 +00:00
'hasOccupation': 'schema:hasOccupation',
'Occupation': 'schema:Occupation',
2021-05-16 15:10:39 +00:00
'occupationalCategory': 'schema:occupationalCategory',
'Role': 'schema:Role',
'WebSite': 'schema:Project',
'CategoryCode': 'schema:CategoryCode',
'CategoryCodeSet': 'schema:CategoryCodeSet'
2020-08-05 10:28:54 +00:00
}
def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, saveToFile: bool,
manualFollowerApproval: bool,
2021-07-30 10:51:33 +00:00
groupAccount: bool,
password: str) -> (str, str, {}, {}):
2019-06-28 18:55:29 +00:00
"""Returns the private key, public key, actor and webfinger endpoint
"""
2020-04-03 18:12:08 +00:00
privateKeyPem, publicKeyPem = generateRSAKey()
webfingerEndpoint = \
createWebfingerEndpoint(nickname, domain, port,
2021-07-30 10:51:33 +00:00
httpPrefix, publicKeyPem,
groupAccount)
2019-06-28 18:55:29 +00:00
if saveToFile:
2020-04-03 18:12:08 +00:00
storeWebfingerEndpoint(nickname, domain, port,
baseDir, webfingerEndpoint)
2019-06-30 18:23:18 +00:00
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
2020-04-03 18:12:08 +00:00
originalDomain = domain
2020-12-16 11:04:46 +00:00
domain = getFullDomain(domain, port)
2020-04-03 18:12:08 +00:00
personType = 'Person'
2021-07-30 10:51:33 +00:00
if groupAccount:
personType = 'Group'
2020-07-12 11:58:32 +00:00
# Enable follower approval by default
2020-07-12 12:31:28 +00:00
approveFollowers = manualFollowerApproval
2020-04-03 18:12:08 +00:00
personName = nickname
personId = httpPrefix + '://' + domain + '/users/' + nickname
inboxStr = personId + '/inbox'
personUrl = httpPrefix + '://' + domain + '/@' + personName
if nickname == 'inbox':
# shared inbox
2020-04-03 18:12:08 +00:00
inboxStr = httpPrefix + '://' + domain + '/actor/inbox'
personId = httpPrefix + '://' + domain + '/actor'
personUrl = httpPrefix + '://' + domain + \
'/about/more?instance_actor=true'
personName = originalDomain
approveFollowers = True
personType = 'Application'
2020-10-27 16:00:57 +00:00
elif nickname == 'news':
personUrl = httpPrefix + '://' + domain + \
'/about/more?news_actor=true'
approveFollowers = True
personType = 'Application'
2020-04-03 18:12:08 +00:00
2020-07-08 15:09:27 +00:00
# NOTE: these image files don't need to have
# cryptographically secure names
2020-04-03 18:12:08 +00:00
imageUrl = \
personId + '/image' + \
2020-07-08 15:17:00 +00:00
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
2020-04-03 18:12:08 +00:00
iconUrl = \
personId + '/avatar' + \
2020-07-08 15:17:00 +00:00
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
2020-04-03 18:12:08 +00:00
2021-05-08 17:13:46 +00:00
statusNumber, published = getStatusNumber()
2020-04-03 18:12:08 +00:00
newPerson = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
2020-08-05 10:28:54 +00:00
getDefaultPersonContext()
2020-04-03 18:12:08 +00:00
],
2021-05-08 17:13:46 +00:00
'published': published,
2020-03-22 20:36:19 +00:00
'alsoKnownAs': [],
'attachment': [],
2020-08-05 10:28:54 +00:00
'devices': personId + '/collections/devices',
2020-03-22 20:36:19 +00:00
'endpoints': {
'id': personId + '/endpoints',
2021-03-03 09:52:38 +00:00
'sharedInbox': httpPrefix + '://' + domain + '/inbox',
2020-03-22 20:36:19 +00:00
},
'featured': personId + '/collections/featured',
'featuredTags': personId + '/collections/tags',
'followers': personId + '/followers',
'following': personId + '/following',
2021-03-03 09:52:38 +00:00
'tts': personId + '/speaker',
2021-08-02 10:12:45 +00:00
'shares': personId + '/catalog',
2021-05-16 15:10:39 +00:00
'hasOccupation': [
{
'@type': 'Occupation',
'name': "",
2021-05-17 10:27:14 +00:00
"occupationLocation": {
"@type": "City",
"name": "Fediverse"
2021-05-17 09:12:10 +00:00
},
2021-05-16 15:10:39 +00:00
'skills': []
}
],
2020-03-22 20:36:19 +00:00
'availability': None,
'icon': {
'mediaType': 'image/png',
'type': 'Image',
2020-04-03 18:12:08 +00:00
'url': iconUrl
2020-03-22 20:36:19 +00:00
},
'id': personId,
'image': {
'mediaType': 'image/png',
'type': 'Image',
2020-04-03 18:12:08 +00:00
'url': imageUrl
2020-03-22 20:36:19 +00:00
},
'inbox': inboxStr,
'manuallyApprovesFollowers': approveFollowers,
'discoverable': True,
2020-03-22 20:36:19 +00:00
'name': personName,
'outbox': personId + '/outbox',
2020-03-22 20:36:19 +00:00
'preferredUsername': personName,
'summary': '',
'publicKey': {
'id': personId + '#main-key',
2020-03-22 20:36:19 +00:00
'owner': personId,
'publicKeyPem': publicKeyPem
},
'tag': [],
'type': personType,
'url': personUrl
2019-06-28 18:55:29 +00:00
}
2020-04-03 18:12:08 +00:00
if nickname == 'inbox':
# fields not needed by the shared inbox
del newPerson['outbox']
del newPerson['icon']
del newPerson['image']
if newPerson.get('skills'):
del newPerson['skills']
del newPerson['shares']
if newPerson.get('roles'):
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
2020-04-03 18:12:08 +00:00
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 + '/queue'):
os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
filename = baseDir + peopleSubdir + '/' + handle + '.json'
saveJson(newPerson, filename)
2019-06-28 18:55:29 +00:00
2019-08-22 14:43:43 +00:00
# save to cache
2020-04-03 18:12:08 +00:00
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)
2019-08-22 14:43:43 +00:00
2019-06-28 18:55:29 +00:00
# save the private key
2020-04-03 18:12:08 +00:00
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'
2020-08-29 11:14:19 +00:00
with open(filename, 'w+') as text_file:
2019-06-28 18:55:29 +00:00
print(privateKeyPem, file=text_file)
# save the public key
2020-04-03 18:12:08 +00:00
publicKeysSubdir = '/keys/public'
if not os.path.isdir(baseDir + publicKeysSubdir):
os.mkdir(baseDir + publicKeysSubdir)
filename = baseDir + publicKeysSubdir + '/' + handle + '.pem'
2020-08-29 11:14:19 +00:00
with open(filename, 'w+') as text_file:
2019-06-28 18:55:29 +00:00
print(publicKeyPem, file=text_file)
if password:
password = removeLineEndings(password)
2020-04-03 18:12:08 +00:00
storeBasicCredentials(baseDir, nickname, password)
2020-04-03 18:12:08 +00:00
return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
2019-06-28 18:55:29 +00:00
2020-04-03 18:12:08 +00:00
def registerAccount(baseDir: str, httpPrefix: str, domain: str, port: int,
2020-07-12 12:31:28 +00:00
nickname: str, password: str,
manualFollowerApproval: bool) -> bool:
2019-08-08 13:38:33 +00:00
"""Registers a new account from the web interface
"""
if _accountExists(baseDir, nickname, domain):
return False
2020-04-03 18:12:08 +00:00
if not validNickname(domain, nickname):
print('REGISTER: Nickname ' + nickname + ' is invalid')
2019-08-08 13:38:33 +00:00
return False
2020-04-03 18:12:08 +00:00
if len(password) < 8:
2019-08-08 13:38:33 +00:00
print('REGISTER: Password should be at least 8 characters')
return False
2020-04-03 18:12:08 +00:00
(privateKeyPem, publicKeyPem,
newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
domain, port,
httpPrefix, True,
2020-07-12 12:31:28 +00:00
manualFollowerApproval,
2020-04-03 18:12:08 +00:00
password)
2019-08-08 13:38:33 +00:00
if privateKeyPem:
return True
return False
2020-04-03 18:12:08 +00:00
def createGroup(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, saveToFile: bool,
2021-06-20 11:28:35 +00:00
password: str = None) -> (str, str, {}, {}):
2019-10-04 12:39:46 +00:00
"""Returns a group
"""
2020-04-03 18:12:08 +00:00
(privateKeyPem, publicKeyPem,
newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
domain, port,
httpPrefix, saveToFile,
2021-07-30 10:51:33 +00:00
False, password, True)
2020-04-03 18:12:08 +00:00
return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
def savePersonQrcode(baseDir: str,
nickname: str, domain: str, port: int,
scale=6) -> None:
"""Saves a qrcode image for the handle of the person
This helps to transfer onion or i2p handles to a mobile device
"""
2021-07-13 21:59:53 +00:00
qrcodeFilename = acctDir(baseDir, nickname, domain) + '/qrcode.png'
if os.path.isfile(qrcodeFilename):
return
2020-12-16 11:04:46 +00:00
handle = getFullDomain('@' + nickname + '@' + domain, port)
url = pyqrcode.create(handle)
url.png(qrcodeFilename, scale)
2020-04-03 18:12:08 +00:00
def createPerson(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, saveToFile: bool,
2020-07-12 12:31:28 +00:00
manualFollowerApproval: bool,
2021-07-30 10:51:33 +00:00
password: str,
groupAccount: bool = False) -> (str, str, {}, {}):
2019-07-05 11:27:18 +00:00
"""Returns the private key, public key, actor and webfinger endpoint
"""
2020-04-03 18:12:08 +00:00
if not validNickname(domain, nickname):
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
2020-11-24 12:42:33 +00:00
if nickname != 'news':
remainingConfigExists = \
getConfigParam(baseDir, 'registrationsRemaining')
if remainingConfigExists:
registrationsRemaining = int(remainingConfigExists)
if registrationsRemaining <= 0:
return None, None, None, None
else:
if os.path.isdir(baseDir + '/accounts/news@' + domain):
# news account already exists
2020-04-03 18:12:08 +00:00
return None, None, None, None
(privateKeyPem, publicKeyPem,
newPerson, webfingerEndpoint) = _createPersonBase(baseDir, nickname,
domain, port,
httpPrefix,
saveToFile,
manualFollowerApproval,
2021-07-30 10:51:33 +00:00
groupAccount,
password)
if not getConfigParam(baseDir, 'admin'):
2020-11-24 12:42:33 +00:00
if nickname != 'news':
# print(nickname+' becomes the instance admin and a moderator')
setConfigParam(baseDir, 'admin', nickname)
setRole(baseDir, nickname, domain, 'admin')
setRole(baseDir, nickname, domain, 'moderator')
setRole(baseDir, nickname, domain, 'editor')
2020-04-03 18:12:08 +00:00
2020-08-29 11:14:19 +00:00
if not os.path.isdir(baseDir + '/accounts'):
os.mkdir(baseDir + '/accounts')
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
if not os.path.isdir(accountDir):
os.mkdir(accountDir)
2020-08-29 11:14:19 +00:00
2020-07-12 13:28:03 +00:00
if manualFollowerApproval:
2021-07-13 21:59:53 +00:00
followDMsFilename = acctDir(baseDir, nickname, domain) + '/.followDMs'
with open(followDMsFilename, 'w+') as fFile:
fFile.write('\n')
2020-07-12 13:28:03 +00:00
2020-08-27 09:23:21 +00:00
# notify when posts are liked
2020-11-24 12:42:33 +00:00
if nickname != 'news':
2021-07-13 21:59:53 +00:00
notifyLikesFilename = \
acctDir(baseDir, nickname, domain) + '/.notifyLikes'
with open(notifyLikesFilename, 'w+') as nFile:
nFile.write('\n')
2020-11-24 12:42:33 +00:00
2020-04-03 18:12:08 +00:00
theme = getConfigParam(baseDir, 'theme')
2020-11-14 12:02:12 +00:00
if not theme:
theme = 'default'
2020-11-24 12:42:33 +00:00
if nickname != 'news':
if os.path.isfile(baseDir + '/img/default-avatar.png'):
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
2020-11-24 12:42:33 +00:00
copyfile(baseDir + '/img/default-avatar.png',
2021-07-13 21:59:53 +00:00
accountDir + '/avatar.png')
2020-11-24 12:42:33 +00:00
else:
newsAvatar = baseDir + '/theme/' + theme + '/icons/avatar_news.png'
if os.path.isfile(newsAvatar):
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
copyfile(newsAvatar, accountDir + '/avatar.png')
2020-11-24 12:42:33 +00:00
2020-11-14 12:44:24 +00:00
defaultProfileImageFilename = baseDir + '/theme/default/image.png'
if theme:
2020-11-14 12:44:24 +00:00
if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'):
2020-11-24 12:19:35 +00:00
defaultProfileImageFilename = \
baseDir + '/theme/' + theme + '/image.png'
if os.path.isfile(defaultProfileImageFilename):
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
copyfile(defaultProfileImageFilename, accountDir + '/image.png')
2020-11-14 12:44:24 +00:00
defaultBannerFilename = baseDir + '/theme/default/banner.png'
if theme:
2020-11-14 12:44:24 +00:00
if os.path.isfile(baseDir + '/theme/' + theme + '/banner.png'):
defaultBannerFilename = baseDir + '/theme/' + theme + '/banner.png'
if os.path.isfile(defaultBannerFilename):
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
copyfile(defaultBannerFilename, accountDir + '/banner.png')
2020-11-24 12:42:33 +00:00
if nickname != 'news' and remainingConfigExists:
2020-04-03 18:12:08 +00:00
registrationsRemaining -= 1
setConfigParam(baseDir, 'registrationsRemaining',
str(registrationsRemaining))
savePersonQrcode(baseDir, nickname, domain, port)
2020-04-03 18:12:08 +00:00
return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
2019-07-05 11:27:18 +00:00
2020-04-03 18:12:08 +00:00
def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str) -> (str, str, {}, {}):
2019-07-05 11:27:18 +00:00
"""Generates the shared inbox
"""
return _createPersonBase(baseDir, nickname, domain, port, httpPrefix,
2021-07-30 10:51:33 +00:00
True, True, False, None)
2020-04-03 18:12:08 +00:00
2020-10-07 16:01:45 +00:00
def createNewsInbox(baseDir: str, domain: str, port: int,
httpPrefix: str) -> (str, str, {}, {}):
"""Generates the news inbox
"""
2020-11-24 12:42:33 +00:00
return createPerson(baseDir, 'news', domain, port,
httpPrefix, True, True, None)
2020-10-07 16:01:45 +00:00
2020-04-03 18:12:08 +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-04-03 18:12:08 +00:00
updateActor = False
2020-01-19 20:43:03 +00:00
if not os.path.isfile(filename):
2020-04-03 18:12:08 +00:00
print('WARN: actor file not found ' + filename)
2020-01-19 20:43:03 +00:00
return
2020-03-22 21:16:02 +00:00
if not personJson:
2020-04-03 18:12:08 +00:00
personJson = loadJson(filename)
2020-01-19 20:58:50 +00:00
2021-05-08 17:13:46 +00:00
# add a speaker endpoint
if not personJson.get('tts'):
personJson['tts'] = personJson['id'] + '/speaker'
updateActor = True
if not personJson.get('published'):
statusNumber, published = getStatusNumber()
personJson['published'] = published
updateActor = True
2021-03-03 09:52:38 +00:00
2021-08-02 10:12:45 +00:00
if personJson.get('shares'):
if personJson['shares'].endswith('/shares'):
2021-08-02 11:13:02 +00:00
personJson['shares'] = personJson['id'] + '/catalog'
2021-08-02 10:12:45 +00:00
updateActor = True
2021-05-13 11:14:14 +00:00
occupationName = ''
2021-05-13 11:27:29 +00:00
if personJson.get('occupationName'):
occupationName = personJson['occupationName']
del personJson['occupationName']
2021-05-16 10:52:16 +00:00
updateActor = True
2021-05-13 11:14:14 +00:00
if personJson.get('occupation'):
occupationName = personJson['occupation']
del personJson['occupation']
2021-05-16 10:52:16 +00:00
updateActor = True
2021-05-13 11:14:14 +00:00
2021-05-13 20:21:37 +00:00
# if the older skills format is being used then switch
# to the new one
2021-05-13 11:14:14 +00:00
if not personJson.get('hasOccupation'):
2021-07-04 11:39:13 +00:00
personJson['hasOccupation'] = [{
'@type': 'Occupation',
'name': occupationName,
"occupationLocation": {
"@type": "City",
"name": "Fediverse"
},
'skills': []
}]
2021-05-14 18:02:58 +00:00
updateActor = True
2021-05-13 20:21:37 +00:00
# remove the old skills format
if personJson.get('skills'):
del personJson['skills']
updateActor = True
2021-05-13 20:21:37 +00:00
# if the older roles format is being used then switch
# to the new one
2021-05-16 15:10:39 +00:00
if personJson.get('affiliation'):
del personJson['affiliation']
updateActor = True
2021-05-16 15:10:39 +00:00
if not isinstance(personJson['hasOccupation'], list):
2021-07-04 11:39:13 +00:00
personJson['hasOccupation'] = [{
'@type': 'Occupation',
'name': occupationName,
'occupationLocation': {
'@type': 'City',
'name': 'Fediverse'
},
'skills': []
}]
2021-05-14 18:02:58 +00:00
updateActor = True
2021-05-17 09:28:15 +00:00
else:
# add location if it is missing
for index in range(len(personJson['hasOccupation'])):
ocItem = personJson['hasOccupation'][index]
if ocItem.get('hasOccupation'):
ocItem = ocItem['hasOccupation']
2021-05-17 10:04:26 +00:00
if ocItem.get('location'):
del ocItem['location']
updateActor = True
if not ocItem.get('occupationLocation'):
ocItem['occupationLocation'] = {
2021-05-17 10:27:14 +00:00
"@type": "City",
"name": "Fediverse"
2021-05-17 09:28:15 +00:00
}
updateActor = True
2021-05-17 10:29:07 +00:00
else:
if ocItem['occupationLocation']['@type'] != 'City':
ocItem['occupationLocation'] = {
"@type": "City",
"name": "Fediverse"
}
updateActor = True
2021-05-14 18:02:58 +00:00
2021-05-13 20:21:37 +00:00
# if no roles are defined then ensure that the admin
# roles are configured
2021-05-16 15:10:39 +00:00
rolesList = getActorRolesList(personJson)
if not rolesList:
2021-05-13 20:21:37 +00:00
adminName = getConfigParam(baseDir, 'admin')
if personJson['id'].endswith('/users/' + adminName):
2021-05-16 15:10:39 +00:00
rolesList = ["admin", "moderator", "editor"]
setRolesFromList(personJson, rolesList)
updateActor = True
2021-05-13 20:21:37 +00:00
# remove the old roles format
if personJson.get('roles'):
del personJson['roles']
updateActor = True
2021-05-08 17:13:46 +00:00
if updateActor:
2021-05-13 11:14:14 +00:00
personJson['@context'] = [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
getDefaultPersonContext()
],
2020-04-03 18:12:08 +00:00
saveJson(personJson, filename)
2020-01-19 20:58:50 +00:00
# also update the actor within the cache
2020-04-03 18:12:08 +00:00
actorCacheFilename = \
baseDir + '/accounts/cache/actors/' + \
personJson['id'].replace('/', '#') + '.json'
2020-01-19 20:58:50 +00:00
if os.path.isfile(actorCacheFilename):
2020-04-03 18:12:08 +00:00
saveJson(personJson, actorCacheFilename)
2020-01-19 20:58:50 +00:00
# update domain/@nickname in actors cache
2020-04-03 18:12:08 +00:00
actorCacheFilename = \
baseDir + '/accounts/cache/actors/' + \
personJson['id'].replace('/users/', '/@').replace('/', '#') + \
'.json'
2020-01-19 20:58:50 +00:00
if os.path.isfile(actorCacheFilename):
2020-04-03 18:12:08 +00:00
saveJson(personJson, actorCacheFilename)
2020-01-19 20:29:39 +00:00
2020-04-03 18:12:08 +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'):
2020-04-03 18:12:08 +00:00
path = path.replace('#main-key', '')
2019-08-05 15:52:18 +00:00
# is this a shared inbox lookup?
2020-04-03 18:12:08 +00:00
isSharedInbox = False
if path == '/inbox' or path == '/users/inbox' or path == '/sharedInbox':
2019-08-23 13:47:29 +00:00
# shared inbox actor on @domain@domain
2020-04-03 18:12:08 +00:00
path = '/users/' + domain
isSharedInbox = True
2019-08-05 15:22:59 +00:00
else:
2020-04-03 18:12:08 +00:00
notPersonLookup = ('/inbox', '/outbox', '/outboxarchive',
'/followers', '/following', '/featured',
2021-01-11 22:27:57 +00:00
'.png', '.jpg', '.gif', '.svg', '.mpv')
2020-03-22 21:16:02 +00:00
for ending in notPersonLookup:
2019-08-05 15:22:59 +00:00
if path.endswith(ending):
return None
2020-04-03 18:12:08 +00:00
nickname = None
2019-06-28 18:55:29 +00:00
if path.startswith('/users/'):
2020-04-03 18:12:08 +00:00
nickname = path.replace('/users/', '', 1)
2019-06-28 18:55:29 +00:00
if path.startswith('/@'):
2020-04-03 18:12:08 +00:00
nickname = path.replace('/@', '', 1)
2019-07-03 09:40:27 +00:00
if not nickname:
2019-06-28 18:55:29 +00:00
return None
2020-04-03 18:12:08 +00:00
if not isSharedInbox and not validNickname(domain, nickname):
2019-06-28 18:55:29 +00:00
return None
domain = removeDomainPort(domain)
2020-04-03 18:12:08 +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
2020-04-03 18:12:08 +00:00
personJson = loadJson(filename)
personUpgradeActor(baseDir, personJson, handle, filename)
# if not personJson:
# personJson={"user": "unknown"}
2019-06-28 18:55:29 +00:00
return personJson
2019-06-29 14:35:26 +00:00
2020-04-03 18:12:08 +00:00
def personBoxJson(recentPostsCache: {},
session, baseDir: str, domain: str, port: int, path: str,
httpPrefix: str, noOfItems: int, boxname: str,
2020-10-08 19:47:23 +00:00
authorized: bool,
2020-10-09 12:15:20 +00:00
newswireVotesThreshold: int, positiveVoting: bool,
votingTimeMins: int) -> {}:
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
"""
2020-04-03 18:12:08 +00:00
if boxname != 'inbox' and boxname != 'dm' and \
boxname != 'tlreplies' and boxname != 'tlmedia' and \
2020-10-07 09:10:42 +00:00
boxname != 'tlblogs' and boxname != 'tlnews' and \
2020-11-27 12:29:20 +00:00
boxname != 'tlfeatures' and \
2020-04-03 18:12:08 +00:00
boxname != 'outbox' and boxname != 'moderation' and \
boxname != 'tlbookmarks' and boxname != 'bookmarks':
2021-08-01 16:29:12 +00:00
print('ERROR: personBoxJson invalid box name ' + boxname)
2019-07-04 16:24:23 +00:00
return None
2020-04-03 18:12:08 +00:00
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
2020-04-03 18:12:08 +00:00
headerOnly = True
2019-06-29 17:12:26 +00:00
2019-06-29 16:47:37 +00:00
# handle page numbers
2020-04-03 18:12:08 +00:00
pageNumber = None
2019-06-29 16:47:37 +00:00
if '?page=' in path:
2020-04-03 18:12:08 +00:00
pageNumber = path.split('?page=')[1]
if pageNumber == 'true':
pageNumber = 1
2019-06-29 16:47:37 +00:00
else:
try:
2020-04-03 18:12:08 +00:00
pageNumber = int(pageNumber)
except BaseException:
2019-06-29 16:47:37 +00:00
pass
2020-04-03 18:12:08 +00:00
path = path.split('?page=')[0]
headerOnly = False
2019-06-29 16:47:37 +00:00
2020-04-03 18:12:08 +00:00
if not path.endswith('/' + boxname):
2019-06-29 15:18:35 +00:00
return None
2020-04-03 18:12:08 +00:00
nickname = None
2019-06-29 14:35:26 +00:00
if path.startswith('/users/'):
2020-04-03 18:12:08 +00:00
nickname = path.replace('/users/', '', 1).replace('/' + boxname, '')
2019-06-29 14:35:26 +00:00
if path.startswith('/@'):
2020-04-03 18:12:08 +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
2020-04-03 18:12:08 +00:00
if not validNickname(domain, nickname):
2019-06-29 15:18:35 +00:00
return None
2020-04-03 18:12:08 +00:00
if boxname == 'inbox':
return createInbox(recentPostsCache,
session, baseDir, nickname, domain, port,
httpPrefix,
2020-09-27 19:27:24 +00:00
noOfItems, headerOnly, pageNumber)
2020-04-03 18:12:08 +00:00
elif boxname == 'dm':
return createDMTimeline(recentPostsCache,
session, baseDir, nickname, domain, port,
2020-04-03 18:12:08 +00:00
httpPrefix,
2020-09-27 19:27:24 +00:00
noOfItems, headerOnly, pageNumber)
2020-05-21 19:58:21 +00:00
elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
2020-04-03 18:12:08 +00:00
return createBookmarksTimeline(session, baseDir, nickname, domain,
port, httpPrefix,
2020-09-27 19:27:24 +00:00
noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
elif boxname == 'tlreplies':
return createRepliesTimeline(recentPostsCache,
session, baseDir, nickname, domain,
2020-04-03 18:12:08 +00:00
port, httpPrefix,
2020-09-27 19:27:24 +00:00
noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
elif boxname == 'tlmedia':
return createMediaTimeline(session, baseDir, nickname, domain, port,
2020-09-27 19:27:24 +00:00
httpPrefix, noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
2020-10-07 09:10:42 +00:00
elif boxname == 'tlnews':
return createNewsTimeline(session, baseDir, nickname, domain, port,
httpPrefix, noOfItems, headerOnly,
2020-10-08 19:47:23 +00:00
newswireVotesThreshold, positiveVoting,
2020-10-09 12:15:20 +00:00
votingTimeMins, pageNumber)
2020-11-27 12:29:20 +00:00
elif boxname == 'tlfeatures':
return createFeaturesTimeline(session, baseDir, nickname, domain, port,
httpPrefix, noOfItems, headerOnly,
pageNumber)
2020-04-03 18:12:08 +00:00
elif boxname == 'tlblogs':
return createBlogsTimeline(session, baseDir, nickname, domain, port,
2020-09-27 19:27:24 +00:00
httpPrefix, noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
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,
2020-09-28 17:11:48 +00:00
noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
2019-08-12 13:22:17 +00:00
return None
2019-06-29 14:35:26 +00:00
2020-04-03 18:12:08 +00:00
def setDisplayNickname(baseDir: str, nickname: str, domain: str,
displayName: str) -> bool:
2020-04-03 18:12:08 +00:00
if len(displayName) > 32:
2019-06-28 18:55:29 +00:00
return False
2020-09-15 09:16:03 +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 False
2019-09-30 22:39:02 +00:00
2020-04-03 18:12:08 +00:00
personJson = loadJson(filename)
2019-06-28 18:55:29 +00:00
if not personJson:
return False
2020-04-03 18:12:08 +00:00
personJson['name'] = displayName
saveJson(personJson, filename)
2019-06-28 18:55:29 +00:00
return True
2019-06-28 20:00:25 +00:00
2020-04-03 18:12:08 +00:00
def setBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool:
if len(bio) > 32:
2019-06-28 20:00:25 +00:00
return False
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
filename = baseDir + '/accounts/' + handle + '.json'
2019-06-28 20:00:25 +00:00
if not os.path.isfile(filename):
return False
2019-09-30 22:39:02 +00:00
2020-04-03 18:12:08 +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
2020-04-03 18:12:08 +00:00
personJson['summary'] = bio
2019-09-30 22:39:02 +00:00
2020-04-03 18:12:08 +00:00
saveJson(personJson, filename)
2019-06-28 20:00:25 +00:00
return True
2019-08-13 09:24:55 +00:00
2020-04-03 18:12:08 +00:00
def reenableAccount(baseDir: str, nickname: str) -> None:
2019-08-13 09:24:55 +00:00
"""Removes an account suspention
"""
2020-04-03 18:12:08 +00:00
suspendedFilename = baseDir + '/accounts/suspended.txt'
2019-08-13 09:24:55 +00:00
if os.path.isfile(suspendedFilename):
2021-06-22 12:27:10 +00:00
lines = []
2021-07-13 14:40:49 +00:00
with open(suspendedFilename, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
2021-06-22 12:27:10 +00:00
with open(suspendedFilename, 'w+') as suspendedFile:
for suspended in lines:
if suspended.strip('\n').strip('\r') != nickname:
suspendedFile.write(suspended)
2019-08-13 09:24:55 +00:00
2020-04-03 18:12:08 +00:00
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
2020-04-03 18:12:08 +00:00
adminNickname = getConfigParam(baseDir, 'admin')
2020-10-10 16:10:32 +00:00
if not adminNickname:
return
2020-04-03 18:12:08 +00:00
if nickname == adminNickname:
2019-08-13 09:24:55 +00:00
return
# Don't suspend moderators
2020-04-03 18:12:08 +00:00
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-13 09:24:55 +00:00
if os.path.isfile(moderatorsFile):
2021-07-13 14:40:49 +00:00
with open(moderatorsFile, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
2019-08-13 09:24:55 +00:00
for moderator in lines:
2020-05-22 11:32:38 +00:00
if moderator.strip('\n').strip('\r') == nickname:
2019-08-13 09:24:55 +00:00
return
2021-07-13 21:59:53 +00:00
saltFilename = acctDir(baseDir, nickname, domain) + '/.salt'
if os.path.isfile(saltFilename):
os.remove(saltFilename)
2021-07-13 21:59:53 +00:00
tokenFilename = acctDir(baseDir, nickname, domain) + '/.token'
if os.path.isfile(tokenFilename):
os.remove(tokenFilename)
2020-03-22 21:16:02 +00:00
2020-04-03 18:12:08 +00:00
suspendedFilename = baseDir + '/accounts/suspended.txt'
2019-08-13 09:24:55 +00:00
if os.path.isfile(suspendedFilename):
2021-07-13 14:40:49 +00:00
with open(suspendedFilename, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
2019-08-13 09:24:55 +00:00
for suspended in lines:
2020-05-22 11:32:38 +00:00
if suspended.strip('\n').strip('\r') == nickname:
2019-08-13 09:24:55 +00:00
return
2021-06-22 12:27:10 +00:00
with open(suspendedFilename, 'a+') as suspendedFile:
suspendedFile.write(nickname + '\n')
2019-08-13 09:24:55 +00:00
else:
2021-06-22 12:27:10 +00:00
with open(suspendedFilename, 'w+') as suspendedFile:
suspendedFile.write(nickname + '\n')
2019-08-13 11:59:38 +00:00
2020-04-03 18:12:08 +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
2020-12-16 11:04:46 +00:00
domainFull = getFullDomain(domain, port)
# is the post by the admin?
2020-04-03 18:12:08 +00:00
adminNickname = getConfigParam(baseDir, 'admin')
2020-10-10 16:10:32 +00:00
if not adminNickname:
return False
2020-04-03 18:12:08 +00:00
if domainFull + '/users/' + adminNickname + '/' in postId:
return False
# is the post by a moderator?
2020-04-03 18:12:08 +00:00
moderatorsFile = baseDir + '/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
2021-07-13 14:40:49 +00:00
with open(moderatorsFile, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
for moderator in lines:
2020-04-03 18:12:08 +00:00
if domainFull + '/users/' + moderator.strip('\n') + '/' in postId:
return False
return True
2020-04-03 18:12:08 +00:00
def _removeTagsForNickname(baseDir: str, nickname: str,
domain: str, port: int) -> None:
2019-08-13 12:14:11 +00:00
"""Removes tags for a nickname
"""
2020-04-03 18:12:08 +00:00
if not os.path.isdir(baseDir + '/tags'):
2019-08-13 12:14:11 +00:00
return
2020-12-16 11:04:46 +00:00
domainFull = getFullDomain(domain, port)
2020-04-03 18:12:08 +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):
2020-04-03 18:12:08 +00:00
f = f.name
filename = os.fsdecode(f)
2019-08-13 12:14:11 +00:00
if not filename.endswith(".txt"):
continue
2020-09-12 09:50:24 +00:00
try:
tagFilename = os.path.join(directory, filename)
except BaseException:
continue
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
2021-06-22 12:27:10 +00:00
lines = []
2021-07-13 14:40:49 +00:00
with open(tagFilename, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
2021-06-22 12:27:10 +00:00
with open(tagFilename, 'w+') as tagFile:
2019-08-13 12:14:11 +00:00
for tagline in lines:
if matchStr not in tagline:
tagFile.write(tagline)
2020-04-03 18:12:08 +00:00
def removeAccount(baseDir: str, nickname: str,
domain: str, port: int) -> bool:
2019-08-13 11:59:38 +00:00
"""Removes an account
2020-03-22 21:16:02 +00:00
"""
2019-08-13 12:00:17 +00:00
# Don't remove the admin
2020-04-03 18:12:08 +00:00
adminNickname = getConfigParam(baseDir, 'admin')
2020-10-10 16:10:32 +00:00
if not adminNickname:
return False
2020-04-03 18:12:08 +00:00
if nickname == adminNickname:
2019-08-13 11:59:38 +00:00
return False
2019-08-13 12:00:17 +00:00
# Don't remove moderators
2020-04-03 18:12:08 +00:00
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-13 11:59:38 +00:00
if os.path.isfile(moderatorsFile):
2021-07-13 14:40:49 +00:00
with open(moderatorsFile, 'r') as f:
2020-04-03 18:12:08 +00:00
lines = f.readlines()
2019-08-13 11:59:38 +00:00
for moderator in lines:
2020-04-03 18:12:08 +00:00
if moderator.strip('\n') == nickname:
2019-08-13 11:59:38 +00:00
return False
reenableAccount(baseDir, nickname)
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
removePassword(baseDir, nickname)
_removeTagsForNickname(baseDir, nickname, domain, port)
2020-04-03 18:12:08 +00:00
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)
refreshNewswire(baseDir)
2019-08-13 11:59:38 +00:00
return True
2020-04-03 18:12:08 +00:00
def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool:
"""Makes an account temporarily unavailable
"""
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
2019-11-05 12:07:18 +00:00
2020-04-03 18:12:08 +00:00
accountDir = baseDir + '/accounts/' + handle
if not os.path.isdir(accountDir):
2019-11-05 10:37:37 +00:00
return False
2020-04-03 18:12:08 +00:00
deactivatedDir = baseDir + '/deactivated'
if not os.path.isdir(deactivatedDir):
os.mkdir(deactivatedDir)
2020-04-03 18:12:08 +00:00
shutil.move(accountDir, deactivatedDir + '/' + handle)
2019-11-05 12:07:18 +00:00
2020-04-03 18:12:08 +00:00
if os.path.isfile(baseDir + '/wfendpoints/' + handle + '.json'):
deactivatedWebfingerDir = baseDir + '/wfdeactivated'
2019-11-05 12:07:18 +00:00
if not os.path.isdir(deactivatedWebfingerDir):
os.mkdir(deactivatedWebfingerDir)
2020-04-03 18:12:08 +00:00
shutil.move(baseDir + '/wfendpoints/' + handle + '.json',
deactivatedWebfingerDir + '/' + handle + '.json')
2019-11-05 12:07:18 +00:00
2020-04-03 18:12:08 +00:00
if os.path.isdir(baseDir + '/sharefiles/' + nickname):
deactivatedSharefilesDir = baseDir + '/sharefilesdeactivated'
2019-11-05 12:07:18 +00:00
if not os.path.isdir(deactivatedSharefilesDir):
os.mkdir(deactivatedSharefilesDir)
2020-04-03 18:12:08 +00:00
shutil.move(baseDir + '/sharefiles/' + nickname,
deactivatedSharefilesDir + '/' + nickname)
refreshNewswire(baseDir)
2020-04-03 18:12:08 +00:00
return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
2020-04-03 18:12:08 +00:00
def activateAccount(baseDir: str, nickname: str, domain: str) -> None:
"""Makes a deactivated account available
"""
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
2019-11-05 12:07:18 +00:00
2020-04-03 18:12:08 +00:00
deactivatedDir = baseDir + '/deactivated'
deactivatedAccountDir = deactivatedDir + '/' + handle
2019-11-05 12:07:18 +00:00
if os.path.isdir(deactivatedAccountDir):
2020-04-03 18:12:08 +00:00
accountDir = baseDir + '/accounts/' + handle
2019-11-05 12:07:18 +00:00
if not os.path.isdir(accountDir):
2020-04-03 18:12:08 +00:00
shutil.move(deactivatedAccountDir, accountDir)
deactivatedWebfingerDir = baseDir + '/wfdeactivated'
if os.path.isfile(deactivatedWebfingerDir + '/' + handle + '.json'):
shutil.move(deactivatedWebfingerDir + '/' + handle + '.json',
baseDir + '/wfendpoints/' + handle + '.json')
2019-11-05 12:07:18 +00:00
2020-04-03 18:12:08 +00:00
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-05 12:07:18 +00:00
refreshNewswire(baseDir)
2019-11-06 11:39:41 +00:00
2020-04-03 18:12:08 +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
"""
2021-07-13 21:59:53 +00:00
snoozedFilename = acctDir(baseDir, nickname, domain) + '/snoozed.txt'
2019-11-06 11:39:41 +00:00
if not os.path.isfile(snoozedFilename):
return False
2020-04-03 18:12:08 +00:00
if snoozeActor + ' ' not in open(snoozedFilename).read():
2019-11-06 11:39:41 +00:00
return False
# remove the snooze entry if it has timed out
2020-04-03 18:12:08 +00:00
replaceStr = None
2019-11-06 11:39:41 +00:00
with open(snoozedFilename, 'r') as snoozedFile:
for line in snoozedFile:
# is this the entry for the actor?
2020-04-03 18:12:08 +00:00
if line.startswith(snoozeActor + ' '):
2020-05-22 11:32:38 +00:00
snoozedTimeStr = \
line.split(' ')[1].replace('\n', '').replace('\r', '')
2019-11-06 11:39:41 +00:00
# is there a time appended?
if snoozedTimeStr.isdigit():
2020-04-03 18:12:08 +00:00
snoozedTime = int(snoozedTimeStr)
currTime = int(time.time())
2019-11-06 11:39:41 +00:00
# has the snooze timed out?
2020-04-03 18:12:08 +00:00
if int(currTime - snoozedTime) > 60 * 60 * 24:
replaceStr = line
2019-11-06 11:39:41 +00:00
else:
2020-04-03 18:12:08 +00:00
replaceStr = line
2019-11-06 11:39:41 +00:00
break
if replaceStr:
content = None
with open(snoozedFilename, 'r') as snoozedFile:
content = snoozedFile.read().replace(replaceStr, '')
2019-11-06 11:39:41 +00:00
if content:
2021-06-22 12:27:10 +00:00
with open(snoozedFilename, 'w+') as writeSnoozedFile:
writeSnoozedFile.write(content)
2019-11-06 11:39:41 +00:00
2020-04-03 18:12:08 +00:00
if snoozeActor + ' ' in open(snoozedFilename).read():
2019-11-06 11:39:41 +00:00
return True
return False
2020-04-03 18:12:08 +00:00
def personSnooze(baseDir: str, nickname: str, domain: str,
snoozeActor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Temporarily ignores the given actor
"""
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
2019-11-06 11:39:41 +00:00
if not os.path.isdir(accountDir):
2020-04-03 18:12:08 +00:00
print('ERROR: unknown account ' + accountDir)
2019-11-06 11:39:41 +00:00
return
2020-04-03 18:12:08 +00:00
snoozedFilename = accountDir + '/snoozed.txt'
2019-11-06 11:57:43 +00:00
if os.path.isfile(snoozedFilename):
2020-04-03 18:12:08 +00:00
if snoozeActor + ' ' in open(snoozedFilename).read():
2019-11-06 11:57:43 +00:00
return
2021-06-22 12:27:10 +00:00
with open(snoozedFilename, 'a+') as snoozedFile:
snoozedFile.write(snoozeActor + ' ' +
str(int(time.time())) + '\n')
2019-11-06 11:39:41 +00:00
2020-04-03 18:12:08 +00:00
def personUnsnooze(baseDir: str, nickname: str, domain: str,
snoozeActor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Undoes a temporarily ignore of the given actor
"""
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
2019-11-06 11:39:41 +00:00
if not os.path.isdir(accountDir):
2020-04-03 18:12:08 +00:00
print('ERROR: unknown account ' + accountDir)
2019-11-06 11:39:41 +00:00
return
2020-04-03 18:12:08 +00:00
snoozedFilename = accountDir + '/snoozed.txt'
2019-11-06 11:39:41 +00:00
if not os.path.isfile(snoozedFilename):
return
2020-04-03 18:12:08 +00:00
if snoozeActor + ' ' not in open(snoozedFilename).read():
2019-11-06 11:39:41 +00:00
return
2020-04-03 18:12:08 +00:00
replaceStr = None
2019-11-06 11:39:41 +00:00
with open(snoozedFilename, 'r') as snoozedFile:
for line in snoozedFile:
2020-04-03 18:12:08 +00:00
if line.startswith(snoozeActor + ' '):
replaceStr = line
2019-11-06 11:39:41 +00:00
break
if replaceStr:
content = None
with open(snoozedFilename, 'r') as snoozedFile:
content = snoozedFile.read().replace(replaceStr, '')
2019-11-06 11:39:41 +00:00
if content:
2021-06-22 12:27:10 +00:00
with open(snoozedFilename, 'w+') as writeSnoozedFile:
writeSnoozedFile.write(content)
2020-08-05 21:12:09 +00:00
def setPersonNotes(baseDir: str, nickname: str, domain: str,
handle: str, notes: str) -> bool:
"""Adds notes about a person
"""
if '@' not in handle:
return False
if handle.startswith('@'):
handle = handle[1:]
2021-07-13 21:59:53 +00:00
notesDir = acctDir(baseDir, nickname, domain) + '/notes'
2020-08-05 21:24:35 +00:00
if not os.path.isdir(notesDir):
os.mkdir(notesDir)
2020-08-05 21:29:26 +00:00
notesFilename = notesDir + '/' + handle + '.txt'
with open(notesFilename, 'w+') as notesFile:
notesFile.write(notes)
2020-08-05 21:12:09 +00:00
return True
2021-03-11 18:15:04 +00:00
def _detectUsersPath(url: str) -> str:
"""Tries to detect the /users/ path
"""
if '/' not in url:
return '/users/'
usersPaths = getUserPaths()
for possibleUsersPath in usersPaths:
if possibleUsersPath in url:
return possibleUsersPath
return '/users/'
2021-06-18 09:22:16 +00:00
def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool,
2021-06-20 11:28:35 +00:00
debug: bool, quiet: bool = False) -> ({}, {}):
2021-03-11 18:15:04 +00:00
"""Returns the actor json
"""
2021-03-17 20:18:00 +00:00
if debug:
print('getActorJson for ' + handle)
2021-03-11 18:15:04 +00:00
originalActor = handle
2021-07-30 13:00:23 +00:00
groupAccount = False
# try to determine the users path
detectedUsersPath = _detectUsersPath(handle)
2021-03-11 18:15:04 +00:00
if '/@' in handle or \
detectedUsersPath in handle or \
2021-03-11 18:15:04 +00:00
handle.startswith('http') or \
2021-07-01 17:59:24 +00:00
handle.startswith('hyper'):
2021-07-30 13:00:23 +00:00
groupPaths = getGroupPaths()
if detectedUsersPath in groupPaths:
groupAccount = True
2021-03-11 18:15:04 +00:00
# format: https://domain/@nick
2021-06-03 18:30:48 +00:00
originalHandle = handle
if not hasUsersPath(originalHandle):
2021-03-17 20:18:00 +00:00
if not quiet or debug:
print('getActorJson: Expected actor format: ' +
'https://domain/@nick or https://domain' +
detectedUsersPath + 'nick')
2021-06-03 19:46:35 +00:00
return None, None
2021-06-03 18:30:48 +00:00
prefixes = getProtocolPrefixes()
for prefix in prefixes:
handle = handle.replace(prefix, '')
handle = handle.replace('/@', detectedUsersPath)
2021-07-04 22:58:01 +00:00
paths = getUserPaths()
2021-07-04 11:39:13 +00:00
userPathFound = False
for userPath in paths:
if userPath in handle:
nickname = handle.split(userPath)[1]
nickname = nickname.replace('\n', '').replace('\r', '')
domain = handle.split(userPath)[0]
2021-07-04 19:59:08 +00:00
userPathFound = True
2021-07-04 11:39:13 +00:00
break
if not userPathFound and '://' in originalHandle:
2021-06-03 18:30:48 +00:00
domain = originalHandle.split('://')[1]
if '/' in domain:
domain = domain.split('/')[0]
if '://' + domain + '/' not in originalHandle:
2021-06-03 19:46:35 +00:00
return None, None
2021-06-03 18:30:48 +00:00
nickname = originalHandle.split('://' + domain + '/')[1]
if '/' in nickname or '.' in nickname:
2021-06-03 19:46:35 +00:00
return None, None
2021-03-11 18:15:04 +00:00
else:
# format: @nick@domain
if '@' not in handle:
if not quiet:
2021-03-17 20:18:00 +00:00
print('getActorJson Syntax: --actor nickname@domain')
2021-06-03 19:46:35 +00:00
return None, None
2021-03-11 18:15:04 +00:00
if handle.startswith('@'):
handle = handle[1:]
2021-07-29 11:25:01 +00:00
elif handle.startswith('!'):
# handle for a group
handle = handle[1:]
2021-07-30 13:00:23 +00:00
groupAccount = True
2021-03-11 18:15:04 +00:00
if '@' not in handle:
if not quiet:
2021-03-17 20:18:00 +00:00
print('getActorJsonSyntax: --actor nickname@domain')
2021-06-03 19:46:35 +00:00
return None, None
2021-03-11 18:15:04 +00:00
nickname = handle.split('@')[0]
domain = handle.split('@')[1]
domain = domain.replace('\n', '').replace('\r', '')
2021-07-04 19:59:08 +00:00
2021-03-11 18:15:04 +00:00
cachedWebfingers = {}
proxyType = None
if http or domain.endswith('.onion'):
httpPrefix = 'http'
proxyType = 'tor'
elif domain.endswith('.i2p'):
httpPrefix = 'http'
proxyType = 'i2p'
elif gnunet:
httpPrefix = 'gnunet'
proxyType = 'gnunet'
else:
2021-03-17 20:18:00 +00:00
if '127.0.' not in domain and '192.168.' not in domain:
httpPrefix = 'https'
else:
httpPrefix = 'http'
2021-03-11 18:15:04 +00:00
session = createSession(proxyType)
if nickname == 'inbox':
nickname = domain
handle = nickname + '@' + domain
2021-07-30 13:00:23 +00:00
wfRequest = webfingerHandle(session, handle,
httpPrefix, cachedWebfingers,
None, __version__, debug,
groupAccount)
if not wfRequest:
2021-07-29 11:25:01 +00:00
if not quiet:
2021-07-30 13:00:23 +00:00
print('getActorJson Unable to webfinger ' + handle)
return None, None
if not isinstance(wfRequest, dict):
if not quiet:
print('getActorJson Webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return None, None
if not quiet:
pprint(wfRequest)
personUrl = None
if wfRequest.get('errors'):
if not quiet or debug:
print('getActorJson wfRequest error: ' +
str(wfRequest['errors']))
if hasUsersPath(handle):
personUrl = originalActor
else:
if debug:
print('No users path in ' + handle)
return None, None
2021-07-29 11:25:01 +00:00
2021-03-11 18:15:04 +00:00
profileStr = 'https://www.w3.org/ns/activitystreams'
2021-06-02 19:38:12 +00:00
headersList = (
"activity+json", "ld+json", "jrd+json"
)
2021-03-11 18:15:04 +00:00
if not personUrl:
2021-03-14 20:41:37 +00:00
personUrl = getUserUrl(wfRequest, 0, debug)
2021-03-11 18:15:04 +00:00
if nickname == domain:
2021-07-04 22:58:01 +00:00
paths = getUserPaths()
2021-07-04 22:46:53 +00:00
for userPath in paths:
personUrl = personUrl.replace(userPath, '/actor/')
2021-03-11 18:15:04 +00:00
if not personUrl:
# try single user instance
2021-06-03 18:49:09 +00:00
personUrl = httpPrefix + '://' + domain + '/' + nickname
2021-06-02 19:38:12 +00:00
headersList = (
"ld+json", "jrd+json", "activity+json"
)
2021-03-11 18:15:04 +00:00
if '/channel/' in personUrl or '/accounts/' in personUrl:
2021-06-02 19:38:12 +00:00
headersList = (
"ld+json", "jrd+json", "activity+json"
)
2021-06-03 18:49:09 +00:00
if debug:
print('personUrl: ' + personUrl)
2021-06-02 19:38:12 +00:00
for headerType in headersList:
headerMimeType = 'application/' + headerType
2021-03-11 18:15:04 +00:00
asHeader = {
2021-06-02 19:38:12 +00:00
'Accept': headerMimeType + '; profile="' + profileStr + '"'
2021-03-11 18:15:04 +00:00
}
personJson = \
getJson(session, personUrl, asHeader, None,
2021-06-18 09:22:16 +00:00
debug, __version__, httpPrefix, hostDomain, 20, quiet)
2021-06-02 19:38:12 +00:00
if personJson:
if not quiet:
2021-03-11 18:15:04 +00:00
pprint(personJson)
2021-06-03 19:46:35 +00:00
return personJson, asHeader
return None, None
2021-06-25 14:33:16 +00:00
def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {},
allowDownloads: bool) -> str:
"""Returns the avatar url for the person
"""
personJson = \
getPersonFromCache(baseDir, personUrl, personCache, allowDownloads)
if not personJson:
return None
# get from locally stored image
if not personJson.get('id'):
return None
actorStr = personJson['id'].replace('/', '-')
avatarImagePath = baseDir + '/cache/avatars/' + actorStr
imageExtension = getImageExtensions()
for ext in imageExtension:
if os.path.isfile(avatarImagePath + '.' + ext):
return '/avatars/' + actorStr + '.' + ext
elif os.path.isfile(avatarImagePath.lower() + '.' + ext):
return '/avatars/' + actorStr.lower() + '.' + ext
if personJson.get('icon'):
if personJson['icon'].get('url'):
return personJson['icon']['url']
return None