Skip to content
Snippets Groups Projects
actors.py 6.51 KiB
Newer Older
import requests

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
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):
    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 = {}

    def get_actor_instance(self):
        a = models.Actor(
            **self.get_instance_argument(
                self.id,
                name=self.name,
                summary=self.summary,
                **self.additional_attributes
            )
        )
        a.pk = self.id
        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):
        raise NotImplementedError

    def get_outbox(self, data, actor=None):
        raise NotImplementedError

    def post_outbox(self, data, actor=None):
        raise NotImplementedError


class LibraryActor(SystemActor):
    id = 'library'
    name = '{host}\'s library'
    summary = 'Bot account to federate with {host}\'s library'
    additional_attributes = {
        'manually_approves_followers': True
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 post_inbox(self, data, actor=None):
        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.validated_data
        logger.info('Received activity on %s inbox', self.id)
        if ac['type'] == 'Create' and ac['object']['type'] == 'Note':
            # we received a toot \o/
            command = self.parse_command(ac['object']['content'])
            logger.debug('Parsed command: %s', command)
            if command == 'ping':
                self.handle_ping(ac, actor)

    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_ping(self, ac, sender):
        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",
Eliot Berriot's avatar
Eliot Berriot committed
        		{}
        	],
            'type': 'Create',
            'actor': test_actor.url,
            'id': '{}/activity'.format(reply_url),
            'published': now.isoformat(),
            'to': ac['actor'],
            'cc': [],
            'object': factories.NoteFactory(
                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'],
Eliot Berriot's avatar
Eliot Berriot committed
                        "name": sender.mention_username
                    }
                ]
            )
        }
        activity.deliver(
            reply_activity,
            to=[ac['actor']],
            on_behalf_of=test_actor)

SYSTEM_ACTORS = {
    'library': LibraryActor(),
    'test': TestActor(),
}