diff --git a/relay/misc.py b/relay/misc.py index e71845d..296082b 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -5,12 +5,8 @@ import os import socket import typing -from aiohttp.abc import AbstractView -from aiohttp.hdrs import METH_ALL as METHODS from aiohttp.web import Response as AiohttpResponse -from aiohttp.web_exceptions import HTTPMethodNotAllowed from aputils.message import Message as ApMessage -from functools import cached_property from uuid import uuid4 if typing.TYPE_CHECKING: @@ -232,67 +228,3 @@ class Response(AiohttpResponse): @location.setter def location(self, value: str) -> None: self.headers['Location'] = value - - -class View(AbstractView): - def __await__(self) -> Generator[Response]: - if self.request.method not in METHODS: - raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods) - - if not (handler := self.handlers.get(self.request.method)): - raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods) from None - - return self._run_handler(handler).__await__() - - - async def _run_handler(self, handler: Awaitable) -> Response: - with self.database.config.connection_class(self.database) as conn: - # todo: remove on next tinysql release - conn.open() - - return await handler(self.request, conn, **self.request.match_info) - - - @cached_property - def allowed_methods(self) -> tuple[str]: - return tuple(self.handlers.keys()) - - - @cached_property - def handlers(self) -> dict[str, Coroutine]: - data = {} - - for method in METHODS: - try: - data[method] = getattr(self, method.lower()) - - except AttributeError: - continue - - return data - - - # app components - @property - def app(self) -> Application: - return self.request.app - - - @property - def cache(self) -> Cache: - return self.app.cache - - - @property - def client(self) -> HttpClient: - return self.app.client - - - @property - def config(self) -> Config: - return self.app.config - - - @property - def database(self) -> Database: - return self.app.database diff --git a/relay/views/__init__.py b/relay/views/__init__.py new file mode 100644 index 0000000..85c2bd3 --- /dev/null +++ b/relay/views/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from . import activitypub, frontend, misc +from .base import VIEWS diff --git a/relay/views.py b/relay/views/activitypub.py similarity index 65% rename from relay/views.py rename to relay/views/activitypub.py index cb648a2..be51047 100644 --- a/relay/views.py +++ b/relay/views/activitypub.py @@ -1,95 +1,27 @@ from __future__ import annotations -import subprocess import traceback import typing from aputils.errors import SignatureFailureError from aputils.misc import Digest, HttpDate, Signature -from aputils.objects import Nodeinfo, Webfinger, WellKnownNodeinfo -from pathlib import Path +from aputils.objects import Webfinger -from . import __version__ -from . import logger as logging -from .database.connection import Connection -from .misc import Message, Response, View -from .processors import run_processor +from .base import View, register_route + +from .. import logger as logging +from ..misc import Message, Response +from ..processors import run_processor if typing.TYPE_CHECKING: from aiohttp.web import Request from aputils.signer import Signer - from collections.abc import Callable from tinysql import Row - - -VIEWS = [] -VERSION = __version__ -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}

