ActivityPub

BookWyrm uses the ActivityPub protocol to send and receive user activity between other BookWyrm instances and other services that implement ActivityPub, like Mastodon. To handle book data, BookWyrm has a handful of extended Activity types which are not part of the standard, but are legible to other BookWyrm instances.

To view the ActivityPub data for a BookWyrm entity (user, book, list, etc) you can usually add .json to the end of the URL. e.g. https://www.example.com/user/sam.json and see the JSON in your web browser or via an http request (e.g. using curl).

Activities and Objects

Users and relationships

User relationship interactions follow the standard ActivityPub spec.

  • Follow: request to receive statuses from a user, and view their statuses that have followers-only privacy
  • Accept: approves a Follow and finalizes the relationship
  • Reject: denies a Follow
  • Block: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile
  • Update: updates a user's profile and settings
  • Delete: deactivates a user
  • Undo: reverses a Follow or Block
  • Move: communicate that a user has changed their ID and has moved to a new server. Most ActivityPub software will "follow" the user to the new identity. BookWyrm sends a notification to followers and requires them to confirm they want to follow the user to their new identity.

Statuses

Object types

  • Note: On services like Mastodon, Notes are the primary type of status. They contain a message body, attachments, can mention users, and be replies to statuses of any type. Within BookWyrm, Notes can only be created as direct messages or as replies to other statuses.
  • Review: A review is a status in response to a book (indicated by the inReplyToBook field), which has a title, body, and numerical rating between 0 (not rated) and 5.
  • Comment: A comment on a book mentions a book and has a message body.
  • Quotation: A quote has a message body, an excerpt from a book, and mentions a book.

Activities

  • Create: saves a new status in the database.

    Note: BookWyrm only accepts Create activities if they are:

    • Direct messages (i.e., Notes with the privacy level direct, which mention a local user),
    • Related to a book (of a custom status type that includes the field inReplyToBook),
    • Replies to existing statuses saved in the database
  • Delete: Removes a status

  • Like: Creates a favorite on the status
  • Announce: Boosts the status into the actor's timeline
  • Undo: Reverses a Like or Announce

Collections

User's books and lists are represented by OrderedCollection

Objects

  • Shelf: A user's book collection. By default, every user has a to-read, reading, and read shelf which are used to track reading progress.
  • List: A collection of books that may have items contributed by users other than the one who created the list.

Activities

  • Create: Adds a shelf or list to the database.
  • Delete: Removes a shelf or list.
  • Add: Adds a book to a shelf or list.
  • Remove: Removes a book from a shelf or list.

Alternative Serialization

Because BookWyrm uses custom object types (Review, Comment, Quotation) that aren't supported by ActivityPub, statuses are transformed into standard types when sent to or viewed by non-BookWyrm services. Reviews are converted into Articles, and Comments and Quotations are converted into Notes, with a link to the book and the cover image attached.

This may change in future in favor of the more ActivityPub-compliant extended Object types listed alongside core ActivityPub types.

Making ActivityPub-aware models

The way BookWyrm sends and receives ActivityPub objects can be confusing for developers who are new to BookWyrm. It is mostly controlled by:

Serializing data to and from ActivityPub JSON

BookWyrm needs to know how to serialize the data from the model into an ActivityPub JSON-LD object.

The /activitypub/base_activity.py file provides the core functions that turn ActivityPub JSON-LD strings into usable Django model objects, and vice-versa. We do this by creating a data class in bookwyrm/activitypub, and defining how the model should be serialized by providing an activity_serializer value in the model, which points to the relevant data class. From ActivityObject we inherit id and type, and two class methods:

to_model

This method takes an ActivityPub JSON string and tries to turn it into a BookWyrm model object, finding an existing object wherever possible. This is how we process incoming ActivityPub objects.

serialize

This method takes a BookWyrm model object, and turns it into a valid ActivityPub JSON string using the dataclass definitions. This is how we process outgoing ActivityPub objects.

Example - Users

A BookWyrm user is defined in models/user.py:

class User(OrderedCollectionPageMixin, AbstractUser):
    """a user who wants to read books"""

Notice that we are inheriting from ("subclassing") OrderedCollectionPageMixin. This in turn inherits from ObjectMixin, which inherits from ActivitypubMixin. This may seem convoluted, but this inheritence chain allows us to avoid duplicating code as our ActivityPub objects become more specific. AbstractUser is a Django model intended to be subclassed, giving us things like hashed password logins and permission levels "out of the box".

Because User inherits from ObjectMixin, when we save() a User object we will send a Create activity (if this is the first time the user was saved) or an Update activity (if we're just saving a change – e.g. to the user description or avatar). Any other model you add to BookWyrm will have the same capability if it inherits from ObjectMixin.

For BookWyrm users, the activity_serializer is defined in the User model:

activity_serializer = activitypub.Person

The data class definition for activitypub.Person is at /activitypub/person.py:

@dataclass(init=False)
class Person(ActivityObject):
    """actor activitypub json"""

    preferredUsername: str
    inbox: str
    publicKey: PublicKey
    followers: str = None
    following: str = None
    outbox: str = None
    endpoints: Dict = None
    name: str = None
    summary: str = None
    icon: Image = None
    bookwyrmUser: bool = False
    manuallyApprovesFollowers: str = False
    discoverable: str = False
    hideFollows: str = False
    movedTo: str = None
    alsoKnownAs: dict[str] = None
    type: str = "Person"

You might notice that some of these fields are not a perfect match to the fields in the User model. If you have a field name in your model that needs to be called something different in the ActivityPub object (e.g. to comply with Python naming conventions in the model but JSON naming conventions in JSON string), you can define an activitypub_field in the model field definition:

followers_url = fields.CharField(max_length=255, activitypub_field="followers")