mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-27 08:51:07 +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/swagger.yaml
|
||||
|
|
|
@ -9,7 +9,7 @@ use `python3 -m relay` if installed via pip or `~/.local/bin/activityrelay` if i
|
|||
|
||||
## Run
|
||||
|
||||
Run the relay.
|
||||
Run the relay. Optionally add `-d` or `--dev` to enable auto-reloading on code changes.
|
||||
|
||||
activityrelay run
|
||||
|
||||
|
@ -58,6 +58,50 @@ Set a value for a config option
|
|||
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
|
||||
|
||||
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`.
|
||||
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
|
||||
|
@ -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
|
||||
the inbox list.
|
||||
|
||||
activityrelay instance ban <domain>
|
||||
activityrelay instance ban <domain> --reason <text> --note <text>
|
||||
|
||||
|
||||
### Unban
|
||||
|
@ -167,9 +211,10 @@ Remove an instance from the ban list.
|
|||
|
||||
### 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
|
||||
|
@ -194,7 +239,7 @@ name via nodeinfo.
|
|||
|
||||
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
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
|
||||
### Push Workers
|
||||
### Web Workers
|
||||
|
||||
The relay can be configured to use threads to push messages out. For smaller relays, this isn't
|
||||
necessary, but bigger ones (>100 instances) will want to set this to the number of available cpu
|
||||
threads.
|
||||
The number of processes to spawn for handling web requests. Leave it at 0 to automatically detect
|
||||
how many processes should be spawned.
|
||||
|
||||
workers: 0
|
||||
|
||||
|
|
11
relay.spec
11
relay.spec
|
@ -1,7 +1,10 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
block_cipher = None
|
||||
aiohttp_swagger_path = Path(importlib.import_module('aiohttp_swagger').__file__).parent
|
||||
|
||||
|
||||
a = Analysis(
|
||||
|
@ -9,9 +12,13 @@ a = Analysis(
|
|||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('relay/data', 'relay/data')
|
||||
('relay/data', 'relay/data'),
|
||||
(aiohttp_swagger_path, 'aiohttp_swagger')
|
||||
],
|
||||
hiddenimports=[
|
||||
'gunicorn',
|
||||
'gunicorn.glogging'
|
||||
],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '0.2.5'
|
||||
__version__ = '0.2.6'
|
||||
|
|
|
@ -9,9 +9,10 @@ import time
|
|||
import typing
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_swagger import setup_swagger
|
||||
from aputils.signer import Signer
|
||||
from datetime import datetime, timedelta
|
||||
from gunicorn.app.wsgiapp import WSGIApplication
|
||||
from threading import Event, Thread
|
||||
|
||||
from . import logger as logging
|
||||
from .cache import get_cache
|
||||
|
@ -22,10 +23,14 @@ from .misc import check_open_port
|
|||
from .views import VIEWS
|
||||
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:
|
||||
from collections.abc import Awaitable
|
||||
from tinysql import Database, Row
|
||||
from typing import Any
|
||||
from .cache import Cache
|
||||
from .misc import Message
|
||||
|
||||
|
@ -47,6 +52,7 @@ class Application(web.Application):
|
|||
self['proc'] = None
|
||||
self['signer'] = None
|
||||
self['start_time'] = None
|
||||
self['cleanup_thread'] = None
|
||||
|
||||
self['config'] = Config(cfgpath, load = True)
|
||||
self['database'] = get_database(self.config)
|
||||
|
@ -57,10 +63,16 @@ class Application(web.Application):
|
|||
return
|
||||
|
||||
self.on_response_prepare.append(handle_access_log)
|
||||
self.on_cleanup.append(handle_cleanup)
|
||||
|
||||
for path, view in VIEWS:
|
||||
self.router.add_view(path, view)
|
||||
|
||||
setup_swagger(self,
|
||||
ui_version = 3,
|
||||
swagger_from_file = pkgfiles('relay').joinpath('data', 'swagger.yaml')
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def cache(self) -> Cache:
|
||||
|
@ -144,7 +156,9 @@ class Application(web.Application):
|
|||
'--bind', f'{self.config.listen}:{self.config.port}',
|
||||
'--worker-class', 'aiohttp.GunicornWebWorker',
|
||||
'--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:
|
||||
|
@ -152,12 +166,15 @@ class Application(web.Application):
|
|||
|
||||
self.set_signal_handler(True)
|
||||
self['proc'] = subprocess.Popen(cmd) # pylint: disable=consider-using-with
|
||||
self['cleanup_thread'] = CacheCleanupThread(self)
|
||||
self['cleanup_thread'].start()
|
||||
|
||||
|
||||
def stop(self, *_) -> None:
|
||||
if not self['proc']:
|
||||
return
|
||||
|
||||
self['cleanup_thread'].stop()
|
||||
self['proc'].terminate()
|
||||
time_wait = 0.0
|
||||
|
||||
|
@ -172,30 +189,36 @@ class Application(web.Application):
|
|||
self.set_signal_handler(False)
|
||||
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):
|
||||
Thread.__init__(self)
|
||||
|
||||
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)
|
||||
self.running = Event()
|
||||
|
||||
|
||||
def load_config(self):
|
||||
for key, value in self.options.items():
|
||||
self.cfg.set(key, value)
|
||||
def run(self) -> None:
|
||||
cache = get_cache(self.app)
|
||||
|
||||
while self.running.is_set():
|
||||
time.sleep(3600)
|
||||
logging.verbose("Removing old cache items")
|
||||
cache.delete_old(14)
|
||||
|
||||
cache.close()
|
||||
|
||||
|
||||
def run(self):
|
||||
logging.info('Starting webserver for %s', self.app.config.domain)
|
||||
WSGIApplication.run(self)
|
||||
def start(self) -> None:
|
||||
self.running.set()
|
||||
Thread.start(self)
|
||||
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running.clear()
|
||||
|
||||
|
||||
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.path,
|
||||
response.status,
|
||||
len(response.body),
|
||||
response.content_length or 0,
|
||||
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():
|
||||
try:
|
||||
app = Application(os.environ['CONFIG_FILE'], gunicorn = True)
|
||||
|
|
|
@ -6,7 +6,7 @@ import typing
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from redis import Redis
|
||||
|
||||
from .database import get_database
|
||||
|
@ -15,7 +15,6 @@ from .misc import Message, boolean
|
|||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Iterator
|
||||
from tinysql import Database
|
||||
from .application import Application
|
||||
|
||||
|
||||
|
@ -94,6 +93,7 @@ class Cache(ABC):
|
|||
self.app = app
|
||||
self.setup()
|
||||
|
||||
|
||||
@abstractmethod
|
||||
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
|
||||
def setup(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
def set_item(self, item: Item) -> Item:
|
||||
return self.set(
|
||||
item.namespace,
|
||||
|
@ -201,12 +216,32 @@ class SqlCache(Cache):
|
|||
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:
|
||||
with self._db.connection() as conn:
|
||||
with conn.exec_statement(f'create-cache-table-{self._db.type.name.lower()}', None):
|
||||
pass
|
||||
|
||||
|
||||
def close(self) -> None:
|
||||
self._db.close()
|
||||
self._db = None
|
||||
|
||||
|
||||
@register_cache
|
||||
class RedisCache(Cache):
|
||||
name: str = 'redis'
|
||||
|
@ -239,7 +274,7 @@ class RedisCache(Cache):
|
|||
|
||||
|
||||
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)
|
||||
yield key_name
|
||||
|
||||
|
@ -247,7 +282,7 @@ class RedisCache(Cache):
|
|||
def get_namespaces(self) -> Iterator[str]:
|
||||
namespaces = []
|
||||
|
||||
for key in self._rd.keys(f'{self.prefix}:*'):
|
||||
for key in self._rd.scan_iter(f'{self.prefix}:*'):
|
||||
_, namespace, _ = key.split(':', 2)
|
||||
|
||||
if namespace not in namespaces:
|
||||
|
@ -269,6 +304,21 @@ class RedisCache(Cache):
|
|||
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:
|
||||
options = {
|
||||
'client_name': f'ActivityRelay_{self.app.config.domain}',
|
||||
|
@ -286,3 +336,8 @@ class RedisCache(Cache):
|
|||
options['port'] = self.app.config.rd_port
|
||||
|
||||
self._rd = Redis(**options)
|
||||
|
||||
|
||||
def close(self) -> None:
|
||||
self._rd.close()
|
||||
self._rd = None
|
||||
|
|
|
@ -188,6 +188,14 @@ class Config:
|
|||
raise KeyError(key)
|
||||
|
||||
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)
|
||||
|
|
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 click
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from aputils.signer import Signer
|
||||
from gunicorn.app.wsgiapp import WSGIApplication
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
from urllib.parse import urlparse
|
||||
|
@ -234,9 +237,21 @@ def cli_run(ctx: click.Context, dev: bool = False) -> None:
|
|||
click.echo(pip_command)
|
||||
return
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
subprocess.run([sys.executable, 'run-gunicorn'], check = False)
|
||||
return
|
||||
|
||||
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')
|
||||
@click.option('--old-config', '-o', help = 'Path to the config file to convert from')
|
||||
@click.pass_context
|
||||
|
@ -903,6 +918,31 @@ def cli_whitelist_import(ctx: click.Context) -> None:
|
|||
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:
|
||||
# pylint: disable=no-value-for-parameter
|
||||
cli(prog_name='relay')
|
||||
|
|
|
@ -10,14 +10,8 @@ from aputils.message import Message as ApMessage
|
|||
from uuid import uuid4
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Awaitable, Coroutine, Generator
|
||||
from tinysql import Connection
|
||||
from typing import Any
|
||||
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'))
|
||||
|
@ -215,7 +209,7 @@ class Response(AiohttpResponse):
|
|||
ctype: str = 'text') -> Response:
|
||||
|
||||
if ctype == 'json':
|
||||
body = json.dumps({'status': status, 'error': body})
|
||||
body = json.dumps({'error': body})
|
||||
|
||||
return cls.new(body=body, status=status, ctype=ctype)
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ if typing.TYPE_CHECKING:
|
|||
from aiohttp.web import Request
|
||||
from aputils.signer import Signer
|
||||
from tinysql import Row
|
||||
from ..database.connection import Connection
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
|
|
@ -17,7 +17,6 @@ from ..misc import Message, Response
|
|||
if typing.TYPE_CHECKING:
|
||||
from aiohttp.web import Request
|
||||
from collections.abc import Coroutine
|
||||
from ..database.connection import Connection
|
||||
|
||||
|
||||
CONFIG_IGNORE = (
|
||||
|
@ -35,7 +34,7 @@ PUBLIC_API_PATHS: tuple[tuple[str, str]] = (
|
|||
|
||||
|
||||
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 path.startswith('/api')
|
||||
|
@ -68,7 +67,7 @@ async def handle_api_path(request: web.Request, handler: Coroutine) -> web.Respo
|
|||
@register_route('/api/v1/token')
|
||||
class Login(View):
|
||||
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:
|
||||
|
@ -214,28 +213,14 @@ class Inbox(View):
|
|||
return Response.new(row, ctype = 'json')
|
||||
|
||||
|
||||
@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:
|
||||
async def patch(self, request: Request) -> Response:
|
||||
with self.database.connection(True) as conn:
|
||||
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'])
|
||||
data = await self.get_api_data(['domain'], ['actor', 'software', 'followid'])
|
||||
|
||||
if isinstance(data, Response):
|
||||
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')
|
||||
|
||||
instance = conn.update_inbox(instance['inbox'], **data)
|
||||
|
@ -245,10 +230,15 @@ class InboxSingle(View):
|
|||
|
||||
async def delete(self, request: Request, domain: str) -> Response:
|
||||
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')
|
||||
|
||||
conn.del_inbox(domain)
|
||||
conn.del_inbox(data['domain'])
|
||||
|
||||
return Response.new({'message': 'Deleted instance'}, ctype = 'json')
|
||||
|
||||
|
@ -277,40 +267,35 @@ class DomainBan(View):
|
|||
return Response.new(ban, ctype = 'json')
|
||||
|
||||
|
||||
@register_route('/api/v1/domain_ban/{domain}')
|
||||
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:
|
||||
async def patch(self, request: Request) -> Response:
|
||||
with self.database.connection(True) as conn:
|
||||
if not conn.get_domain_ban(domain):
|
||||
return Response.new_error(404, 'Domain not banned', 'json')
|
||||
|
||||
data = await self.get_api_data([], ['note', 'reason'])
|
||||
data = await self.get_api_data(['domain'], ['note', 'reason'])
|
||||
|
||||
if isinstance(data, Response):
|
||||
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')]):
|
||||
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')
|
||||
|
||||
|
||||
async def delete(self, request: Request, domain: str) -> Response:
|
||||
async def delete(self, request: Request) -> Response:
|
||||
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')
|
||||
|
||||
conn.del_domain_ban(domain)
|
||||
conn.del_domain_ban(data['domain'])
|
||||
|
||||
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
||||
|
||||
|
@ -339,40 +324,35 @@ class SoftwareBan(View):
|
|||
return Response.new(ban, ctype = 'json')
|
||||
|
||||
|
||||
@register_route('/api/v1/software_ban/{name}')
|
||||
class SoftwareBanSingle(View):
|
||||
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')
|
||||
|
||||
|
||||
async def patch(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')
|
||||
|
||||
data = await self.get_api_data([], ['note', 'reason'])
|
||||
async def patch(self, request: Request) -> Response:
|
||||
data = await self.get_api_data(['name'], ['note', 'reason'])
|
||||
|
||||
if isinstance(data, Response):
|
||||
return data
|
||||
|
||||
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(name, **data)
|
||||
ban = conn.update_software_ban(data['name'], **data)
|
||||
|
||||
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:
|
||||
if not conn.get_software_ban(name):
|
||||
if not conn.get_software_ban(data['name']):
|
||||
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')
|
||||
|
||||
|
@ -401,21 +381,16 @@ class Whitelist(View):
|
|||
return Response.new(item, ctype = 'json')
|
||||
|
||||
|
||||
@register_route('/api/v1/domain/{domain}')
|
||||
class WhitelistSingle(View):
|
||||
async def get(self, request: Request, domain: str) -> Response:
|
||||
async def delete(self, request: Request) -> Response:
|
||||
data = await self.get_api_data(['domain'], [])
|
||||
|
||||
if isinstance(data, Response):
|
||||
return data
|
||||
|
||||
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(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)
|
||||
conn.del_domain_whitelist(data['domain'])
|
||||
|
||||
return Response.new({'message': 'Removed domain from whitelist'}, ctype = 'json')
|
||||
|
|
|
@ -4,15 +4,10 @@ import typing
|
|||
|
||||
from .base import View, register_route
|
||||
|
||||
from .. import __version__
|
||||
from ..misc import Response
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
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 = """
|
||||
|
|
|
@ -13,7 +13,6 @@ from ..misc import Response
|
|||
|
||||
if typing.TYPE_CHECKING:
|
||||
from aiohttp.web import Request
|
||||
from ..database.connection import Connection
|
||||
|
||||
|
||||
VERSION = __version__
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
aiohttp>=3.9.1
|
||||
aiohttp-swagger[performance]==1.0.16
|
||||
aputils@https://git.barkshark.xyz/barkshark/aputils/archive/0.1.6a.tar.gz
|
||||
argon2-cffi==23.1.0
|
||||
click>=8.1.2
|
||||
|
@ -6,6 +7,6 @@ gunicorn==21.1.0
|
|||
hiredis==2.3.2
|
||||
pyyaml>=6.0
|
||||
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'
|
||||
|
|
Loading…
Reference in a new issue