Trying to get federated posting to work

This commit is contained in:
Mouse Reeve 2020-02-15 11:13:49 -08:00
parent e0e419a757
commit 818e5bd0fa
9 changed files with 115 additions and 86 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@
<h4>{{ review.name }}
<small>{{ review.rating | stars }} stars, by {% include 'snippets/username.html' with user=review.user %}</small>
</h4>
<blockquote>{{ review.review_content }}</blockquote>
<blockquote>{{ review.content }}</blockquote>
</div>
{% endfor %}
</div>

View file

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