Merge pull request #117 from cthulahoops/boosts

Boosts
This commit is contained in:
Mouse Reeve 2020-03-30 14:22:09 -07:00 committed by GitHub
commit eb04600dc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 3 deletions

View file

@ -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

View file

@ -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()

View file

@ -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':

View file

@ -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'),
),
]

View file

@ -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

View file

@ -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 '''

View file

@ -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 '''

View file

@ -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)

View file

@ -32,6 +32,9 @@
<div class="row shrink">
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
</div>
{% elif notification.notification_type == 'BOOST' %}
boosted your <a href="{{ notification.related_status.absolute_id}}">status</a>
{% endif %}
</div>
{% endfor %}

View file

@ -10,6 +10,8 @@
reviewed {{ status.book.title }}
{% elif status.status_type == 'Comment' %}
commented on {{ status.book.title }}
{% elif status.status_type == 'Boost' %}
boosted
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.absolute_id }}">{{ parent_status.status_type|lower }}</a>
@ -43,10 +45,15 @@
{% if status.status_type == 'Review' %}<h4>{{ status.name }}
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
</h4>{% endif %}
{% if status.status_type != 'Update' %}
{% if status.status_type != 'Update' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote>
{% 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 %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
</div>
{% if status.status_type != 'Boost' %}
{% include 'snippets/interaction.html' with activity=status %}
{% endif %}

View file

@ -85,6 +85,7 @@ urlpatterns = [
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
re_path(r'^shelve/?$', actions.shelve),

View file

@ -240,6 +240,12 @@ def unfavorite(request, status_id):
outgoing.handle_unfavorite(request.user, status)
return redirect(request.headers.get('Referer', '/'))
@login_required
def boost(request, status_id):
''' boost a status '''
status = models.Status.objects.get(id=status_id)
outgoing.handle_boost(request.user, status)
return redirect(request.headers.get('Referer', '/'))
@login_required
def follow(request):