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)