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 import requests
from urllib.parse import urlparse from urllib.parse import urlparse
from fedireads import incoming
def get_recipients(user, post_privacy, direct_recipients=None): def get_recipients(user, post_privacy, direct_recipients=None):
''' deduplicated list of recipient inboxes ''' ''' deduplicated list of recipient inboxes '''
@ -40,7 +38,7 @@ def broadcast(sender, activity, recipients):
errors = [] errors = []
for recipient in recipients: for recipient in recipients:
try: try:
sign_and_send(sender, activity, recipient) response = sign_and_send(sender, activity, recipient)
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
# TODO: maybe keep track of users who cause errors # TODO: maybe keep track of users who cause errors
errors.append({ errors.append({
@ -85,5 +83,5 @@ def sign_and_send(sender, activity, destination):
) )
if not response.ok: if not response.ok:
response.raise_for_status() response.raise_for_status()
incoming.handle_response(response) return response

View file

@ -28,7 +28,7 @@ class RegisterForm(ModelForm):
class ReviewForm(ModelForm): class ReviewForm(ModelForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['name', 'review_content', 'rating'] fields = ['name', 'content', 'rating']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
review_content = IntegerField(validators=[ review_content = IntegerField(validators=[
MinValueValidator(0), MaxValueValidator(5) MinValueValidator(0), MaxValueValidator(5)

View file

@ -11,6 +11,7 @@ import requests
from fedireads import models from fedireads import models
from fedireads import outgoing from fedireads import outgoing
from fedireads.activity import create_review
from fedireads.openlibrary import get_or_create_book from fedireads.openlibrary import get_or_create_book
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
from fedireads.sanitize_html import InputHtmlParser from fedireads.sanitize_html import InputHtmlParser
@ -290,46 +291,34 @@ def handle_incoming_create(activity):
response = HttpResponse() response = HttpResponse()
# if it's an article and in reply to a book, we have a review # 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']: '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( else:
uuid=activity['id'], models.Activity(
user=user, uuid=activity['id'],
content=activity, user=user,
activity_type=activity['object']['type'] content=activity,
) activity_type=activity['object']['type']
).save()
return response 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): def handle_incoming_accept(activity):
''' someone is accepting a follow request ''' ''' someone is accepting a follow request '''
@ -350,14 +339,3 @@ def handle_incoming_accept(activity):
).save() ).save()
return HttpResponse() 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 from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
@ -118,11 +118,16 @@ class Migration(migrations.Migration):
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Note', name='Status',
fields=[ 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( migrations.CreateModel(
name='ShelfBook', name='ShelfBook',
@ -186,16 +191,23 @@ class Migration(migrations.Migration):
unique_together={('user', 'name')}, unique_together={('user', 'name')},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Review', name='ReviewActivity',
fields=[ 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')), ('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')), ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
], ],
bases=('fedireads.activity',), 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( migrations.CreateModel(
name='FollowActivity', name='FollowActivity',
fields=[ fields=[

View file

@ -1,5 +1,6 @@
''' bring all the models into the app namespace ''' ''' bring all the models into the app namespace '''
from .book import Shelf, ShelfBook, Book, Author from .book import Shelf, ShelfBook, Book, Author
from .user import User, FederatedServer 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) super().save(*args, **kwargs)
class Review(Activity): class ReviewActivity(Activity):
''' a book review '''
book = models.ForeignKey('Book', on_delete=models.PROTECT) 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): def save(self, *args, **kwargs):
self.activity_type = 'Article' self.activity_type = 'Article'
self.fedireads_type = 'Review'
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Note(Activity): class Status(models.Model):
''' reply to a review, etc ''' ''' 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): def save(self, *args, **kwargs):
self.activity_type = 'Note' self.status_type = 'Review'
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -7,6 +7,7 @@ from urllib.parse import urlencode
from uuid import uuid4 from uuid import uuid4
from fedireads import models from fedireads import models
from fedireads.activity import create_review
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
from fedireads.broadcast import get_recipients, broadcast from fedireads.broadcast import get_recipients, broadcast
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
@ -216,34 +217,57 @@ def handle_unshelve(user, book, shelf):
def handle_review(user, book, name, content, rating): def handle_review(user, book, name, content, rating):
''' post a review ''' ''' post a review '''
review_uuid = uuid4() # validated and saves the review in the database so it has an id
obj = { review = create_review(user, book, name, content, rating)
'@context': 'https://www.w3.org/ns/activitystreams',
'id': str(review_uuid), review_path = 'https://%s/user/%s/status/%d' % \
'type': 'Article', (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(), 'published': datetime.utcnow().isoformat(),
'attributedTo': user.actor, '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, 'content': content,
'inReplyTo': book.openlibrary_key, # TODO is this the right identifier? 'type': 'Note',
'fedireadsType': 'Review',
'name': name,
'rating': rating, # fedireads-only custom field '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 = { activity = {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'id': str(create_uuid), 'id': '%s/activity' % review_path,
'type': 'Create', 'type': 'Create',
'actor': user.actor, 'actor': user.actor,
'published': datetime.utcnow().isoformat(),
'to': ['%s/followers' % user.actor], 'to': ['%s/followers' % user.actor],
'cc': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'object': obj, 'object': review_activity,
# TODO: signature
} }
recipients = get_recipients(user, 'public')
broadcast(user, activity, recipients) broadcast(user, activity, recipients)

View file

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

View file

@ -233,14 +233,13 @@ def review(request):
if not form.is_valid(): if not form.is_valid():
return redirect('/') return redirect('/')
book_identifier = request.POST.get('book') book_identifier = request.POST.get('book')
book = openlibrary.get_or_create_book(book_identifier)
# TODO: validation, htmlification # TODO: validation, htmlification
name = form.data.get('name') name = form.data.get('name')
content = form.data.get('review_content') content = form.data.get('content')
rating = form.data.get('rating') 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) return redirect('/book/%s' % book_identifier)