Runs black

This commit is contained in:
Mouse Reeve
2021-03-08 08:49:10 -08:00
parent a07f955781
commit 70296e760b
198 changed files with 10239 additions and 8572 deletions

View File

@ -1,4 +1,4 @@
''' bring activitypub functions into the namespace '''
""" bring activitypub functions into the namespace """
import inspect
import sys
@ -21,9 +21,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)

View File

@ -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,25 +90,27 @@ 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):
if (
allow_create
and hasattr(model, "ignore_activity")
and model.ignore_activity(self)
):
raise ActivitySerializerError()
# check for an existing instance
@ -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)

View File

@ -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 = None
type: str = 'Book'
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"

View File

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

View File

@ -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,64 +6,72 @@ 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):
''' this should never really get serialized, just searched for '''
model = apps.get_model('bookwyrm.Status')
""" 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
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 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 Quotation(Comment):
''' a quote and commentary on a book '''
""" a quote and commentary on a book """
quote: str
type: str = 'Quotation'
type: str = "Quotation"

View File

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

View File

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

View File

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

View File

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