mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-23 23:17:58 +00:00
Compare commits
No commits in common. "3391749800f9858091e205753da33145542f9a2f" and "b259f2d76068e1499718eeeeaa55234d637f1873" have entirely different histories.
3391749800
...
b259f2d760
|
@ -1,2 +1 @@
|
||||||
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. Optionally add `-d` or `--dev` to enable auto-reloading on code changes.
|
Run the relay.
|
||||||
|
|
||||||
activityrelay run
|
activityrelay run
|
||||||
|
|
||||||
|
@ -58,50 +58,6 @@ 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.
|
||||||
|
@ -119,7 +75,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> --actor <actor url> --followid <follow activity ID> --software <nodeinfo software name>
|
activityrelay inbox add <inbox or domain>
|
||||||
|
|
||||||
|
|
||||||
### Remove
|
### Remove
|
||||||
|
@ -199,7 +155,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> --reason <text> --note <text>
|
activityrelay instance ban <domain>
|
||||||
|
|
||||||
|
|
||||||
### Unban
|
### Unban
|
||||||
|
@ -211,10 +167,9 @@ Remove an instance from the ban list.
|
||||||
|
|
||||||
### Update
|
### Update
|
||||||
|
|
||||||
Update the ban reason or note for an instance ban. Either `--reason` and/or `--note` must be
|
Update the ban reason or note for an instance ban.
|
||||||
specified.
|
|
||||||
|
|
||||||
activityrelay instance update bad.example.com --reason <text> --note <text>
|
activityrelay instance update bad.example.com --reason "the baddest reason"
|
||||||
|
|
||||||
|
|
||||||
## Software
|
## Software
|
||||||
|
@ -239,7 +194,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> --reason <text> --note <text>
|
activityrelay software ban [-f/--fetch-nodeinfo] <name, domain, or RELAYS>
|
||||||
|
|
||||||
|
|
||||||
### Unban
|
### Unban
|
||||||
|
@ -259,4 +214,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 <text> --note <text>
|
activityrelay software update relay.example.com --reason "begone relay"
|
||||||
|
|
|
@ -19,10 +19,11 @@ proxy is on the same host.
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
|
||||||
### Web Workers
|
### Push Workers
|
||||||
|
|
||||||
The number of processes to spawn for handling web requests. Leave it at 0 to automatically detect
|
The relay can be configured to use threads to push messages out. For smaller relays, this isn't
|
||||||
how many processes should be spawned.
|
necessary, but bigger ones (>100 instances) will want to set this to the number of available cpu
|
||||||
|
threads.
|
||||||
|
|
||||||
workers: 0
|
workers: 0
|
||||||
|
|
||||||
|
|
11
relay.spec
11
relay.spec
|
@ -1,10 +1,7 @@
|
||||||
# -*- 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(
|
||||||
|
@ -12,13 +9,9 @@ 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.6'
|
__version__ = '0.2.5'
|
||||||
|
|
|
@ -9,10 +9,9 @@ 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 threading import Event, Thread
|
from gunicorn.app.wsgiapp import WSGIApplication
|
||||||
|
|
||||||
from . import logger as logging
|
from . import logger as logging
|
||||||
from .cache import get_cache
|
from .cache import get_cache
|
||||||
|
@ -23,14 +22,10 @@ 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
|
||||||
|
|
||||||
|
@ -52,7 +47,6 @@ 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)
|
||||||
|
@ -63,16 +57,10 @@ 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:
|
||||||
|
@ -156,9 +144,7 @@ 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:
|
||||||
|
@ -166,15 +152,12 @@ 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
|
||||||
|
|
||||||
|
@ -189,36 +172,30 @@ 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 CacheCleanupThread(Thread):
|
class GunicornRunner(WSGIApplication):
|
||||||
def __init__(self, app: Application):
|
def __init__(self, app: Application):
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.running = Event()
|
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 run(self) -> None:
|
def load_config(self):
|
||||||
cache = get_cache(self.app)
|
for key, value in self.options.items():
|
||||||
|
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 start(self) -> None:
|
def run(self):
|
||||||
self.running.set()
|
logging.info('Starting webserver for %s', self.app.config.domain)
|
||||||
Thread.start(self)
|
WSGIApplication.run(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:
|
||||||
|
@ -236,17 +213,11 @@ async def handle_access_log(request: web.Request, response: web.Response) -> Non
|
||||||
request.method,
|
request.method,
|
||||||
request.path,
|
request.path,
|
||||||
response.status,
|
response.status,
|
||||||
response.content_length or 0,
|
len(response.body),
|
||||||
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, timedelta, timezone
|
from datetime import datetime, timezone
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
from .database import get_database
|
from .database import get_database
|
||||||
|
@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,7 +94,6 @@ 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,26 +119,11 @@ 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,
|
||||||
|
@ -216,32 +201,12 @@ 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'
|
||||||
|
@ -274,7 +239,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.scan_iter(self.get_key_name(namespace, '*')):
|
for key in self._rd.keys(self.get_key_name(namespace, '*')):
|
||||||
*_, key_name = key.split(':', 2)
|
*_, key_name = key.split(':', 2)
|
||||||
yield key_name
|
yield key_name
|
||||||
|
|
||||||
|
@ -282,7 +247,7 @@ class RedisCache(Cache):
|
||||||
def get_namespaces(self) -> Iterator[str]:
|
def get_namespaces(self) -> Iterator[str]:
|
||||||
namespaces = []
|
namespaces = []
|
||||||
|
|
||||||
for key in self._rd.scan_iter(f'{self.prefix}:*'):
|
for key in self._rd.keys(f'{self.prefix}:*'):
|
||||||
_, namespace, _ = key.split(':', 2)
|
_, namespace, _ = key.split(':', 2)
|
||||||
|
|
||||||
if namespace not in namespaces:
|
if namespace not in namespaces:
|
||||||
|
@ -304,21 +269,6 @@ 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}',
|
||||||
|
@ -336,8 +286,3 @@ 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,14 +188,6 @@ 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):
|
||||||
if (value := int(value)) < 1:
|
value = int(value)
|
||||||
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)
|
||||||
|
|
|
@ -1,713 +0,0 @@
|
||||||
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,12 +4,9 @@ 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
|
||||||
|
@ -237,21 +234,9 @@ 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
|
||||||
|
@ -918,31 +903,6 @@ 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,8 +10,14 @@ 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'))
|
||||||
|
@ -209,7 +215,7 @@ class Response(AiohttpResponse):
|
||||||
ctype: str = 'text') -> Response:
|
ctype: str = 'text') -> Response:
|
||||||
|
|
||||||
if ctype == 'json':
|
if ctype == 'json':
|
||||||
body = json.dumps({'error': body})
|
body = json.dumps({'status': status, 'error': body})
|
||||||
|
|
||||||
return cls.new(body=body, status=status, ctype=ctype)
|
return cls.new(body=body, status=status, ctype=ctype)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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,6 +17,7 @@ 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 = (
|
||||||
|
@ -34,7 +35,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 path.startswith('/api/doc') or (method, path) in PUBLIC_API_PATHS:
|
if (method, path) in PUBLIC_API_PATHS:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return path.startswith('/api')
|
return path.startswith('/api')
|
||||||
|
@ -67,7 +68,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'}, ctype = 'json')
|
return Response.new({'message': 'Token valid :3'})
|
||||||
|
|
||||||
|
|
||||||
async def post(self, request: Request) -> Response:
|
async def post(self, request: Request) -> Response:
|
||||||
|
@ -213,14 +214,28 @@ class Inbox(View):
|
||||||
return Response.new(row, ctype = 'json')
|
return Response.new(row, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request) -> Response:
|
@register_route('/api/v1/instance/{domain}')
|
||||||
|
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:
|
||||||
data = await self.get_api_data(['domain'], ['actor', 'software', 'followid'])
|
if not conn.get_inbox(domain):
|
||||||
|
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(data['domain'])):
|
if not (instance := conn.get_inbox(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)
|
||||||
|
@ -230,15 +245,10 @@ class Inbox(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:
|
||||||
data = await self.get_api_data(['domain'], [])
|
if not conn.get_inbox(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(data['domain'])
|
conn.del_inbox(domain)
|
||||||
|
|
||||||
return Response.new({'message': 'Deleted instance'}, ctype = 'json')
|
return Response.new({'message': 'Deleted instance'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -267,35 +277,40 @@ class DomainBan(View):
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request) -> Response:
|
@register_route('/api/v1/domain_ban/{domain}')
|
||||||
with self.database.connection(True) as conn:
|
class DomainBanSingle(View):
|
||||||
data = await self.get_api_data(['domain'], ['note', 'reason'])
|
async def get(self, request: Request, domain: str) -> Response:
|
||||||
|
with self.database.connection(False) as conn:
|
||||||
if isinstance(data, Response):
|
if not (ban := conn.get_domain_ban(domain)):
|
||||||
return data
|
return Response.new_error(404, 'Domain ban not found', 'json')
|
||||||
|
|
||||||
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')]):
|
|
||||||
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
|
||||||
|
|
||||||
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) -> Response:
|
async def patch(self, request: Request, domain: str) -> Response:
|
||||||
with self.database.connection(True) as conn:
|
with self.database.connection(True) as conn:
|
||||||
data = await self.get_api_data(['domain'], [])
|
if not conn.get_domain_ban(domain):
|
||||||
|
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']):
|
if not any([data.get('note'), data.get('reason')]):
|
||||||
|
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
||||||
|
|
||||||
|
ban = conn.update_domain_ban(domain, **data)
|
||||||
|
|
||||||
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
|
async def delete(self, request: Request, domain: str) -> Response:
|
||||||
|
with self.database.connection(True) as conn:
|
||||||
|
if not conn.get_domain_ban(domain):
|
||||||
return Response.new_error(404, 'Domain not banned', 'json')
|
return Response.new_error(404, 'Domain not banned', 'json')
|
||||||
|
|
||||||
conn.del_domain_ban(data['domain'])
|
conn.del_domain_ban(domain)
|
||||||
|
|
||||||
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -324,35 +339,40 @@ class SoftwareBan(View):
|
||||||
return Response.new(ban, ctype = 'json')
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request) -> Response:
|
@register_route('/api/v1/software_ban/{name}')
|
||||||
data = await self.get_api_data(['name'], ['note', 'reason'])
|
class SoftwareBanSingle(View):
|
||||||
|
async def get(self, request: Request, name: str) -> Response:
|
||||||
if isinstance(data, Response):
|
with self.database.connection(False) as conn:
|
||||||
return data
|
if not (ban := conn.get_software_ban(name)):
|
||||||
|
return Response.new_error(404, 'Software ban not found', 'json')
|
||||||
with self.database.connection(True) as conn:
|
|
||||||
if not conn.get_software_ban(data['name']):
|
|
||||||
return Response.new_error(404, 'Software not banned', 'json')
|
|
||||||
|
|
||||||
if not any([data.get('note'), data.get('reason')]):
|
|
||||||
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
|
||||||
|
|
||||||
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) -> Response:
|
async def patch(self, request: Request, name: str) -> 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(data['name']):
|
if not conn.get_software_ban(name):
|
||||||
return Response.new_error(404, 'Software not banned', 'json')
|
return Response.new_error(404, 'Software not banned', 'json')
|
||||||
|
|
||||||
conn.del_software_ban(data['name'])
|
data = await self.get_api_data([], ['note', 'reason'])
|
||||||
|
|
||||||
|
if isinstance(data, Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if not any([data.get('note'), data.get('reason')]):
|
||||||
|
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
||||||
|
|
||||||
|
ban = conn.update_software_ban(name, **data)
|
||||||
|
|
||||||
|
return Response.new(ban, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
|
async def delete(self, request: Request, name: str) -> Response:
|
||||||
|
with self.database.connection(True) as conn:
|
||||||
|
if not conn.get_software_ban(name):
|
||||||
|
return Response.new_error(404, 'Software not banned', 'json')
|
||||||
|
|
||||||
|
conn.del_software_ban(name)
|
||||||
|
|
||||||
return Response.new({'message': 'Unbanned software'}, ctype = 'json')
|
return Response.new({'message': 'Unbanned software'}, ctype = 'json')
|
||||||
|
|
||||||
|
@ -381,16 +401,21 @@ class Whitelist(View):
|
||||||
return Response.new(item, ctype = 'json')
|
return Response.new(item, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def delete(self, request: Request) -> Response:
|
@register_route('/api/v1/domain/{domain}')
|
||||||
data = await self.get_api_data(['domain'], [])
|
class WhitelistSingle(View):
|
||||||
|
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 conn.get_domain_whitelist(data['domain']):
|
if not (item := conn.get_domain_whitelist(domain)):
|
||||||
return Response.new_error(404, 'Domain not in whitelist', 'json')
|
return Response.new_error(404, 'Domain not in whitelist', 'json')
|
||||||
|
|
||||||
conn.del_domain_whitelist(data['domain'])
|
return Response.new(item, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
|
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,10 +4,15 @@ 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,6 +13,7 @@ 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,5 +1,4 @@
|
||||||
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
|
||||||
|
@ -7,6 +6,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/main.tar.gz
|
tinysql[postgres]@https://git.barkshark.xyz/barkshark/tinysql/archive/f8db814084dded0a46bd3a9576e09fca860f2166.tar.gz
|
||||||
|
|
||||||
importlib_resources==6.1.1;python_version<'3.9'
|
importlib_resources==6.1.1;python_version<'3.9'
|
||||||
|
|
|
@ -34,7 +34,6 @@ 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 =
|
||||||
|
@ -43,5 +42,3 @@ console_scripts =
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
select = F401
|
select = F401
|
||||||
per-file-ignores =
|
|
||||||
relay/views/__init__.py: F401
|
|
||||||
|
|
Loading…
Reference in a new issue