diff --git a/fedireads/activitypub/__init__.py b/fedireads/activitypub/__init__.py index c14ff51b..d934ae89 100644 --- a/fedireads/activitypub/__init__.py +++ b/fedireads/activitypub/__init__.py @@ -10,4 +10,5 @@ from .status import get_review, get_review_article from .status import get_comment, get_comment_article from .status import get_status, get_replies, get_replies_page from .status import get_favorite, get_unfavorite +from .status import get_boost from .status import get_add_tag, get_remove_tag diff --git a/fedireads/activitypub/status.py b/fedireads/activitypub/status.py index f1ff05bf..779bb3af 100644 --- a/fedireads/activitypub/status.py +++ b/fedireads/activitypub/status.py @@ -158,6 +158,17 @@ def get_unfavorite(favorite): } +def get_boost(boost): + ''' boost/announce a post ''' + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': boost.absolute_id, + 'type': 'Announce', + 'actor': boost.user.actor, + 'object': boost.boosted_status.absolute_id, + } + + def get_add_tag(tag): ''' add activity for tagging a book ''' uuid = uuid4() diff --git a/fedireads/incoming.py b/fedireads/incoming.py index 12649f43..a766cb68 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -38,6 +38,7 @@ def shared_inbox(request): 'Reject': handle_follow_reject, 'Create': handle_create, 'Like': handle_favorite, + 'Announce': handle_boost, 'Add': { 'Tag': handle_add, }, @@ -286,6 +287,27 @@ def handle_unfavorite(activity): return HttpResponse() +def handle_boost(activity): + ''' someone gave us a boost! ''' + try: + status_id = activity['object'].split('/')[-1] + status = models.Status.objects.get(id=status_id) + booster = get_or_create_remote_user(activity['actor']) + except (models.Status.DoesNotExist, models.User.DoesNotExist): + return HttpResponseNotFound() + + if not booster.local: + status_builder.create_boost_from_activity(booster, activity) + + status_builder.create_notification( + status.user, + 'BOOST', + related_user=booster, + related_status=status, + ) + + return HttpResponse() + def handle_add(activity): ''' someone is tagging or shelving a book ''' if activity['object']['type'] == 'Tag': diff --git a/fedireads/migrations/0026_auto_20200330_1456.py b/fedireads/migrations/0026_auto_20200330_1456.py new file mode 100644 index 00000000..50e002a8 --- /dev/null +++ b/fedireads/migrations/0026_auto_20200330_1456.py @@ -0,0 +1,42 @@ +# Generated by Django 3.0.3 on 2020-03-30 14:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fedireads', '0025_auto_20200330_0037'), + ] + + operations = [ + migrations.CreateModel( + name='Boost', + fields=[ + ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Status')), + ], + options={ + 'abstract': False, + }, + bases=('fedireads.status',), + ), + migrations.RemoveConstraint( + model_name='notification', + name='notification_type_valid', + ), + migrations.AlterField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost')], max_length=255), + ), + migrations.AddConstraint( + model_name='notification', + constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST']), name='notification_type_valid'), + ), + migrations.AddField( + model_name='boost', + name='boosted_status', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='fedireads.Status'), + ), + ] diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index 4d26f032..e843b26d 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -1,6 +1,6 @@ ''' bring all the models into the app namespace ''' from .book import Connector, Book, Work, Edition, Author from .shelf import Shelf, ShelfBook -from .status import Status, Review, Comment, Favorite, Tag, Notification +from .status import Status, Review, Comment, Favorite, Boost, Tag, Notification from .user import User, UserFollows, UserFollowRequest, UserBlocks from .user import FederatedServer diff --git a/fedireads/models/status.py b/fedireads/models/status.py index b2d213ae..f8cd1921 100644 --- a/fedireads/models/status.py +++ b/fedireads/models/status.py @@ -89,6 +89,21 @@ class Favorite(FedireadsModel): unique_together = ('user', 'status') +class Boost(Status): + ''' boost'ing a post ''' + boosted_status = models.ForeignKey( + 'Status', + on_delete=models.PROTECT, + related_name="boosters") + + def save(self, *args, **kwargs): + self.status_type = 'Boost' + self.activity_type = 'Announce' + super().save(*args, **kwargs) + # This constraint can't work as it would cross tables. + # class Meta: + # unique_together = ('user', 'boosted_status') + class Tag(FedireadsModel): ''' freeform tags for books ''' user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -107,7 +122,7 @@ class Tag(FedireadsModel): NotificationType = models.TextChoices( - 'NotificationType', 'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST') + 'NotificationType', 'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST BOOST') class Notification(FedireadsModel): ''' you've been tagged, liked, followed, etc ''' diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 9a272538..dc60b2c0 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -291,6 +291,21 @@ def handle_unfavorite(user, status): recipients = get_recipients(user, 'direct', [status.user]) broadcast(user, fav_activity, recipients) +def handle_boost(user, status): + ''' a user wishes to boost a status ''' + if models.Boost.objects.filter( + boosted_status=status, user=user).exists(): + # you already boosted that. + return + boost = models.Boost.objects.create( + boosted_status=status, + user=user, + ) + boost.save() + + boost_activity = activitypub.get_boost(boost) + recipients = get_recipients(user, 'public') + broadcast(user, boost_activity, recipients) def handle_update_book(user, book): ''' broadcast the news about our book ''' diff --git a/fedireads/status.py b/fedireads/status.py index c46b1c88..bc79b99c 100644 --- a/fedireads/status.py +++ b/fedireads/status.py @@ -102,6 +102,20 @@ def create_favorite_from_activity(user, activity): return models.Favorite.objects.get(status=status, user=user) +def create_boost_from_activity(user, activity): + ''' create a new boost activity ''' + status = get_status(activity['object']) + remote_id = activity['id'] + try: + return models.Boost.objects.create( + status=status, + user=user, + remote_id=remote_id, + ) + except IntegrityError: + return models.Boost.objects.get(status=status, user=user) + + def get_status(absolute_id): ''' find a status in the database ''' return get_by_absolute_id(absolute_id, models.Status) diff --git a/fedireads/templates/notifications.html b/fedireads/templates/notifications.html index a6d215dc..dd5f135c 100644 --- a/fedireads/templates/notifications.html +++ b/fedireads/templates/notifications.html @@ -32,6 +32,9 @@
{{ status.content | safe }}{% endif %} + {% if status.status_type == 'Boost' %} + {% include 'snippets/status.html' with status=status.boosted_status depth=depth|add:1 %} + {% endif %} {% if not max_depth and status.reply_parent or status|replies %}
Thread{% endif %}
+{% if status.status_type != 'Boost' %}
{% include 'snippets/interaction.html' with activity=status %}
+{% endif %}
diff --git a/fedireads/urls.py b/fedireads/urls.py
index a22ba7dd..a839220b 100644
--- a/fedireads/urls.py
+++ b/fedireads/urls.py
@@ -86,6 +86,7 @@ urlpatterns = [
re_path(r'^favorite/(?P