import logging import requests import uuid import xml from django.conf import settings from django.urls import reverse from django.utils import timezone from rest_framework.exceptions import PermissionDenied from dynamic_preferences.registries import global_preferences_registry from . import activity from . import models from . import serializers from . import utils logger = logging.getLogger(__name__) def remove_tags(text): logger.debug('Removing tags from %s', text) return ''.join(xml.etree.ElementTree.fromstring('
{}
'.format(text)).itertext()) def get_actor_data(actor_url): response = requests.get( actor_url, headers={ 'Accept': 'application/activity+json', } ) response.raise_for_status() try: return response.json() except: raise ValueError( 'Invalid actor payload: {}'.format(response.text)) def get_actor(actor_url): data = get_actor_data(actor_url) serializer = serializers.ActorSerializer(data=data) serializer.is_valid(raise_exception=True) return serializer.build() class SystemActor(object): additional_attributes = {} manually_approves_followers = False def get_actor_instance(self): args = self.get_instance_argument( self.id, name=self.name, summary=self.summary, **self.additional_attributes ) url = args.pop('url') a, created = models.Actor.objects.get_or_create( url=url, defaults=args, ) return a def get_instance_argument(self, id, name, summary, **kwargs): preferences = global_preferences_registry.manager() p = { 'preferred_username': id, 'domain': settings.FEDERATION_HOSTNAME, 'type': 'Person', 'name': name.format(host=settings.FEDERATION_HOSTNAME), 'manually_approves_followers': True, 'url': utils.full_url( reverse( 'federation:instance-actors-detail', kwargs={'actor': id})), 'shared_inbox_url': utils.full_url( reverse( 'federation:instance-actors-inbox', kwargs={'actor': id})), 'inbox_url': utils.full_url( reverse( 'federation:instance-actors-inbox', kwargs={'actor': id})), 'outbox_url': utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': id})), 'public_key': preferences['federation__public_key'], 'private_key': preferences['federation__private_key'], 'summary': summary.format(host=settings.FEDERATION_HOSTNAME) } p.update(kwargs) return p def get_inbox(self, data, actor=None): raise NotImplementedError def post_inbox(self, data, actor=None): return self.handle(data, actor=actor) def get_outbox(self, data, actor=None): raise NotImplementedError def post_outbox(self, data, actor=None): raise NotImplementedError def handle(self, data, actor=None): """ Main entrypoint for handling activities posted to the actor's inbox """ logger.info('Received activity on %s inbox', self.id) if actor is None: raise PermissionDenied('Actor not authenticated') serializer = serializers.ActivitySerializer( data=data, context={'actor': actor}) serializer.is_valid(raise_exception=True) ac = serializer.data try: handler = getattr( self, 'handle_{}'.format(ac['type'].lower())) except (KeyError, AttributeError): logger.debug( 'No handler for activity %s', ac['type']) return return handler(ac, actor) class LibraryActor(SystemActor): id = 'library' name = '{host}\'s library' summary = 'Bot account to federate with {host}\'s library' additional_attributes = { 'manually_approves_followers': True } @property def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL class TestActor(SystemActor): id = 'test' name = '{host}\'s test account' summary = ( 'Bot account to test federation with {host}. ' 'Send me /ping and I\'ll answer you.' ) additional_attributes = { 'manually_approves_followers': False } manually_approves_followers = False def get_outbox(self, data, actor=None): return { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {} ], "id": utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': self.id})), "type": "OrderedCollection", "totalItems": 0, "orderedItems": [] } def parse_command(self, message): """ Remove any links or fancy markup to extract /command from a note message. """ raw = remove_tags(message) try: return raw.split('/')[1] except IndexError: return def handle_create(self, ac, sender): if ac['object']['type'] != 'Note': return # we received a toot \o/ command = self.parse_command(ac['object']['content']) logger.debug('Parsed command: %s', command) if command != 'ping': return now = timezone.now() test_actor = self.get_actor_instance() reply_url = 'https://{}/activities/note/{}'.format( settings.FEDERATION_HOSTNAME, now.timestamp() ) reply_content = '{} Pong!'.format( sender.mention_username ) reply_activity = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {} ], 'type': 'Create', 'actor': test_actor.url, 'id': '{}/activity'.format(reply_url), 'published': now.isoformat(), 'to': ac['actor'], 'cc': [], 'object': { 'type': 'Note', 'content': 'Pong!', 'summary': None, 'published': now.isoformat(), 'id': reply_url, 'inReplyTo': ac['object']['id'], 'sensitive': False, 'url': reply_url, 'to': [ac['actor']], 'attributedTo': test_actor.url, 'cc': [], 'attachment': [], 'tag': [{ "type": "Mention", "href": ac['actor'], "name": sender.mention_username }] } } activity.deliver( reply_activity, to=[ac['actor']], on_behalf_of=test_actor) def handle_follow(self, ac, sender): # on a follow we: # 1. send the accept answer # 2. follow back # test_actor = self.get_actor_instance() accept_uuid = uuid.uuid4() accept = activity.get_accept_follow( accept_id=accept_uuid, accept_actor=test_actor, follow=ac, follow_actor=sender) activity.deliver( accept, to=[ac['actor']], on_behalf_of=test_actor) models.Follow.objects.get_or_create( actor=sender, target=test_actor, ) follow_uuid = uuid.uuid4() follow = activity.get_follow( follow_id=follow_uuid, follower=test_actor, followed=sender) activity.deliver( follow, to=[ac['actor']], on_behalf_of=test_actor) SYSTEM_ACTORS = { 'library': LibraryActor(), 'test': TestActor(), }