epicyon/person.py

1771 lines
65 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"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-04-03 18:12:08 +00:00
__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
2021-10-29 12:52:23 +00:00
import datetime
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 removeHtml
from utils import containsInvalidChars
from utils import replaceUsersWithAt
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-08-14 11:13:39 +00:00
from utils import localActorUrl
from utils import dangerousSVG
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
from cache import storePersonInCache
from filters import isFilteredBio
from follow import isFollowingActor
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
2021-12-25 17:09:22 +00:00
def setProfileImage(base_dir: str, http_prefix: str,
nickname: str, domain: str,
2020-04-03 18:12:08 +00:00
port: int, imageFilename: str, imageType: str,
2021-11-08 18:09:24 +00:00
resolution: str, city: str,
2021-12-25 17:13:38 +00:00
content_license_url: 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
2021-12-25 16:17:53 +00:00
personFilename = base_dir + '/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
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/accounts/' + handle):
print('Account not found: ' + base_dir + '/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'
2021-12-25 16:17:53 +00:00
profileFilename = base_dir + '/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'] = \
2021-12-25 17:09:22 +00:00
localActorUrl(http_prefix, nickname, fullDomain) + \
2021-08-14 11:13:39 +00:00
'/' + 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-12-25 16:17:53 +00:00
processMetaData(base_dir, nickname, domain,
2021-11-08 18:09:24 +00:00
profileFilename, profileFilename, city,
2021-12-25 17:13:38 +00:00
content_license_url)
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
2021-12-25 16:17:53 +00:00
def _accountExists(base_dir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account exists
"""
domain = removeDomainPort(domain)
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
return os.path.isdir(accountDir) or \
2021-12-25 16:17:53 +00:00
os.path.isdir(base_dir + '/deactivated/' + nickname + '@' + domain)
2020-04-03 18:12:08 +00:00
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 + '/system/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 + '/system/accounts/headers/' + nickname + \
'/image' + randStr + '.' + existingExtension
2020-04-03 18:12:08 +00:00
def getActorUpdateJson(actorJson: {}) -> {}:
"""Returns the json for an Person Update
2021-09-30 13:28:43 +00:00
"""
pubNumber, _ = getStatusNumber()
manuallyApprovesFollowers = actorJson['manuallyApprovesFollowers']
return {
'@context': [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
2021-09-30 13:28:43 +00:00
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured":
{
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags":
{
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs":
{
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo":
{
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"IdentityProof": "toot:IdentityProof",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim":
{
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey":
{
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey":
{
"@type": "@id",
"@id": "toot:identityKey"
},
"devices":
{
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint":
{
"@container": "@list",
"@id": "toot:focalPoint"
}
2021-09-30 13:28:43 +00:00
}
],
'id': actorJson['id'] + '#updates/' + pubNumber,
'type': 'Update',
'actor': actorJson['id'],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [actorJson['id'] + '/followers'],
'object': {
'id': actorJson['id'],
'type': actorJson['type'],
2021-09-30 15:14:05 +00:00
'icon': {
'type': 'Image',
'url': actorJson['icon']['url']
},
'image': {
'type': 'Image',
'url': actorJson['image']['url']
},
'attachment': actorJson['attachment'],
'following': actorJson['id'] + '/following',
'followers': actorJson['id'] + '/followers',
'inbox': actorJson['id'] + '/inbox',
'outbox': actorJson['id'] + '/outbox',
'featured': actorJson['id'] + '/collections/featured',
'featuredTags': actorJson['id'] + '/collections/tags',
'preferredUsername': actorJson['preferredUsername'],
'name': actorJson['name'],
'summary': actorJson['summary'],
'url': actorJson['url'],
'manuallyApprovesFollowers': manuallyApprovesFollowers,
'discoverable': actorJson['discoverable'],
'published': actorJson['published'],
'devices': actorJson['devices'],
"publicKey": actorJson['publicKey'],
2021-09-30 13:28:43 +00:00
}
}
2021-09-30 13:28:43 +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
}
2021-12-25 16:17:53 +00:00
def _createPersonBase(base_dir: str, nickname: str, domain: str, port: int,
2021-12-25 17:09:22 +00:00
http_prefix: str, saveToFile: bool,
2021-12-25 21:42:26 +00:00
manual_follower_approval: bool,
2021-12-26 00:07:44 +00:00
group_account: bool,
2021-07-30 10:51:33 +00:00
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-12-25 17:09:22 +00:00
http_prefix, publicKeyPem,
2021-12-26 00:07:44 +00:00
group_account)
2019-06-28 18:55:29 +00:00
if saveToFile:
2020-04-03 18:12:08 +00:00
storeWebfingerEndpoint(nickname, domain, port,
2021-12-25 16:17:53 +00:00
base_dir, 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-12-26 00:07:44 +00:00
if group_account:
2021-07-30 10:51:33 +00:00
personType = 'Group'
2020-07-12 11:58:32 +00:00
# Enable follower approval by default
2021-12-25 21:42:26 +00:00
approveFollowers = manual_follower_approval
2020-04-03 18:12:08 +00:00
personName = nickname
2021-12-25 17:09:22 +00:00
personId = localActorUrl(http_prefix, nickname, domain)
2020-04-03 18:12:08 +00:00
inboxStr = personId + '/inbox'
2021-12-25 17:09:22 +00:00
personUrl = http_prefix + '://' + domain + '/@' + personName
2020-04-03 18:12:08 +00:00
if nickname == 'inbox':
# shared inbox
2021-12-25 17:09:22 +00:00
inboxStr = http_prefix + '://' + domain + '/actor/inbox'
personId = http_prefix + '://' + domain + '/actor'
personUrl = http_prefix + '://' + domain + \
2020-04-03 18:12:08 +00:00
'/about/more?instance_actor=true'
personName = originalDomain
approveFollowers = True
personType = 'Application'
2020-10-27 16:00:57 +00:00
elif nickname == 'news':
2021-12-25 17:09:22 +00:00
personUrl = http_prefix + '://' + domain + \
2020-10-27 16:00:57 +00:00
'/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-12-25 17:09:22 +00:00
'sharedInbox': http_prefix + '://' + 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'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + peopleSubdir):
os.mkdir(base_dir + peopleSubdir)
if not os.path.isdir(base_dir + peopleSubdir + '/' + handle):
os.mkdir(base_dir + peopleSubdir + '/' + handle)
if not os.path.isdir(base_dir + peopleSubdir + '/' +
handle + '/inbox'):
os.mkdir(base_dir + peopleSubdir + '/' + handle + '/inbox')
if not os.path.isdir(base_dir + peopleSubdir + '/' +
2020-04-03 18:12:08 +00:00
handle + '/outbox'):
2021-12-25 16:17:53 +00:00
os.mkdir(base_dir + peopleSubdir + '/' + handle + '/outbox')
if not os.path.isdir(base_dir + peopleSubdir + '/' +
handle + '/queue'):
os.mkdir(base_dir + peopleSubdir + '/' + handle + '/queue')
filename = base_dir + peopleSubdir + '/' + handle + '.json'
2020-04-03 18:12:08 +00:00
saveJson(newPerson, filename)
2019-06-28 18:55:29 +00:00
2019-08-22 14:43:43 +00:00
# save to cache
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/cache'):
os.mkdir(base_dir + '/cache')
if not os.path.isdir(base_dir + '/cache/actors'):
os.mkdir(base_dir + '/cache/actors')
cacheFilename = base_dir + '/cache/actors/' + \
2020-04-03 18:12:08 +00:00
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'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/keys'):
os.mkdir(base_dir + '/keys')
if not os.path.isdir(base_dir + privateKeysSubdir):
os.mkdir(base_dir + privateKeysSubdir)
filename = base_dir + privateKeysSubdir + '/' + handle + '.key'
2021-11-25 21:18:53 +00:00
try:
with open(filename, 'w+') as text_file:
print(privateKeyPem, file=text_file)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to save ' + filename)
2019-06-28 18:55:29 +00:00
# save the public key
2020-04-03 18:12:08 +00:00
publicKeysSubdir = '/keys/public'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + publicKeysSubdir):
os.mkdir(base_dir + publicKeysSubdir)
filename = base_dir + publicKeysSubdir + '/' + handle + '.pem'
2021-11-25 21:18:53 +00:00
try:
with open(filename, 'w+') as text_file:
print(publicKeyPem, file=text_file)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to save 2 ' + filename)
2019-06-28 18:55:29 +00:00
if password:
password = removeLineEndings(password)
2021-12-25 16:17:53 +00:00
storeBasicCredentials(base_dir, 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
2021-12-25 17:09:22 +00:00
def registerAccount(base_dir: str, http_prefix: str, domain: str, port: int,
2020-07-12 12:31:28 +00:00
nickname: str, password: str,
2021-12-25 21:42:26 +00:00
manual_follower_approval: bool) -> bool:
2019-08-08 13:38:33 +00:00
"""Registers a new account from the web interface
"""
2021-12-25 16:17:53 +00:00
if _accountExists(base_dir, 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,
2021-12-25 16:17:53 +00:00
newPerson, webfingerEndpoint) = createPerson(base_dir, nickname,
2020-04-03 18:12:08 +00:00
domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, True,
2021-12-25 21:42:26 +00:00
manual_follower_approval,
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
2021-12-25 16:17:53 +00:00
def createGroup(base_dir: str, nickname: str, domain: str, port: int,
2021-12-25 17:09:22 +00:00
http_prefix: 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,
2021-12-25 16:17:53 +00:00
newPerson, webfingerEndpoint) = createPerson(base_dir, nickname,
2020-04-03 18:12:08 +00:00
domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, 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
2021-12-25 16:17:53 +00:00
def savePersonQrcode(base_dir: 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-12-25 16:17:53 +00:00
qrcodeFilename = acctDir(base_dir, 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)
2021-12-25 16:17:53 +00:00
def createPerson(base_dir: str, nickname: str, domain: str, port: int,
2021-12-25 17:09:22 +00:00
http_prefix: str, saveToFile: bool,
2021-12-25 21:42:26 +00:00
manual_follower_approval: bool,
2021-07-30 10:51:33 +00:00
password: str,
2021-12-26 00:07:44 +00:00
group_account: 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 = \
2021-12-25 16:17:53 +00:00
getConfigParam(base_dir, 'registrationsRemaining')
2020-11-24 12:42:33 +00:00
if remainingConfigExists:
registrationsRemaining = int(remainingConfigExists)
if registrationsRemaining <= 0:
return None, None, None, None
else:
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/accounts/news@' + domain):
2020-11-24 12:42:33 +00:00
# news account already exists
2020-04-03 18:12:08 +00:00
return None, None, None, None
2021-12-25 21:42:26 +00:00
manual_follower = manual_follower_approval
2020-04-03 18:12:08 +00:00
(privateKeyPem, publicKeyPem,
2021-12-25 16:17:53 +00:00
newPerson, webfingerEndpoint) = _createPersonBase(base_dir, nickname,
domain, port,
2021-12-25 17:09:22 +00:00
http_prefix,
saveToFile,
2021-12-25 21:42:26 +00:00
manual_follower,
2021-12-26 00:07:44 +00:00
group_account,
password)
2021-12-25 16:17:53 +00:00
if not getConfigParam(base_dir, 'admin'):
2020-11-24 12:42:33 +00:00
if nickname != 'news':
# print(nickname+' becomes the instance admin and a moderator')
2021-12-25 16:17:53 +00:00
setConfigParam(base_dir, 'admin', nickname)
setRole(base_dir, nickname, domain, 'admin')
setRole(base_dir, nickname, domain, 'moderator')
setRole(base_dir, nickname, domain, 'editor')
if not os.path.isdir(base_dir + '/accounts'):
os.mkdir(base_dir + '/accounts')
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
if not os.path.isdir(accountDir):
os.mkdir(accountDir)
2020-08-29 11:14:19 +00:00
2021-12-25 21:42:26 +00:00
if manual_follower_approval:
2021-12-25 16:17:53 +00:00
followDMsFilename = acctDir(base_dir, nickname, domain) + '/.followDMs'
2021-11-25 21:18:53 +00:00
try:
with open(followDMsFilename, 'w+') as fFile:
fFile.write('\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + followDMsFilename)
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 = \
2021-12-25 16:17:53 +00:00
acctDir(base_dir, nickname, domain) + '/.notifyLikes'
2021-11-25 21:18:53 +00:00
try:
with open(notifyLikesFilename, 'w+') as nFile:
nFile.write('\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + notifyLikesFilename)
2020-11-24 12:42:33 +00:00
# notify when posts have emoji reactions
if nickname != 'news':
notifyReactionsFilename = \
2021-12-25 16:17:53 +00:00
acctDir(base_dir, nickname, domain) + '/.notifyReactions'
2021-11-25 21:18:53 +00:00
try:
with open(notifyReactionsFilename, 'w+') as nFile:
nFile.write('\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + notifyReactionsFilename)
2021-12-25 16:17:53 +00:00
theme = getConfigParam(base_dir, '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':
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/img/default-avatar.png'):
accountDir = acctDir(base_dir, nickname, domain)
copyfile(base_dir + '/img/default-avatar.png',
2021-07-13 21:59:53 +00:00
accountDir + '/avatar.png')
2020-11-24 12:42:33 +00:00
else:
2021-12-25 16:17:53 +00:00
newsAvatar = base_dir + '/theme/' + theme + '/icons/avatar_news.png'
2020-11-24 12:42:33 +00:00
if os.path.isfile(newsAvatar):
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
copyfile(newsAvatar, accountDir + '/avatar.png')
2020-11-24 12:42:33 +00:00
2021-12-25 16:17:53 +00:00
defaultProfileImageFilename = base_dir + '/theme/default/image.png'
if theme:
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/theme/' + theme + '/image.png'):
2020-11-24 12:19:35 +00:00
defaultProfileImageFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + theme + '/image.png'
if os.path.isfile(defaultProfileImageFilename):
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
copyfile(defaultProfileImageFilename, accountDir + '/image.png')
2021-12-25 16:17:53 +00:00
defaultBannerFilename = base_dir + '/theme/default/banner.png'
if theme:
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/theme/' + theme + '/banner.png'):
defaultBannerFilename = \
base_dir + '/theme/' + theme + '/banner.png'
if os.path.isfile(defaultBannerFilename):
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
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
2021-12-25 16:17:53 +00:00
setConfigParam(base_dir, 'registrationsRemaining',
2020-04-03 18:12:08 +00:00
str(registrationsRemaining))
2021-12-25 16:17:53 +00:00
savePersonQrcode(base_dir, 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
2021-12-25 16:17:53 +00:00
def createSharedInbox(base_dir: str, nickname: str, domain: str, port: int,
2021-12-25 17:09:22 +00:00
http_prefix: str) -> (str, str, {}, {}):
2019-07-05 11:27:18 +00:00
"""Generates the shared inbox
"""
2021-12-25 17:09:22 +00:00
return _createPersonBase(base_dir, nickname, domain, port, http_prefix,
2021-07-30 10:51:33 +00:00
True, True, False, None)
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def createNewsInbox(base_dir: str, domain: str, port: int,
2021-12-25 17:09:22 +00:00
http_prefix: str) -> (str, str, {}, {}):
2020-10-07 16:01:45 +00:00
"""Generates the news inbox
"""
2021-12-25 16:17:53 +00:00
return createPerson(base_dir, 'news', domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, True, True, None)
2020-10-07 16:01:45 +00:00
2021-12-25 16:17:53 +00:00
def personUpgradeActor(base_dir: str, personJson: {},
2020-04-03 18:12:08 +00:00
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-12-25 16:17:53 +00:00
adminName = getConfigParam(base_dir, 'admin')
2021-05-13 20:21:37 +00:00
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 = \
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/cache/actors/' + \
2020-04-03 18:12:08 +00:00
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 = \
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/cache/actors/' + \
replaceUsersWithAt(personJson['id']).replace('/', '#') + \
2020-04-03 18:12:08 +00:00
'.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
2021-12-25 16:17:53 +00:00
def personLookup(domain: str, path: str, base_dir: 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
2021-08-31 18:30:39 +00:00
path = '/users/inbox'
2020-04-03 18:12:08 +00:00
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
2021-12-25 16:17:53 +00:00
filename = base_dir + '/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)
2021-08-31 18:39:03 +00:00
if not isSharedInbox:
2021-12-25 16:17:53 +00:00
personUpgradeActor(base_dir, personJson, handle, filename)
2020-04-03 18:12:08 +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
2020-04-03 18:12:08 +00:00
def personBoxJson(recentPostsCache: {},
2021-12-25 16:17:53 +00:00
session, base_dir: str, domain: str, port: int, path: str,
2021-12-25 17:09:22 +00:00
http_prefix: str, noOfItems: int, boxname: str,
2020-10-08 19:47:23 +00:00
authorized: bool,
2021-12-25 20:17:35 +00:00
newswire_votes_threshold: int, positive_voting: bool,
2021-12-25 20:12:07 +00:00
voting_time_mins: 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:
2021-10-29 18:48:15 +00:00
print('EX: personBoxJson unable to convert to int ' +
str(pageNumber))
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,
2021-12-25 16:17:53 +00:00
session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix,
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,
2021-12-25 16:17:53 +00:00
session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix,
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':
2021-12-25 16:17:53 +00:00
return createBookmarksTimeline(session, base_dir, nickname, domain,
2021-12-25 17:09:22 +00:00
port, http_prefix,
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,
2021-12-25 16:17:53 +00:00
session, base_dir, nickname, domain,
2021-12-25 17:09:22 +00:00
port, http_prefix,
2020-09-27 19:27:24 +00:00
noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
elif boxname == 'tlmedia':
2021-12-25 16:17:53 +00:00
return createMediaTimeline(session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
2020-10-07 09:10:42 +00:00
elif boxname == 'tlnews':
2021-12-25 16:17:53 +00:00
return createNewsTimeline(session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, noOfItems, headerOnly,
2021-12-25 20:17:35 +00:00
newswire_votes_threshold, positive_voting,
2021-12-25 20:12:07 +00:00
voting_time_mins, pageNumber)
2020-11-27 12:29:20 +00:00
elif boxname == 'tlfeatures':
2021-12-25 16:17:53 +00:00
return createFeaturesTimeline(session, base_dir,
nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, noOfItems, headerOnly,
2020-11-27 12:29:20 +00:00
pageNumber)
2020-04-03 18:12:08 +00:00
elif boxname == 'tlblogs':
2021-12-25 16:17:53 +00:00
return createBlogsTimeline(session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix, noOfItems, headerOnly,
2020-04-03 18:12:08 +00:00
pageNumber)
elif boxname == 'outbox':
2021-12-25 16:17:53 +00:00
return createOutbox(session, base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix,
2020-04-03 18:12:08 +00:00
noOfItems, headerOnly, authorized,
pageNumber)
elif boxname == 'moderation':
2021-12-25 16:17:53 +00:00
return createModeration(base_dir, nickname, domain, port,
2021-12-25 17:09:22 +00:00
http_prefix,
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
2021-12-25 16:17:53 +00:00
def setDisplayNickname(base_dir: 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
2021-12-25 16:17:53 +00:00
filename = base_dir + '/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
2021-12-25 16:17:53 +00:00
def setBio(base_dir: str, nickname: str, domain: str, bio: str) -> bool:
2021-12-19 18:18:55 +00:00
"""Only used within tests
"""
2020-04-03 18:12:08 +00:00
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
2021-12-25 16:17:53 +00:00
filename = base_dir + '/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
2021-12-25 16:17:53 +00:00
def reenableAccount(base_dir: str, nickname: str) -> None:
2019-08-13 09:24:55 +00:00
"""Removes an account suspention
"""
2021-12-25 16:17:53 +00:00
suspendedFilename = base_dir + '/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-11-25 21:18:53 +00:00
try:
with open(suspendedFilename, 'w+') as suspendedFile:
for suspended in lines:
if suspended.strip('\n').strip('\r') != nickname:
suspendedFile.write(suspended)
2021-12-25 15:28:52 +00:00
except OSError as ex:
2021-11-25 22:22:54 +00:00
print('EX: unable to save ' + suspendedFilename +
2021-12-25 15:28:52 +00:00
' ' + str(ex))
2019-08-13 09:24:55 +00:00
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def suspendAccount(base_dir: str, nickname: str, domain: str) -> None:
2019-08-13 09:24:55 +00:00
"""Suspends the given account
"""
# Don't suspend the admin
2021-12-25 16:17:53 +00:00
adminNickname = getConfigParam(base_dir, '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
2021-12-25 16:17:53 +00:00
moderatorsFile = base_dir + '/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-12-25 16:17:53 +00:00
saltFilename = acctDir(base_dir, nickname, domain) + '/.salt'
if os.path.isfile(saltFilename):
try:
os.remove(saltFilename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: suspendAccount unable to delete ' + saltFilename)
2021-12-25 16:17:53 +00:00
tokenFilename = acctDir(base_dir, nickname, domain) + '/.token'
if os.path.isfile(tokenFilename):
try:
os.remove(tokenFilename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: suspendAccount unable to delete ' + tokenFilename)
2020-03-22 21:16:02 +00:00
2021-12-25 16:17:53 +00:00
suspendedFilename = base_dir + '/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-11-25 21:18:53 +00:00
try:
with open(suspendedFilename, 'a+') as suspendedFile:
suspendedFile.write(nickname + '\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to append ' + suspendedFilename)
2019-08-13 09:24:55 +00:00
else:
2021-11-25 21:18:53 +00:00
try:
with open(suspendedFilename, 'w+') as suspendedFile:
suspendedFile.write(nickname + '\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + suspendedFilename)
2019-08-13 11:59:38 +00:00
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def canRemovePost(base_dir: str, nickname: str,
2020-04-03 18:12:08 +00:00
domain: str, port: int, postId: str) -> bool:
"""Returns true if the given post can be removed
"""
if '/statuses/' not in postId:
return False
2021-12-26 10:00:46 +00:00
domain_full = getFullDomain(domain, port)
# is the post by the admin?
2021-12-25 16:17:53 +00:00
adminNickname = getConfigParam(base_dir, 'admin')
2020-10-10 16:10:32 +00:00
if not adminNickname:
return False
2021-12-26 10:00:46 +00:00
if domain_full + '/users/' + adminNickname + '/' in postId:
return False
# is the post by a moderator?
2021-12-25 16:17:53 +00:00
moderatorsFile = base_dir + '/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:
2021-12-26 10:00:46 +00:00
if domain_full + '/users/' + moderator.strip('\n') + '/' in postId:
return False
return True
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def _removeTagsForNickname(base_dir: str, nickname: str,
domain: str, port: int) -> None:
2019-08-13 12:14:11 +00:00
"""Removes tags for a nickname
"""
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/tags'):
2019-08-13 12:14:11 +00:00
return
2021-12-26 10:00:46 +00:00
domain_full = getFullDomain(domain, port)
matchStr = domain_full + '/users/' + nickname + '/'
2021-12-25 16:17:53 +00:00
directory = os.fsencode(base_dir + '/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:
2021-10-29 18:48:15 +00:00
print('EX: _removeTagsForNickname unable to join ' +
str(directory) + ' ' + str(filename))
2020-09-12 09:50:24 +00:00
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-11-25 21:18:53 +00:00
try:
with open(tagFilename, 'w+') as tagFile:
for tagline in lines:
if matchStr not in tagline:
tagFile.write(tagline)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + tagFilename)
2019-08-13 12:14:11 +00:00
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def removeAccount(base_dir: str, nickname: str,
2020-04-03 18:12:08 +00:00
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
2021-12-25 16:17:53 +00:00
adminNickname = getConfigParam(base_dir, '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
2021-12-25 16:17:53 +00:00
moderatorsFile = base_dir + '/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
2021-12-25 16:17:53 +00:00
reenableAccount(base_dir, nickname)
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
2021-12-25 16:17:53 +00:00
removePassword(base_dir, nickname)
_removeTagsForNickname(base_dir, nickname, domain, port)
if os.path.isdir(base_dir + '/deactivated/' + handle):
shutil.rmtree(base_dir + '/deactivated/' + handle,
2021-10-29 18:48:15 +00:00
ignore_errors=False, onerror=None)
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/accounts/' + handle):
shutil.rmtree(base_dir + '/accounts/' + handle,
2021-10-29 18:48:15 +00:00
ignore_errors=False, onerror=None)
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/accounts/' + handle + '.json'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/accounts/' + handle + '.json')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: removeAccount unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/' + handle + '.json')
if os.path.isfile(base_dir + '/wfendpoints/' + handle + '.json'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/wfendpoints/' + handle + '.json')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: removeAccount unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/wfendpoints/' + handle + '.json')
if os.path.isfile(base_dir + '/keys/private/' + handle + '.key'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/keys/private/' + handle + '.key')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: removeAccount unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/keys/private/' + handle + '.key')
if os.path.isfile(base_dir + '/keys/public/' + handle + '.pem'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/keys/public/' + handle + '.pem')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: removeAccount unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/keys/public/' + handle + '.pem')
if os.path.isdir(base_dir + '/sharefiles/' + nickname):
shutil.rmtree(base_dir + '/sharefiles/' + nickname,
2021-10-29 18:48:15 +00:00
ignore_errors=False, onerror=None)
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/wfdeactivated/' + handle + '.json'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/wfdeactivated/' + handle + '.json')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: removeAccount unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/wfdeactivated/' + handle + '.json')
if os.path.isdir(base_dir + '/sharefilesdeactivated/' + nickname):
shutil.rmtree(base_dir + '/sharefilesdeactivated/' + nickname,
2021-10-29 18:48:15 +00:00
ignore_errors=False, onerror=None)
2021-12-25 16:17:53 +00:00
refreshNewswire(base_dir)
2019-08-13 11:59:38 +00:00
return True
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def deactivateAccount(base_dir: 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
2021-12-25 16:17:53 +00:00
accountDir = base_dir + '/accounts/' + handle
if not os.path.isdir(accountDir):
2019-11-05 10:37:37 +00:00
return False
2021-12-25 16:17:53 +00:00
deactivatedDir = base_dir + '/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
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/wfendpoints/' + handle + '.json'):
deactivatedWebfingerDir = base_dir + '/wfdeactivated'
2019-11-05 12:07:18 +00:00
if not os.path.isdir(deactivatedWebfingerDir):
os.mkdir(deactivatedWebfingerDir)
2021-12-25 16:17:53 +00:00
shutil.move(base_dir + '/wfendpoints/' + handle + '.json',
2020-04-03 18:12:08 +00:00
deactivatedWebfingerDir + '/' + handle + '.json')
2019-11-05 12:07:18 +00:00
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/sharefiles/' + nickname):
deactivatedSharefilesDir = base_dir + '/sharefilesdeactivated'
2019-11-05 12:07:18 +00:00
if not os.path.isdir(deactivatedSharefilesDir):
os.mkdir(deactivatedSharefilesDir)
2021-12-25 16:17:53 +00:00
shutil.move(base_dir + '/sharefiles/' + nickname,
2020-04-03 18:12:08 +00:00
deactivatedSharefilesDir + '/' + nickname)
2021-12-25 16:17:53 +00:00
refreshNewswire(base_dir)
2020-04-03 18:12:08 +00:00
return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def activateAccount(base_dir: 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
2021-12-25 16:17:53 +00:00
deactivatedDir = base_dir + '/deactivated'
2020-04-03 18:12:08 +00:00
deactivatedAccountDir = deactivatedDir + '/' + handle
2019-11-05 12:07:18 +00:00
if os.path.isdir(deactivatedAccountDir):
2021-12-25 16:17:53 +00:00
accountDir = base_dir + '/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)
2021-12-25 16:17:53 +00:00
deactivatedWebfingerDir = base_dir + '/wfdeactivated'
2020-04-03 18:12:08 +00:00
if os.path.isfile(deactivatedWebfingerDir + '/' + handle + '.json'):
shutil.move(deactivatedWebfingerDir + '/' + handle + '.json',
2021-12-25 16:17:53 +00:00
base_dir + '/wfendpoints/' + handle + '.json')
2019-11-05 12:07:18 +00:00
2021-12-25 16:17:53 +00:00
deactivatedSharefilesDir = base_dir + '/sharefilesdeactivated'
2020-04-03 18:12:08 +00:00
if os.path.isdir(deactivatedSharefilesDir + '/' + nickname):
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/sharefiles/' + nickname):
2020-04-03 18:12:08 +00:00
shutil.move(deactivatedSharefilesDir + '/' + nickname,
2021-12-25 16:17:53 +00:00
base_dir + '/sharefiles/' + nickname)
2019-11-05 12:07:18 +00:00
2021-12-25 16:17:53 +00:00
refreshNewswire(base_dir)
2019-11-06 11:39:41 +00:00
2021-12-25 16:17:53 +00:00
def isPersonSnoozed(base_dir: str, nickname: str, domain: str,
2020-04-03 18:12:08 +00:00
snoozeActor: str) -> bool:
2019-11-06 11:39:41 +00:00
"""Returns true if the given actor is snoozed
"""
2021-12-25 16:17:53 +00:00
snoozedFilename = acctDir(base_dir, 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-11-25 21:18:53 +00:00
try:
with open(snoozedFilename, 'w+') as writeSnoozedFile:
writeSnoozedFile.write(content)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + snoozedFilename)
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
2021-12-25 16:17:53 +00:00
def personSnooze(base_dir: str, nickname: str, domain: str,
2020-04-03 18:12:08 +00:00
snoozeActor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Temporarily ignores the given actor
"""
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, 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-11-25 21:18:53 +00:00
try:
with open(snoozedFilename, 'a+') as snoozedFile:
snoozedFile.write(snoozeActor + ' ' +
str(int(time.time())) + '\n')
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to append ' + snoozedFilename)
2019-11-06 11:39:41 +00:00
2020-04-03 18:12:08 +00:00
2021-12-25 16:17:53 +00:00
def personUnsnooze(base_dir: str, nickname: str, domain: str,
2020-04-03 18:12:08 +00:00
snoozeActor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Undoes a temporarily ignore of the given actor
"""
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, 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-11-25 21:18:53 +00:00
try:
with open(snoozedFilename, 'w+') as writeSnoozedFile:
writeSnoozedFile.write(content)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + snoozedFilename)
2020-08-05 21:12:09 +00:00
2021-12-25 16:17:53 +00:00
def setPersonNotes(base_dir: str, nickname: str, domain: str,
2020-08-05 21:12:09 +00:00
handle: str, notes: str) -> bool:
"""Adds notes about a person
"""
if '@' not in handle:
return False
if handle.startswith('@'):
handle = handle[1:]
2021-12-25 16:17:53 +00:00
notesDir = acctDir(base_dir, 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'
2021-11-25 21:18:53 +00:00
try:
with open(notesFilename, 'w+') as notesFile:
notesFile.write(notes)
except OSError:
2021-11-25 22:22:54 +00:00
print('EX: unable to write ' + notesFilename)
2021-11-25 21:18:53 +00:00
return False
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,
debug: bool, quiet: bool,
2021-12-25 23:03:28 +00:00
signing_priv_key_pem: str,
existingSession) -> ({}, {}):
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-12-26 00:07:44 +00:00
group_account = 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:
2021-12-26 00:07:44 +00:00
group_account = 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-12-26 00:07:44 +00:00
group_account = 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-12-25 22:28:18 +00:00
cached_webfingers = {}
2021-12-25 21:09:22 +00:00
proxy_type = None
2021-03-11 18:15:04 +00:00
if http or domain.endswith('.onion'):
2021-12-25 17:09:22 +00:00
http_prefix = 'http'
2021-12-25 21:09:22 +00:00
proxy_type = 'tor'
2021-03-11 18:15:04 +00:00
elif domain.endswith('.i2p'):
2021-12-25 17:09:22 +00:00
http_prefix = 'http'
2021-12-25 21:09:22 +00:00
proxy_type = 'i2p'
2021-03-11 18:15:04 +00:00
elif gnunet:
2021-12-25 17:09:22 +00:00
http_prefix = 'gnunet'
2021-12-25 21:09:22 +00:00
proxy_type = 'gnunet'
2021-03-11 18:15:04 +00:00
else:
2021-03-17 20:18:00 +00:00
if '127.0.' not in domain and '192.168.' not in domain:
2021-12-25 17:09:22 +00:00
http_prefix = 'https'
2021-03-17 20:18:00 +00:00
else:
2021-12-25 17:09:22 +00:00
http_prefix = 'http'
if existingSession:
session = existingSession
else:
2021-12-25 21:09:22 +00:00
session = createSession(proxy_type)
2021-03-11 18:15:04 +00:00
if nickname == 'inbox':
nickname = domain
2021-07-30 13:00:23 +00:00
personUrl = None
2021-09-02 13:02:07 +00:00
wfRequest = None
2021-09-12 19:53:38 +00:00
2021-09-02 13:02:07 +00:00
if '://' in originalActor and \
originalActor.lower().endswith('/actor'):
if debug:
print(originalActor + ' is an instance actor')
personUrl = originalActor
2021-12-26 00:07:44 +00:00
elif '://' in originalActor and group_account:
2021-09-12 19:48:05 +00:00
if debug:
print(originalActor + ' is a group actor')
personUrl = originalActor
2021-09-02 13:02:07 +00:00
else:
handle = nickname + '@' + domain
wfRequest = webfingerHandle(session, handle,
2021-12-25 22:28:18 +00:00
http_prefix, cached_webfingers,
2021-09-08 12:27:59 +00:00
hostDomain, __version__, debug,
2021-12-26 00:07:44 +00:00
group_account, signing_priv_key_pem)
2021-09-02 13:02:07 +00:00
if not wfRequest:
if not quiet:
print('getActorJson Unable to webfinger ' + handle)
2021-07-30 13:00:23 +00:00
return None, None
2021-09-02 13:02:07 +00:00
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)
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-09-02 13:02:07 +00:00
if not personUrl and wfRequest:
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-12-26 00:07:44 +00:00
if not personUrl and group_account:
2021-12-25 17:09:22 +00:00
personUrl = http_prefix + '://' + domain + '/c/' + nickname
2021-03-11 18:15:04 +00:00
if not personUrl:
# try single user instance
2021-12-25 17:09:22 +00:00
personUrl = http_prefix + '://' + domain + '/' + nickname
2021-06-02 19:38:12 +00:00
headersList = (
"ld+json", "jrd+json", "activity+json"
)
2021-09-12 19:48:05 +00:00
if debug:
print('Trying single user instance ' + personUrl)
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 = \
2021-12-25 23:03:28 +00:00
getJson(signing_priv_key_pem, session, personUrl, asHeader, None,
2021-12-25 17:09:22 +00:00
debug, __version__, http_prefix, 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
2021-12-25 22:17:49 +00:00
def getPersonAvatarUrl(base_dir: str, personUrl: str, person_cache: {},
2021-06-25 14:33:16 +00:00
allowDownloads: bool) -> str:
"""Returns the avatar url for the person
"""
personJson = \
2021-12-25 22:17:49 +00:00
getPersonFromCache(base_dir, personUrl, person_cache, allowDownloads)
2021-06-25 14:33:16 +00:00
if not personJson:
return None
# get from locally stored image
if not personJson.get('id'):
return None
actorStr = personJson['id'].replace('/', '-')
2021-12-25 16:17:53 +00:00
avatarImagePath = base_dir + '/cache/avatars/' + actorStr
2021-06-25 14:33:16 +00:00
imageExtension = getImageExtensions()
for ext in imageExtension:
2021-09-13 19:38:15 +00:00
imFilename = avatarImagePath + '.' + ext
imPath = '/avatars/' + actorStr + '.' + ext
if not os.path.isfile(imFilename):
imFilename = avatarImagePath.lower() + '.' + ext
imPath = '/avatars/' + actorStr.lower() + '.' + ext
if not os.path.isfile(imFilename):
continue
2021-09-13 19:39:43 +00:00
if ext != 'svg':
return imPath
else:
2021-09-13 19:42:51 +00:00
content = ''
with open(imFilename, 'r') as fp:
content = fp.read()
if not dangerousSVG(content, False):
2021-09-13 19:38:15 +00:00
return imPath
2021-06-25 14:33:16 +00:00
if personJson.get('icon'):
if personJson['icon'].get('url'):
if '.svg' not in personJson['icon']['url'].lower():
return personJson['icon']['url']
2021-06-25 14:33:16 +00:00
return None
2021-10-29 12:52:23 +00:00
def addActorUpdateTimestamp(actorJson: {}) -> None:
"""Adds 'updated' fields with a timestamp
"""
updatedTime = datetime.datetime.utcnow()
currDateStr = updatedTime.strftime("%Y-%m-%dT%H:%M:%SZ")
actorJson['updated'] = currDateStr
# add updated timestamp to avatar and banner
actorJson['icon']['updated'] = currDateStr
actorJson['image']['updated'] = currDateStr
2021-12-25 16:17:53 +00:00
def validSendingActor(session, base_dir: str,
nickname: str, domain: str,
2021-12-25 22:17:49 +00:00
person_cache: {},
2021-12-25 22:09:19 +00:00
post_json_object: {},
2021-12-25 23:03:28 +00:00
signing_priv_key_pem: str,
2021-12-25 21:32:15 +00:00
debug: bool, unit_test: bool) -> bool:
"""When a post arrives in the inbox this is used to check that
the sending actor is valid
"""
# who sent this post?
2021-12-25 22:09:19 +00:00
sendingActor = post_json_object['actor']
# If you are following them then allow their posts
2021-12-25 16:17:53 +00:00
if isFollowingActor(base_dir, nickname, domain, sendingActor):
return True
# sending to yourself (reminder)
if sendingActor.endswith(domain + '/users/' + nickname):
return True
# get their actor
2021-12-25 22:17:49 +00:00
actorJson = getPersonFromCache(base_dir, sendingActor, person_cache, True)
downloadedActor = False
if not actorJson:
# download the actor
actorJson, _ = getActorJson(domain, sendingActor,
True, False, debug, True,
2021-12-25 23:03:28 +00:00
signing_priv_key_pem, session)
if actorJson:
downloadedActor = True
if not actorJson:
# if the actor couldn't be obtained then proceed anyway
return True
if not actorJson.get('preferredUsername'):
print('REJECT: no preferredUsername within actor ' + str(actorJson))
return False
# does the actor have a bio ?
2021-12-25 21:32:15 +00:00
if not unit_test:
2021-12-19 18:29:43 +00:00
bioStr = ''
if actorJson.get('summary'):
bioStr = removeHtml(actorJson['summary']).strip()
if not bioStr:
# allow no bio if it's an actor in this instance
if domain not in sendingActor:
# probably a spam actor with no bio
print('REJECT: spam actor ' + sendingActor)
return False
2021-12-19 18:29:43 +00:00
if len(bioStr) < 10:
print('REJECT: actor bio is not long enough ' +
sendingActor + ' ' + bioStr)
return False
bioStr += ' ' + removeHtml(actorJson['preferredUsername'])
if actorJson.get('attachment'):
if isinstance(actorJson['attachment'], list):
for tag in actorJson['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name'):
continue
if isinstance(tag['name'], str):
bioStr += ' ' + tag['name']
if tag.get('value'):
continue
if isinstance(tag['value'], str):
bioStr += ' ' + tag['value']
2021-12-14 14:02:32 +00:00
if actorJson.get('name'):
bioStr += ' ' + removeHtml(actorJson['name'])
if containsInvalidChars(bioStr):
print('REJECT: post actor bio contains invalid characters')
return False
2021-12-25 16:17:53 +00:00
if isFilteredBio(base_dir, nickname, domain, bioStr):
print('REJECT: post actor bio contains filtered text')
return False
else:
print('Skipping check for missing bio in ' + sendingActor)
# Check any attached fields for the actor.
# Spam actors will sometimes have attached fields which are all empty
if actorJson.get('attachment'):
if isinstance(actorJson['attachment'], list):
noOfTags = 0
tagsWithoutValue = 0
for tag in actorJson['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name'):
continue
noOfTags += 1
if not tag.get('value'):
tagsWithoutValue += 1
continue
if not isinstance(tag['value'], str):
tagsWithoutValue += 1
continue
if not tag['value'].strip():
tagsWithoutValue += 1
continue
if len(tag['value']) < 2:
tagsWithoutValue += 1
continue
if noOfTags > 0:
if int(tagsWithoutValue * 100 / noOfTags) > 50:
print('REJECT: actor has empty attachments ' +
sendingActor)
return False
if downloadedActor:
# if the actor is valid and was downloaded then
# store it in the cache, but don't write it to file
2021-12-25 22:17:49 +00:00
storePersonInCache(base_dir, sendingActor, actorJson, person_cache,
False)
return True