From 2e77df6454762fe482f69ce574e7dd0cf0b5f629 Mon Sep 17 00:00:00 2001 From: Marnanel Thurman Date: Wed, 6 May 2020 20:03:11 +0100 Subject: [PATCH] Loads of rewriting of sombrero's "deliver" function so it works with the new model. It still needs more work, but this is an intermediate checkin. OutgoingActivity model added to sombrero. sombrero gains a "receivers" file so it can respond to follows etc. Fetch model deleted from bowler, though I think it'll need to reappear in sombrero soon. --- .../migrations/0002_delete_fetch.py | 16 +++ kepi/sombrero_sendpub/__init__.py | 1 + kepi/sombrero_sendpub/delivery.py | 128 ++++++++---------- .../migrations/0001_initial.py | 21 +++ kepi/sombrero_sendpub/models.py | 48 ++++++- kepi/sombrero_sendpub/receivers.py | 31 +++++ 6 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 kepi/bowler_pub/migrations/0002_delete_fetch.py create mode 100644 kepi/sombrero_sendpub/migrations/0001_initial.py create mode 100644 kepi/sombrero_sendpub/receivers.py diff --git a/kepi/bowler_pub/migrations/0002_delete_fetch.py b/kepi/bowler_pub/migrations/0002_delete_fetch.py new file mode 100644 index 0000000..a2719e2 --- /dev/null +++ b/kepi/bowler_pub/migrations/0002_delete_fetch.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.4 on 2020-05-06 18:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bowler_pub', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Fetch', + ), + ] diff --git a/kepi/sombrero_sendpub/__init__.py b/kepi/sombrero_sendpub/__init__.py index e69de29..271acd7 100644 --- a/kepi/sombrero_sendpub/__init__.py +++ b/kepi/sombrero_sendpub/__init__.py @@ -0,0 +1 @@ +import kepi.sombrero_sendpub.receivers diff --git a/kepi/sombrero_sendpub/delivery.py b/kepi/sombrero_sendpub/delivery.py index e36df96..c7f29d7 100644 --- a/kepi/sombrero_sendpub/delivery.py +++ b/kepi/sombrero_sendpub/delivery.py @@ -9,23 +9,28 @@ This module contains deliver(), which delivers objects to their audiences. """ -from __future__ import absolute_import, unicode_literals from celery import shared_task -from kepi.bowler_pub.utils import is_local -import kepi.bowler_pub.models -from httpsig.verify import HeaderVerifier -from urllib.parse import urlparse -from django.http.request import HttpRequest -from django.conf import settings -import django.urls -import django.utils.datastructures import logging import requests import json +import httpsig +import random +from django.http.request import HttpRequest +from django.conf import settings +from kepi.bowler_pub.utils import configured_url, as_json, is_local + +""" +from __future__ import absolute_import, unicode_literals +from kepi.bowler_pub.utils import is_local +import kepi.bowler_pub.models +from urllib.parse import urlparse +import django.urls +import django.utils.datastructures import datetime import pytz -import httpsig +from httpsig.verify import HeaderVerifier from collections.abc import Iterable +""" # FIXME logger = logging.getLogger(name='kepi') @@ -88,6 +93,8 @@ def _recipients_to_inboxes(recipients, there will be no duplicates. """ + from kepi.bowler_pub import PUBLIC_IDS + logger.info('Looking up inboxes for: %s', recipients) @@ -397,93 +404,69 @@ def _deliver_remote( @shared_task() def deliver( - activity_id, - incoming = False, + activity, ): """ Deliver an activity to an actor. Keyword arguments: - activity_id -- the "id" field of an Activity - incoming -- True if we just received this, False otherwise + activity -- a dict representing an ActivityPub activity. This function is a shared task; it will be run by Celery behind the scenes. """ - try: - activity = kepi.bowler_pub.models.AcActivity.objects.get(id=activity_id) - except kepi.bowler_pub.models.AcActivity.DoesNotExist: - logger.warn("Can't deliver activity %s because it doesn't exist", - activity_id) - return None + import kepi.sombrero_sendpub.models as sombrero_models + from kepi.bowler_pub import PUBLIC_IDS - logger.info('%s: begin delivery; incoming==%s', - activity, incoming) + message = sombrero_models.OutgoingActivity( + content=activity, + ) + message.save() - activity_form = activity.activity_form - logger.debug('%s: full form is %s', - activity, activity_form) - - local_actor = _find_local_actor(activity_form) - logger.debug('%s: local actor is %s', - activity, local_actor) + logger.info('activity %d: begin delivery: %s', + message.pk, message.content) recipients = set() for field in ['to', 'bto', 'cc', 'bcc', 'audience']: - if field in activity_form: - recipients.update(activity_form[field]) - - if local_actor is not None: - if incoming: - # Actors don't get told about their own (incoming) activities - if local_actor.url in recipients: - logger.info(' -- removing actor from recipients') - recipients.remove(local_actor.url) - else: - # but if it originated locally, the status should appear in the - # actor's own inbox too - if local_actor.url not in recipients: - logger.info(' -- adding actor to recipients') - recipients.add(local_actor.url) + if field in activity: + for recipient in activity[field]: + if not is_local(recipient): + recipients.update(activity[field]) if not recipients: - logger.debug('%s: there are no recipients; giving up', - activity) + logger.debug('activity %d: there are no recipients; giving up', + message.pk) return - logger.debug('%s: recipients are %s', + logger.debug('activity %d: recipients are %s', activity, recipients) - if incoming: + # Dereference collections. - inboxes = recipients - message = '' - signer = None + inboxes = _recipients_to_inboxes(recipients, + local_actor=activity['actor']) - else: + if not inboxes: + logger.debug('activity %s: there are no inboxes to send to; giving up', + message.pk) + return - # Dereference collections. + logger.debug('%s: inboxes are %s', + activity, inboxes) - inboxes = _recipients_to_inboxes(recipients, - local_actor=local_actor) + ############################ + raise ValueError("XXX TO HERE") # XXX TO HERE + ############################ - if not inboxes: - logger.debug('%s: there are no inboxes to send to; giving up', - activity) - return + message = _activity_form_to_outgoing_string( + activity_form = activity_form, + ) - logger.debug('%s: inboxes are %s', - activity, inboxes) - - message = _activity_form_to_outgoing_string( - activity_form = activity_form, - ) - - signer = _signer_for_local_actor( - local_actor = local_actor, - ) + signer = _signer_for_local_actor( + local_actor = local_actor, + ) for inbox in inboxes: logger.debug('%s: %s: begin delivery', @@ -497,12 +480,7 @@ def deliver( logger.debug(" -- mustn't deliver to Public") continue - parsed_target_url = urlparse(inbox, - allow_fragments = False, - ) - is_local = parsed_target_url.hostname in settings.ALLOWED_HOSTS - - if is_local: + if is_local(inbox): _deliver_local( activity, inbox, diff --git a/kepi/sombrero_sendpub/migrations/0001_initial.py b/kepi/sombrero_sendpub/migrations/0001_initial.py new file mode 100644 index 0000000..e8b1ec0 --- /dev/null +++ b/kepi/sombrero_sendpub/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.4 on 2020-05-06 18:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OutgoingActivity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ], + ), + ] diff --git a/kepi/sombrero_sendpub/models.py b/kepi/sombrero_sendpub/models.py index 71a8362..22f572f 100644 --- a/kepi/sombrero_sendpub/models.py +++ b/kepi/sombrero_sendpub/models.py @@ -1,3 +1,49 @@ from django.db import models +from kepi.bowler_pub.utils import configured_url, as_json +import json -# Create your models here. +class OutgoingActivity(models.Model): + + content = models.TextField() + + @property + def url(self): + return configured_url( + 'ACTIVITY_LINK', + serial = self.pk, + ) + + @property + def value(self): + result = json.loads(self.content) + + if not isinstance(result['to'], list): + result['to'] = [result['to']] + + if 'id' not in result: + result['id'] = self.url + + if '@context' not in result: + from kepi.bowler_pub import ATSIGN_CONTEXT + result['@context'] = ATSIGN_CONTEXT + + return result + + def __repr__(self): + return as_json(self.value) + + def __str__(self): + return self.__repr__() + + def save(self, *args, **kwargs): + + if not isinstance(self.content, str): + + for field in ['type', 'actor', 'to']: + if field not in self.content: + raise ValueError("activity is missing required fields: %s", + self.content) + + self.content = json.dumps(self.content) + + super().save(*args, **kwargs) diff --git a/kepi/sombrero_sendpub/receivers.py b/kepi/sombrero_sendpub/receivers.py new file mode 100644 index 0000000..1003895 --- /dev/null +++ b/kepi/sombrero_sendpub/receivers.py @@ -0,0 +1,31 @@ +import kepi.trilby_api.signals as kepi_signals +from django.dispatch import receiver +from kepi.sombrero_sendpub.delivery import deliver +import logging + +logger = logging.Logger("kepi") + +@receiver(kepi_signals.followed) +def on_follow(sender, **kwargs): + """ + If the Follow event describes a remote person being followed, + then send them an ActivityPub "Follow" activity message about it. + + The spec for "Follow" is here: + https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow + """ + if sender.following.is_local: + logger.debug("%s is local; not sending update", sender) + return + + print("Follow received:", sender) + logger.info("Follow received: %s", sender) + + deliver( + activity = { + 'type': 'Follow', + 'actor': sender.follower.url, + 'object': sender.following.url, + 'to': sender.following.url, + }, + )