epicyon/person.py

2302 lines
84 KiB
Python
Raw Normal View History

2020-04-03 18:12:08 +00:00
__filename__ = "person.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2024-01-21 19:01:20 +00:00
__version__ = "1.5.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
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
2021-12-29 21:55:09 +00:00
from webfinger import create_webfinger_endpoint
from webfinger import store_webfinger_endpoint
from posts import get_user_url
from posts import create_dm_timeline
from posts import create_replies_timeline
from posts import create_media_timeline
from posts import create_news_timeline
from posts import create_blogs_timeline
from posts import create_features_timeline
from posts import create_bookmarks_timeline
from posts import create_inbox
from posts import create_outbox
from posts import create_moderation
2021-12-28 21:36:27 +00:00
from auth import store_basic_credentials
2021-12-29 21:55:09 +00:00
from auth import remove_password
2021-12-28 22:22:09 +00:00
from roles import set_role
2022-09-02 18:06:13 +00:00
from roles import actor_roles_from_list
2021-12-28 22:22:09 +00:00
from roles import get_actor_roles_list
2021-12-28 21:36:27 +00:00
from media import process_meta_data
2024-02-05 20:05:00 +00:00
from utils import get_image_mime_type
2024-01-29 21:45:04 +00:00
from utils import get_instance_url
2023-12-09 14:18:24 +00:00
from utils import get_url_from_post
2023-11-20 22:27:58 +00:00
from utils import date_utcnow
from utils import get_memorials
from utils import is_account_dir
from utils import valid_hash_tag
2022-12-18 15:29:54 +00:00
from utils import acct_handle_dir
from utils import safe_system_string
from utils import get_attachment_property_value
2022-03-14 15:51:12 +00:00
from utils import get_nickname_from_actor
2021-12-27 15:43:22 +00:00
from utils import remove_html
2021-12-27 17:53:41 +00:00
from utils import contains_invalid_chars
2022-12-26 10:49:41 +00:00
from utils import contains_invalid_actor_url_chars
2021-12-26 17:21:37 +00:00
from utils import replace_users_with_at
2022-06-21 11:58:50 +00:00
from utils import remove_eol
2021-12-26 18:17:37 +00:00
from utils import remove_domain_port
2021-12-27 17:42:35 +00:00
from utils import get_status_number
2021-12-26 12:45:03 +00:00
from utils import get_full_domain
2021-12-28 14:41:10 +00:00
from utils import valid_nickname
2021-12-26 15:13:34 +00:00
from utils import load_json
2021-12-26 14:47:21 +00:00
from utils import save_json
2021-12-27 20:38:02 +00:00
from utils import set_config_param
2021-12-26 14:08:58 +00:00
from utils import get_config_param
2021-12-26 12:10:21 +00:00
from utils import refresh_newswire
2021-12-27 17:20:01 +00:00
from utils import get_protocol_prefixes
2021-12-26 12:19:00 +00:00
from utils import has_users_path
2021-12-26 14:26:16 +00:00
from utils import get_image_extensions
2021-12-27 15:58:46 +00:00
from utils import is_image_file
2021-12-26 12:02:29 +00:00
from utils import acct_dir
2021-12-26 12:24:40 +00:00
from utils import get_user_paths
2021-12-26 17:53:07 +00:00
from utils import get_group_paths
2021-12-26 10:19:59 +00:00
from utils import local_actor_url
2021-12-27 21:44:48 +00:00
from utils import dangerous_svg
2022-06-10 11:43:33 +00:00
from utils import text_in_file
2023-08-14 18:46:27 +00:00
from utils import contains_statuses
2024-01-09 16:59:23 +00:00
from utils import get_actor_from_post
2024-05-12 12:35:26 +00:00
from utils import data_dir
2023-08-13 09:58:02 +00:00
from session import get_json_valid
2021-12-28 16:56:57 +00:00
from session import create_session
2021-12-29 21:55:09 +00:00
from session import get_json
from webfinger import webfinger_handle
2021-03-11 18:15:04 +00:00
from pprint import pprint
2024-01-29 21:45:04 +00:00
from cache import get_actor_public_key_from_id
2021-12-29 21:55:09 +00:00
from cache import get_person_from_cache
from cache import store_person_in_cache
from cache import remove_person_from_cache
2021-12-29 21:55:09 +00:00
from filters import is_filtered_bio
2021-12-28 20:32:11 +00:00
from follow import is_following_actor
2019-06-28 18:55:29 +00:00
2021-12-29 21:55:09 +00:00
def generate_rsa_key() -> (str, str):
2022-06-12 10:58:00 +00:00
"""Creates an RSA key for signing
"""
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
2022-01-03 14:49:33 +00:00
private_key_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
pubkey = key.public_key()
2022-01-03 14:49:33 +00:00
public_key_pem = pubkey.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
2022-01-03 14:49:33 +00:00
private_key_pem = private_key_pem.decode("utf-8")
public_key_pem = public_key_pem.decode("utf-8")
return private_key_pem, public_key_pem
2020-04-03 18:12:08 +00:00
2021-12-29 21:55:09 +00:00
def set_profile_image(base_dir: str, http_prefix: str,
nickname: str, domain: str,
2022-01-03 14:49:33 +00:00
port: int, image_filename: str, image_type: str,
2021-12-29 21:55:09 +00:00
resolution: str, city: str,
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
"""
2022-06-21 11:58:50 +00:00
image_filename = remove_eol(image_filename)
2021-12-27 15:58:46 +00:00
if not is_image_file(image_filename):
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
2021-12-26 14:42:21 +00:00
if image_filename.startswith('~/'):
image_filename = image_filename.replace('~/', str(Path.home()) + '/')
2019-07-12 14:31:56 +00:00
2021-12-26 18:17:37 +00:00
domain = remove_domain_port(domain)
2022-01-03 14:49:33 +00:00
full_domain = get_full_domain(domain, port)
2019-07-12 13:51:04 +00:00
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
2022-12-18 15:29:54 +00:00
person_filename = acct_handle_dir(base_dir, handle) + '.json'
2022-01-03 14:49:33 +00:00
if not os.path.isfile(person_filename):
print('person definition not found: ' + person_filename)
2019-07-12 14:31:56 +00:00
return False
2022-12-18 15:29:54 +00:00
handle_dir = acct_handle_dir(base_dir, handle)
if not os.path.isdir(handle_dir):
print('Account not found: ' + handle_dir)
2019-07-12 14:31:56 +00:00
return False
2019-07-12 13:51:04 +00:00
2022-01-03 14:49:33 +00:00
icon_filename_base = 'icon'
if image_type in ('avatar', 'icon'):
icon_filename_base = 'icon'
2019-07-12 13:51:04 +00:00
else:
2022-01-03 14:49:33 +00:00
icon_filename_base = 'image'
2020-03-22 21:16:02 +00:00
2022-01-03 14:49:33 +00:00
media_type = 'image/png'
icon_filename = icon_filename_base + '.png'
2024-02-05 19:07:55 +00:00
extensions = get_image_extensions()
for ext in extensions:
if image_filename.endswith('.' + ext):
2024-02-05 20:05:00 +00:00
media_type = get_image_mime_type(image_filename)
2024-02-05 19:07:55 +00:00
icon_filename = icon_filename_base + '.' + ext
2022-12-18 15:29:54 +00:00
profile_filename = acct_handle_dir(base_dir, handle) + '/' + icon_filename
2022-01-03 14:49:33 +00:00
person_json = load_json(person_filename)
if person_json:
person_json[icon_filename_base]['mediaType'] = media_type
person_json[icon_filename_base]['url'] = \
local_actor_url(http_prefix, nickname, full_domain) + \
'/' + icon_filename
save_json(person_json, person_filename)
2020-04-03 18:12:08 +00:00
cmd = \
'/usr/bin/convert ' + safe_system_string(image_filename) + \
' -size ' + resolution + ' -quality 50 ' + \
safe_system_string(profile_filename)
2019-07-12 14:31:56 +00:00
subprocess.call(cmd, shell=True)
2021-12-28 21:36:27 +00:00
process_meta_data(base_dir, nickname, domain,
2022-01-03 14:49:33 +00:00
profile_filename, profile_filename, city,
2021-12-28 21:36:27 +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-29 21:55:09 +00:00
def _account_exists(base_dir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account exists
"""
2021-12-26 18:17:37 +00:00
domain = remove_domain_port(domain)
2022-01-03 14:49:33 +00:00
account_dir = acct_dir(base_dir, nickname, domain)
return os.path.isdir(account_dir) 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
2022-01-03 14:49:33 +00:00
def randomize_actor_images(person_json: {}) -> None:
"""Randomizes the filenames for avatar image and background
This causes other instances to update their cached avatar image
"""
2022-01-03 14:49:33 +00:00
person_id = person_json['id']
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(person_json['icon']['url'])
last_part_of_filename = url_str.split('/')[-1]
2022-01-03 14:49:33 +00:00
existing_extension = last_part_of_filename.split('.')[1]
2020-07-08 15:09:27 +00:00
# NOTE: these files don't need to have cryptographically
# secure names
2022-01-03 14:49:33 +00:00
rand_str = str(randint(10000000000000, 99999999999999)) # nosec
base_url = person_id.split('/users/')[0]
nickname = person_json['preferredUsername']
person_json['icon']['url'] = \
base_url + '/system/accounts/avatars/' + nickname + \
'/avatar' + rand_str + '.' + existing_extension
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(person_json['image']['url'])
last_part_of_filename = url_str.split('/')[-1]
2022-01-03 14:49:33 +00:00
existing_extension = last_part_of_filename.split('.')[1]
rand_str = str(randint(10000000000000, 99999999999999)) # nosec
person_json['image']['url'] = \
base_url + '/system/accounts/headers/' + nickname + \
'/image' + rand_str + '.' + existing_extension
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def get_actor_update_json(actor_json: {}) -> {}:
"""Returns the json for an Person Update
2021-09-30 13:28:43 +00:00
"""
2022-01-03 14:49:33 +00:00
pub_number, _ = get_status_number()
manually_approves_followers = actor_json['manuallyApprovesFollowers']
2023-08-27 09:30:20 +00:00
memorial = False
if actor_json.get('memorial'):
memorial = True
indexable = False
if actor_json.get('indexable'):
indexable = True
2024-01-06 11:53:05 +00:00
searchable_by = []
if actor_json.get('searchableBy'):
if isinstance(actor_json['searchableBy'], list):
searchable_by = actor_json['searchableBy']
2023-12-09 14:18:24 +00:00
actor_url = get_url_from_post(actor_json['url'])
icon_url = get_url_from_post(actor_json['icon']['url'])
image_url = get_url_from_post(actor_json['image']['url'])
return {
'@context': [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
2021-09-30 13:28:43 +00:00
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
2023-08-27 09:39:46 +00:00
"indexable": "toot:indexable",
2024-01-06 11:53:05 +00:00
"searchableBy": {
"@id": "fedibird:searchableBy",
"@type": "@id"
},
2023-08-27 09:39:46 +00:00
"memorial": "toot:memorial",
"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"
},
2022-05-11 14:34:05 +00:00
"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
}
],
2022-01-03 14:49:33 +00:00
'id': actor_json['id'] + '#updates/' + pub_number,
'type': 'Update',
2021-12-26 10:29:52 +00:00
'actor': actor_json['id'],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
2021-12-26 10:29:52 +00:00
'cc': [actor_json['id'] + '/followers'],
'object': {
2021-12-26 10:29:52 +00:00
'id': actor_json['id'],
'type': actor_json['type'],
2021-09-30 15:14:05 +00:00
'icon': {
'type': 'Image',
2023-12-09 14:18:24 +00:00
'url': icon_url
2021-09-30 15:14:05 +00:00
},
'image': {
'type': 'Image',
2023-12-09 14:18:24 +00:00
'url': image_url
2021-09-30 15:14:05 +00:00
},
2021-12-26 10:29:52 +00:00
'attachment': actor_json['attachment'],
'following': actor_json['id'] + '/following',
'followers': actor_json['id'] + '/followers',
'inbox': actor_json['id'] + '/inbox',
'outbox': actor_json['id'] + '/outbox',
'featured': actor_json['id'] + '/collections/featured',
'featuredTags': actor_json['id'] + '/collections/tags',
'preferredUsername': actor_json['preferredUsername'],
'name': actor_json['name'],
'summary': actor_json['summary'],
2023-12-09 14:18:24 +00:00
'url': actor_url,
2024-01-07 13:45:53 +00:00
'vcard:Address': '',
'vcard:bday': '',
2022-01-03 14:49:33 +00:00
'manuallyApprovesFollowers': manually_approves_followers,
2021-12-26 10:29:52 +00:00
'discoverable': actor_json['discoverable'],
2023-08-27 09:30:20 +00:00
'memorial': memorial,
'indexable': indexable,
2021-12-26 10:29:52 +00:00
'published': actor_json['published'],
2024-01-06 11:53:05 +00:00
'searchableBy': searchable_by,
2021-12-26 10:29:52 +00:00
'devices': actor_json['devices'],
2023-08-27 09:30:20 +00:00
"publicKey": actor_json['publicKey']
2021-09-30 13:28:43 +00:00
}
}
2021-09-30 13:28:43 +00:00
def get_actor_move_json(actor_json: {}) -> {}:
"""Returns the json for a Move activity after movedTo has been set
within the actor
https://codeberg.org/fediverse/fep/src/branch/main/fep/7628/fep-7628.md
"""
if not actor_json.get('movedTo'):
return None
if '://' not in actor_json['movedTo'] or \
'.' not in actor_json['movedTo']:
return None
if actor_json['movedTo'] == actor_json['id']:
return None
pub_number, _ = get_status_number()
return {
"@context": "https://www.w3.org/ns/activitystreams",
"id": actor_json['id'] + '#moved/' + pub_number,
"type": "Move",
"actor": actor_json['id'],
"object": actor_json['id'],
"target": actor_json['movedTo'],
"to": ['https://www.w3.org/ns/activitystreams#Public'],
"cc": [actor_json['id'] + '/followers']
}
2021-12-28 18:13:52 +00:00
def get_default_person_context() -> 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',
2023-08-27 09:39:46 +00:00
'indexable': 'toot:indexable',
2024-01-06 11:53:05 +00:00
'searchableBy': {
'@id': 'fedibird:searchableBy',
'@type': '@id'
},
2023-08-27 09:39:46 +00:00
'memorial': 'toot:memorial',
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',
2022-05-11 14:34:05 +00:00
'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-29 21:55:09 +00:00
def _create_person_base(base_dir: str, nickname: str, domain: str, port: int,
2022-06-09 14:46:30 +00:00
http_prefix: str, save_to_file: bool,
2021-12-29 21:55:09 +00:00
manual_follower_approval: bool,
group_account: bool,
password: str) -> (str, str, {}, {}):
2019-06-28 18:55:29 +00:00
"""Returns the private key, public key, actor and webfinger endpoint
"""
2022-01-03 14:49:33 +00:00
private_key_pem, public_key_pem = generate_rsa_key()
webfinger_endpoint = \
2021-12-29 21:55:09 +00:00
create_webfinger_endpoint(nickname, domain, port,
2024-02-26 13:05:26 +00:00
http_prefix)
2022-06-09 14:46:30 +00:00
if save_to_file:
2021-12-29 21:55:09 +00:00
store_webfinger_endpoint(nickname, domain, port,
2022-01-03 14:49:33 +00:00
base_dir, webfinger_endpoint)
2019-06-30 18:23:18 +00:00
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
2022-01-03 14:49:33 +00:00
original_domain = domain
2021-12-26 12:45:03 +00:00
domain = get_full_domain(domain, port)
2020-04-03 18:12:08 +00:00
2022-01-03 14:49:33 +00:00
person_type = 'Person'
2021-12-26 00:07:44 +00:00
if group_account:
2022-01-03 14:49:33 +00:00
person_type = 'Group'
2020-07-12 11:58:32 +00:00
# Enable follower approval by default
2022-01-03 14:49:33 +00:00
approve_followers = manual_follower_approval
person_name = nickname
person_id = local_actor_url(http_prefix, nickname, domain)
inbox_str = person_id + '/inbox'
person_url = http_prefix + '://' + domain + '/@' + person_name
2020-04-03 18:12:08 +00:00
if nickname == 'inbox':
# shared inbox
2022-01-03 14:49:33 +00:00
inbox_str = http_prefix + '://' + domain + '/actor/inbox'
person_id = http_prefix + '://' + domain + '/actor'
person_url = http_prefix + '://' + domain + \
2020-04-03 18:12:08 +00:00
'/about/more?instance_actor=true'
2022-01-03 14:49:33 +00:00
person_name = original_domain
approve_followers = True
person_type = 'Application'
2020-10-27 16:00:57 +00:00
elif nickname == 'news':
2022-01-03 14:49:33 +00:00
person_url = http_prefix + '://' + domain + \
2020-10-27 16:00:57 +00:00
'/about/more?news_actor=true'
2022-01-03 14:49:33 +00:00
approve_followers = True
person_type = '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
2022-01-03 14:49:33 +00:00
image_url = \
person_id + '/image' + \
2020-07-08 15:17:00 +00:00
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
2020-04-03 18:12:08 +00:00
2022-01-03 14:49:33 +00:00
icon_url = \
person_id + '/avatar' + \
2020-07-08 15:17:00 +00:00
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
2020-04-03 18:12:08 +00:00
2022-01-03 14:49:33 +00:00
_, published = get_status_number()
new_person = {
2020-04-03 18:12:08 +00:00
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
2021-12-28 18:13:52 +00:00
get_default_person_context()
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': [],
2022-01-03 14:49:33 +00:00
'devices': person_id + '/collections/devices',
2020-03-22 20:36:19 +00:00
'endpoints': {
2022-01-03 14:49:33 +00:00
'id': person_id + '/endpoints',
2021-12-25 17:09:22 +00:00
'sharedInbox': http_prefix + '://' + domain + '/inbox',
2023-06-27 18:21:56 +00:00
'offers': person_id + '/offers',
2023-07-11 11:15:27 +00:00
'wanted': person_id + '/wanted',
'blocked': person_id + '/blocked',
'pendingFollowers': person_id + '/pendingFollowers'
2020-03-22 20:36:19 +00:00
},
2022-01-03 14:49:33 +00:00
'featured': person_id + '/collections/featured',
'featuredTags': person_id + '/collections/tags',
'followers': person_id + '/followers',
'following': person_id + '/following',
'tts': person_id + '/speaker',
'shares': person_id + '/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',
2022-01-03 14:49:33 +00:00
'url': icon_url
2020-03-22 20:36:19 +00:00
},
2022-01-03 14:49:33 +00:00
'id': person_id,
2020-03-22 20:36:19 +00:00
'image': {
'mediaType': 'image/png',
'type': 'Image',
2022-01-03 14:49:33 +00:00
'url': image_url
2020-03-22 20:36:19 +00:00
},
2022-01-03 14:49:33 +00:00
'inbox': inbox_str,
'manuallyApprovesFollowers': approve_followers,
2024-05-16 11:50:12 +00:00
'capabilities': {
'acceptsChatMessages': False,
'supportsFriendRequests': False
},
'discoverable': True,
2023-08-27 09:30:20 +00:00
'indexable': False,
2024-01-06 11:53:05 +00:00
'searchableBy': [],
2023-08-27 09:30:20 +00:00
'memorial': False,
'hideFollows': False,
2022-01-03 14:49:33 +00:00
'name': person_name,
'outbox': person_id + '/outbox',
'preferredUsername': person_name,
2020-03-22 20:36:19 +00:00
'summary': '',
'publicKey': {
2022-01-03 14:49:33 +00:00
'id': person_id + '#main-key',
'owner': person_id,
'publicKeyPem': public_key_pem
2020-03-22 20:36:19 +00:00
},
'tag': [],
2022-01-03 14:49:33 +00:00
'type': person_type,
2024-01-07 13:45:53 +00:00
'url': person_url,
'vcard:Address': '',
'vcard:bday': ''
2019-06-28 18:55:29 +00:00
}
2023-06-17 09:37:48 +00:00
# extra fields used only by groups
if group_account:
new_person['postingRestrictedToMods'] = False
new_person['moderators'] = person_id + '/moderators'
2020-04-03 18:12:08 +00:00
if nickname == 'inbox':
# fields not needed by the shared inbox
2022-01-03 14:49:33 +00:00
del new_person['outbox']
del new_person['icon']
del new_person['image']
if new_person.get('skills'):
del new_person['skills']
del new_person['shares']
if new_person.get('roles'):
del new_person['roles']
del new_person['tag']
del new_person['availability']
del new_person['followers']
del new_person['following']
del new_person['attachment']
2022-06-09 14:46:30 +00:00
if save_to_file:
2019-06-28 18:55:29 +00:00
# save person to file
if not os.path.isdir(base_dir):
os.mkdir(base_dir)
2024-05-12 12:35:26 +00:00
people_subdir = data_dir(base_dir)
if not os.path.isdir(people_subdir):
os.mkdir(people_subdir)
if not os.path.isdir(people_subdir + '/' + handle):
os.mkdir(people_subdir + '/' + handle)
if not os.path.isdir(people_subdir + '/' +
2021-12-25 16:17:53 +00:00
handle + '/inbox'):
2024-05-12 12:35:26 +00:00
os.mkdir(people_subdir + '/' + handle + '/inbox')
if not os.path.isdir(people_subdir + '/' +
2020-04-03 18:12:08 +00:00
handle + '/outbox'):
2024-05-12 12:35:26 +00:00
os.mkdir(people_subdir + '/' + handle + '/outbox')
if not os.path.isdir(people_subdir + '/' +
2021-12-25 16:17:53 +00:00
handle + '/queue'):
2024-05-12 12:35:26 +00:00
os.mkdir(people_subdir + '/' + handle + '/queue')
filename = people_subdir + '/' + handle + '.json'
2022-01-03 14:49:33 +00:00
save_json(new_person, 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')
2022-01-03 14:49:33 +00:00
cache_filename = base_dir + '/cache/actors/' + \
new_person['id'].replace('/', '#') + '.json'
save_json(new_person, cache_filename)
2019-08-22 14:43:43 +00:00
2019-06-28 18:55:29 +00:00
# save the private key
2022-01-03 14:49:33 +00:00
private_keys_subdir = '/keys/private'
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/keys'):
os.mkdir(base_dir + '/keys')
2022-01-03 14:49:33 +00:00
if not os.path.isdir(base_dir + private_keys_subdir):
os.mkdir(base_dir + private_keys_subdir)
filename = base_dir + private_keys_subdir + '/' + handle + '.key'
2021-11-25 21:18:53 +00:00
try:
2022-06-10 14:32:48 +00:00
with open(filename, 'w+', encoding='utf-8') as text_file:
2022-01-03 14:49:33 +00:00
print(private_key_pem, file=text_file)
2021-11-25 21:18:53 +00:00
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
2022-01-03 14:49:33 +00:00
public_keys_subdir = '/keys/public'
if not os.path.isdir(base_dir + public_keys_subdir):
os.mkdir(base_dir + public_keys_subdir)
filename = base_dir + public_keys_subdir + '/' + handle + '.pem'
2021-11-25 21:18:53 +00:00
try:
2022-06-10 14:32:48 +00:00
with open(filename, 'w+', encoding='utf-8') as text_file:
2022-01-03 14:49:33 +00:00
print(public_key_pem, file=text_file)
2021-11-25 21:18:53 +00:00
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:
2022-06-21 13:35:35 +00:00
password = remove_eol(password).strip()
2021-12-28 21:36:27 +00:00
store_basic_credentials(base_dir, nickname, password)
2022-01-03 14:49:33 +00:00
return private_key_pem, public_key_pem, new_person, webfinger_endpoint
2019-06-28 18:55:29 +00:00
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def register_account(base_dir: str, http_prefix: str, domain: str, port: int,
nickname: str, password: str,
manual_follower_approval: bool) -> bool:
2019-08-08 13:38:33 +00:00
"""Registers a new account from the web interface
"""
2021-12-29 21:55:09 +00:00
if _account_exists(base_dir, nickname, domain):
return False
2021-12-28 14:41:10 +00:00
if not valid_nickname(domain, nickname):
2020-04-03 18:12:08 +00:00
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
2022-01-03 14:49:33 +00:00
(private_key_pem, _,
_, _) = create_person(base_dir, nickname,
domain, port,
http_prefix, True,
manual_follower_approval,
password)
if private_key_pem:
2019-08-08 13:38:33 +00:00
return True
return False
2020-04-03 18:12:08 +00:00
2021-12-29 21:55:09 +00:00
def create_group(base_dir: str, nickname: str, domain: str, port: int,
2022-06-09 14:46:30 +00:00
http_prefix: str, save_to_file: bool,
2024-02-19 15:38:08 +00:00
password: str) -> (str, str, {}, {}):
2019-10-04 12:39:46 +00:00
"""Returns a group
"""
2022-01-03 14:49:33 +00:00
(private_key_pem, public_key_pem,
new_person, webfinger_endpoint) = create_person(base_dir, nickname,
domain, port,
2022-06-09 14:46:30 +00:00
http_prefix, save_to_file,
2022-01-03 14:49:33 +00:00
False, password, True)
2021-07-30 10:51:33 +00:00
2022-01-03 14:49:33 +00:00
return private_key_pem, public_key_pem, new_person, webfinger_endpoint
2020-04-03 18:12:08 +00:00
2022-03-22 11:42:24 +00:00
def clear_person_qrcodes(base_dir: str) -> None:
"""Clears qrcodes for all accounts
"""
2024-05-12 12:35:26 +00:00
dir_str = data_dir(base_dir)
for _, dirs, _ in os.walk(dir_str):
2022-03-22 11:42:24 +00:00
for handle in dirs:
if '@' not in handle:
continue
nickname = handle.split('@')[0]
domain = handle.split('@')[1]
qrcode_filename = \
acct_dir(base_dir, nickname, domain) + '/qrcode.png'
if os.path.isfile(qrcode_filename):
try:
os.remove(qrcode_filename)
except OSError:
pass
if os.path.isfile(qrcode_filename + '.etag'):
try:
os.remove(qrcode_filename + '.etag')
except OSError:
pass
break
2021-12-28 18:13:52 +00:00
def save_person_qrcode(base_dir: str,
nickname: str, domain: str, qrcode_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
"""
2022-01-03 14:49:33 +00:00
qrcode_filename = acct_dir(base_dir, nickname, domain) + '/qrcode.png'
if os.path.isfile(qrcode_filename):
return
handle = get_full_domain('@' + nickname + '@' + qrcode_domain, port)
url = pyqrcode.create(handle)
2022-09-10 09:24:13 +00:00
try:
url.png(qrcode_filename, scale)
except ModuleNotFoundError:
print('EX: pyqrcode png module not found')
2021-12-29 21:55:09 +00:00
def create_person(base_dir: str, nickname: str, domain: str, port: int,
2022-06-09 14:46:30 +00:00
http_prefix: str, save_to_file: bool,
2021-12-29 21:55:09 +00:00
manual_follower_approval: bool,
password: str,
group_account: bool = False) -> (str, str, {}, {}):
2019-07-05 11:27:18 +00:00
"""Returns the private key, public key, actor and webfinger endpoint
"""
2021-12-28 14:41:10 +00:00
if not valid_nickname(domain, nickname):
2020-04-03 18:12:08 +00:00
return None, None, None, None
2019-08-08 10:50:58 +00:00
2019-08-09 09:14:31 +00:00
# If a config.json file doesn't exist then don't decrement
# remaining registrations counter
2020-11-24 12:42:33 +00:00
if nickname != 'news':
2022-01-03 14:49:33 +00:00
remaining_config_exists = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'registrationsRemaining')
2022-01-03 14:49:33 +00:00
if remaining_config_exists:
registrations_remaining = int(remaining_config_exists)
if registrations_remaining <= 0:
2020-11-24 12:42:33 +00:00
return None, None, None, None
else:
2024-05-12 12:35:26 +00:00
dir_str = data_dir(base_dir)
if os.path.isdir(dir_str + '/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
2022-01-03 14:49:33 +00:00
(private_key_pem, public_key_pem,
new_person, webfinger_endpoint) = _create_person_base(base_dir, nickname,
domain, port,
http_prefix,
2022-06-09 14:46:30 +00:00
save_to_file,
2022-01-03 14:49:33 +00:00
manual_follower,
group_account,
password)
2021-12-26 14:08:58 +00:00
if not get_config_param(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-27 20:38:02 +00:00
set_config_param(base_dir, 'admin', nickname)
2021-12-28 22:22:09 +00:00
set_role(base_dir, nickname, domain, 'admin')
set_role(base_dir, nickname, domain, 'moderator')
set_role(base_dir, nickname, domain, 'editor')
2021-12-25 16:17:53 +00:00
2024-05-12 12:35:26 +00:00
dir_str = data_dir(base_dir)
if not os.path.isdir(dir_str):
os.mkdir(dir_str)
2022-01-03 14:49:33 +00:00
account_dir = acct_dir(base_dir, nickname, domain)
if not os.path.isdir(account_dir):
os.mkdir(account_dir)
2020-08-29 11:14:19 +00:00
2021-12-25 21:42:26 +00:00
if manual_follower_approval:
2022-01-03 14:49:33 +00:00
follow_dms_filename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, nickname, domain) + '/.followDMs'
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(follow_dms_filename, 'w+', encoding='utf-8') as ffile:
2022-01-03 14:49:33 +00:00
ffile.write('\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + follow_dms_filename)
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':
2022-01-03 14:49:33 +00:00
notify_likes_filename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, nickname, domain) + '/.notifyLikes'
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(notify_likes_filename, 'w+', encoding='utf-8') as nfile:
2022-01-03 14:49:33 +00:00
nfile.write('\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + notify_likes_filename)
2020-11-24 12:42:33 +00:00
# notify when posts have emoji reactions
if nickname != 'news':
2022-01-03 14:49:33 +00:00
notify_reactions_filename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, nickname, domain) + '/.notifyReactions'
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(notify_reactions_filename, 'w+',
encoding='utf-8') as nfile:
2022-01-03 14:49:33 +00:00
nfile.write('\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + notify_reactions_filename)
2021-12-26 14:08:58 +00:00
theme = get_config_param(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'):
2022-01-03 14:49:33 +00:00
account_dir = acct_dir(base_dir, nickname, domain)
2021-12-25 16:17:53 +00:00
copyfile(base_dir + '/img/default-avatar.png',
2022-01-03 14:49:33 +00:00
account_dir + '/avatar.png')
2020-11-24 12:42:33 +00:00
else:
2022-01-03 14:49:33 +00:00
news_avatar = base_dir + '/theme/' + theme + '/icons/avatar_news.png'
if os.path.isfile(news_avatar):
account_dir = acct_dir(base_dir, nickname, domain)
copyfile(news_avatar, account_dir + '/avatar.png')
2020-11-24 12:42:33 +00:00
2022-01-03 14:49:33 +00:00
default_profile_image_filename = 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'):
2022-01-03 14:49:33 +00:00
default_profile_image_filename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + theme + '/image.png'
2022-01-03 14:49:33 +00:00
if os.path.isfile(default_profile_image_filename):
account_dir = acct_dir(base_dir, nickname, domain)
copyfile(default_profile_image_filename, account_dir + '/image.png')
default_banner_filename = 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'):
2022-01-03 14:49:33 +00:00
default_banner_filename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + theme + '/banner.png'
2022-01-03 14:49:33 +00:00
if os.path.isfile(default_banner_filename):
account_dir = acct_dir(base_dir, nickname, domain)
copyfile(default_banner_filename, account_dir + '/banner.png')
if nickname != 'news' and remaining_config_exists:
registrations_remaining -= 1
2021-12-27 20:38:02 +00:00
set_config_param(base_dir, 'registrationsRemaining',
2022-01-03 14:49:33 +00:00
str(registrations_remaining))
save_person_qrcode(base_dir, nickname, domain, domain, port)
2022-01-03 14:49:33 +00:00
return private_key_pem, public_key_pem, new_person, webfinger_endpoint
2019-07-05 11:27:18 +00:00
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def create_shared_inbox(base_dir: str, nickname: str, domain: str, port: int,
http_prefix: str) -> (str, str, {}, {}):
2019-07-05 11:27:18 +00:00
"""Generates the shared inbox
"""
2021-12-29 21:55:09 +00:00
return _create_person_base(base_dir, nickname, domain, port, http_prefix,
True, True, False, None)
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def create_news_inbox(base_dir: str, domain: str, port: int,
http_prefix: str) -> (str, str, {}, {}):
2020-10-07 16:01:45 +00:00
"""Generates the news inbox
"""
2021-12-29 21:55:09 +00:00
return create_person(base_dir, 'news', domain, port,
http_prefix, True, True, None)
2020-10-07 16:01:45 +00:00
2022-01-03 14:49:33 +00:00
def person_upgrade_actor(base_dir: str, person_json: {},
2022-06-12 12:30:14 +00:00
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
"""
2022-01-03 14:49:33 +00:00
update_actor = 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
2022-01-03 14:49:33 +00:00
if not person_json:
person_json = load_json(filename)
2020-01-19 20:58:50 +00:00
# add extra group fields
if person_json.get('type') and person_json.get('id'):
if person_json['type'] == 'Group':
person_json['postingRestrictedToMods'] = False
person_id = person_json['id']
person_json['moderators'] = person_id + '/moderators'
update_actor = True
2024-05-16 09:02:33 +00:00
if 'capabilities' not in person_json:
2024-05-16 11:50:12 +00:00
person_json['capabilities'] = {
'acceptsChatMessages': False,
'supportsFriendRequests': False
}
2024-05-16 09:02:33 +00:00
update_actor = True
else:
if 'acceptsChatMessages' not in person_json['capabilities']:
person_json['capabilities']['acceptsChatMessages'] = False
update_actor = True
2024-05-16 11:50:12 +00:00
if 'supportsFriendRequests' not in person_json['capabilities']:
person_json['capabilities']['supportsFriendRequests'] = False
update_actor = True
2024-05-16 09:02:33 +00:00
2024-01-06 11:53:05 +00:00
if 'memorial' not in person_json:
2023-08-27 09:30:20 +00:00
person_json['memorial'] = False
update_actor = True
2024-01-06 11:53:05 +00:00
if 'hideFollows' not in person_json:
person_json['hideFollows'] = False
update_actor = True
2024-01-06 11:53:05 +00:00
if 'indexable' not in person_json:
2023-08-27 09:30:20 +00:00
person_json['indexable'] = False
update_actor = True
2024-01-07 13:45:53 +00:00
if 'vcard:Address' not in person_json:
person_json['vcard:Address'] = ''
update_actor = True
if 'vcard:bday' not in person_json:
person_json['vcard:bday'] = ''
update_actor = True
2024-01-06 11:53:05 +00:00
if 'searchableBy' not in person_json:
person_json['searchableBy'] = []
update_actor = True
2021-05-08 17:13:46 +00:00
# add a speaker endpoint
2022-01-03 14:49:33 +00:00
if not person_json.get('tts'):
person_json['tts'] = person_json['id'] + '/speaker'
update_actor = True
if not person_json.get('published'):
_, published = get_status_number()
person_json['published'] = published
update_actor = True
2023-06-27 18:21:56 +00:00
if person_json.get('endpoints'):
2023-07-11 11:15:27 +00:00
if not person_json['endpoints'].get('pendingFollowers'):
person_json['endpoints']['pendingFollowers'] = \
person_json['id'] + '/pendingFollowers'
update_actor = True
2023-07-05 11:17:25 +00:00
if not person_json['endpoints'].get('blocked'):
2023-07-05 11:56:02 +00:00
person_json['endpoints']['blocked'] = \
person_json['id'] + '/blocked'
2023-07-05 11:17:25 +00:00
update_actor = True
2023-06-27 18:21:56 +00:00
if not person_json['endpoints'].get('offers'):
person_json['endpoints']['offers'] = person_json['id'] + '/offers'
update_actor = True
if not person_json['endpoints'].get('wanted'):
person_json['endpoints']['wanted'] = person_json['id'] + '/wanted'
update_actor = True
2022-01-03 14:49:33 +00:00
if person_json.get('shares'):
if person_json['shares'].endswith('/shares'):
person_json['shares'] = person_json['id'] + '/catalog'
update_actor = True
occupation_name = ''
if person_json.get('occupationName'):
occupation_name = person_json['occupationName']
del person_json['occupationName']
update_actor = True
if person_json.get('occupation'):
occupation_name = person_json['occupation']
del person_json['occupation']
update_actor = 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
2022-01-03 14:49:33 +00:00
if not person_json.get('hasOccupation'):
person_json['hasOccupation'] = [{
2021-07-04 11:39:13 +00:00
'@type': 'Occupation',
2022-01-03 14:49:33 +00:00
'name': occupation_name,
2021-07-04 11:39:13 +00:00
"occupationLocation": {
"@type": "City",
"name": "Fediverse"
},
'skills': []
}]
2022-01-03 14:49:33 +00:00
update_actor = True
2021-05-14 18:02:58 +00:00
2021-05-13 20:21:37 +00:00
# remove the old skills format
2022-01-03 14:49:33 +00:00
if person_json.get('skills'):
del person_json['skills']
update_actor = True
2021-05-13 20:21:37 +00:00
# if the older roles format is being used then switch
# to the new one
2022-01-03 14:49:33 +00:00
if person_json.get('affiliation'):
del person_json['affiliation']
update_actor = True
2022-01-03 14:49:33 +00:00
if not isinstance(person_json['hasOccupation'], list):
person_json['hasOccupation'] = [{
2021-07-04 11:39:13 +00:00
'@type': 'Occupation',
2022-01-03 14:49:33 +00:00
'name': occupation_name,
2021-07-04 11:39:13 +00:00
'occupationLocation': {
'@type': 'City',
'name': 'Fediverse'
},
'skills': []
}]
2022-01-03 14:49:33 +00:00
update_actor = True
2021-05-17 09:28:15 +00:00
else:
# add location if it is missing
2022-01-08 10:58:54 +00:00
for index, _ in enumerate(person_json['hasOccupation']):
2022-01-03 14:49:33 +00:00
oc_item = person_json['hasOccupation'][index]
if oc_item.get('hasOccupation'):
oc_item = oc_item['hasOccupation']
if oc_item.get('location'):
del oc_item['location']
update_actor = True
if not oc_item.get('occupationLocation'):
oc_item['occupationLocation'] = {
2021-05-17 10:27:14 +00:00
"@type": "City",
"name": "Fediverse"
2021-05-17 09:28:15 +00:00
}
2022-01-03 14:49:33 +00:00
update_actor = True
2021-05-17 10:29:07 +00:00
else:
2022-01-03 14:49:33 +00:00
if oc_item['occupationLocation']['@type'] != 'City':
oc_item['occupationLocation'] = {
2021-05-17 10:29:07 +00:00
"@type": "City",
"name": "Fediverse"
}
2022-01-03 14:49:33 +00:00
update_actor = 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
2022-01-03 14:49:33 +00:00
roles_list = get_actor_roles_list(person_json)
if not roles_list:
2021-12-26 14:11:30 +00:00
admin_name = get_config_param(base_dir, 'admin')
2022-01-03 14:49:33 +00:00
if person_json['id'].endswith('/users/' + admin_name):
roles_list = ["admin", "moderator", "editor"]
2022-09-02 18:06:13 +00:00
actor_roles_from_list(person_json, roles_list)
2022-01-03 14:49:33 +00:00
update_actor = True
2021-05-13 20:21:37 +00:00
# remove the old roles format
2022-01-03 14:49:33 +00:00
if person_json.get('roles'):
del person_json['roles']
update_actor = True
2022-01-03 14:49:33 +00:00
if update_actor:
person_json['@context'] = [
2021-05-13 11:14:14 +00:00
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
2021-12-28 18:13:52 +00:00
get_default_person_context()
2022-01-03 14:49:33 +00:00
]
2021-05-13 11:14:14 +00:00
2022-01-03 14:49:33 +00:00
save_json(person_json, filename)
2020-01-19 20:58:50 +00:00
# also update the actor within the cache
2022-01-03 14:49:33 +00:00
actor_cache_filename = \
2024-05-12 12:35:26 +00:00
data_dir(base_dir) + '/cache/actors/' + \
2022-01-03 14:49:33 +00:00
person_json['id'].replace('/', '#') + '.json'
if os.path.isfile(actor_cache_filename):
save_json(person_json, actor_cache_filename)
2020-01-19 20:58:50 +00:00
# update domain/@nickname in actors cache
2022-01-03 14:49:33 +00:00
actor_cache_filename = \
2024-05-12 12:35:26 +00:00
data_dir(base_dir) + '/cache/actors/' + \
2022-01-03 14:49:33 +00:00
replace_users_with_at(person_json['id']).replace('/', '#') + \
2020-04-03 18:12:08 +00:00
'.json'
2022-01-03 14:49:33 +00:00
if os.path.isfile(actor_cache_filename):
save_json(person_json, actor_cache_filename)
2020-04-03 18:12:08 +00:00
2020-01-19 20:29:39 +00:00
2022-03-14 15:51:12 +00:00
def add_alternate_domains(actor_json: {}, domain: str,
onion_domain: str, i2p_domain: str) -> None:
"""Adds alternate onion and/or i2p domains to alsoKnownAs
"""
if not onion_domain and not i2p_domain:
return
if not actor_json.get('id'):
return
if domain not in actor_json['id']:
return
nickname = get_nickname_from_actor(actor_json['id'])
if not nickname:
return
if 'alsoKnownAs' not in actor_json:
actor_json['alsoKnownAs'] = []
if onion_domain:
onion_actor = 'http://' + onion_domain + '/users/' + nickname
if onion_actor not in actor_json['alsoKnownAs']:
actor_json['alsoKnownAs'].append(onion_actor)
if i2p_domain:
i2p_actor = 'http://' + i2p_domain + '/users/' + nickname
if i2p_actor not in actor_json['alsoKnownAs']:
actor_json['alsoKnownAs'].append(i2p_actor)
2021-12-28 18:13:52 +00:00
def person_lookup(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
"""
if path.endswith('#/publicKey'):
path = path.replace('#/publicKey', '')
elif path.endswith('/main-key'):
path = path.replace('/main-key', '')
elif 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?
2022-01-03 14:49:33 +00:00
is_shared_inbox = False
if path in ('/inbox', '/users/inbox', '/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'
2022-01-03 14:49:33 +00:00
is_shared_inbox = True
2019-08-05 15:22:59 +00:00
else:
2022-01-03 14:49:33 +00:00
not_person_lookup = ('/inbox', '/outbox', '/outboxarchive',
'/followers', '/following', '/featured',
'.png', '.jpg', '.gif', '.svg', '.mpv')
for ending in not_person_lookup:
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('/@'):
2023-04-23 15:55:48 +00:00
if '/@/' not in path:
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
2022-01-03 14:49:33 +00:00
if not is_shared_inbox and not valid_nickname(domain, nickname):
2019-06-28 18:55:29 +00:00
return None
2021-12-26 18:17:37 +00:00
domain = remove_domain_port(domain)
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
2022-12-18 15:29:54 +00:00
filename = acct_handle_dir(base_dir, handle) + '.json'
2019-06-28 18:55:29 +00:00
if not os.path.isfile(filename):
return None
2022-01-03 14:49:33 +00:00
person_json = load_json(filename)
if not is_shared_inbox:
2022-06-12 12:30:14 +00:00
person_upgrade_actor(base_dir, person_json, filename)
2022-01-03 14:49:33 +00:00
# if not person_json:
# person_json={"user": "unknown"}
return person_json
2019-06-29 14:35:26 +00:00
2020-04-03 18:12:08 +00:00
2021-12-28 16:50:20 +00:00
def person_box_json(recent_posts_cache: {},
2022-09-08 17:59:19 +00:00
base_dir: str, domain: str, port: int, path: str,
2022-01-03 14:49:33 +00:00
http_prefix: str, no_of_items: int, boxname: str,
2021-12-28 16:50:20 +00:00
authorized: bool,
newswire_votes_threshold: int, positive_voting: bool,
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
"""
2022-01-03 14:49:33 +00:00
if boxname not in ('inbox', 'dm', 'tlreplies', 'tlmedia', 'tlblogs',
'tlnews', 'tlfeatures', 'outbox', 'moderation',
'tlbookmarks', 'bookmarks'):
2021-12-28 16:50:20 +00:00
print('ERROR: person_box_json 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
2022-01-03 14:49:33 +00:00
header_only = True
2019-06-29 17:12:26 +00:00
2022-11-20 17:10:20 +00:00
# first post in the timeline
first_post_id = ''
if ';firstpost=' in path:
first_post_id = \
path.split(';firstpost=')[1]
2022-12-28 14:14:44 +00:00
if ';' in first_post_id:
first_post_id = first_post_id.split(';')[0]
2022-11-20 17:10:20 +00:00
first_post_id = \
first_post_id.replace('--', '/')
2019-06-29 16:47:37 +00:00
# handle page numbers
2022-01-03 14:49:33 +00:00
page_number = None
2019-06-29 16:47:37 +00:00
if '?page=' in path:
2022-01-03 14:49:33 +00:00
page_number = path.split('?page=')[1]
2022-11-20 17:10:20 +00:00
if ';' in page_number:
page_number = page_number.split(';')[0]
if len(page_number) > 5:
page_number = 1
2022-01-03 14:49:33 +00:00
if page_number == 'true':
page_number = 1
2019-06-29 16:47:37 +00:00
else:
try:
2022-01-03 14:49:33 +00:00
page_number = int(page_number)
2020-04-03 18:12:08 +00:00
except BaseException:
2021-12-28 16:50:20 +00:00
print('EX: person_box_json unable to convert to int ' +
2022-01-03 14:49:33 +00:00
str(page_number))
2020-04-03 18:12:08 +00:00
path = path.split('?page=')[0]
2022-01-03 14:49:33 +00:00
header_only = 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('/@'):
2023-04-23 15:55:48 +00:00
if '/@/' not in path:
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
2021-12-28 14:41:10 +00:00
if not valid_nickname(domain, nickname):
2019-06-29 15:18:35 +00:00
return None
2020-04-03 18:12:08 +00:00
if boxname == 'inbox':
2021-12-29 21:55:09 +00:00
return create_inbox(recent_posts_cache,
2022-05-31 15:16:55 +00:00
base_dir, nickname, domain, port,
2021-12-29 21:55:09 +00:00
http_prefix,
2022-11-20 17:10:20 +00:00
no_of_items, header_only, page_number,
first_post_id)
2022-01-03 14:49:33 +00:00
if boxname == 'dm':
2021-12-29 21:55:09 +00:00
return create_dm_timeline(recent_posts_cache,
2022-05-31 15:16:55 +00:00
base_dir, nickname, domain, port,
2021-12-29 21:55:09 +00:00
http_prefix,
2022-11-20 18:09:30 +00:00
no_of_items, header_only, page_number,
first_post_id)
2022-01-03 14:49:33 +00:00
if boxname in ('tlbookmarks', 'bookmarks'):
2022-05-31 15:16:55 +00:00
return create_bookmarks_timeline(base_dir, nickname, domain,
2021-12-29 21:55:09 +00:00
port, http_prefix,
2022-01-03 14:49:33 +00:00
no_of_items, header_only,
page_number)
if boxname == 'tlreplies':
2021-12-29 21:55:09 +00:00
return create_replies_timeline(recent_posts_cache,
2022-05-31 15:16:55 +00:00
base_dir, nickname, domain,
2021-12-25 17:09:22 +00:00
port, http_prefix,
2022-01-03 14:49:33 +00:00
no_of_items, header_only,
2022-11-20 18:09:30 +00:00
page_number,
first_post_id)
2022-01-03 14:49:33 +00:00
if boxname == 'tlmedia':
2022-05-31 15:16:55 +00:00
return create_media_timeline(base_dir, nickname, domain, port,
2022-01-03 14:49:33 +00:00
http_prefix, no_of_items, header_only,
page_number)
if boxname == 'tlnews':
2022-05-31 15:16:55 +00:00
return create_news_timeline(base_dir, domain, port,
2022-01-03 14:49:33 +00:00
http_prefix, no_of_items, header_only,
2021-12-29 21:55:09 +00:00
newswire_votes_threshold, positive_voting,
2022-01-03 14:49:33 +00:00
voting_time_mins, page_number)
if boxname == 'tlfeatures':
2022-05-31 15:16:55 +00:00
return create_features_timeline(base_dir, nickname, domain, port,
2022-01-03 14:49:33 +00:00
http_prefix, no_of_items, header_only,
page_number)
if boxname == 'tlblogs':
2022-05-31 15:16:55 +00:00
return create_blogs_timeline(base_dir, nickname, domain, port,
2022-01-03 14:49:33 +00:00
http_prefix, no_of_items, header_only,
2023-01-02 10:44:49 +00:00
page_number)
2022-01-03 14:49:33 +00:00
if boxname == 'outbox':
2022-05-31 15:16:55 +00:00
return create_outbox(base_dir, nickname, domain, port,
2021-12-29 21:55:09 +00:00
http_prefix,
2022-01-03 14:49:33 +00:00
no_of_items, header_only, authorized,
page_number)
if boxname == 'moderation':
2021-12-29 21:55:09 +00:00
return create_moderation(base_dir, nickname, domain, port,
http_prefix,
2022-01-03 14:49:33 +00:00
no_of_items, header_only,
page_number)
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-29 21:55:09 +00:00
def set_display_nickname(base_dir: str, nickname: str, domain: str,
2022-06-09 14:46:30 +00:00
display_name: str) -> bool:
2022-06-12 10:58:00 +00:00
"""Sets the display name for an account
"""
2022-06-09 14:46:30 +00:00
if len(display_name) > 32:
2019-06-28 18:55:29 +00:00
return False
2020-09-15 09:16:03 +00:00
handle = nickname + '@' + domain
2022-12-18 15:29:54 +00:00
filename = acct_handle_dir(base_dir, 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
2022-01-03 14:49:33 +00:00
person_json = load_json(filename)
if not person_json:
2019-06-28 18:55:29 +00:00
return False
2022-06-09 14:46:30 +00:00
person_json['name'] = display_name
2022-01-03 14:49:33 +00:00
save_json(person_json, 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-29 21:55:09 +00:00
def set_bio(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
2022-12-18 15:29:54 +00:00
filename = acct_handle_dir(base_dir, 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
2022-01-03 14:49:33 +00:00
person_json = load_json(filename)
if not person_json:
2019-06-28 20:00:25 +00:00
return False
2022-01-03 14:49:33 +00:00
if not person_json.get('summary'):
2019-06-28 20:00:25 +00:00
return False
2022-01-03 14:49:33 +00:00
person_json['summary'] = bio
2019-09-30 22:39:02 +00:00
2022-01-03 14:49:33 +00:00
save_json(person_json, 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-28 18:13:52 +00:00
def reenable_account(base_dir: str, nickname: str) -> None:
2022-06-09 14:46:30 +00:00
"""Removes an account suspension
2019-08-13 09:24:55 +00:00
"""
2024-05-12 12:35:26 +00:00
suspended_filename = data_dir(base_dir) + '/suspended.txt'
2022-01-03 14:49:33 +00:00
if os.path.isfile(suspended_filename):
2021-06-22 12:27:10 +00:00
lines = []
2022-06-09 14:46:30 +00:00
with open(suspended_filename, 'r', encoding='utf-8') as fp_sus:
2022-01-03 14:49:33 +00:00
lines = fp_sus.readlines()
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(suspended_filename, 'w+', encoding='utf-8') as fp_sus:
2021-11-25 21:18:53 +00:00
for suspended in lines:
if suspended.strip('\n').strip('\r') != nickname:
2022-01-03 14:49:33 +00:00
fp_sus.write(suspended)
2021-12-25 15:28:52 +00:00
except OSError as ex:
2022-01-03 14:49:33 +00:00
print('EX: unable to save ' + suspended_filename +
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-28 18:13:52 +00:00
def suspend_account(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-31 21:18:12 +00:00
admin_nickname = get_config_param(base_dir, 'admin')
if not admin_nickname:
2020-10-10 16:10:32 +00:00
return
2021-12-31 21:18:12 +00:00
if nickname == admin_nickname:
2019-08-13 09:24:55 +00:00
return
# Don't suspend moderators
2024-05-12 12:35:26 +00:00
moderators_file = data_dir(base_dir) + '/moderators.txt'
2022-01-03 14:49:33 +00:00
if os.path.isfile(moderators_file):
2022-06-09 14:46:30 +00:00
with open(moderators_file, 'r', encoding='utf-8') as fp_mod:
2022-01-03 14:49:33 +00:00
lines = fp_mod.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
2022-01-03 14:49:33 +00:00
salt_filename = acct_dir(base_dir, nickname, domain) + '/.salt'
if os.path.isfile(salt_filename):
try:
2022-01-03 14:49:33 +00:00
os.remove(salt_filename)
2021-11-25 18:42:38 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: suspend_account unable to delete ' + salt_filename)
token_filename = acct_dir(base_dir, nickname, domain) + '/.token'
if os.path.isfile(token_filename):
try:
2022-01-03 14:49:33 +00:00
os.remove(token_filename)
2021-11-25 18:42:38 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: suspend_account unable to delete ' + token_filename)
2020-03-22 21:16:02 +00:00
2024-05-12 12:35:26 +00:00
suspended_filename = data_dir(base_dir) + '/suspended.txt'
2022-01-03 14:49:33 +00:00
if os.path.isfile(suspended_filename):
2022-06-09 14:46:30 +00:00
with open(suspended_filename, 'r', encoding='utf-8') as fp_sus:
2022-01-03 14:49:33 +00:00
lines = fp_sus.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:
2022-06-09 14:46:30 +00:00
with open(suspended_filename, 'a+', encoding='utf-8') as fp_sus:
2022-01-03 14:49:33 +00:00
fp_sus.write(nickname + '\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to append ' + suspended_filename)
2019-08-13 09:24:55 +00:00
else:
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(suspended_filename, 'w+', encoding='utf-8') as fp_sus:
2022-01-03 14:49:33 +00:00
fp_sus.write(nickname + '\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + suspended_filename)
2019-08-13 11:59:38 +00:00
2020-04-03 18:12:08 +00:00
2022-06-12 12:30:14 +00:00
def can_remove_post(base_dir: str,
2021-12-28 18:13:52 +00:00
domain: str, port: int, post_id: str) -> bool:
"""Returns true if the given post can be removed
"""
2023-08-14 18:46:27 +00:00
if not contains_statuses(post_id):
return False
2021-12-26 12:45:03 +00:00
domain_full = get_full_domain(domain, port)
# is the post by the admin?
2021-12-31 21:18:12 +00:00
admin_nickname = get_config_param(base_dir, 'admin')
if not admin_nickname:
2020-10-10 16:10:32 +00:00
return False
2021-12-31 21:18:12 +00:00
if domain_full + '/users/' + admin_nickname + '/' in post_id:
return False
# is the post by a moderator?
2024-05-12 12:35:26 +00:00
moderators_file = data_dir(base_dir) + '/moderators.txt'
2022-01-03 14:49:33 +00:00
if os.path.isfile(moderators_file):
2022-06-09 14:46:30 +00:00
with open(moderators_file, 'r', encoding='utf-8') as fp_mod:
2022-01-03 14:49:33 +00:00
lines = fp_mod.readlines()
for moderator in lines:
2021-12-26 19:47:06 +00:00
if domain_full + '/users/' + \
moderator.strip('\n') + '/' in post_id:
return False
return True
2020-04-03 18:12:08 +00:00
2021-12-29 21:55:09 +00:00
def _remove_tags_for_nickname(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 12:45:03 +00:00
domain_full = get_full_domain(domain, port)
2022-01-03 14:49:33 +00:00
match_str = domain_full + '/users/' + nickname + '/'
2021-12-25 16:17:53 +00:00
directory = os.fsencode(base_dir + '/tags/')
2022-01-03 14:49:33 +00:00
for fname in os.scandir(directory):
filename = os.fsdecode(fname.name)
2019-08-13 12:14:11 +00:00
if not filename.endswith(".txt"):
continue
2020-09-12 09:50:24 +00:00
try:
2023-05-25 08:52:19 +00:00
tag_filename = os.path.join(base_dir + '/tags/', filename)
2022-08-22 20:19:20 +00:00
except OSError:
2021-12-29 21:55:09 +00:00
print('EX: _remove_tags_for_nickname unable to join ' +
2023-05-25 08:52:19 +00:00
base_dir + '/tags/ ' + str(filename))
2020-09-12 09:50:24 +00:00
continue
2022-01-03 14:49:33 +00:00
if not os.path.isfile(tag_filename):
2019-11-27 09:51:59 +00:00
continue
2022-06-10 11:43:33 +00:00
if not text_in_file(match_str, tag_filename):
2019-08-13 12:14:11 +00:00
continue
2021-06-22 12:27:10 +00:00
lines = []
2022-06-09 14:46:30 +00:00
with open(tag_filename, 'r', encoding='utf-8') as fp_tag:
2022-01-03 14:49:33 +00:00
lines = fp_tag.readlines()
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(tag_filename, 'w+', encoding='utf-8') as tag_file:
2021-11-25 21:18:53 +00:00
for tagline in lines:
2022-01-03 14:49:33 +00:00
if match_str not in tagline:
tag_file.write(tagline)
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + tag_filename)
2019-08-13 12:14:11 +00:00
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def remove_account(base_dir: 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
2021-12-31 21:18:12 +00:00
admin_nickname = get_config_param(base_dir, 'admin')
if not admin_nickname:
2020-10-10 16:10:32 +00:00
return False
2021-12-31 21:18:12 +00:00
if nickname == admin_nickname:
2019-08-13 11:59:38 +00:00
return False
2019-08-13 12:00:17 +00:00
# Don't remove moderators
2024-05-12 12:35:26 +00:00
moderators_file = data_dir(base_dir) + '/moderators.txt'
2022-01-03 14:49:33 +00:00
if os.path.isfile(moderators_file):
2022-06-09 14:46:30 +00:00
with open(moderators_file, 'r', encoding='utf-8') as fp_mod:
2022-01-03 14:49:33 +00:00
lines = fp_mod.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-28 18:13:52 +00:00
reenable_account(base_dir, nickname)
2020-04-03 18:12:08 +00:00
handle = nickname + '@' + domain
2021-12-29 21:55:09 +00:00
remove_password(base_dir, nickname)
_remove_tags_for_nickname(base_dir, nickname, domain, port)
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/deactivated/' + handle):
shutil.rmtree(base_dir + '/deactivated/' + handle,
2024-05-16 15:57:44 +00:00
ignore_errors=False)
2022-12-18 15:29:54 +00:00
handle_dir = acct_handle_dir(base_dir, handle)
if os.path.isdir(handle_dir):
2024-05-16 15:57:44 +00:00
shutil.rmtree(handle_dir, ignore_errors=False)
2022-12-18 15:29:54 +00:00
if os.path.isfile(handle_dir + '.json'):
try:
2022-12-18 15:29:54 +00:00
os.remove(handle_dir + '.json')
2021-11-25 18:42:38 +00:00
except OSError:
2021-12-28 18:13:52 +00:00
print('EX: remove_account unable to delete ' +
2022-12-18 15:29:54 +00:00
handle_dir + '.json')
2021-12-25 16:17:53 +00:00
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-12-28 18:13:52 +00:00
print('EX: remove_account 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-12-28 18:13:52 +00:00
print('EX: remove_account 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-12-28 18:13:52 +00:00
print('EX: remove_account 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,
2024-05-16 15:57:44 +00:00
ignore_errors=False)
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-12-28 18:13:52 +00:00
print('EX: remove_account 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,
2024-05-16 15:57:44 +00:00
ignore_errors=False)
2021-12-26 12:10:21 +00:00
refresh_newswire(base_dir)
2019-08-13 11:59:38 +00:00
return True
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def deactivate_account(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
2022-12-18 15:29:54 +00:00
account_dir = acct_handle_dir(base_dir, handle)
2022-01-03 14:49:33 +00:00
if not os.path.isdir(account_dir):
2019-11-05 10:37:37 +00:00
return False
2022-01-03 14:49:33 +00:00
deactivated_dir = base_dir + '/deactivated'
if not os.path.isdir(deactivated_dir):
os.mkdir(deactivated_dir)
shutil.move(account_dir, deactivated_dir + '/' + 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'):
2022-01-03 14:49:33 +00:00
deactivated_webfinger_dir = base_dir + '/wfdeactivated'
if not os.path.isdir(deactivated_webfinger_dir):
os.mkdir(deactivated_webfinger_dir)
2021-12-25 16:17:53 +00:00
shutil.move(base_dir + '/wfendpoints/' + handle + '.json',
2022-01-03 14:49:33 +00:00
deactivated_webfinger_dir + '/' + 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):
2022-01-03 14:49:33 +00:00
deactivated_sharefiles_dir = base_dir + '/sharefilesdeactivated'
if not os.path.isdir(deactivated_sharefiles_dir):
os.mkdir(deactivated_sharefiles_dir)
2021-12-25 16:17:53 +00:00
shutil.move(base_dir + '/sharefiles/' + nickname,
2022-01-03 14:49:33 +00:00
deactivated_sharefiles_dir + '/' + nickname)
2021-12-26 12:10:21 +00:00
refresh_newswire(base_dir)
2022-01-03 14:49:33 +00:00
return os.path.isdir(deactivated_dir + '/' + nickname + '@' + domain)
2020-04-03 18:12:08 +00:00
def activate_account2(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
2022-01-03 14:49:33 +00:00
deactivated_dir = base_dir + '/deactivated'
deactivated_account_dir = deactivated_dir + '/' + handle
if os.path.isdir(deactivated_account_dir):
2022-12-18 15:29:54 +00:00
account_dir = acct_handle_dir(base_dir, handle)
2022-01-03 14:49:33 +00:00
if not os.path.isdir(account_dir):
shutil.move(deactivated_account_dir, account_dir)
2020-04-03 18:12:08 +00:00
2022-01-03 14:49:33 +00:00
deactivated_webfinger_dir = base_dir + '/wfdeactivated'
if os.path.isfile(deactivated_webfinger_dir + '/' + handle + '.json'):
shutil.move(deactivated_webfinger_dir + '/' + handle + '.json',
2021-12-25 16:17:53 +00:00
base_dir + '/wfendpoints/' + handle + '.json')
2019-11-05 12:07:18 +00:00
2022-01-03 14:49:33 +00:00
deactivated_sharefiles_dir = base_dir + '/sharefilesdeactivated'
if os.path.isdir(deactivated_sharefiles_dir + '/' + nickname):
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/sharefiles/' + nickname):
2022-01-03 14:49:33 +00:00
shutil.move(deactivated_sharefiles_dir + '/' + nickname,
2021-12-25 16:17:53 +00:00
base_dir + '/sharefiles/' + nickname)
2019-11-05 12:07:18 +00:00
2021-12-26 12:10:21 +00:00
refresh_newswire(base_dir)
2019-11-06 11:39:41 +00:00
2021-12-29 21:55:09 +00:00
def is_person_snoozed(base_dir: str, nickname: str, domain: str,
2022-01-03 14:49:33 +00:00
snooze_actor: str) -> bool:
2019-11-06 11:39:41 +00:00
"""Returns true if the given actor is snoozed
"""
2022-01-03 14:49:33 +00:00
snoozed_filename = acct_dir(base_dir, nickname, domain) + '/snoozed.txt'
if not os.path.isfile(snoozed_filename):
2019-11-06 11:39:41 +00:00
return False
2022-06-10 11:43:33 +00:00
if not text_in_file(snooze_actor + ' ', snoozed_filename):
2019-11-06 11:39:41 +00:00
return False
# remove the snooze entry if it has timed out
2022-01-03 14:49:33 +00:00
replace_str = None
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'r', encoding='utf-8') as snoozed_file:
2022-01-03 14:49:33 +00:00
for line in snoozed_file:
2019-11-06 11:39:41 +00:00
# is this the entry for the actor?
2022-01-03 14:49:33 +00:00
if line.startswith(snooze_actor + ' '):
2022-06-21 11:58:50 +00:00
snoozed_time_str1 = line.split(' ')[1]
snoozed_time_str = remove_eol(snoozed_time_str1)
2019-11-06 11:39:41 +00:00
# is there a time appended?
2022-01-03 14:49:33 +00:00
if snoozed_time_str.isdigit():
snoozed_time = int(snoozed_time_str)
2021-12-26 13:17:46 +00:00
curr_time = int(time.time())
2019-11-06 11:39:41 +00:00
# has the snooze timed out?
2022-01-03 14:49:33 +00:00
if int(curr_time - snoozed_time) > 60 * 60 * 24:
replace_str = line
2019-11-06 11:39:41 +00:00
else:
2022-01-03 14:49:33 +00:00
replace_str = line
2019-11-06 11:39:41 +00:00
break
2022-01-03 14:49:33 +00:00
if replace_str:
content = None
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'r', encoding='utf-8') as snoozed_file:
2022-01-03 14:49:33 +00:00
content = snoozed_file.read().replace(replace_str, '')
2019-11-06 11:39:41 +00:00
if content:
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'w+',
encoding='utf-8') as snoozfile:
2022-01-03 14:49:33 +00:00
snoozfile.write(content)
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + snoozed_filename)
2019-11-06 11:39:41 +00:00
2022-06-10 13:01:39 +00:00
if text_in_file(snooze_actor + ' ', snoozed_filename):
2019-11-06 11:39:41 +00:00
return True
return False
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def person_snooze(base_dir: str, nickname: str, domain: str,
2022-01-03 14:49:33 +00:00
snooze_actor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Temporarily ignores the given actor
"""
2022-01-03 14:49:33 +00:00
account_dir = acct_dir(base_dir, nickname, domain)
if not os.path.isdir(account_dir):
print('ERROR: unknown account ' + account_dir)
2019-11-06 11:39:41 +00:00
return
2022-01-03 14:49:33 +00:00
snoozed_filename = account_dir + '/snoozed.txt'
if os.path.isfile(snoozed_filename):
2022-06-10 13:01:39 +00:00
if text_in_file(snooze_actor + ' ', snoozed_filename):
2019-11-06 11:57:43 +00:00
return
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'a+', encoding='utf-8') as snoozed_file:
2022-01-03 14:49:33 +00:00
snoozed_file.write(snooze_actor + ' ' +
str(int(time.time())) + '\n')
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to append ' + snoozed_filename)
2019-11-06 11:39:41 +00:00
2020-04-03 18:12:08 +00:00
2021-12-28 18:13:52 +00:00
def person_unsnooze(base_dir: str, nickname: str, domain: str,
2022-01-03 14:49:33 +00:00
snooze_actor: str) -> None:
2019-11-06 11:39:41 +00:00
"""Undoes a temporarily ignore of the given actor
"""
2022-01-03 14:49:33 +00:00
account_dir = acct_dir(base_dir, nickname, domain)
if not os.path.isdir(account_dir):
print('ERROR: unknown account ' + account_dir)
2019-11-06 11:39:41 +00:00
return
2022-01-03 14:49:33 +00:00
snoozed_filename = account_dir + '/snoozed.txt'
if not os.path.isfile(snoozed_filename):
2019-11-06 11:39:41 +00:00
return
2022-06-10 11:43:33 +00:00
if not text_in_file(snooze_actor + ' ', snoozed_filename):
2019-11-06 11:39:41 +00:00
return
2022-01-03 14:49:33 +00:00
replace_str = None
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'r', encoding='utf-8') as snoozed_file:
2022-01-03 14:49:33 +00:00
for line in snoozed_file:
if line.startswith(snooze_actor + ' '):
replace_str = line
2019-11-06 11:39:41 +00:00
break
2022-01-03 14:49:33 +00:00
if replace_str:
content = None
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'r', encoding='utf-8') as snoozed_file:
2022-01-03 14:49:33 +00:00
content = snoozed_file.read().replace(replace_str, '')
2023-07-14 10:05:33 +00:00
if content is not None:
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(snoozed_filename, 'w+',
encoding='utf-8') as snoozfile:
2022-01-03 14:49:33 +00:00
snoozfile.write(content)
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + snoozed_filename)
2020-08-05 21:12:09 +00:00
2021-12-28 18:13:52 +00:00
def set_person_notes(base_dir: str, nickname: str, domain: str,
handle: str, notes: str) -> bool:
2020-08-05 21:12:09 +00:00
"""Adds notes about a person
"""
if '@' not in handle:
return False
if handle.startswith('@'):
handle = handle[1:]
2022-01-03 14:49:33 +00:00
notes_dir = acct_dir(base_dir, nickname, domain) + '/notes'
if not os.path.isdir(notes_dir):
os.mkdir(notes_dir)
notes_filename = notes_dir + '/' + handle + '.txt'
2021-11-25 21:18:53 +00:00
try:
2022-06-09 14:46:30 +00:00
with open(notes_filename, 'w+', encoding='utf-8') as notes_file:
2022-01-03 14:49:33 +00:00
notes_file.write(notes)
2021-11-25 21:18:53 +00:00
except OSError:
2022-01-03 14:49:33 +00:00
print('EX: unable to write ' + notes_filename)
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
2023-12-20 18:22:28 +00:00
def get_person_notes(base_dir: str, nickname: str, domain: str,
handle: str) -> str:
"""Returns notes about a person
"""
person_notes = ''
person_notes_filename = \
acct_dir(base_dir, nickname, domain) + \
'/notes/' + handle + '.txt'
if os.path.isfile(person_notes_filename):
with open(person_notes_filename, 'r',
encoding='utf-8') as fp_notes:
person_notes = fp_notes.read()
return person_notes
2021-12-29 21:55:09 +00:00
def _detect_users_path(url: str) -> str:
"""Tries to detect the /users/ path
"""
if '/' not in url:
return '/users/'
2022-01-03 14:49:33 +00:00
users_paths = get_user_paths()
for possible_users_path in users_paths:
if possible_users_path in url:
return possible_users_path
return '/users/'
2022-03-11 14:38:47 +00:00
def get_actor_json(host_domain: str, handle: str, http: bool, gnunet: bool,
2022-04-29 13:54:13 +00:00
ipfs: bool, ipns: bool,
2021-12-29 21:55:09 +00:00
debug: bool, quiet: bool,
signing_priv_key_pem: str,
2022-03-11 14:38:47 +00:00
existing_session) -> ({}, {}):
2021-03-11 18:15:04 +00:00
"""Returns the actor json
"""
2021-03-17 20:18:00 +00:00
if debug:
2021-12-29 21:55:09 +00:00
print('get_actor_json for ' + handle)
2022-01-03 14:49:33 +00:00
original_actor = handle
2021-12-26 00:07:44 +00:00
group_account = False
# try to determine the users path
2022-01-03 14:49:33 +00:00
detected_users_path = _detect_users_path(handle)
2021-03-11 18:15:04 +00:00
if '/@' in handle or \
2022-01-03 14:49:33 +00:00
detected_users_path in handle or \
2021-03-11 18:15:04 +00:00
handle.startswith('http') or \
2022-04-29 13:54:13 +00:00
handle.startswith('ipfs') or \
handle.startswith('ipns') or \
2021-07-01 17:59:24 +00:00
handle.startswith('hyper'):
2022-01-03 14:49:33 +00:00
group_paths = get_group_paths()
if detected_users_path in group_paths:
2021-12-26 00:07:44 +00:00
group_account = True
2021-03-11 18:15:04 +00:00
# format: https://domain/@nick
2022-01-03 14:49:33 +00:00
original_handle = handle
if not has_users_path(original_handle):
2021-03-17 20:18:00 +00:00
if not quiet or debug:
2021-12-29 21:55:09 +00:00
print('get_actor_json: Expected actor format: ' +
'https://domain/@nick or https://domain' +
2022-01-03 14:49:33 +00:00
detected_users_path + 'nick')
2021-06-03 19:46:35 +00:00
return None, None
2021-12-27 17:20:01 +00:00
prefixes = get_protocol_prefixes()
2021-06-03 18:30:48 +00:00
for prefix in prefixes:
handle = handle.replace(prefix, '')
2023-04-23 15:55:48 +00:00
if '/@/' not in handle:
handle = handle.replace('/@', detected_users_path)
2021-12-26 12:24:40 +00:00
paths = get_user_paths()
2022-01-03 14:49:33 +00:00
user_path_found = False
for user_path in paths:
if user_path in handle:
nickname = handle.split(user_path)[1]
2022-06-21 11:58:50 +00:00
nickname = remove_eol(nickname)
2022-01-03 14:49:33 +00:00
domain = handle.split(user_path)[0]
user_path_found = True
2021-07-04 11:39:13 +00:00
break
2022-01-03 14:49:33 +00:00
if not user_path_found and '://' in original_handle:
domain = original_handle.split('://')[1]
2021-06-03 18:30:48 +00:00
if '/' in domain:
domain = domain.split('/')[0]
2022-01-03 14:49:33 +00:00
if '://' + domain + '/' not in original_handle:
2021-06-03 19:46:35 +00:00
return None, None
2022-01-03 14:49:33 +00:00
nickname = original_handle.split('://' + domain + '/')[1]
2021-06-03 18:30:48 +00:00
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-12-29 21:55:09 +00:00
print('get_actor_json 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-12-29 21:55:09 +00:00
print('get_actor_jsonSyntax: --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]
2022-06-21 11:58:50 +00:00
domain = remove_eol(domain)
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'
2022-04-29 13:54:13 +00:00
elif ipfs:
http_prefix = 'ipfs'
proxy_type = 'ipfs'
elif ipns:
http_prefix = 'ipns'
proxy_type = 'ipfs'
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'
2022-03-11 14:38:47 +00:00
if existing_session:
session = existing_session
2022-03-11 18:07:17 +00:00
if debug:
print('DEBUG: get_actor_json using existing session ' +
str(proxy_type) + ' ' + domain)
else:
2021-12-28 16:56:57 +00:00
session = create_session(proxy_type)
2022-03-11 18:07:17 +00:00
if debug:
print('DEBUG: get_actor_json using session ' +
str(proxy_type) + ' ' + domain)
2021-03-11 18:15:04 +00:00
if nickname == 'inbox':
nickname = domain
2022-01-03 14:49:33 +00:00
person_url = None
2022-01-02 14:51:02 +00:00
wf_request = None
2021-09-12 19:53:38 +00:00
2022-02-26 17:07:14 +00:00
original_actor_lower = original_actor.lower()
ends_with_instance_actor = False
if original_actor_lower.endswith('/actor') or \
original_actor_lower.endswith('/instance.actor'):
ends_with_instance_actor = True
if '://' in original_actor and ends_with_instance_actor:
2021-09-02 13:02:07 +00:00
if debug:
2022-01-03 14:49:33 +00:00
print(original_actor + ' is an instance actor')
person_url = original_actor
elif '://' in original_actor and group_account:
2021-09-12 19:48:05 +00:00
if debug:
2022-01-03 14:49:33 +00:00
print(original_actor + ' is a group actor')
person_url = original_actor
2021-09-02 13:02:07 +00:00
else:
handle = nickname + '@' + domain
2023-10-12 12:59:04 +00:00
if debug:
print('get_actor_json webfinger: ' + handle)
2022-01-02 14:51:02 +00:00
wf_request = webfinger_handle(session, handle,
http_prefix, cached_webfingers,
2022-03-11 14:38:47 +00:00
host_domain, __version__, debug,
2022-01-02 14:51:02 +00:00
group_account, signing_priv_key_pem)
if not wf_request:
2021-09-02 13:02:07 +00:00
if not quiet:
2022-03-11 14:38:47 +00:00
print('get_actor_json Unable to webfinger ' + handle +
' ' + http_prefix + ' proxy: ' + str(proxy_type))
2021-07-30 13:00:23 +00:00
return None, None
2022-01-02 14:51:02 +00:00
if not isinstance(wf_request, dict):
2021-09-02 13:02:07 +00:00
if not quiet:
2021-12-29 21:55:09 +00:00
print('get_actor_json Webfinger for ' + handle +
2022-01-02 14:51:02 +00:00
' did not return a dict. ' + str(wf_request))
2021-09-02 13:02:07 +00:00
return None, None
if not quiet:
2022-01-02 14:51:02 +00:00
pprint(wf_request)
2021-09-02 13:02:07 +00:00
2022-01-02 14:51:02 +00:00
if wf_request.get('errors'):
2021-09-02 13:02:07 +00:00
if not quiet or debug:
2022-01-02 14:51:02 +00:00
print('get_actor_json wf_request error: ' +
str(wf_request['errors']))
2021-12-26 12:19:00 +00:00
if has_users_path(handle):
2022-01-03 14:49:33 +00:00
person_url = original_actor
2021-09-02 13:02:07 +00:00
else:
if debug:
print('No users path in ' + handle)
return None, None
2021-07-29 11:25:01 +00:00
2022-01-03 14:49:33 +00:00
profile_str = 'https://www.w3.org/ns/activitystreams'
headers_list = (
2021-06-02 19:38:12 +00:00
"activity+json", "ld+json", "jrd+json"
)
2022-01-03 14:49:33 +00:00
if not person_url and wf_request:
person_url = get_user_url(wf_request, 0, debug)
2023-10-12 12:59:04 +00:00
if debug and person_url:
print('\nget_actor_json getting json for ' + person_url)
2021-03-11 18:15:04 +00:00
if nickname == domain:
2021-12-26 12:24:40 +00:00
paths = get_user_paths()
2022-01-03 14:49:33 +00:00
for user_path in paths:
2023-10-12 12:59:04 +00:00
if user_path != '/@':
person_url = person_url.replace(user_path, '/actor/')
2022-01-03 14:49:33 +00:00
if not person_url and group_account:
person_url = http_prefix + '://' + domain + '/c/' + nickname
if not person_url:
2021-03-11 18:15:04 +00:00
# try single user instance
2022-01-03 14:49:33 +00:00
person_url = http_prefix + '://' + domain + '/' + nickname
headers_list = (
2021-06-02 19:38:12 +00:00
"ld+json", "jrd+json", "activity+json"
)
2021-09-12 19:48:05 +00:00
if debug:
2022-01-03 14:49:33 +00:00
print('Trying single user instance ' + person_url)
if '/channel/' in person_url or '/accounts/' in person_url:
headers_list = (
2021-06-02 19:38:12 +00:00
"ld+json", "jrd+json", "activity+json"
)
2021-06-03 18:49:09 +00:00
if debug:
2022-01-03 14:49:33 +00:00
print('person_url: ' + person_url)
for header_type in headers_list:
header_mime_type = 'application/' + header_type
as_header = {
'Accept': header_mime_type + '; profile="' + profile_str + '"'
2021-03-11 18:15:04 +00:00
}
2022-01-03 14:49:33 +00:00
person_json = \
get_json(signing_priv_key_pem, session, person_url, as_header,
2022-03-11 14:38:47 +00:00
None, debug, __version__, http_prefix, host_domain,
2022-01-03 14:49:33 +00:00
20, quiet)
2023-08-13 09:58:02 +00:00
if get_json_valid(person_json):
2021-06-02 19:38:12 +00:00
if not quiet:
2022-01-03 14:49:33 +00:00
pprint(person_json)
return person_json, as_header
2021-06-03 19:46:35 +00:00
return None, None
2021-06-25 14:33:16 +00:00
2022-06-12 12:30:14 +00:00
def get_person_avatar_url(base_dir: str, person_url: str,
person_cache: {}) -> str:
2021-06-25 14:33:16 +00:00
"""Returns the avatar url for the person
"""
2022-01-03 14:49:33 +00:00
person_json = \
2022-06-09 16:54:44 +00:00
get_person_from_cache(base_dir, person_url, person_cache)
2022-01-03 14:49:33 +00:00
if not person_json:
2021-06-25 14:33:16 +00:00
return None
# get from locally stored image
2022-01-03 14:49:33 +00:00
if not person_json.get('id'):
2021-06-25 14:33:16 +00:00
return None
2022-01-03 14:49:33 +00:00
actor_str = person_json['id'].replace('/', '-')
avatar_image_path = base_dir + '/cache/avatars/' + actor_str
image_extension = get_image_extensions()
for ext in image_extension:
im_filename = avatar_image_path + '.' + ext
im_path = '/avatars/' + actor_str + '.' + ext
if not os.path.isfile(im_filename):
im_filename = avatar_image_path.lower() + '.' + ext
im_path = '/avatars/' + actor_str.lower() + '.' + ext
if not os.path.isfile(im_filename):
2021-09-13 19:38:15 +00:00
continue
2021-09-13 19:39:43 +00:00
if ext != 'svg':
2022-01-03 14:49:33 +00:00
return im_path
content = ''
2022-06-09 14:46:30 +00:00
with open(im_filename, 'r', encoding='utf-8') as fp_im:
2022-01-03 14:49:33 +00:00
content = fp_im.read()
if not dangerous_svg(content, False):
return im_path
if person_json.get('icon'):
if person_json['icon'].get('url'):
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(person_json['icon']['url'])
if '.svg' not in url_str.lower():
return remove_html(url_str)
2021-06-25 14:33:16 +00:00
return None
2021-10-29 12:52:23 +00:00
2021-12-28 18:13:52 +00:00
def add_actor_update_timestamp(actor_json: {}) -> None:
2021-10-29 12:52:23 +00:00
"""Adds 'updated' fields with a timestamp
"""
2023-11-20 22:27:58 +00:00
updated_time = date_utcnow()
2022-01-03 14:49:33 +00:00
curr_date_str = updated_time.strftime("%Y-%m-%dT%H:%M:%SZ")
actor_json['updated'] = curr_date_str
2021-10-29 12:52:23 +00:00
# add updated timestamp to avatar and banner
2022-01-03 14:49:33 +00:00
actor_json['icon']['updated'] = curr_date_str
actor_json['image']['updated'] = curr_date_str
2021-12-29 21:55:09 +00:00
def valid_sending_actor(session, base_dir: str,
nickname: str, domain: str,
person_cache: {},
post_json_object: {},
signing_priv_key_pem: str,
2022-09-25 17:26:11 +00:00
debug: bool, unit_test: bool,
system_language: str) -> bool:
"""When a post arrives in the inbox this is used to check that
the sending actor is valid
"""
# who sent this post?
2024-01-09 16:59:23 +00:00
sending_actor = get_actor_from_post(post_json_object)
2022-12-26 10:49:41 +00:00
if not isinstance(sending_actor, str):
return False
if contains_invalid_actor_url_chars(sending_actor):
return False
# If you are following them then allow their posts
2022-01-03 14:49:33 +00:00
if is_following_actor(base_dir, nickname, domain, sending_actor):
return True
# sending to yourself (reminder)
2022-01-03 14:49:33 +00:00
if sending_actor.endswith(domain + '/users/' + nickname):
return True
# download the actor
# NOTE: the actor should not be obtained from the local cache,
# because they may have changed fields which are being tested here,
# such as the bio length
2022-04-29 13:54:13 +00:00
gnunet = False
ipfs = False
ipns = False
actor_json, _ = get_actor_json(domain, sending_actor,
2022-04-29 13:54:13 +00:00
True, gnunet, ipfs, ipns,
debug, True,
signing_priv_key_pem, session)
2021-12-26 10:29:52 +00:00
if not actor_json:
# if the actor couldn't be obtained then proceed anyway
return True
2021-12-26 10:29:52 +00:00
if not actor_json.get('preferredUsername'):
print('REJECT: no preferredUsername within actor ' + str(actor_json))
return False
2022-09-07 18:55:48 +00:00
2022-12-26 10:49:41 +00:00
# is this a known spam actor?
2022-09-07 18:55:48 +00:00
actor_spam_filter_filename = \
acct_dir(base_dir, nickname, domain) + '/.reject_spam_actors'
if not os.path.isfile(actor_spam_filter_filename):
return True
# does the actor have a bio ?
2021-12-25 21:32:15 +00:00
if not unit_test:
2022-01-03 14:49:33 +00:00
bio_str = ''
2021-12-26 10:29:52 +00:00
if actor_json.get('summary'):
2022-01-03 14:49:33 +00:00
bio_str = remove_html(actor_json['summary']).strip()
if not bio_str:
# allow no bio if it's an actor in this instance
2022-01-03 14:49:33 +00:00
if domain not in sending_actor:
# probably a spam actor with no bio
2022-01-03 14:49:33 +00:00
print('REJECT: spam actor ' + sending_actor)
return False
2022-01-03 14:49:33 +00:00
if len(bio_str) < 10:
2021-12-19 18:29:43 +00:00
print('REJECT: actor bio is not long enough ' +
2022-01-03 14:49:33 +00:00
sending_actor + ' ' + bio_str)
2021-12-19 18:29:43 +00:00
return False
2022-01-03 14:49:33 +00:00
bio_str += ' ' + remove_html(actor_json['preferredUsername'])
2021-12-26 10:29:52 +00:00
if actor_json.get('attachment'):
if isinstance(actor_json['attachment'], list):
for tag in actor_json['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name'):
continue
if isinstance(tag['name'], str):
2022-01-03 14:49:33 +00:00
bio_str += ' ' + tag['name']
prop_value_name, _ = \
get_attachment_property_value(tag)
if not prop_value_name:
continue
if tag.get(prop_value_name):
continue
if isinstance(tag[prop_value_name], str):
bio_str += ' ' + tag[prop_value_name]
2021-12-26 10:29:52 +00:00
if actor_json.get('name'):
2022-01-03 14:49:33 +00:00
bio_str += ' ' + remove_html(actor_json['name'])
if contains_invalid_chars(bio_str):
print('REJECT: post actor bio contains invalid characters')
return False
2022-09-25 17:26:11 +00:00
if is_filtered_bio(base_dir, nickname, domain, bio_str,
system_language):
print('REJECT: post actor bio contains filtered text')
return False
else:
2022-01-03 14:49:33 +00:00
print('Skipping check for missing bio in ' + sending_actor)
# Check any attached fields for the actor.
# Spam actors will sometimes have attached fields which are all empty
2021-12-26 10:29:52 +00:00
if actor_json.get('attachment'):
if isinstance(actor_json['attachment'], list):
2022-01-03 14:49:33 +00:00
no_of_tags = 0
tags_without_value = 0
2021-12-26 10:29:52 +00:00
for tag in actor_json['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name') and not tag.get('schema:name'):
continue
2022-01-03 14:49:33 +00:00
no_of_tags += 1
prop_value_name, _ = get_attachment_property_value(tag)
if not prop_value_name:
2022-01-03 14:49:33 +00:00
tags_without_value += 1
continue
if not isinstance(tag[prop_value_name], str):
2022-01-03 14:49:33 +00:00
tags_without_value += 1
continue
if not tag[prop_value_name].strip():
2022-01-03 14:49:33 +00:00
tags_without_value += 1
continue
if len(tag[prop_value_name]) < 2:
2022-01-03 14:49:33 +00:00
tags_without_value += 1
continue
2022-01-03 14:49:33 +00:00
if no_of_tags > 0:
if int(tags_without_value * 100 / no_of_tags) > 50:
print('REJECT: actor has empty attachments ' +
2022-01-03 14:49:33 +00:00
sending_actor)
return False
# if the actor is valid and was downloaded then
# store it in the cache, but don't write it to file
store_person_in_cache(base_dir, sending_actor, actor_json,
person_cache, False)
return True
def get_featured_hashtags(actor_json: {}) -> str:
"""returns a string containing featured hashtags
"""
result = ''
if not actor_json.get('tag'):
return result
if not isinstance(actor_json['tag'], list):
return result
2023-05-03 11:03:00 +00:00
ctr = 0
for tag_dict in actor_json['tag']:
if not tag_dict.get('type'):
continue
if not isinstance(tag_dict['type'], str):
continue
if not tag_dict['type'].endswith('Hashtag'):
continue
if not tag_dict.get('name'):
continue
if not isinstance(tag_dict['name'], str):
continue
if not tag_dict.get('href'):
continue
if not isinstance(tag_dict['href'], str):
continue
tag_name = tag_dict['name']
if not tag_name:
continue
if tag_name.startswith('#'):
tag_name = tag_name[1:]
if not tag_name:
continue
tag_url = remove_html(tag_dict['href'])
2023-05-03 10:09:44 +00:00
if '://' not in tag_url:
continue
if not valid_hash_tag(tag_name):
continue
result += '#' + tag_name + ' '
2023-05-03 11:03:00 +00:00
ctr += 1
if ctr >= 10:
break
return result.strip()
2023-05-03 11:07:34 +00:00
def get_featured_hashtags_as_html(actor_json: {},
profile_description: str) -> str:
2023-05-03 10:34:54 +00:00
"""returns a html string containing featured hashtags
"""
result = ''
if not actor_json.get('tag'):
return result
if not isinstance(actor_json['tag'], list):
return result
2023-05-03 11:03:00 +00:00
ctr = 0
2023-05-03 10:34:54 +00:00
for tag_dict in actor_json['tag']:
if not tag_dict.get('type'):
continue
if not isinstance(tag_dict['type'], str):
continue
if not tag_dict['type'].endswith('Hashtag'):
continue
if not tag_dict.get('name'):
continue
if not isinstance(tag_dict['name'], str):
continue
if not tag_dict.get('href'):
continue
if not isinstance(tag_dict['href'], str):
continue
tag_name = tag_dict['name']
if not tag_name:
continue
if tag_name.startswith('#'):
tag_name = tag_name[1:]
if not tag_name:
continue
2023-05-03 11:07:34 +00:00
if '/tags/' + tag_name + '"' in profile_description:
continue
if ' #' + tag_name in profile_description:
continue
tag_url = remove_html(tag_dict['href'])
2023-05-03 10:34:54 +00:00
if '://' not in tag_url:
continue
if not valid_hash_tag(tag_name):
continue
result += \
'<a href="' + tag_url + '" ' + \
2023-05-03 10:34:54 +00:00
'class="mention hashtag" rel="tag" ' + \
'tabindex="10">#' + tag_name + '</a> '
2023-05-03 11:03:00 +00:00
ctr += 1
if ctr >= 10:
break
2023-05-03 10:34:54 +00:00
result = result.strip()
if result:
result = '<p>' + result + '</p>'
return result
2023-05-03 09:54:11 +00:00
def set_featured_hashtags(actor_json: {}, hashtags: str,
append: bool = False) -> None:
"""sets featured hashtags
"""
separator_str = ' '
2023-05-02 22:42:10 +00:00
separators = (',', ' ')
for separator_str in separators:
if separator_str in hashtags:
break
tag_list = hashtags.split(separator_str)
result = []
tags_used = []
2023-05-02 22:42:10 +00:00
actor_id = actor_json['id']
actor_domain = actor_id.split('://')[1]
if '/' in actor_domain:
actor_domain = actor_domain.split('/')[0]
actor_url = \
actor_id.split('://')[0] + '://' + actor_domain
for tag_str in tag_list:
if not tag_str:
continue
if not tag_str.startswith('#'):
tag_str = '#' + tag_str
if tag_str in tags_used:
continue
2023-05-02 22:42:10 +00:00
url = actor_url + '/tags/' + tag_str.replace('#', '')
result.append({
"name": tag_str,
"type": "Hashtag",
"href": url
})
tags_used.append(tag_str)
2023-05-03 11:03:00 +00:00
if len(result) >= 10:
break
# add any non-hashtags to the result
if actor_json.get('tag'):
for tag_dict in actor_json['tag']:
if not tag_dict.get('type'):
continue
if not isinstance(tag_dict['type'], str):
continue
if tag_dict['type'] != 'Hashtag':
result.append(tag_dict)
2023-05-03 09:54:11 +00:00
if not append:
actor_json['tag'] = result
else:
actor_json['tag'] += result
def update_memorial_flags(base_dir: str, person_cache: {}) -> None:
"""Sets or clears the memorial flags based upon the content of
accounts/memorial file
"""
memorials = get_memorials(base_dir).split('\n')
2024-05-12 12:35:26 +00:00
dir_str = data_dir(base_dir)
for _, dirs, _ in os.walk(dir_str):
for account in dirs:
if not is_account_dir(account):
continue
2024-05-12 12:35:26 +00:00
actor_filename = data_dir(base_dir) + '/' + account + '.json'
if not os.path.isfile(actor_filename):
continue
actor_json = load_json(actor_filename)
if not actor_json:
continue
if not actor_json.get('id'):
continue
nickname = account.split('@')[0]
actor_changed = False
if not actor_json.get('memorial'):
if nickname in memorials:
actor_json['memorial'] = True
actor_changed = True
else:
if nickname not in memorials:
actor_json['memorial'] = False
actor_changed = True
2023-09-01 09:32:33 +00:00
if not actor_changed:
continue
save_json(actor_json, actor_filename)
actor = actor_json['id']
remove_person_from_cache(base_dir, actor, person_cache)
store_person_in_cache(base_dir, actor, actor_json,
person_cache, True)
break
2024-01-29 21:45:04 +00:00
def get_account_pub_key(path: str, person_cache: {},
base_dir: str, domain: str,
calling_domain: str,
http_prefix: str,
domain_full: str,
onion_domain: str,
i2p_domain: str) -> str:
"""Returns the public key for an account
"""
if '/users/' not in path:
return None
nickname = path.split('/users/')[1]
if '#main-key' in nickname:
nickname = nickname.split('#main-key')[0]
elif '/main-key' in nickname:
nickname = nickname.split('/main-key')[0]
elif '#/publicKey' in nickname:
nickname = nickname.split('#/publicKey')[0]
else:
return None
actor = \
get_instance_url(calling_domain,
http_prefix,
domain_full,
onion_domain,
i2p_domain) + \
'/users/' + nickname
actor_json = get_person_from_cache(base_dir, actor, person_cache)
if not actor_json:
actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
if not os.path.isfile(actor_filename):
return None
2024-06-20 10:47:58 +00:00
actor_json = load_json(actor_filename)
2024-01-29 21:45:04 +00:00
if not actor_json:
return None
store_person_in_cache(base_dir, actor, actor_json,
person_cache, False)
if not actor_json.get('publicKey') and \
not actor_json.get('assertionMethod'):
return None
original_person_url = \
get_instance_url(calling_domain,
http_prefix,
domain_full,
onion_domain,
i2p_domain) + \
path
pub_key, _ = \
get_actor_public_key_from_id(actor_json, original_person_url)
return pub_key