forked from mirrors/bookwyrm
Merge pull request #11 from mouse-reeve/stop-storing-ap-json
Stop storing ap json
This commit is contained in:
commit
978545717e
23 changed files with 630 additions and 476 deletions
7
fedireads/activitypub/__init__.py
Normal file
7
fedireads/activitypub/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
''' bring activitypub functions into the namespace '''
|
||||||
|
from .actor import get_actor
|
||||||
|
from .collection import get_outbox, get_outbox_page, get_add, get_remove, \
|
||||||
|
get_following, get_followers
|
||||||
|
from .create import get_create
|
||||||
|
from .follow import get_follow_request, get_accept
|
||||||
|
from .status import get_review, get_review_article, get_status, get_replies
|
28
fedireads/activitypub/actor.py
Normal file
28
fedireads/activitypub/actor.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
''' actor serializer '''
|
||||||
|
|
||||||
|
def get_actor(user):
|
||||||
|
''' activitypub actor from db User '''
|
||||||
|
return {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1'
|
||||||
|
],
|
||||||
|
|
||||||
|
'id': user.actor,
|
||||||
|
'type': 'Person',
|
||||||
|
'preferredUsername': user.localname,
|
||||||
|
'name': user.name,
|
||||||
|
'inbox': user.inbox,
|
||||||
|
'followers': '%s/followers' % user.actor,
|
||||||
|
'following': '%s/following' % user.actor,
|
||||||
|
'summary': user.summary,
|
||||||
|
'publicKey': {
|
||||||
|
'id': '%s/#main-key' % user.actor,
|
||||||
|
'owner': user.actor,
|
||||||
|
'publicKeyPem': user.public_key,
|
||||||
|
},
|
||||||
|
'endpoints': {
|
||||||
|
'sharedInbox': user.shared_inbox,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
128
fedireads/activitypub/collection.py
Normal file
128
fedireads/activitypub/collection.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
''' activitypub json for collections '''
|
||||||
|
from uuid import uuid4
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from .status import get_status
|
||||||
|
|
||||||
|
def get_outbox(user, size):
|
||||||
|
''' helper function for creating an outbox '''
|
||||||
|
return get_ordered_collection(user.outbox, size)
|
||||||
|
|
||||||
|
|
||||||
|
def get_outbox_page(user, page_id, statuses, max_id, min_id):
|
||||||
|
''' helper for formatting outbox pages '''
|
||||||
|
# not generalizing this more because the format varies for some reason
|
||||||
|
page = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': page_id,
|
||||||
|
'type': 'OrderedCollectionPage',
|
||||||
|
'partOf': user.outbox,
|
||||||
|
'orderedItems': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for status in statuses:
|
||||||
|
page['orderedItems'].append(get_status(status))
|
||||||
|
|
||||||
|
if max_id:
|
||||||
|
page['next'] = user.outbox + '?' + \
|
||||||
|
urlencode({'min_id': max_id, 'page': 'true'})
|
||||||
|
if min_id:
|
||||||
|
page['prev'] = user.outbox + '?' + \
|
||||||
|
urlencode({'max_id': min_id, 'page': 'true'})
|
||||||
|
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
def get_followers(user, page, follow_queryset):
|
||||||
|
''' list of people who follow a user '''
|
||||||
|
id_slug = '%s/followers' % user.actor
|
||||||
|
return get_follow_info(id_slug, page, follow_queryset)
|
||||||
|
|
||||||
|
|
||||||
|
def get_following(user, page, follow_queryset):
|
||||||
|
''' list of people who follow a user '''
|
||||||
|
id_slug = '%s/following' % user.actor
|
||||||
|
return get_follow_info(id_slug, page, follow_queryset)
|
||||||
|
|
||||||
|
|
||||||
|
def get_follow_info(id_slug, page, follow_queryset):
|
||||||
|
''' a list of followers or following '''
|
||||||
|
if page:
|
||||||
|
return get_follow_page(follow_queryset, id_slug, page)
|
||||||
|
count = follow_queryset.count()
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': id_slug,
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'totalItems': count,
|
||||||
|
'first': '%s?page=1' % id_slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: generalize these pagination functions
|
||||||
|
def get_follow_page(user_list, id_slug, page):
|
||||||
|
''' format a list of followers/following '''
|
||||||
|
page = int(page)
|
||||||
|
page_length = 10
|
||||||
|
start = (page - 1) * page_length
|
||||||
|
end = start + page_length
|
||||||
|
follower_page = user_list.all()[start:end]
|
||||||
|
data = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': '%s?page=%d' % (id_slug, page),
|
||||||
|
'type': 'OrderedCollectionPage',
|
||||||
|
'totalItems': user_list.count(),
|
||||||
|
'partOf': id_slug,
|
||||||
|
'orderedItems': [u.actor for u in follower_page],
|
||||||
|
}
|
||||||
|
if end <= user_list.count():
|
||||||
|
# there are still more pages
|
||||||
|
data['next'] = '%s?page=%d' % (id_slug, page + 1)
|
||||||
|
if start > 0:
|
||||||
|
data['prev'] = '%s?page=%d' % (id_slug, page - 1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_ordered_collection(id_slug, size):
|
||||||
|
''' create an ordered collection '''
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': id_slug,
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'totalItems': size,
|
||||||
|
'first': '%s?page=true' % id_slug,
|
||||||
|
'last': '%s?min_id=0&page=true' % id_slug
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_add(*args):
|
||||||
|
''' activitypub Add activity '''
|
||||||
|
return get_add_remove(*args, action='Add')
|
||||||
|
|
||||||
|
|
||||||
|
def get_remove(*args):
|
||||||
|
''' activitypub Add activity '''
|
||||||
|
return get_add_remove(*args, action='Remove')
|
||||||
|
|
||||||
|
|
||||||
|
def get_add_remove(user, book, shelf, action='Add'):
|
||||||
|
''' format an Add or Remove json blob '''
|
||||||
|
uuid = uuid4()
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': str(uuid),
|
||||||
|
'type': action,
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': {
|
||||||
|
'type': 'Document',
|
||||||
|
'name': book.data['title'],
|
||||||
|
'url': book.openlibrary_key
|
||||||
|
},
|
||||||
|
'target': {
|
||||||
|
'type': 'Collection',
|
||||||
|
'name': shelf.name,
|
||||||
|
'id': shelf.absolute_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
33
fedireads/activitypub/create.py
Normal file
33
fedireads/activitypub/create.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
''' format Create activities and sign them '''
|
||||||
|
from base64 import b64encode
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Signature import pkcs1_15
|
||||||
|
from Crypto.Hash import SHA256
|
||||||
|
|
||||||
|
def get_create(user, status_json):
|
||||||
|
''' create activitypub json for a Create activity '''
|
||||||
|
signer = pkcs1_15.new(RSA.import_key(user.private_key))
|
||||||
|
content = status_json['content']
|
||||||
|
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
|
||||||
|
'id': '%s/activity' % status_json['id'],
|
||||||
|
'type': 'Create',
|
||||||
|
'actor': user.actor,
|
||||||
|
'published': status_json['published'],
|
||||||
|
|
||||||
|
'to': ['%s/followers' % user.actor],
|
||||||
|
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
|
||||||
|
'object': status_json,
|
||||||
|
'signature': {
|
||||||
|
'type': 'RsaSignature2017',
|
||||||
|
'creator': '%s#main-key' % user.absolute_id,
|
||||||
|
'created': status_json['published'],
|
||||||
|
'signatureValue': b64encode(signed_message).decode('utf8'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
29
fedireads/activitypub/follow.py
Normal file
29
fedireads/activitypub/follow.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
''' makin' freinds inthe ap json format '''
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fedireads.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
def get_follow_request(user, to_follow):
|
||||||
|
''' a local user wants to follow someone '''
|
||||||
|
uuid = uuid4()
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': 'https://%s/%s' % (DOMAIN, str(uuid)),
|
||||||
|
'summary': '',
|
||||||
|
'type': 'Follow',
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': to_follow.actor,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_accept(user, request_activity):
|
||||||
|
''' accept a follow request '''
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': '%s#accepts/follows/' % user.absolute_id,
|
||||||
|
'type': 'Accept',
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': request_activity,
|
||||||
|
}
|
||||||
|
|
73
fedireads/activitypub/status.py
Normal file
73
fedireads/activitypub/status.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
''' status serializers '''
|
||||||
|
def get_review(review):
|
||||||
|
''' fedireads json for book reviews '''
|
||||||
|
status = get_status(review)
|
||||||
|
status['inReplyToBook'] = review.book.absolute_id
|
||||||
|
status['fedireadsType'] = review.status_type,
|
||||||
|
status['name'] = review.name
|
||||||
|
status['rating'] = review.rating
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def get_review_article(review):
|
||||||
|
''' a book review formatted for a non-fedireads isntance (mastodon) '''
|
||||||
|
status = get_status(review)
|
||||||
|
name = 'Review of "%s" (%d stars): %s' % (
|
||||||
|
review.book.data['title'],
|
||||||
|
review.rating,
|
||||||
|
review.name
|
||||||
|
)
|
||||||
|
status['name'] = name
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def get_status(status):
|
||||||
|
''' create activitypub json for a status '''
|
||||||
|
user = status.user
|
||||||
|
uri = status.absolute_id
|
||||||
|
reply_parent_id = status.reply_parent.absolute_id \
|
||||||
|
if status.reply_parent else None
|
||||||
|
status_json = {
|
||||||
|
'id': uri,
|
||||||
|
'url': uri,
|
||||||
|
'inReplyTo': reply_parent_id,
|
||||||
|
'published': status.created_date.isoformat(),
|
||||||
|
'attributedTo': user.actor,
|
||||||
|
# TODO: assuming all posts are public -- should check privacy db field
|
||||||
|
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
'cc': ['%s/followers' % user.absolute_id],
|
||||||
|
'sensitive': status.sensitive,
|
||||||
|
'content': status.content,
|
||||||
|
'type': status.activity_type,
|
||||||
|
'attachment': [], # TODO: the book cover
|
||||||
|
'replies': {
|
||||||
|
'id': '%s/replies' % uri,
|
||||||
|
'type': 'Collection',
|
||||||
|
'first': {
|
||||||
|
'type': 'CollectionPage',
|
||||||
|
'next': '%s/replies?only_other_accounts=true&page=true' % uri,
|
||||||
|
'partOf': '%s/replies' % uri,
|
||||||
|
'items': [], # TODO: populate with replies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status_json
|
||||||
|
|
||||||
|
|
||||||
|
def get_replies(status, replies):
|
||||||
|
''' collection of replies '''
|
||||||
|
id_slug = status.absolute_id + '/replies'
|
||||||
|
# TODO only partially implemented
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': id_slug,
|
||||||
|
'type': 'Collection',
|
||||||
|
'first': {
|
||||||
|
'id': '%s?page=true' % id_slug,
|
||||||
|
'type': 'CollectionPage',
|
||||||
|
'next': '%s?only_other_accounts=true&page=true' % id_slug,
|
||||||
|
'partOf': id_slug,
|
||||||
|
'items': [get_status(r) for r in replies]
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import requests
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
def get_recipients(user, post_privacy, direct_recipients=None):
|
def get_recipients(user, post_privacy, direct_recipients=None, limit=False):
|
||||||
''' deduplicated list of recipient inboxes '''
|
''' deduplicated list of recipient inboxes '''
|
||||||
recipients = direct_recipients or []
|
recipients = direct_recipients or []
|
||||||
if post_privacy == 'direct':
|
if post_privacy == 'direct':
|
||||||
|
@ -17,7 +17,12 @@ def get_recipients(user, post_privacy, direct_recipients=None):
|
||||||
return [u.inbox for u in recipients]
|
return [u.inbox for u in recipients]
|
||||||
|
|
||||||
# load all the followers of the user who is sending the message
|
# load all the followers of the user who is sending the message
|
||||||
|
if not limit:
|
||||||
followers = user.followers.all()
|
followers = user.followers.all()
|
||||||
|
else:
|
||||||
|
fedireads_user = limit == 'fedireads'
|
||||||
|
followers = user.followers.filter(fedireads_user=fedireads_user).all()
|
||||||
|
|
||||||
if post_privacy == 'public':
|
if post_privacy == 'public':
|
||||||
# post to public shared inboxes
|
# post to public shared inboxes
|
||||||
shared_inboxes = set(
|
shared_inboxes = set(
|
||||||
|
|
|
@ -40,8 +40,17 @@ class ReviewForm(ModelForm):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Status
|
||||||
|
fields = ['content']
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
labels = {'content': 'Comment'}
|
||||||
|
|
||||||
|
|
||||||
class EditUserForm(ModelForm):
|
class EditUserForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = ['avatar', 'name', 'summary']
|
fields = ['avatar', 'name', 'summary']
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,10 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads import outgoing
|
from fedireads import outgoing
|
||||||
from fedireads.activity import create_review
|
from fedireads.status import create_review, create_status
|
||||||
from fedireads.remote_user import get_or_create_remote_user
|
from fedireads.remote_user import get_or_create_remote_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,10 +34,7 @@ def shared_inbox(request):
|
||||||
return HttpResponse(status=401)
|
return HttpResponse(status=401)
|
||||||
|
|
||||||
response = HttpResponseNotFound()
|
response = HttpResponseNotFound()
|
||||||
if activity['type'] == 'Add':
|
if activity['type'] == 'Follow':
|
||||||
response = handle_incoming_shelve(activity)
|
|
||||||
|
|
||||||
elif activity['type'] == 'Follow':
|
|
||||||
response = handle_incoming_follow(activity)
|
response = handle_incoming_follow(activity)
|
||||||
|
|
||||||
elif activity['type'] == 'Create':
|
elif activity['type'] == 'Create':
|
||||||
|
@ -45,7 +43,7 @@ def shared_inbox(request):
|
||||||
elif activity['type'] == 'Accept':
|
elif activity['type'] == 'Accept':
|
||||||
response = handle_incoming_follow_accept(activity)
|
response = handle_incoming_follow_accept(activity)
|
||||||
|
|
||||||
# TODO: Undo, Remove, etc
|
# TODO: Add, Undo, Remove, etc
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -114,29 +112,7 @@ def get_actor(request, username):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user = models.User.objects.get(localname=username)
|
user = models.User.objects.get(localname=username)
|
||||||
return JsonResponse({
|
return JsonResponse(activitypub.get_actor(user))
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1'
|
|
||||||
],
|
|
||||||
|
|
||||||
'id': user.actor,
|
|
||||||
'type': 'Person',
|
|
||||||
'preferredUsername': user.localname,
|
|
||||||
'name': user.name,
|
|
||||||
'inbox': user.inbox,
|
|
||||||
'followers': '%s/followers' % user.actor,
|
|
||||||
'following': '%s/following' % user.actor,
|
|
||||||
'summary': user.summary,
|
|
||||||
'publicKey': {
|
|
||||||
'id': '%s/#main-key' % user.actor,
|
|
||||||
'owner': user.actor,
|
|
||||||
'publicKeyPem': user.public_key,
|
|
||||||
},
|
|
||||||
'endpoints': {
|
|
||||||
'sharedInbox': user.shared_inbox,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -154,7 +130,26 @@ def get_status(request, username, status_id):
|
||||||
if user != status.user:
|
if user != status.user:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
return JsonResponse(status.activity)
|
return JsonResponse(activitypub.get_status(status))
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def get_replies(request, username, status_id):
|
||||||
|
''' ordered collection of replies to a status '''
|
||||||
|
# TODO: this isn't a full implmentation
|
||||||
|
if request.method != 'GET':
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
if status.user.localname != username:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
replies = models.Status.objects.filter(
|
||||||
|
reply_parent=status
|
||||||
|
).first()
|
||||||
|
|
||||||
|
replies_activity = activitypub.get_replies(status, [replies])
|
||||||
|
return JsonResponse(replies_activity)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -165,7 +160,8 @@ def get_followers(request, username):
|
||||||
|
|
||||||
user = models.User.objects.get(localname=username)
|
user = models.User.objects.get(localname=username)
|
||||||
followers = user.followers
|
followers = user.followers
|
||||||
return format_follow_info(user, request.GET.get('page'), followers)
|
page = request.GET.get('page')
|
||||||
|
return JsonResponse(activitypub.get_followers(user, page, followers))
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -176,70 +172,8 @@ def get_following(request, username):
|
||||||
|
|
||||||
user = models.User.objects.get(localname=username)
|
user = models.User.objects.get(localname=username)
|
||||||
following = models.User.objects.filter(followers=user)
|
following = models.User.objects.filter(followers=user)
|
||||||
return format_follow_info(user, request.GET.get('page'), following)
|
page = request.GET.get('page')
|
||||||
|
return JsonResponse(activitypub.get_following(user, page, following))
|
||||||
|
|
||||||
def format_follow_info(user, page, follow_queryset):
|
|
||||||
''' create the activitypub json for followers/following '''
|
|
||||||
id_slug = '%s/following' % user.actor
|
|
||||||
if page:
|
|
||||||
return JsonResponse(get_follow_page(follow_queryset, id_slug, page))
|
|
||||||
count = follow_queryset.count()
|
|
||||||
return JsonResponse({
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': id_slug,
|
|
||||||
'type': 'OrderedCollection',
|
|
||||||
'totalItems': count,
|
|
||||||
'first': '%s?page=1' % id_slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def get_follow_page(user_list, id_slug, page):
|
|
||||||
''' format a list of followers/following '''
|
|
||||||
page = int(page)
|
|
||||||
page_length = 10
|
|
||||||
start = (page - 1) * page_length
|
|
||||||
end = start + page_length
|
|
||||||
follower_page = user_list.all()[start:end]
|
|
||||||
data = {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': '%s?page=%d' % (id_slug, page),
|
|
||||||
'type': 'OrderedCollectionPage',
|
|
||||||
'totalItems': user_list.count(),
|
|
||||||
'partOf': id_slug,
|
|
||||||
'orderedItems': [u.actor for u in follower_page],
|
|
||||||
}
|
|
||||||
if end <= user_list.count():
|
|
||||||
# there are still more pages
|
|
||||||
data['next'] = '%s?page=%d' % (id_slug, page + 1)
|
|
||||||
if start > 0:
|
|
||||||
data['prev'] = '%s?page=%d' % (id_slug, page - 1)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def handle_incoming_shelve(activity):
|
|
||||||
''' receiving an Add activity (to shelve a book) '''
|
|
||||||
# TODO what happens here? If it's a remote over, then I think
|
|
||||||
# I should save both the activity and the ShelfBook entry. But
|
|
||||||
# I'll do that later.
|
|
||||||
uuid = activity['id']
|
|
||||||
models.ShelveActivity.objects.get(uuid=uuid)
|
|
||||||
'''
|
|
||||||
book_id = activity['object']['url']
|
|
||||||
book = openlibrary.get_or_create_book(book_id)
|
|
||||||
user_ap_id = activity['actor'].replace('https//:', '')
|
|
||||||
user = models.User.objects.get(actor=user_ap_id)
|
|
||||||
if not user or not user.local:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
|
|
||||||
models.ShelfBook(
|
|
||||||
shelf=shelf,
|
|
||||||
book=book,
|
|
||||||
added_by=user,
|
|
||||||
).save()
|
|
||||||
'''
|
|
||||||
return HttpResponse()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_incoming_follow(activity):
|
def handle_incoming_follow(activity):
|
||||||
|
@ -248,12 +182,6 @@ def handle_incoming_follow(activity):
|
||||||
to_follow = models.User.objects.get(actor=activity['object'])
|
to_follow = models.User.objects.get(actor=activity['object'])
|
||||||
# figure out who they are
|
# figure out who they are
|
||||||
user = get_or_create_remote_user(activity['actor'])
|
user = get_or_create_remote_user(activity['actor'])
|
||||||
models.FollowActivity(
|
|
||||||
uuid=activity['id'],
|
|
||||||
user=user,
|
|
||||||
followed=to_follow,
|
|
||||||
content=activity,
|
|
||||||
)
|
|
||||||
# TODO: allow users to manually approve requests
|
# TODO: allow users to manually approve requests
|
||||||
outgoing.handle_outgoing_accept(user, to_follow, activity)
|
outgoing.handle_outgoing_accept(user, to_follow, activity)
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
@ -277,37 +205,30 @@ def handle_incoming_create(activity):
|
||||||
if not 'object' in activity:
|
if not 'object' in activity:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
# TODO: should only create notes if they are relevent to a book,
|
||||||
|
# so, not every single thing someone posts on mastodon
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
|
content = activity['object'].get('content')
|
||||||
if activity['object'].get('fedireadsType') == 'Review' and \
|
if activity['object'].get('fedireadsType') == 'Review' and \
|
||||||
'inReplyTo' in activity['object']:
|
'inReplyToBook' in activity['object']:
|
||||||
book = activity['object']['inReplyTo']
|
book = activity['object']['inReplyToBook']
|
||||||
book = book.split('/')[-1]
|
book = book.split('/')[-1]
|
||||||
name = activity['object'].get('name')
|
name = activity['object'].get('name')
|
||||||
content = activity['object'].get('content')
|
|
||||||
rating = activity['object'].get('rating')
|
rating = activity['object'].get('rating')
|
||||||
if user.local:
|
if user.local:
|
||||||
review_id = activity['object']['id'].split('/')[-1]
|
review_id = activity['object']['id'].split('/')[-1]
|
||||||
review = models.Review.objects.get(id=review_id)
|
models.Review.objects.get(id=review_id)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
review = create_review(user, book, name, content, rating)
|
create_review(user, book, name, content, rating)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
elif not user.local:
|
||||||
|
try:
|
||||||
|
create_status(user, content)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
models.ReviewActivity.objects.create(
|
|
||||||
uuid=activity['id'],
|
|
||||||
user=user,
|
|
||||||
content=activity['object'],
|
|
||||||
activity_type=activity['object']['type'],
|
|
||||||
book=review.book,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
models.Activity.objects.create(
|
|
||||||
uuid=activity['id'],
|
|
||||||
user=user,
|
|
||||||
content=activity,
|
|
||||||
activity_type=activity['object']['type']
|
|
||||||
)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,12 +242,5 @@ def handle_incoming_accept(activity):
|
||||||
# save this relationship in the db
|
# save this relationship in the db
|
||||||
followed.followers.add(user)
|
followed.followers.add(user)
|
||||||
|
|
||||||
# save the activity record
|
|
||||||
models.FollowActivity(
|
|
||||||
uuid=activity['id'],
|
|
||||||
user=user,
|
|
||||||
followed=followed,
|
|
||||||
content=activity,
|
|
||||||
).save()
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.3 on 2020-02-15 22:50
|
# Generated by Django 3.0.3 on 2020-02-17 02:39
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
@ -56,20 +56,6 @@ class Migration(migrations.Migration):
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='Activity',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('uuid', models.CharField(max_length=255, unique=True)),
|
|
||||||
('content', fedireads.utils.fields.JSONField(max_length=5000)),
|
|
||||||
('activity_type', models.CharField(max_length=255)),
|
|
||||||
('fedireads_type', models.CharField(blank=True, max_length=255, null=True)),
|
|
||||||
('local', models.BooleanField(default=True)),
|
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Author',
|
name='Author',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -118,11 +104,14 @@ class Migration(migrations.Migration):
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('status_type', models.CharField(default='Note', max_length=255)),
|
('status_type', models.CharField(default='Note', max_length=255)),
|
||||||
('activity', fedireads.utils.fields.JSONField(max_length=5000, null=True)),
|
('activity_type', models.CharField(default='Note', max_length=255)),
|
||||||
('local', models.BooleanField(default=True)),
|
('local', models.BooleanField(default=True)),
|
||||||
|
('privacy', models.CharField(default='public', max_length=255)),
|
||||||
|
('sensitive', models.BooleanField(default=False)),
|
||||||
('content', models.TextField(blank=True, null=True)),
|
('content', models.TextField(blank=True, null=True)),
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('mention_books', models.ManyToManyField(related_name='mention_book', to='fedireads.Book')),
|
||||||
|
('mention_users', models.ManyToManyField(related_name='mention_user', to=settings.AUTH_USER_MODEL)),
|
||||||
('reply_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Status')),
|
('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)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
|
@ -175,27 +164,10 @@ class Migration(migrations.Migration):
|
||||||
name='user_permissions',
|
name='user_permissions',
|
||||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='ShelveActivity',
|
|
||||||
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')),
|
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
|
||||||
('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Shelf')),
|
|
||||||
],
|
|
||||||
bases=('fedireads.activity',),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='shelf',
|
name='shelf',
|
||||||
unique_together={('user', 'identifier')},
|
unique_together={('user', 'identifier')},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
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')),
|
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
|
||||||
],
|
|
||||||
bases=('fedireads.activity',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Review',
|
name='Review',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -206,12 +178,4 @@ class Migration(migrations.Migration):
|
||||||
],
|
],
|
||||||
bases=('fedireads.status',),
|
bases=('fedireads.status',),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='FollowActivity',
|
|
||||||
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')),
|
|
||||||
('followed', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='followed', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
bases=('fedireads.activity',),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
80
fedireads/migrations/0002_auto_20200219_0118.py
Normal file
80
fedireads/migrations/0002_auto_20200219_0118.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-19 01:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fedireads', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='author',
|
||||||
|
old_name='added_date',
|
||||||
|
new_name='created_date',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='book',
|
||||||
|
old_name='added_date',
|
||||||
|
new_name='created_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='author',
|
||||||
|
name='updated_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='book',
|
||||||
|
name='updated_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shelf',
|
||||||
|
name='updated_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='created_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='updated_date',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='author',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='federatedserver',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='federatedserver',
|
||||||
|
name='created_date',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shelf',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shelfbook',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='fedireads_user',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,5 @@
|
||||||
''' 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, \
|
from .activity import Status, Review
|
||||||
ReviewActivity, Status, Review
|
|
||||||
|
|
||||||
|
|
|
@ -3,81 +3,31 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
from fedireads.utils.fields import JSONField
|
from fedireads.utils.models import FedireadsModel
|
||||||
|
|
||||||
# TODO: I don't know that these Activity models should exist, at least in this way
|
|
||||||
# but I'm not sure what the right approach is for now.
|
|
||||||
|
|
||||||
class Activity(models.Model):
|
|
||||||
''' basic fields for storing activities '''
|
|
||||||
uuid = models.CharField(max_length=255, unique=True)
|
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
|
||||||
content = JSONField(max_length=5000)
|
|
||||||
# the activitypub activity type (Create, Add, Follow, ...)
|
|
||||||
activity_type = models.CharField(max_length=255)
|
|
||||||
# custom types internal to fedireads (Review, Shelve, ...)
|
|
||||||
fedireads_type = models.CharField(max_length=255, blank=True, null=True)
|
|
||||||
local = models.BooleanField(default=True)
|
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
|
|
||||||
class ShelveActivity(Activity):
|
class Status(FedireadsModel):
|
||||||
''' someone put a book on a shelf '''
|
''' any post, like a reply to a review, etc '''
|
||||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
|
||||||
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if not self.activity_type:
|
|
||||||
self.activity_type = 'Add'
|
|
||||||
self.fedireads_type = 'Shelve'
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FollowActivity(Activity):
|
|
||||||
''' record follow requests sent out '''
|
|
||||||
followed = models.ForeignKey(
|
|
||||||
'User',
|
|
||||||
related_name='followed',
|
|
||||||
on_delete=models.PROTECT
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.activity_type = 'Follow'
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewActivity(Activity):
|
|
||||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.activity_type = 'Note'
|
|
||||||
self.fedireads_type = 'Review'
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Status(models.Model):
|
|
||||||
''' reply to a review, etc '''
|
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
status_type = models.CharField(max_length=255, default='Note')
|
status_type = models.CharField(max_length=255, default='Note')
|
||||||
activity = JSONField(max_length=5000, null=True)
|
mention_users = models.ManyToManyField('User', related_name='mention_user')
|
||||||
|
mention_books = models.ManyToManyField('Book', related_name='mention_book')
|
||||||
|
activity_type = models.CharField(max_length=255, default='Note')
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
privacy = models.CharField(max_length=255, default='public')
|
||||||
|
sensitive = models.BooleanField(default=False)
|
||||||
reply_parent = models.ForeignKey(
|
reply_parent = models.ForeignKey(
|
||||||
'self',
|
'self',
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.PROTECT
|
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()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
|
||||||
class Review(Status):
|
class Review(Status):
|
||||||
''' a book review '''
|
''' a book review '''
|
||||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||||
rating = models.IntegerField(
|
rating = models.IntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(5)]
|
validators=[MinValueValidator(0), MaxValueValidator(5)]
|
||||||
|
@ -85,6 +35,6 @@ class Review(Status):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.status_type = 'Review'
|
self.status_type = 'Review'
|
||||||
|
self.activity_type = 'Article'
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
''' database schema for books and shelves '''
|
''' database schema for books and shelves '''
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from fedireads.settings import DOMAIN
|
||||||
from fedireads.utils.fields import JSONField
|
from fedireads.utils.fields import JSONField
|
||||||
|
from fedireads.utils.models import FedireadsModel
|
||||||
|
|
||||||
|
|
||||||
class Shelf(models.Model):
|
class Shelf(FedireadsModel):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
|
@ -14,14 +17,19 @@ class Shelf(models.Model):
|
||||||
through='ShelfBook',
|
through='ShelfBook',
|
||||||
through_fields=('shelf', 'book')
|
through_fields=('shelf', 'book')
|
||||||
)
|
)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
@property
|
||||||
|
def absolute_id(self):
|
||||||
|
''' use shelf identifier as absolute id '''
|
||||||
|
base_path = self.user.absolute_id
|
||||||
|
model_name = type(self).__name__.lower()
|
||||||
|
return '%s/%s/%s' % (base_path, model_name, self.identifier)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'identifier')
|
unique_together = ('user', 'identifier')
|
||||||
|
|
||||||
|
|
||||||
class ShelfBook(models.Model):
|
class ShelfBook(FedireadsModel):
|
||||||
# many to many join table for books and shelves
|
# many to many join table for books and shelves
|
||||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||||
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
||||||
|
@ -31,13 +39,12 @@ class ShelfBook(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('book', 'shelf')
|
unique_together = ('book', 'shelf')
|
||||||
|
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(FedireadsModel):
|
||||||
''' a non-canonical copy of a work (not book) from open library '''
|
''' a non-canonical copy of a work (not book) from open library '''
|
||||||
openlibrary_key = models.CharField(max_length=255, unique=True)
|
openlibrary_key = models.CharField(max_length=255, unique=True)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
|
@ -56,14 +63,17 @@ class Book(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
@property
|
||||||
|
def absolute_id(self):
|
||||||
|
''' constructs the absolute reference to any db object '''
|
||||||
|
base_path = 'https://%s' % DOMAIN
|
||||||
|
model_name = type(self).__name__.lower()
|
||||||
|
return '%s/%s/%s' % (base_path, model_name, self.openlibrary_key)
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(FedireadsModel):
|
||||||
''' copy of an author from OL '''
|
''' copy of an author from OL '''
|
||||||
openlibrary_key = models.CharField(max_length=255)
|
openlibrary_key = models.CharField(max_length=255)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.dispatch import receiver
|
||||||
|
|
||||||
from fedireads.models import Shelf
|
from fedireads.models import Shelf
|
||||||
from fedireads.settings import DOMAIN
|
from fedireads.settings import DOMAIN
|
||||||
|
from fedireads.utils.models import FedireadsModel
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
@ -24,6 +25,7 @@ class User(AbstractUser):
|
||||||
outbox = models.CharField(max_length=255, unique=True)
|
outbox = models.CharField(max_length=255, unique=True)
|
||||||
summary = models.TextField(blank=True, null=True)
|
summary = models.TextField(blank=True, null=True)
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
fedireads_user = models.BooleanField(default=True)
|
||||||
localname = models.CharField(
|
localname = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -33,11 +35,15 @@ class User(AbstractUser):
|
||||||
name = models.CharField(max_length=100, blank=True, null=True)
|
name = models.CharField(max_length=100, blank=True, null=True)
|
||||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||||
followers = models.ManyToManyField('self', symmetrical=False)
|
followers = models.ManyToManyField('self', symmetrical=False)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
@property
|
||||||
|
def absolute_id(self):
|
||||||
|
''' users are identified by their username, so overriding this prop '''
|
||||||
|
model_name = type(self).__name__.lower()
|
||||||
|
return 'https://%s/%s/%s' % (DOMAIN, model_name, self.localname)
|
||||||
|
|
||||||
|
|
||||||
class FederatedServer(models.Model):
|
class FederatedServer(FedireadsModel):
|
||||||
''' store which server's we federate with '''
|
''' store which server's we federate with '''
|
||||||
server_name = models.CharField(max_length=255, unique=True)
|
server_name = models.CharField(max_length=255, unique=True)
|
||||||
# federated, blocked, whatever else
|
# federated, blocked, whatever else
|
||||||
|
@ -56,10 +62,10 @@ def execute_before_save(sender, instance, *args, **kwargs):
|
||||||
# populate fields for local users
|
# populate fields for local users
|
||||||
instance.localname = instance.username
|
instance.localname = instance.username
|
||||||
instance.username = '%s@%s' % (instance.username, DOMAIN)
|
instance.username = '%s@%s' % (instance.username, DOMAIN)
|
||||||
instance.actor = 'https://%s/user/%s' % (DOMAIN, instance.localname)
|
instance.actor = instance.absolute_id
|
||||||
instance.inbox = 'https://%s/user/%s/inbox' % (DOMAIN, instance.localname)
|
instance.inbox = '%s/inbox' % instance.absolute_id
|
||||||
instance.shared_inbox = 'https://%s/inbox' % DOMAIN
|
instance.shared_inbox = 'https://%s/inbox' % DOMAIN
|
||||||
instance.outbox = 'https://%s/user/%s/outbox' % (DOMAIN, instance.localname)
|
instance.outbox = '%s/outbox' % instance.absolute_id
|
||||||
if not instance.private_key:
|
if not instance.private_key:
|
||||||
random_generator = Random.new().read
|
random_generator = Random.new().read
|
||||||
key = RSA.generate(1024, random_generator)
|
key = RSA.generate(1024, random_generator)
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
''' handles all the activity coming out of the server '''
|
''' handles all the activity coming out of the server '''
|
||||||
from base64 import b64encode
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Signature import pkcs1_15
|
|
||||||
from Crypto.Hash import SHA256
|
|
||||||
from datetime import datetime
|
|
||||||
from django.http import HttpResponseNotFound, JsonResponse
|
from django.http import HttpResponseNotFound, JsonResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
|
from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads.activity import create_review
|
from fedireads.status import create_review, create_status
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -30,7 +24,6 @@ def outbox(request, username):
|
||||||
min_id = request.GET.get('min_id')
|
min_id = request.GET.get('min_id')
|
||||||
max_id = request.GET.get('max_id')
|
max_id = request.GET.get('max_id')
|
||||||
|
|
||||||
query_path = user.outbox + '?'
|
|
||||||
# filters for use in the django queryset min/max
|
# filters for use in the django queryset min/max
|
||||||
filters = {}
|
filters = {}
|
||||||
# params for the outbox page id
|
# params for the outbox page id
|
||||||
|
@ -41,39 +34,20 @@ def outbox(request, username):
|
||||||
if max_id != None:
|
if max_id != None:
|
||||||
params['max_id'] = max_id
|
params['max_id'] = max_id
|
||||||
filters['id__lte'] = max_id
|
filters['id__lte'] = max_id
|
||||||
collection_id = query_path + urlencode(params)
|
|
||||||
|
|
||||||
messages = models.Activity.objects.filter(
|
page_id = user.outbox + '?' + urlencode(params)
|
||||||
|
statuses = models.Status.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
activity_type__in=['Article', 'Note'],
|
|
||||||
**filters
|
**filters
|
||||||
).all()[:limit]
|
).all()[:limit]
|
||||||
|
|
||||||
outbox_page = {
|
return JsonResponse(
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
activitypub.get_outbox_page(user, page_id, statuses, max_id, min_id)
|
||||||
'id': collection_id,
|
)
|
||||||
'type': 'OrderedCollectionPage',
|
|
||||||
'partOf': user.outbox,
|
|
||||||
'orderedItems': [m.content for m in messages],
|
|
||||||
}
|
|
||||||
if max_id:
|
|
||||||
outbox_page['next'] = query_path + \
|
|
||||||
urlencode({'min_id': max_id, 'page': 'true'})
|
|
||||||
if min_id:
|
|
||||||
outbox_page['prev'] = query_path + \
|
|
||||||
urlencode({'max_id': min_id, 'page': 'true'})
|
|
||||||
return JsonResponse(outbox_page)
|
|
||||||
|
|
||||||
# collection overview
|
# collection overview
|
||||||
size = models.Review.objects.filter(user=user).count()
|
size = models.Status.objects.filter(user=user).count()
|
||||||
return JsonResponse({
|
return JsonResponse(activitypub.get_outbox(user, size))
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': '%s/outbox' % user.actor,
|
|
||||||
'type': 'OrderedCollection',
|
|
||||||
'totalItems': size,
|
|
||||||
'first': '%s/outbox?page=true' % user.actor,
|
|
||||||
'last': '%s/outbox?min_id=0&page=true' % user.actor
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def handle_account_search(query):
|
def handle_account_search(query):
|
||||||
|
@ -97,127 +71,56 @@ def handle_account_search(query):
|
||||||
|
|
||||||
def handle_outgoing_follow(user, to_follow):
|
def handle_outgoing_follow(user, to_follow):
|
||||||
''' someone local wants to follow someone '''
|
''' someone local wants to follow someone '''
|
||||||
uuid = uuid4()
|
activity = activitypub.get_follow_request(user, to_follow)
|
||||||
activity = {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': 'https://%s/%s' % (DOMAIN, str(uuid)),
|
|
||||||
'summary': '',
|
|
||||||
'type': 'Follow',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': to_follow.actor,
|
|
||||||
}
|
|
||||||
|
|
||||||
errors = broadcast(user, activity, [to_follow.inbox])
|
errors = broadcast(user, activity, [to_follow.inbox])
|
||||||
for error in errors:
|
for error in errors:
|
||||||
# TODO: following masto users is returning 400
|
|
||||||
raise(error['error'])
|
raise(error['error'])
|
||||||
|
|
||||||
|
|
||||||
def handle_outgoing_accept(user, to_follow, activity):
|
def handle_outgoing_accept(user, to_follow, request_activity):
|
||||||
''' send an acceptance message to a follow request '''
|
''' send an acceptance message to a follow request '''
|
||||||
to_follow.followers.add(user)
|
to_follow.followers.add(user)
|
||||||
activity = {
|
activity = activitypub.get_accept(to_follow, request_activity)
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
|
||||||
'id': 'https://%s/%s#accepts/follows/' % (DOMAIN, to_follow.localname),
|
|
||||||
'type': 'Accept',
|
|
||||||
'actor': to_follow.actor,
|
|
||||||
'object': activity,
|
|
||||||
}
|
|
||||||
recipient = get_recipients(
|
|
||||||
to_follow,
|
|
||||||
'direct',
|
|
||||||
direct_recipients=[user]
|
|
||||||
)
|
|
||||||
broadcast(to_follow, activity, recipient)
|
broadcast(to_follow, activity, recipient)
|
||||||
|
|
||||||
|
|
||||||
def handle_shelve(user, book, shelf):
|
def handle_shelve(user, book, shelf):
|
||||||
''' a local user is getting a book put on their shelf '''
|
''' a local user is getting a book put on their shelf '''
|
||||||
# update the database
|
# update the database
|
||||||
|
# TODO: this should probably happen in incoming instead
|
||||||
models.ShelfBook(book=book, shelf=shelf, added_by=user).save()
|
models.ShelfBook(book=book, shelf=shelf, added_by=user).save()
|
||||||
|
|
||||||
# send out the activitypub action
|
activity = activitypub.get_add(user, book, shelf)
|
||||||
summary = '%s marked %s as %s' % (
|
|
||||||
user.username,
|
|
||||||
book.data['title'],
|
|
||||||
shelf.name
|
|
||||||
)
|
|
||||||
|
|
||||||
uuid = uuid4()
|
|
||||||
activity = {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': str(uuid),
|
|
||||||
'summary': summary,
|
|
||||||
'type': 'Add',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': {
|
|
||||||
'type': 'Document',
|
|
||||||
'name': book.data['title'],
|
|
||||||
'url': book.openlibrary_key
|
|
||||||
},
|
|
||||||
'target': {
|
|
||||||
'type': 'Collection',
|
|
||||||
'name': shelf.name,
|
|
||||||
'id': 'https://%s/user/%s/shelf/%s' % \
|
|
||||||
(DOMAIN, user.localname, shelf.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipients = get_recipients(user, 'public')
|
recipients = get_recipients(user, 'public')
|
||||||
|
|
||||||
models.ShelveActivity(
|
|
||||||
uuid=uuid,
|
|
||||||
user=user,
|
|
||||||
content=activity,
|
|
||||||
shelf=shelf,
|
|
||||||
book=book,
|
|
||||||
).save()
|
|
||||||
|
|
||||||
broadcast(user, activity, recipients)
|
broadcast(user, activity, recipients)
|
||||||
|
|
||||||
|
# tell the world about this cool thing that happened
|
||||||
|
verb = {
|
||||||
|
'to-read': 'wants to read',
|
||||||
|
'reading': 'started reading',
|
||||||
|
'read': 'finished reading'
|
||||||
|
}[shelf.identifier]
|
||||||
|
name = user.name if user.name else user.localname
|
||||||
|
message = '%s %s %s' % (name, verb, book.data['title'])
|
||||||
|
status = create_status(user, message, mention_books=[book])
|
||||||
|
|
||||||
|
activity = activitypub.get_status(status)
|
||||||
|
create_activity = activitypub.get_create(user, activity)
|
||||||
|
|
||||||
|
broadcast(user, create_activity, recipients)
|
||||||
|
|
||||||
|
|
||||||
def handle_unshelve(user, book, shelf):
|
def handle_unshelve(user, book, shelf):
|
||||||
''' a local user is getting a book put on their shelf '''
|
''' a local user is getting a book put on their shelf '''
|
||||||
# update the database
|
# update the database
|
||||||
|
# TODO: this should probably happen in incoming instead
|
||||||
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
||||||
row.delete()
|
row.delete()
|
||||||
|
|
||||||
# send out the activitypub action
|
activity = activitypub.get_remove(user, book, shelf)
|
||||||
summary = '%s removed %s from %s' % (
|
|
||||||
user.username,
|
|
||||||
book.data['title'],
|
|
||||||
shelf.name
|
|
||||||
)
|
|
||||||
|
|
||||||
uuid = uuid4()
|
|
||||||
activity = {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': str(uuid),
|
|
||||||
'summary': summary,
|
|
||||||
'type': 'Remove',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': {
|
|
||||||
'type': 'Document',
|
|
||||||
'name': book.data['title'],
|
|
||||||
'url': book.openlibrary_key
|
|
||||||
},
|
|
||||||
'target': {
|
|
||||||
'type': 'Collection',
|
|
||||||
'name': shelf.name,
|
|
||||||
'id': 'https://%s/user/%s/shelf/%s' % \
|
|
||||||
(DOMAIN, user.localname, shelf.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipients = get_recipients(user, 'public')
|
recipients = get_recipients(user, 'public')
|
||||||
|
|
||||||
models.ShelveActivity(
|
|
||||||
uuid=uuid,
|
|
||||||
user=user,
|
|
||||||
content=activity,
|
|
||||||
shelf=shelf,
|
|
||||||
book=book,
|
|
||||||
activity_type='Remove',
|
|
||||||
).save()
|
|
||||||
|
|
||||||
broadcast(user, activity, recipients)
|
broadcast(user, activity, recipients)
|
||||||
|
|
||||||
|
|
||||||
|
@ -226,63 +129,25 @@ def handle_review(user, book, name, content, rating):
|
||||||
# validated and saves the review in the database so it has an id
|
# validated and saves the review in the database so it has an id
|
||||||
review = create_review(user, book, name, content, rating)
|
review = create_review(user, book, name, content, rating)
|
||||||
|
|
||||||
review_path = 'https://%s/user/%s/status/%d' % \
|
review_activity = activitypub.get_review(review)
|
||||||
(DOMAIN, user.localname, review.id)
|
review_create_activity = activitypub.get_create(user, review_activity)
|
||||||
book_path = 'https://%s/book/%s' % (DOMAIN, review.book.openlibrary_key)
|
fr_recipients = get_recipients(user, 'public', limit='fedireads')
|
||||||
|
broadcast(user, review_create_activity, fr_recipients)
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat() #TODO: should this be http_date?
|
# re-format the activity for non-fedireads servers
|
||||||
review_activity = {
|
article_activity = activitypub.get_review_article(review)
|
||||||
'id': review_path,
|
article_create_activity = activitypub.get_create(user, article_activity)
|
||||||
'url': review_path,
|
|
||||||
'inReplyTo': book_path,
|
|
||||||
'published': now,
|
|
||||||
'attributedTo': user.actor,
|
|
||||||
# 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,
|
|
||||||
'type': 'Note',
|
|
||||||
'fedireadsType': 'Review',
|
|
||||||
'name': name,
|
|
||||||
'rating': rating, # fedireads-only custom field
|
|
||||||
'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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
review.activity = review_activity
|
|
||||||
review.save()
|
|
||||||
|
|
||||||
signer = pkcs1_15.new(RSA.import_key(user.private_key))
|
other_recipients = get_recipients(user, 'public', limit='other')
|
||||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
broadcast(user, article_create_activity, other_recipients)
|
||||||
create_activity = {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
|
|
||||||
'id': '%s/activity' % review_path,
|
|
||||||
'type': 'Create',
|
|
||||||
'actor': user.actor,
|
|
||||||
'published': now,
|
|
||||||
|
|
||||||
'to': ['%s/followers' % user.actor],
|
def handle_comment(user, review, content):
|
||||||
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
''' post a review '''
|
||||||
|
# validated and saves the comment in the database so it has an id
|
||||||
'object': review_activity,
|
comment = create_status(user, content, reply_parent=review)
|
||||||
'signature': {
|
comment_activity = activitypub.get_status(comment)
|
||||||
'type': 'RsaSignature2017',
|
create_activity = activitypub.get_create(user, comment_activity)
|
||||||
'creator': 'https://%s/user/%s#main-key' % (DOMAIN, user.localname),
|
|
||||||
'created': now,
|
|
||||||
'signatureValue': b64encode(signed_message).decode('utf8'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients = get_recipients(user, 'public')
|
recipients = get_recipients(user, 'public')
|
||||||
broadcast(user, create_activity, recipients)
|
broadcast(user, create_activity, recipients)
|
||||||
|
|
|
@ -41,7 +41,8 @@ def get_or_create_remote_user(actor):
|
||||||
shared_inbox=shared_inbox,
|
shared_inbox=shared_inbox,
|
||||||
# TODO: I'm never actually using this for remote users
|
# TODO: I'm never actually using this for remote users
|
||||||
public_key=data.get('publicKey').get('publicKeyPem'),
|
public_key=data.get('publicKey').get('publicKeyPem'),
|
||||||
local=False
|
local=False,
|
||||||
|
fedireads_user=False,
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -9,15 +9,12 @@ def create_review(user, possible_book, name, content, rating):
|
||||||
# throws a value error if the book is not found
|
# throws a value error if the book is not found
|
||||||
book = get_or_create_book(possible_book)
|
book = get_or_create_book(possible_book)
|
||||||
|
|
||||||
# sanitize review html
|
content = sanitize(content)
|
||||||
parser = InputHtmlParser()
|
|
||||||
parser.feed(content)
|
|
||||||
content = parser.get_output()
|
|
||||||
|
|
||||||
# no ratings outside of 0-5
|
# no ratings outside of 0-5
|
||||||
rating = rating if 0 <= rating <= 5 else 0
|
rating = rating if 0 <= rating <= 5 else 0
|
||||||
|
|
||||||
review = models.Review.objects.create(
|
return models.Review.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
book=book,
|
book=book,
|
||||||
name=name,
|
name=name,
|
||||||
|
@ -25,6 +22,31 @@ def create_review(user, possible_book, name, content, rating):
|
||||||
content=content,
|
content=content,
|
||||||
)
|
)
|
||||||
|
|
||||||
return review
|
|
||||||
|
def create_status(user, content, reply_parent=None, mention_books=None):
|
||||||
|
''' a status update '''
|
||||||
|
# TODO: handle @'ing users
|
||||||
|
|
||||||
|
# sanitize input html
|
||||||
|
parser = InputHtmlParser()
|
||||||
|
parser.feed(content)
|
||||||
|
content = parser.get_output()
|
||||||
|
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=user,
|
||||||
|
content=content,
|
||||||
|
reply_parent=reply_parent,
|
||||||
|
)
|
||||||
|
|
||||||
|
if mention_books:
|
||||||
|
for book in mention_books:
|
||||||
|
status.mention_books.add(book)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(content):
|
||||||
|
''' remove invalid html from free text '''
|
||||||
|
parser = InputHtmlParser()
|
||||||
|
parser.feed(content)
|
||||||
|
return parser.get_output()
|
|
@ -55,40 +55,34 @@
|
||||||
<h2>
|
<h2>
|
||||||
{% include 'snippets/avatar.html' with user=activity.user %}
|
{% include 'snippets/avatar.html' with user=activity.user %}
|
||||||
{% include 'snippets/username.html' with user=activity.user %}
|
{% include 'snippets/username.html' with user=activity.user %}
|
||||||
{% if activity.fedireads_type == 'Shelve' %}
|
{% if activity.status_type == 'Review' %}
|
||||||
{# display a reading/shelving activity #}
|
|
||||||
{% if activity.shelf.identifier == 'to-read' %}
|
|
||||||
wants to read
|
|
||||||
{% elif activity.shelf.identifier == 'read' %}
|
|
||||||
finished reading
|
|
||||||
{% elif activity.shelf.identifier == 'reading' %}
|
|
||||||
started reading
|
|
||||||
{% else %}
|
|
||||||
shelved in "{{ activity.shelf.name }}"
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
<div class="book-preview">
|
|
||||||
{% include 'snippets/book.html' with book=activity.book size=large description=True %}
|
|
||||||
</div>
|
|
||||||
<div class="interaction"><button>⭐️ Like</button></div>
|
|
||||||
{% elif activity.fedireads_type == 'Review' %}
|
|
||||||
{# display a review #}
|
{# display a review #}
|
||||||
reviewed {{ activity.book.data.title }}
|
reviewed {{ activity.book.data.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="book-preview review">
|
<div class="book-preview review">
|
||||||
{% include 'snippets/book.html' with book=activity.book size=large %}
|
{% include 'snippets/book.html' with book=activity.book size=large %}
|
||||||
|
|
||||||
<h3>{{ activity.content.name }}</h3>
|
<h3>{{ activity.name }}</h3>
|
||||||
<p>{{ activity.content.rating | stars }}</p>
|
<p>{{ activity.rating | stars }}</p>
|
||||||
<p>{{ activity.content.content | safe }}</p>
|
<p>{{ activity.content | safe }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="interaction"><button>⭐️ Like</button></div>
|
<div class="interaction">
|
||||||
{% elif activity.activity_type == 'Follow' %}
|
<button>⭐️ Like</button>
|
||||||
started following someone
|
<form name="comment" action="/comment" method="post">
|
||||||
</h2>
|
{% csrf_token %}
|
||||||
{% elif activity.activity_type == 'Note' %}
|
<input type="hidden" name="review" value="{{ activity.id }}"></input>
|
||||||
|
{{ comment_form.content }}
|
||||||
|
<button type="submit">Comment</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% elif activity.status_type == 'Note' %}
|
||||||
posted</h2>
|
posted</h2>
|
||||||
{{ activity.content.object.content | safe }}
|
{{ activity.content | safe }}
|
||||||
|
{% for book in activity.mention_books.all %}
|
||||||
|
<div class="book-preview review">
|
||||||
|
{% include 'snippets/book.html' with book=book size=large description=True %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# generic handling for a misc activity, which perhaps should not be displayed at all #}
|
{# generic handling for a misc activity, which perhaps should not be displayed at all #}
|
||||||
did {{ activity.activity_type }}
|
did {{ activity.activity_type }}
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
<img src="/images/{{ book.cover }}" class="book-cover small">
|
<img src="/images/{{ book.cover }}" class="book-cover small">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ book.openlibrary_key }}">{{ book.data.title }}</a>
|
<a href="/book/{{ book.openlibrary_key }}">{{ book.data.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ book.authors.first.data.name }}
|
{{ book.authors.first.data.name }}
|
||||||
|
|
|
@ -17,14 +17,17 @@ urlpatterns = [
|
||||||
re_path(r'^user/(?P<username>\w+)/followers/?$', incoming.get_followers),
|
re_path(r'^user/(?P<username>\w+)/followers/?$', incoming.get_followers),
|
||||||
re_path(r'^user/(?P<username>\w+)/following/?$', incoming.get_following),
|
re_path(r'^user/(?P<username>\w+)/following/?$', incoming.get_following),
|
||||||
re_path(
|
re_path(
|
||||||
r'^user/(?P<username>\w+)/status/(?P<status_id>\d+)/?$',
|
r'^user/(?P<username>\w+)/(status|review)/(?P<status_id>\d+)/?$',
|
||||||
incoming.get_status
|
incoming.get_status
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r'^user/(?P<username>\w+)/status/(?P<status_id>\d+)/activity/?$',
|
r'^user/(?P<username>\w+)/(status|review)/(?P<status_id>\d+)/activity/?$',
|
||||||
incoming.get_status
|
incoming.get_status
|
||||||
),
|
),
|
||||||
re_path(r'^user/(?P<username>\w+)/status/?$', incoming.get_following),
|
re_path(
|
||||||
|
r'^user/(?P<username>\w+)/(status|review)/(?P<status_id>\d+)/replies/?$',
|
||||||
|
incoming.get_replies
|
||||||
|
),
|
||||||
# TODO: shelves need pages in the UI and for their activitypub Collection
|
# TODO: shelves need pages in the UI and for their activitypub Collection
|
||||||
|
|
||||||
# .well-known endpoints
|
# .well-known endpoints
|
||||||
|
@ -47,6 +50,7 @@ urlpatterns = [
|
||||||
|
|
||||||
# internal action endpoints
|
# internal action endpoints
|
||||||
re_path(r'^review/?$', views.review),
|
re_path(r'^review/?$', views.review),
|
||||||
|
re_path(r'^comment/?$', views.comment),
|
||||||
re_path(
|
re_path(
|
||||||
r'^shelve/(?P<username>\w+)/(?P<shelf_id>[\w-]+)/(?P<book_id>\d+)/?$',
|
r'^shelve/(?P<username>\w+)/(?P<shelf_id>[\w-]+)/(?P<book_id>\d+)/?$',
|
||||||
views.shelve
|
views.shelve
|
||||||
|
|
21
fedireads/utils/models.py
Normal file
21
fedireads/utils/models.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from fedireads.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class FedireadsModel(models.Model):
|
||||||
|
content = models.TextField(blank=True, null=True)
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def absolute_id(self):
|
||||||
|
''' constructs the absolute reference to any db object '''
|
||||||
|
base_path = 'https://%s' % DOMAIN
|
||||||
|
if self.user:
|
||||||
|
base_path = self.user.absolute_id
|
||||||
|
model_name = type(self).__name__.lower()
|
||||||
|
return '%s/%s/%d' % (base_path, model_name, self.id)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
|
@ -29,22 +29,21 @@ def home(request):
|
||||||
|
|
||||||
# books new to the instance, for discovery
|
# books new to the instance, for discovery
|
||||||
recent_books = models.Book.objects.order_by(
|
recent_books = models.Book.objects.order_by(
|
||||||
'-added_date'
|
'-created_date'
|
||||||
)[:5]
|
)[:5]
|
||||||
|
|
||||||
# status updates for your follow network
|
# status updates for your follow network
|
||||||
following = models.User.objects.filter(
|
following = models.User.objects.filter(
|
||||||
Q(followers=request.user) | Q(id=request.user.id)
|
Q(followers=request.user) | Q(id=request.user.id)
|
||||||
)
|
)
|
||||||
# TODO: this is fundamentally not how the feed should work I think? it
|
|
||||||
# should do something smart with inboxes. (in this implementation it would
|
activities = models.Status.objects.filter(
|
||||||
# show DMs meant for other local users)
|
Q(user__in=following, privacy='public') | Q(mention_users=request.user)
|
||||||
activities = models.Activity.objects.filter(
|
|
||||||
user__in=following,
|
|
||||||
).select_subclasses().order_by(
|
).select_subclasses().order_by(
|
||||||
'-created_date'
|
'-created_date'
|
||||||
)[:10]
|
)[:10]
|
||||||
|
|
||||||
|
comment_form = forms.CommentForm()
|
||||||
data = {
|
data = {
|
||||||
'user': request.user,
|
'user': request.user,
|
||||||
'reading': reading,
|
'reading': reading,
|
||||||
|
@ -52,6 +51,7 @@ def home(request):
|
||||||
'recent_books': recent_books,
|
'recent_books': recent_books,
|
||||||
'user_books': user_books,
|
'user_books': user_books,
|
||||||
'activities': activities,
|
'activities': activities,
|
||||||
|
'comment_form': comment_form,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'feed.html', data)
|
return TemplateResponse(request, 'feed.html', data)
|
||||||
|
|
||||||
|
@ -252,6 +252,18 @@ def review(request):
|
||||||
return redirect('/book/%s' % book_identifier)
|
return redirect('/book/%s' % book_identifier)
|
||||||
|
|
||||||
|
|
||||||
|
def comment(request):
|
||||||
|
''' respond to a book review '''
|
||||||
|
form = forms.CommentForm(request.POST)
|
||||||
|
# this is a bit of a formality, the form is just one text field
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect('/')
|
||||||
|
review_id = request.POST['review']
|
||||||
|
parent = models.Review.objects.get(id=review_id)
|
||||||
|
outgoing.handle_comment(request.user, parent, form.data['content'])
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def follow(request):
|
def follow(request):
|
||||||
''' follow another user, here or abroad '''
|
''' follow another user, here or abroad '''
|
||||||
|
|
Loading…
Reference in a new issue