Skip to content
Snippets Groups Projects
actors.py 11.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • import uuid
    
    
    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 keys
    
    from . import models
    
    from . import serializers
    
    from . import signing
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from . import utils
    
    logger = logging.getLogger(__name__)
    
    
    def remove_tags(text):
    
        logger.debug('Removing tags from %s', text)
        return ''.join(xml.etree.ElementTree.fromstring('<div>{}</div>'.format(text)).itertext())
    
    def get_actor_data(actor_url):
    
            timeout=5,
    
            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 = {}
    
        def get_request_auth(self):
            actor = self.get_actor_instance()
            return signing.get_auth(
                actor.private_key, actor.private_key_id)
    
    
        def serialize(self):
            actor = self.get_actor_instance()
    
            serializer = serializers.ActorSerializer(actor)
    
            return serializer.data
    
    
        def get_actor_instance(self):
    
            try:
                return models.Actor.objects.get(url=self.get_actor_url())
            except models.Actor.DoesNotExist:
                pass
            private, public = keys.get_key_pair()
    
            args = self.get_instance_argument(
                self.id,
                name=self.name,
                summary=self.summary,
                **self.additional_attributes
            )
    
            args['private_key'] = private.decode('utf-8')
            args['public_key'] = public.decode('utf-8')
            return models.Actor.objects.create(**args)
    
        def get_actor_url(self):
            return utils.full_url(
                reverse(
                    'federation:instance-actors-detail',
                    kwargs={'actor': self.id}))
    
    
        def get_instance_argument(self, id, name, summary, **kwargs):
            p = {
                'preferred_username': id,
                'domain': settings.FEDERATION_HOSTNAME,
                'type': 'Person',
                'name': name.format(host=settings.FEDERATION_HOSTNAME),
                'manually_approves_followers': True,
    
                'url': self.get_actor_url(),
    
                '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})),
                '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(data, actor)
    
        def handle_follow(self, ac, sender):
            system_actor = self.get_actor_instance()
            if self.manually_approves_followers:
                fr, created = models.FollowRequest.objects.get_or_create(
                    actor=sender,
                    target=system_actor,
                    approved=None,
                )
                return fr
    
            return activity.accept_follow(
                system_actor, ac, sender
            )
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        def handle_undo_follow(self, ac, sender):
            actor = self.get_actor_instance()
            models.Follow.objects.filter(
                actor=sender,
                target=actor,
            ).delete()
    
        def handle_undo(self, ac, sender):
            if ac['object']['type'] != 'Follow':
                return
    
            if ac['object']['actor'] != sender.url:
                # not the same actor, permission issue
                return
    
            self.handle_undo_follow(ac, sender)
    
    
    
    class LibraryActor(SystemActor):
        id = 'library'
        name = '{host}\'s library'
        summary = 'Bot account to federate with {host}\'s library'
        additional_attributes = {
            'manually_approves_followers': True
    
        def serialize(self):
            data = super().serialize()
            urls = data.setdefault('url', [])
            urls.append({
                'type': 'Link',
                'mediaType': 'application/activity+json',
                'name': 'library',
                'href': utils.full_url(reverse('federation:music:files-list'))
            })
            return data
    
    
        @property
        def manually_approves_followers(self):
            return settings.FEDERATION_MUSIC_NEEDS_APPROVAL
    
        def handle_follow(self, ac, sender):
            system_actor = self.get_actor_instance()
            if self.manually_approves_followers:
                fr, created = models.FollowRequest.objects.get_or_create(
                    actor=sender,
                    target=system_actor,
                    approved=None,
                )
                return fr
    
            return activity.accept_follow(
                system_actor, ac, sender
            )
    
    
        def handle_create(self, ac, sender):
    
            try:
                remote_library = models.Library.objects.get(
                    actor=sender,
                    federation_enabled=True,
                )
            except models.Library.DoesNotExist:
    
                logger.info(
                    'Skipping import, we\'re not following %s', sender.url)
                return
    
            if ac['object']['type'] != 'Collection':
                return
    
            if ac['object']['totalItems'] <= 0:
                return
    
    
            try:
                items = ac['object']['items']
            except KeyError:
                logger.warning('No items in collection!')
    
            item_serializers = [
                serializers.AudioSerializer(
                    data=i, context={'library': remote_library})
                for i in items
            ]
    
            valid_serializers = []
            for s in item_serializers:
                if s.is_valid():
                    valid_serializers.append(s)
                else:
                    logger.debug(
                        'Skipping invalid item %s, %s', s.initial_data, s.errors)
    
            for s in valid_serializers:
                s.save()
    
    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
    
    
        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(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                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': [],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                '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):
    
            super().handle_follow(ac, sender)
            # also, we follow back
    
            test_actor = self.get_actor_instance()
            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)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        def handle_undo_follow(self, ac, sender):
            super().handle_undo_follow(ac, sender)
            actor = self.get_actor_instance()
    
            # we also unfollow the sender, if possible
            try:
                follow = models.Follow.objects.get(
                    target=sender,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    actor=actor,
    
                )
            except models.Follow.DoesNotExist:
                return
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            undo = activity.get_undo(
                id=follow.get_federation_url(),
                actor=actor,
                object=serializers.FollowSerializer(follow).data,
            )
    
            follow.delete()
            activity.deliver(
                undo,
                to=[sender.url],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                on_behalf_of=actor)
    
    
    SYSTEM_ACTORS = {
        'library': LibraryActor(),
        'test': TestActor(),
    }