Merge branch 'main' into inventaire

This commit is contained in:
Mouse Reeve
2021-04-26 14:22:05 -07:00
280 changed files with 20693 additions and 9991 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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()