diff --git a/fedireads/broadcast.py b/fedireads/broadcast.py index 6ffff60eb..afd73ab49 100644 --- a/fedireads/broadcast.py +++ b/fedireads/broadcast.py @@ -8,8 +8,6 @@ import json import requests from urllib.parse import urlparse -from fedireads import incoming - def get_recipients(user, post_privacy, direct_recipients=None): ''' deduplicated list of recipient inboxes ''' @@ -40,7 +38,7 @@ def broadcast(sender, activity, recipients): errors = [] for recipient in recipients: try: - sign_and_send(sender, activity, recipient) + response = sign_and_send(sender, activity, recipient) except requests.exceptions.HTTPError as e: # TODO: maybe keep track of users who cause errors errors.append({ @@ -85,5 +83,5 @@ def sign_and_send(sender, activity, destination): ) if not response.ok: response.raise_for_status() - incoming.handle_response(response) + return response diff --git a/fedireads/forms.py b/fedireads/forms.py index 37b4645f4..5a5948be8 100644 --- a/fedireads/forms.py +++ b/fedireads/forms.py @@ -28,7 +28,7 @@ class RegisterForm(ModelForm): class ReviewForm(ModelForm): class Meta: model = models.Review - fields = ['name', 'review_content', 'rating'] + fields = ['name', 'content', 'rating'] help_texts = {f: None for f in fields} review_content = IntegerField(validators=[ MinValueValidator(0), MaxValueValidator(5) diff --git a/fedireads/incoming.py b/fedireads/incoming.py index 1579f1c08..d51bb6c1e 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -11,6 +11,7 @@ import requests from fedireads import models from fedireads import outgoing +from fedireads.activity import create_review from fedireads.openlibrary import get_or_create_book from fedireads.remote_user import get_or_create_remote_user from fedireads.sanitize_html import InputHtmlParser @@ -290,46 +291,34 @@ def handle_incoming_create(activity): response = HttpResponse() # if it's an article and in reply to a book, we have a review - if activity['object']['type'] == 'Article' and \ + if activity['object']['fedireadsType'] == 'Review' and \ 'inReplyTo' in activity['object']: - response = create_review(user, activity) + book = activity['object']['inReplyTo'] + name = activity['object'].get('name') + content = activity['object'].get('content') + rating = activity['object'].get('rating') + try: + create_review(user, book, name, content, rating) + except ValueError: + return HttpResponseBadRequest() + models.ReviewActivity( + uuid=activity['id'], + user=user, + content=activity, + activity_type=activity['object']['type'], + book=book, + ).save() - models.Activity( - uuid=activity['id'], - user=user, - content=activity, - activity_type=activity['object']['type'] - ) + else: + models.Activity( + uuid=activity['id'], + user=user, + content=activity, + activity_type=activity['object']['type'] + ).save() return response -def create_review(user, activity): - ''' a book review has been added ''' - possible_book = activity['object']['inReplyTo'] - try: - book = get_or_create_book(possible_book) - except ValueError: - return HttpResponseNotFound('Book \'%s\' not found' % possible_book) - - content = activity['object'].get('content') - parser = InputHtmlParser() - parser.feed(content) - content = parser.get_output() - review_title = activity['object'].get('name', 'Untitled') - rating = activity['object'].get('rating', 0) - - models.Review( - uuid=activity.get('id'), - user=user, - content=activity, - activity_type='Article', - book=book, - name=review_title, - rating=rating, - review_content=content, - ).save() - return HttpResponse() - def handle_incoming_accept(activity): ''' someone is accepting a follow request ''' @@ -350,14 +339,3 @@ def handle_incoming_accept(activity): ).save() return HttpResponse() - -def handle_response(response): - ''' hopefully it's an accept from our follow request ''' - try: - activity = response.json() - except ValueError: - return - if activity['type'] == 'Accept': - handle_incoming_accept(activity) - - diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index 634808605..6e6c9ff47 100644 --- a/fedireads/migrations/0001_initial.py +++ b/fedireads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.2 on 2020-02-14 16:08 +# Generated by Django 3.0.2 on 2020-02-15 18:25 from django.conf import settings import django.contrib.auth.models @@ -118,11 +118,16 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Note', + name='Status', fields=[ - ('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status_type', models.CharField(default='Note', max_length=255)), + ('content', models.TextField(blank=True, null=True)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('reply_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Status')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ], - bases=('fedireads.activity',), ), migrations.CreateModel( name='ShelfBook', @@ -186,16 +191,23 @@ class Migration(migrations.Migration): unique_together={('user', 'name')}, ), migrations.CreateModel( - name='Review', + name='ReviewActivity', fields=[ ('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')), - ('name', models.CharField(max_length=255)), - ('rating', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), - ('review_content', models.TextField(blank=True, null=True)), ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')), ], bases=('fedireads.activity',), ), + migrations.CreateModel( + name='Review', + 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')), + ('name', models.CharField(max_length=255)), + ('rating', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')), + ], + bases=('fedireads.status',), + ), migrations.CreateModel( name='FollowActivity', fields=[ diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index 704cb2622..a3e286754 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -1,5 +1,6 @@ ''' bring all the models into the app namespace ''' from .book import Shelf, ShelfBook, Book, Author from .user import User, FederatedServer -from .activity import Activity, ShelveActivity, FollowActivity, Review, Note +from .activity import Activity, ShelveActivity, FollowActivity, \ + ReviewActivity, Status, Review diff --git a/fedireads/models/activity.py b/fedireads/models/activity.py index 7b1407f33..619ab1638 100644 --- a/fedireads/models/activity.py +++ b/fedireads/models/activity.py @@ -44,23 +44,40 @@ class FollowActivity(Activity): super().save(*args, **kwargs) -class Review(Activity): - ''' a book review ''' +class ReviewActivity(Activity): book = models.ForeignKey('Book', on_delete=models.PROTECT) - name = models.CharField(max_length=255) - rating = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(5)]) - review_content = models.TextField(blank=True, null=True) def save(self, *args, **kwargs): self.activity_type = 'Article' - self.fedireads_type = 'Review' super().save(*args, **kwargs) -class Note(Activity): +class Status(models.Model): ''' reply to a review, etc ''' + user = models.ForeignKey('User', on_delete=models.PROTECT) + status_type = models.CharField(max_length=255, default='Note') + reply_parent = models.ForeignKey( + 'self', + null=True, + on_delete=models.PROTECT + ) + content = models.TextField(blank=True, null=True) + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + objects = InheritanceManager() + + +class Review(Status): + ''' a book review ''' + book = models.ForeignKey('Book', on_delete=models.PROTECT) + name = models.CharField(max_length=255) + rating = models.IntegerField( + default=0, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + def save(self, *args, **kwargs): - self.activity_type = 'Note' + self.status_type = 'Review' super().save(*args, **kwargs) diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 3691d19a8..20e7b6259 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -7,6 +7,7 @@ from urllib.parse import urlencode from uuid import uuid4 from fedireads import models +from fedireads.activity import create_review from fedireads.remote_user import get_or_create_remote_user from fedireads.broadcast import get_recipients, broadcast from fedireads.settings import DOMAIN @@ -216,34 +217,57 @@ def handle_unshelve(user, book, shelf): def handle_review(user, book, name, content, rating): ''' post a review ''' - review_uuid = uuid4() - obj = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(review_uuid), - 'type': 'Article', + # validated and saves the review in the database so it has an id + review = create_review(user, book, name, content, rating) + + review_path = 'https://%s/user/%s/status/%d' % \ + (DOMAIN, user.localname, review.id) + book_path = 'https://%s/book/%s' % (DOMAIN, review.book.openlibrary_key) + + review_activity = { + 'id': review_path, + 'url': review_path, + 'inReplyTo': book_path, 'published': datetime.utcnow().isoformat(), 'attributedTo': user.actor, - 'name': name, + # TODO: again, assuming all posts are public + 'to': ['https://www.w3.org/ns/activitystreams#Public'], + 'cc': ['https://%s/user/%s/followers' % (DOMAIN, user.localname)], + 'sensitive': False, # TODO: allow content warning/sensitivity 'content': content, - 'inReplyTo': book.openlibrary_key, # TODO is this the right identifier? + 'type': 'Note', + 'fedireadsType': 'Review', + 'name': name, 'rating': rating, # fedireads-only custom field - 'to': 'https://www.w3.org/ns/activitystreams#Public' + 'attachment': [], # TODO: the book cover + 'replies': { + 'id': '%s/replies' % review_path, + 'type': 'Collection', + 'first': { + 'type': 'CollectionPage', + 'next': '%s/replies?only_other_accounts=true&page=true' % \ + review_path, + 'partOf': '%s/replies' % review_path, + 'items': [], # TODO: populate with replies + } + } } - # TODO: create alt version for mastodon - recipients = get_recipients(user, 'public') - create_uuid = uuid4() + activity = { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(create_uuid), + 'id': '%s/activity' % review_path, 'type': 'Create', 'actor': user.actor, + 'published': datetime.utcnow().isoformat(), 'to': ['%s/followers' % user.actor], 'cc': ['https://www.w3.org/ns/activitystreams#Public'], - 'object': obj, + 'object': review_activity, + # TODO: signature } + recipients = get_recipients(user, 'public') broadcast(user, activity, recipients) diff --git a/fedireads/templates/book.html b/fedireads/templates/book.html index 72ad5d83c..624a3bb58 100644 --- a/fedireads/templates/book.html +++ b/fedireads/templates/book.html @@ -23,7 +23,7 @@

{{ review.name }} {{ review.rating | stars }} stars, by {% include 'snippets/username.html' with user=review.user %}

-
{{ review.review_content }}
+
{{ review.content }}
{% endfor %} diff --git a/fedireads/views.py b/fedireads/views.py index ba0a4c6a1..e40c4dc52 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -233,14 +233,13 @@ def review(request): if not form.is_valid(): return redirect('/') book_identifier = request.POST.get('book') - book = openlibrary.get_or_create_book(book_identifier) # TODO: validation, htmlification name = form.data.get('name') - content = form.data.get('review_content') - rating = form.data.get('rating') + content = form.data.get('content') + rating = int(form.data.get('rating')) - outgoing.handle_review(request.user, book, name, content, rating) + outgoing.handle_review(request.user, book_identifier, name, content, rating) return redirect('/book/%s' % book_identifier)