mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-23 15:08:00 +00:00
Compare commits
3 commits
e0ca93ab93
...
b00daa5a78
Author | SHA1 | Date | |
---|---|---|---|
b00daa5a78 | |||
091f8175b5 | |||
f9d6d7b18d |
|
@ -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,
|
||||||
|
@ -295,14 +298,12 @@ class CacheCleanupThread(Thread):
|
||||||
|
|
||||||
|
|
||||||
def format_error(request: web.Request, error: HttpError) -> Response:
|
def format_error(request: web.Request, error: HttpError) -> Response:
|
||||||
app: Application = request.app # type: ignore[assignment]
|
|
||||||
|
|
||||||
if request.path.startswith(JSON_PATHS) or 'json' in request.headers.get('accept', ''):
|
if request.path.startswith(JSON_PATHS) or 'json' in request.headers.get('accept', ''):
|
||||||
return Response.new({'error': error.message}, error.status, ctype = 'json')
|
return Response.new({'error': error.message}, error.status, ctype = 'json')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
body = app.template.render('page/error.haml', request, e = error)
|
context = {"e": error}
|
||||||
return Response.new(body, error.status, ctype = 'html')
|
return Response.new_template(error.status, "page/error.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
|
|
|
@ -5,7 +5,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
from aiohttp.web import Response as AiohttpResponse
|
from aiohttp.web import Request, Response as AiohttpResponse
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar
|
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar
|
||||||
|
@ -207,6 +207,19 @@ class Response(AiohttpResponse):
|
||||||
return cls.new(body, status, {'Location': path}, ctype = 'html')
|
return cls.new(body, status, {'Location': path}, ctype = 'html')
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_template(cls: type[Self],
|
||||||
|
status: int,
|
||||||
|
path: str,
|
||||||
|
request: Request,
|
||||||
|
context: dict[str, Any] | None = None,
|
||||||
|
headers: dict[str, Any] | None = None,
|
||||||
|
ctype: str = "html") -> Self:
|
||||||
|
|
||||||
|
body = get_app().template.render(path, request, **(context or {}))
|
||||||
|
return cls.new(body, status, headers, ctype)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self) -> str:
|
def location(self) -> str:
|
||||||
return self.headers.get('Location', '')
|
return self.headers.get('Location', '')
|
||||||
|
|
|
@ -4,10 +4,11 @@ import typing
|
||||||
|
|
||||||
from . import logger as logging
|
from . import logger as logging
|
||||||
from .database import Connection
|
from .database import Connection
|
||||||
from .misc import Message
|
from .misc import Message, get_app
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from .views.activitypub import ActorView
|
from .app import Application
|
||||||
|
from .views.activitypub import InboxData
|
||||||
|
|
||||||
|
|
||||||
def actor_type_check(actor: Message, software: str | None) -> bool:
|
def actor_type_check(actor: Message, software: str | None) -> bool:
|
||||||
|
@ -21,98 +22,98 @@ def actor_type_check(actor: Message, software: str | None) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def handle_relay(view: ActorView, conn: Connection) -> None:
|
async def handle_relay(app: Application, data: InboxData, conn: Connection) -> None:
|
||||||
try:
|
try:
|
||||||
view.cache.get('handle-relay', view.message.object_id)
|
app.cache.get('handle-relay', data.message.object_id)
|
||||||
logging.verbose('already relayed %s', view.message.object_id)
|
logging.verbose('already relayed %s', data.message.object_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
message = Message.new_announce(view.config.domain, view.message.object_id)
|
message = Message.new_announce(app.config.domain, data.message.object_id)
|
||||||
logging.debug('>> relay: %s', message)
|
logging.debug('>> relay: %s', message)
|
||||||
|
|
||||||
for instance in conn.distill_inboxes(view.message):
|
for instance in conn.distill_inboxes(data.message):
|
||||||
view.app.push_message(instance.inbox, message, instance)
|
app.push_message(instance.inbox, message, instance)
|
||||||
|
|
||||||
view.cache.set('handle-relay', view.message.object_id, message.id, 'str')
|
app.cache.set('handle-relay', data.message.object_id, message.id, 'str')
|
||||||
|
|
||||||
|
|
||||||
async def handle_forward(view: ActorView, conn: Connection) -> None:
|
async def handle_forward(app: Application, data: InboxData, conn: Connection) -> None:
|
||||||
try:
|
try:
|
||||||
view.cache.get('handle-relay', view.message.id)
|
app.cache.get('handle-relay', data.message.id)
|
||||||
logging.verbose('already forwarded %s', view.message.id)
|
logging.verbose('already forwarded %s', data.message.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
message = Message.new_announce(view.config.domain, view.message)
|
message = Message.new_announce(app.config.domain, data.message)
|
||||||
logging.debug('>> forward: %s', message)
|
logging.debug('>> forward: %s', message)
|
||||||
|
|
||||||
for instance in conn.distill_inboxes(view.message):
|
for instance in conn.distill_inboxes(data.message):
|
||||||
view.app.push_message(instance.inbox, view.message, instance)
|
app.push_message(instance.inbox, data.message, instance)
|
||||||
|
|
||||||
view.cache.set('handle-relay', view.message.id, message.id, 'str')
|
app.cache.set('handle-relay', data.message.id, message.id, 'str')
|
||||||
|
|
||||||
|
|
||||||
async def handle_follow(view: ActorView, conn: Connection) -> None:
|
async def handle_follow(app: Application, data: InboxData, conn: Connection) -> None:
|
||||||
nodeinfo = await view.client.fetch_nodeinfo(view.actor.domain, force = True)
|
nodeinfo = await app.client.fetch_nodeinfo(data.actor.domain, force = True)
|
||||||
software = nodeinfo.sw_name if nodeinfo else None
|
software = nodeinfo.sw_name if nodeinfo else None
|
||||||
config = conn.get_config_all()
|
config = conn.get_config_all()
|
||||||
|
|
||||||
# reject if software used by actor is banned
|
# reject if software used by actor is banned
|
||||||
if software and conn.get_software_ban(software):
|
if software and conn.get_software_ban(software):
|
||||||
logging.verbose('Rejected banned actor: %s', view.actor.id)
|
logging.verbose('Rejected banned actor: %s', data.actor.id)
|
||||||
|
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_response(
|
Message.new_response(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
accept = False
|
accept = False
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.verbose(
|
logging.verbose(
|
||||||
'Rejected follow from actor for using specific software: actor=%s, software=%s',
|
'Rejected follow from actor for using specific software: actor=%s, software=%s',
|
||||||
view.actor.id,
|
data.actor.id,
|
||||||
software
|
software
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# reject if the actor is not an instance actor
|
# reject if the actor is not an instance actor
|
||||||
if actor_type_check(view.actor, software):
|
if actor_type_check(data.actor, software):
|
||||||
logging.verbose('Non-application actor tried to follow: %s', view.actor.id)
|
logging.verbose('Non-application actor tried to follow: %s', data.actor.id)
|
||||||
|
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_response(
|
Message.new_response(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
accept = False
|
accept = False
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not conn.get_domain_whitelist(view.actor.domain):
|
if not conn.get_domain_whitelist(data.actor.domain):
|
||||||
# add request if approval-required is enabled
|
# add request if approval-required is enabled
|
||||||
if config.approval_required:
|
if config.approval_required:
|
||||||
logging.verbose('New follow request fromm actor: %s', view.actor.id)
|
logging.verbose('New follow request fromm actor: %s', data.actor.id)
|
||||||
|
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
view.instance = conn.put_inbox(
|
data.instance = conn.put_inbox(
|
||||||
domain = view.actor.domain,
|
domain = data.actor.domain,
|
||||||
inbox = view.actor.shared_inbox,
|
inbox = data.actor.shared_inbox,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
software = software,
|
software = software,
|
||||||
accepted = False
|
accepted = False
|
||||||
)
|
)
|
||||||
|
@ -121,81 +122,84 @@ async def handle_follow(view: ActorView, conn: Connection) -> None:
|
||||||
|
|
||||||
# reject if the actor isn't whitelisted while the whiltelist is enabled
|
# reject if the actor isn't whitelisted while the whiltelist is enabled
|
||||||
if config.whitelist_enabled:
|
if config.whitelist_enabled:
|
||||||
logging.verbose('Rejected actor for not being in the whitelist: %s', view.actor.id)
|
logging.verbose('Rejected actor for not being in the whitelist: %s', data.actor.id)
|
||||||
|
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_response(
|
Message.new_response(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
accept = False
|
accept = False
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
view.instance = conn.put_inbox(
|
data.instance = conn.put_inbox(
|
||||||
domain = view.actor.domain,
|
domain = data.actor.domain,
|
||||||
inbox = view.actor.shared_inbox,
|
inbox = data.actor.shared_inbox,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
software = software,
|
software = software,
|
||||||
accepted = True
|
accepted = True
|
||||||
)
|
)
|
||||||
|
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_response(
|
Message.new_response(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
followid = view.message.id,
|
followid = data.message.id,
|
||||||
accept = True
|
accept = True
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
# Are Akkoma and Pleroma the only two that expect a follow back?
|
# Are Akkoma and Pleroma the only two that expect a follow back?
|
||||||
# Ignoring only Mastodon for now
|
# Ignoring only Mastodon for now
|
||||||
if software != 'mastodon':
|
if software != 'mastodon':
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_follow(
|
Message.new_follow(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id
|
actor = data.actor.id
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_undo(view: ActorView, conn: Connection) -> None:
|
async def handle_undo(app: Application, data: InboxData, conn: Connection) -> None:
|
||||||
if view.message.object['type'] != 'Follow':
|
if data.message.object['type'] != 'Follow':
|
||||||
# forwarding deletes does not work, so don't bother
|
# forwarding deletes does not work, so don't bother
|
||||||
# await handle_forward(view, conn)
|
# await handle_forward(app, data, conn)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if data.instance is None:
|
||||||
|
raise ValueError(f"Actor not in database: {data.actor.id}")
|
||||||
|
|
||||||
# prevent past unfollows from removing an instance
|
# prevent past unfollows from removing an instance
|
||||||
if view.instance.followid and view.instance.followid != view.message.object_id:
|
if data.instance.followid and data.instance.followid != data.message.object_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
if not conn.del_inbox(view.actor.id):
|
if not conn.del_inbox(data.actor.id):
|
||||||
logging.verbose(
|
logging.verbose(
|
||||||
'Failed to delete "%s" with follow ID "%s"',
|
'Failed to delete "%s" with follow ID "%s"',
|
||||||
view.actor.id,
|
data.actor.id,
|
||||||
view.message.object_id
|
data.message.object_id
|
||||||
)
|
)
|
||||||
|
|
||||||
view.app.push_message(
|
app.push_message(
|
||||||
view.actor.shared_inbox,
|
data.actor.shared_inbox,
|
||||||
Message.new_unfollow(
|
Message.new_unfollow(
|
||||||
host = view.config.domain,
|
host = app.config.domain,
|
||||||
actor = view.actor.id,
|
actor = data.actor.id,
|
||||||
follow = view.message
|
follow = data.message
|
||||||
),
|
),
|
||||||
view.instance
|
data.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,32 +213,34 @@ processors = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def run_processor(view: ActorView) -> None:
|
async def run_processor(data: InboxData) -> None:
|
||||||
if view.message.type not in processors:
|
if data.message.type not in processors:
|
||||||
logging.verbose(
|
logging.verbose(
|
||||||
'Message type "%s" from actor cannot be handled: %s',
|
'Message type "%s" from actor cannot be handled: %s',
|
||||||
view.message.type,
|
data.message.type,
|
||||||
view.actor.id
|
data.actor.id
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with view.database.session() as conn:
|
app = get_app()
|
||||||
if view.instance:
|
|
||||||
if not view.instance.software:
|
with app.database.session() as conn:
|
||||||
if (nodeinfo := await view.client.fetch_nodeinfo(view.instance.domain)):
|
if data.instance:
|
||||||
|
if not data.instance.software:
|
||||||
|
if (nodeinfo := await app.client.fetch_nodeinfo(data.instance.domain)):
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
view.instance = conn.put_inbox(
|
data.instance = conn.put_inbox(
|
||||||
domain = view.instance.domain,
|
domain = data.instance.domain,
|
||||||
software = nodeinfo.sw_name
|
software = nodeinfo.sw_name
|
||||||
)
|
)
|
||||||
|
|
||||||
if not view.instance.actor:
|
if not data.instance.actor:
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
view.instance = conn.put_inbox(
|
data.instance = conn.put_inbox(
|
||||||
domain = view.instance.domain,
|
domain = data.instance.domain,
|
||||||
actor = view.actor.id
|
actor = data.actor.id
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.verbose('New "%s" from actor: %s', view.message.type, view.actor.id)
|
logging.verbose('New "%s" from actor: %s', data.message.type, data.actor.id)
|
||||||
await processors[view.message.type](view, conn)
|
await processors[data.message.type](app, data, conn)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,112 +1,86 @@
|
||||||
|
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 blib import HttpError
|
from aputils import Signature, SignatureFailureError, Signer
|
||||||
|
from blib import HttpError, HttpMethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .base import View, register_route
|
from .base import register_route
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@register_route('/actor', '/inbox')
|
try:
|
||||||
class ActorView(View):
|
from typing import Self
|
||||||
signature: aputils.Signature
|
|
||||||
|
except ImportError:
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots = True)
|
||||||
|
class InboxData:
|
||||||
|
signature: Signature
|
||||||
message: Message
|
message: Message
|
||||||
actor: Message
|
actor: Message
|
||||||
instance: schema.Instance
|
signer: Signer
|
||||||
signer: aputils.Signer
|
instance: schema.Instance | None
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, request: Request):
|
@classmethod
|
||||||
View.__init__(self, request)
|
async def parse(cls: type[Self], app: Application, request: Request) -> Self:
|
||||||
|
signature: Signature | None = None
|
||||||
|
message: Message | None = None
|
||||||
|
actor: Message | None = None
|
||||||
|
signer: Signer | None = None
|
||||||
|
|
||||||
|
|
||||||
async def get(self, request: Request) -> Response:
|
|
||||||
with self.database.session(False) as conn:
|
|
||||||
config = conn.get_config_all()
|
|
||||||
|
|
||||||
data = Message.new_actor(
|
|
||||||
host = self.config.domain,
|
|
||||||
pubkey = self.app.signer.pubkey,
|
|
||||||
description = self.app.template.render_markdown(config.note),
|
|
||||||
approves = config.approval_required
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.new(data, ctype='activity')
|
|
||||||
|
|
||||||
|
|
||||||
async def post(self, request: Request) -> Response:
|
|
||||||
await self.get_post_data()
|
|
||||||
|
|
||||||
with self.database.session() as conn:
|
|
||||||
self.instance = conn.get_inbox(self.actor.shared_inbox) # type: ignore[assignment]
|
|
||||||
|
|
||||||
# reject if actor is banned
|
|
||||||
if conn.get_domain_ban(self.actor.domain):
|
|
||||||
logging.verbose('Ignored request from banned actor: %s', self.actor.id)
|
|
||||||
raise HttpError(403, 'access denied')
|
|
||||||
|
|
||||||
# reject if activity type isn't 'Follow' and the actor isn't following
|
|
||||||
if self.message.type != 'Follow' and not self.instance:
|
|
||||||
logging.verbose(
|
|
||||||
'Rejected actor for trying to post while not following: %s',
|
|
||||||
self.actor.id
|
|
||||||
)
|
|
||||||
|
|
||||||
raise HttpError(401, 'access denied')
|
|
||||||
|
|
||||||
logging.debug('>> payload %s', self.message.to_json(4))
|
|
||||||
|
|
||||||
await run_processor(self)
|
|
||||||
return Response.new(status = 202)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_post_data(self) -> None:
|
|
||||||
try:
|
try:
|
||||||
self.signature = aputils.Signature.parse(self.request.headers['signature'])
|
signature = Signature.parse(request.headers['signature'])
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.verbose('Missing signature header')
|
logging.verbose('Missing signature header')
|
||||||
raise HttpError(400, 'missing signature header')
|
raise HttpError(400, 'missing signature header')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message: Message | None = await self.request.json(loads = Message.parse)
|
message = await request.json(loads = Message.parse)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
logging.verbose('Failed to parse message from actor: %s', self.signature.keyid)
|
logging.verbose('Failed to parse message from actor: %s', signature.keyid)
|
||||||
raise HttpError(400, 'failed to parse message')
|
raise HttpError(400, 'failed to parse message')
|
||||||
|
|
||||||
if message is None:
|
if message is None:
|
||||||
logging.verbose('empty message')
|
logging.verbose('empty message')
|
||||||
raise HttpError(400, 'missing message')
|
raise HttpError(400, 'missing message')
|
||||||
|
|
||||||
self.message = message
|
if 'actor' not in message:
|
||||||
|
|
||||||
if 'actor' not in self.message:
|
|
||||||
logging.verbose('actor not in message')
|
logging.verbose('actor not in message')
|
||||||
raise HttpError(400, 'no actor in message')
|
raise HttpError(400, 'no actor in message')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.actor = await self.client.get(self.signature.keyid, True, Message)
|
actor = await app.client.get(signature.keyid, True, Message)
|
||||||
|
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
# ld signatures aren't handled atm, so just ignore it
|
# ld signatures aren't handled atm, so just ignore it
|
||||||
if self.message.type == 'Delete':
|
if message.type == 'Delete':
|
||||||
logging.verbose('Instance sent a delete which cannot be handled')
|
logging.verbose('Instance sent a delete which cannot be handled')
|
||||||
raise HttpError(202, '')
|
raise HttpError(202, '')
|
||||||
|
|
||||||
logging.verbose('Failed to fetch actor: %s', self.signature.keyid)
|
logging.verbose('Failed to fetch actor: %s', signature.keyid)
|
||||||
logging.debug('HTTP Status %i: %s', e.status, e.message)
|
logging.debug('HTTP Status %i: %s', e.status, e.message)
|
||||||
raise HttpError(400, 'failed to fetch actor')
|
raise HttpError(400, 'failed to fetch actor')
|
||||||
|
|
||||||
except ClientConnectorError as e:
|
except ClientConnectorError as e:
|
||||||
logging.warning('Error when trying to fetch actor: %s, %s', self.signature.keyid, str(e))
|
logging.warning('Error when trying to fetch actor: %s, %s', signature.keyid, str(e))
|
||||||
raise HttpError(400, 'failed to fetch actor')
|
raise HttpError(400, 'failed to fetch actor')
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -114,69 +88,110 @@ class ActorView(View):
|
||||||
raise HttpError(500, 'unexpected error when fetching actor')
|
raise HttpError(500, 'unexpected error when fetching actor')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.signer = self.actor.signer
|
signer = actor.signer
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.verbose('Actor missing public key: %s', self.signature.keyid)
|
logging.verbose('Actor missing public key: %s', signature.keyid)
|
||||||
raise HttpError(400, 'actor missing public key')
|
raise HttpError(400, 'actor missing public key')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.signer.validate_request_async(self.request)
|
await signer.validate_request_async(request)
|
||||||
|
|
||||||
except aputils.SignatureFailureError as e:
|
except SignatureFailureError as e:
|
||||||
logging.verbose('signature validation failed for "%s": %s', self.actor.id, e)
|
logging.verbose('signature validation failed for "%s": %s', actor.id, e)
|
||||||
raise HttpError(401, str(e))
|
raise HttpError(401, str(e))
|
||||||
|
|
||||||
|
return cls(signature, message, actor, signer, None)
|
||||||
@register_route('/outbox')
|
|
||||||
class OutboxView(View):
|
|
||||||
async def get(self, request: Request) -> Response:
|
|
||||||
msg = aputils.Message.new(
|
|
||||||
aputils.ObjectType.ORDERED_COLLECTION,
|
|
||||||
{
|
|
||||||
"id": f'https://{self.config.domain}/outbox',
|
|
||||||
"totalItems": 0,
|
|
||||||
"orderedItems": []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.new(msg, ctype = 'activity')
|
|
||||||
|
|
||||||
|
|
||||||
@register_route('/following', '/followers')
|
@register_route(HttpMethod.GET, "/actor", "/inbox")
|
||||||
class RelationshipView(View):
|
async def handle_actor(app: Application, request: Request) -> Response:
|
||||||
async def get(self, request: Request) -> Response:
|
with app.database.session(False) as conn:
|
||||||
with self.database.session(False) as s:
|
config = conn.get_config_all()
|
||||||
inboxes = [row['actor'] for row in s.get_inboxes()]
|
|
||||||
|
|
||||||
msg = aputils.Message.new(
|
data = Message.new_actor(
|
||||||
aputils.ObjectType.COLLECTION,
|
host = app.config.domain,
|
||||||
{
|
pubkey = app.signer.pubkey,
|
||||||
"id": f'https://{self.config.domain}{request.path}',
|
description = app.template.render_markdown(config.note),
|
||||||
"totalItems": len(inboxes),
|
approves = config.approval_required
|
||||||
"items": inboxes
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.new(msg, ctype = 'activity')
|
return Response.new(data, ctype = "activity")
|
||||||
|
|
||||||
|
|
||||||
@register_route('/.well-known/webfinger')
|
@register_route(HttpMethod.POST, "/actor", "/inbox")
|
||||||
class WebfingerView(View):
|
async def handle_inbox(app: Application, request: Request) -> Response:
|
||||||
async def get(self, request: Request) -> Response:
|
data = await InboxData.parse(app, request)
|
||||||
try:
|
|
||||||
subject = request.query['resource']
|
|
||||||
|
|
||||||
except KeyError:
|
with app.database.session() as conn:
|
||||||
raise HttpError(400, 'missing "resource" query key')
|
data.instance = conn.get_inbox(data.actor.shared_inbox)
|
||||||
|
|
||||||
if subject != f'acct:relay@{self.config.domain}':
|
# reject if actor is banned
|
||||||
raise HttpError(404, 'user not found')
|
if conn.get_domain_ban(data.actor.domain):
|
||||||
|
logging.verbose('Ignored request from banned actor: %s', data.actor.id)
|
||||||
|
raise HttpError(403, 'access denied')
|
||||||
|
|
||||||
data = aputils.Webfinger.new(
|
# reject if activity type isn't 'Follow' and the actor isn't following
|
||||||
handle = 'relay',
|
if data.message.type != 'Follow' and not data.instance:
|
||||||
domain = self.config.domain,
|
logging.verbose(
|
||||||
actor = self.config.actor
|
'Rejected actor for trying to post while not following: %s',
|
||||||
)
|
data.actor.id
|
||||||
|
)
|
||||||
|
|
||||||
return Response.new(data, ctype = 'json')
|
raise HttpError(401, 'access denied')
|
||||||
|
|
||||||
|
logging.debug('>> payload %s', data.message.to_json(4))
|
||||||
|
|
||||||
|
await run_processor(data)
|
||||||
|
return Response.new(status = 202)
|
||||||
|
|
||||||
|
|
||||||
|
@register_route(HttpMethod.GET, '/outbox')
|
||||||
|
async def handle_outbox(app: Application, request: Request) -> Response:
|
||||||
|
msg = aputils.Message.new(
|
||||||
|
aputils.ObjectType.ORDERED_COLLECTION,
|
||||||
|
{
|
||||||
|
"id": f'https://{app.config.domain}/outbox',
|
||||||
|
"totalItems": 0,
|
||||||
|
"orderedItems": []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response.new(msg, ctype = "activity")
|
||||||
|
|
||||||
|
|
||||||
|
@register_route(HttpMethod.GET, '/following', '/followers')
|
||||||
|
async def handle_follow(app: Application, request: Request) -> Response:
|
||||||
|
with app.database.session(False) as s:
|
||||||
|
inboxes = [row['actor'] for row in s.get_inboxes()]
|
||||||
|
|
||||||
|
msg = aputils.Message.new(
|
||||||
|
aputils.ObjectType.COLLECTION,
|
||||||
|
{
|
||||||
|
"id": f'https://{app.config.domain}{request.path}',
|
||||||
|
"totalItems": len(inboxes),
|
||||||
|
"items": inboxes
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response.new(msg, ctype = "activity")
|
||||||
|
|
||||||
|
|
||||||
|
@register_route(HttpMethod.GET, '/.well-known/webfinger')
|
||||||
|
async def get(app: Application, request: Request) -> Response:
|
||||||
|
try:
|
||||||
|
subject = request.query['resource']
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
raise HttpError(400, 'missing "resource" query key')
|
||||||
|
|
||||||
|
if subject != f'acct:relay@{app.config.domain}':
|
||||||
|
raise HttpError(404, 'user not found')
|
||||||
|
|
||||||
|
data = aputils.Webfinger.new(
|
||||||
|
handle = 'relay',
|
||||||
|
domain = app.config.domain,
|
||||||
|
actor = app.config.actor
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response.new(data, ctype = 'json')
|
||||||
|
|
|
@ -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'], [])
|
||||||
|
@ -75,19 +75,16 @@ class OauthAuthorize(View):
|
||||||
raise HttpError(400, 'Application has already been authorized')
|
raise HttpError(400, 'Application has already been authorized')
|
||||||
|
|
||||||
if app.auth_code is not None:
|
if app.auth_code is not None:
|
||||||
context = {'application': app}
|
page = "page/authorization_show.haml"
|
||||||
html = self.template.render(
|
|
||||||
'page/authorize_show.haml', self.request, **context
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.new(html, ctype = 'html')
|
else:
|
||||||
|
page = "page/authorize_new.haml"
|
||||||
|
|
||||||
if data['redirect_uri'] != app.redirect_uri:
|
if data['redirect_uri'] != app.redirect_uri:
|
||||||
raise HttpError(400, 'redirect_uri does not match application')
|
raise HttpError(400, 'redirect_uri does not match application')
|
||||||
|
|
||||||
context = {'application': app}
|
context = {'application': app}
|
||||||
html = self.template.render('page/authorize_new.haml', self.request, **context)
|
return Response.new_template(200, page, request, context)
|
||||||
return Response.new(html, ctype = 'html')
|
|
||||||
|
|
||||||
|
|
||||||
async def post(self, request: Request) -> Response:
|
async def post(self, request: Request) -> Response:
|
||||||
|
@ -108,11 +105,7 @@ class OauthAuthorize(View):
|
||||||
|
|
||||||
if app.redirect_uri == DEFAULT_REDIRECT:
|
if app.redirect_uri == DEFAULT_REDIRECT:
|
||||||
context = {'application': app}
|
context = {'application': app}
|
||||||
html = self.template.render(
|
return Response.new_template(200, "page/authorize_show.haml", request, context)
|
||||||
'page/authorize_show.haml', self.request, **context
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.new(html, ctype = 'html')
|
|
||||||
|
|
||||||
return Response.new_redir(f'{app.redirect_uri}?code={app.auth_code}')
|
return Response.new_redir(f'{app.redirect_uri}?code={app.auth_code}')
|
||||||
|
|
||||||
|
@ -122,8 +115,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 +141,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 +160,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 +189,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 +221,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 +243,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 +292,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 +371,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 +415,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 +475,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 +531,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 +587,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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,199 @@ 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)
|
return Response.new_template(200, "page/home.haml", request, context)
|
||||||
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:
|
context = {"redir": unquote(request.query.get("redir", "/"))}
|
||||||
with self.database.session(False) as conn:
|
return Response.new_template(200, "page/login.haml", request, context)
|
||||||
config = conn.get_config_all()
|
|
||||||
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
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-instances.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
@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
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-whitelist.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-domain_bans.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-software_bans.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-users.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
|
@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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.new_template(200, "page/admin-config.haml", request, context)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
return Response.new_template(200, "variables.css", request, context, ctype = "css")
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue