@ -157,10 +157,14 @@ class ActivitypubMixin:
|
||||
return recipients
|
||||
|
||||
|
||||
def to_activity(self):
|
||||
def to_activity_dataclass(self):
|
||||
''' convert from a model to an activity '''
|
||||
activity = generate_activity(self)
|
||||
return self.activity_serializer(**activity).serialize()
|
||||
return self.activity_serializer(**activity)
|
||||
|
||||
def to_activity(self, **kwargs): # pylint: disable=unused-argument
|
||||
''' convert from a model to a json activity '''
|
||||
return self.to_activity_dataclass().serialize()
|
||||
|
||||
|
||||
class ObjectMixin(ActivitypubMixin):
|
||||
@ -188,7 +192,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
|
||||
try:
|
||||
software = None
|
||||
# do we have a "pure" activitypub version of this for mastodon?
|
||||
# do we have a "pure" activitypub version of this for mastodon?
|
||||
if hasattr(self, 'pure_content'):
|
||||
pure_activity = self.to_create_activity(user, pure=True)
|
||||
self.broadcast(pure_activity, user, software='other')
|
||||
@ -196,7 +200,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
# sends to BW only if we just did a pure version for masto
|
||||
activity = self.to_create_activity(user)
|
||||
self.broadcast(activity, user, software=software)
|
||||
except KeyError:
|
||||
except AttributeError:
|
||||
# janky as heck, this catches the mutliple inheritence chain
|
||||
# for boosts and ignores this auxilliary broadcast
|
||||
return
|
||||
@ -225,26 +229,26 @@ class ObjectMixin(ActivitypubMixin):
|
||||
|
||||
def to_create_activity(self, user, **kwargs):
|
||||
''' returns the object wrapped in a Create activity '''
|
||||
activity_object = self.to_activity(**kwargs)
|
||||
activity_object = self.to_activity_dataclass(**kwargs)
|
||||
|
||||
signature = None
|
||||
create_id = self.remote_id + '/activity'
|
||||
if 'content' in activity_object and activity_object['content']:
|
||||
if hasattr(activity_object, 'content') and activity_object.content:
|
||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||
content = activity_object['content']
|
||||
content = activity_object.content
|
||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
||||
|
||||
signature = activitypub.Signature(
|
||||
creator='%s#main-key' % user.remote_id,
|
||||
created=activity_object['published'],
|
||||
created=activity_object.published,
|
||||
signatureValue=b64encode(signed_message).decode('utf8')
|
||||
)
|
||||
|
||||
return activitypub.Create(
|
||||
id=create_id,
|
||||
actor=user.remote_id,
|
||||
to=activity_object['to'],
|
||||
cc=activity_object['cc'],
|
||||
to=activity_object.to,
|
||||
cc=activity_object.cc,
|
||||
object=activity_object,
|
||||
signature=signature,
|
||||
).serialize()
|
||||
@ -257,7 +261,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
actor=user.remote_id,
|
||||
to=['%s/followers' % user.remote_id],
|
||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity(),
|
||||
object=self,
|
||||
).serialize()
|
||||
|
||||
|
||||
@ -268,7 +272,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
id=activity_id,
|
||||
actor=user.remote_id,
|
||||
to=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity()
|
||||
object=self
|
||||
).serialize()
|
||||
|
||||
|
||||
@ -283,7 +287,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||
|
||||
def to_ordered_collection(self, queryset, \
|
||||
remote_id=None, page=False, collection_only=False, **kwargs):
|
||||
'pure=pure, '' an ordered collection of whatevers '''
|
||||
''' an ordered collection of whatevers '''
|
||||
if not queryset.ordered:
|
||||
raise RuntimeError('queryset must be ordered')
|
||||
|
||||
@ -309,7 +313,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||
activity['first'] = '%s?page=1' % remote_id
|
||||
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
||||
|
||||
return serializer(**activity).serialize()
|
||||
return serializer(**activity)
|
||||
|
||||
|
||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
@ -321,9 +325,13 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
|
||||
activity_serializer = activitypub.OrderedCollection
|
||||
|
||||
def to_activity_dataclass(self, **kwargs):
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
|
||||
def to_activity(self, **kwargs):
|
||||
''' an ordered collection of the specified model queryset '''
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
return self.to_ordered_collection(
|
||||
self.collection_queryset, **kwargs).serialize()
|
||||
|
||||
|
||||
class CollectionItemMixin(ActivitypubMixin):
|
||||
@ -360,7 +368,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=self.user.remote_id,
|
||||
object=object_field.to_activity(),
|
||||
object=object_field,
|
||||
target=collection_field.remote_id
|
||||
).serialize()
|
||||
|
||||
@ -371,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
return activitypub.Remove(
|
||||
id='%s#remove' % self.remote_id,
|
||||
actor=self.user.remote_id,
|
||||
object=object_field.to_activity(),
|
||||
object=object_field,
|
||||
target=collection_field.remote_id
|
||||
).serialize()
|
||||
|
||||
@ -400,7 +408,7 @@ class ActivityMixin(ActivitypubMixin):
|
||||
return activitypub.Undo(
|
||||
id='%s#undo' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.to_activity()
|
||||
object=self,
|
||||
).serialize()
|
||||
|
||||
|
||||
@ -495,4 +503,4 @@ def to_ordered_collection_page(
|
||||
orderedItems=items,
|
||||
next=next_page,
|
||||
prev=prev_page
|
||||
).serialize()
|
||||
)
|
||||
|
@ -122,13 +122,12 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||
return None
|
||||
|
||||
related_model = self.related_model
|
||||
if isinstance(value, dict) and value.get('id'):
|
||||
if hasattr(value, 'id') and value.id:
|
||||
if not self.load_remote:
|
||||
# only look in the local database
|
||||
return related_model.find_existing(value)
|
||||
return related_model.find_existing(value.serialize())
|
||||
# this is an activitypub object, which we can deserialize
|
||||
activity_serializer = related_model.activity_serializer
|
||||
return activity_serializer(**value).to_model(related_model)
|
||||
return value.to_model(model=related_model)
|
||||
try:
|
||||
# make sure the value looks like a remote id
|
||||
validate_remote_id(value)
|
||||
@ -139,7 +138,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||
if not self.load_remote:
|
||||
# only look in the local database
|
||||
return related_model.find_existing_by_remote_id(value)
|
||||
return activitypub.resolve_remote_id(related_model, value)
|
||||
return activitypub.resolve_remote_id(value, model=related_model)
|
||||
|
||||
|
||||
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||
@ -280,7 +279,8 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||
except ValidationError:
|
||||
continue
|
||||
items.append(
|
||||
activitypub.resolve_remote_id(self.related_model, remote_id)
|
||||
activitypub.resolve_remote_id(
|
||||
remote_id, model=self.related_model)
|
||||
)
|
||||
return items
|
||||
|
||||
@ -317,7 +317,8 @@ class TagField(ManyToManyField):
|
||||
# tags can contain multiple types
|
||||
continue
|
||||
items.append(
|
||||
activitypub.resolve_remote_id(self.related_model, link.href)
|
||||
activitypub.resolve_remote_id(
|
||||
link.href, model=self.related_model)
|
||||
)
|
||||
return items
|
||||
|
||||
@ -366,8 +367,8 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
image_slug = value
|
||||
# when it's an inline image (User avatar/icon, Book cover), it's a json
|
||||
# blob, but when it's an attached image, it's just a url
|
||||
if isinstance(image_slug, dict):
|
||||
url = image_slug.get('url')
|
||||
if hasattr(image_slug, 'url'):
|
||||
url = image_slug.url
|
||||
elif isinstance(image_slug, str):
|
||||
url = image_slug
|
||||
else:
|
||||
|
@ -68,7 +68,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||
order = fields.IntegerField(blank=True, null=True)
|
||||
endorsement = models.ManyToManyField('User', related_name='endorsers')
|
||||
|
||||
activity_serializer = activitypub.AddListItem
|
||||
activity_serializer = activitypub.Add
|
||||
object_field = 'book'
|
||||
collection_field = 'book_list'
|
||||
|
||||
|
@ -5,6 +5,7 @@ from django.db.models import Q
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
|
||||
from .activitypub_mixin import generate_activity
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
@ -55,10 +56,13 @@ class UserRelationship(BookWyrmModel):
|
||||
return '%s#%s/%d' % (base_path, status, self.id)
|
||||
|
||||
|
||||
class UserFollows(ActivitypubMixin, UserRelationship):
|
||||
class UserFollows(ActivityMixin, UserRelationship):
|
||||
''' Following a user '''
|
||||
status = 'follows'
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def to_activity(self):
|
||||
''' overrides default to manually set serializer '''
|
||||
return activitypub.Follow(**generate_activity(self))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' really really don't let a user follow someone who blocked them '''
|
||||
@ -73,7 +77,9 @@ class UserFollows(ActivitypubMixin, UserRelationship):
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
super().save(*args, **kwargs)
|
||||
# don't broadcast this type of relationship -- accepts and requests
|
||||
# are handled by the UserFollowRequest model
|
||||
super().save(*args, broadcast=False, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
@ -109,16 +115,19 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
self.broadcast(self.to_activity(), self.user_subject)
|
||||
|
||||
if self.user_object.local:
|
||||
manually_approves = self.user_object.manually_approves_followers
|
||||
if not manually_approves:
|
||||
self.accept()
|
||||
|
||||
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
||||
notification_type = 'FOLLOW_REQUEST' \
|
||||
if self.user_object.manually_approves_followers else 'FOLLOW'
|
||||
notification_type = 'FOLLOW_REQUEST' if \
|
||||
manually_approves else 'FOLLOW'
|
||||
model.objects.create(
|
||||
user=self.user_object,
|
||||
related_user=self.user_subject,
|
||||
@ -129,28 +138,30 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
def accept(self):
|
||||
''' turn this request into the real deal'''
|
||||
user = self.user_object
|
||||
activity = activitypub.Accept(
|
||||
id=self.get_remote_id(status='accepts'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
if not self.user_subject.local:
|
||||
activity = activitypub.Accept(
|
||||
id=self.get_remote_id(status='accepts'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
self.broadcast(activity, user)
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
self.delete()
|
||||
|
||||
self.broadcast(activity, user)
|
||||
|
||||
|
||||
def reject(self):
|
||||
''' generate a Reject for this follow request '''
|
||||
user = self.user_object
|
||||
activity = activitypub.Reject(
|
||||
id=self.get_remote_id(status='rejects'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
if self.user_object.local:
|
||||
activity = activitypub.Reject(
|
||||
id=self.get_remote_id(status='rejects'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
self.broadcast(activity, self.user_object)
|
||||
|
||||
self.delete()
|
||||
self.broadcast(activity, user)
|
||||
|
||||
|
||||
class UserBlocks(ActivityMixin, UserRelationship):
|
||||
|
@ -57,7 +57,7 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
activity_serializer = activitypub.Add
|
||||
object_field = 'book'
|
||||
collection_field = 'shelf'
|
||||
|
||||
|
@ -84,6 +84,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
related_status=self,
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):#pylint: disable=unused-argument
|
||||
''' "delete" a status '''
|
||||
if hasattr(self, 'boosted_status'):
|
||||
# okay but if it's a boost really delete it
|
||||
super().delete(*args, **kwargs)
|
||||
return
|
||||
self.deleted = True
|
||||
self.deleted_date = timezone.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
''' tagged users who definitely need to get this status in broadcast '''
|
||||
@ -96,6 +106,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
@classmethod
|
||||
def ignore_activity(cls, activity):
|
||||
''' keep notes if they are replies to existing statuses '''
|
||||
if activity.type == 'Announce':
|
||||
# keep it if the booster or the boosted are local
|
||||
boosted = activitypub.resolve_remote_id(activity.object, save=False)
|
||||
return cls.ignore_activity(boosted.to_activity_dataclass())
|
||||
|
||||
# keep if it if it's a custom type
|
||||
if activity.type != 'Note':
|
||||
return False
|
||||
if cls.objects.filter(
|
||||
@ -106,8 +122,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
if activity.tag == MISSING or activity.tag is None:
|
||||
return True
|
||||
tags = [l['href'] for l in activity.tag if l['type'] == 'Mention']
|
||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
||||
for tag in tags:
|
||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
||||
if user_model.objects.filter(
|
||||
remote_id=tag, local=True).exists():
|
||||
# we found a mention of a known use boost
|
||||
@ -139,9 +155,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
remote_id='%s/replies' % self.remote_id,
|
||||
collection_only=True,
|
||||
**kwargs
|
||||
)
|
||||
).serialize()
|
||||
|
||||
def to_activity(self, pure=False):# pylint: disable=arguments-differ
|
||||
def to_activity_dataclass(self, pure=False):# pylint: disable=arguments-differ
|
||||
''' return tombstone if the status is deleted '''
|
||||
if self.deleted:
|
||||
return activitypub.Tombstone(
|
||||
@ -149,25 +165,29 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
url=self.remote_id,
|
||||
deleted=self.deleted_date.isoformat(),
|
||||
published=self.deleted_date.isoformat()
|
||||
).serialize()
|
||||
activity = ActivitypubMixin.to_activity(self)
|
||||
activity['replies'] = self.to_replies()
|
||||
)
|
||||
activity = ActivitypubMixin.to_activity_dataclass(self)
|
||||
activity.replies = self.to_replies()
|
||||
|
||||
# "pure" serialization for non-bookwyrm instances
|
||||
if pure and hasattr(self, 'pure_content'):
|
||||
activity['content'] = self.pure_content
|
||||
if 'name' in activity:
|
||||
activity['name'] = self.pure_name
|
||||
activity['type'] = self.pure_type
|
||||
activity['attachment'] = [
|
||||
activity.content = self.pure_content
|
||||
if hasattr(activity, 'name'):
|
||||
activity.name = self.pure_name
|
||||
activity.type = self.pure_type
|
||||
activity.attachment = [
|
||||
image_serializer(b.cover, b.alt_text) \
|
||||
for b in self.mention_books.all()[:4] if b.cover]
|
||||
if hasattr(self, 'book') and self.book.cover:
|
||||
activity['attachment'].append(
|
||||
activity.attachment.append(
|
||||
image_serializer(self.book.cover, self.book.alt_text)
|
||||
)
|
||||
return activity
|
||||
|
||||
def to_activity(self, pure=False):# pylint: disable=arguments-differ
|
||||
''' json serialized activitypub class '''
|
||||
return self.to_activity_dataclass(pure=pure).serialize()
|
||||
|
||||
|
||||
class GeneratedNote(Status):
|
||||
''' these are app-generated messages about user activity '''
|
||||
@ -266,7 +286,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 '''
|
||||
|
@ -1,6 +1,7 @@
|
||||
''' models for storing different kinds of Activities '''
|
||||
import urllib.parse
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
@ -15,17 +16,15 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
|
||||
name = fields.CharField(max_length=100, unique=True)
|
||||
identifier = models.CharField(max_length=100)
|
||||
|
||||
@classmethod
|
||||
def book_queryset(cls, identifier):
|
||||
''' county of books associated with this tag '''
|
||||
return cls.objects.filter(
|
||||
identifier=identifier
|
||||
).order_by('-updated_date')
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
''' books associated with this tag '''
|
||||
return self.book_queryset(self.identifier)
|
||||
def books(self):
|
||||
''' count of books associated with this tag '''
|
||||
edition_model = apps.get_model('bookwyrm.Edition', require_ready=True)
|
||||
return edition_model.objects.filter(
|
||||
usertag__tag__identifier=self.identifier
|
||||
).order_by('-created_date').distinct()
|
||||
|
||||
collection_queryset = books
|
||||
|
||||
def get_remote_id(self):
|
||||
''' tag should use identifier not id in remote_id '''
|
||||
@ -50,7 +49,7 @@ class UserTag(CollectionItemMixin, BookWyrmModel):
|
||||
tag = fields.ForeignKey(
|
||||
'Tag', on_delete=models.PROTECT, activitypub_field='target')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
activity_serializer = activitypub.Add
|
||||
object_field = 'book'
|
||||
collection_field = 'tag'
|
||||
|
||||
|
@ -140,7 +140,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||
privacy__in=['public', 'unlisted'],
|
||||
).select_subclasses().order_by('-published_date')
|
||||
return self.to_ordered_collection(queryset, \
|
||||
collection_only=True, remote_id=self.outbox, **kwargs)
|
||||
collection_only=True, remote_id=self.outbox, **kwargs).serialize()
|
||||
|
||||
def to_following_activity(self, **kwargs):
|
||||
''' activitypub following list '''
|
||||
@ -375,4 +375,4 @@ def get_remote_reviews(outbox):
|
||||
for activity in data['orderedItems']:
|
||||
if not activity['type'] == 'Review':
|
||||
continue
|
||||
activitypub.Review(**activity).to_model(Review)
|
||||
activitypub.Review(**activity).to_model()
|
||||
|
Reference in New Issue
Block a user