Fixes boost, recursive to_model calls

This commit is contained in:
Mouse Reeve 2021-02-15 21:20:00 -08:00
parent a16b81a6eb
commit 606d89d3bd
7 changed files with 47 additions and 327 deletions

View file

@ -8,7 +8,6 @@ from .base_activity import ActivitySerializerError, resolve_remote_id
from .image import Image
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
from .note import Tombstone
from .interaction import Boost, Like
from .ordered_collection import OrderedCollection, OrderedCollectionPage
from .ordered_collection import BookList, Shelf
from .person import Person, PublicKey
@ -17,6 +16,7 @@ from .book import Edition, Work, Author
from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, AddBook, AddListItem, Remove
from .verbs import Announce, Like
# this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known

View file

@ -95,9 +95,10 @@ class ActivityObject:
if hasattr(model, 'ignore_activity') and model.ignore_activity(self):
return instance
# check for an existing instance, if we're not updating a known obj
# check for an existing instance
instance = instance or model.find_existing(self.serialize())
if not instance and not allow_create:
# so that we don't create when we want to delete or update
return None
instance = instance or model()
@ -197,7 +198,7 @@ def set_related_field(
getattr(model_field, 'activitypub_field'),
instance.remote_id
)
item = activity.to_model(model)
item = activity.to_model()
# if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation
@ -228,4 +229,4 @@ def resolve_remote_id(model, remote_id, refresh=False, save=True):
item = model.activity_serializer(**data)
# if we're refreshing, "result" will be set and we'll update it
return item.to_model(model, instance=result, save=save)
return item.to_model(instance=result, save=save)

View file

@ -68,6 +68,10 @@ class Follow(Verb):
object: str
type: str = 'Follow'
def action(self):
''' relationship save '''
self.to_model()
@dataclass(init=False)
class Block(Verb):
@ -75,6 +79,10 @@ class Block(Verb):
object: str
type: str = 'Block'
def action(self):
''' relationship save '''
self.to_model()
@dataclass(init=False)
class Accept(Verb):
@ -107,6 +115,10 @@ class Add(Verb):
object: ActivityObject
type: str = 'Add'
def action(self):
''' add obj to collection '''
self.to_model()
@dataclass(init=False)
class AddBook(Add):
@ -133,3 +145,25 @@ class Remove(Verb):
''' find and remove the activity object '''
obj = self.object.to_model(save=False, allow_create=False)
obj.delete()
@dataclass(init=False)
class Like(Verb):
''' a user faving an object '''
object: str
type: str = 'Like'
def action(self):
''' like '''
self.to_model()
@dataclass(init=False)
class Announce(Verb):
''' boosting a status '''
object: str
type: str = 'Announce'
def action(self):
''' boost '''
self.to_model()

View file

