mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-14 11:37:59 +00:00
305 lines
6.1 KiB
Python
305 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import typing
|
|
import yaml
|
|
|
|
from functools import cached_property
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
from . import logger as logging
|
|
from .misc import Message, boolean
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
|
|
# pylint: disable=duplicate-code
|
|
|
|
class RelayConfig(dict):
|
|
def __init__(self, path: str):
|
|
dict.__init__(self, {})
|
|
|
|
if self.is_docker:
|
|
path = '/data/config.yaml'
|
|
|
|
self._path = Path(path).expanduser().resolve()
|
|
self.reset()
|
|
|
|
|
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
if key in {'blocked_instances', 'blocked_software', 'whitelist'}:
|
|
assert isinstance(value, (list, set, tuple))
|
|
|
|
elif key in {'port', 'workers', 'json_cache', 'timeout'}:
|
|
if not isinstance(value, int):
|
|
value = int(value)
|
|
|
|
elif key == 'whitelist_enabled':
|
|
if not isinstance(value, bool):
|
|
value = boolean(value)
|
|
|
|
super().__setitem__(key, value)
|
|
|
|
|
|
@property
|
|
def db(self) -> RelayDatabase:
|
|
return Path(self['db']).expanduser().resolve()
|
|
|
|
|
|
@property
|
|
def actor(self) -> str:
|
|
return f'https://{self["host"]}/actor'
|
|
|
|
|
|
@property
|
|
def inbox(self) -> str:
|
|
return f'https://{self["host"]}/inbox'
|
|
|
|
|
|
@property
|
|
def keyid(self) -> str:
|
|
return f'{self.actor}#main-key'
|
|
|
|
|
|
@cached_property
|
|
def is_docker(self) -> bool:
|
|
return bool(os.environ.get('DOCKER_RUNNING'))
|
|
|
|
|
|
def reset(self) -> None:
|
|
self.clear()
|
|
self.update({
|
|
'db': str(self._path.parent.joinpath(f'{self._path.stem}.jsonld')),
|
|
'listen': '0.0.0.0',
|
|
'port': 8080,
|
|
'note': 'Make a note about your instance here.',
|
|
'push_limit': 512,
|
|
'json_cache': 1024,
|
|
'timeout': 10,
|
|
'workers': 0,
|
|
'host': 'relay.example.com',
|
|
'whitelist_enabled': False,
|
|
'blocked_software': [],
|
|
'blocked_instances': [],
|
|
'whitelist': []
|
|
})
|
|
|
|
|
|
def load(self) -> None:
|
|
self.reset()
|
|
|
|
options = {}
|
|
|
|
try:
|
|
options['Loader'] = yaml.FullLoader
|
|
|
|
except AttributeError:
|
|
pass
|
|
|
|
try:
|
|
with self._path.open('r', encoding = 'UTF-8') as fd:
|
|
config = yaml.load(fd, **options)
|
|
|
|
except FileNotFoundError:
|
|
return
|
|
|
|
if not config:
|
|
return
|
|
|
|
for key, value in config.items():
|
|
if key == 'ap':
|
|
for k, v in value.items():
|
|
if k not in self:
|
|
continue
|
|
|
|
self[k] = v
|
|
|
|
continue
|
|
|
|
if key not in self:
|
|
continue
|
|
|
|
self[key] = value
|
|
|
|
|
|
class RelayDatabase(dict):
|
|
def __init__(self, config: RelayConfig):
|
|
dict.__init__(self, {
|
|
'relay-list': {},
|
|
'private-key': None,
|
|
'follow-requests': {},
|
|
'version': 1
|
|
})
|
|
|
|
self.config = config
|
|
self.signer = None
|
|
|
|
|
|
@property
|
|
def hostnames(self) -> tuple[str]:
|
|
return tuple(self['relay-list'].keys())
|
|
|
|
|
|
@property
|
|
def inboxes(self) -> tuple[dict[str, str]]:
|
|
return tuple(data['inbox'] for data in self['relay-list'].values())
|
|
|
|
|
|
def load(self) -> None:
|
|
try:
|
|
with self.config.db.open() as fd:
|
|
data = json.load(fd)
|
|
|
|
self['version'] = data.get('version', None)
|
|
self['private-key'] = data.get('private-key')
|
|
|
|
if self['version'] is None:
|
|
self['version'] = 1
|
|
|
|
if 'actorKeys' in data:
|
|
self['private-key'] = data['actorKeys']['privateKey']
|
|
|
|
for item in data.get('relay-list', []):
|
|
domain = urlparse(item).hostname
|
|
self['relay-list'][domain] = {
|
|
'domain': domain,
|
|
'inbox': item,
|
|
'followid': None
|
|
}
|
|
|
|
else:
|
|
self['relay-list'] = data.get('relay-list', {})
|
|
|
|
for domain, instance in self['relay-list'].items():
|
|
if not instance.get('domain'):
|
|
instance['domain'] = domain
|
|
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
except json.decoder.JSONDecodeError as e:
|
|
if self.config.db.stat().st_size > 0:
|
|
raise e from None
|
|
|
|
|
|
def save(self) -> None:
|
|
with self.config.db.open('w', encoding = 'UTF-8') as fd:
|
|
json.dump(self, fd, indent=4)
|
|
|
|
|
|
def get_inbox(self, domain: str, fail: bool = False) -> dict[str, str] | None:
|
|
if domain.startswith('http'):
|
|
domain = urlparse(domain).hostname
|
|
|
|
if (inbox := self['relay-list'].get(domain)):
|
|
return inbox
|
|
|
|
if fail:
|
|
raise KeyError(domain)
|
|
|
|
return None
|
|
|
|
|
|
def add_inbox(self,
|
|
inbox: str,
|
|
followid: str | None = None,
|
|
software: str | None = None) -> dict[str, str]:
|
|
|
|
assert inbox.startswith('https'), 'Inbox must be a url'
|
|
domain = urlparse(inbox).hostname
|
|
|
|
if (instance := self.get_inbox(domain)):
|
|
if followid:
|
|
instance['followid'] = followid
|
|
|
|
if software:
|
|
instance['software'] = software
|
|
|
|
return instance
|
|
|
|
self['relay-list'][domain] = {
|
|
'domain': domain,
|
|
'inbox': inbox,
|
|
'followid': followid,
|
|
'software': software
|
|
}
|
|
|
|
logging.verbose('Added inbox to database: %s', inbox)
|
|
return self['relay-list'][domain]
|
|
|
|
|
|
def del_inbox(self,
|
|
domain: str,
|
|
followid: str = None,
|
|
fail: bool = False) -> bool:
|
|
|
|
if not (data := self.get_inbox(domain, fail=False)):
|
|
if fail:
|
|
raise KeyError(domain)
|
|
|
|
return False
|
|
|
|
if not data['followid'] or not followid or data['followid'] == followid:
|
|
del self['relay-list'][data['domain']]
|
|
logging.verbose('Removed inbox from database: %s', data['inbox'])
|
|
return True
|
|
|
|
if fail:
|
|
raise ValueError('Follow IDs do not match')
|
|
|
|
logging.debug('Follow ID does not match: db = %s, object = %s', data['followid'], followid)
|
|
return False
|
|
|
|
|
|
def get_request(self, domain: str, fail: bool = True) -> dict[str, str] | None:
|
|
if domain.startswith('http'):
|
|
domain = urlparse(domain).hostname
|
|
|
|
try:
|
|
return self['follow-requests'][domain]
|
|
|
|
except KeyError as e:
|
|
if fail:
|
|
raise e
|
|
|
|
return None
|
|
|
|
|
|
def add_request(self, actor: str, inbox: str, followid: str) -> None:
|
|
domain = urlparse(inbox).hostname
|
|
|
|
try:
|
|
request = self.get_request(domain)
|
|
request['followid'] = followid
|
|
|
|
except KeyError:
|
|
pass
|
|
|
|
self['follow-requests'][domain] = {
|
|
'actor': actor,
|
|
'inbox': inbox,
|
|
'followid': followid
|
|
}
|
|
|
|
|
|
def del_request(self, domain: str) -> None:
|
|
if domain.startswith('http'):
|
|
domain = urlparse(domain).hostname
|
|
|
|
del self['follow-requests'][domain]
|
|
|
|
|
|
def distill_inboxes(self, message: Message) -> Iterator[str]:
|
|
src_domains = {
|
|
message.domain,
|
|
urlparse(message.object_id).netloc
|
|
}
|
|
|
|
for domain, instance in self['relay-list'].items():
|
|
if domain not in src_domains:
|
|
yield instance['inbox']
|