Merge branch 'main' into review-rate
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
''' bring activitypub functions into the namespace '''
|
||||
""" bring activitypub functions into the namespace """
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
@ -22,9 +22,9 @@ 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
|
||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||
activity_objects = {c[0]: c[1] for c in cls_members \
|
||||
if hasattr(c[1], 'to_model')}
|
||||
activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model")}
|
||||
|
||||
|
||||
def parse(activity_json):
|
||||
''' figure out what activity this is and parse it '''
|
||||
""" figure out what activity this is and parse it """
|
||||
return naive_parse(activity_objects, activity_json)
|
||||
|
@ -1,4 +1,4 @@
|
||||
''' basics for an activitypub serializer '''
|
||||
""" basics for an activitypub serializer """
|
||||
from dataclasses import dataclass, fields, MISSING
|
||||
from json import JSONEncoder
|
||||
|
||||
@ -8,46 +8,52 @@ from django.db import IntegrityError, transaction
|
||||
from bookwyrm.connectors import ConnectorException, get_data
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
|
||||
class ActivitySerializerError(ValueError):
|
||||
''' routine problems serializing activitypub json '''
|
||||
""" routine problems serializing activitypub json """
|
||||
|
||||
|
||||
class ActivityEncoder(JSONEncoder):
|
||||
''' used to convert an Activity object into json '''
|
||||
""" used to convert an Activity object into json """
|
||||
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
''' for tagging a book in a status '''
|
||||
""" for tagging a book in a status """
|
||||
|
||||
href: str
|
||||
name: str
|
||||
type: str = 'Link'
|
||||
type: str = "Link"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mention(Link):
|
||||
''' a subtype of Link for mentioning an actor '''
|
||||
type: str = 'Mention'
|
||||
""" a subtype of Link for mentioning an actor """
|
||||
|
||||
type: str = "Mention"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signature:
|
||||
''' public key block '''
|
||||
""" public key block """
|
||||
|
||||
creator: str
|
||||
created: str
|
||||
signatureValue: str
|
||||
type: str = 'RsaSignature2017'
|
||||
type: str = "RsaSignature2017"
|
||||
|
||||
|
||||
def naive_parse(activity_objects, activity_json, serializer=None):
|
||||
''' this navigates circular import issues '''
|
||||
""" this navigates circular import issues """
|
||||
if not serializer:
|
||||
if activity_json.get('publicKeyPem'):
|
||||
if activity_json.get("publicKeyPem"):
|
||||
# ugh
|
||||
activity_json['type'] = 'PublicKey'
|
||||
activity_json["type"] = "PublicKey"
|
||||
try:
|
||||
activity_type = activity_json['type']
|
||||
activity_type = activity_json["type"]
|
||||
serializer = activity_objects[activity_type]
|
||||
except KeyError as e:
|
||||
raise ActivitySerializerError(e)
|
||||
@ -57,14 +63,15 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||
|
||||
@dataclass(init=False)
|
||||
class ActivityObject:
|
||||
''' actor activitypub json '''
|
||||
""" actor activitypub json """
|
||||
|
||||
id: str
|
||||
type: str
|
||||
|
||||
def __init__(self, activity_objects=None, **kwargs):
|
||||
''' this lets you pass in an object with fields that aren't in the
|
||||
"""this lets you pass in an object with fields that aren't in the
|
||||
dataclass, which it ignores. Any field in the dataclass is required or
|
||||
has a default value '''
|
||||
has a default value"""
|
||||
for field in fields(self):
|
||||
try:
|
||||
value = kwargs[field.name]
|
||||
@ -75,7 +82,7 @@ class ActivityObject:
|
||||
except TypeError:
|
||||
is_subclass = False
|
||||
# serialize a model obj
|
||||
if hasattr(value, 'to_activity'):
|
||||
if hasattr(value, "to_activity"):
|
||||
value = value.to_activity()
|
||||
# parse a dict into the appropriate activity
|
||||
elif is_subclass and isinstance(value, dict):
|
||||
@ -83,26 +90,28 @@ class ActivityObject:
|
||||
value = naive_parse(activity_objects, value)
|
||||
else:
|
||||
value = naive_parse(
|
||||
activity_objects, value, serializer=field.type)
|
||||
activity_objects, value, serializer=field.type
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
if field.default == MISSING and \
|
||||
field.default_factory == MISSING:
|
||||
raise ActivitySerializerError(\
|
||||
'Missing required field: %s' % field.name)
|
||||
if field.default == MISSING and field.default_factory == MISSING:
|
||||
raise ActivitySerializerError(
|
||||
"Missing required field: %s" % field.name
|
||||
)
|
||||
value = field.default
|
||||
setattr(self, field.name, value)
|
||||
|
||||
|
||||
def to_model(self, model=None, instance=None, allow_create=True, save=True):
|
||||
''' convert from an activity to a model instance '''
|
||||
""" convert from an activity to a model instance """
|
||||
model = model or get_model_from_type(self.type)
|
||||
|
||||
# only reject statuses if we're potentially creating them
|
||||
if allow_create and \
|
||||
hasattr(model, 'ignore_activity') and \
|
||||
model.ignore_activity(self):
|
||||
return None
|
||||
if (
|
||||
allow_create
|
||||
and hasattr(model, "ignore_activity")
|
||||
and model.ignore_activity(self)
|
||||
):
|
||||
raise ActivitySerializerError()
|
||||
|
||||
# check for an existing instance
|
||||
instance = instance or model.find_existing(self.serialize())
|
||||
@ -142,8 +151,10 @@ class ActivityObject:
|
||||
field.set_field_from_activity(instance, self)
|
||||
|
||||
# reversed relationships in the models
|
||||
for (model_field_name, activity_field_name) in \
|
||||
instance.deserialize_reverse_fields:
|
||||
for (
|
||||
model_field_name,
|
||||
activity_field_name,
|
||||
) in instance.deserialize_reverse_fields:
|
||||
# attachments on Status, for example
|
||||
values = getattr(self, activity_field_name)
|
||||
if values is None or values is MISSING:
|
||||
@ -161,13 +172,12 @@ class ActivityObject:
|
||||
instance.__class__.__name__,
|
||||
related_field_name,
|
||||
instance.remote_id,
|
||||
item
|
||||
item,
|
||||
)
|
||||
return instance
|
||||
|
||||
|
||||
def serialize(self):
|
||||
''' convert to dictionary with context attr '''
|
||||
""" convert to dictionary with context attr """
|
||||
data = self.__dict__.copy()
|
||||
# recursively serialize
|
||||
for (k, v) in data.items():
|
||||
@ -176,22 +186,19 @@ class ActivityObject:
|
||||
data[k] = v.serialize()
|
||||
except TypeError:
|
||||
pass
|
||||
data = {k:v for (k, v) in data.items() if v is not None}
|
||||
data['@context'] = 'https://www.w3.org/ns/activitystreams'
|
||||
data = {k: v for (k, v) in data.items() if v is not None}
|
||||
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
return data
|
||||
|
||||
|
||||
@app.task
|
||||
@transaction.atomic
|
||||
def set_related_field(
|
||||
model_name, origin_model_name, related_field_name,
|
||||
related_remote_id, data):
|
||||
''' load reverse related fields (editions, attachments) without blocking '''
|
||||
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
||||
origin_model = apps.get_model(
|
||||
'bookwyrm.%s' % origin_model_name,
|
||||
require_ready=True
|
||||
)
|
||||
model_name, origin_model_name, related_field_name, related_remote_id, data
|
||||
):
|
||||
""" load reverse related fields (editions, attachments) without blocking """
|
||||
model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True)
|
||||
origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True)
|
||||
|
||||
with transaction.atomic():
|
||||
if isinstance(data, str):
|
||||
@ -205,43 +212,45 @@ def set_related_field(
|
||||
# this must exist because it's the object that triggered this function
|
||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||
if not instance:
|
||||
raise ValueError(
|
||||
'Invalid related remote id: %s' % related_remote_id)
|
||||
raise ValueError("Invalid related remote id: %s" % related_remote_id)
|
||||
|
||||
# set the origin's remote id on the activity so it will be there when
|
||||
# the model instance is created
|
||||
# edition.parentWork = instance, for example
|
||||
model_field = getattr(model, related_field_name)
|
||||
if hasattr(model_field, 'activitypub_field'):
|
||||
if hasattr(model_field, "activitypub_field"):
|
||||
setattr(
|
||||
activity,
|
||||
getattr(model_field, 'activitypub_field'),
|
||||
instance.remote_id
|
||||
activity, getattr(model_field, "activitypub_field"), instance.remote_id
|
||||
)
|
||||
item = activity.to_model()
|
||||
|
||||
# if the related field isn't serialized (attachments on Status), then
|
||||
# we have to set it post-creation
|
||||
if not hasattr(model_field, 'activitypub_field'):
|
||||
if not hasattr(model_field, "activitypub_field"):
|
||||
setattr(item, related_field_name, instance)
|
||||
item.save()
|
||||
|
||||
|
||||
def get_model_from_type(activity_type):
|
||||
''' given the activity, what type of model '''
|
||||
""" given the activity, what type of model """
|
||||
models = apps.get_models()
|
||||
model = [m for m in models if hasattr(m, 'activity_serializer') and \
|
||||
hasattr(m.activity_serializer, 'type') and \
|
||||
m.activity_serializer.type == activity_type]
|
||||
model = [
|
||||
m
|
||||
for m in models
|
||||
if hasattr(m, "activity_serializer")
|
||||
and hasattr(m.activity_serializer, "type")
|
||||
and m.activity_serializer.type == activity_type
|
||||
]
|
||||
if not model:
|
||||
raise ActivitySerializerError(
|
||||
'No model found for activity type "%s"' % activity_type)
|
||||
'No model found for activity type "%s"' % activity_type
|
||||
)
|
||||
return model[0]
|
||||
|
||||
|
||||
def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||
''' take a remote_id and return an instance, creating if necessary '''
|
||||
if model:# a bonus check we can do if we already know the model
|
||||
""" take a remote_id and return an instance, creating if necessary """
|
||||
if model: # a bonus check we can do if we already know the model
|
||||
result = model.find_existing_by_remote_id(remote_id)
|
||||
if result and not refresh:
|
||||
return result
|
||||
@ -251,11 +260,12 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||
data = get_data(remote_id)
|
||||
except (ConnectorException, ConnectionError):
|
||||
raise ActivitySerializerError(
|
||||
'Could not connect to host for remote_id in %s model: %s' % \
|
||||
(model.__name__, remote_id))
|
||||
"Could not connect to host for remote_id in %s model: %s"
|
||||
% (model.__name__, remote_id)
|
||||
)
|
||||
# determine the model implicitly, if not provided
|
||||
if not model:
|
||||
model = get_model_from_type(data.get('type'))
|
||||
model = get_model_from_type(data.get("type"))
|
||||
|
||||
# check for existing items with shared unique identifiers
|
||||
result = model.find_existing(data)
|
||||
|
@ -1,70 +1,75 @@
|
||||
''' book and author data '''
|
||||
""" book and author data """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject
|
||||
from .image import Image
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(ActivityObject):
|
||||
''' serializes an edition or work, abstract '''
|
||||
""" serializes an edition or work, abstract """
|
||||
|
||||
title: str
|
||||
sortTitle: str = ''
|
||||
subtitle: str = ''
|
||||
description: str = ''
|
||||
sortTitle: str = ""
|
||||
subtitle: str = ""
|
||||
description: str = ""
|
||||
languages: List[str] = field(default_factory=lambda: [])
|
||||
series: str = ''
|
||||
seriesNumber: str = ''
|
||||
series: str = ""
|
||||
seriesNumber: str = ""
|
||||
subjects: List[str] = field(default_factory=lambda: [])
|
||||
subjectPlaces: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
authors: List[str] = field(default_factory=lambda: [])
|
||||
firstPublishedDate: str = ''
|
||||
publishedDate: str = ''
|
||||
firstPublishedDate: str = ""
|
||||
publishedDate: str = ""
|
||||
|
||||
openlibraryKey: str = ''
|
||||
librarythingKey: str = ''
|
||||
goodreadsKey: str = ''
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
|
||||
cover: Image = field(default_factory=lambda: {})
|
||||
type: str = 'Book'
|
||||
cover: Image = None
|
||||
type: str = "Book"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Edition(Book):
|
||||
''' Edition instance of a book object '''
|
||||
""" Edition instance of a book object """
|
||||
|
||||
work: str
|
||||
isbn10: str = ''
|
||||
isbn13: str = ''
|
||||
oclcNumber: str = ''
|
||||
asin: str = ''
|
||||
isbn10: str = ""
|
||||
isbn13: str = ""
|
||||
oclcNumber: str = ""
|
||||
asin: str = ""
|
||||
pages: int = None
|
||||
physicalFormat: str = ''
|
||||
physicalFormat: str = ""
|
||||
publishers: List[str] = field(default_factory=lambda: [])
|
||||
editionRank: int = 0
|
||||
|
||||
type: str = 'Edition'
|
||||
type: str = "Edition"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
''' work instance of a book object '''
|
||||
lccn: str = ''
|
||||
defaultEdition: str = ''
|
||||
""" work instance of a book object """
|
||||
|
||||
lccn: str = ""
|
||||
defaultEdition: str = ""
|
||||
editions: List[str] = field(default_factory=lambda: [])
|
||||
type: str = 'Work'
|
||||
type: str = "Work"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(ActivityObject):
|
||||
''' author of a book '''
|
||||
""" author of a book """
|
||||
|
||||
name: str
|
||||
born: str = None
|
||||
died: str = None
|
||||
aliases: List[str] = field(default_factory=lambda: [])
|
||||
bio: str = ''
|
||||
openlibraryKey: str = ''
|
||||
librarythingKey: str = ''
|
||||
goodreadsKey: str = ''
|
||||
wikipediaLink: str = ''
|
||||
type: str = 'Author'
|
||||
bio: str = ""
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
wikipediaLink: str = ""
|
||||
type: str = "Author"
|
||||
|
@ -1,11 +1,13 @@
|
||||
''' an image, nothing fancy '''
|
||||
""" an image, nothing fancy """
|
||||
from dataclasses import dataclass
|
||||
from .base_activity import ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(ActivityObject):
|
||||
''' image block '''
|
||||
""" image block """
|
||||
|
||||
url: str
|
||||
name: str = ''
|
||||
type: str = 'Image'
|
||||
id: str = ''
|
||||
name: str = ""
|
||||
type: str = "Image"
|
||||
id: str = ""
|
||||
|
@ -1,4 +1,4 @@
|
||||
''' note serializer and children thereof '''
|
||||
""" note serializer and children thereof """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
from django.apps import apps
|
||||
@ -6,10 +6,12 @@ from django.apps import apps
|
||||
from .base_activity import ActivityObject, Link
|
||||
from .image import Image
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Tombstone(ActivityObject):
|
||||
''' the placeholder for a deleted status '''
|
||||
type: str = 'Tombstone'
|
||||
""" the placeholder for a deleted status """
|
||||
|
||||
type: str = "Tombstone"
|
||||
|
||||
def to_model(self, *args, **kwargs):# pylint: disable=unused-argument
|
||||
''' this should never really get serialized, just searched for '''
|
||||
@ -19,59 +21,66 @@ class Tombstone(ActivityObject):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Note(ActivityObject):
|
||||
''' Note activity '''
|
||||
""" Note activity """
|
||||
|
||||
published: str
|
||||
attributedTo: str
|
||||
content: str = ''
|
||||
content: str = ""
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
replies: Dict = field(default_factory=lambda: {})
|
||||
inReplyTo: str = ''
|
||||
summary: str = ''
|
||||
inReplyTo: str = ""
|
||||
summary: str = ""
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Image] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
type: str = 'Note'
|
||||
type: str = "Note"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Article(Note):
|
||||
''' what's an article except a note with more fields '''
|
||||
""" what's an article except a note with more fields """
|
||||
|
||||
name: str
|
||||
type: str = 'Article'
|
||||
type: str = "Article"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class GeneratedNote(Note):
|
||||
''' just a re-typed note '''
|
||||
type: str = 'GeneratedNote'
|
||||
""" just a re-typed note """
|
||||
|
||||
type: str = "GeneratedNote"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Comment(Note):
|
||||
''' like a note but with a book '''
|
||||
""" like a note but with a book """
|
||||
|
||||
inReplyToBook: str
|
||||
type: str = 'Comment'
|
||||
type: str = "Comment"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Quotation(Comment):
|
||||
''' a quote and commentary on a book '''
|
||||
""" a quote and commentary on a book """
|
||||
|
||||
quote: str
|
||||
type: str = 'Quotation'
|
||||
type: str = "Quotation"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Review(Comment):
|
||||
''' a full book review '''
|
||||
""" a full book review """
|
||||
|
||||
name: str = None
|
||||
rating: int = None
|
||||
type: str = 'Review'
|
||||
type: str = "Review"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Rating(Comment):
|
||||
''' just a star rating '''
|
||||
""" just a star rating """
|
||||
|
||||
rating: int
|
||||
content: str = None
|
||||
type: str = 'Rating'
|
||||
type: str = "Rating"
|
||||
|
@ -1,4 +1,4 @@
|
||||
''' defines activitypub collections (lists) '''
|
||||
""" defines activitypub collections (lists) """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
@ -7,38 +7,46 @@ from .base_activity import ActivityObject
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollection(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
totalItems: int
|
||||
first: str
|
||||
last: str = None
|
||||
name: str = None
|
||||
owner: str = None
|
||||
type: str = 'OrderedCollection'
|
||||
type: str = "OrderedCollection"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPrivate(OrderedCollection):
|
||||
''' an ordered collection with privacy settings '''
|
||||
""" an ordered collection with privacy settings """
|
||||
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Shelf(OrderedCollectionPrivate):
|
||||
''' structure of an ordered collection activity '''
|
||||
type: str = 'Shelf'
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
type: str = "Shelf"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class BookList(OrderedCollectionPrivate):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
summary: str = None
|
||||
curation: str = 'closed'
|
||||
type: str = 'BookList'
|
||||
curation: str = "closed"
|
||||
type: str = "BookList"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPage(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
partOf: str
|
||||
orderedItems: List
|
||||
next: str = None
|
||||
prev: str = None
|
||||
type: str = 'OrderedCollectionPage'
|
||||
type: str = "OrderedCollectionPage"
|
||||
|
@ -1,4 +1,4 @@
|
||||
''' actor serializer '''
|
||||
""" actor serializer """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
@ -8,15 +8,17 @@ from .image import Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class PublicKey(ActivityObject):
|
||||
''' public key block '''
|
||||
""" public key block """
|
||||
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
type: str = 'PublicKey'
|
||||
type: str = "PublicKey"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Person(ActivityObject):
|
||||
''' actor activitypub json '''
|
||||
""" actor activitypub json """
|
||||
|
||||
preferredUsername: str
|
||||
inbox: str
|
||||
outbox: str
|
||||
@ -29,4 +31,4 @@ class Person(ActivityObject):
|
||||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = True
|
||||
type: str = 'Person'
|
||||
type: str = "Person"
|
||||
|
@ -2,6 +2,7 @@ from django.http import JsonResponse
|
||||
|
||||
from .base_activity import ActivityEncoder
|
||||
|
||||
|
||||
class ActivitypubResponse(JsonResponse):
|
||||
"""
|
||||
A class to be used in any place that's serializing responses for
|
||||
@ -9,10 +10,17 @@ class ActivitypubResponse(JsonResponse):
|
||||
configures some stuff beforehand. Made to be a drop-in replacement of
|
||||
JsonResponse.
|
||||
"""
|
||||
def __init__(self, data, encoder=ActivityEncoder, safe=False,
|
||||
json_dumps_params=None, **kwargs):
|
||||
|
||||
if 'content_type' not in kwargs:
|
||||
kwargs['content_type'] = 'application/activity+json'
|
||||
def __init__(
|
||||
self,
|
||||
data,
|
||||
encoder=ActivityEncoder,
|
||||
safe=False,
|
||||
json_dumps_params=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
if "content_type" not in kwargs:
|
||||
kwargs["content_type"] = "application/activity+json"
|
||||
|
||||
super().__init__(data, encoder, safe, json_dumps_params, **kwargs)
|
||||
|
@ -1,4 +1,4 @@
|
||||
''' undo wrapper activity '''
|
||||
""" undo wrapper activity """
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from django.apps import apps
|
||||
@ -9,160 +9,173 @@ from .book import Edition
|
||||
|
||||
@dataclass(init=False)
|
||||
class Verb(ActivityObject):
|
||||
''' generic fields for activities - maybe an unecessary level of
|
||||
abstraction but w/e '''
|
||||
"""generic fields for activities - maybe an unecessary level of
|
||||
abstraction but w/e"""
|
||||
|
||||
actor: str
|
||||
object: ActivityObject
|
||||
|
||||
def action(self):
|
||||
''' usually we just want to save, this can be overridden as needed '''
|
||||
""" usually we just want to save, this can be overridden as needed """
|
||||
self.object.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Create(Verb):
|
||||
''' Create activity '''
|
||||
""" Create activity """
|
||||
|
||||
to: List
|
||||
cc: List
|
||||
signature: Signature = None
|
||||
type: str = 'Create'
|
||||
type: str = "Create"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Delete(Verb):
|
||||
''' Create activity '''
|
||||
""" Create activity """
|
||||
|
||||
to: List
|
||||
cc: List
|
||||
type: str = 'Delete'
|
||||
type: str = "Delete"
|
||||
|
||||
def action(self):
|
||||
''' find and delete the activity object '''
|
||||
""" find and delete the activity object """
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.delete()
|
||||
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
''' Update activity '''
|
||||
""" Update activity """
|
||||
|
||||
to: List
|
||||
type: str = 'Update'
|
||||
type: str = "Update"
|
||||
|
||||
def action(self):
|
||||
''' update a model instance from the dataclass '''
|
||||
""" update a model instance from the dataclass """
|
||||
self.object.to_model(allow_create=False)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Undo(Verb):
|
||||
''' Undo an activity '''
|
||||
type: str = 'Undo'
|
||||
""" Undo an activity """
|
||||
|
||||
type: str = "Undo"
|
||||
|
||||
def action(self):
|
||||
''' find and remove the activity object '''
|
||||
""" find and remove the activity object """
|
||||
# this is so hacky but it does make it work....
|
||||
# (because you Reject a request and Undo a follow
|
||||
model = None
|
||||
if self.object.type == 'Follow':
|
||||
model = apps.get_model('bookwyrm.UserFollows')
|
||||
if self.object.type == "Follow":
|
||||
model = apps.get_model("bookwyrm.UserFollows")
|
||||
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
||||
obj.delete()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Follow(Verb):
|
||||
''' Follow activity '''
|
||||
""" Follow activity """
|
||||
|
||||
object: str
|
||||
type: str = 'Follow'
|
||||
type: str = "Follow"
|
||||
|
||||
def action(self):
|
||||
''' relationship save '''
|
||||
""" relationship save """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Block(Verb):
|
||||
''' Block activity '''
|
||||
""" Block activity """
|
||||
|
||||
object: str
|
||||
type: str = 'Block'
|
||||
type: str = "Block"
|
||||
|
||||
def action(self):
|
||||
''' relationship save '''
|
||||
""" relationship save """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Accept(Verb):
|
||||
''' Accept activity '''
|
||||
""" Accept activity """
|
||||
|
||||
object: Follow
|
||||
type: str = 'Accept'
|
||||
type: str = "Accept"
|
||||
|
||||
def action(self):
|
||||
''' find and remove the activity object '''
|
||||
""" find and remove the activity object """
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.accept()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Reject(Verb):
|
||||
''' Reject activity '''
|
||||
""" Reject activity """
|
||||
|
||||
object: Follow
|
||||
type: str = 'Reject'
|
||||
type: str = "Reject"
|
||||
|
||||
def action(self):
|
||||
''' find and remove the activity object '''
|
||||
""" find and remove the activity object """
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.reject()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Add(Verb):
|
||||
'''Add activity '''
|
||||
"""Add activity """
|
||||
|
||||
target: str
|
||||
object: Edition
|
||||
type: str = 'Add'
|
||||
type: str = "Add"
|
||||
notes: str = None
|
||||
order: int = 0
|
||||
approved: bool = True
|
||||
|
||||
def action(self):
|
||||
''' add obj to collection '''
|
||||
""" add obj to collection """
|
||||
target = resolve_remote_id(self.target, refresh=False)
|
||||
# we want to related field that isn't the book, this is janky af sorry
|
||||
model = [t for t in type(target)._meta.related_objects \
|
||||
if t.name != 'edition'][0].related_model
|
||||
model = [t for t in type(target)._meta.related_objects if t.name != "edition"][
|
||||
0
|
||||
].related_model
|
||||
self.to_model(model=model)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Remove(Verb):
|
||||
'''Remove activity '''
|
||||
"""Remove activity """
|
||||
|
||||
target: ActivityObject
|
||||
type: str = 'Remove'
|
||||
type: str = "Remove"
|
||||
|
||||
def action(self):
|
||||
''' find and remove the activity object '''
|
||||
""" 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 '''
|
||||
""" a user faving an object """
|
||||
|
||||
object: str
|
||||
type: str = 'Like'
|
||||
type: str = "Like"
|
||||
|
||||
def action(self):
|
||||
''' like '''
|
||||
""" like """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Announce(Verb):
|
||||
''' boosting a status '''
|
||||
""" boosting a status """
|
||||
|
||||
object: str
|
||||
type: str = 'Announce'
|
||||
type: str = "Announce"
|
||||
|
||||
def action(self):
|
||||
''' boost '''
|
||||
""" boost """
|
||||
self.to_model()
|
||||
|
Reference in New Issue
Block a user