From 001aa87667d8850a414f35cb5d5ae6394727f97a Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 22 Feb 2024 11:54:09 -0500 Subject: [PATCH 01/31] replace gunicorn with push worker processes --- docs/commands.md | 2 +- docs/configuration.md | 6 +- relay/application.py | 140 +++++++++++++++++++++++++----------------- relay/cache.py | 16 ++++- relay/manage.py | 41 +------------ requirements.txt | 1 - 6 files changed, 101 insertions(+), 105 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index f437c8f..e28acbe 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -9,7 +9,7 @@ use `python3 -m relay` if installed via pip or `~/.local/bin/activityrelay` if i ## Run -Run the relay. Optionally add `-d` or `--dev` to enable auto-reloading on code changes. +Run the relay. activityrelay run diff --git a/docs/configuration.md b/docs/configuration.md index ec8212f..2fad0af 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,10 +19,10 @@ proxy is on the same host. port: 8080 -### Web Workers +### Push Workers -The number of processes to spawn for handling web requests. Leave it at 0 to automatically detect -how many processes should be spawned. +The number of processes to spawn for pushing messages to subscribed instances. Leave it at 0 to +automatically detect how many processes should be spawned. workers: 0 diff --git a/relay/application.py b/relay/application.py index cafef59..02fe73c 100644 --- a/relay/application.py +++ b/relay/application.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import multiprocessing import os import signal import subprocess @@ -12,6 +13,7 @@ from aiohttp import web from aiohttp_swagger import setup_swagger from aputils.signer import Signer from datetime import datetime, timedelta +from queue import Empty from threading import Event, Thread from . import logger as logging @@ -40,7 +42,7 @@ if typing.TYPE_CHECKING: class Application(web.Application): DEFAULT: Application = None - def __init__(self, cfgpath: str, gunicorn: bool = False): + def __init__(self, cfgpath: str): web.Application.__init__(self, middlewares = [ handle_api_path @@ -49,7 +51,7 @@ class Application(web.Application): Application.DEFAULT = self - self['proc'] = None + self['running'] = None self['signer'] = None self['start_time'] = None self['cleanup_thread'] = None @@ -58,9 +60,8 @@ class Application(web.Application): self['database'] = get_database(self.config) self['client'] = HttpClient() self['cache'] = get_cache(self) - - if not gunicorn: - return + self['push_queue'] = multiprocessing.Queue() + self['workers'] = [] self.on_response_prepare.append(handle_access_log) self.on_cleanup.append(handle_cleanup) @@ -119,16 +120,18 @@ class Application(web.Application): def push_message(self, inbox: str, message: Message, instance: Row) -> None: - asyncio.ensure_future(self.client.post(inbox, message, instance)) + self['push_queue'].put((inbox, message, instance)) - def run(self, dev: bool = False) -> None: - self.start(dev) + def run(self) -> None: + if self["running"]: + return - while self['proc'] and self['proc'].poll() is None: - time.sleep(0.1) + if not check_open_port(self.config.listen, self.config.port): + return logging.error(f'A server is already running on port {self.config.port}') - self.stop() + logging.info(f'Starting webserver at {self.config.domain} ({self.config.listen}:{self.config.port})') + asyncio.run(self.handle_run()) def set_signal_handler(self, startup: bool) -> None: @@ -141,56 +144,54 @@ class Application(web.Application): pass + def stop(self, *_): + self['running'] = False - def start(self, dev: bool = False) -> None: - if self['proc']: - return - if not check_open_port(self.config.listen, self.config.port): - logging.error('Server already running on %s:%s', self.config.listen, self.config.port) - return - - cmd = [ - sys.executable, '-m', 'gunicorn', - 'relay.application:main_gunicorn', - '--bind', f'{self.config.listen}:{self.config.port}', - '--worker-class', 'aiohttp.GunicornWebWorker', - '--workers', str(self.config.workers), - '--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: - cmd.append('--reload') + async def handle_run(self): + self['running'] = True self.set_signal_handler(True) - self['proc'] = subprocess.Popen(cmd) # pylint: disable=consider-using-with + + self['database'].connect() + self['cache'].setup() self['cleanup_thread'] = CacheCleanupThread(self) self['cleanup_thread'].start() + for i in range(self.config.workers): + worker = PushWorker(self['push_queue']) + worker.start() - def stop(self, *_) -> None: - if not self['proc']: - return + self['workers'].append(worker) - self['cleanup_thread'].stop() - self['proc'].terminate() - time_wait = 0.0 + runner = web.AppRunner(self, access_log_format='%{X-Forwarded-For}i "%r" %s %b "%{User-Agent}i"') + await runner.setup() - while self['proc'].poll() is None: - time.sleep(0.1) - time_wait += 0.1 + site = web.TCPSite(runner, + host = self.config.listen, + port = self.config.port, + reuse_address = True + ) - if time_wait >= 5.0: - self['proc'].kill() - break + await site.start() + self['starttime'] = datetime.now() + + while self['running']: + await asyncio.sleep(0.25) + + await site.stop() + + for worker in self['workers']: + worker.stop() self.set_signal_handler(False) - self['proc'] = None - self.cache.close() - self.database.disconnect() + self['starttime'] = None + self['running'] = False + self['cleanup_thread'].stop() + self['workers'].clear() + self['database'].disconnect() + self['cache'].close() class CacheCleanupThread(Thread): @@ -217,6 +218,40 @@ class CacheCleanupThread(Thread): self.running.clear() +class PushWorker(multiprocessing.Process): + def __init__(self, queue: multiprocessing.Queue): + multiprocessing.Process.__init__(self) + self.queue = queue + self.shutdown = multiprocessing.Event() + + + def stop(self) -> None: + self.shutdown.set() + + + def run(self) -> None: + asyncio.run(self.handle_queue()) + + + async def handle_queue(self) -> None: + client = HttpClient() + + while not self.shutdown.is_set(): + try: + inbox, message, instance = self.queue.get(block=True, timeout=0.25) + await client.post(inbox, message, instance) + + except Empty: + pass + + ## make sure an exception doesn't bring down the worker + except Exception: + traceback.print_exc() + + await client.close() + + + async def handle_access_log(request: web.Request, response: web.Response) -> None: address = request.headers.get( 'X-Forwarded-For', @@ -241,14 +276,3 @@ async def handle_cleanup(app: Application) -> None: await app.client.close() app.cache.close() app.database.disconnect() - - -async def main_gunicorn(): - try: - app = Application(os.environ['CONFIG_FILE'], gunicorn = True) - - except KeyError: - logging.error('Failed to set "CONFIG_FILE" environment. Trying to run without gunicorn?') - raise RuntimeError from None - - return app diff --git a/relay/cache.py b/relay/cache.py index 3f258eb..448267f 100644 --- a/relay/cache.py +++ b/relay/cache.py @@ -91,7 +91,6 @@ class Cache(ABC): def __init__(self, app: Application): self.app = app - self.setup() @abstractmethod @@ -158,8 +157,8 @@ class SqlCache(Cache): def __init__(self, app: Application): - self._db = get_database(app.config) Cache.__init__(self, app) + self._db = None def get(self, namespace: str, key: str) -> Item: @@ -232,6 +231,10 @@ class SqlCache(Cache): def setup(self) -> None: + if self._db.connected: + return + + self._db = get_database(self.app.config) self._db.connect() with self._db.session(True) as conn: @@ -247,7 +250,11 @@ class SqlCache(Cache): @register_cache class RedisCache(Cache): name: str = 'redis' - _rd: Redis + + + def __init__(self, app: Application): + Cache.__init__(self, app) + self._rd = None @property @@ -322,6 +329,9 @@ class RedisCache(Cache): def setup(self) -> None: + if self._rd: + return + options = { 'client_name': f'ActivityRelay_{self.app.config.domain}', 'decode_responses': True, diff --git a/relay/manage.py b/relay/manage.py index 3acd1c2..8066d79 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -10,7 +10,6 @@ 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 @@ -208,9 +207,8 @@ def cli_setup(ctx: click.Context) -> None: @cli.command('run') -@click.option('--dev', '-d', is_flag = True, help = 'Enable worker reloading on code change') @click.pass_context -def cli_run(ctx: click.Context, dev: bool = False) -> None: +def cli_run(ctx: click.Context) -> None: 'Run the relay' if ctx.obj.config.domain.endswith('example.com') or not ctx.obj.signer: @@ -237,23 +235,12 @@ 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) - - else: - ctx.obj.run(dev) + ctx.obj.run() # todo: figure out why the relay doesn't quit properly without this os._exit(0) -@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') @@ -921,30 +908,6 @@ 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 diff --git a/requirements.txt b/requirements.txt index 65b70b0..aea9c24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ 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 -gunicorn==21.1.0 hiredis==2.3.2 pyyaml>=6.0 redis==5.0.1 From 61014f791d182e276d7d587300fbc5e5d5126e55 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 22 Feb 2024 13:28:41 -0500 Subject: [PATCH 02/31] re-add windows support --- relay.spec | 4 ++-- relay/application.py | 1 + relay/config.py | 11 ++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/relay.spec b/relay.spec index 8f84331..45fb419 100644 --- a/relay.spec +++ b/relay.spec @@ -16,8 +16,8 @@ a = Analysis( (aiohttp_swagger_path, 'aiohttp_swagger') ], hiddenimports=[ - 'gunicorn', - 'gunicorn.glogging' + 'pg8000', + 'sqlite3' ], hookspath=[], hooksconfig={}, diff --git a/relay/application.py b/relay/application.py index 02fe73c..f7007b0 100644 --- a/relay/application.py +++ b/relay/application.py @@ -7,6 +7,7 @@ import signal import subprocess import sys import time +import traceback import typing from aiohttp import web diff --git a/relay/config.py b/relay/config.py index 94bc293..b6963bd 100644 --- a/relay/config.py +++ b/relay/config.py @@ -2,6 +2,7 @@ from __future__ import annotations import getpass import os +import platform import typing import yaml @@ -13,11 +14,19 @@ if typing.TYPE_CHECKING: from typing import Any +if platform.system() == 'Windows': + import multiprocessing + CORE_COUNT = multiprocessing.cpu_count() + +else: + CORE_COUNT = len(os.sched_getaffinity(0)) + + DEFAULTS: dict[str, Any] = { 'listen': '0.0.0.0', 'port': 8080, 'domain': 'relay.example.com', - 'workers': len(os.sched_getaffinity(0)), + 'workers': CORE_COUNT, 'db_type': 'sqlite', 'ca_type': 'database', 'sq_path': 'relay.sqlite3', From 097a53a53946003ee038651a9d40370230d18746 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 22 Feb 2024 13:44:58 -0500 Subject: [PATCH 03/31] ensure cache is setup --- relay/application.py | 3 +++ relay/cache.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/relay/application.py b/relay/application.py index f7007b0..cf9611b 100644 --- a/relay/application.py +++ b/relay/application.py @@ -61,9 +61,12 @@ class Application(web.Application): self['database'] = get_database(self.config) self['client'] = HttpClient() self['cache'] = get_cache(self) + self['cache'].setup() self['push_queue'] = multiprocessing.Queue() self['workers'] = [] + self.cache.setup() + self.on_response_prepare.append(handle_access_log) self.on_cleanup.append(handle_cleanup) diff --git a/relay/cache.py b/relay/cache.py index 448267f..5647106 100644 --- a/relay/cache.py +++ b/relay/cache.py @@ -231,7 +231,7 @@ class SqlCache(Cache): def setup(self) -> None: - if self._db.connected: + if self._db and self._db.connected: return self._db = get_database(self.app.config) @@ -243,6 +243,9 @@ class SqlCache(Cache): def close(self) -> None: + if not self._db: + return + self._db.disconnect() self._db = None @@ -351,5 +354,8 @@ class RedisCache(Cache): def close(self) -> None: + if not self._rd: + return + self._rd.close() self._rd = None From 26c5c05320cca3ba46cde55419394c2c6516c94b Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 22 Feb 2024 17:54:15 -0500 Subject: [PATCH 04/31] create get_resource method --- relay/application.py | 10 ++-------- relay/database/__init__.py | 19 +++++++------------ relay/misc.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/relay/application.py b/relay/application.py index cf9611b..12c60b9 100644 --- a/relay/application.py +++ b/relay/application.py @@ -22,16 +22,10 @@ from .cache import get_cache from .config import Config from .database import get_database from .http_client import HttpClient -from .misc import check_open_port +from .misc import check_open_port, get_resource 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 tinysql import Database, Row from .cache import Cache @@ -75,7 +69,7 @@ class Application(web.Application): setup_swagger(self, ui_version = 3, - swagger_from_file = pkgfiles('relay').joinpath('data', 'swagger.yaml') + swagger_from_file = get_resource('data/swagger.yaml') ) diff --git a/relay/database/__init__.py b/relay/database/__init__.py index c7e9a1f..a55f2c8 100644 --- a/relay/database/__init__.py +++ b/relay/database/__init__.py @@ -8,12 +8,7 @@ from .connection import RELAY_SOFTWARE, Connection from .schema import TABLES, VERSIONS, migrate_0 from .. import logger as logging - -try: - from importlib.resources import files as pkgfiles - -except ImportError: # pylint: disable=duplicate-code - from importlib_resources import files as pkgfiles +from ..misc import get_resource if typing.TYPE_CHECKING: from .config import Config @@ -21,15 +16,15 @@ if typing.TYPE_CHECKING: def get_database(config: Config, migrate: bool = True) -> bsql.Database: options = { - "connection_class": Connection, - "pool_size": 5, - "tables": TABLES + 'connection_class': Connection, + 'pool_size': 5, + 'tables': TABLES } - if config.db_type == "sqlite": + if config.db_type == 'sqlite': db = bsql.Database.sqlite(config.sqlite_path, **options) - elif config.db_type == "postgres": + elif config.db_type == 'postgres': db = bsql.Database.postgresql( config.pg_name, config.pg_host, @@ -39,7 +34,7 @@ def get_database(config: Config, migrate: bool = True) -> bsql.Database: **options ) - db.load_prepared_statements(pkgfiles("relay").joinpath("data", "statements.sql")) + db.load_prepared_statements(get_resource('data/statements.sql')) db.connect() if not migrate: diff --git a/relay/misc.py b/relay/misc.py index 25c0a1e..62d4643 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -10,7 +10,14 @@ from aputils.message import Message as ApMessage from datetime import datetime from uuid import uuid4 +try: + from importlib.resources import files as pkgfiles + +except ImportError: + from importlib_resources import files as pkgfiles + if typing.TYPE_CHECKING: + from pathlib import Path from typing import Any from .application import Application @@ -75,6 +82,10 @@ def get_app() -> Application: return Application.DEFAULT +def get_resource(path: str) -> Path: + return pkgfiles('relay').joinpath(path) + + class JsonEncoder(json.JSONEncoder): def default(self, obj: Any) -> str: if isinstance(obj, datetime): From a08d1c96125e45cff27d9cbae3632fe73e5601b2 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 19:19:44 -0500 Subject: [PATCH 05/31] use jinja for web pages --- MANIFEST.in | 3 + relay.spec | 1 + relay/application.py | 4 +- relay/config.py | 46 ++++++----- relay/database/config.py | 42 ++++++++++ relay/database/connection.py | 16 +++- relay/frontend/base.haml | 16 ++++ relay/frontend/page/home.haml | 34 ++++++++ relay/frontend/style.css | 151 ++++++++++++++++++++++++++++++++++ relay/misc.py | 1 + relay/template.py | 51 ++++++++++++ relay/views/base.py | 6 ++ relay/views/frontend.py | 54 ++++-------- requirements.txt | 3 +- 14 files changed, 365 insertions(+), 63 deletions(-) create mode 100644 relay/frontend/base.haml create mode 100644 relay/frontend/page/home.haml create mode 100644 relay/frontend/style.css create mode 100644 relay/template.py diff --git a/MANIFEST.in b/MANIFEST.in index 313c6af..aaac993 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,5 @@ include data/statements.sql include data/swagger.yaml +include frontend/page/home.haml +include frontend/base.haml +include frontend/style.css diff --git a/relay.spec b/relay.spec index 45fb419..5965b51 100644 --- a/relay.spec +++ b/relay.spec @@ -13,6 +13,7 @@ a = Analysis( binaries=[], datas=[ ('relay/data', 'relay/data'), + ('relay/frontend', 'relay/frontend'), (aiohttp_swagger_path, 'aiohttp_swagger') ], hiddenimports=[ diff --git a/relay/application.py b/relay/application.py index 12c60b9..fdf2503 100644 --- a/relay/application.py +++ b/relay/application.py @@ -23,6 +23,7 @@ from .config import Config from .database import get_database from .http_client import HttpClient from .misc import check_open_port, get_resource +from .template import Template from .views import VIEWS from .views.api import handle_api_path @@ -56,12 +57,13 @@ class Application(web.Application): self['client'] = HttpClient() self['cache'] = get_cache(self) self['cache'].setup() + self['template'] = Template(self) self['push_queue'] = multiprocessing.Queue() self['workers'] = [] self.cache.setup() - 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: diff --git a/relay/config.py b/relay/config.py index b6963bd..3512726 100644 --- a/relay/config.py +++ b/relay/config.py @@ -163,7 +163,30 @@ class Config: def save(self) -> None: self.path.parent.mkdir(exist_ok = True, parents = True) - config = { + with self.path.open('w', encoding = 'utf-8') as fd: + yaml.dump(self.to_dict(), fd, sort_keys = False) + + + def set(self, key: str, value: Any) -> None: + if key not in DEFAULTS: + raise KeyError(key) + + if key in {'port', 'pg_port', 'workers'} and not isinstance(value, int): + 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) + + + def to_dict(self) -> dict[str, Any]: + return { 'listen': self.listen, 'port': self.port, 'domain': self.domain, @@ -187,24 +210,3 @@ class Config: 'refix': self.rd_prefix } } - - with self.path.open('w', encoding = 'utf-8') as fd: - yaml.dump(config, fd, sort_keys = False) - - - def set(self, key: str, value: Any) -> None: - if key not in DEFAULTS: - raise KeyError(key) - - if key in {'port', 'pg_port', 'workers'} and not isinstance(value, int): - 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) diff --git a/relay/database/config.py b/relay/database/config.py index b69f13e..c961a0c 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import typing from .. import logger as logging @@ -10,12 +11,52 @@ if typing.TYPE_CHECKING: from typing import Any +THEMES = { + 'default': { + 'text': '#DDD', + 'background': '#222', + 'primary': '#D85', + 'primary-hover': '#DA8', + 'section-background': '#333', + 'table-background': '#444', + 'border': '#444', + 'message-text': '#DDD', + 'message-background': '#335', + 'message-border': '#446' + }, + 'pink': { + 'text': '#DDD', + 'background': '#222', + 'primary': '#D69', + 'primary-hover': '#D36', + 'section-background': '#333', + 'table-background': '#444', + 'border': '#444', + 'message-text': '#DDD', + 'message-background': '#335', + 'message-border': '#446' + }, + 'blue': { + 'text': '#DDD', + 'background': '#222', + 'primary': '#69D', + 'primary-hover': '#36D', + 'section-background': '#333', + 'table-background': '#444', + 'border': '#444', + 'message-text': '#DDD', + 'message-background': '#335', + 'message-border': '#446' + } +} + CONFIG_DEFAULTS: dict[str, tuple[str, Any]] = { 'schema-version': ('int', 20240206), 'log-level': ('loglevel', logging.LogLevel.INFO), 'name': ('str', 'ActivityRelay'), 'note': ('str', 'Make a note about your instance here.'), 'private-key': ('str', None), + 'theme': ('str', 'default'), 'whitelist-enabled': ('bool', False) } @@ -24,6 +65,7 @@ CONFIG_CONVERT: dict[str, tuple[Callable, Callable]] = { 'str': (str, str), 'int': (str, int), 'bool': (str, boolean), + 'json': (json.dumps, json.loads), 'loglevel': (lambda x: x.name, logging.LogLevel.parse) } diff --git a/relay/database/connection.py b/relay/database/connection.py index 200e17e..16168b7 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -8,7 +8,14 @@ from datetime import datetime, timezone from urllib.parse import urlparse from uuid import uuid4 -from .config import CONFIG_DEFAULTS, get_default_type, get_default_value, serialize, deserialize +from .config import ( + CONFIG_DEFAULTS, + THEMES, + get_default_type, + get_default_value, + serialize, + deserialize +) from .. import logger as logging from ..misc import get_app @@ -95,6 +102,13 @@ class Connection(SqlConnection): value = logging.LogLevel.parse(value) logging.set_level(value) + elif key == 'whitelist-enabled': + value = boolean(value) + + elif key == 'theme': + if value not in THEMES: + raise ValueError(f'"{value}" is not a valid theme') + params = { 'key': key, 'value': serialize(key, value) if value is not None else None, diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml new file mode 100644 index 0000000..a47d508 --- /dev/null +++ b/relay/frontend/base.haml @@ -0,0 +1,16 @@ +!!! +%html + %head + %title << {{config.name}}: {{page}} + %meta(charset="UTF-8") + %meta(name="viewport" content="width=device-width, initial-scale=1") + %link(rel="stylesheet" type="text/css" href="/style.css") + -block head + + %body + #container + #header.section + %a(href="https://{{domain}}/") -> =config.name + + #content + -block content diff --git a/relay/frontend/page/home.haml b/relay/frontend/page/home.haml new file mode 100644 index 0000000..6af5b2b --- /dev/null +++ b/relay/frontend/page/home.haml @@ -0,0 +1,34 @@ +-extends "base.haml" +-set page = "Home" +-block content + .section + =config.note + + .section + %p + This is an Activity Relay for fediverse instances. + + %p + You may subscribe to this relay with the address: + %a(href="https://{{domain}}/actor") << https://{{domain}}/actor + %p + To host your own relay, you may download the code at + %a(href="https://git.pleroma.social/pleroma/relay") << git.pleroma.social/pleroma/relay + + -if config["whitelist-enabled"] + %p.section.message + Note: The whitelist is enabled on this instance. Ask the admin to add your instance + before joining. + + #instances.section + %table + %thead + %tr + %td.instance << Instance + %td.date << Joined + + %tbody + -for instance in instances + %tr + %td.instance -> %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain + %td.date -> =instance.created.strftime("%Y-%m-%d") diff --git a/relay/frontend/style.css b/relay/frontend/style.css new file mode 100644 index 0000000..e48c200 --- /dev/null +++ b/relay/frontend/style.css @@ -0,0 +1,151 @@ +:root { + --text: {{theme["text"]}}; + --background: {{theme["background"]}}; + --primary: {{theme["primary"]}}; + --primary-hover: {{theme["primary-hover"]}}; + --section-background: {{theme["section-background"]}}; + --table-background: {{theme["table-background"]}}; + --border: {{theme["border"]}}; + --message-text: {{theme["message-text"]}}; + --message-background: {{theme["message-background"]}}; + --message-border: {{theme["message-border"]}}; + --spacing: 10px; +} + +body { + color: var(--text); + background-color: #222; + margin: var(--spacing); + font-family: sans serif; +} + +a { + color: var(--primary); + text-decoration: none; +} + +a:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +p { + line-height: 1em; + margin: 0px; +} + +p:not(:first-child) { + margin-top: 1em; +} + +p:not(:last-child) { + margin-bottom: 1em; +} + +table { + border-spacing: 0px; +/* border-radius: 10px; */ + border-collapse: collapse; +} + +td { + border: 1px solid var(--primary); +} + +/*thead td:first-child { + border-top-left-radius: 10px; +} + +thead td:last-child { + border-top-right-radius: 10px; +} + +tbody tr:last-child td:first-child { + border-bottom-left-radius: 10px; +} + +tbody tr:last-child td:last-child { + border-bottom-right-radius: 10px; +}*/ + +table td { + padding: 5px; +} + +table thead td { + background-color: var(--primary); + color: var(--table-background) +} + +table tbody td { + background-color: var(--table-background); +} + +#container { + width: 1024px; + margin: 0px auto; +} + +#header { + text-align: center; +} + +#header a { + font-size: 3em; +} + +#instances table { + width: 100%; +} + +#instances .instance { + width: 100%; +} + +#instances .date { + width: max-content; + text-align: right; +} + +#instances thead td { + text-align: center !important; +} + + +.message { + color: var(--message-text) !important; + background-color: var(--message-background) !important; + border: 1px solid var(--message-border) !important; +} + +.section { + background-color: var(--section-background); + padding: var(--spacing); + border: 1px solid var(--border); + border-radius: 10px; +} + +.section:not(:first-child) { + margin-top: var(--spacing); +} + +.section:not(:last-child) { + margin-bottom: var(--spacing); +} + + +@media (max-width: 1026px) { + body { + margin: 0px; + } + + #container { + width: unset; + margin: unset; + } + + .section { + border-width: 0px; + border-radius: 0px; + } +} diff --git a/relay/misc.py b/relay/misc.py index 62d4643..7060aa4 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -25,6 +25,7 @@ if typing.TYPE_CHECKING: IS_DOCKER = bool(os.environ.get('DOCKER_RUNNING')) MIMETYPES = { 'activity': 'application/activity+json', + 'css': 'text/css', 'html': 'text/html', 'json': 'application/json', 'text': 'text/plain' diff --git a/relay/template.py b/relay/template.py new file mode 100644 index 0000000..ecb556a --- /dev/null +++ b/relay/template.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import typing + +from hamlish_jinja.extension import HamlishExtension +from jinja2 import Environment, FileSystemLoader + +from pathlib import Path + +from .database.config import THEMES +from .misc import get_resource + +if typing.TYPE_CHECKING: + from typing import Any + from .application import Application + from .views.base import View + + +class Template(Environment): + def __init__(self, app: Application): + Environment.__init__(self, + autoescape = True, + trim_blocks = True, + lstrip_blocks = True, + extensions = [ + HamlishExtension + ], + loader = FileSystemLoader([ + get_resource('frontend'), + app.config.path.parent.joinpath('template') + ]) + ) + + self.app = app + self.hamlish_enable_div_shortcut = True + self.hamlish_mode = 'indented' + + + def render(self, path: str, view: View | None = None, **context: Any) -> str: + with self.app.database.session(False) as s: + config = s.get_config_all() + + new_context = { + 'view': view, + 'domain': self.app.config.domain, + 'config': config, + 'theme': THEMES.get(config['theme'], THEMES['default']), + **(context or {}) + } + + return self.get_template(path).render(new_context) diff --git a/relay/views/base.py b/relay/views/base.py index ce72e4b..8d6d1ff 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -17,6 +17,7 @@ if typing.TYPE_CHECKING: from ..cache import Cache from ..config import Config from ..http_client import HttpClient + from ..template import Template VIEWS = [] @@ -91,6 +92,11 @@ class View(AbstractView): return self.app.database + @property + def template(self) -> Template: + return self.app['template'] + + async def get_api_data(self, required: list[str], optional: list[str]) -> dict[str, str] | Response: diff --git a/relay/views/frontend.py b/relay/views/frontend.py index fb6028f..567a44b 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -10,49 +10,27 @@ if typing.TYPE_CHECKING: from aiohttp.web import Request -HOME_TEMPLATE = """ - - ActivityPub Relay at {host} - - - -

