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'