rename main code directory
This commit is contained in:
19
bookwyrm/activitypub/__init__.py
Normal file
19
bookwyrm/activitypub/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
''' bring activitypub functions into the namespace '''
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
||||
from .note import Note, Article, Comment, Review, Quotation
|
||||
from .interaction import Boost, Like
|
||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||
from .person import Person
|
||||
from .book import Edition, Work, Author
|
||||
from .verbs import Create, Undo, Update
|
||||
from .verbs import Follow, Accept, Reject
|
||||
from .verbs import Add, Remove
|
||||
|
||||
# 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')}
|
118
bookwyrm/activitypub/base_activity.py
Normal file
118
bookwyrm/activitypub/base_activity.py
Normal file
@ -0,0 +1,118 @@
|
||||
''' basics for an activitypub serializer '''
|
||||
from dataclasses import dataclass, fields, MISSING
|
||||
from json import JSONEncoder
|
||||
|
||||
from django.db.models.fields.related_descriptors \
|
||||
import ForwardManyToOneDescriptor
|
||||
|
||||
|
||||
class ActivityEncoder(JSONEncoder):
|
||||
''' used to convert an Activity object into json '''
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
''' image block '''
|
||||
mediaType: str
|
||||
url: str
|
||||
type: str = 'Image'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PublicKey:
|
||||
''' public key block '''
|
||||
id: str
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signature:
|
||||
''' public key block '''
|
||||
creator: str
|
||||
created: str
|
||||
signatureValue: str
|
||||
type: str = 'RsaSignature2017'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ActivityObject:
|
||||
''' actor activitypub json '''
|
||||
id: str
|
||||
type: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' 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 '''
|
||||
for field in fields(self):
|
||||
try:
|
||||
value = kwargs[field.name]
|
||||
except KeyError:
|
||||
if field.default == MISSING:
|
||||
raise TypeError('Missing required field: %s' % field.name)
|
||||
value = field.default
|
||||
setattr(self, field.name, value)
|
||||
|
||||
|
||||
def to_model(self, model, instance=None):
|
||||
''' convert from an activity to a model '''
|
||||
if not isinstance(self, model.activity_serializer):
|
||||
raise TypeError('Wrong activity type for model')
|
||||
|
||||
model_fields = [m.name for m in model._meta.get_fields()]
|
||||
mapped_fields = {}
|
||||
|
||||
for mapping in model.activity_mappings:
|
||||
if mapping.model_key not in model_fields:
|
||||
continue
|
||||
# value is None if there's a default that isn't supplied
|
||||
# in the activity but is supplied in the formatter
|
||||
value = None
|
||||
if mapping.activity_key:
|
||||
value = getattr(self, mapping.activity_key)
|
||||
model_field = getattr(model, mapping.model_key)
|
||||
|
||||
# remote_id -> foreign key resolver
|
||||
if isinstance(model_field, ForwardManyToOneDescriptor) and value:
|
||||
fk_model = model_field.field.related_model
|
||||
value = resolve_foreign_key(fk_model, value)
|
||||
|
||||
mapped_fields[mapping.model_key] = mapping.model_formatter(value)
|
||||
|
||||
|
||||
# updating an existing model isntance
|
||||
if instance:
|
||||
for k, v in mapped_fields.items():
|
||||
setattr(instance, k, v)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
# creating a new model instance
|
||||
return model.objects.create(**mapped_fields)
|
||||
|
||||
|
||||
def serialize(self):
|
||||
''' convert to dictionary with context attr '''
|
||||
data = self.__dict__
|
||||
data['@context'] = 'https://www.w3.org/ns/activitystreams'
|
||||
return data
|
||||
|
||||
|
||||
def resolve_foreign_key(model, remote_id):
|
||||
''' look up the remote_id on an activity json field '''
|
||||
result = model.objects
|
||||
if hasattr(model.objects, 'select_subclasses'):
|
||||
result = result.select_subclasses()
|
||||
|
||||
result = result.filter(
|
||||
remote_id=remote_id
|
||||
).first()
|
||||
|
||||
if not result:
|
||||
raise ValueError('Could not resolve remote_id in %s model: %s' % \
|
||||
(model.__name__, remote_id))
|
||||
return result
|
67
bookwyrm/activitypub/book.py
Normal file
67
bookwyrm/activitypub/book.py
Normal file
@ -0,0 +1,67 @@
|
||||
''' book and author data '''
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject, Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(ActivityObject):
|
||||
''' serializes an edition or work, abstract '''
|
||||
authors: List[str]
|
||||
first_published_date: str
|
||||
published_date: str
|
||||
|
||||
title: str
|
||||
sort_title: str
|
||||
subtitle: str
|
||||
description: str
|
||||
languages: List[str]
|
||||
series: str
|
||||
series_number: str
|
||||
subjects: List[str]
|
||||
subject_places: List[str]
|
||||
|
||||
openlibrary_key: str
|
||||
librarything_key: str
|
||||
goodreads_key: str
|
||||
|
||||
attachment: List[Image] = field(default=lambda: [])
|
||||
type: str = 'Book'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Edition(Book):
|
||||
''' Edition instance of a book object '''
|
||||
isbn_10: str
|
||||
isbn_13: str
|
||||
oclc_number: str
|
||||
asin: str
|
||||
pages: str
|
||||
physical_format: str
|
||||
publishers: List[str]
|
||||
|
||||
work: str
|
||||
type: str = 'Edition'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
''' work instance of a book object '''
|
||||
lccn: str
|
||||
editions: List[str]
|
||||
type: str = 'Work'
|
||||
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(ActivityObject):
|
||||
''' author of a book '''
|
||||
url: str
|
||||
name: str
|
||||
born: str
|
||||
died: str
|
||||
aliases: str
|
||||
bio: str
|
||||
openlibrary_key: str
|
||||
wikipedia_link: str
|
||||
type: str = 'Person'
|
20
bookwyrm/activitypub/interaction.py
Normal file
20
bookwyrm/activitypub/interaction.py
Normal file
@ -0,0 +1,20 @@
|
||||
''' boosting and liking posts '''
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base_activity import ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Like(ActivityObject):
|
||||
''' a user faving an object '''
|
||||
actor: str
|
||||
object: str
|
||||
type: str = 'Like'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Boost(ActivityObject):
|
||||
''' boosting a status '''
|
||||
actor: str
|
||||
object: str
|
||||
type: str = 'Announce'
|
50
bookwyrm/activitypub/note.py
Normal file
50
bookwyrm/activitypub/note.py
Normal file
@ -0,0 +1,50 @@
|
||||
''' note serializer and children thereof '''
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
from .base_activity import ActivityObject, Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class Note(ActivityObject):
|
||||
''' Note activity '''
|
||||
url: str
|
||||
inReplyTo: str
|
||||
published: str
|
||||
attributedTo: str
|
||||
to: List[str]
|
||||
cc: List[str]
|
||||
content: str
|
||||
replies: Dict
|
||||
# TODO: this is wrong???
|
||||
attachment: List[Image] = field(default=lambda: [])
|
||||
sensitive: bool = False
|
||||
type: str = 'Note'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Article(Note):
|
||||
''' what's an article except a note with more fields '''
|
||||
name: str
|
||||
type: str = 'Article'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Comment(Note):
|
||||
''' like a note but with a book '''
|
||||
inReplyToBook: str
|
||||
type: str = 'Comment'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Review(Comment):
|
||||
''' a full book review '''
|
||||
name: str
|
||||
rating: int
|
||||
type: str = 'Review'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Quotation(Comment):
|
||||
''' a quote and commentary on a book '''
|
||||
quote: str
|
||||
type: str = 'Quotation'
|
25
bookwyrm/activitypub/ordered_collection.py
Normal file
25
bookwyrm/activitypub/ordered_collection.py
Normal file
@ -0,0 +1,25 @@
|
||||
''' defines activitypub collections (lists) '''
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollection(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
totalItems: int
|
||||
first: str
|
||||
last: str = ''
|
||||
name: str = ''
|
||||
type: str = 'OrderedCollection'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPage(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
partOf: str
|
||||
orderedItems: List
|
||||
next: str
|
||||
prev: str
|
||||
type: str = 'OrderedCollectionPage'
|
22
bookwyrm/activitypub/person.py
Normal file
22
bookwyrm/activitypub/person.py
Normal file
@ -0,0 +1,22 @@
|
||||
''' actor serializer '''
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
from .base_activity import ActivityObject, Image, PublicKey
|
||||
|
||||
@dataclass(init=False)
|
||||
class Person(ActivityObject):
|
||||
''' actor activitypub json '''
|
||||
preferredUsername: str
|
||||
name: str
|
||||
inbox: str
|
||||
outbox: str
|
||||
followers: str
|
||||
summary: str
|
||||
publicKey: PublicKey
|
||||
endpoints: Dict
|
||||
icon: Image = field(default=lambda: {})
|
||||
fedireadsUser: str = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = True
|
||||
type: str = 'Person'
|
68
bookwyrm/activitypub/verbs.py
Normal file
68
bookwyrm/activitypub/verbs.py
Normal file
@ -0,0 +1,68 @@
|
||||
''' undo wrapper activity '''
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject, Signature
|
||||
|
||||
@dataclass(init=False)
|
||||
class Verb(ActivityObject):
|
||||
''' generic fields for activities - maybe an unecessary level of
|
||||
abstraction but w/e '''
|
||||
actor: str
|
||||
object: ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Create(Verb):
|
||||
''' Create activity '''
|
||||
to: List
|
||||
cc: List
|
||||
signature: Signature
|
||||
type: str = 'Create'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
''' Update activity '''
|
||||
to: List
|
||||
type: str = 'Update'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Undo(Verb):
|
||||
''' Undo an activity '''
|
||||
type: str = 'Undo'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Follow(Verb):
|
||||
''' Follow activity '''
|
||||
type: str = 'Follow'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Accept(Verb):
|
||||
''' Accept activity '''
|
||||
object: Follow
|
||||
type: str = 'Accept'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Reject(Verb):
|
||||
''' Reject activity '''
|
||||
object: Follow
|
||||
type: str = 'Reject'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Add(Verb):
|
||||
'''Add activity '''
|
||||
target: ActivityObject
|
||||
type: str = 'Add'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Remove(Verb):
|
||||
'''Remove activity '''
|
||||
target: ActivityObject
|
||||
type: str = 'Remove'
|
Reference in New Issue
Block a user