sedi-relay/relay/processors.py

205 lines
5.5 KiB
Python

import asyncio
import logging
from cachetools import LRUCache
from uuid import uuid4
from .database import RELAY_SOFTWARE
from .misc import Message
cache = LRUCache(1024)
def person_check(actor, software):
## pleroma and akkoma use Person for the actor type for some reason
if software in {'akkoma', 'pleroma'} and actor.id != f'https://{actor.domain}/relay':
return True
## make sure the actor is an application
elif actor.type != 'Application':
return True
async def handle_relay(request, s):
if request.message.objectid in cache:
logging.verbose(f'already relayed {request.message.objectid}')
return
if request.message.get('to') != ['https://www.w3.org/ns/activitystreams#Public']:
logging.verbose('Message was not public')
logging.verbose(request.message.get('to'))
return
message = Message.new_announce(
host = request.config.host,
object = request.message.objectid
)
cache[request.message.objectid] = message.id
logging.debug(f'>> relay: {message}')
inboxes = s.distill_inboxes(request.message)
for inbox in inboxes:
request.app.push_message(inbox, message)
async def handle_forward(request, s):
if request.message.id in cache:
logging.verbose(f'already forwarded {request.message.id}')
return
message = Message.new_announce(
host = request.config.host,
object = request.message
)
cache[request.message.id] = message.id
logging.debug(f'>> forward: {message}')
inboxes = s.distill_inboxes(request.message)
for inbox in inboxes:
request.app.push_message(inbox, message)
async def handle_follow(request, s):
approve = True
nodeinfo = await request.app.client.fetch_nodeinfo(request.actor.domain)
software = nodeinfo.sw_name if nodeinfo else None
## reject if the actor isn't whitelisted while the whiltelist is enabled
if s.get_config('whitelist') and not s.get_whitelist(request.actor.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {request.actor.id}')
approve = False
## reject if software used by actor is banned
if s.get_ban('software', software):
logging.verbose(f'Rejected follow from actor for using specific software: actor={request.actor.id}, software={software}')
approve = False
## reject if the actor is not an instance actor
if person_check(request.actor, software):
logging.verbose(f'Non-application actor tried to follow: {request.actor.id}')
approve = False
if approve:
if not request.instance:
s.put_instance(
domain = request.actor.domain,
actor = request.actor.id,
inbox = request.actor.shared_inbox,
actor_data = request.actor,
software = software,
followid = request.message.id,
accept = not s.get_config('require_approval')
)
if s.get_config('require_approval'):
return
else:
s.put_instance(
domain = request.actor.domain,
followid = request.message.id
)
# Rejects don't seem to work right with mastodon
request.app.push_message(
request.actor.inbox,
Message.new_response(
host = request.config.host,
actor = request.message.actorid,
followid = request.message.id,
accept = approve
)
)
## Don't send a follow if the the follow has been rejected
if not approve:
return
## Make sure two relays aren't continuously following each other
if software in RELAY_SOFTWARE and not request.instance:
return
# Are Akkoma and Pleroma the only two that expect a follow back?
# Ignoring only Mastodon for now
if software != 'mastodon':
request.app.push_message(
request.actor.shared_inbox,
Message.new_follow(
host = request.config.host,
actor = request.actor.id
)
)
async def handle_undo(request, s):
## If the object is not a Follow, forward it
if request.message.object.type != 'Follow':
return await handle_forward(request)
instance_follow = request.instance.followid
message_follow = request.message.object.id
if person_check(request.actor, request.instance.software):
return logging.verbose(f'Non-application actor tried to unfollow: {request.actor.id}')
if instance_follow and instance_follow != message_follow:
return logging.verbose(f'Followid does not match: {instance_follow}, {message_follow}')
s.delete('instances', id=request.instance.id)
logging.verbose(f'Removed inbox: {request.instance.inbox}')
if request.instance.software != 'mastodon':
request.app.push_message(
request.actor.shared_inbox,
Message.new_unfollow(
host = request.config.host,
actor = request.actor.id,
follow = request.message
)
)
processors = {
'Announce': handle_relay,
'Create': handle_relay,
'Delete': handle_forward,
'Follow': handle_follow,
'Undo': handle_undo,
'Update': handle_forward,
}
async def run_processor(request):
if request.message.type not in processors:
return
with request.database.session as s:
if request.instance:
new_data = {}
if not request.instance.software:
logging.verbose(f'Fetching nodeinfo for instance: {request.instance.domain}')
nodeinfo = await request.app.client.fetch_nodeinfo(request.instance.domain)
if nodeinfo:
new_data['software'] = nodeinfo.sw_name
if not request.instance.actor:
logging.verbose(f'Fetching actor for instance: {request.instance.domain}')
new_data['actor'] = request.signature.keyid.split('#', 1)[0]
if not request.instance.actor_data:
new_data['actor_data'] = request.actor
if new_data:
s.put_instance(request.actor.domain, **new_data)
logging.verbose(f'New "{request.message.type}" from actor: {request.actor.id}')
return await processors[request.message.type](request, s)