From f9d6d7b18dc56e2f532b43de43a71ff406d52479 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 12 Oct 2024 10:28:22 -0400 Subject: [PATCH] route handler changes * rename `register_route` to `register_view` * add `register_route` function * convert frontend and misc routes to use `register_route` --- relay/application.py | 5 +- relay/views/__init__.py | 2 +- relay/views/activitypub.py | 108 +++++++++- relay/views/api.py | 34 ++-- relay/views/base.py | 23 ++- relay/views/frontend.py | 407 +++++++++++++++++++------------------ relay/views/misc.py | 66 +++--- 7 files changed, 388 insertions(+), 257 deletions(-) diff --git a/relay/application.py b/relay/application.py index 22115f6..0e702c7 100644 --- a/relay/application.py +++ b/relay/application.py @@ -29,7 +29,7 @@ from .database.schema import Instance from .http_client import HttpClient from .misc import JSON_PATHS, TOKEN_PATHS, Message, Response from .template import Template -from .views import VIEWS +from .views import ROUTES, VIEWS from .views.api import handle_api_path from .views.frontend import handle_frontend_path from .workers import PushWorkers @@ -87,6 +87,9 @@ class Application(web.Application): for path, view in VIEWS: self.router.add_view(path, view) + for method, path, handler in ROUTES: + self.router.add_route(method, path, handler) + setup_swagger( self, ui_version = 3, diff --git a/relay/views/__init__.py b/relay/views/__init__.py index 25a7a62..265c7ad 100644 --- a/relay/views/__init__.py +++ b/relay/views/__init__.py @@ -1,4 +1,4 @@ from __future__ import annotations from . import activitypub, api, frontend, misc -from .base import VIEWS, View +from .base import ROUTES, VIEWS, View diff --git a/relay/views/activitypub.py b/relay/views/activitypub.py index 00a1d76..6af132c 100644 --- a/relay/views/activitypub.py +++ b/relay/views/activitypub.py @@ -1,19 +1,117 @@ +from __future__ import annotations + import aputils import traceback from aiohttp import ClientConnectorError from aiohttp.web import Request +from aputils import Signature, SignatureFailureError, Signer from blib import HttpError +from dataclasses import dataclass +from typing import TYPE_CHECKING -from .base import View, register_route +from .base import View, register_view from .. import logger as logging from ..database import schema from ..misc import Message, Response from ..processors import run_processor +if TYPE_CHECKING: + from ..application import Application + + try: + from typing import Self + + except ImportError: + from typing_extensions import Self + + +# def route( +# method: HttpMethod | str, +# *path: str, +# activity: bool = True) -> Callable[[Application, Request], JsonBase[Any]]: +# +# def wrapper + + +@dataclass(slots = True) +class InboxData: + signature: Signature + message: Message + actor: Message + signer: Signer + instance: schema.Instance | None + + + @classmethod + async def parse_request(cls: type[Self], app: Application, request: Request) -> Self: + signature: Signature | None = None + message: Message | None = None + actor: Message | None = None + signer: Signer | None = None + + try: + signature = Signature.parse(request.headers['signature']) + + except KeyError: + logging.verbose('Missing signature header') + raise HttpError(400, 'missing signature header') + + try: + message = await request.json(loads = Message.parse) + + except Exception: + traceback.print_exc() + logging.verbose('Failed to parse message from actor: %s', signature.keyid) + raise HttpError(400, 'failed to parse message') + + if message is None: + logging.verbose('empty message') + raise HttpError(400, 'missing message') + + if 'actor' not in message: + logging.verbose('actor not in message') + raise HttpError(400, 'no actor in message') + + try: + actor = await app.client.get(signature.keyid, True, Message) + + except HttpError as e: + # ld signatures aren't handled atm, so just ignore it + if message.type == 'Delete': + logging.verbose('Instance sent a delete which cannot be handled') + raise HttpError(202, '') + + logging.verbose('Failed to fetch actor: %s', signature.keyid) + logging.debug('HTTP Status %i: %s', e.status, e.message) + raise HttpError(400, 'failed to fetch actor') + + except ClientConnectorError as e: + logging.warning('Error when trying to fetch actor: %s, %s', signature.keyid, str(e)) + raise HttpError(400, 'failed to fetch actor') + + except Exception: + traceback.print_exc() + raise HttpError(500, 'unexpected error when fetching actor') + + try: + signer = actor.signer + + except KeyError: + logging.verbose('Actor missing public key: %s', signature.keyid) + raise HttpError(400, 'actor missing public key') + + try: + await signer.validate_request_async(request) + + except SignatureFailureError as e: + logging.verbose('signature validation failed for "%s": %s', actor.id, e) + raise HttpError(401, str(e)) + + return cls(signature, message, actor, signer, None) + -@register_route('/actor', '/inbox') class ActorView(View): signature: aputils.Signature message: Message @@ -128,7 +226,7 @@ class ActorView(View): raise HttpError(401, str(e)) -@register_route('/outbox') +@register_view('/outbox') class OutboxView(View): async def get(self, request: Request) -> Response: msg = aputils.Message.new( @@ -143,7 +241,7 @@ class OutboxView(View): return Response.new(msg, ctype = 'activity') -@register_route('/following', '/followers') +@register_view('/following', '/followers') class RelationshipView(View): async def get(self, request: Request) -> Response: with self.database.session(False) as s: @@ -161,7 +259,7 @@ class RelationshipView(View): return Response.new(msg, ctype = 'activity') -@register_route('/.well-known/webfinger') +@register_view('/.well-known/webfinger') class WebfingerView(View): async def get(self, request: Request) -> Response: try: diff --git a/relay/views/api.py b/relay/views/api.py index b92802c..1023147 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -6,7 +6,7 @@ from blib import HttpError, convert_to_boolean from collections.abc import Awaitable, Callable, Sequence from urllib.parse import urlparse -from .base import View, register_route +from .base import View, register_view from .. import __version__ from ..database import ConfigData, schema @@ -57,8 +57,8 @@ async def handle_api_path( return response -@register_route('/oauth/authorize') -@register_route('/api/oauth/authorize') +@register_view('/oauth/authorize') +@register_view('/api/oauth/authorize') class OauthAuthorize(View): async def get(self, request: Request) -> Response: data = await self.get_api_data(['response_type', 'client_id', 'redirect_uri'], []) @@ -122,8 +122,8 @@ class OauthAuthorize(View): return Response.new_redir('/') -@register_route('/oauth/token') -@register_route('/api/oauth/token') +@register_view('/oauth/token') +@register_view('/api/oauth/token') class OauthToken(View): async def post(self, request: Request) -> Response: data = await self.get_api_data( @@ -148,8 +148,8 @@ class OauthToken(View): return Response.new(app.get_api_data(True), ctype = 'json') -@register_route('/oauth/revoke') -@register_route('/api/oauth/revoke') +@register_view('/oauth/revoke') +@register_view('/api/oauth/revoke') class OauthRevoke(View): async def post(self, request: Request) -> Response: data = await self.get_api_data(['client_id', 'client_secret', 'token'], []) @@ -167,7 +167,7 @@ class OauthRevoke(View): return Response.new({'msg': 'Token deleted'}, ctype = 'json') -@register_route('/api/v1/app') +@register_view('/api/v1/app') class App(View): async def get(self, request: Request) -> Response: return Response.new(request['token'].get_api_data(), ctype = 'json') @@ -196,7 +196,7 @@ class App(View): return Response.new({'msg': 'Token deleted'}, ctype = 'json') -@register_route('/api/v1/login') +@register_view('/api/v1/login') class Login(View): async def post(self, request: Request) -> Response: data = await self.get_api_data(['username', 'password'], []) @@ -228,7 +228,7 @@ class Login(View): return resp -@register_route('/api/v1/relay') +@register_view('/api/v1/relay') class RelayInfo(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -250,7 +250,7 @@ class RelayInfo(View): return Response.new(data, ctype = 'json') -@register_route('/api/v1/config') +@register_view('/api/v1/config') class Config(View): async def get(self, request: Request) -> Response: data = {} @@ -299,7 +299,7 @@ class Config(View): return Response.new({'message': 'Updated config'}, ctype = 'json') -@register_route('/api/v1/instance') +@register_view('/api/v1/instance') class Inbox(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -378,7 +378,7 @@ class Inbox(View): return Response.new({'message': 'Deleted instance'}, ctype = 'json') -@register_route('/api/v1/request') +@register_view('/api/v1/request') class RequestView(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -422,7 +422,7 @@ class RequestView(View): return Response.new(resp_message, ctype = 'json') -@register_route('/api/v1/domain_ban') +@register_view('/api/v1/domain_ban') class DomainBan(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -482,7 +482,7 @@ class DomainBan(View): return Response.new({'message': 'Unbanned domain'}, ctype = 'json') -@register_route('/api/v1/software_ban') +@register_view('/api/v1/software_ban') class SoftwareBan(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -538,7 +538,7 @@ class SoftwareBan(View): return Response.new({'message': 'Unbanned software'}, ctype = 'json') -@register_route('/api/v1/user') +@register_view('/api/v1/user') class User(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: @@ -594,7 +594,7 @@ class User(View): return Response.new({'message': 'Deleted user'}, ctype = 'json') -@register_route('/api/v1/whitelist') +@register_view('/api/v1/whitelist') class Whitelist(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: diff --git a/relay/views/base.py b/relay/views/base.py index 624ed9d..49e0bfc 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -3,7 +3,7 @@ from __future__ import annotations from aiohttp.abc import AbstractView from aiohttp.hdrs import METH_ALL as METHODS from aiohttp.web import Request -from blib import HttpError +from blib import HttpError, HttpMethod from bsql import Database from collections.abc import Awaitable, Callable, Generator, Sequence, Mapping from functools import cached_property @@ -21,16 +21,19 @@ if TYPE_CHECKING: from ..application import Application from ..template import Template + RouteHandler = Callable[[Application, Request], Awaitable[Response]] + HandlerCallback = Callable[[Request], Awaitable[Response]] + -HandlerCallback = Callable[[Request], Awaitable[Response]] VIEWS: list[tuple[str, type[View]]] = [] +ROUTES: list[tuple[str, str, HandlerCallback]] = [] def convert_data(data: Mapping[str, Any]) -> dict[str, str]: return {key: str(value) for key, value in data.items()} -def register_route(*paths: str) -> Callable[[type[View]], type[View]]: +def register_view(*paths: str) -> Callable[[type[View]], type[View]]: def wrapper(view: type[View]) -> type[View]: for path in paths: VIEWS.append((path, view)) @@ -39,6 +42,20 @@ def register_route(*paths: str) -> Callable[[type[View]], type[View]]: return wrapper +def register_route( + method: HttpMethod | str, *paths: str) -> Callable[[RouteHandler], HandlerCallback]: + + def wrapper(handler: RouteHandler) -> HandlerCallback: + async def inner(request: Request) -> Response: + return await handler(get_app(), request, **request.match_info) + + for path in paths: + ROUTES.append((HttpMethod.parse(method), path, inner)) + + return inner + return wrapper + + class View(AbstractView): def __await__(self) -> Generator[Any, None, Response]: if self.request.method not in METHODS: diff --git a/relay/views/frontend.py b/relay/views/frontend.py index b6dba7b..5e12ee8 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -1,19 +1,25 @@ -from aiohttp import web +from __future__ import annotations + +from aiohttp.web import Request, middleware +from blib import HttpMethod from collections.abc import Awaitable, Callable -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import unquote -from .base import View, register_route +from .base import register_route from ..database import THEMES from ..logger import LogLevel from ..misc import TOKEN_PATHS, Response +if TYPE_CHECKING: + from ..application import Application -@web.middleware + +@middleware async def handle_frontend_path( - request: web.Request, - handler: Callable[[web.Request], Awaitable[Response]]) -> Response: + request: Request, + handler: Callable[[Request], Awaitable[Response]]) -> Response: if request['user'] is not None and request.path == '/login': return Response.new_redir('/') @@ -38,211 +44,208 @@ async def handle_frontend_path( return response -@register_route('/') -class HomeView(View): - async def get(self, request: web.Request) -> Response: - with self.database.session() as conn: - context: dict[str, Any] = { - 'instances': tuple(conn.get_inboxes()) - } - - data = self.template.render('page/home.haml', self.request, **context) - return Response.new(data, ctype='html') - - -@register_route('/login') -class Login(View): - async def get(self, request: web.Request) -> Response: - redir = unquote(request.query.get('redir', '/')) - data = self.template.render('page/login.haml', self.request, redir = redir) - return Response.new(data, ctype = 'html') - - -@register_route('/logout') -class Logout(View): - async def get(self, request: web.Request) -> Response: - with self.database.session(True) as conn: - conn.del_app(request['token'].client_id, request['token'].client_secret) - - 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: web.Request) -> Response: - return Response.new_redir(f'/login?redir={request.path}', 301) - - -@register_route('/admin/instances') -class AdminInstances(View): - async def get(self, - request: web.Request, - error: str | None = None, - message: str | None = None) -> Response: - - with self.database.session() as conn: - context: dict[str, Any] = { - 'instances': tuple(conn.get_inboxes()), - 'requests': tuple(conn.get_requests()) - } - - if error: - context['error'] = error - - if message: - context['message'] = message - - data = self.template.render('page/admin-instances.haml', self.request, **context) - return Response.new(data, ctype = 'html') - - -@register_route('/admin/whitelist') -class AdminWhitelist(View): - async def get(self, - request: web.Request, - error: str | None = None, - message: str | None = None) -> Response: - - with self.database.session() as conn: - context: dict[str, Any] = { - 'whitelist': tuple(conn.execute('SELECT * FROM whitelist ORDER BY domain ASC')) - } - - if error: - context['error'] = error - - if message: - context['message'] = message - - data = self.template.render('page/admin-whitelist.haml', self.request, **context) - return Response.new(data, ctype = 'html') - - -@register_route('/admin/domain_bans') -class AdminDomainBans(View): - async def get(self, - request: web.Request, - error: str | None = None, - message: str | None = None) -> Response: - - with self.database.session() as conn: - context: dict[str, Any] = { - 'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC')) - } - - if error: - context['error'] = error - - if message: - context['message'] = message - - data = self.template.render('page/admin-domain_bans.haml', self.request, **context) - return Response.new(data, ctype = 'html') - - -@register_route('/admin/software_bans') -class AdminSoftwareBans(View): - async def get(self, - request: web.Request, - error: str | None = None, - message: str | None = None) -> Response: - - with self.database.session() as conn: - context: dict[str, Any] = { - 'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC')) - } - - if error: - context['error'] = error - - if message: - context['message'] = message - - data = self.template.render('page/admin-software_bans.haml', self.request, **context) - return Response.new(data, ctype = 'html') - - -@register_route('/admin/users') -class AdminUsers(View): - async def get(self, - request: web.Request, - error: str | None = None, - message: str | None = None) -> Response: - - with self.database.session() as conn: - context: dict[str, Any] = { - 'users': tuple(conn.execute('SELECT * FROM users ORDER BY username ASC')) - } - - if error: - context['error'] = error - - if message: - context['message'] = message - - data = self.template.render('page/admin-users.haml', self.request, **context) - return Response.new(data, ctype = 'html') - - -@register_route('/admin/config') -class AdminConfig(View): - async def get(self, request: web.Request, message: str | None = None) -> Response: +@register_route(HttpMethod.GET, "/") +async def handle_home(app: Application, request: Request) -> Response: + with app.database.session() as conn: context: dict[str, Any] = { - 'themes': tuple(THEMES.keys()), - 'levels': tuple(level.name for level in LogLevel), - 'message': message, - 'desc': { - "name": "Name of the relay to be displayed in the header of the pages and in " + - "the actor endpoint.", # noqa: E131 - "note": "Description of the relay to be displayed on the front page and as the " + - "bio in the actor endpoint.", - "theme": "Color theme to use on the web pages.", - "log_level": "Minimum level of logging messages to print to the console.", - "whitelist_enabled": "Only allow instances in the whitelist to be able to follow.", - "approval_required": "Require instances not on the whitelist to be approved by " + - "and admin. The `whitelist-enabled` setting is ignored when this is enabled." - } + 'instances': tuple(conn.get_inboxes()) } - data = self.template.render('page/admin-config.haml', self.request, **context) - return Response.new(data, ctype = 'html') + data = app.template.render('page/home.haml', request, **context) + return Response.new(data, ctype='html') -@register_route('/manifest.json') -class ManifestJson(View): - async def get(self, request: web.Request) -> Response: - with self.database.session(False) as conn: - config = conn.get_config_all() - theme = THEMES[config.theme] +@register_route(HttpMethod.GET, '/login') +async def handle_login(app: Application, request: Request) -> Response: + redir = unquote(request.query.get('redir', '/')) + data = app.template.render('page/login.haml', request, redir = redir) + return Response.new(data, ctype = 'html') - data = { - 'background_color': theme['background'], - 'categories': ['activitypub'], - 'description': 'Message relay for the ActivityPub network', - 'display': 'standalone', - 'name': config['name'], - 'orientation': 'portrait', - 'scope': f"https://{self.config.domain}/", - 'short_name': 'ActivityRelay', - 'start_url': f"https://{self.config.domain}/", - 'theme_color': theme['primary'] + +@register_route(HttpMethod.GET, '/logout') +async def handle_logout(app: Application, request: Request) -> Response: + with app.database.session(True) as conn: + conn.del_app(request['token'].client_id, request['token'].client_secret) + + resp = Response.new_redir('/') + resp.del_cookie('user-token', domain = app.config.domain, path = '/') + return resp + + +@register_route(HttpMethod.GET, '/admin') +async def handle_admin(app: Application, request: Request) -> Response: + return Response.new_redir(f'/login?redir={request.path}', 301) + + +@register_route(HttpMethod.GET, '/admin/instances') +async def handle_admin_instances( + app: Application, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with app.database.session() as conn: + context: dict[str, Any] = { + 'instances': tuple(conn.get_inboxes()), + 'requests': tuple(conn.get_requests()) } - return Response.new(data, ctype = 'webmanifest') + if error: + context['error'] = error + + if message: + context['message'] = message + + data = app.template.render('page/admin-instances.haml', request, **context) + return Response.new(data, ctype = 'html') -@register_route('/theme/{theme}.css') -class ThemeCss(View): - async def get(self, request: web.Request, theme: str) -> Response: - try: - context: dict[str, Any] = { - 'theme': THEMES[theme] - } +@register_route(HttpMethod.GET, '/admin/whitelist') +async def handle_admin_whitelist( + app: Application, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: - except KeyError: - return Response.new('Invalid theme', 404) + with app.database.session() as conn: + context: dict[str, Any] = { + 'whitelist': tuple(conn.execute('SELECT * FROM whitelist ORDER BY domain ASC')) + } - data = self.template.render('variables.css', self.request, **context) - return Response.new(data, ctype = 'css') + if error: + context['error'] = error + + if message: + context['message'] = message + + data = app.template.render('page/admin-whitelist.haml', request, **context) + return Response.new(data, ctype = 'html') + + +@register_route(HttpMethod.GET, '/admin/domain_bans') +async def handle_admin_instance_bans( + app: Application, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with app.database.session() as conn: + context: dict[str, Any] = { + 'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC')) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = app.template.render('page/admin-domain_bans.haml', request, **context) + return Response.new(data, ctype = 'html') + + +@register_route(HttpMethod.GET, '/admin/software_bans') +async def handle_admin_software_bans( + app: Application, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with app.database.session() as conn: + context: dict[str, Any] = { + 'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC')) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = app.template.render('page/admin-software_bans.haml', request, **context) + return Response.new(data, ctype = 'html') + + +@register_route(HttpMethod.GET, '/admin/users') +async def handle_admin_users( + app: Application, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with app.database.session() as conn: + context: dict[str, Any] = { + 'users': tuple(conn.execute('SELECT * FROM users ORDER BY username ASC')) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = app.template.render('page/admin-users.haml', request, **context) + return Response.new(data, ctype = 'html') + + +@register_route(HttpMethod.GET, '/admin/config') +async def handle_admin_config( + app: Application, + request: Request, + message: str | None = None) -> Response: + + context: dict[str, Any] = { + 'themes': tuple(THEMES.keys()), + 'levels': tuple(level.name for level in LogLevel), + 'message': message, + 'desc': { + "name": "Name of the relay to be displayed in the header of the pages and in " + + "the actor endpoint.", # noqa: E131 + "note": "Description of the relay to be displayed on the front page and as the " + + "bio in the actor endpoint.", + "theme": "Color theme to use on the web pages.", + "log_level": "Minimum level of logging messages to print to the console.", + "whitelist_enabled": "Only allow instances in the whitelist to be able to follow.", + "approval_required": "Require instances not on the whitelist to be approved by " + + "and admin. The `whitelist-enabled` setting is ignored when this is enabled." + } + } + + data = app.template.render('page/admin-config.haml', request, **context) + return Response.new(data, ctype = 'html') + + +@register_route(HttpMethod.GET, '/manifest.json') +async def handle_manifest(app: Application, request: Request) -> Response: + with app.database.session(False) as conn: + config = conn.get_config_all() + theme = THEMES[config.theme] + + data = { + 'background_color': theme['background'], + 'categories': ['activitypub'], + 'description': 'Message relay for the ActivityPub network', + 'display': 'standalone', + 'name': config['name'], + 'orientation': 'portrait', + 'scope': f"https://{app.config.domain}/", + 'short_name': 'ActivityRelay', + 'start_url': f"https://{app.config.domain}/", + 'theme_color': theme['primary'] + } + + return Response.new(data, ctype = 'webmanifest') + + +@register_route(HttpMethod.GET, '/theme/{theme}.css') # type: ignore[arg-type] +async def handle_theme(app: Application, request: Request, theme: str) -> Response: + try: + context: dict[str, Any] = { + 'theme': THEMES[theme] + } + + except KeyError: + return Response.new('Invalid theme', 404) + + data = app.template.render('variables.css', request, **context) + return Response.new(data, ctype = 'css') diff --git a/relay/views/misc.py b/relay/views/misc.py index 5e2be52..015c274 100644 --- a/relay/views/misc.py +++ b/relay/views/misc.py @@ -1,19 +1,25 @@ +from __future__ import annotations + import aputils import subprocess from aiohttp.web import Request -from pathlib import Path +from blib import File, HttpMethod +from typing import TYPE_CHECKING -from .base import View, register_route +from .base import register_route from .. import __version__ from ..misc import Response +if TYPE_CHECKING: + from ..application import Application + VERSION = __version__ -if Path(__file__).parent.parent.joinpath('.git').exists(): +if File(__file__).join("../../../.git").resolve().exists: try: commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii') VERSION = f'{__version__} {commit_label}' @@ -22,31 +28,35 @@ if Path(__file__).parent.parent.joinpath('.git').exists(): pass -@register_route('/nodeinfo/{niversion:\\d.\\d}.json', '/nodeinfo/{niversion:\\d.\\d}') -class NodeinfoView(View): - async def get(self, request: Request, niversion: str) -> Response: - with self.database.session() as conn: - inboxes = conn.get_inboxes() - - nodeinfo = aputils.Nodeinfo.new( - name = 'activityrelay', - version = VERSION, - protocols = ['activitypub'], - open_regs = not conn.get_config('whitelist-enabled'), - users = 1, - repo = 'https://git.pleroma.social/pleroma/relay' if niversion == '2.1' else None, - metadata = { - 'approval_required': conn.get_config('approval-required'), - 'peers': [inbox['domain'] for inbox in inboxes] - } - ) - - return Response.new(nodeinfo, ctype = 'json') +NODEINFO_PATHS = [ + '/nodeinfo/{niversion:\\d.\\d}.json', + '/nodeinfo/{niversion:\\d.\\d}' +] -@register_route('/.well-known/nodeinfo') -class WellknownNodeinfoView(View): - async def get(self, request: Request) -> Response: - data = aputils.WellKnownNodeinfo.new_template(self.config.domain) +@register_route(HttpMethod.GET, *NODEINFO_PATHS) # type: ignore[arg-type] +async def handle_nodeinfo(app: Application, request: Request, niversion: str) -> Response: + with app.database.session() as conn: + inboxes = conn.get_inboxes() - return Response.new(data, ctype = 'json') + nodeinfo = aputils.Nodeinfo.new( + name = 'activityrelay', + version = VERSION, + protocols = ['activitypub'], + open_regs = not conn.get_config('whitelist-enabled'), + users = 1, + repo = 'https://git.pleroma.social/pleroma/relay' if niversion == '2.1' else None, + metadata = { + 'approval_required': conn.get_config('approval-required'), + 'peers': [inbox['domain'] for inbox in inboxes] + } + ) + + return Response.new(nodeinfo, ctype = 'json') + + +@register_route(HttpMethod.GET, '/.well-known/nodeinfo') +async def handle_wk_nodeinfo(app: Application, request: Request) -> Response: + data = aputils.WellKnownNodeinfo.new_template(app.config.domain) + + return Response.new(data, ctype = 'json')