This is an Activity Relay for fediverse instances.

-

{note}

-

- You may subscribe to this relay with the address: - https://{host}/actor -

-

- To host your own relay, you may download the code at this address: - - https://git.pleroma.social/pleroma/relay - -

-

List of {count} registered instances:
{targets}

- -""" - - # pylint: disable=unused-argument @register_route('/') class HomeView(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: - config = conn.get_config_all() - inboxes = tuple(conn.execute('SELECT * FROM inboxes').all()) + instances = tuple(conn.execute('SELECT * FROM inboxes').all()) - text = HOME_TEMPLATE.format( - host = self.config.domain, - note = config['note'], - count = len(inboxes), - targets = '
'.join(inbox['domain'] for inbox in inboxes) - ) + # text = HOME_TEMPLATE.format( + # host = self.config.domain, + # note = config['note'], + # count = len(inboxes), + # targets = '
'.join(inbox['domain'] for inbox in inboxes) + # ) - return Response.new(text, ctype='html') + data = self.template.render('page/home.haml', instances = instances) + return Response.new(data, ctype='html') + + +@register_route('/style.css') +class StyleCss(View): + async def get(self, request: Request) -> Response: + data = self.template.render('style.css') + return Response.new(data, ctype = 'css') diff --git a/requirements.txt b/requirements.txt index aea9c24..e3ba14a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,11 @@ 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 +barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.1.tar.gz click>=8.1.2 +hamlish-jinja@https://git.barkshark.xyz/barkshark/hamlish-jinja/archive/0.3.5.tar.gz hiredis==2.3.2 pyyaml>=6.0 redis==5.0.1 -barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.1.tar.gz importlib_resources==6.1.1;python_version<'3.9' From cd43aae535d5a7d64b4305c200f76334fae026c8 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 19:58:35 -0500 Subject: [PATCH 06/31] add dev commands --- MANIFEST.in | 4 +- dev-requirements.txt | 1 + relay/dev.py | 153 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 relay/dev.py diff --git a/MANIFEST.in b/MANIFEST.in index aaac993..d58d6d6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ +include frontend/base.haml +include frontend/style.css include data/statements.sql include data/swagger.yaml include frontend/page/home.haml -include frontend/base.haml -include frontend/style.css diff --git a/dev-requirements.txt b/dev-requirements.txt index 65f6520..f0fb91f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ flake8 == 7.0.0 pyinstaller == 6.3.0 pylint == 3.0 +watchdog == 4.0.0 diff --git a/relay/dev.py b/relay/dev.py new file mode 100644 index 0000000..443a0d0 --- /dev/null +++ b/relay/dev.py @@ -0,0 +1,153 @@ +import click +import subprocess +import sys +import time + +from datetime import datetime +from pathlib import Path + +try: + from watchdog.observers import Observer + from watchdog.events import PatternMatchingEventHandler + +except ImportError: + class PatternMatchingEventHandler: + pass + + +SCRIPT = Path(__file__).parent +REPO = SCRIPT.parent +IGNORE_EXT = { + '.py', + '.pyc' +} + + +@click.group('cli') +def cli(): + 'Useful commands for development' + + +@cli.command('install') +def cli_install(): + cmd = [ + sys.executable, '-m', 'pip', 'install', + '-r', 'requirements.txt', + '-r', 'dev-requirements.txt' + ] + + subprocess.run(cmd, check = False) + + +@cli.command('lint') +@click.argument('path', required = False, default = 'relay') +def cli_lint(path): + subprocess.run([sys.executable, '-m', 'flake8', path], check = False) + subprocess.run([sys.executable, '-m', 'pylint', path], check = False) + + +@cli.command('manifest-gen') +def cli_manifest_install(): + paths = [] + + for path in SCRIPT.rglob('*'): + if path.suffix.lower() in IGNORE_EXT or not path.is_file(): + continue + + paths.append(path) + + with REPO.joinpath('MANIFEST.in').open('w', encoding = 'utf-8') as fd: + for path in paths: + fd.write(f'include {str(path.relative_to(SCRIPT))}\n') + + +@cli.command('build') +def cli_build(): + cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec'] + subprocess.run(cmd, check = False) + + +@cli.command('run') +def cli_run(): + print('Starting process watcher') + + handler = WatchHandler() + handler.run_proc() + + watcher = Observer() + watcher.schedule(handler, str(SCRIPT), recursive=True) + watcher.start() + + try: + while True: + handler.proc.stdin.write(sys.stdin.read().encode('UTF-8')) + handler.proc.stdin.flush() + + except KeyboardInterrupt: + pass + + handler.kill_proc() + watcher.stop() + watcher.join() + + + +class WatchHandler(PatternMatchingEventHandler): + patterns = ['*.py'] + cmd = [sys.executable, '-m', 'relay', 'run'] + + + def __init__(self): + PatternMatchingEventHandler.__init__(self) + + self.proc = None + self.last_restart = None + + + def kill_proc(self): + if self.proc.poll() is not None: + return + + print(f'Terminating process {self.proc.pid}') + self.proc.terminate() + sec = 0.0 + + while self.proc.poll() is None: + time.sleep(0.1) + sec += 0.1 + + if sec >= 5: + print('Failed to terminate. Killing process...') + self.proc.kill() + break + + print('Process terminated') + + + def run_proc(self, restart=False): + timestamp = datetime.timestamp(datetime.now()) + self.last_restart = timestamp if not self.last_restart else 0 + + if restart and self.proc.pid != '': + if timestamp - 3 < self.last_restart: + return + + self.kill_proc() + + # pylint: disable=consider-using-with + self.proc = subprocess.Popen(self.cmd, stdin = subprocess.PIPE) + self.last_restart = timestamp + + print(f'Started process with PID {self.proc.pid}') + + + def on_any_event(self, event): + if event.event_type not in ['modified', 'created', 'deleted']: + return + + print(event.src_path) + self.run_proc(restart = True) + + +if __name__ == '__main__': + cli() From 6f03e2ad4cffd5ca91359785801acce24b652ff7 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 20:04:31 -0500 Subject: [PATCH 07/31] fix linter issues --- relay/application.py | 18 ++++++++++-------- relay/database/connection.py | 2 +- relay/manage.py | 2 -- relay/misc.py | 8 ++++---- relay/template.py | 2 -- relay/views/api.py | 1 - 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/relay/application.py b/relay/application.py index fdf2503..05aa8cc 100644 --- a/relay/application.py +++ b/relay/application.py @@ -2,10 +2,7 @@ from __future__ import annotations import asyncio import multiprocessing -import os import signal -import subprocess -import sys import time import traceback import typing @@ -127,10 +124,15 @@ class Application(web.Application): if self["running"]: return - if not check_open_port(self.config.listen, self.config.port): - return logging.error(f'A server is already running on port {self.config.port}') + domain = self.config.domain + host = self.config.listen + port = self.config.port - logging.info(f'Starting webserver at {self.config.domain} ({self.config.listen}:{self.config.port})') + if not check_open_port(host, port): + logging.error(f'A server is already running on {host}:{port}') + return + + logging.info(f'Starting webserver at {domain} ({host}:{port})') asyncio.run(self.handle_run()) @@ -158,7 +160,7 @@ class Application(web.Application): self['cleanup_thread'] = CacheCleanupThread(self) self['cleanup_thread'].start() - for i in range(self.config.workers): + for _ in range(self.config.workers): worker = PushWorker(self['push_queue']) worker.start() @@ -181,7 +183,7 @@ class Application(web.Application): await site.stop() - for worker in self['workers']: + for worker in self['workers']: # pylint: disable=not-an-iterable worker.stop() self.set_signal_handler(False) diff --git a/relay/database/connection.py b/relay/database/connection.py index 16168b7..f2247aa 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -18,7 +18,7 @@ from .config import ( ) from .. import logger as logging -from ..misc import get_app +from ..misc import boolean, get_app if typing.TYPE_CHECKING: from collections.abc import Iterator diff --git a/relay/manage.py b/relay/manage.py index 8066d79..9343d3d 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -5,8 +5,6 @@ import asyncio import click import os import platform -import subprocess -import sys import typing from aputils.signer import Signer diff --git a/relay/misc.py b/relay/misc.py index 7060aa4..231eea5 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -88,11 +88,11 @@ def get_resource(path: str) -> Path: class JsonEncoder(json.JSONEncoder): - def default(self, obj: Any) -> str: - if isinstance(obj, datetime): - return obj.isoformat() + def default(self, o: Any) -> str: + if isinstance(o, datetime): + return o.isoformat() - return JSONEncoder.default(self, obj) + return json.JSONEncoder.default(self, o) class Message(ApMessage): diff --git a/relay/template.py b/relay/template.py index ecb556a..11687b5 100644 --- a/relay/template.py +++ b/relay/template.py @@ -5,8 +5,6 @@ import typing from hamlish_jinja.extension import HamlishExtension from jinja2 import Environment, FileSystemLoader -from pathlib import Path - from .database.config import THEMES from .misc import get_resource diff --git a/relay/views/api.py b/relay/views/api.py index 07a5c9a..a5c0fba 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -4,7 +4,6 @@ import typing from aiohttp import web from argon2.exceptions import VerifyMismatchError -from datetime import datetime, timezone from urllib.parse import urlparse from .base import View, register_route From b11abca7bc29c8d970f265fd86bdad943596dc3c Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 20:40:59 -0500 Subject: [PATCH 08/31] update aputils --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e3ba14a..0439a98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ 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.7.tar.gz argon2-cffi==23.1.0 barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.1.tar.gz click>=8.1.2 From 6a891ccf89861bcdc9fd8bcd7e9f87a4ec9e04f4 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 20:58:24 -0500 Subject: [PATCH 09/31] simplify aputils imports --- relay/manage.py | 4 ++-- relay/misc.py | 14 ++------------ relay/views/activitypub.py | 32 ++++++++++++++------------------ relay/views/misc.py | 6 +++--- 4 files changed, 21 insertions(+), 35 deletions(-) diff --git a/relay/manage.py b/relay/manage.py index 9343d3d..392f97b 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -1,13 +1,13 @@ from __future__ import annotations import Crypto +import aputils import asyncio import click import os import platform import typing -from aputils.signer import Signer from pathlib import Path from shutil import copyfile from urllib.parse import urlparse @@ -193,7 +193,7 @@ def cli_setup(ctx: click.Context) -> None: ctx.obj.config.save() config = { - 'private-key': Signer.new('n/a').export() + 'private-key': aputils.Signer.new('n/a').export() } with ctx.obj.database.session() as conn: diff --git a/relay/misc.py b/relay/misc.py index 231eea5..b4d333c 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -1,12 +1,12 @@ from __future__ import annotations +import aputils import json import os import socket import typing from aiohttp.web import Response as AiohttpResponse -from aputils.message import Message as ApMessage from datetime import datetime from uuid import uuid4 @@ -95,7 +95,7 @@ class JsonEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -class Message(ApMessage): +class Message(aputils.Message): @classmethod def new_actor(cls: type[Message], # pylint: disable=arguments-differ host: str, @@ -182,16 +182,6 @@ class Message(ApMessage): }) - # todo: remove when fixed in aputils - @property - def object_id(self) -> str: - try: - return self["object"]["id"] - - except (KeyError, TypeError): - return self["object"] - - class Response(AiohttpResponse): # AiohttpResponse.__len__ method returns 0, so bool(response) always returns False def __bool__(self) -> bool: diff --git a/relay/views/activitypub.py b/relay/views/activitypub.py index 08cbf2f..31266f6 100644 --- a/relay/views/activitypub.py +++ b/relay/views/activitypub.py @@ -1,12 +1,9 @@ from __future__ import annotations +import aputils import traceback import typing -from aputils.errors import SignatureFailureError -from aputils.misc import Digest, HttpDate, Signature -from aputils.objects import Webfinger - from .base import View, register_route from .. import logger as logging @@ -15,7 +12,6 @@ from ..processors import run_processor if typing.TYPE_CHECKING: from aiohttp.web import Request - from aputils.signer import Signer from tinysql import Row @@ -26,11 +22,11 @@ class ActorView(View): def __init__(self, request: Request): View.__init__(self, request) - self.signature: Signature = None + self.signature: aputils.Signature = None self.message: Message = None self.actor: Message = None self.instance: Row = None - self.signer: Signer = None + self.signer: aputils.Signer = None async def get(self, request: Request) -> Response: @@ -77,7 +73,7 @@ class ActorView(View): async def get_post_data(self) -> Response | None: try: - self.signature = Signature.new_from_signature(self.request.headers['signature']) + self.signature = aputils.Signature.new_from_signature(self.request.headers['signature']) except KeyError: logging.verbose('Missing signature header') @@ -124,7 +120,7 @@ class ActorView(View): try: self.validate_signature(await self.request.read()) - except SignatureFailureError as e: + except aputils.SignatureFailureError as e: logging.verbose('signature validation failed for "%s": %s', self.actor.id, e) return Response.new_error(401, str(e), 'json') @@ -133,31 +129,31 @@ class ActorView(View): headers = {key.lower(): value for key, value in self.request.headers.items()} headers["(request-target)"] = " ".join([self.request.method.lower(), self.request.path]) - if (digest := Digest.new_from_digest(headers.get("digest"))): + if (digest := aputils.Digest.new_from_digest(headers.get("digest"))): if not body: - raise SignatureFailureError("Missing body for digest verification") + raise aputils.SignatureFailureError("Missing body for digest verification") if not digest.validate(body): - raise SignatureFailureError("Body digest does not match") + raise aputils.SignatureFailureError("Body digest does not match") if self.signature.algorithm_type == "hs2019": if "(created)" not in self.signature.headers: - raise SignatureFailureError("'(created)' header not used") + raise aputils.SignatureFailureError("'(created)' header not used") - current_timestamp = HttpDate.new_utc().timestamp() + current_timestamp = aputils.HttpDate.new_utc().timestamp() if self.signature.created > current_timestamp: - raise SignatureFailureError("Creation date after current date") + raise aputils.SignatureFailureError("Creation date after current date") if current_timestamp > self.signature.expires: - raise SignatureFailureError("Expiration date before current date") + raise aputils.SignatureFailureError("Expiration date before current date") headers["(created)"] = self.signature.created headers["(expires)"] = self.signature.expires # pylint: disable=protected-access if not self.signer._validate_signature(headers, self.signature): - raise SignatureFailureError("Signature does not match") + raise aputils.SignatureFailureError("Signature does not match") @register_route('/.well-known/webfinger') @@ -172,7 +168,7 @@ class WebfingerView(View): if subject != f'acct:relay@{self.config.domain}': return Response.new_error(404, 'user not found', 'json') - data = Webfinger.new( + data = aputils.Webfinger.new( handle = 'relay', domain = self.config.domain, actor = self.config.actor diff --git a/relay/views/misc.py b/relay/views/misc.py index bede27d..65025e3 100644 --- a/relay/views/misc.py +++ b/relay/views/misc.py @@ -1,9 +1,9 @@ from __future__ import annotations +import aputils import subprocess import typing -from aputils.objects import Nodeinfo, WellKnownNodeinfo from pathlib import Path from .base import View, register_route @@ -48,12 +48,12 @@ class NodeinfoView(View): if niversion == '2.1': data['repo'] = 'https://git.pleroma.social/pleroma/relay' - return Response.new(Nodeinfo.new(**data), ctype = 'json') + return Response.new(aputils.Nodeinfo.new(**data), ctype = 'json') @register_route('/.well-known/nodeinfo') class WellknownNodeinfoView(View): async def get(self, request: Request) -> Response: - data = WellKnownNodeinfo.new_template(self.config.domain) + data = aputils.WellKnownNodeinfo.new_template(self.config.domain) return Response.new(data, ctype = 'json') From ae490a9bf3e4f971ec4d4bc1c6506791ff246b4c Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 23 Feb 2024 20:58:36 -0500 Subject: [PATCH 10/31] version bump to 0.3.0 --- docs/installation.md | 4 ++-- relay/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index a391389..ad2c2fa 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -15,7 +15,7 @@ the [official pipx docs](https://pypa.github.io/pipx/installation/) for more in- Now simply install ActivityRelay directly from git - pipx install git+https://git.pleroma.social/pleroma/relay@0.2.5 + pipx install git+https://git.pleroma.social/pleroma/relay@0.3.0 Or from a cloned git repo. @@ -39,7 +39,7 @@ be installed via [pyenv](https://github.com/pyenv/pyenv). The instructions for installation via pip are very similar to pipx. Installation can be done from git - python3 -m pip install git+https://git.pleroma.social/pleroma/relay@0.2.5 + python3 -m pip install git+https://git.pleroma.social/pleroma/relay@0.3.0 or a cloned git repo. diff --git a/relay/__init__.py b/relay/__init__.py index 44b1806..0404d81 100644 --- a/relay/__init__.py +++ b/relay/__init__.py @@ -1 +1 @@ -__version__ = '0.2.6' +__version__ = '0.3.0' From 0ad0bb0ff502c9461319124b9cc0676977e9ad7a Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 24 Feb 2024 08:40:57 -0500 Subject: [PATCH 11/31] fix instance table styling and add footer --- relay/frontend/base.haml | 6 ++++++ relay/frontend/page/home.haml | 3 --- relay/frontend/style.css | 35 ++++++++++++++++++++++------------- relay/template.py | 2 ++ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index a47d508..2df7899 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -14,3 +14,9 @@ #content -block content + + #footer.section + .col1 + .version + %a(href="https://git.pleroma.social/pleroma/relay") + ActivityRelay/{{version}} diff --git a/relay/frontend/page/home.haml b/relay/frontend/page/home.haml index 6af5b2b..efa7088 100644 --- a/relay/frontend/page/home.haml +++ b/relay/frontend/page/home.haml @@ -11,9 +11,6 @@ %p You may subscribe to this relay with the address: %a(href="https://{{domain}}/actor") << https://{{domain}}/actor - %p - To host your own relay, you may download the code at - %a(href="https://git.pleroma.social/pleroma/relay") << git.pleroma.social/pleroma/relay -if config["whitelist-enabled"] %p.section.message diff --git a/relay/frontend/style.css b/relay/frontend/style.css index e48c200..743b2b5 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -43,30 +43,30 @@ p:not(:last-child) { } table { - border-spacing: 0px; -/* border-radius: 10px; */ - border-collapse: collapse; -} - -td { border: 1px solid var(--primary); + border-radius: 5px; + border-spacing: 0px; } -/*thead td:first-child { - border-top-left-radius: 10px; +table tbody tr:nth-child(even) td { + background-color: var(--section-background); +} + +thead td:first-child { + border-top-left-radius: 3px; } thead td:last-child { - border-top-right-radius: 10px; + border-top-right-radius: 3px; } tbody tr:last-child td:first-child { - border-bottom-left-radius: 10px; + border-bottom-left-radius: 5px; } tbody tr:last-child td:last-child { - border-bottom-right-radius: 10px; -}*/ + border-bottom-right-radius: 5px; +} table td { padding: 5px; @@ -111,6 +111,15 @@ table tbody td { text-align: center !important; } +#footer { + display: grid; + grid-template-columns: auto auto; +} + +#footer .version { + text-align: right +} + .message { color: var(--message-text) !important; @@ -122,7 +131,7 @@ table tbody td { background-color: var(--section-background); padding: var(--spacing); border: 1px solid var(--border); - border-radius: 10px; + border-radius: 5px; } .section:not(:first-child) { diff --git a/relay/template.py b/relay/template.py index 11687b5..9f3a09b 100644 --- a/relay/template.py +++ b/relay/template.py @@ -5,6 +5,7 @@ import typing from hamlish_jinja.extension import HamlishExtension from jinja2 import Environment, FileSystemLoader +from . import __version__ from .database.config import THEMES from .misc import get_resource @@ -41,6 +42,7 @@ class Template(Environment): new_context = { 'view': view, 'domain': self.app.config.domain, + 'version': __version__, 'config': config, 'theme': THEMES.get(config['theme'], THEMES['default']), **(context or {}) From a271cf22b49452aedb3e97c5e3c2306d81391861 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 2 Mar 2024 07:38:48 -0500 Subject: [PATCH 12/31] check for config in multiple directories --- relay/application.py | 2 +- relay/config.py | 21 ++++++++++++++++++++- relay/manage.py | 4 ++-- requirements.txt | 1 + 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/relay/application.py b/relay/application.py index 05aa8cc..9a49433 100644 --- a/relay/application.py +++ b/relay/application.py @@ -35,7 +35,7 @@ if typing.TYPE_CHECKING: class Application(web.Application): DEFAULT: Application = None - def __init__(self, cfgpath: str): + def __init__(self, cfgpath: str | None): web.Application.__init__(self, middlewares = [ handle_api_path diff --git a/relay/config.py b/relay/config.py index 3512726..e61c99a 100644 --- a/relay/config.py +++ b/relay/config.py @@ -7,6 +7,7 @@ import typing import yaml from pathlib import Path +from platformdirs import user_config_dir from .misc import IS_DOCKER @@ -51,7 +52,7 @@ if IS_DOCKER: class Config: def __init__(self, path: str, load: bool = False): - self.path = Path(path).expanduser().resolve() + self.path = Config.get_config_dir() self.listen = None self.port = None @@ -82,6 +83,24 @@ class Config: self.save() + @staticmethod + def get_config_dir(path: str | None = None) -> Path: + if path: + return Path(path).expanduser().resolve() + + dirs = ( + Path("relay.yaml").resolve(), + Path(user_config_dir("activityrelay"), "relay.yaml"), + Path("/etc/activityrelay/relay.yaml") + ) + + for directory in dirs: + if directory.exists(): + return directory + + return dirs[0] + + @property def sqlite_path(self) -> Path: if not os.path.isabs(self.sq_path): diff --git a/relay/manage.py b/relay/manage.py index 392f97b..01e034e 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -59,10 +59,10 @@ def check_alphanumeric(text: str) -> str: @click.group('cli', context_settings={'show_default': True}, invoke_without_command=True) -@click.option('--config', '-c', default='relay.yaml', help='path to the relay\'s config') +@click.option('--config', '-c', help='path to the relay\'s config') @click.version_option(version=__version__, prog_name='ActivityRelay') @click.pass_context -def cli(ctx: click.Context, config: str) -> None: +def cli(ctx: click.Context, config: str | None) -> None: ctx.obj = Application(config) if not ctx.invoked_subcommand: diff --git a/requirements.txt b/requirements.txt index 0439a98..310e1eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.1.tar.gz click>=8.1.2 hamlish-jinja@https://git.barkshark.xyz/barkshark/hamlish-jinja/archive/0.3.5.tar.gz hiredis==2.3.2 +platformdirs==4.2.0 pyyaml>=6.0 redis==5.0.1 From 7af3b9c20bf7845ab898aa3baf4de3e6238662d1 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 2 Mar 2024 17:36:44 -0500 Subject: [PATCH 13/31] add login/logout and start on admin interface --- relay/application.py | 4 +- relay/database/config.py | 15 +- relay/frontend/base.haml | 60 +++++++- relay/frontend/functions.haml | 1 + relay/frontend/page/admin-config.haml | 5 + relay/frontend/page/admin-domain_bans.haml | 5 + relay/frontend/page/admin-instances.haml | 5 + relay/frontend/page/admin-software_bans.haml | 5 + relay/frontend/page/admin-whitelist.haml | 5 + relay/frontend/page/login.haml | 9 ++ relay/frontend/style.css | 123 ++++++++++++++-- relay/frontend/style/config.css | 0 relay/frontend/style/domain_bans.css | 0 relay/frontend/style/home.css | 16 ++ relay/frontend/style/instances.css | 0 relay/frontend/style/login.css | 18 +++ relay/frontend/style/software_bans.css | 0 relay/frontend/style/whitelist.css | 0 relay/misc.py | 6 + relay/views/frontend.py | 145 ++++++++++++++++++- 20 files changed, 402 insertions(+), 20 deletions(-) create mode 100644 relay/frontend/functions.haml create mode 100644 relay/frontend/page/admin-config.haml create mode 100644 relay/frontend/page/admin-domain_bans.haml create mode 100644 relay/frontend/page/admin-instances.haml create mode 100644 relay/frontend/page/admin-software_bans.haml create mode 100644 relay/frontend/page/admin-whitelist.haml create mode 100644 relay/frontend/page/login.haml create mode 100644 relay/frontend/style/config.css create mode 100644 relay/frontend/style/domain_bans.css create mode 100644 relay/frontend/style/home.css create mode 100644 relay/frontend/style/instances.css create mode 100644 relay/frontend/style/login.css create mode 100644 relay/frontend/style/software_bans.css create mode 100644 relay/frontend/style/whitelist.css diff --git a/relay/application.py b/relay/application.py index 9a49433..6fc09ae 100644 --- a/relay/application.py +++ b/relay/application.py @@ -23,6 +23,7 @@ from .misc import check_open_port, get_resource from .template import Template from .views import VIEWS from .views.api import handle_api_path +from .views.frontend import handle_frontend_path if typing.TYPE_CHECKING: from tinysql import Database, Row @@ -38,7 +39,8 @@ class Application(web.Application): def __init__(self, cfgpath: str | None): web.Application.__init__(self, middlewares = [ - handle_api_path + handle_api_path, + handle_frontend_path ] ) diff --git a/relay/database/config.py b/relay/database/config.py index c961a0c..d49f1b5 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -22,7 +22,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' }, 'pink': { 'text': '#DDD', @@ -34,7 +37,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' }, 'blue': { 'text': '#DDD', @@ -46,7 +52,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' } } diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 2df7899..8fb7e80 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -1,22 +1,80 @@ +-macro menu_item(name, path) + -if view.request.path == path + %a.button(active="true") -> =name + + -else + %a.button(href="{{path}}") -> =name + !!! %html %head %title << {{config.name}}: {{page}} %meta(charset="UTF-8") %meta(name="viewport" content="width=device-width, initial-scale=1") - %link(rel="stylesheet" type="text/css" href="/style.css") + %link(rel="stylesheet" type="text/css" href="/style.css?page={{page}}") -block head %body + #menu.section(visible="false") + .menu-head + %span.menu-title << Menu + %span#menu-close.button << ✖ + + {{menu_item("Home", "/")}} + + -if view.request["user"] + {{menu_item("Instances", "/admin/instances")}} + {{menu_item("Whitelist", "/admin/whitelist")}} + {{menu_item("Domain Bans", "/admin/domain_bans")}} + {{menu_item("Software Bans", "/admin/software_bans")}} + {{menu_item("Config", "/admin/config")}} + {{menu_item("Logout", "/logout")}} + + -else + {{menu_item("Login", "/login")}} + #container #header.section + %span#menu-open.button << ⁞ %a(href="https://{{domain}}/") -> =config.name + .empty + + -if error + .error.section -> =error + + -if message + .message.section -> =message #content -block content #footer.section .col1 + -if not view.request["user"] + %a(href="/login") << Login + + -else + =view.request["user"]["username"] + ( + %a(href="/logout") << Logout + ) + .version %a(href="https://git.pleroma.social/pleroma/relay") ActivityRelay/{{version}} + + %script(type="application/javascript") + const body = document.getElementById("container") + const menu = document.getElementById("menu"); + const menu_open = document.getElementById("menu-open"); + const menu_close = document.getElementById("menu-close"); + + menu_open.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "true"}); + menu_close.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "false"}); + body.addEventListener("click", (event) => { + if (event.target === menu_open) { + return; + } + + menu.attributes.visible.nodeValue = "false"; + }); diff --git a/relay/frontend/functions.haml b/relay/frontend/functions.haml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/relay/frontend/functions.haml @@ -0,0 +1 @@ + diff --git a/relay/frontend/page/admin-config.haml b/relay/frontend/page/admin-config.haml new file mode 100644 index 0000000..6587dbe --- /dev/null +++ b/relay/frontend/page/admin-config.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Config" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml new file mode 100644 index 0000000..6550244 --- /dev/null +++ b/relay/frontend/page/admin-domain_bans.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Domain Bans" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml new file mode 100644 index 0000000..04c84f9 --- /dev/null +++ b/relay/frontend/page/admin-instances.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Instances" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml new file mode 100644 index 0000000..c215fc8 --- /dev/null +++ b/relay/frontend/page/admin-software_bans.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Software Bans" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml new file mode 100644 index 0000000..083c8bb --- /dev/null +++ b/relay/frontend/page/admin-whitelist.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Whitelist" +-block content + .section + UvU diff --git a/relay/frontend/page/login.haml b/relay/frontend/page/login.haml new file mode 100644 index 0000000..ee7d1dc --- /dev/null +++ b/relay/frontend/page/login.haml @@ -0,0 +1,9 @@ +-extends "base.haml" +-set page="Login" +-block content + %form.section(action="/login" method="post") + %label(for="username") << Username + %input(id="username" name="username" placeholder="Username" value="{{username or ''}}") + %label(for="password") << Password + %input(id="password" name="password" placeholder="Password" type="password") + %input(type="submit" value="Login") diff --git a/relay/frontend/style.css b/relay/frontend/style.css index 743b2b5..61c358d 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -9,6 +9,9 @@ --message-text: {{theme["message-text"]}}; --message-background: {{theme["message-background"]}}; --message-border: {{theme["message-border"]}}; + --error-text: {{theme["error-text"]}}; + --error-background: {{theme["error-background"]}}; + --error-border: {{theme["error-border"]}}; --spacing: 10px; } @@ -84,31 +87,76 @@ table tbody td { #container { width: 1024px; margin: 0px auto; + height: 100vh; } #header { - text-align: center; + display: grid; + grid-template-columns: 50px auto 50px; + justify-items: center; + align-items: center; } -#header a { - font-size: 3em; +#header > * { + font-size: 2em; } -#instances table { - width: 100%; +#header > *:nth-child(2) { + font-weight: bold; } -#instances .instance { - width: 100%; -} - -#instances .date { +#menu { + padding: 0px; + position: fixed; + top: 0px; + left: 0px; + margin: 0px; + height: 100%; width: max-content; - text-align: right; + z-index: 1; + font-size: 1.5em; + min-width: 300px; } -#instances thead td { - text-align: center !important; +#menu[visible="false"] { + visibility: hidden; +} + +#menu > a { + margin: var(--spacing); + display: block; + border-radius: 5px; + padding: 5px; +} + +#menu > a[active="true"] { + cursor: default; + background-color: var(--background); + color: var(--primary); + border-color: transparent; +} + +#menu .menu-head { + display: grid; + grid-template-columns: auto max-content; + margin: var(--spacing); +} + +#menu .menu-head > * { + font-weight: bold; +} + +#menu .menu-title { + color: var(--primary); +} + +#menu-open, #menu-close { + cursor: pointer; +} + +#menu-close, #menu-open { + min-width: 35px; + text-align: center; } #footer { @@ -121,6 +169,34 @@ table tbody td { } +.button { + background-color: var(--primary); + border: 1px solid var(--primary); + color: var(--background); + border-radius: 5px; +} + +.button:hover { + background-color: var(--background); + color: var(--primary); + text-decoration: None; +} + +.container { + display: grid; + grid-template-columns: max-content auto; +} + +.error, .message { + text-align: center; +} + +.error{ + color: var(--error-text) !important; + background-color: var(--error-background) !important; + border: 1px solid var(--error-border) !important; +} + .message { color: var(--message-text) !important; background-color: var(--message-background) !important; @@ -143,16 +219,37 @@ table tbody td { } +{% if page %} + {% include "style/" + page.lower().replace(" ", "_") + ".css" %} +{% endif %} + + @media (max-width: 1026px) { body { margin: 0px; } + #menu { + width: 100%; + } + + #menu > a { + text-align: center; + } + #container { width: unset; margin: unset; } + .container { + grid-template-columns: auto; + } + + .content { + grid-template-columns: auto; + } + .section { border-width: 0px; border-radius: 0px; diff --git a/relay/frontend/style/config.css b/relay/frontend/style/config.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/home.css b/relay/frontend/style/home.css new file mode 100644 index 0000000..4cdfaa9 --- /dev/null +++ b/relay/frontend/style/home.css @@ -0,0 +1,16 @@ +#instances table { + width: 100%; +} + +#instances .instance { + width: 100%; +} + +#instances .date { + width: max-content; + text-align: right; +} + +#instances thead td { + text-align: center !important; +} diff --git a/relay/frontend/style/instances.css b/relay/frontend/style/instances.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/login.css b/relay/frontend/style/login.css new file mode 100644 index 0000000..48f623b --- /dev/null +++ b/relay/frontend/style/login.css @@ -0,0 +1,18 @@ +label, input { + margin: 0 auto; + display: block; +} + +label, input:not([type="submit"]) { + width: 50%; +} + +input:not([type="submit"]) { + margin-bottom: var(--spacing); +} + +@media (max-width: 1026px) { + label, input:not([type="submit"]) { + width: 75%; + } +} diff --git a/relay/frontend/style/software_bans.css b/relay/frontend/style/software_bans.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/whitelist.css b/relay/frontend/style/whitelist.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/misc.py b/relay/misc.py index b4d333c..3007863 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -225,6 +225,12 @@ class Response(AiohttpResponse): return cls.new(body=body, status=status, ctype=ctype) + @classmethod + def new_redir(cls: type[Response], path: str) -> Response: + body = f'Redirect to {path}' + return cls.new(body, 302, {'Location': path}) + + @property def location(self) -> str: return self.headers.get('Location') diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 567a44b..7b45011 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -2,6 +2,9 @@ from __future__ import annotations import typing +from aiohttp import web +from argon2.exceptions import VerifyMismatchError + from .base import View, register_route from ..misc import Response @@ -10,6 +13,44 @@ if typing.TYPE_CHECKING: from aiohttp.web import Request +AUTH_ROUTES = { + '/admin', + '/admin/instances', + '/admin/domain_bans', + '/admin/software_bans', + '/admin/whitelist', + '/admin/config', + '/logout' +} + + +UNAUTH_ROUTES = { + '/', + '/login' +} + +ALL_ROUTES = {*AUTH_ROUTES, *UNAUTH_ROUTES} + + +@web.middleware +async def handle_frontend_path(request: web.Request, handler: Coroutine) -> Response: + if request.path in ALL_ROUTES: + request['token'] = request.cookies.get('user-token') + request['user'] = None + + if request['token']: + with request.app.database.session(False) as conn: + request['user'] = conn.get_user_by_token(request['token']) + + if request['user'] and request.path == '/login': + return Response.new('', 302, {'Location': '/'}) + + if not request['user'] and request.path.startswith('/admin'): + return Response.new('', 302, {'Location': f'/login?redir={request.path}'}) + + return await handler(request) + + # pylint: disable=unused-argument @register_route('/') @@ -25,12 +66,112 @@ class HomeView(View): # targets = '
'.join(inbox['domain'] for inbox in inboxes) # ) - data = self.template.render('page/home.haml', instances = instances) + data = self.template.render('page/home.haml', self, instances = instances) return Response.new(data, ctype='html') +@register_route('/login') +class Login(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/login.haml', self) + return Response.new(data, ctype = 'html') + + + async def post(self, request: Request) -> Response: + form = await request.post() + params = {} + + with self.database.session(True) as conn: + if not (user := conn.get_user(form['username'])): + params = { + 'username': form['username'], + 'error': 'User not found' + } + + else: + try: + conn.hasher.verify(user['hash'], form['password']) + + except VerifyMismatchError: + params = { + 'username': form['username'], + 'error': 'Invalid password' + } + + if params: + data = self.template.render('page/login.haml', self, **params) + return Response.new(data, ctype = 'html') + + token = conn.put_token(user['username']) + resp = Response.new_redir(request.query.getone('redir', '/')) + resp.set_cookie( + 'user-token', + token['code'], + max_age = 60 * 60 * 24 * 365, + domain = self.config.domain, + path = '/', + secure = True, + httponly = True, + samesite = 'Strict' + ) + + return resp + + +@register_route('/logout') +class Logout(View): + async def get(self, request: Request) -> Response: + with self.database.session(True) as conn: + conn.del_token(request['token']) + + resp = Response.new_redir('/') + resp.del_cookie('user-token', domain = self.config.domain, path = '/') + return resp + + +@register_route('/admin') +class Admin(View): + async def get(self, request: Request) -> Response: + return Response.new('', 302, {'Location': '/admin/instances'}) + + +@register_route('/admin/instances') +class AdminInstances(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-instances.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/whitelist') +class AdminWhitelist(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-whitelist.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/domain_bans') +class AdminDomainBans(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-domain_bans.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/software_bans') +class AdminSoftwareBans(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-software_bans.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/config') +class AdminConfig(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-config.haml', self) + return Response.new(data, ctype = 'html') + + @register_route('/style.css') class StyleCss(View): async def get(self, request: Request) -> Response: - data = self.template.render('style.css') + data = self.template.render('style.css', self, page = request.query.getone('page', "")) return Response.new(data, ctype = 'css') From e6831f04eb545ef662f2ca560755380b5ad6daf5 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 2 Mar 2024 19:58:06 -0500 Subject: [PATCH 14/31] create admin instances page --- relay/frontend/functions.haml | 1 - relay/frontend/page/admin-instances.haml | 40 ++++++++++- relay/frontend/style.css | 4 ++ relay/frontend/style/instances.css | 33 +++++++++ relay/manage.py | 19 +----- relay/misc.py | 17 +++++ relay/views/frontend.py | 87 +++++++++++++++++------- 7 files changed, 154 insertions(+), 47 deletions(-) delete mode 100644 relay/frontend/functions.haml diff --git a/relay/frontend/functions.haml b/relay/frontend/functions.haml deleted file mode 100644 index 8b13789..0000000 --- a/relay/frontend/functions.haml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index 04c84f9..9b2c5a6 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -1,5 +1,41 @@ -extends "base.haml" -set page="Instances" -block content - .section - UvU + %details.section + %summary << Add Instance + %form(target="/admin/instances", method="POST") + #add-instance + %label(for="domain") << Domain + %input(type="domain", name="domain", placeholder="Domain") + %label(for="actor") << Actor URL + %input(type="url", name="actor", placeholder="Actor URL") + %label(for="inbox") << Inbox URL + %input(type="url", name="inbox", placeholder="Inbox URL") + %label(for="software") << Software + %input(name="software", placeholder="software") + + %input(type="submit" value="Add Instance") + + #instances.section + %table + %thead + %tr + %td.instance << Instance + %td.software << Software + %td.date << Joined + %td.remove + + %tbody + -for instance in instances + %tr + %td.instance + %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain + + %td.software + =instance.software or "n/a" + + %td.date + =instance.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/instances/delete/{{instance.domain}}" title="Remove Instance") << ✖ diff --git a/relay/frontend/style.css b/relay/frontend/style.css index 61c358d..cc4818f 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -32,6 +32,10 @@ a:hover { text-decoration: underline; } +details summary { + cursor: pointer; +} + p { line-height: 1em; margin: 0px; diff --git a/relay/frontend/style/instances.css b/relay/frontend/style/instances.css index e69de29..8e8789b 100644 --- a/relay/frontend/style/instances.css +++ b/relay/frontend/style/instances.css @@ -0,0 +1,33 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +#instances table { + width: 100%; +} + +#instances .instance { + width: 100%; +} + +#instances .software { + text-align: center; +} + +#instances .date { + width: max-content; + text-align: right; +} + +#instances thead td { + text-align: center !important; +} + +#add-instance { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/manage.py b/relay/manage.py index 01e034e..9fe0b17 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -18,7 +18,7 @@ from . import logger as logging from .application import Application from .compat import RelayConfig, RelayDatabase from .database import RELAY_SOFTWARE, get_database -from .misc import IS_DOCKER, Message +from .misc import ACTOR_FORMATS, SOFTWARE, IS_DOCKER, Message if typing.TYPE_CHECKING: from tinysql import Row @@ -33,23 +33,6 @@ CONFIG_IGNORE = ( 'private-key' ) -ACTOR_FORMATS = { - 'mastodon': 'https://{domain}/actor', - 'akkoma': 'https://{domain}/relay', - 'pleroma': 'https://{domain}/relay' -} - -SOFTWARE = ( - 'mastodon', - 'akkoma', - 'pleroma', - 'misskey', - 'friendica', - 'hubzilla', - 'firefish', - 'gotosocial' -) - def check_alphanumeric(text: str) -> str: if not text.isalnum(): diff --git a/relay/misc.py b/relay/misc.py index 3007863..33e7a06 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -36,6 +36,23 @@ NODEINFO_NS = { '21': 'http://nodeinfo.diaspora.software/ns/schema/2.1' } +ACTOR_FORMATS = { + 'mastodon': 'https://{domain}/actor', + 'akkoma': 'https://{domain}/relay', + 'pleroma': 'https://{domain}/relay' +} + +SOFTWARE = ( + 'mastodon', + 'akkoma', + 'pleroma', + 'misskey', + 'friendica', + 'hubzilla', + 'firefish', + 'gotosocial' +) + def boolean(value: Any) -> bool: if isinstance(value, str): diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 7b45011..5206c67 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -7,34 +7,21 @@ from argon2.exceptions import VerifyMismatchError from .base import View, register_route -from ..misc import Response +from ..misc import ACTOR_FORMATS, Message, Response if typing.TYPE_CHECKING: from aiohttp.web import Request -AUTH_ROUTES = { - '/admin', - '/admin/instances', - '/admin/domain_bans', - '/admin/software_bans', - '/admin/whitelist', - '/admin/config', - '/logout' -} - - UNAUTH_ROUTES = { '/', '/login' } -ALL_ROUTES = {*AUTH_ROUTES, *UNAUTH_ROUTES} - @web.middleware async def handle_frontend_path(request: web.Request, handler: Coroutine) -> Response: - if request.path in ALL_ROUTES: + if request.path in UNAUTH_ROUTES or request.path.startswith('/admin'): request['token'] = request.cookies.get('user-token') request['user'] = None @@ -57,16 +44,11 @@ async def handle_frontend_path(request: web.Request, handler: Coroutine) -> Resp class HomeView(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: - instances = tuple(conn.execute('SELECT * FROM inboxes').all()) + context = { + 'instances': tuple(conn.execute('SELECT * FROM inboxes').all()) + } - # text = HOME_TEMPLATE.format( - # host = self.config.domain, - # note = config['note'], - # count = len(inboxes), - # targets = '
'.join(inbox['domain'] for inbox in inboxes) - # ) - - data = self.template.render('page/home.haml', self, instances = instances) + data = self.template.render('page/home.haml', self, **context) return Response.new(data, ctype='html') @@ -137,11 +119,64 @@ class Admin(View): @register_route('/admin/instances') class AdminInstances(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-instances.haml', self) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'instances': tuple(conn.execute('SELECT * FROM inboxes').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-instances.haml', self, **context) return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + data = {key: value for key, value in (await request.post()).items()} + + if not data['actor'] and not data['domain']: + return await self.get(request, error = 'Missing actor and/or domain') + + if not data['domain']: + data['domain'] = urlparse(data['actor']).netloc + + if not data['software']: + nodeinfo = await self.client.fetch_nodeinfo(data['domain']) + data['software'] = nodeinfo.sw_name + + if not data['actor'] and data['software'] in ACTOR_FORMATS: + data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain']) + + if not data['inbox'] and data['actor']: + actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse) + data['inbox'] = actor.shared_inbox + + with self.database.session(True) as conn: + conn.put_inbox(**data) + + return await self.get(request, message = "Added new inbox") + + +@register_route('/admin/instances/delete/{domain}') +class AdminInstancesDelete(View): + async def get(self, request: Request, domain: str) -> Response: + with self.database.session() as conn: + if not (conn.get_inbox(domain)): + return await AdminInstances(request).get(request, message = 'Instance not found') + + conn.del_inbox(domain) + + return await AdminInstances(request).get(request, message = 'Removed instance') + + @register_route('/admin/whitelist') class AdminWhitelist(View): async def get(self, request: Request) -> Response: From e5d8c9dcb0bee0070e9bd24b01af4a8c4767fc59 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sun, 3 Mar 2024 00:20:38 -0500 Subject: [PATCH 15/31] create admin config page --- relay/database/__init__.py | 2 +- relay/database/config.py | 2 +- relay/frontend/page/admin-config.haml | 36 +++++++++++++++++++++-- relay/frontend/page/admin-instances.haml | 8 ++--- relay/frontend/page/home.haml | 4 ++- relay/frontend/style/config.css | 20 +++++++++++++ relay/views/frontend.py | 37 ++++++++++++++++++++++-- 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/relay/database/__init__.py b/relay/database/__init__.py index a55f2c8..d248713 100644 --- a/relay/database/__init__.py +++ b/relay/database/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import bsql import typing -from .config import get_default_value +from .config import CONFIG_DEFAULTS, THEMES, get_default_value from .connection import RELAY_SOFTWARE, Connection from .schema import TABLES, VERSIONS, migrate_0 diff --git a/relay/database/config.py b/relay/database/config.py index d49f1b5..82e2e69 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -61,10 +61,10 @@ THEMES = { CONFIG_DEFAULTS: dict[str, tuple[str, Any]] = { 'schema-version': ('int', 20240206), + 'private-key': ('str', None), 'log-level': ('loglevel', logging.LogLevel.INFO), 'name': ('str', 'ActivityRelay'), 'note': ('str', 'Make a note about your instance here.'), - 'private-key': ('str', None), 'theme': ('str', 'default'), 'whitelist-enabled': ('bool', False) } diff --git a/relay/frontend/page/admin-config.haml b/relay/frontend/page/admin-config.haml index 6587dbe..bf3d8b8 100644 --- a/relay/frontend/page/admin-config.haml +++ b/relay/frontend/page/admin-config.haml @@ -1,5 +1,37 @@ -extends "base.haml" -set page="Config" -block content - .section - UvU + %form.section(action="/admin/config" method="POST") + #config-options + %label(for="name") << Name + %input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}") + + %label(for="description") << Description + %textarea(id="description" name="note" value="{{config.note}}") << {{config.note}} + + %label(for="theme") << Color Theme + %select(id="theme" name="theme") + -for theme in themes + -if theme == config.theme + %option(value="{{theme}}" selected) -> =theme.title() + + -else + %option(value="{{theme}}") -> =theme.title() + + %label(for="log-level") << Log Level + %select(id="log-level" name="log-level") + -for level in LogLevel + -if level == config["log-level"] + %option(value="{{level.name}}" selected) -> =level.name.title() + + -else + %option(value="{{level.name}}") -> =level.name.title() + + %label(for="whitelist-enabled") << Whitelist + -if config["whitelist-enabled"] + %input(id="whitelist-enabled" name="whitelist-enabled" type="checkbox" checked) + + -else + %input(id="whitelist-enabled" name="whitelist-enabled" type="checkbox") + + %input(type="submit" value="Save") diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index 9b2c5a6..b563383 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -6,13 +6,13 @@ %form(target="/admin/instances", method="POST") #add-instance %label(for="domain") << Domain - %input(type="domain", name="domain", placeholder="Domain") + %input(type="domain", id="domain" name="domain", placeholder="Domain") %label(for="actor") << Actor URL - %input(type="url", name="actor", placeholder="Actor URL") + %input(type="url", id="actor" name="actor", placeholder="Actor URL") %label(for="inbox") << Inbox URL - %input(type="url", name="inbox", placeholder="Inbox URL") + %input(type="url", id="inbox" name="inbox", placeholder="Inbox URL") %label(for="software") << Software - %input(name="software", placeholder="software") + %input(name="software", id="software" placeholder="software") %input(type="submit" value="Add Instance") diff --git a/relay/frontend/page/home.haml b/relay/frontend/page/home.haml index efa7088..f64b489 100644 --- a/relay/frontend/page/home.haml +++ b/relay/frontend/page/home.haml @@ -2,7 +2,9 @@ -set page = "Home" -block content .section - =config.note + -for line in config.note.splitlines() + -if line + %p -> =line .section %p diff --git a/relay/frontend/style/config.css b/relay/frontend/style/config.css index e69de29..0dff253 100644 --- a/relay/frontend/style/config.css +++ b/relay/frontend/style/config.css @@ -0,0 +1,20 @@ +#config-options { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-bottom: var(--spacing); + align-items: center; +} + +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +form input[type="checkbox"] { + justify-self: left; +} + +textarea { + height: 4em; +} diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 5206c67..616017c 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -7,6 +7,8 @@ from argon2.exceptions import VerifyMismatchError from .base import View, register_route +from ..database import CONFIG_DEFAULTS, THEMES +from ..logger import LogLevel from ..misc import ACTOR_FORMATS, Message, Response if typing.TYPE_CHECKING: @@ -18,6 +20,11 @@ UNAUTH_ROUTES = { '/login' } +CONFIG_IGNORE = ( + 'schema-version', + 'private-key' +) + @web.middleware async def handle_frontend_path(request: web.Request, handler: Coroutine) -> Response: @@ -200,11 +207,37 @@ class AdminSoftwareBans(View): @register_route('/admin/config') class AdminConfig(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-config.haml', self) + async def get(self, request: Request, message: str | None = None) -> Response: + context = { + 'themes': tuple(THEMES.keys()), + 'LogLevel': LogLevel, + 'message': message + } + data = self.template.render('page/admin-config.haml', self, **context) return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + form = dict(await request.post()) + + with self.database.session(True) as conn: + for key in CONFIG_DEFAULTS: + value = form.get(key) + + if key == 'whitelist-enabled': + value = bool(value) + + elif key.lower() in CONFIG_IGNORE: + continue + + if value is None: + continue + + conn.put_config(key, value) + + return await self.get(request, message = 'Updated config') + + @register_route('/style.css') class StyleCss(View): async def get(self, request: Request) -> Response: From 15e7cac1b8f6a6d5d33521a7594415fc24fd4e64 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sun, 3 Mar 2024 01:41:54 -0500 Subject: [PATCH 16/31] update barkshark-sql to fix an issue --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 310e1eb..85bbf3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ aiohttp>=3.9.1 aiohttp-swagger[performance]==1.0.16 aputils@https://git.barkshark.xyz/barkshark/aputils/archive/0.1.7.tar.gz argon2-cffi==23.1.0 -barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.1.tar.gz +barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/499649a736fd22eb3752ce38fd7304a9b8432ab9.tar.gz click>=8.1.2 hamlish-jinja@https://git.barkshark.xyz/barkshark/hamlish-jinja/archive/0.3.5.tar.gz hiredis==2.3.2 From 2a866eaaaa2413f6f93e2a6851d5f4e65f189b50 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sun, 3 Mar 2024 03:03:32 -0500 Subject: [PATCH 17/31] create admin domain bans page --- relay/database/connection.py | 8 +-- relay/frontend/base.haml | 15 +++-- relay/frontend/page/admin-domain_bans.haml | 47 +++++++++++++- relay/frontend/style.css | 5 +- relay/frontend/style/domain_bans.css | 40 ++++++++++++ relay/views/base.py | 15 +++-- relay/views/frontend.py | 75 ++++++++++++++++++++-- 7 files changed, 184 insertions(+), 21 deletions(-) diff --git a/relay/database/connection.py b/relay/database/connection.py index f2247aa..2792111 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -266,10 +266,10 @@ class Connection(SqlConnection): params = {} - if reason: + if reason is not None: params['reason'] = reason - if note: + if note is not None: params['note'] = note statement = Update('domain_bans', params) @@ -321,10 +321,10 @@ class Connection(SqlConnection): params = {} - if reason: + if reason is not None: params['reason'] = reason - if note: + if note is not None: params['note'] = note statement = Update('software_bans', params) diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 8fb7e80..13b83e9 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -1,6 +1,6 @@ -macro menu_item(name, path) - -if view.request.path == path - %a.button(active="true") -> =name + -if view.request.path == path or (path != "/" and view.request.path.startswith(path)) + %a.button(href="{{path}}" active="true") -> =name -else %a.button(href="{{path}}") -> =name @@ -69,8 +69,15 @@ const menu_open = document.getElementById("menu-open"); const menu_close = document.getElementById("menu-close"); - menu_open.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "true"}); - menu_close.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "false"}); + menu_open.addEventListener("click", (event) => { + var new_value = menu.attributes.visible.nodeValue === "true" ? "false" : "true"; + menu.attributes.visible.nodeValue = new_value; + }); + + menu_close.addEventListener("click", (event) => { + menu.attributes.visible.nodeValue = "false" + }); + body.addEventListener("click", (event) => { if (event.target === menu_open) { return; diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index 6550244..e2b3e5a 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -1,5 +1,48 @@ -extends "base.haml" -set page="Domain Bans" -block content - .section - UvU + %details.section + %summary << Ban Domain + %form(action="/admin/domain_bans", method="POST") + #add-domain + %label(for="domain") << Domain + %input(type="domain" id="domain" name="domain" placeholder="Domain") + + %label(for="reason") << Ban Reason + %textarea(id="reason" name="reason") << {{""}} + + %label(for="note") << Admin Note + %textarea(id="note" name="note") << {{""}} + + %input(type="submit" value="Ban Domain") + + #domains.section + %table + %thead + %tr + %td.domain << Instance + %td.date << Joined + %td.remove + + %tbody + -for ban in bans + %tr + %td.domain + %details + %summary -> =ban.domain + %form(action="/admin/domain_bans" method="POST") + .items + .reason << Reason + %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} + + .note << Note + %textarea.note(id="note" name="note") << {{ban.note or ""}} + + %input(type="hidden" name="domain", value="{{ban.domain}}") + %input(type="submit" value="Update") + + %td.date + =ban.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/domain_bans/delete/{{ban.domain}}" title="Unban domain") << ✖ diff --git a/relay/frontend/style.css b/relay/frontend/style.css index cc4818f..986426f 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -133,9 +133,8 @@ table tbody td { padding: 5px; } -#menu > a[active="true"] { - cursor: default; - background-color: var(--background); +#menu > a[active="true"]:not(:hover) { + background-color: var(--primary-hover); color: var(--primary); border-color: transparent; } diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css index e69de29..437ab98 100644 --- a/relay/frontend/style/domain_bans.css +++ b/relay/frontend/style/domain_bans.css @@ -0,0 +1,40 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +textarea { + height: calc(5em); +} + +table .items { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); +} + +#domains table { + width: 100%; +} + +#domains .domain { + width: 100%; +} + +#domains .date { + width: max-content; + text-align: right; +} + +#domains thead td { + text-align: center !important; +} + +#add-domain { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/views/base.py b/relay/views/base.py index 8d6d1ff..65859a5 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -12,7 +12,8 @@ from ..misc import Response if typing.TYPE_CHECKING: from collections.abc import Callable, Coroutine, Generator - from tinysql import Database + from bsql import Database + from typing import Self from ..application import Application from ..cache import Cache from ..config import Config @@ -28,7 +29,7 @@ def register_route(*paths: str) -> Callable: for path in paths: VIEWS.append([path, view]) - return View + return view return wrapper @@ -43,8 +44,14 @@ class View(AbstractView): return self._run_handler(handler).__await__() - async def _run_handler(self, handler: Coroutine) -> Response: - return await handler(self.request, **self.request.match_info) + @classmethod + async def run(cls: type[Self], method: str, request: Request, **kwargs: Any) -> Self: + view = cls(request) + return await view.handlers[method](request, **kwargs) + + + async def _run_handler(self, handler: Coroutine, **kwargs: Any) -> Response: + return await handler(self.request, **self.request.match_info, **kwargs) @cached_property diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 616017c..3f90234 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -186,18 +186,85 @@ class AdminInstancesDelete(View): @register_route('/admin/whitelist') class AdminWhitelist(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-whitelist.haml', self) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'domains': tuple(conn.execute('SELECT * FROM whitelist').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-whitelist.haml', self, **context) return Response.new(data, ctype = 'html') @register_route('/admin/domain_bans') class AdminDomainBans(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-domain_bans.haml', self) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-domain_bans.haml', self, **context) return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + data = await request.post() + print(data) + + if not data['domain']: + return await self.get(request, error = 'Missing domain') + + with self.database.session(True) as conn: + if (ban := conn.get_domain_ban(data['domain'])): + conn.update_domain_ban( + data['domain'], + data.get('reason'), + data.get('note') + ) + + else: + conn.put_domain_ban( + data['domain'], + data.get('reason'), + data.get('note') + ) + + return await self.get(request, message = "Added/updated domain ban") + + +@register_route('/admin/domain_bans/delete/{domain}') +class AdminDomainBansDelete(View): + async def get(self, request: Request, domain: str) -> Response: + with self.database.session() as conn: + if not (conn.get_domain_ban(domain)): + return await AdminDomainBans.run("GET", request, message = 'Domain ban not found') + + conn.del_domain_ban(domain) + + return await AdminDomainBans.run("GET", request, message = 'Unbanned domain') + + @register_route('/admin/software_bans') class AdminSoftwareBans(View): async def get(self, request: Request) -> Response: From 4639d8a78d5f4c16c8ab285921d027a21733aacc Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sun, 3 Mar 2024 18:28:49 -0500 Subject: [PATCH 18/31] minor frontend tweaks --- relay/frontend/base.haml | 9 ++++++-- relay/frontend/page/admin-domain_bans.haml | 2 +- relay/frontend/page/admin-instances.haml | 5 ++++- relay/frontend/style.css | 25 +++++++++++++++++++--- relay/frontend/style/domain_bans.css | 8 ------- relay/frontend/style/instances.css | 8 ------- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 13b83e9..aa118bd 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -35,8 +35,13 @@ #container #header.section - %span#menu-open.button << ⁞ - %a(href="https://{{domain}}/") -> =config.name + %span#menu-open << ⁞ + %span.title-container + %a.title(href="/") -> =config.name + + -if view.request.path not in ["/", "/login"] + .page -> =page + .empty -if error diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index e2b3e5a..fc5010a 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -21,7 +21,7 @@ %thead %tr %td.domain << Instance - %td.date << Joined + %td << Joined %td.remove %tbody diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index b563383..c366acc 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -7,10 +7,13 @@ #add-instance %label(for="domain") << Domain %input(type="domain", id="domain" name="domain", placeholder="Domain") + %label(for="actor") << Actor URL %input(type="url", id="actor" name="actor", placeholder="Actor URL") + %label(for="inbox") << Inbox URL %input(type="url", id="inbox" name="inbox", placeholder="Inbox URL") + %label(for="software") << Software %input(name="software", id="software" placeholder="software") @@ -22,7 +25,7 @@ %tr %td.instance << Instance %td.software << Software - %td.date << Joined + %td << Joined %td.remove %tbody diff --git a/relay/frontend/style.css b/relay/frontend/style.css index 986426f..b489fb1 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -53,6 +53,7 @@ table { border: 1px solid var(--primary); border-radius: 5px; border-spacing: 0px; + width: 100%; } table tbody tr:nth-child(even) td { @@ -81,7 +82,8 @@ table td { table thead td { background-color: var(--primary); - color: var(--table-background) + color: var(--background); + text-align: center; } table tbody td { @@ -105,8 +107,17 @@ table tbody td { font-size: 2em; } -#header > *:nth-child(2) { +#header .title-container { + text-align: center; +} + +#header .title { font-weight: bold; + line-height: 0.75em; +} + +#header .page { + font-size: 0.5em; } #menu { @@ -153,11 +164,19 @@ table tbody td { color: var(--primary); } +#menu-open { + color: var(--primary); +} + +#menu-open:hover { + color: var(--primary-hover); +} + #menu-open, #menu-close { cursor: pointer; } -#menu-close, #menu-open { +#menu-open, #menu-close { min-width: 35px; text-align: center; } diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css index 437ab98..af4705a 100644 --- a/relay/frontend/style/domain_bans.css +++ b/relay/frontend/style/domain_bans.css @@ -14,10 +14,6 @@ table .items { margin-top: var(--spacing); } -#domains table { - width: 100%; -} - #domains .domain { width: 100%; } @@ -27,10 +23,6 @@ table .items { text-align: right; } -#domains thead td { - text-align: center !important; -} - #add-domain { display: grid; grid-template-columns: max-content auto; diff --git a/relay/frontend/style/instances.css b/relay/frontend/style/instances.css index 8e8789b..256ca12 100644 --- a/relay/frontend/style/instances.css +++ b/relay/frontend/style/instances.css @@ -3,10 +3,6 @@ form input[type="submit"] { margin: 0 auto; } -#instances table { - width: 100%; -} - #instances .instance { width: 100%; } @@ -20,10 +16,6 @@ form input[type="submit"] { text-align: right; } -#instances thead td { - text-align: center !important; -} - #add-instance { display: grid; grid-template-columns: max-content auto; From d7cfa1214551190d195974a30c7fde6398c1ea8e Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 00:32:13 -0500 Subject: [PATCH 19/31] add `Cache-Control` header --- relay/application.py | 35 +++++++++++++++-------------------- relay/dev.py | 2 +- relay/manage.py | 4 +++- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/relay/application.py b/relay/application.py index 6fc09ae..c834598 100644 --- a/relay/application.py +++ b/relay/application.py @@ -28,7 +28,7 @@ from .views.frontend import handle_frontend_path if typing.TYPE_CHECKING: from tinysql import Database, Row from .cache import Cache - from .misc import Message + from .misc import Message, Response # pylint: disable=unsubscriptable-object @@ -36,11 +36,12 @@ if typing.TYPE_CHECKING: class Application(web.Application): DEFAULT: Application = None - def __init__(self, cfgpath: str | None): + def __init__(self, cfgpath: str | None, dev: bool = False): web.Application.__init__(self, middlewares = [ handle_api_path, - handle_frontend_path + handle_frontend_path, + handle_response_headers ] ) @@ -50,6 +51,7 @@ class Application(web.Application): self['signer'] = None self['start_time'] = None self['cleanup_thread'] = None + self['dev'] = dev self['config'] = Config(cfgpath, load = True) self['database'] = get_database(self.config) @@ -255,25 +257,18 @@ class PushWorker(multiprocessing.Process): await client.close() +@web.middleware +async def handle_response_headers(request: web.Request, handler: Coroutine) -> Response: + resp = await handler(request) + resp.headers['Server'] = 'ActivityRelay' -async def handle_access_log(request: web.Request, response: web.Response) -> None: - address = request.headers.get( - 'X-Forwarded-For', - request.headers.get( - 'X-Real-Ip', - request.remote - ) - ) + if request.app['dev'] and request.path.endswith(('.css', '.js')): + resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' - logging.info( - '%s "%s %s" %i %i "%s"', - address, - request.method, - request.path, - response.status, - response.content_length or 0, - request.headers.get('User-Agent', 'n/a') - ) + else: + resp.headers['Cache-Control'] = 'no-store' + + return resp async def handle_cleanup(app: Application) -> None: diff --git a/relay/dev.py b/relay/dev.py index 443a0d0..c37508e 100644 --- a/relay/dev.py +++ b/relay/dev.py @@ -94,7 +94,7 @@ def cli_run(): class WatchHandler(PatternMatchingEventHandler): patterns = ['*.py'] - cmd = [sys.executable, '-m', 'relay', 'run'] + cmd = [sys.executable, '-m', 'relay', 'run', '-d'] def __init__(self): diff --git a/relay/manage.py b/relay/manage.py index 9fe0b17..63fc6db 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -188,8 +188,9 @@ def cli_setup(ctx: click.Context) -> None: @cli.command('run') +@click.option('--dev', '-d', is_flag=True, help='Enable developer mode') @click.pass_context -def cli_run(ctx: click.Context) -> None: +def cli_run(ctx: click.Context, dev: bool = False) -> None: 'Run the relay' if ctx.obj.config.domain.endswith('example.com') or not ctx.obj.signer: @@ -216,6 +217,7 @@ def cli_run(ctx: click.Context) -> None: click.echo(pip_command) return + ctx.obj['dev'] = True ctx.obj.run() # todo: figure out why the relay doesn't quit properly without this From a163f2baab73e0060e4a926190f3a60f1c1ede30 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 00:43:12 -0500 Subject: [PATCH 20/31] create admin software bans page --- relay/frontend/page/admin-domain_bans.haml | 2 +- relay/frontend/page/admin-software_bans.haml | 47 +++++++++++++++- relay/frontend/style/software_bans.css | 32 +++++++++++ relay/views/frontend.py | 56 ++++++++++++++++++-- 4 files changed, 131 insertions(+), 6 deletions(-) diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index fc5010a..ddf1947 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -21,7 +21,7 @@ %thead %tr %td.domain << Instance - %td << Joined + %td << Date %td.remove %tbody diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml index c215fc8..efe6f12 100644 --- a/relay/frontend/page/admin-software_bans.haml +++ b/relay/frontend/page/admin-software_bans.haml @@ -1,5 +1,48 @@ -extends "base.haml" -set page="Software Bans" -block content - .section - UvU + %details.section + %summary << Ban Software + %form(action="/admin/software_bans", method="POST") + #add-software + %label(for="name") << Name + %input(id="name" name="name" placeholder="Name") + + %label(for="reason") << Ban Reason + %textarea(id="reason" name="reason") << {{""}} + + %label(for="note") << Admin Note + %textarea(id="note" name="note") << {{""}} + + %input(type="submit" value="Ban Software") + + #software.section + %table + %thead + %tr + %td.name << Instance + %td << Date + %td.remove + + %tbody + -for ban in bans + %tr + %td.name + %details + %summary -> =ban.name + %form(action="/admin/software_bans" method="POST") + .items + .reason << Reason + %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} + + .note << Note + %textarea.note(id="note" name="note") << {{ban.note or ""}} + + %input(type="hidden" name="name", value="{{ban.name}}") + %input(type="submit" value="Update") + + %td.date + =ban.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/software_bans/delete/{{ban.name}}" title="Unban software") << ✖ diff --git a/relay/frontend/style/software_bans.css b/relay/frontend/style/software_bans.css index e69de29..c3109b1 100644 --- a/relay/frontend/style/software_bans.css +++ b/relay/frontend/style/software_bans.css @@ -0,0 +1,32 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +textarea { + height: calc(5em); +} + +table .items { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); +} + +#software .name{ + width: 100%; +} + +#software .date { + width: max-content; + text-align: right; +} + +#add-software { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 3f90234..089751d 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -230,7 +230,6 @@ class AdminDomainBans(View): async def post(self, request: Request) -> Response: data = await request.post() - print(data) if not data['domain']: return await self.get(request, error = 'Missing domain') @@ -267,11 +266,62 @@ class AdminDomainBansDelete(View): @register_route('/admin/software_bans') class AdminSoftwareBans(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-software_bans.haml', self) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-software_bans.haml', self, **context) return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + data = await request.post() + + if not data['name']: + return await self.get(request, error = 'Missing name') + + with self.database.session(True) as conn: + if (ban := conn.get_software_ban(data['name'])): + conn.update_software_ban( + data['name'], + data.get('reason'), + data.get('note') + ) + + else: + conn.put_software_ban( + data['name'], + data.get('reason'), + data.get('note') + ) + + return await self.get(request, message = "Added/updated software ban") + + +@register_route('/admin/software_bans/delete/{name}') +class AdminSoftwareBansDelete(View): + async def get(self, request: Request, name: str) -> Response: + with self.database.session() as conn: + if not (conn.get_software_ban(name)): + return await AdminSoftwareBans.run("GET", request, message = 'Software ban not found') + + conn.del_software_ban(name) + + return await AdminSoftwareBans.run("GET", request, message = 'Unbanned software') + + @register_route('/admin/config') class AdminConfig(View): async def get(self, request: Request, message: str | None = None) -> Response: From 5dcf3752474a822cf0efdf246ef3862c3c628457 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 01:00:57 -0500 Subject: [PATCH 21/31] create admin whitelist page --- relay/frontend/page/admin-instances.haml | 2 +- relay/frontend/page/admin-whitelist.haml | 30 ++++++++++++++++++++++-- relay/frontend/style/whitelist.css | 21 +++++++++++++++++ relay/views/frontend.py | 30 +++++++++++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index c366acc..ae26d7a 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -25,7 +25,7 @@ %tr %td.instance << Instance %td.software << Software - %td << Joined + %td.date << Joined %td.remove %tbody diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml index 083c8bb..be31f0d 100644 --- a/relay/frontend/page/admin-whitelist.haml +++ b/relay/frontend/page/admin-whitelist.haml @@ -1,5 +1,31 @@ -extends "base.haml" -set page="Whitelist" -block content - .section - UvU + %details.section + %summary << Add Domain + %form(target="/admin/whitelist", method="POST") + #add-domain + %label(for="domain") << Domain + %input(type="domain", id="domain" name="domain", placeholder="Domain") + + %input(type="submit" value="Add Domain") + + #whitelist.section + %table + %thead + %tr + %td.domain << Domain + %td.date << Added + %td.remove + + %tbody + -for item in whitelist + %tr + %td.domain + =item.domain + + %td.date + =item.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/whitelist/delete/{{item.domain}}" title="Remove whitlisted domain") << ✖ diff --git a/relay/frontend/style/whitelist.css b/relay/frontend/style/whitelist.css index e69de29..6d3566a 100644 --- a/relay/frontend/style/whitelist.css +++ b/relay/frontend/style/whitelist.css @@ -0,0 +1,21 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +#whitelist .domain{ + width: 100%; +} + +#whitelist .date { + width: max-content; + text-align: right; +} + +#add-domain { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 089751d..550528c 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -193,7 +193,7 @@ class AdminWhitelist(View): with self.database.session() as conn: context = { - 'domains': tuple(conn.execute('SELECT * FROM whitelist').all()) + 'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all()) } if error: @@ -206,6 +206,34 @@ class AdminWhitelist(View): return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + data = await request.post() + + if not data['domain']: + return await self.get(request, error = 'Missing domain') + + with self.database.session(True) as conn: + if (ban := conn.get_domain_whitelist(data['domain'])): + return await self.get(request, message = "Domain already in whitelist") + + conn.put_domain_whitelist(data['domain']) + + return await self.get(request, message = "Added/updated domain ban") + + +@register_route('/admin/whitelist/delete/{domain}') +class AdminWhitlistDelete(View): + async def get(self, request: Request, domain: str) -> Response: + with self.database.session() as conn: + if not (conn.get_domain_whitelist(domain)): + msg = 'Whitelisted domain not found' + return await AdminWhitelist.run("GET", request, message = msg) + + conn.del_domain_whitelist(domain) + + return await AdminWhitelist.run("GET", request, message = 'Removed domain from whitelist') + + @register_route('/admin/domain_bans') class AdminDomainBans(View): async def get(self, From d44498b966c1edac2ba54ec3db2c1c3b36be647d Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 01:09:40 -0500 Subject: [PATCH 22/31] fix dev mode --- relay/application.py | 2 +- relay/manage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/relay/application.py b/relay/application.py index c834598..ea65992 100644 --- a/relay/application.py +++ b/relay/application.py @@ -262,7 +262,7 @@ async def handle_response_headers(request: web.Request, handler: Coroutine) -> R resp = await handler(request) resp.headers['Server'] = 'ActivityRelay' - if request.app['dev'] and request.path.endswith(('.css', '.js')): + if not request.app['dev'] and request.path.endswith(('.css', '.js')): resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' else: diff --git a/relay/manage.py b/relay/manage.py index 63fc6db..796ec0b 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -217,7 +217,7 @@ def cli_run(ctx: click.Context, dev: bool = False) -> None: click.echo(pip_command) return - ctx.obj['dev'] = True + ctx.obj['dev'] = dev ctx.obj.run() # todo: figure out why the relay doesn't quit properly without this From 2223695d152056ea570fb49a7423b45818a8d003 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 01:14:17 -0500 Subject: [PATCH 23/31] remove commas in element attributes --- relay/frontend/page/admin-domain_bans.haml | 4 ++-- relay/frontend/page/admin-instances.haml | 10 +++++----- relay/frontend/page/admin-software_bans.haml | 4 ++-- relay/frontend/page/admin-whitelist.haml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index ddf1947..982c00d 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -3,7 +3,7 @@ -block content %details.section %summary << Ban Domain - %form(action="/admin/domain_bans", method="POST") + %form(action="/admin/domain_bans" method="POST") #add-domain %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") @@ -38,7 +38,7 @@ .note << Note %textarea.note(id="note" name="note") << {{ban.note or ""}} - %input(type="hidden" name="domain", value="{{ban.domain}}") + %input(type="hidden" name="domain" value="{{ban.domain}}") %input(type="submit" value="Update") %td.date diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index ae26d7a..3cadc93 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -3,19 +3,19 @@ -block content %details.section %summary << Add Instance - %form(target="/admin/instances", method="POST") + %form(target="/admin/instances" method="POST") #add-instance %label(for="domain") << Domain - %input(type="domain", id="domain" name="domain", placeholder="Domain") + %input(type="domain" id="domain" name="domain" placeholder="Domain") %label(for="actor") << Actor URL - %input(type="url", id="actor" name="actor", placeholder="Actor URL") + %input(type="url" id="actor" name="actor" placeholder="Actor URL") %label(for="inbox") << Inbox URL - %input(type="url", id="inbox" name="inbox", placeholder="Inbox URL") + %input(type="url" id="inbox" name="inbox" placeholder="Inbox URL") %label(for="software") << Software - %input(name="software", id="software" placeholder="software") + %input(name="software" id="software" placeholder="software") %input(type="submit" value="Add Instance") diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml index efe6f12..b03e1a7 100644 --- a/relay/frontend/page/admin-software_bans.haml +++ b/relay/frontend/page/admin-software_bans.haml @@ -3,7 +3,7 @@ -block content %details.section %summary << Ban Software - %form(action="/admin/software_bans", method="POST") + %form(action="/admin/software_bans" method="POST") #add-software %label(for="name") << Name %input(id="name" name="name" placeholder="Name") @@ -38,7 +38,7 @@ .note << Note %textarea.note(id="note" name="note") << {{ban.note or ""}} - %input(type="hidden" name="name", value="{{ban.name}}") + %input(type="hidden" name="name" value="{{ban.name}}") %input(type="submit" value="Update") %td.date diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml index be31f0d..2fba308 100644 --- a/relay/frontend/page/admin-whitelist.haml +++ b/relay/frontend/page/admin-whitelist.haml @@ -3,10 +3,10 @@ -block content %details.section %summary << Add Domain - %form(target="/admin/whitelist", method="POST") + %form(target="/admin/whitelist" method="POST") #add-domain %label(for="domain") << Domain - %input(type="domain", id="domain" name="domain", placeholder="Domain") + %input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="submit" value="Add Domain") From b73fdece9543be43415ff4e2a7aacd179e039d25 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 01:40:51 -0500 Subject: [PATCH 24/31] create admin users page --- relay/frontend/base.haml | 1 + relay/frontend/page/admin-users.haml | 44 +++++++++++++++++++++++ relay/frontend/style/users.css | 25 +++++++++++++ relay/views/frontend.py | 54 ++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 relay/frontend/page/admin-users.haml create mode 100644 relay/frontend/style/users.css diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index aa118bd..7de72ce 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -27,6 +27,7 @@ {{menu_item("Whitelist", "/admin/whitelist")}} {{menu_item("Domain Bans", "/admin/domain_bans")}} {{menu_item("Software Bans", "/admin/software_bans")}} + {{menu_item("Users", "/admin/users")}} {{menu_item("Config", "/admin/config")}} {{menu_item("Logout", "/logout")}} diff --git a/relay/frontend/page/admin-users.haml b/relay/frontend/page/admin-users.haml new file mode 100644 index 0000000..aff5ee7 --- /dev/null +++ b/relay/frontend/page/admin-users.haml @@ -0,0 +1,44 @@ +-extends "base.haml" +-set page="Users" +-block content + %details.section + %summary << Add User + %form(target="/admin/users", method="POST") + #add-user + %label(for="username") << Username + %input(id="username" name="username" placeholder="Username") + + %label(for="password") << Password + %input(type="password" id="password" name="password" placeholder="Password") + + %label(for="password2") << Password Again + %input(type="password" id="password2" name="password2" placeholder="Password Again") + + %label(for="handle") << Handle + %input(type="email" name="handle" id="handle" placeholder="handle") + + %input(type="submit" value="Add User") + + #users.section + %table + %thead + %tr + %td.username << Username + %td.handle << Handle + %td.date << Joined + %td.remove + + %tbody + -for user in users + %tr + %td.username + =user.username + + %td.handle + =user.handle or "n/a" + + %td.date + =user.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/users/delete/{{user.username}}" title="Remove User") << ✖ diff --git a/relay/frontend/style/users.css b/relay/frontend/style/users.css new file mode 100644 index 0000000..c7fe20a --- /dev/null +++ b/relay/frontend/style/users.css @@ -0,0 +1,25 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +#users .username { + width: 100%; +} + +#users .handle { + text-align: center; +} + +#users .date { + width: max-content; + text-align: right; +} + +#add-user { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 550528c..20c3ddb 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -350,6 +350,60 @@ class AdminSoftwareBansDelete(View): return await AdminSoftwareBans.run("GET", request, message = 'Unbanned software') +@register_route('/admin/users') +class AdminUsers(View): + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'users': tuple(conn.execute('SELECT * FROM users').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-users.haml', self, **context) + return Response.new(data, ctype = 'html') + + + async def post(self, request: Request) -> Response: + data = await request.post() + required_fields = {'username', 'password', 'password2'} + print(data) + + if not all(map(data.get, required_fields)): + return await self.get(request, error = 'Missing username and/or password') + + if data['password'] != data['password2']: + return await self.get(request, error = 'Passwords do not match') + + with self.database.session(True) as conn: + if conn.get_user(data['username']): + return await self.get(request, message = "User already exists") + + conn.put_user(data['username'], data['password'], data['handle']) + + return await self.get(request, message = "Added user") + + +@register_route('/admin/users/delete/{name}') +class AdminUsersDelete(View): + async def get(self, request: Request, name: str) -> Response: + with self.database.session() as conn: + if not (conn.get_user(name)): + return await AdminUsers.run("GET", request, message = 'User not found') + + conn.del_user(name) + + return await AdminUsers.run("GET", request, message = 'User deleted') + + @register_route('/admin/config') class AdminConfig(View): async def get(self, request: Request, message: str | None = None) -> Response: From 33102f9e4eada028d6e299769e7ed86fdda47975 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 01:42:27 -0500 Subject: [PATCH 25/31] disable caching for now --- relay/application.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/relay/application.py b/relay/application.py index ea65992..d463244 100644 --- a/relay/application.py +++ b/relay/application.py @@ -262,11 +262,11 @@ async def handle_response_headers(request: web.Request, handler: Coroutine) -> R resp = await handler(request) resp.headers['Server'] = 'ActivityRelay' - if not request.app['dev'] and request.path.endswith(('.css', '.js')): - resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' - - else: - resp.headers['Cache-Control'] = 'no-store' +# if not request.app['dev'] and request.path.endswith(('.css', '.js')): +# resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' +# +# else: +# resp.headers['Cache-Control'] = 'no-store' return resp From 4cbf83a7b7e03f886030a11ad0a9c33bac2696e0 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 02:19:26 -0500 Subject: [PATCH 26/31] simplify css --- relay/dev.py | 1 - relay/frontend/base.haml | 4 +- relay/frontend/page/admin-config.haml | 2 +- relay/frontend/page/admin-domain_bans.haml | 6 +- relay/frontend/page/admin-instances.haml | 4 +- relay/frontend/page/admin-software_bans.haml | 6 +- relay/frontend/page/admin-users.haml | 4 +- relay/frontend/page/admin-whitelist.haml | 4 +- relay/frontend/page/home.haml | 9 ++- relay/frontend/page/login.haml | 11 ++-- relay/frontend/style.css | 63 ++++++++++++++++---- relay/frontend/style/config.css | 20 ------- relay/frontend/style/domain_bans.css | 32 ---------- relay/frontend/style/home.css | 16 ----- relay/frontend/style/instances.css | 25 -------- relay/frontend/style/login.css | 18 ------ relay/frontend/style/software_bans.css | 32 ---------- relay/frontend/style/users.css | 25 -------- relay/frontend/style/whitelist.css | 21 ------- relay/views/frontend.py | 2 +- 20 files changed, 81 insertions(+), 224 deletions(-) delete mode 100644 relay/frontend/style/config.css delete mode 100644 relay/frontend/style/domain_bans.css delete mode 100644 relay/frontend/style/home.css delete mode 100644 relay/frontend/style/instances.css delete mode 100644 relay/frontend/style/login.css delete mode 100644 relay/frontend/style/software_bans.css delete mode 100644 relay/frontend/style/users.css delete mode 100644 relay/frontend/style/whitelist.css diff --git a/relay/dev.py b/relay/dev.py index c37508e..5f06f3c 100644 --- a/relay/dev.py +++ b/relay/dev.py @@ -145,7 +145,6 @@ class WatchHandler(PatternMatchingEventHandler): if event.event_type not in ['modified', 'created', 'deleted']: return - print(event.src_path) self.run_proc(restart = True) diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 7de72ce..c74feda 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -11,7 +11,7 @@ %title << {{config.name}}: {{page}} %meta(charset="UTF-8") %meta(name="viewport" content="width=device-width, initial-scale=1") - %link(rel="stylesheet" type="text/css" href="/style.css?page={{page}}") + %link(rel="stylesheet" type="text/css" href="/style.css") -block head %body @@ -51,7 +51,7 @@ -if message .message.section -> =message - #content + #content(class="page-{{page.lower().replace(' ', '_')}}") -block content #footer.section diff --git a/relay/frontend/page/admin-config.haml b/relay/frontend/page/admin-config.haml index bf3d8b8..4028eb1 100644 --- a/relay/frontend/page/admin-config.haml +++ b/relay/frontend/page/admin-config.haml @@ -2,7 +2,7 @@ -set page="Config" -block content %form.section(action="/admin/config" method="POST") - #config-options + .grid-2col %label(for="name") << Name %input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}") diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index 982c00d..fbee683 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -4,7 +4,7 @@ %details.section %summary << Ban Domain %form(action="/admin/domain_bans" method="POST") - #add-domain + #add-item %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") @@ -16,7 +16,7 @@ %input(type="submit" value="Ban Domain") - #domains.section + #data-table.section %table %thead %tr @@ -31,7 +31,7 @@ %details %summary -> =ban.domain %form(action="/admin/domain_bans" method="POST") - .items + .grid-2col .reason << Reason %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index 3cadc93..70aa523 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -4,7 +4,7 @@ %details.section %summary << Add Instance %form(target="/admin/instances" method="POST") - #add-instance + #add-item %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") @@ -19,7 +19,7 @@ %input(type="submit" value="Add Instance") - #instances.section + #data-table.section %table %thead %tr diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml index b03e1a7..9490405 100644 --- a/relay/frontend/page/admin-software_bans.haml +++ b/relay/frontend/page/admin-software_bans.haml @@ -4,7 +4,7 @@ %details.section %summary << Ban Software %form(action="/admin/software_bans" method="POST") - #add-software + #add-item %label(for="name") << Name %input(id="name" name="name" placeholder="Name") @@ -16,7 +16,7 @@ %input(type="submit" value="Ban Software") - #software.section + #data-table.section %table %thead %tr @@ -31,7 +31,7 @@ %details %summary -> =ban.name %form(action="/admin/software_bans" method="POST") - .items + .grid-2col .reason << Reason %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} diff --git a/relay/frontend/page/admin-users.haml b/relay/frontend/page/admin-users.haml index aff5ee7..ccc445c 100644 --- a/relay/frontend/page/admin-users.haml +++ b/relay/frontend/page/admin-users.haml @@ -4,7 +4,7 @@ %details.section %summary << Add User %form(target="/admin/users", method="POST") - #add-user + #add-item %label(for="username") << Username %input(id="username" name="username" placeholder="Username") @@ -19,7 +19,7 @@ %input(type="submit" value="Add User") - #users.section + #data-table.section %table %thead %tr diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml index 2fba308..b902cd3 100644 --- a/relay/frontend/page/admin-whitelist.haml +++ b/relay/frontend/page/admin-whitelist.haml @@ -4,13 +4,13 @@ %details.section %summary << Add Domain %form(target="/admin/whitelist" method="POST") - #add-domain + #add-item %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="submit" value="Add Domain") - #whitelist.section + #data-table.section %table %thead %tr diff --git a/relay/frontend/page/home.haml b/relay/frontend/page/home.haml index f64b489..7f09644 100644 --- a/relay/frontend/page/home.haml +++ b/relay/frontend/page/home.haml @@ -19,7 +19,7 @@ Note: The whitelist is enabled on this instance. Ask the admin to add your instance before joining. - #instances.section + #data-table.section %table %thead %tr @@ -29,5 +29,8 @@ %tbody -for instance in instances %tr - %td.instance -> %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain - %td.date -> =instance.created.strftime("%Y-%m-%d") + %td.instance -> %a(href="https://{{instance.domain}}/" target="_new") + =instance.domain + + %td.date + =instance.created.strftime("%Y-%m-%d") diff --git a/relay/frontend/page/login.haml b/relay/frontend/page/login.haml index ee7d1dc..5f2fed4 100644 --- a/relay/frontend/page/login.haml +++ b/relay/frontend/page/login.haml @@ -2,8 +2,11 @@ -set page="Login" -block content %form.section(action="/login" method="post") - %label(for="username") << Username - %input(id="username" name="username" placeholder="Username" value="{{username or ''}}") - %label(for="password") << Password - %input(id="password" name="password" placeholder="Password" type="password") + .grid-2col + %label(for="username") << Username + %input(id="username" name="username" placeholder="Username" value="{{username or ''}}") + + %label(for="password") << Password + %input(id="password" name="password" placeholder="Password" type="password") + %input(type="submit" value="Login") diff --git a/relay/frontend/style.css b/relay/frontend/style.css index b489fb1..16fc5b4 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -15,13 +15,6 @@ --spacing: 10px; } -body { - color: var(--text); - background-color: #222; - margin: var(--spacing); - font-family: sans serif; -} - a { color: var(--primary); text-decoration: none; @@ -32,10 +25,26 @@ a:hover { text-decoration: underline; } +body { + color: var(--text); + background-color: #222; + margin: var(--spacing); + font-family: sans serif; +} + +details *:nth-child(2) { + margin-top: 5px; +} + details summary { cursor: pointer; } +form input[type="submit"] { + display: block; + margin: 0 auto; +} + p { line-height: 1em; margin: 0px; @@ -90,6 +99,10 @@ table tbody td { background-color: var(--table-background); } +textarea { + height: calc(5em); +} + #container { width: 1024px; margin: 0px auto; @@ -190,6 +203,23 @@ table tbody td { text-align: right } +#add-item { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); + align-items: center; +} + +#data-table td:first-child { + width: 100%; +} + +#data-table .date { + width: max-content; + text-align: right; +} .button { background-color: var(--primary); @@ -219,6 +249,15 @@ table tbody td { border: 1px solid var(--error-border) !important; } +.grid-2col { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-bottom: var(--spacing); + align-items: center; + +} + .message { color: var(--message-text) !important; background-color: var(--message-background) !important; @@ -241,9 +280,10 @@ table tbody td { } -{% if page %} - {% include "style/" + page.lower().replace(" ", "_") + ".css" %} -{% endif %} +/* config */ +#content.page-config input[type="checkbox"] { + justify-self: left; +} @media (max-width: 1026px) { @@ -273,7 +313,8 @@ table tbody td { } .section { - border-width: 0px; + border-left-width: 0px; + border-right-width: 0px; border-radius: 0px; } } diff --git a/relay/frontend/style/config.css b/relay/frontend/style/config.css deleted file mode 100644 index 0dff253..0000000 --- a/relay/frontend/style/config.css +++ /dev/null @@ -1,20 +0,0 @@ -#config-options { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-bottom: var(--spacing); - align-items: center; -} - -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -form input[type="checkbox"] { - justify-self: left; -} - -textarea { - height: 4em; -} diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css deleted file mode 100644 index af4705a..0000000 --- a/relay/frontend/style/domain_bans.css +++ /dev/null @@ -1,32 +0,0 @@ -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -textarea { - height: calc(5em); -} - -table .items { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); -} - -#domains .domain { - width: 100%; -} - -#domains .date { - width: max-content; - text-align: right; -} - -#add-domain { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); - margin-bottom: var(--spacing); -} diff --git a/relay/frontend/style/home.css b/relay/frontend/style/home.css deleted file mode 100644 index 4cdfaa9..0000000 --- a/relay/frontend/style/home.css +++ /dev/null @@ -1,16 +0,0 @@ -#instances table { - width: 100%; -} - -#instances .instance { - width: 100%; -} - -#instances .date { - width: max-content; - text-align: right; -} - -#instances thead td { - text-align: center !important; -} diff --git a/relay/frontend/style/instances.css b/relay/frontend/style/instances.css deleted file mode 100644 index 256ca12..0000000 --- a/relay/frontend/style/instances.css +++ /dev/null @@ -1,25 +0,0 @@ -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -#instances .instance { - width: 100%; -} - -#instances .software { - text-align: center; -} - -#instances .date { - width: max-content; - text-align: right; -} - -#add-instance { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); - margin-bottom: var(--spacing); -} diff --git a/relay/frontend/style/login.css b/relay/frontend/style/login.css deleted file mode 100644 index 48f623b..0000000 --- a/relay/frontend/style/login.css +++ /dev/null @@ -1,18 +0,0 @@ -label, input { - margin: 0 auto; - display: block; -} - -label, input:not([type="submit"]) { - width: 50%; -} - -input:not([type="submit"]) { - margin-bottom: var(--spacing); -} - -@media (max-width: 1026px) { - label, input:not([type="submit"]) { - width: 75%; - } -} diff --git a/relay/frontend/style/software_bans.css b/relay/frontend/style/software_bans.css deleted file mode 100644 index c3109b1..0000000 --- a/relay/frontend/style/software_bans.css +++ /dev/null @@ -1,32 +0,0 @@ -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -textarea { - height: calc(5em); -} - -table .items { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); -} - -#software .name{ - width: 100%; -} - -#software .date { - width: max-content; - text-align: right; -} - -#add-software { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); - margin-bottom: var(--spacing); -} diff --git a/relay/frontend/style/users.css b/relay/frontend/style/users.css deleted file mode 100644 index c7fe20a..0000000 --- a/relay/frontend/style/users.css +++ /dev/null @@ -1,25 +0,0 @@ -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -#users .username { - width: 100%; -} - -#users .handle { - text-align: center; -} - -#users .date { - width: max-content; - text-align: right; -} - -#add-user { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); - margin-bottom: var(--spacing); -} diff --git a/relay/frontend/style/whitelist.css b/relay/frontend/style/whitelist.css deleted file mode 100644 index 6d3566a..0000000 --- a/relay/frontend/style/whitelist.css +++ /dev/null @@ -1,21 +0,0 @@ -form input[type="submit"] { - display: block; - margin: 0 auto; -} - -#whitelist .domain{ - width: 100%; -} - -#whitelist .date { - width: max-content; - text-align: right; -} - -#add-domain { - display: grid; - grid-template-columns: max-content auto; - grid-gap: var(--spacing); - margin-top: var(--spacing); - margin-bottom: var(--spacing); -} diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 20c3ddb..8710413 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -440,5 +440,5 @@ class AdminConfig(View): @register_route('/style.css') class StyleCss(View): async def get(self, request: Request) -> Response: - data = self.template.render('style.css', self, page = request.query.getone('page', "")) + data = self.template.render('style.css', self) return Response.new(data, ctype = 'css') From 5f156401c7453c54b43e195745715eca71266d57 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 02:30:54 -0500 Subject: [PATCH 27/31] fix linter warnings --- relay/application.py | 3 ++- relay/config.py | 6 +++++- relay/views/base.py | 3 ++- relay/views/frontend.py | 35 +++++++++++++++++++---------------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/relay/application.py b/relay/application.py index d463244..f3d9810 100644 --- a/relay/application.py +++ b/relay/application.py @@ -26,6 +26,7 @@ from .views.api import handle_api_path from .views.frontend import handle_frontend_path if typing.TYPE_CHECKING: + from collections.abc import Coroutine from tinysql import Database, Row from .cache import Cache from .misc import Message, Response @@ -264,7 +265,7 @@ async def handle_response_headers(request: web.Request, handler: Coroutine) -> R # if not request.app['dev'] and request.path.endswith(('.css', '.js')): # resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' -# + # else: # resp.headers['Cache-Control'] = 'no-store' diff --git a/relay/config.py b/relay/config.py index e61c99a..84faab1 100644 --- a/relay/config.py +++ b/relay/config.py @@ -52,7 +52,11 @@ if IS_DOCKER: class Config: def __init__(self, path: str, load: bool = False): - self.path = Config.get_config_dir() + if path: + self.path = Path(path).expanduser().resolve() + + else: + self.path = Config.get_config_dir() self.listen = None self.port = None diff --git a/relay/views/base.py b/relay/views/base.py index 65859a5..f568525 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -11,9 +11,10 @@ from json.decoder import JSONDecodeError from ..misc import Response if typing.TYPE_CHECKING: + from aiohttp.web import Request from collections.abc import Callable, Coroutine, Generator from bsql import Database - from typing import Self + from typing import Any, Self from ..application import Application from ..cache import Cache from ..config import Config diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 8710413..9df5e45 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -4,6 +4,7 @@ import typing from aiohttp import web from argon2.exceptions import VerifyMismatchError +from urllib.parse import urlparse from .base import View, register_route @@ -13,8 +14,11 @@ from ..misc import ACTOR_FORMATS, Message, Response if typing.TYPE_CHECKING: from aiohttp.web import Request + from collections.abc import Coroutine +# pylint: disable=no-self-use + UNAUTH_ROUTES = { '/', '/login' @@ -147,22 +151,22 @@ class AdminInstances(View): async def post(self, request: Request) -> Response: - data = {key: value for key, value in (await request.post()).items()} + data = await request.post() - if not data['actor'] and not data['domain']: + if not data.get('actor') and not data.get('domain'): return await self.get(request, error = 'Missing actor and/or domain') - if not data['domain']: + if not data.get('domain'): data['domain'] = urlparse(data['actor']).netloc - if not data['software']: + if not data.get('software'): nodeinfo = await self.client.fetch_nodeinfo(data['domain']) data['software'] = nodeinfo.sw_name - if not data['actor'] and data['software'] in ACTOR_FORMATS: + if not data.get('actor') and data['software'] in ACTOR_FORMATS: data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain']) - if not data['inbox'] and data['actor']: + if not data.get('inbox') and data['actor']: actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse) data['inbox'] = actor.shared_inbox @@ -176,7 +180,7 @@ class AdminInstances(View): class AdminInstancesDelete(View): async def get(self, request: Request, domain: str) -> Response: with self.database.session() as conn: - if not (conn.get_inbox(domain)): + if not conn.get_inbox(domain): return await AdminInstances(request).get(request, message = 'Instance not found') conn.del_inbox(domain) @@ -213,7 +217,7 @@ class AdminWhitelist(View): return await self.get(request, error = 'Missing domain') with self.database.session(True) as conn: - if (ban := conn.get_domain_whitelist(data['domain'])): + if conn.get_domain_whitelist(data['domain']): return await self.get(request, message = "Domain already in whitelist") conn.put_domain_whitelist(data['domain']) @@ -225,7 +229,7 @@ class AdminWhitelist(View): class AdminWhitlistDelete(View): async def get(self, request: Request, domain: str) -> Response: with self.database.session() as conn: - if not (conn.get_domain_whitelist(domain)): + if not conn.get_domain_whitelist(domain): msg = 'Whitelisted domain not found' return await AdminWhitelist.run("GET", request, message = msg) @@ -263,7 +267,7 @@ class AdminDomainBans(View): return await self.get(request, error = 'Missing domain') with self.database.session(True) as conn: - if (ban := conn.get_domain_ban(data['domain'])): + if conn.get_domain_ban(data['domain']): conn.update_domain_ban( data['domain'], data.get('reason'), @@ -284,7 +288,7 @@ class AdminDomainBans(View): class AdminDomainBansDelete(View): async def get(self, request: Request, domain: str) -> Response: with self.database.session() as conn: - if not (conn.get_domain_ban(domain)): + if not conn.get_domain_ban(domain): return await AdminDomainBans.run("GET", request, message = 'Domain ban not found') conn.del_domain_ban(domain) @@ -321,7 +325,7 @@ class AdminSoftwareBans(View): return await self.get(request, error = 'Missing name') with self.database.session(True) as conn: - if (ban := conn.get_software_ban(data['name'])): + if conn.get_software_ban(data['name']): conn.update_software_ban( data['name'], data.get('reason'), @@ -342,7 +346,7 @@ class AdminSoftwareBans(View): class AdminSoftwareBansDelete(View): async def get(self, request: Request, name: str) -> Response: with self.database.session() as conn: - if not (conn.get_software_ban(name)): + if not conn.get_software_ban(name): return await AdminSoftwareBans.run("GET", request, message = 'Software ban not found') conn.del_software_ban(name) @@ -375,9 +379,8 @@ class AdminUsers(View): async def post(self, request: Request) -> Response: data = await request.post() required_fields = {'username', 'password', 'password2'} - print(data) - if not all(map(data.get, required_fields)): + if not all(data.get(field) for field in required_fields): return await self.get(request, error = 'Missing username and/or password') if data['password'] != data['password2']: @@ -396,7 +399,7 @@ class AdminUsers(View): class AdminUsersDelete(View): async def get(self, request: Request, name: str) -> Response: with self.database.session() as conn: - if not (conn.get_user(name)): + if not conn.get_user(name): return await AdminUsers.run("GET", request, message = 'User not found') conn.del_user(name) From 01a491f2728bb3a42b18b814438e3775bdb2634e Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 02:32:57 -0500 Subject: [PATCH 28/31] replace `target` with `action` in forms --- relay/frontend/page/admin-instances.haml | 2 +- relay/frontend/page/admin-users.haml | 2 +- relay/frontend/page/admin-whitelist.haml | 2 +- relay/frontend/page/login.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index 70aa523..106e31d 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -3,7 +3,7 @@ -block content %details.section %summary << Add Instance - %form(target="/admin/instances" method="POST") + %form(action="/admin/instances" method="POST") #add-item %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") diff --git a/relay/frontend/page/admin-users.haml b/relay/frontend/page/admin-users.haml index ccc445c..65c268e 100644 --- a/relay/frontend/page/admin-users.haml +++ b/relay/frontend/page/admin-users.haml @@ -3,7 +3,7 @@ -block content %details.section %summary << Add User - %form(target="/admin/users", method="POST") + %form(action="/admin/users", method="POST") #add-item %label(for="username") << Username %input(id="username" name="username" placeholder="Username") diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml index b902cd3..b294552 100644 --- a/relay/frontend/page/admin-whitelist.haml +++ b/relay/frontend/page/admin-whitelist.haml @@ -3,7 +3,7 @@ -block content %details.section %summary << Add Domain - %form(target="/admin/whitelist" method="POST") + %form(action="/admin/whitelist" method="POST") #add-item %label(for="domain") << Domain %input(type="domain" id="domain" name="domain" placeholder="Domain") diff --git a/relay/frontend/page/login.haml b/relay/frontend/page/login.haml index 5f2fed4..1e08185 100644 --- a/relay/frontend/page/login.haml +++ b/relay/frontend/page/login.haml @@ -1,7 +1,7 @@ -extends "base.haml" -set page="Login" -block content - %form.section(action="/login" method="post") + %form.section(action="/login" method="POST") .grid-2col %label(for="username") << Username %input(id="username" name="username" placeholder="Username" value="{{username or ''}}") From 0af6a33b6952371436271f8d86f7c916e6f6da7b Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 02:34:28 -0500 Subject: [PATCH 29/31] use darker text color for active menu item --- relay/frontend/style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/relay/frontend/style.css b/relay/frontend/style.css index 16fc5b4..d258640 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -159,7 +159,6 @@ textarea { #menu > a[active="true"]:not(:hover) { background-color: var(--primary-hover); - color: var(--primary); border-color: transparent; } From a0ee22406bdfe9a604ac74d0f6732e03f30437e2 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 05:39:21 -0500 Subject: [PATCH 30/31] don't use spec file when building bin --- MANIFEST.in | 5 ----- relay.spec | 56 ----------------------------------------------- relay/__main__.py | 3 +++ relay/dev.py | 46 ++++++++++++++++++++++++-------------- setup.cfg | 5 +++-- 5 files changed, 35 insertions(+), 80 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 relay.spec diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d58d6d6..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include frontend/base.haml -include frontend/style.css -include data/statements.sql -include data/swagger.yaml -include frontend/page/home.haml diff --git a/relay.spec b/relay.spec deleted file mode 100644 index 5965b51..0000000 --- a/relay.spec +++ /dev/null @@ -1,56 +0,0 @@ -# -*- 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( - ['relay/__main__.py'], - pathex=[], - binaries=[], - datas=[ - ('relay/data', 'relay/data'), - ('relay/frontend', 'relay/frontend'), - (aiohttp_swagger_path, 'aiohttp_swagger') - ], - hiddenimports=[ - 'pg8000', - 'sqlite3' - ], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) - -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='activityrelay', - icon=None, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/relay/__main__.py b/relay/__main__.py index 8ed335a..d3d7c18 100644 --- a/relay/__main__.py +++ b/relay/__main__.py @@ -1,5 +1,8 @@ +import multiprocessing + from relay.manage import main if __name__ == '__main__': + multiprocessing.freeze_support() main() diff --git a/relay/dev.py b/relay/dev.py index 5f06f3c..6407068 100644 --- a/relay/dev.py +++ b/relay/dev.py @@ -1,10 +1,14 @@ import click +import platform import subprocess import sys import time from datetime import datetime from pathlib import Path +from tempfile import TemporaryDirectory + +from . import __version__ try: from watchdog.observers import Observer @@ -46,25 +50,33 @@ def cli_lint(path): subprocess.run([sys.executable, '-m', 'pylint', path], check = False) -@cli.command('manifest-gen') -def cli_manifest_install(): - paths = [] - - for path in SCRIPT.rglob('*'): - if path.suffix.lower() in IGNORE_EXT or not path.is_file(): - continue - - paths.append(path) - - with REPO.joinpath('MANIFEST.in').open('w', encoding = 'utf-8') as fd: - for path in paths: - fd.write(f'include {str(path.relative_to(SCRIPT))}\n') - - @cli.command('build') def cli_build(): - cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec'] - subprocess.run(cmd, check = False) + with TemporaryDirectory() as tmp: + arch = 'amd64' if sys.maxsize >= 2**32 else 'i386' + cmd = [ + sys.executable, '-m', 'PyInstaller', + '--collect-data', 'relay', + '--collect-data', 'aiohttp_swagger', + '--hidden-import', 'pg8000', + '--hidden-import', 'sqlite3', + '--name', f'activityrelay-{__version__}-{platform.system().lower()}-{arch}', + '--workpath', tmp, + '--onefile', 'relay/__main__.py', + ] + + if platform.system() == 'Windows': + cmd.append('--console') + + # putting the spec path on a different drive than the source dir breaks + if str(SCRIPT)[0] == tmp[0]: + cmd.extend(['--specpath', tmp]) + + else: + cmd.append('--strip') + cmd.extend(['--specpath', tmp]) + + subprocess.run(cmd, check = False) @cli.command('run') diff --git a/setup.cfg b/setup.cfg index 685f357..41c2a30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,9 @@ dev = file: dev-requirements.txt [options.package_data] relay = - data/statements.sql - data/swagger.yaml + data/* + frontend/* + frontend/page/* [options.entry_points] console_scripts = From e86a376f6e925048aec8279356752c15b34b0f4e Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 4 Mar 2024 15:29:08 -0500 Subject: [PATCH 31/31] move stylesheet variables to separate file and re-enable cache --- relay/application.py | 9 +++++---- relay/frontend/base.haml | 1 + relay/frontend/style.css | 17 ----------------- relay/template.py | 2 +- relay/views/frontend.py | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/relay/application.py b/relay/application.py index f3d9810..dfa3861 100644 --- a/relay/application.py +++ b/relay/application.py @@ -263,11 +263,12 @@ async def handle_response_headers(request: web.Request, handler: Coroutine) -> R resp = await handler(request) resp.headers['Server'] = 'ActivityRelay' -# if not request.app['dev'] and request.path.endswith(('.css', '.js')): -# resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' + if not request.app['dev'] and request.path.endswith(('.css', '.js')): + # cache for 2 weeks + resp.headers['Cache-Control'] = 'public,max-age=1209600,immutable' -# else: -# resp.headers['Cache-Control'] = 'no-store' + else: + resp.headers['Cache-Control'] = 'no-store' return resp diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index c74feda..e1da33c 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -11,6 +11,7 @@ %title << {{config.name}}: {{page}} %meta(charset="UTF-8") %meta(name="viewport" content="width=device-width, initial-scale=1") + %link(rel="stylesheet" type="text/css" href="/theme/{{theme_name}}.css") %link(rel="stylesheet" type="text/css" href="/style.css") -block head diff --git a/relay/frontend/style.css b/relay/frontend/style.css index d258640..47b0f3a 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -1,20 +1,3 @@ -:root { - --text: {{theme["text"]}}; - --background: {{theme["background"]}}; - --primary: {{theme["primary"]}}; - --primary-hover: {{theme["primary-hover"]}}; - --section-background: {{theme["section-background"]}}; - --table-background: {{theme["table-background"]}}; - --border: {{theme["border"]}}; - --message-text: {{theme["message-text"]}}; - --message-background: {{theme["message-background"]}}; - --message-border: {{theme["message-border"]}}; - --error-text: {{theme["error-text"]}}; - --error-background: {{theme["error-background"]}}; - --error-border: {{theme["error-border"]}}; - --spacing: 10px; -} - a { color: var(--primary); text-decoration: none; diff --git a/relay/template.py b/relay/template.py index 9f3a09b..64738e0 100644 --- a/relay/template.py +++ b/relay/template.py @@ -44,7 +44,7 @@ class Template(Environment): 'domain': self.app.config.domain, 'version': __version__, 'config': config, - 'theme': THEMES.get(config['theme'], THEMES['default']), + 'theme_name': config['theme'] or 'Default', **(context or {}) } diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 9df5e45..bd63417 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -445,3 +445,18 @@ class StyleCss(View): async def get(self, request: Request) -> Response: data = self.template.render('style.css', self) return Response.new(data, ctype = 'css') + + +@register_route('/theme/{theme}.css') +class ThemeCss(View): + async def get(self, request: Request, theme: str) -> Response: + try: + context = { + 'theme': THEMES[theme] + } + + except KeyError: + return Response.new('Invalid theme', 404) + + data = self.template.render('variables.css', self, **context) + return Response.new(data, ctype = 'css')