route handler changes

* rename `register_route` to `register_view`
* add `register_route` function
* convert frontend and misc routes to use `register_route`
This commit is contained in:
Izalia Mae 2024-10-12 10:28:22 -04:00
parent e0ca93ab93
commit f9d6d7b18d
7 changed files with 388 additions and 257 deletions

View file

@ -29,7 +29,7 @@ from .database.schema import Instance
from .http_client import HttpClient from .http_client import HttpClient
from .misc import JSON_PATHS, TOKEN_PATHS, Message, Response from .misc import JSON_PATHS, TOKEN_PATHS, Message, Response
from .template import Template from .template import Template
from .views import VIEWS from .views import ROUTES, VIEWS
from .views.api import handle_api_path from .views.api import handle_api_path
from .views.frontend import handle_frontend_path from .views.frontend import handle_frontend_path
from .workers import PushWorkers from .workers import PushWorkers
@ -87,6 +87,9 @@ class Application(web.Application):
for path, view in VIEWS: for path, view in VIEWS:
self.router.add_view(path, view) self.router.add_view(path, view)
for method, path, handler in ROUTES:
self.router.add_route(method, path, handler)
setup_swagger( setup_swagger(
self, self,
ui_version = 3, ui_version = 3,

View file

@ -1,4 +1,4 @@
from __future__ import annotations from __future__ import annotations
from . import activitypub, api, frontend, misc from . import activitypub, api, frontend, misc
from .base import VIEWS, View from .base import ROUTES, VIEWS, View

View file

@ -1,19 +1,117 @@
from __future__ import annotations
import aputils import aputils
import traceback import traceback
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from aiohttp.web import Request from aiohttp.web import Request
from aputils import Signature, SignatureFailureError, Signer
from blib import HttpError 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 .. import logger as logging
from ..database import schema from ..database import schema
from ..misc import Message, Response from ..misc import Message, Response
from ..processors import run_processor 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): class ActorView(View):
signature: aputils.Signature signature: aputils.Signature
message: Message message: Message
@ -128,7 +226,7 @@ class ActorView(View):
raise HttpError(401, str(e)) raise HttpError(401, str(e))
@register_route('/outbox') @register_view('/outbox')
class OutboxView(View): class OutboxView(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
msg = aputils.Message.new( msg = aputils.Message.new(
@ -143,7 +241,7 @@ class OutboxView(View):
return Response.new(msg, ctype = 'activity') return Response.new(msg, ctype = 'activity')
@register_route('/following', '/followers') @register_view('/following', '/followers')
class RelationshipView(View): class RelationshipView(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session(False) as s: with self.database.session(False) as s:
@ -161,7 +259,7 @@ class RelationshipView(View):
return Response.new(msg, ctype = 'activity') return Response.new(msg, ctype = 'activity')
@register_route('/.well-known/webfinger') @register_view('/.well-known/webfinger')
class WebfingerView(View): class WebfingerView(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
try: try:

View file

@ -6,7 +6,7 @@ from blib import HttpError, convert_to_boolean
from collections.abc import Awaitable, Callable, Sequence from collections.abc import Awaitable, Callable, Sequence
from urllib.parse import urlparse from urllib.parse import urlparse
from .base import View, register_route from .base import View, register_view
from .. import __version__ from .. import __version__
from ..database import ConfigData, schema from ..database import ConfigData, schema
@ -57,8 +57,8 @@ async def handle_api_path(
return response return response
@register_route('/oauth/authorize') @register_view('/oauth/authorize')
@register_route('/api/oauth/authorize') @register_view('/api/oauth/authorize')
class OauthAuthorize(View): class OauthAuthorize(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
data = await self.get_api_data(['response_type', 'client_id', 'redirect_uri'], []) data = await self.get_api_data(['response_type', 'client_id', 'redirect_uri'], [])
@ -122,8 +122,8 @@ class OauthAuthorize(View):
return Response.new_redir('/') return Response.new_redir('/')
@register_route('/oauth/token') @register_view('/oauth/token')
@register_route('/api/oauth/token') @register_view('/api/oauth/token')
class OauthToken(View): class OauthToken(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await self.get_api_data( data = await self.get_api_data(
@ -148,8 +148,8 @@ class OauthToken(View):
return Response.new(app.get_api_data(True), ctype = 'json') return Response.new(app.get_api_data(True), ctype = 'json')
@register_route('/oauth/revoke') @register_view('/oauth/revoke')
@register_route('/api/oauth/revoke') @register_view('/api/oauth/revoke')
class OauthRevoke(View): class OauthRevoke(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await self.get_api_data(['client_id', 'client_secret', 'token'], []) 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') return Response.new({'msg': 'Token deleted'}, ctype = 'json')
@register_route('/api/v1/app') @register_view('/api/v1/app')
class App(View): class App(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
return Response.new(request['token'].get_api_data(), ctype = 'json') 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') return Response.new({'msg': 'Token deleted'}, ctype = 'json')
@register_route('/api/v1/login') @register_view('/api/v1/login')
class Login(View): class Login(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await self.get_api_data(['username', 'password'], []) data = await self.get_api_data(['username', 'password'], [])
@ -228,7 +228,7 @@ class Login(View):
return resp return resp
@register_route('/api/v1/relay') @register_view('/api/v1/relay')
class RelayInfo(View): class RelayInfo(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -250,7 +250,7 @@ class RelayInfo(View):
return Response.new(data, ctype = 'json') return Response.new(data, ctype = 'json')
@register_route('/api/v1/config') @register_view('/api/v1/config')
class Config(View): class Config(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
data = {} data = {}
@ -299,7 +299,7 @@ class Config(View):
return Response.new({'message': 'Updated config'}, ctype = 'json') return Response.new({'message': 'Updated config'}, ctype = 'json')
@register_route('/api/v1/instance') @register_view('/api/v1/instance')
class Inbox(View): class Inbox(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -378,7 +378,7 @@ class Inbox(View):
return Response.new({'message': 'Deleted instance'}, ctype = 'json') return Response.new({'message': 'Deleted instance'}, ctype = 'json')
@register_route('/api/v1/request') @register_view('/api/v1/request')
class RequestView(View): class RequestView(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -422,7 +422,7 @@ class RequestView(View):
return Response.new(resp_message, ctype = 'json') return Response.new(resp_message, ctype = 'json')
@register_route('/api/v1/domain_ban') @register_view('/api/v1/domain_ban')
class DomainBan(View): class DomainBan(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -482,7 +482,7 @@ class DomainBan(View):
return Response.new({'message': 'Unbanned domain'}, ctype = 'json') return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
@register_route('/api/v1/software_ban') @register_view('/api/v1/software_ban')
class SoftwareBan(View): class SoftwareBan(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -538,7 +538,7 @@ class SoftwareBan(View):
return Response.new({'message': 'Unbanned software'}, ctype = 'json') return Response.new({'message': 'Unbanned software'}, ctype = 'json')
@register_route('/api/v1/user') @register_view('/api/v1/user')
class User(View): class User(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
@ -594,7 +594,7 @@ class User(View):
return Response.new({'message': 'Deleted user'}, ctype = 'json') return Response.new({'message': 'Deleted user'}, ctype = 'json')
@register_route('/api/v1/whitelist') @register_view('/api/v1/whitelist')
class Whitelist(View): class Whitelist(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from aiohttp.abc import AbstractView from aiohttp.abc import AbstractView
from aiohttp.hdrs import METH_ALL as METHODS from aiohttp.hdrs import METH_ALL as METHODS
from aiohttp.web import Request from aiohttp.web import Request
from blib import HttpError from blib import HttpError, HttpMethod
from bsql import Database from bsql import Database
from collections.abc import Awaitable, Callable, Generator, Sequence, Mapping from collections.abc import Awaitable, Callable, Generator, Sequence, Mapping
from functools import cached_property from functools import cached_property
@ -21,16 +21,19 @@ if TYPE_CHECKING:
from ..application import Application from ..application import Application
from ..template import Template 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]]] = [] VIEWS: list[tuple[str, type[View]]] = []
ROUTES: list[tuple[str, str, HandlerCallback]] = []
def convert_data(data: Mapping[str, Any]) -> dict[str, str]: def convert_data(data: Mapping[str, Any]) -> dict[str, str]:
return {key: str(value) for key, value in data.items()} 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]: def wrapper(view: type[View]) -> type[View]:
for path in paths: for path in paths:
VIEWS.append((path, view)) VIEWS.append((path, view))
@ -39,6 +42,20 @@ def register_route(*paths: str) -> Callable[[type[View]], type[View]]:
return wrapper 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): class View(AbstractView):
def __await__(self) -> Generator[Any, None, Response]: def __await__(self) -> Generator[Any, None, Response]:
if self.request.method not in METHODS: if self.request.method not in METHODS:

View file

@ -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 collections.abc import Awaitable, Callable
from typing import Any from typing import TYPE_CHECKING, Any
from urllib.parse import unquote from urllib.parse import unquote
from .base import View, register_route from .base import register_route
from ..database import THEMES from ..database import THEMES
from ..logger import LogLevel from ..logger import LogLevel
from ..misc import TOKEN_PATHS, Response from ..misc import TOKEN_PATHS, Response
if TYPE_CHECKING:
from ..application import Application
@web.middleware
@middleware
async def handle_frontend_path( async def handle_frontend_path(
request: web.Request, request: Request,
handler: Callable[[web.Request], Awaitable[Response]]) -> Response: handler: Callable[[Request], Awaitable[Response]]) -> Response:
if request['user'] is not None and request.path == '/login': if request['user'] is not None and request.path == '/login':
return Response.new_redir('/') return Response.new_redir('/')
@ -38,211 +44,208 @@ async def handle_frontend_path(
return response return response
@register_route('/') @register_route(HttpMethod.GET, "/")
class HomeView(View): async def handle_home(app: Application, request: Request) -> Response:
async def get(self, request: web.Request) -> Response: with app.database.session() as conn:
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:
context: dict[str, Any] = { context: dict[str, Any] = {
'themes': tuple(THEMES.keys()), 'instances': tuple(conn.get_inboxes())
'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 = self.template.render('page/admin-config.haml', self.request, **context) data = app.template.render('page/home.haml', request, **context)
return Response.new(data, ctype = 'html') return Response.new(data, ctype='html')
@register_route('/manifest.json') @register_route(HttpMethod.GET, '/login')
class ManifestJson(View): async def handle_login(app: Application, request: Request) -> Response:
async def get(self, request: web.Request) -> Response: redir = unquote(request.query.get('redir', '/'))
with self.database.session(False) as conn: data = app.template.render('page/login.haml', request, redir = redir)
config = conn.get_config_all() return Response.new(data, ctype = 'html')
theme = THEMES[config.theme]
data = {
'background_color': theme['background'], @register_route(HttpMethod.GET, '/logout')
'categories': ['activitypub'], async def handle_logout(app: Application, request: Request) -> Response:
'description': 'Message relay for the ActivityPub network', with app.database.session(True) as conn:
'display': 'standalone', conn.del_app(request['token'].client_id, request['token'].client_secret)
'name': config['name'],
'orientation': 'portrait', resp = Response.new_redir('/')
'scope': f"https://{self.config.domain}/", resp.del_cookie('user-token', domain = app.config.domain, path = '/')
'short_name': 'ActivityRelay', return resp
'start_url': f"https://{self.config.domain}/",
'theme_color': theme['primary']
@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') @register_route(HttpMethod.GET, '/admin/whitelist')
class ThemeCss(View): async def handle_admin_whitelist(
async def get(self, request: web.Request, theme: str) -> Response: app: Application,
try: request: Request,
context: dict[str, Any] = { error: str | None = None,
'theme': THEMES[theme] message: str | None = None) -> Response:
}
except KeyError: with app.database.session() as conn:
return Response.new('Invalid theme', 404) context: dict[str, Any] = {
'whitelist': tuple(conn.execute('SELECT * FROM whitelist ORDER BY domain ASC'))
}
data = self.template.render('variables.css', self.request, **context) if error:
return Response.new(data, ctype = 'css') 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')

View file

@ -1,19 +1,25 @@
from __future__ import annotations
import aputils import aputils
import subprocess import subprocess
from aiohttp.web import Request 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 .. import __version__
from ..misc import Response from ..misc import Response
if TYPE_CHECKING:
from ..application import Application
VERSION = __version__ VERSION = __version__
if Path(__file__).parent.parent.joinpath('.git').exists(): if File(__file__).join("../../../.git").resolve().exists:
try: try:
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii') commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
VERSION = f'{__version__} {commit_label}' VERSION = f'{__version__} {commit_label}'
@ -22,31 +28,35 @@ if Path(__file__).parent.parent.joinpath('.git').exists():
pass pass
@register_route('/nodeinfo/{niversion:\\d.\\d}.json', '/nodeinfo/{niversion:\\d.\\d}') NODEINFO_PATHS = [
class NodeinfoView(View): '/nodeinfo/{niversion:\\d.\\d}.json',
async def get(self, request: Request, niversion: str) -> Response: '/nodeinfo/{niversion:\\d.\\d}'
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')
@register_route('/.well-known/nodeinfo') @register_route(HttpMethod.GET, *NODEINFO_PATHS) # type: ignore[arg-type]
class WellknownNodeinfoView(View): async def handle_nodeinfo(app: Application, request: Request, niversion: str) -> Response:
async def get(self, request: Request) -> Response: with app.database.session() as conn:
data = aputils.WellKnownNodeinfo.new_template(self.config.domain) 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')