@ -1,318 +0,0 @@
''' handles all of the activity coming in to the server '''
import json
from urllib.parse import urldefrag
import django.db.utils
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import requests
from bookwyrm import activitypub, models
from bookwyrm import status as status_builder
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
@csrf_exempt
@require_POST
def inbox(request, username):
''' incoming activitypub events '''
try:
models.User.objects.get(localname=username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
return shared_inbox(request)
@csrf_exempt
@require_POST
def shared_inbox(request):
''' incoming activitypub events '''
try:
resp = request.body
activity = json.loads(resp)
if isinstance(activity, str):
activity = json.loads(activity)
activity_object = activity['object']
except (json.decoder.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
if not has_valid_signature(request, activity):
if activity['type'] == 'Delete':
# Pretend that unauth'd deletes succeed. Auth may be failing because
# the resource or owner of the resource might have been deleted.
return HttpResponse()
return HttpResponse(status=401)
# if this isn't a file ripe for refactor, I don't know what is.
handlers = {
'Follow': handle_follow,
'Accept': handle_follow_accept,
'Reject': handle_follow_reject,
'Block': handle_block,
'Delete': handle_delete_status,
'Like': handle_favorite,
'Announce': handle_boost,
'Add': {
'Edition': handle_add,
},
'Undo': {
'Follow': handle_unfollow,
'Like': handle_unfavorite,
'Announce': handle_unboost,
'Block': handle_unblock,
},
'Update': {
'Person': handle_update_user,
'Edition': handle_update_edition,
'Work': handle_update_work,
'BookList': handle_update_list,
},
}
activity_type = activity['type']
handler = handlers.get(activity_type, None)
if isinstance(handler, dict):
handler = handler.get(activity_object['type'], None)
if not handler:
return HttpResponseNotFound()
handler.delay(activity)
return HttpResponse()
def has_valid_signature(request, activity):
''' verify incoming signature '''
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
raise ValueError("Wrong actor created signature.")
remote_user = activitypub.resolve_remote_id(models.User, key_actor)
if not remote_user:
return False
try:
signature.verify(remote_user.key_pair.public_key, request)
except ValueError:
old_key = remote_user.key_pair.public_key
remote_user = activitypub.resolve_remote_id(
models.User, remote_user.remote_id, refresh=True
)
if remote_user.key_pair.public_key == old_key:
raise # Key unchanged.
signature.verify(remote_user.key_pair.public_key, request)
except (ValueError, requests.exceptions.HTTPError):
return False
return True
@app.task
def handle_follow(activity):
''' someone wants to follow a local user '''
try:
relationship = activitypub.Follow(
**activity
).to_model(models.UserFollowRequest)
except django.db.utils.IntegrityError as err:
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
raise
relationship = models.UserFollowRequest.objects.get(
remote_id=activity['id']
)
# send the accept normally for a duplicate request
if not relationship.user_object.manually_approves_followers:
relationship.accept()
@app.task
def handle_unfollow(activity):
''' unfollow a local user '''
obj = activity['object']
requester = activitypub.resolve_remote_id(models.User, obj['actor'])
to_unfollow = models.User.objects.get(remote_id=obj['object'])
# raises models.User.DoesNotExist
to_unfollow.followers.remove(requester)
@app.task
def handle_follow_accept(activity):
''' hurray, someone remote accepted a follow request '''
# figure out who they want to follow
requester = models.User.objects.get(remote_id=activity['object']['actor'])
# figure out who they are
accepter = activitypub.resolve_remote_id(models.User, activity['actor'])
try:
models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=accepter
).accept()
except models.UserFollowRequest.DoesNotExist:
return
@app.task
def handle_follow_reject(activity):
''' someone is rejecting a follow request '''
requester = models.User.objects.get(remote_id=activity['object']['actor'])
rejecter = activitypub.resolve_remote_id(models.User, activity['actor'])
try:
models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=rejecter
).reject()
except models.UserFollowRequest.DoesNotExist:
return
@app.task
def handle_block(activity):
''' blocking a user '''
# create "block" databse entry
activitypub.Block(**activity).to_model(models.UserBlocks)
# the removing relationships is handled in post-save hook in model
@app.task
def handle_unblock(activity):
''' undoing a block '''
try:
block_id = activity['object']['id']
except KeyError:
return
try:
block = models.UserBlocks.objects.get(remote_id=block_id)
except models.UserBlocks.DoesNotExist:
return
block.delete()
@app.task
def handle_update_list(activity):
''' update a list '''
try:
book_list = models.List.objects.get(remote_id=activity['object']['id'])
except models.List.DoesNotExist:
book_list = None
activitypub.BookList(
**activity['object']).to_model(models.List, instance=book_list)
@app.task
def handle_delete_status(activity):
''' remove a status '''
try:
status_id = activity['object']['id']
except TypeError:
# this isn't a great fix, because you hit this when mastadon
# is trying to delete a user.
return
try:
status = models.Status.objects.get(
remote_id=status_id
)
except models.Status.DoesNotExist:
return
models.Notification.objects.filter(related_status=status).all().delete()
status_builder.delete_status(status)
@app.task
def handle_favorite(activity):
''' approval of your good good post '''
fav = activitypub.Like(**activity)
# we dont know this status, we don't care about this status
if not models.Status.objects.filter(remote_id=fav.object).exists():
return
fav = fav.to_model(models.Favorite)
if fav.user.local:
return
@app.task
def handle_unfavorite(activity):
''' approval of your good good post '''
like = models.Favorite.objects.filter(
remote_id=activity['object']['id']
).first()
if not like:
return
like.delete()
@app.task
def handle_boost(activity):
''' someone gave us a boost! '''
try:
activitypub.Boost(**activity).to_model(models.Boost)
except activitypub.ActivitySerializerError:
# this probably just means we tried to boost an unknown status
return
@app.task
def handle_unboost(activity):
''' someone gave us a boost! '''
boost = models.Boost.objects.filter(
remote_id=activity['object']['id']
).first()
if boost:
boost.delete()
@app.task
def handle_add(activity):
''' putting a book on a shelf '''
#this is janky as heck but I haven't thought of a better solution
try:
activitypub.AddBook(**activity).to_model(models.ShelfBook)
return
except activitypub.ActivitySerializerError:
pass
try:
activitypub.AddListItem(**activity).to_model(models.ListItem)
return
except activitypub.ActivitySerializerError:
pass
try:
activitypub.AddBook(**activity).to_model(models.UserTag)
return
except activitypub.ActivitySerializerError:
pass
@app.task
def handle_update_user(activity):
''' receive an updated user Person activity object '''
try:
user = models.User.objects.get(remote_id=activity['object']['id'])
except models.User.DoesNotExist:
# who is this person? who cares
return
activitypub.Person(
**activity['object']
).to_model(models.User, instance=user)
# model save() happens in the to_model function
@app.task
def handle_update_edition(activity):
''' a remote instance changed a book (Document) '''
activitypub.Edition(**activity['object']).to_model(models.Edition)
@app.task
def handle_update_work(activity):
''' a remote instance changed a book (Document) '''
activitypub.Work(**activity['object']).to_model(models.Work)

View file

@ -266,7 +266,7 @@ class Boost(ActivityMixin, Status):
related_name='boosters',
activitypub_field='object',
)
activity_serializer = activitypub.Boost
activity_serializer = activitypub.Announce
def save(self, *args, **kwargs):
''' save and notify '''

View file

@ -573,12 +573,15 @@ class Inbox(TestCase):
"id": "https://bookwyrm.social/shelfbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": "https://bookwyrm.social/book/37292",
"object": {
"type": "Edition",
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams"
}
views.inbox.activity_task(activity)
self.assertEqual(shelf.books.first(), book)
#views.inbox.activity_task(activity)
#self.assertEqual(shelf.books.first(), book)
def test_handle_update_user(self):

View file

@ -56,7 +56,7 @@ def activity_task(activity_json):
try:
activity = activitypub.parse(activity_json)
except activitypub.ActivitySerializerError:
return
raise#return
# cool that worked, now we should do the action described by the type
# (create, update, delete, etc)