mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-23 23:17:58 +00:00
Compare commits
13 commits
b259f2d760
...
3391749800
Author | SHA1 | Date | |
---|---|---|---|
3391749800 | |||
091a36a3d8 | |||
c704c1e72c | |||
be2c5b144a | |||
38ea8b390e | |||
1eb93ab71a | |||
c3b4d9ca98 | |||
af1caaf7c9 | |||
afad948f85 | |||
8f8f38cc4c | |||
aca34825b1 | |||
8c85f23c86 | |||
6f3a1db17d |
|
@ -1 +1,2 @@
|
||||||
include data/statements.sql
|
include data/statements.sql
|
||||||
|
include data/swagger.yaml
|
||||||
|
|
|
@ -9,7 +9,7 @@ use `python3 -m relay` if installed via pip or `~/.local/bin/activityrelay` if i
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Run the relay.
|
Run the relay. Optionally add `-d` or `--dev` to enable auto-reloading on code changes.
|
||||||
|
|
||||||
activityrelay run
|
activityrelay run
|
||||||
|
|
||||||
|
@ -58,6 +58,50 @@ Set a value for a config option
|
||||||
activityrelay config set <key> <value>
|
activityrelay config set <key> <value>
|
||||||
|
|
||||||
|
|
||||||
|
## User
|
||||||
|
|
||||||
|
### List
|
||||||
|
|
||||||
|
List all available users.
|
||||||
|
|
||||||
|
activityrelay user list
|
||||||
|
|
||||||
|
|
||||||
|
### Create
|
||||||
|
|
||||||
|
Create a new user. You will be prompted for the new password.
|
||||||
|
|
||||||
|
activityrelay user create <username> [associated ActivityPub handle]
|
||||||
|
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
Delete a user.
|
||||||
|
|
||||||
|
activityrelay user delete <username>
|
||||||
|
|
||||||
|
|
||||||
|
### List Tokens
|
||||||
|
|
||||||
|
List all API tokens for a user.
|
||||||
|
|
||||||
|
activityrelay user list-tokens <username>
|
||||||
|
|
||||||
|
|
||||||
|
### Create Token
|
||||||
|
|
||||||
|
Generate a new API token for a user.
|
||||||
|
|
||||||
|
activityrelay user create-token <username>
|
||||||
|
|
||||||
|
|
||||||
|
### Delete Token
|
||||||
|
|
||||||
|
Delete an API token.
|
||||||
|
|
||||||
|
activityrelay user delete-token <code>
|
||||||
|
|
||||||
|
|
||||||
## Inbox
|
## Inbox
|
||||||
|
|
||||||
Manage the list of subscribed instances.
|
Manage the list of subscribed instances.
|
||||||
|
@ -75,7 +119,7 @@ List the currently subscribed instances or relays.
|
||||||
Add an inbox to the database. If a domain is specified, it will default to `https://{domain}/inbox`.
|
Add an inbox to the database. If a domain is specified, it will default to `https://{domain}/inbox`.
|
||||||
If the added instance is not following the relay, expect errors when pushing messages.
|
If the added instance is not following the relay, expect errors when pushing messages.
|
||||||
|
|
||||||
activityrelay inbox add <inbox or domain>
|
activityrelay inbox add <inbox or domain> --actor <actor url> --followid <follow activity ID> --software <nodeinfo software name>
|
||||||
|
|
||||||
|
|
||||||
### Remove
|
### Remove
|
||||||
|
@ -155,7 +199,7 @@ List the currently banned instances.
|
||||||
Add an instance to the ban list. If the instance is currently subscribed, it will be removed from
|
Add an instance to the ban list. If the instance is currently subscribed, it will be removed from
|
||||||
the inbox list.
|
the inbox list.
|
||||||
|
|
||||||
activityrelay instance ban <domain>
|
activityrelay instance ban <domain> --reason <text> --note <text>
|
||||||
|
|
||||||
|
|
||||||
### Unban
|
### Unban
|
||||||
|
@ -167,9 +211,10 @@ Remove an instance from the ban list.
|
||||||
|
|
||||||
### Update
|
### Update
|
||||||
|
|
||||||
Update the ban reason or note for an instance ban.
|
Update the ban reason or note for an instance ban. Either `--reason` and/or `--note` must be
|
||||||
|
specified.
|
||||||
|
|
||||||
activityrelay instance update bad.example.com --reason "the baddest reason"
|
activityrelay instance update bad.example.com --reason <text> --note <text>
|
||||||
|
|
||||||
|
|
||||||
## Software
|
## Software
|
||||||
|
@ -194,7 +239,7 @@ name via nodeinfo.
|
||||||
|
|
||||||
If the name is `RELAYS` (case-sensitive), add all known relay software names to the list.
|
If the name is `RELAYS` (case-sensitive), add all known relay software names to the list.
|
||||||
|
|
||||||
activityrelay software ban [-f/--fetch-nodeinfo] <name, domain, or RELAYS>
|
activityrelay software ban [-f/--fetch-nodeinfo] <name, domain, or RELAYS> --reason <text> --note <text>
|
||||||
|
|
||||||
|
|
||||||
### Unban
|
### Unban
|
||||||
|
@ -214,4 +259,4 @@ If the name is `RELAYS` (case-sensitive), remove all known relay software names
|
||||||
Update the ban reason or note for a software ban. Either `--reason` and/or `--note` must be
|
Update the ban reason or note for a software ban. Either `--reason` and/or `--note` must be
|
||||||
specified.
|
specified.
|
||||||
|
|
||||||
activityrelay software update relay.example.com --reason "begone relay"
|
activityrelay software update relay.example.com --reason <text> --note <text>
|
||||||
|
|
|
@ -19,11 +19,10 @@ proxy is on the same host.
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
|
||||||
### Push Workers
|
### Web Workers
|
||||||
|
|
||||||
The relay can be configured to use threads to push messages out. For smaller relays, this isn't
|
The number of processes to spawn for handling web requests. Leave it at 0 to automatically detect
|
||||||
necessary, but bigger ones (>100 instances) will want to set this to the number of available cpu
|
how many processes should be spawned.
|
||||||
threads.
|
|
||||||
|
|
||||||
workers: 0
|
workers: 0
|
||||||
|
|
||||||
|
|
11
relay.spec
11
relay.spec
|
@ -1,7 +1,10 @@
|
||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
aiohttp_swagger_path = Path(importlib.import_module('aiohttp_swagger').__file__).parent
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
|
@ -9,9 +12,13 @@ a = Analysis(
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[
|
datas=[
|
||||||
('relay/data', 'relay/data')
|
('relay/data', 'relay/data'),
|
||||||
|
(aiohttp_swagger_path, 'aiohttp_swagger')
|
||||||
|
],
|
||||||
|
hiddenimports=[
|
||||||
|
'gunicorn',
|
||||||
|
'gunicorn.glogging'
|
||||||
],
|
],
|
||||||
hiddenimports=[],
|
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.2.5'
|
__version__ = '0.2.6'
|
||||||
|
|
|
@ -9,9 +9,10 @@ import time
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp_swagger import setup_swagger
|
||||||
from aputils.signer import Signer
|
from aputils.signer import Signer
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from gunicorn.app.wsgiapp import WSGIApplication
|
from threading import Event, Thread
|
||||||
|
|
||||||
from . import logger as logging
|
from . import logger as logging
|
||||||
from .cache import get_cache
|
from .cache import get_cache
|
||||||
|
@ -22,10 +23,14 @@ from .misc import check_open_port
|
||||||
from .views import VIEWS
|
from .views import VIEWS
|
||||||
from .views.api import handle_api_path
|
from .views.api import handle_api_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib.resources import files as pkgfiles
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
from importlib_resources import files as pkgfiles
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from collections.abc import Awaitable
|
|
||||||
from tinysql import Database, Row
|
from tinysql import Database, Row
|
||||||
from typing import Any
|
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .misc import Message
|
from .misc import Message
|
||||||
|
|
||||||
|
@ -47,6 +52,7 @@ class Application(web.Application):
|
||||||
self['proc'] = None
|
self['proc'] = None
|
||||||
self['signer'] = None
|
self['signer'] = None
|
||||||
self['start_time'] = None
|
self['start_time'] = None
|
||||||
|
self['cleanup_thread'] = None
|
||||||
|
|
||||||
self['config'] = Config(cfgpath, load = True)
|
self['config'] = Config(cfgpath, load = True)
|
||||||
self['database'] = get_database(self.config)
|
self['database'] = get_database(self.config)
|
||||||
|
@ -57,10 +63,16 @@ class Application(web.Application):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.on_response_prepare.append(handle_access_log)
|
self.on_response_prepare.append(handle_access_log)
|
||||||
|
self.on_cleanup.append(handle_cleanup)
|
||||||
|
|
||||||
for path, view in VIEWS:
|
for path, view in VIEWS:
|
||||||
self.router.add_view(path, view)
|
self.router.add_view(path, view)
|
||||||
|
|
||||||
|
setup_swagger(self,
|
||||||
|
ui_version = 3,
|
||||||
|
swagger_from_file = pkgfiles('relay').joinpath('data', 'swagger.yaml')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache(self) -> Cache:
|
def cache(self) -> Cache:
|
||||||
|
@ -144,7 +156,9 @@ class Application(web.Application):
|
||||||
'--bind', f'{self.config.listen}:{self.config.port}',
|
'--bind', f'{self.config.listen}:{self.config.port}',
|
||||||
'--worker-class', 'aiohttp.GunicornWebWorker',
|
'--worker-class', 'aiohttp.GunicornWebWorker',
|
||||||
'--workers', str(self.config.workers),
|
'--workers', str(self.config.workers),
|
||||||
'--env', f'CONFIG_FILE={self.config.path}'
|
'--env', f'CONFIG_FILE={self.config.path}',
|
||||||
|
'--reload-extra-file', pkgfiles('relay').joinpath('data', 'swagger.yaml'),
|
||||||
|
'--reload-extra-file', pkgfiles('relay').joinpath('data', 'statements.sql')
|
||||||
]
|
]
|
||||||
|
|
||||||
if dev:
|
if dev:
|
||||||
|
@ -152,12 +166,15 @@ class Application(web.Application):
|
||||||
|
|
||||||
self.set_signal_handler(True)
|
self.set_signal_handler(True)
|
||||||
self['proc'] = subprocess.Popen(cmd) # pylint: disable=consider-using-with
|
self['proc'] = subprocess.Popen(cmd) # pylint: disable=consider-using-with
|
||||||
|
self['cleanup_thread'] = CacheCleanupThread(self)
|
||||||
|
self['cleanup_thread'].start()
|
||||||
|
|
||||||
|
|
||||||
def stop(self, *_) -> None:
|
def stop(self, *_) -> None:
|
||||||
if not self['proc']:
|
if not self['proc']:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self['cleanup_thread'].stop()
|
||||||
self['proc'].terminate()
|
self['proc'].terminate()
|
||||||
time_wait = 0.0
|
time_wait = 0.0
|
||||||
|
|
||||||
|
@ -172,30 +189,36 @@ class Application(web.Application):
|
||||||
self.set_signal_handler(False)
|
self.set_signal_handler(False)
|
||||||
self['proc'] = None
|
self['proc'] = None
|
||||||
|
|
||||||
|
self.cache.close()
|
||||||
|
self.database.close()
|
||||||
|
|
||||||
# not used, but keeping just in case
|
|
||||||
class GunicornRunner(WSGIApplication):
|
class CacheCleanupThread(Thread):
|
||||||
def __init__(self, app: Application):
|
def __init__(self, app: Application):
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.app_uri = 'relay.application:main_gunicorn'
|
self.running = Event()
|
||||||
self.options = {
|
|
||||||
'bind': f'{app.config.listen}:{app.config.port}',
|
|
||||||
'worker_class': 'aiohttp.GunicornWebWorker',
|
|
||||||
'workers': app.config.workers,
|
|
||||||
'raw_env': f'CONFIG_FILE={app.config.path}'
|
|
||||||
}
|
|
||||||
|
|
||||||
WSGIApplication.__init__(self)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(self):
|
def run(self) -> None:
|
||||||
for key, value in self.options.items():
|
cache = get_cache(self.app)
|
||||||
self.cfg.set(key, value)
|
|
||||||
|
while self.running.is_set():
|
||||||
|
time.sleep(3600)
|
||||||
|
logging.verbose("Removing old cache items")
|
||||||
|
cache.delete_old(14)
|
||||||
|
|
||||||
|
cache.close()
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def start(self) -> None:
|
||||||
logging.info('Starting webserver for %s', self.app.config.domain)
|
self.running.set()
|
||||||
WSGIApplication.run(self)
|
Thread.start(self)
|
||||||
|
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.running.clear()
|
||||||
|
|
||||||
|
|
||||||
async def handle_access_log(request: web.Request, response: web.Response) -> None:
|
async def handle_access_log(request: web.Request, response: web.Response) -> None:
|
||||||
|
@ -213,11 +236,17 @@ async def handle_access_log(request: web.Request, response: web.Response) -> Non
|
||||||
request.method,
|
request.method,
|
||||||
request.path,
|
request.path,
|
||||||
response.status,
|
response.status,
|
||||||
len(response.body),
|
response.content_length or 0,
|
||||||
request.headers.get('User-Agent', 'n/a')
|
request.headers.get('User-Agent', 'n/a')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cleanup(app: Application) -> None:
|
||||||
|
await app.client.close()
|
||||||
|
app.cache.close()
|
||||||
|
app.database.close()
|
||||||
|
|
||||||
|
|
||||||
async def main_gunicorn():
|
async def main_gunicorn():
|
||||||
try:
|
try:
|
||||||
app = Application(os.environ['CONFIG_FILE'], gunicorn = True)
|
app = Application(os.environ['CONFIG_FILE'], gunicorn = True)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import typing
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
from .database import get_database
|
from .database import get_database
|
||||||
|
@ -15,7 +15,6 @@ from .misc import Message, boolean
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Callable, Iterator
|
||||||
from tinysql import Database
|
|
||||||
from .application import Application
|
from .application import Application
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,6 +93,7 @@ class Cache(ABC):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get(self, namespace: str, key: str) -> Item:
|
def get(self, namespace: str, key: str) -> Item:
|
||||||
...
|
...
|
||||||
|
@ -119,11 +119,26 @@ class Cache(ABC):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_old(self, days: int = 14) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def close(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def set_item(self, item: Item) -> Item:
|
def set_item(self, item: Item) -> Item:
|
||||||
return self.set(
|
return self.set(
|
||||||
item.namespace,
|
item.namespace,
|
||||||
|
@ -201,12 +216,32 @@ class SqlCache(Cache):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete_old(self, days: int = 14) -> None:
|
||||||
|
limit = datetime.now(tz = timezone.utc) - timedelta(days = days)
|
||||||
|
params = {"limit": limit.timestamp()}
|
||||||
|
|
||||||
|
with self._db.connection() as conn:
|
||||||
|
with conn.execute("DELETE FROM cache WHERE updated < :limit", params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
with self._db.connection() as conn:
|
||||||
|
with conn.execute("DELETE FROM cache"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
with self._db.connection() as conn:
|
with self._db.connection() as conn:
|
||||||
with conn.exec_statement(f'create-cache-table-{self._db.type.name.lower()}', None):
|
with conn.exec_statement(f'create-cache-table-{self._db.type.name.lower()}', None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._db.close()
|
||||||
|
self._db = None
|
||||||
|
|
||||||
|
|
||||||
@register_cache
|
@register_cache
|
||||||
class RedisCache(Cache):
|
class RedisCache(Cache):
|
||||||
name: str = 'redis'
|
name: str = 'redis'
|
||||||
|
@ -239,7 +274,7 @@ class RedisCache(Cache):
|
||||||
|
|
||||||
|
|
||||||
def get_keys(self, namespace: str) -> Iterator[str]:
|
def get_keys(self, namespace: str) -> Iterator[str]:
|
||||||
for key in self._rd.keys(self.get_key_name(namespace, '*')):
|
for key in self._rd.scan_iter(self.get_key_name(namespace, '*')):
|
||||||
*_, key_name = key.split(':', 2)
|
*_, key_name = key.split(':', 2)
|
||||||
yield key_name
|
yield key_name
|
||||||
|
|
||||||
|
@ -247,7 +282,7 @@ class RedisCache(Cache):
|
||||||
def get_namespaces(self) -> Iterator[str]:
|
def get_namespaces(self) -> Iterator[str]:
|
||||||
namespaces = []
|
namespaces = []
|
||||||
|
|
||||||
for key in self._rd.keys(f'{self.prefix}:*'):
|
for key in self._rd.scan_iter(f'{self.prefix}:*'):
|
||||||
_, namespace, _ = key.split(':', 2)
|
_, namespace, _ = key.split(':', 2)
|
||||||
|
|
||||||
if namespace not in namespaces:
|
if namespace not in namespaces:
|
||||||
|
@ -269,6 +304,21 @@ class RedisCache(Cache):
|
||||||
self._rd.delete(self.get_key_name(namespace, key))
|
self._rd.delete(self.get_key_name(namespace, key))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_old(self, days: int = 14) -> None:
|
||||||
|
limit = datetime.now(tz = timezone.utc) - timedelta(days = days)
|
||||||
|
|
||||||
|
for full_key in self._rd.scan_iter(f'{self.prefix}:*'):
|
||||||
|
_, namespace, key = full_key.split(':', 2)
|
||||||
|
item = self.get(namespace, key)
|
||||||
|
|
||||||
|
if item.updated < limit:
|
||||||
|
self.delete_item(item)
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self._rd.delete(f"{self.prefix}:*")
|
||||||
|
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
options = {
|
options = {
|
||||||
'client_name': f'ActivityRelay_{self.app.config.domain}',
|
'client_name': f'ActivityRelay_{self.app.config.domain}',
|
||||||
|
@ -286,3 +336,8 @@ class RedisCache(Cache):
|
||||||
options['port'] = self.app.config.rd_port
|
options['port'] = self.app.config.rd_port
|
||||||
|
|
||||||
self._rd = Redis(**options)
|
self._rd = Redis(**options)
|
||||||
|
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._rd.close()
|
||||||
|
self._rd = None
|
||||||
|
|
|
@ -188,6 +188,14 @@ class Config:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
if key in {'port', 'pg_port', 'workers'} and not isinstance(value, int):
|
if key in {'port', 'pg_port', 'workers'} and not isinstance(value, int):
|
||||||
value = int(value)
|
if (value := int(value)) < 1:
|
||||||
|
if key == 'port':
|
||||||
|
value = 8080
|
||||||
|
|
||||||
|
elif key == 'pg_port':
|
||||||
|
value = 5432
|
||||||
|
|
||||||
|
elif key == 'workers':
|
||||||
|
value = len(os.sched_getaffinity(0))
|
||||||
|
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
713
relay/data/swagger.yaml
Normal file
713
relay/data/swagger.yaml
Normal file
|
@ -0,0 +1,713 @@
|
||||||
|
swagger: "2.0"
|
||||||
|
info:
|
||||||
|
description: |
|
||||||
|
ActivityRelay API
|
||||||
|
version: "0.2.5"
|
||||||
|
title: ActivityRelay API
|
||||||
|
license:
|
||||||
|
name: AGPL 3
|
||||||
|
url: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
|
|
||||||
|
basePath: /api
|
||||||
|
schemes:
|
||||||
|
- https
|
||||||
|
|
||||||
|
securityDefinitions:
|
||||||
|
Bearer:
|
||||||
|
type: apiKey
|
||||||
|
name: Authorization
|
||||||
|
in: header
|
||||||
|
description: "Enter the token with the `Bearer ` prefix"
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Global
|
||||||
|
description: Attributes that apply to all endpoints
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
description: Token is missing or invalid
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
/v1/relay:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Relay
|
||||||
|
description: Info about the relay instance
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Relay info
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Info"
|
||||||
|
|
||||||
|
/v1/token:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Token
|
||||||
|
description: Verify API token
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Valid token
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Token
|
||||||
|
description: Get a new token
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: username
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: password
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Created token
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Token"
|
||||||
|
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Token
|
||||||
|
description: Revoke a token
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Revoked token
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
|
||||||
|
/v1/config:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
description: Get the current config values
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Config values
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Config"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
description: Set a config value
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: value
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Value was set
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"400":
|
||||||
|
description: Key is invalid
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
description: Set a config option to default
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Value was reset
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"400":
|
||||||
|
description: Key is invalid
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
/v1/instance:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Instance
|
||||||
|
description: Get the list of subscribed instances
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of instances
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Instance"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Instance
|
||||||
|
description: Add an instance
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: actor
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: inbox
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
- in: formData
|
||||||
|
name: software
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: followid
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Newly added instance
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Instance"
|
||||||
|
"500":
|
||||||
|
description: Failed to fetch actor
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- Instance
|
||||||
|
description: Update an instance
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: actor
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: inbox
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
- in: formData
|
||||||
|
name: software
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: followid
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Updated instance data
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Instance"
|
||||||
|
"404":
|
||||||
|
description: Instance with the specified domain does not exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Instance
|
||||||
|
description: Remove an instance from the relay
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Instance was removed
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"404":
|
||||||
|
description: Instance with the specified domain does not exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
/v1/domain_ban:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Domain Ban
|
||||||
|
description: Get a list of all banned domains
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of banned domains
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/DomainBan"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Domain Ban
|
||||||
|
description: Ban a domain
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: reason
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: note
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: New domain ban
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/DomainBan"
|
||||||
|
"404":
|
||||||
|
description: Domain ban already exists
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- Domain Ban
|
||||||
|
description: Update a banned domain
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: reason
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: note
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Updated domain ban
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/DomainBan"
|
||||||
|
"400":
|
||||||
|
description: A reason or note was not specified
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
"404":
|
||||||
|
description: Domain ban doesn't exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Domain Ban
|
||||||
|
description: Unban a domain
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Unbanned domain
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"404":
|
||||||
|
description: Domain ban doesn't exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
/v1/software_ban:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Software Ban
|
||||||
|
description: Get a list of all banned software
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of banned software
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/SoftwareBan"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Software Ban
|
||||||
|
description: Ban software
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: reason
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: note
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: New software ban
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/SoftwareBan"
|
||||||
|
"404":
|
||||||
|
description: Software ban already exists
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- Software Ban
|
||||||
|
description: Update banned software
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: reason
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- in: formData
|
||||||
|
name: note
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Updated software ban
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/SoftwareBan"
|
||||||
|
"400":
|
||||||
|
description: A reason or note was not specified
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
"404":
|
||||||
|
description: Software ban doesn't exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Software Ban
|
||||||
|
description: Unban software
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Unbanned software
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"404":
|
||||||
|
description: Software ban doesn't exist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
/v1/whitelist:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Whitelist
|
||||||
|
description: Get a list of all whitelisted domains
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of whitelisted domains
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Whitelist"
|
||||||
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Whitelist
|
||||||
|
description: Add a domain to the whitelist
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: New whitelisted domain
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Whitelist"
|
||||||
|
"404":
|
||||||
|
description: Domain already whitelisted
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Whitelist
|
||||||
|
description: Remove domain from the whitelist
|
||||||
|
parameters:
|
||||||
|
- in: formData
|
||||||
|
name: domain
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- multipart/form-data
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Domain removed from the whitelist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Message"
|
||||||
|
"404":
|
||||||
|
description: Domain was not in the whitelist
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Error"
|
||||||
|
|
||||||
|
definitions:
|
||||||
|
Message:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
description: Human-readable message text
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
description: Human-readable message text
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Config:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
log-level:
|
||||||
|
description: Maximum level of log messages to print to the console
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: Name of the relay
|
||||||
|
type: string
|
||||||
|
note:
|
||||||
|
description: Blurb to display on the home page
|
||||||
|
type: string
|
||||||
|
whitelist-enabled:
|
||||||
|
description: Only allow specific instances to join the relay when enabled
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
DomainBan:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Banned domain
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
description: Public reason for the domain ban
|
||||||
|
type: string
|
||||||
|
note:
|
||||||
|
description: Private note for the software ban
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
description: Date the ban was added
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
Info:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Domain the relay is hosted on
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: Name of the relay
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
description: Short blurb that describes the relay
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
description: Version of the relay
|
||||||
|
type: string
|
||||||
|
whitelist_enabled:
|
||||||
|
description: Only allow specific instances to join the relay when enabled
|
||||||
|
type: boolean
|
||||||
|
email:
|
||||||
|
description: E-Mail address used to contact the admin
|
||||||
|
type: string
|
||||||
|
admin:
|
||||||
|
description: Fediverse account of the admin
|
||||||
|
type: string
|
||||||
|
icon:
|
||||||
|
description: Image for the relay instance
|
||||||
|
type: string
|
||||||
|
instances:
|
||||||
|
description: List of currently subscribed Fediverse instances
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Instance:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Domain the instance is hosted on
|
||||||
|
type: string
|
||||||
|
actor:
|
||||||
|
description: ActivityPub actor of the instance that joined
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
inbox:
|
||||||
|
description: Inbox (usually shared) of the instance to post to
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
followid:
|
||||||
|
description: Url to the Follow activity of the instance
|
||||||
|
type: string
|
||||||
|
format: url
|
||||||
|
software:
|
||||||
|
description: Nodeinfo-formatted name of the instance's software
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
description: Date the instance joined or was added
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
SoftwareBan:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Nodeinfo-formatted software name
|
||||||
|
reason:
|
||||||
|
description: Public reason for the software ban
|
||||||
|
type: string
|
||||||
|
note:
|
||||||
|
description: Private note for the software ban
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
description: Date the ban was added
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
Token:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
description: Character string used for authenticating with the api
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Whitelist:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
description: Whitelisted domain
|
||||||
|
created:
|
||||||
|
description: Date the domain was added to the whitelist
|
||||||
|
type: string
|
||||||
|
format: date-time
|
|
@ -4,9 +4,12 @@ import Crypto
|
||||||
import asyncio
|
import asyncio
|
||||||
import click
|
import click
|
||||||
import platform
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from aputils.signer import Signer
|
from aputils.signer import Signer
|
||||||
|
from gunicorn.app.wsgiapp import WSGIApplication
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
@ -234,9 +237,21 @@ def cli_run(ctx: click.Context, dev: bool = False) -> None:
|
||||||
click.echo(pip_command)
|
click.echo(pip_command)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
subprocess.run([sys.executable, 'run-gunicorn'], check = False)
|
||||||
|
return
|
||||||
|
|
||||||
ctx.obj.run(dev)
|
ctx.obj.run(dev)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command('run-gunicorn')
|
||||||
|
@click.pass_context
|
||||||
|
def cli_run_gunicorn(ctx: click.Context) -> None:
|
||||||
|
runner = GunicornRunner(ctx.obj)
|
||||||
|
runner.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command('convert')
|
@cli.command('convert')
|
||||||
@click.option('--old-config', '-o', help = 'Path to the config file to convert from')
|
@click.option('--old-config', '-o', help = 'Path to the config file to convert from')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
|
@ -903,6 +918,31 @@ def cli_whitelist_import(ctx: click.Context) -> None:
|
||||||
click.echo('Imported whitelist from inboxes')
|
click.echo('Imported whitelist from inboxes')
|
||||||
|
|
||||||
|
|
||||||
|
class GunicornRunner(WSGIApplication):
|
||||||
|
def __init__(self, app: Application):
|
||||||
|
self.app = app
|
||||||
|
self.app_uri = 'relay.application:main_gunicorn'
|
||||||
|
self.options = {
|
||||||
|
'bind': f'{app.config.listen}:{app.config.port}',
|
||||||
|
'worker_class': 'aiohttp.GunicornWebWorker',
|
||||||
|
'workers': app.config.workers,
|
||||||
|
'raw_env': f'CONFIG_FILE={app.config.path}'
|
||||||
|
}
|
||||||
|
|
||||||
|
WSGIApplication.__init__(self)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
for key, value in self.options.items():
|
||||||
|
self.cfg.set(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logging.info('Starting webserver for %s', self.app.config.domain)
|
||||||
|
WSGIApplication.run(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
cli(prog_name='relay')
|
cli(prog_name='relay')
|
||||||
|
|
|
@ -10,14 +10,8 @@ from aputils.message import Message as ApMessage
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from collections.abc import Awaitable, Coroutine, Generator
|
|
||||||
from tinysql import Connection
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from .application import Application
|
from .application import Application
|
||||||
from .cache import Cache
|
|
||||||
from .config import Config
|
|
||||||
from .database import Database
|
|
||||||
from .http_client import HttpClient
|
|
||||||
|
|
||||||
|
|
||||||
IS_DOCKER = bool(os.environ.get('DOCKER_RUNNING'))
|
IS_DOCKER = bool(os.environ.get('DOCKER_RUNNING'))
|
||||||
|
@ -215,7 +209,7 @@ class Response(AiohttpResponse):
|
||||||
ctype: str = 'text') -> Response:
|
ctype: str = 'text') -> Response:
|
||||||
|
|
||||||
if ctype == 'json':
|
if ctype == 'json':
|
||||||
body = json.dumps({'status': status, 'error': body})
|
body = json.dumps({'error': body})
|
||||||
|
|
||||||
return cls.new(body=body, status=status, ctype=ctype)
|
return cls.new(body=body, status=status, ctype=ctype)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ if typing.TYPE_CHECKING:
|
||||||
from aiohttp.web import Request
|
from aiohttp.web import Request
|
||||||
from aputils.signer import Signer
|
from aputils.signer import Signer
|
||||||
from tinysql import Row
|
from tinysql import Row
|
||||||
from ..database.connection import Connection
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
|
@ -17,7 +17,6 @@ from ..misc import Message, Response
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from aiohttp.web import Request
|
from aiohttp.web import Request
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
from ..database.connection import Connection
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_IGNORE = (
|
CONFIG_IGNORE = (
|
||||||
|
@ -35,7 +34,7 @@ PUBLIC_API_PATHS: tuple[tuple[str, str]] = (
|
||||||
|
|
||||||
|
|
||||||
def check_api_path(method: str, path: str) -> bool:
|
def check_api_path(method: str, path: str) -> bool:
|
||||||
if (method, path) in PUBLIC_API_PATHS:
|
if path.startswith('/api/doc') or (method, path) in PUBLIC_API_PATHS:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return path.startswith('/api')
|
return path.startswith('/api')
|
||||||
|
@ -68,7 +67,7 @@ async def handle_api_path(request: web.Request, handler: Coroutine) -> web.Respo
|
||||||
@register_route('/api/v1/token')
|
@register_route('/api/v1/token')
|
||||||
class Login(View):
|
class Login(View):
|
||||||
async def get(self, request: Request) -> Response:
|
async def get(self, request: Request) -> Response:
|
||||||
return Response.new({'message': 'Token valid :3'})
|
return Response.new({'message': 'Token valid'}, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def post(self, request: Request) -> Response:
|
async def post(self, request: Request) -> Response:
|
||||||
|
@ -214,28 +213,14 @@ class Inbox(View):
|
||||||
return Response.new(row, ctype = 'json')
|
return Response.new(row, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
@register_route('/api/v1/instance/{domain}')
|
async def patch(self, request: Request) -> Response:
|
||||||
class InboxSingle(View):
|
|
||||||
async def get(self, request: Request, domain: str) -> Response:
|
|
||||||
with self.database.connection(False) as conn:
|
|
||||||
if not (row := conn.get_inbox(domain)):
|
|
||||||
return Response.new_error(404, 'Instance with domain not found', 'json')
|
|
||||||
|
|
||||||
row['created'] = datetime.fromtimestamp(row['created'], tz = timezone.utc).isoformat()
|
|
||||||
return Response.new(row, ctype = 'json')
|
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request, domain: str) -> Response:
|
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_inbox(domain):
|
data = await self.get_api_data(['domain'], ['actor', 'software', 'followid'])
|
||||||
return Response.new_error(404, 'Instance with domain not found', 'json')
|
|
||||||
|
|
||||||
data = await self.get_api_data([], ['actor', 'software', 'followid'])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
if isinstance(data, Response):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
if not (instance := conn.get_inbox(domain)):
|
if not (instance := conn.get_inbox(data['domain'])):
|
||||||
return Response.new_error(404, 'Instance with domain not found', 'json')
|
return Response.new_error(404, 'Instance with domain not found', 'json')
|
||||||
|
|
||||||
instance = conn.update_inbox(instance['inbox'], **data)
|
instance = conn.update_inbox(instance['inbox'], **data)
|
||||||
|
@ -245,10 +230,15 @@ class InboxSingle(View):
|
||||||
|
|
||||||
async def delete(self, request: Request, domain: str) -> Response:
|
async def delete(self, request: Request, domain: str) -> Response:
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_inbox(domain):
|
data = await self.get_api_data(['domain'], [])
|
||||||
|
|
||||||
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if not conn.get_inbox(data['domain']):
|
||||||
return Response.new_error(404, 'Instance with domain not found', 'json')
|
return Response.new_error(404, 'Instance with domain not found', 'json')
|
||||||
|
|
||||||
conn.del_inbox(domain)
|
conn.del_inbox(data['domain'])
|
||||||
|
|
||||||
return Response.new({'message': 'Deleted instance'}, ctype = 'json')
|
return Response.new({'message': 'Deleted instance'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -277,40 +267,35 @@ class DomainBan(View):
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
@register_route('/api/v1/domain_ban/{domain}')
|
async def patch(self, request: Request) -> Response:
|
||||||
class DomainBanSingle(View):
|
|
||||||
async def get(self, request: Request, domain: str) -> Response:
|
|
||||||
with self.database.connection(False) as conn:
|
|
||||||
if not (ban := conn.get_domain_ban(domain)):
|
|
||||||
return Response.new_error(404, 'Domain ban not found', 'json')
|
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request, domain: str) -> Response:
|
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_domain_ban(domain):
|
data = await self.get_api_data(['domain'], ['note', 'reason'])
|
||||||
return Response.new_error(404, 'Domain not banned', 'json')
|
|
||||||
|
|
||||||
data = await self.get_api_data([], ['note', 'reason'])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
if isinstance(data, Response):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
if not conn.get_domain_ban(data['domain']):
|
||||||
|
return Response.new_error(404, 'Domain not banned', 'json')
|
||||||
|
|
||||||
if not any([data.get('note'), data.get('reason')]):
|
if not any([data.get('note'), data.get('reason')]):
|
||||||
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
||||||
|
|
||||||
ban = conn.update_domain_ban(domain, **data)
|
ban = conn.update_domain_ban(data['domain'], **data)
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def delete(self, request: Request, domain: str) -> Response:
|
async def delete(self, request: Request) -> Response:
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_domain_ban(domain):
|
data = await self.get_api_data(['domain'], [])
|
||||||
|
|
||||||
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if not conn.get_domain_ban(data['domain']):
|
||||||
return Response.new_error(404, 'Domain not banned', 'json')
|
return Response.new_error(404, 'Domain not banned', 'json')
|
||||||
|
|
||||||
conn.del_domain_ban(domain)
|
conn.del_domain_ban(data['domain'])
|
||||||
|
|
||||||
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -339,40 +324,35 @@ class SoftwareBan(View):
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
@register_route('/api/v1/software_ban/{name}')
|
async def patch(self, request: Request) -> Response:
|
||||||
class SoftwareBanSingle(View):
|
data = await self.get_api_data(['name'], ['note', 'reason'])
|
||||||
async def get(self, request: Request, name: str) -> Response:
|
|
||||||
with self.database.connection(False) as conn:
|
|
||||||
if not (ban := conn.get_software_ban(name)):
|
|
||||||
return Response.new_error(404, 'Software ban not found', 'json')
|
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request, name: str) -> Response:
|
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_software_ban(name):
|
if not conn.get_software_ban(data['name']):
|
||||||
return Response.new_error(404, 'Software not banned', 'json')
|
return Response.new_error(404, 'Software not banned', 'json')
|
||||||
|
|
||||||
data = await self.get_api_data([], ['note', 'reason'])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
|
||||||
return data
|
|
||||||
|
|
||||||
if not any([data.get('note'), data.get('reason')]):
|
if not any([data.get('note'), data.get('reason')]):
|
||||||
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
||||||
|
|
||||||
ban = conn.update_software_ban(name, **data)
|
ban = conn.update_software_ban(data['name'], **data)
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def delete(self, request: Request, name: str) -> Response:
|
async def delete(self, request: Request) -> Response:
|
||||||
|
data = await self.get_api_data(['name'], [])
|
||||||
|
|
||||||
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
if not conn.get_software_ban(name):
|
if not conn.get_software_ban(data['name']):
|
||||||
return Response.new_error(404, 'Software not banned', 'json')
|
return Response.new_error(404, 'Software not banned', 'json')
|
||||||
|
|
||||||
conn.del_software_ban(name)
|
conn.del_software_ban(data['name'])
|
||||||
|
|
||||||
return Response.new({'message': 'Unbanned software'}, ctype = 'json')
|
return Response.new({'message': 'Unbanned software'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -401,21 +381,16 @@ class Whitelist(View):
|
||||||
return Response.new(item, ctype = 'json')
|
return Response.new(item, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
@register_route('/api/v1/domain/{domain}')
|
async def delete(self, request: Request) -> Response:
|
||||||
class WhitelistSingle(View):
|
data = await self.get_api_data(['domain'], [])
|
||||||
async def get(self, request: Request, domain: str) -> Response:
|
|
||||||
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
with self.database.connection(False) as conn:
|
with self.database.connection(False) as conn:
|
||||||
if not (item := conn.get_domain_whitelist(domain)):
|
if not conn.get_domain_whitelist(data['domain']):
|
||||||
return Response.new_error(404, 'Domain not in whitelist', 'json')
|
return Response.new_error(404, 'Domain not in whitelist', 'json')
|
||||||
|
|
||||||
return Response.new(item, ctype = 'json')
|
conn.del_domain_whitelist(data['domain'])
|
||||||
|
|
||||||
|
|
||||||
async def delete(self, request: Request, domain: str) -> Response:
|
|
||||||
with self.database.connection(False) as conn:
|
|
||||||
if not conn.get_domain_whitelist(domain):
|
|
||||||
return Response.new_error(404, 'Domain not in whitelist', 'json')
|
|
||||||
|
|
||||||
conn.del_domain_whitelist(domain)
|
|
||||||
|
|
||||||
return Response.new({'message': 'Removed domain from whitelist'}, ctype = 'json')
|
return Response.new({'message': 'Removed domain from whitelist'}, ctype = 'json')
|
||||||
|
|
|
@ -4,15 +4,10 @@ import typing
|
||||||
|
|
||||||
from .base import View, register_route
|
from .base import View, register_route
|
||||||
|
|
||||||
from .. import __version__
|
|
||||||
from ..misc import Response
|
from ..misc import Response
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from aiohttp.web import Request
|
from aiohttp.web import Request
|
||||||
from aputils.signer import Signer
|
|
||||||
from collections.abc import Callable
|
|
||||||
from tinysql import Row
|
|
||||||
from ..database.connection import Connection
|
|
||||||
|
|
||||||
|
|
||||||
HOME_TEMPLATE = """
|
HOME_TEMPLATE = """
|
||||||
|
|
|
@ -13,7 +13,6 @@ from ..misc import Response
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from aiohttp.web import Request
|
from aiohttp.web import Request
|
||||||
from ..database.connection import Connection
|
|
||||||
|
|
||||||
|
|
||||||
VERSION = __version__
|
VERSION = __version__
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
aiohttp>=3.9.1
|
aiohttp>=3.9.1
|
||||||
|
aiohttp-swagger[performance]==1.0.16
|
||||||
aputils@https://git.barkshark.xyz/barkshark/aputils/archive/0.1.6a.tar.gz
|
aputils@https://git.barkshark.xyz/barkshark/aputils/archive/0.1.6a.tar.gz
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==23.1.0
|
||||||
click>=8.1.2
|
click>=8.1.2
|
||||||
|
@ -6,6 +7,6 @@ gunicorn==21.1.0
|
||||||
hiredis==2.3.2
|
hiredis==2.3.2
|
||||||
pyyaml>=6.0
|
pyyaml>=6.0
|
||||||
redis==5.0.1
|
redis==5.0.1
|
||||||
tinysql[postgres]@https://git.barkshark.xyz/barkshark/tinysql/archive/f8db814084dded0a46bd3a9576e09fca860f2166.tar.gz
|
tinysql[postgres]@https://git.barkshark.xyz/barkshark/tinysql/archive/main.tar.gz
|
||||||
|
|
||||||
importlib_resources==6.1.1;python_version<'3.9'
|
importlib_resources==6.1.1;python_version<'3.9'
|
||||||
|
|
|
@ -34,6 +34,7 @@ dev = file: dev-requirements.txt
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
relay =
|
relay =
|
||||||
data/statements.sql
|
data/statements.sql
|
||||||
|
data/swagger.yaml
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
@ -42,3 +43,5 @@ console_scripts =
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
select = F401
|
select = F401
|
||||||
|
per-file-ignores =
|
||||||
|
relay/views/__init__.py: F401
|
||||||
|
|
Loading…
Reference in a new issue