Merge branch 'main' into inventaire
This commit is contained in:
@ -5,11 +5,12 @@ import sys
|
||||
from .base_activity import ActivityEncoder, Signature, naive_parse
|
||||
from .base_activity import Link, Mention
|
||||
from .base_activity import ActivitySerializerError, resolve_remote_id
|
||||
from .image import Image
|
||||
from .image import Document, Image
|
||||
from .note import Note, GeneratedNote, Article, Comment, Quotation
|
||||
from .note import Review, Rating
|
||||
from .note import Tombstone
|
||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||
from .ordered_collection import CollectionItem, ListItem, ShelfItem
|
||||
from .ordered_collection import BookList, Shelf
|
||||
from .person import Person, PublicKey
|
||||
from .response import ActivitypubResponse
|
||||
@ -26,5 +27,5 @@ 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)
|
||||
|
@ -10,11 +10,11 @@ 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__
|
||||
@ -22,7 +22,7 @@ class ActivityEncoder(JSONEncoder):
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
""" for tagging a book in a status """
|
||||
"""for tagging a book in a status"""
|
||||
|
||||
href: str
|
||||
name: str
|
||||
@ -31,14 +31,14 @@ class Link:
|
||||
|
||||
@dataclass
|
||||
class Mention(Link):
|
||||
""" a subtype of Link for mentioning an actor """
|
||||
"""a subtype of Link for mentioning an actor"""
|
||||
|
||||
type: str = "Mention"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signature:
|
||||
""" public key block """
|
||||
"""public key block"""
|
||||
|
||||
creator: str
|
||||
created: str
|
||||
@ -47,15 +47,19 @@ class Signature:
|
||||
|
||||
|
||||
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"):
|
||||
# ugh
|
||||
activity_json["type"] = "PublicKey"
|
||||
|
||||
activity_type = activity_json.get("type")
|
||||
try:
|
||||
activity_type = activity_json["type"]
|
||||
serializer = activity_objects[activity_type]
|
||||
except KeyError as e:
|
||||
# we know this exists and that we can't handle it
|
||||
if activity_type in ["Question"]:
|
||||
return None
|
||||
raise ActivitySerializerError(e)
|
||||
|
||||
return serializer(activity_objects=activity_objects, **activity_json)
|
||||
@ -63,7 +67,7 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||
|
||||
@dataclass(init=False)
|
||||
class ActivityObject:
|
||||
""" actor activitypub json """
|
||||
"""actor activitypub json"""
|
||||
|
||||
id: str
|
||||
type: str
|
||||
@ -102,7 +106,7 @@ class ActivityObject:
|
||||
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
|
||||
@ -111,7 +115,7 @@ class ActivityObject:
|
||||
and hasattr(model, "ignore_activity")
|
||||
and model.ignore_activity(self)
|
||||
):
|
||||
raise ActivitySerializerError()
|
||||
return None
|
||||
|
||||
# check for an existing instance
|
||||
instance = instance or model.find_existing(self.serialize())
|
||||
@ -177,7 +181,7 @@ class ActivityObject:
|
||||
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():
|
||||
@ -196,7 +200,7 @@ class ActivityObject:
|
||||
def set_related_field(
|
||||
model_name, origin_model_name, related_field_name, related_remote_id, data
|
||||
):
|
||||
""" load reverse related fields (editions, attachments) without blocking """
|
||||
"""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)
|
||||
|
||||
@ -232,7 +236,7 @@ def set_related_field(
|
||||
|
||||
|
||||
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
|
||||
@ -251,7 +255,7 @@ def get_model_from_type(activity_type):
|
||||
def resolve_remote_id(
|
||||
remote_id, model=None, refresh=False, save=True, get_activity=False
|
||||
):
|
||||
""" take a remote_id and return an instance, creating if necessary """
|
||||
"""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:
|
||||
|
@ -3,7 +3,7 @@ from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject
|
||||
from .image import Image
|
||||
from .image import Document
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
@ -15,11 +15,12 @@ class BookData(ActivityObject):
|
||||
librarythingKey: str = None
|
||||
goodreadsKey: str = None
|
||||
bnfId: str = None
|
||||
lastEditedBy: str = None
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(BookData):
|
||||
""" serializes an edition or work, abstract """
|
||||
"""serializes an edition or work, abstract"""
|
||||
|
||||
title: str
|
||||
sortTitle: str = ""
|
||||
@ -35,13 +36,13 @@ class Book(BookData):
|
||||
firstPublishedDate: str = ""
|
||||
publishedDate: str = ""
|
||||
|
||||
cover: Image = None
|
||||
cover: Document = 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 = ""
|
||||
@ -58,7 +59,7 @@ class Edition(Book):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
""" work instance of a book object """
|
||||
"""work instance of a book object"""
|
||||
|
||||
lccn: str = ""
|
||||
defaultEdition: str = ""
|
||||
@ -68,7 +69,7 @@ class Work(Book):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(BookData):
|
||||
""" author of a book """
|
||||
"""author of a book"""
|
||||
|
||||
name: str
|
||||
isni: str = None
|
||||
|
@ -4,10 +4,17 @@ from .base_activity import ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(ActivityObject):
|
||||
""" image block """
|
||||
class Document(ActivityObject):
|
||||
"""a document"""
|
||||
|
||||
url: str
|
||||
name: str = ""
|
||||
type: str = "Document"
|
||||
id: str = None
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(Document):
|
||||
"""an image"""
|
||||
|
||||
type: str = "Image"
|
||||
|
@ -4,24 +4,24 @@ from typing import Dict, List
|
||||
from django.apps import apps
|
||||
|
||||
from .base_activity import ActivityObject, Link
|
||||
from .image import Image
|
||||
from .image import Document
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Tombstone(ActivityObject):
|
||||
""" the placeholder for a deleted status """
|
||||
"""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 """
|
||||
"""this should never really get serialized, just searched for"""
|
||||
model = apps.get_model("bookwyrm.Status")
|
||||
return model.find_existing_by_remote_id(self.id)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Note(ActivityObject):
|
||||
""" Note activity """
|
||||
"""Note activity"""
|
||||
|
||||
published: str
|
||||
attributedTo: str
|
||||
@ -32,14 +32,14 @@ class Note(ActivityObject):
|
||||
inReplyTo: str = ""
|
||||
summary: str = ""
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Image] = field(default_factory=lambda: [])
|
||||
attachment: List[Document] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
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"
|
||||
@ -47,14 +47,14 @@ class Article(Note):
|
||||
|
||||
@dataclass(init=False)
|
||||
class GeneratedNote(Note):
|
||||
""" just a re-typed note """
|
||||
"""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"
|
||||
@ -62,7 +62,7 @@ class Comment(Note):
|
||||
|
||||
@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"
|
||||
@ -70,7 +70,7 @@ class Quotation(Comment):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Review(Comment):
|
||||
""" a full book review """
|
||||
"""a full book review"""
|
||||
|
||||
name: str = None
|
||||
rating: int = None
|
||||
@ -79,7 +79,7 @@ class Review(Comment):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Rating(Comment):
|
||||
""" just a star rating """
|
||||
"""just a star rating"""
|
||||
|
||||
rating: int
|
||||
content: str = None
|
||||
|
@ -7,7 +7,7 @@ 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
|
||||
@ -19,7 +19,7 @@ class OrderedCollection(ActivityObject):
|
||||
|
||||
@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: [])
|
||||
@ -27,14 +27,14 @@ class OrderedCollectionPrivate(OrderedCollection):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Shelf(OrderedCollectionPrivate):
|
||||
""" structure of an ordered collection activity """
|
||||
"""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"
|
||||
@ -43,10 +43,37 @@ class BookList(OrderedCollectionPrivate):
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class CollectionItem(ActivityObject):
|
||||
"""an item in a collection"""
|
||||
|
||||
actor: str
|
||||
type: str = "CollectionItem"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ListItem(CollectionItem):
|
||||
"""a book on a list"""
|
||||
|
||||
book: str
|
||||
notes: str = None
|
||||
approved: bool = True
|
||||
order: int = None
|
||||
type: str = "ListItem"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ShelfItem(CollectionItem):
|
||||
"""a book on a list"""
|
||||
|
||||
book: str
|
||||
type: str = "ShelfItem"
|
||||
|
@ -8,7 +8,7 @@ from .image import Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class PublicKey(ActivityObject):
|
||||
""" public key block """
|
||||
"""public key block"""
|
||||
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
@ -17,12 +17,13 @@ class PublicKey(ActivityObject):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Person(ActivityObject):
|
||||
""" actor activitypub json """
|
||||
"""actor activitypub json"""
|
||||
|
||||
preferredUsername: str
|
||||
inbox: str
|
||||
publicKey: PublicKey
|
||||
followers: str = None
|
||||
following: str = None
|
||||
outbox: str = None
|
||||
endpoints: Dict = None
|
||||
name: str = None
|
||||
|
@ -1,69 +1,83 @@
|
||||
""" undo wrapper activity """
|
||||
""" activities that do things """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from django.apps import apps
|
||||
|
||||
from .base_activity import ActivityObject, Signature, resolve_remote_id
|
||||
from .book import Edition
|
||||
from .ordered_collection import CollectionItem
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Verb(ActivityObject):
|
||||
"""generic fields for activities - maybe an unecessary level of
|
||||
abstraction but w/e"""
|
||||
"""generic fields for activities"""
|
||||
|
||||
actor: str
|
||||
object: ActivityObject
|
||||
|
||||
def action(self):
|
||||
""" usually we just want to save, this can be overridden as needed """
|
||||
self.object.to_model()
|
||||
"""usually we just want to update and save"""
|
||||
# self.object may return None if the object is invalid in an expected way
|
||||
# ie, Question type
|
||||
if self.object:
|
||||
self.object.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Create(Verb):
|
||||
""" Create activity """
|
||||
"""Create activity"""
|
||||
|
||||
to: List
|
||||
cc: List
|
||||
to: List[str]
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
signature: Signature = None
|
||||
type: str = "Create"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Delete(Verb):
|
||||
""" Create activity """
|
||||
"""Create activity"""
|
||||
|
||||
to: List
|
||||
cc: List
|
||||
to: List[str]
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
type: str = "Delete"
|
||||
|
||||
def action(self):
|
||||
""" find and delete the activity object """
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.delete()
|
||||
"""find and delete the activity object"""
|
||||
if not self.object:
|
||||
return
|
||||
|
||||
if isinstance(self.object, str):
|
||||
# Deleted users are passed as strings. Not wild about this fix
|
||||
model = apps.get_model("bookwyrm.User")
|
||||
obj = model.find_existing_by_remote_id(self.object)
|
||||
else:
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
|
||||
if obj:
|
||||
obj.delete()
|
||||
# if we can't find it, we don't need to delete it because we don't have it
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
""" Update activity """
|
||||
"""Update activity"""
|
||||
|
||||
to: List
|
||||
to: List[str]
|
||||
type: str = "Update"
|
||||
|
||||
def action(self):
|
||||
""" update a model instance from the dataclass """
|
||||
self.object.to_model(allow_create=False)
|
||||
"""update a model instance from the dataclass"""
|
||||
if self.object:
|
||||
self.object.to_model(allow_create=False)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Undo(Verb):
|
||||
""" Undo an activity """
|
||||
"""Undo an activity"""
|
||||
|
||||
type: str = "Undo"
|
||||
|
||||
def action(self):
|
||||
""" find and remove the activity object """
|
||||
"""find and remove the activity object"""
|
||||
if isinstance(self.object, str):
|
||||
# it may be that sometihng should be done with these, but idk what
|
||||
# this seems just to be coming from pleroma
|
||||
@ -89,107 +103,98 @@ class Undo(Verb):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Follow(Verb):
|
||||
""" Follow activity """
|
||||
"""Follow activity"""
|
||||
|
||||
object: str
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
target: ActivityObject
|
||||
object: CollectionItem
|
||||
type: str = "Add"
|
||||
notes: str = None
|
||||
order: int = 0
|
||||
approved: bool = True
|
||||
|
||||
def action(self):
|
||||
""" add obj to collection """
|
||||
target = resolve_remote_id(self.target, refresh=False)
|
||||
# we want to get the 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
|
||||
self.to_model(model=model)
|
||||
"""figure out the target to assign the item to a collection"""
|
||||
target = resolve_remote_id(self.target)
|
||||
item = self.object.to_model(save=False)
|
||||
setattr(item, item.collection_field, target)
|
||||
item.save()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Remove(Verb):
|
||||
"""Remove activity """
|
||||
class Remove(Add):
|
||||
"""Remove activity"""
|
||||
|
||||
target: ActivityObject
|
||||
type: str = "Remove"
|
||||
|
||||
def action(self):
|
||||
""" find and remove the activity object """
|
||||
target = resolve_remote_id(self.target, refresh=False)
|
||||
model = [t for t in type(target)._meta.related_objects if t.name != "edition"][
|
||||
0
|
||||
].related_model
|
||||
obj = self.to_model(model=model, save=False, allow_create=False)
|
||||
obj.delete()
|
||||
"""find and remove the activity object"""
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
if obj:
|
||||
obj.delete()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Like(Verb):
|
||||
""" a user faving an object """
|
||||
"""a user faving an object"""
|
||||
|
||||
object: str
|
||||
type: str = "Like"
|
||||
|
||||
def action(self):
|
||||
""" like """
|
||||
"""like"""
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Announce(Verb):
|
||||
""" boosting a status """
|
||||
"""boosting a status"""
|
||||
|
||||
published: str
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
@ -198,5 +203,5 @@ class Announce(Verb):
|
||||
type: str = "Announce"
|
||||
|
||||
def action(self):
|
||||
""" boost """
|
||||
"""boost"""
|
||||
self.to_model()
|
||||
|
Reference in New Issue
Block a user