mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-14 11:37:59 +00:00
convert activitypub routes to use register_route
This commit is contained in:
parent
091f8175b5
commit
b00daa5a78
|
@ -4,10 +4,11 @@ import typing
|
|||
|
||||
from . import logger as logging
|
||||
from .database import Connection
|
||||
from .misc import Message
|
||||
from .misc import Message, get_app
|
||||
|
||||
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:
|
||||
|
@ -21,98 +22,98 @@ def actor_type_check(actor: Message, software: str | None) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
async def handle_relay(view: ActorView, conn: Connection) -> None:
|
||||
async def handle_relay(app: Application, data: InboxData, conn: Connection) -> None:
|
||||
try:
|
||||
view.cache.get('handle-relay', view.message.object_id)
|
||||
logging.verbose('already relayed %s', view.message.object_id)
|
||||
app.cache.get('handle-relay', data.message.object_id)
|
||||
logging.verbose('already relayed %s', data.message.object_id)
|
||||
return
|
||||
|
||||
except KeyError:
|
||||
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)
|
||||
|
||||
for instance in conn.distill_inboxes(view.message):
|
||||
view.app.push_message(instance.inbox, message, instance)
|
||||
for instance in conn.distill_inboxes(data.message):
|
||||
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:
|
||||
view.cache.get('handle-relay', view.message.id)
|
||||
logging.verbose('already forwarded %s', view.message.id)
|
||||
app.cache.get('handle-relay', data.message.id)
|
||||
logging.verbose('already forwarded %s', data.message.id)
|
||||
return
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
message = Message.new_announce(view.config.domain, view.message)
|
||||
message = Message.new_announce(app.config.domain, data.message)
|
||||
logging.debug('>> forward: %s', message)
|
||||
|
||||
for instance in conn.distill_inboxes(view.message):
|
||||
view.app.push_message(instance.inbox, view.message, instance)
|
||||
for instance in conn.distill_inboxes(data.message):
|
||||
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:
|
||||
nodeinfo = await view.client.fetch_nodeinfo(view.actor.domain, force = True)
|
||||
async def handle_follow(app: Application, data: InboxData, conn: Connection) -> None:
|
||||
nodeinfo = await app.client.fetch_nodeinfo(data.actor.domain, force = True)
|
||||
software = nodeinfo.sw_name if nodeinfo else None
|
||||
config = conn.get_config_all()
|
||||
|
||||
# reject if software used by actor is banned
|
||||
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(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_response(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
accept = False
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
logging.verbose(
|
||||
'Rejected follow from actor for using specific software: actor=%s, software=%s',
|
||||
view.actor.id,
|
||||
data.actor.id,
|
||||
software
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
# reject if the actor is not an instance actor
|
||||
if actor_type_check(view.actor, software):
|
||||
logging.verbose('Non-application actor tried to follow: %s', view.actor.id)
|
||||
if actor_type_check(data.actor, software):
|
||||
logging.verbose('Non-application actor tried to follow: %s', data.actor.id)
|
||||
|
||||
view.app.push_message(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_response(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
accept = False
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
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
|
||||
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():
|
||||
view.instance = conn.put_inbox(
|
||||
domain = view.actor.domain,
|
||||
inbox = view.actor.shared_inbox,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
data.instance = conn.put_inbox(
|
||||
domain = data.actor.domain,
|
||||
inbox = data.actor.shared_inbox,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
software = software,
|
||||
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
|
||||
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(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_response(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
accept = False
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
with conn.transaction():
|
||||
view.instance = conn.put_inbox(
|
||||
domain = view.actor.domain,
|
||||
inbox = view.actor.shared_inbox,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
data.instance = conn.put_inbox(
|
||||
domain = data.actor.domain,
|
||||
inbox = data.actor.shared_inbox,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
software = software,
|
||||
accepted = True
|
||||
)
|
||||
|
||||
view.app.push_message(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_response(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id,
|
||||
followid = view.message.id,
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id,
|
||||
followid = data.message.id,
|
||||
accept = True
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
# Are Akkoma and Pleroma the only two that expect a follow back?
|
||||
# Ignoring only Mastodon for now
|
||||
if software != 'mastodon':
|
||||
view.app.push_message(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_follow(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
|
||||
async def handle_undo(view: ActorView, conn: Connection) -> None:
|
||||
if view.message.object['type'] != 'Follow':
|
||||
async def handle_undo(app: Application, data: InboxData, conn: Connection) -> None:
|
||||
if data.message.object['type'] != 'Follow':
|
||||
# forwarding deletes does not work, so don't bother
|
||||
# await handle_forward(view, conn)
|
||||
# await handle_forward(app, data, conn)
|
||||
return
|
||||
|
||||
if data.instance is None:
|
||||
raise ValueError(f"Actor not in database: {data.actor.id}")
|
||||
|
||||
# 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
|
||||
|
||||
with conn.transaction():
|
||||
if not conn.del_inbox(view.actor.id):
|
||||
if not conn.del_inbox(data.actor.id):
|
||||
logging.verbose(
|
||||
'Failed to delete "%s" with follow ID "%s"',
|
||||
view.actor.id,
|
||||
view.message.object_id
|
||||
data.actor.id,
|
||||
data.message.object_id
|
||||
)
|
||||
|
||||
view.app.push_message(
|
||||
view.actor.shared_inbox,
|
||||
app.push_message(
|
||||
data.actor.shared_inbox,
|
||||
Message.new_unfollow(
|
||||
host = view.config.domain,
|
||||
actor = view.actor.id,
|
||||
follow = view.message
|
||||
host = app.config.domain,
|
||||
actor = data.actor.id,
|
||||
follow = data.message
|
||||
),
|
||||
view.instance
|
||||
data.instance
|
||||
)
|
||||
|
||||
|
||||
|
@ -209,32 +213,34 @@ processors = {
|
|||
}
|
||||
|
||||
|
||||
async def run_processor(view: ActorView) -> None:
|
||||
if view.message.type not in processors:
|
||||
async def run_processor(data: InboxData) -> None:
|
||||
if data.message.type not in processors:
|
||||
logging.verbose(
|
||||
'Message type "%s" from actor cannot be handled: %s',
|
||||
view.message.type,
|
||||
view.actor.id
|
||||
data.message.type,
|
||||
data.actor.id
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
with view.database.session() as conn:
|
||||
if view.instance:
|
||||
if not view.instance.software:
|
||||
if (nodeinfo := await view.client.fetch_nodeinfo(view.instance.domain)):
|
||||
app = get_app()
|
||||
|
||||
with app.database.session() as conn:
|
||||
if data.instance:
|
||||
if not data.instance.software:
|
||||
if (nodeinfo := await app.client.fetch_nodeinfo(data.instance.domain)):
|
||||
with conn.transaction():
|
||||
view.instance = conn.put_inbox(
|
||||
domain = view.instance.domain,
|
||||
data.instance = conn.put_inbox(
|
||||
domain = data.instance.domain,
|
||||
software = nodeinfo.sw_name
|
||||
)
|
||||
|
||||
if not view.instance.actor:
|
||||
if not data.instance.actor:
|
||||
with conn.transaction():
|
||||
view.instance = conn.put_inbox(
|
||||
domain = view.instance.domain,
|
||||
actor = view.actor.id
|
||||
data.instance = conn.put_inbox(
|
||||
domain = data.instance.domain,
|
||||
actor = data.actor.id
|
||||
)
|
||||
|
||||
logging.verbose('New "%s" from actor: %s', view.message.type, view.actor.id)
|
||||
await processors[view.message.type](view, conn)
|
||||
logging.verbose('New "%s" from actor: %s', data.message.type, data.actor.id)
|
||||
await processors[data.message.type](app, data, conn)
|
||||
|
|
|
@ -6,11 +6,11 @@ import traceback
|
|||
from aiohttp import ClientConnectorError
|
||||
from aiohttp.web import Request
|
||||
from aputils import Signature, SignatureFailureError, Signer
|
||||
from blib import HttpError
|
||||
from blib import HttpError, HttpMethod
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import View, register_view
|
||||
from .base import register_route
|
||||
|
||||
from .. import logger as logging
|
||||
from ..database import schema
|
||||
|
@ -27,14 +27,6 @@ if TYPE_CHECKING:
|
|||
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
|
||||
|
@ -45,7 +37,7 @@ class InboxData:
|
|||
|
||||
|
||||
@classmethod
|
||||
async def parse_request(cls: type[Self], app: Application, request: Request) -> Self:
|
||||
async def parse(cls: type[Self], app: Application, request: Request) -> Self:
|
||||
signature: Signature | None = None
|
||||
message: Message | None = None
|
||||
actor: Message | None = None
|
||||
|
@ -112,169 +104,94 @@ class InboxData:
|
|||
return cls(signature, message, actor, signer, None)
|
||||
|
||||
|
||||
class ActorView(View):
|
||||
signature: aputils.Signature
|
||||
message: Message
|
||||
actor: Message
|
||||
instance: schema.Instance
|
||||
signer: aputils.Signer
|
||||
@register_route(HttpMethod.GET, "/actor", "/inbox")
|
||||
async def handle_actor(app: Application, request: Request) -> Response:
|
||||
with app.database.session(False) as conn:
|
||||
config = conn.get_config_all()
|
||||
|
||||
data = Message.new_actor(
|
||||
host = app.config.domain,
|
||||
pubkey = app.signer.pubkey,
|
||||
description = app.template.render_markdown(config.note),
|
||||
approves = config.approval_required
|
||||
)
|
||||
|
||||
return Response.new(data, ctype = "activity")
|
||||
|
||||
|
||||
def __init__(self, request: Request):
|
||||
View.__init__(self, request)
|
||||
@register_route(HttpMethod.POST, "/actor", "/inbox")
|
||||
async def handle_inbox(app: Application, request: Request) -> Response:
|
||||
data = await InboxData.parse(app, request)
|
||||
|
||||
with app.database.session() as conn:
|
||||
data.instance = conn.get_inbox(data.actor.shared_inbox)
|
||||
|
||||
# reject if actor is banned
|
||||
if conn.get_domain_ban(data.actor.domain):
|
||||
logging.verbose('Ignored request from banned actor: %s', data.actor.id)
|
||||
raise HttpError(403, 'access denied')
|
||||
|
||||
# reject if activity type isn't 'Follow' and the actor isn't following
|
||||
if data.message.type != 'Follow' and not data.instance:
|
||||
logging.verbose(
|
||||
'Rejected actor for trying to post while not following: %s',
|
||||
data.actor.id
|
||||
)
|
||||
|
||||
raise HttpError(401, 'access denied')
|
||||
|
||||
logging.debug('>> payload %s', data.message.to_json(4))
|
||||
|
||||
await run_processor(data)
|
||||
return Response.new(status = 202)
|
||||
|
||||
|
||||
async def get(self, request: Request) -> Response:
|
||||
with self.database.session(False) as conn:
|
||||
config = conn.get_config_all()
|
||||
@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": []
|
||||
}
|
||||
)
|
||||
|
||||
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')
|
||||
return Response.new(msg, ctype = "activity")
|
||||
|
||||
|
||||
async def post(self, request: Request) -> Response:
|
||||
await self.get_post_data()
|
||||
@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()]
|
||||
|
||||
with self.database.session() as conn:
|
||||
self.instance = conn.get_inbox(self.actor.shared_inbox) # type: ignore[assignment]
|
||||
msg = aputils.Message.new(
|
||||
aputils.ObjectType.COLLECTION,
|
||||
{
|
||||
"id": f'https://{app.config.domain}{request.path}',
|
||||
"totalItems": len(inboxes),
|
||||
"items": inboxes
|
||||
}
|
||||
)
|
||||
|
||||
# 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)
|
||||
return Response.new(msg, ctype = "activity")
|
||||
|
||||
|
||||
async def get_post_data(self) -> None:
|
||||
try:
|
||||
self.signature = aputils.Signature.parse(self.request.headers['signature'])
|
||||
@register_route(HttpMethod.GET, '/.well-known/webfinger')
|
||||
async def get(app: Application, request: Request) -> Response:
|
||||
try:
|
||||
subject = request.query['resource']
|
||||
|
||||
except KeyError:
|
||||
logging.verbose('Missing signature header')
|
||||
raise HttpError(400, 'missing signature header')
|
||||
except KeyError:
|
||||
raise HttpError(400, 'missing "resource" query key')
|
||||
|
||||
try:
|
||||
message: Message | None = await self.request.json(loads = Message.parse)
|
||||
if subject != f'acct:relay@{app.config.domain}':
|
||||
raise HttpError(404, 'user not found')
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logging.verbose('Failed to parse message from actor: %s', self.signature.keyid)
|
||||
raise HttpError(400, 'failed to parse message')
|
||||
data = aputils.Webfinger.new(
|
||||
handle = 'relay',
|
||||
domain = app.config.domain,
|
||||
actor = app.config.actor
|
||||
)
|
||||
|
||||
if message is None:
|
||||
logging.verbose('empty message')
|
||||
raise HttpError(400, 'missing message')
|
||||
|
||||
self.message = message
|
||||
|
||||
if 'actor' not in self.message:
|
||||
logging.verbose('actor not in message')
|
||||
raise HttpError(400, 'no actor in message')
|
||||
|
||||
try:
|
||||
self.actor = await self.client.get(self.signature.keyid, True, Message)
|
||||
|
||||
except HttpError as e:
|
||||
# ld signatures aren't handled atm, so just ignore it
|
||||
if self.message.type == 'Delete':
|
||||
logging.verbose('Instance sent a delete which cannot be handled')
|
||||
raise HttpError(202, '')
|
||||
|
||||
logging.verbose('Failed to fetch actor: %s', self.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', self.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:
|
||||
self.signer = self.actor.signer
|
||||
|
||||
except KeyError:
|
||||
logging.verbose('Actor missing public key: %s', self.signature.keyid)
|
||||
raise HttpError(400, 'actor missing public key')
|
||||
|
||||
try:
|
||||
await self.signer.validate_request_async(self.request)
|
||||
|
||||
except aputils.SignatureFailureError as e:
|
||||
logging.verbose('signature validation failed for "%s": %s', self.actor.id, e)
|
||||
raise HttpError(401, str(e))
|
||||
|
||||
|
||||
@register_view('/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_view('/following', '/followers')
|
||||
class RelationshipView(View):
|
||||
async def get(self, request: Request) -> Response:
|
||||
with self.database.session(False) as s:
|
||||
inboxes = [row['actor'] for row in s.get_inboxes()]
|
||||
|
||||
msg = aputils.Message.new(
|
||||
aputils.ObjectType.COLLECTION,
|
||||
{
|
||||
"id": f'https://{self.config.domain}{request.path}',
|
||||
"totalItems": len(inboxes),
|
||||
"items": inboxes
|
||||
}
|
||||
)
|
||||
|
||||
return Response.new(msg, ctype = 'activity')
|
||||
|
||||
|
||||
@register_view('/.well-known/webfinger')
|
||||
class WebfingerView(View):
|
||||
async def get(self, request: Request) -> Response:
|
||||
try:
|
||||
subject = request.query['resource']
|
||||
|
||||
except KeyError:
|
||||
raise HttpError(400, 'missing "resource" query key')
|
||||
|
||||
if subject != f'acct:relay@{self.config.domain}':
|
||||
raise HttpError(404, 'user not found')
|
||||
|
||||
data = aputils.Webfinger.new(
|
||||
handle = 'relay',
|
||||
domain = self.config.domain,
|
||||
actor = self.config.actor
|
||||
)
|
||||
|
||||
return Response.new(data, ctype = 'json')
|
||||
return Response.new(data, ctype = 'json')
|
||||
|
|
Loading…
Reference in a new issue