- -""" - - -if Path(__file__).parent.parent.joinpath('.git').exists(): - try: - commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii') - VERSION = f'{__version__} {commit_label}' - - except Exception: - pass - - -def register_route(*paths: str) -> Callable: - def wrapper(view: View) -> View: - for path in paths: - VIEWS.append([path, view]) - - return View - return wrapper + from ..database.connection import Connection # pylint: disable=unused-argument -@register_route('/') -class HomeView(View): - async def get(self, request: Request, conn: Connection) -> Response: - config = conn.get_config_all() - inboxes = 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) - ) - - return Response.new(text, ctype='html') - - - @register_route('/actor', '/inbox') class ActorView(View): def __init__(self, request: Request): @@ -247,31 +179,3 @@ class WebfingerView(View): ) return Response.new(data, ctype = 'json') - - -@register_route('/nodeinfo/{niversion:\\d.\\d}.json', '/nodeinfo/{niversion:\\d.\\d}') -class NodeinfoView(View): - # pylint: disable=no-self-use - async def get(self, request: Request, conn: Connection, niversion: str) -> Response: - inboxes = conn.execute('SELECT * FROM inboxes').all() - - data = { - 'name': 'activityrelay', - 'version': VERSION, - 'protocols': ['activitypub'], - 'open_regs': not conn.get_config('whitelist-enabled'), - 'users': 1, - 'metadata': {'peers': [inbox['domain'] for inbox in inboxes]} - } - - if niversion == '2.1': - data['repo'] = 'https://git.pleroma.social/pleroma/relay' - - return Response.new(Nodeinfo.new(**data), ctype = 'json') - - -@register_route('/.well-known/nodeinfo') -class WellknownNodeinfoView(View): - async def get(self, request: Request, conn: Connection) -> Response: - data = WellKnownNodeinfo.new_template(self.config.domain) - return Response.new(data, ctype = 'json') diff --git a/relay/views/base.py b/relay/views/base.py new file mode 100644 index 0000000..95b6562 --- /dev/null +++ b/relay/views/base.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import typing + +from aiohttp.abc import AbstractView +from aiohttp.hdrs import METH_ALL as METHODS +from aiohttp.web import HTTPMethodNotAllowed +from functools import cached_property + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Coroutine, Generator + from tinysql import Database + from ..application import Application + from ..cache import Cache + from ..config import Config + from ..http_client import HttpClient + from ..misc import Response + + +VIEWS = [] + + +def register_route(*paths: str) -> Callable: + def wrapper(view: View) -> View: + for path in paths: + VIEWS.append([path, view]) + + return View + return wrapper + + +class View(AbstractView): + def __await__(self) -> Generator[Response]: + if self.request.method not in METHODS: + raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods) + + if not (handler := self.handlers.get(self.request.method)): + raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods) + + return self._run_handler(handler).__await__() + + + async def _run_handler(self, handler: Coroutine) -> Response: + with self.database.config.connection_class(self.database) as conn: + # todo: remove on next tinysql release + conn.open() + + return await handler(self.request, conn, **self.request.match_info) + + + @cached_property + def allowed_methods(self) -> tuple[str]: + return tuple(self.handlers.keys()) + + + @cached_property + def handlers(self) -> dict[str, Coroutine]: + data = {} + + for method in METHODS: + try: + data[method] = getattr(self, method.lower()) + + except AttributeError: + continue + + return data + + + # app components + @property + def app(self) -> Application: + return self.request.app + + + @property + def cache(self) -> Cache: + return self.app.cache + + + @property + def client(self) -> HttpClient: + return self.app.client + + + @property + def config(self) -> Config: + return self.app.config + + + @property + def database(self) -> Database: + return self.app.database diff --git a/relay/views/frontend.py b/relay/views/frontend.py new file mode 100644 index 0000000..987b9b0 --- /dev/null +++ b/relay/views/frontend.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import typing + +from .base import View, register_route + +from .. import __version__ +from ..misc import Response + +if typing.TYPE_CHECKING: + from aiohttp.web import Request + from aputils.signer import Signer + from collections.abc import Callable + from tinysql import Row + from ..database.connection import Connection + + +HOME_TEMPLATE = """ + + 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, conn: Connection) -> Response: + config = conn.get_config_all() + inboxes = 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) + ) + + return Response.new(text, ctype='html') diff --git a/relay/views/misc.py b/relay/views/misc.py new file mode 100644 index 0000000..e41ae2b --- /dev/null +++ b/relay/views/misc.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import subprocess +import typing + +from aputils.objects import Nodeinfo, WellKnownNodeinfo +from pathlib import Path + +from .base import View, register_route + +from .. import __version__ +from ..misc import Response + +if typing.TYPE_CHECKING: + from aiohttp.web import Request + from ..database.connection import Connection + + +VERSION = __version__ + + +if Path(__file__).parent.parent.joinpath('.git').exists(): + try: + commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii') + VERSION = f'{__version__} {commit_label}' + + except Exception: + pass + + +# pylint: disable=unused-argument + +@register_route('/nodeinfo/{niversion:\\d.\\d}.json', '/nodeinfo/{niversion:\\d.\\d}') +class NodeinfoView(View): + # pylint: disable=no-self-use + async def get(self, request: Request, conn: Connection, niversion: str) -> Response: + inboxes = conn.execute('SELECT * FROM inboxes').all() + + data = { + 'name': 'activityrelay', + 'version': VERSION, + 'protocols': ['activitypub'], + 'open_regs': not conn.get_config('whitelist-enabled'), + 'users': 1, + 'metadata': {'peers': [inbox['domain'] for inbox in inboxes]} + } + + if niversion == '2.1': + data['repo'] = 'https://git.pleroma.social/pleroma/relay' + + return Response.new(Nodeinfo.new(**data), ctype = 'json') + + +@register_route('/.well-known/nodeinfo') +class WellknownNodeinfoView(View): + async def get(self, request: Request, conn: Connection) -> Response: + data = WellKnownNodeinfo.new_template(self.config.domain) + return Response.new(data, ctype = 'json')