isaacschemm: A cartoon of myself as a snail (snail8)
[personal profile] isaacschemm posting in [community profile] snailsharp

Pandacap: Part 5 - ActivityPub

A          screenshot of part of Pandacap's favorites page, showing          gallery-style thumbnails

If I'm thinking about how often I use it, the ActivityPub integration in Pandacap really isn't the most relevant part of the application to me on a regular basis (that would be the inbox, and its integration with DeviantArt and Bluesky). It was, however, my inspiration for building Pandacap in the first place. There were a few reasons I felt this was important:

  • I believe decentralized social media will continue to be useful, regardless of whether it achieves mainstream success - maybe not to replace big social media platforms, but for its own distinct merits. To hopefully facilitate the adoption of such platforms, I wanted to ensure that the ability to view and reply to my work would be available there, and I wanted to know I could follow and reply to their work as well.
  • The ActivityPub server-to-server protocol - though it may be somewhat broad and vague - was within my ability to implement (it only uses HTTP requests and is agnostic to the underlying data model), which gives me a feeling of control over my interaction with the fediverse.
  • Joining an existing server would make me nervous about conforming to unwritten community norms, while using my own Mastodon or Pixelfed instance would increase the cost and saddle me with a tech stack I'm not familiar with. (Pandacap can run on a free web app plan and a database billed only by usage.)

Features

Some of Pandacap's features are exclusive to its ActivityPub integration.

The most visible example is the favorites page:

A          screenshot of Pandacap's favorites page, showing both image          (thumbnail grid) and text (full-width) posts from various          ActivityPub servers

The design of the favorites page is the same as the gallery or inbox, where images get a thumbnail and text posts are just full-width text (unlike in the inbox, text posts are not truncated). When a post is added to the favorites page, this is sent to the original post's server as a Like (and removing from the favorites page sends a corresponding Undo).

The logged-in user can add or remove a favorite from the "view remote post" page:

(This page is only available when logged in; public visitors will get sent to the post on its original server instead.)

Pandacap does not store other users' ActivityPub posts (unless you add them as favorites); instead, it will pull the post from the remote server when this page is loaded - something that's only really feasible for a single-user application. It shows the full title (if any), text, and any images, and allows the logged-in user to send a reply. It also shows the post's ActivityPub addressing.

Replies from others to your own posts are - if you're logged in - shown on the post's page. (In line with Pandacap's philosophy that only your own data - or data you put there intentionally - should be public, these replies are hidden in the public UI.) When replies are shown, they are also nested, to the extent that Pandacap's database knows about them. Other user's replies to your UserPosts or AddressedPosts are stored using the type RemoteActivityPubReply.

Outgoing replies are stored internally as an AddressedPost (as opposed to artwork, journals, and status updates, which all use UserPost). An AddressedPost does not show up in the public outbox or the UI, but is accessible from its URI - these are essentially unlisted public ActivityPub posts, and are used for replies and for posts to Lemmy communities.

Replies to your posts also show up in the notifications page.

Finally, there's the "communites" feature. Next to "following" and "favorites", the user's public profile shows a list of Lemmy communities:

Pandacap does not "follow" these Lemmy communities (it seems quite easy for things to break, and there's really no reason Pandacap needs to have a local cache of a Lemmy community's posts). If a public user clicks one of these links, they get taken to the Lemmy web UI. But if the Pandacap user is logged in and clicks through, they'll see the community inside Pandacap:

The "create new post" link under the community title brings up a dedicated page that asks for a title and post body, and creates an AddressedPost sent to the Lemmy community.

Pandacap can also fetch and show individual posts:

Both of these pages use the Lemmy API, instead of interfacing with Lemmy over ActivityPub. But the "reply" link uses ActivityPub to fetch that post, then sends you to the same "view remote post" page shown above.

Federation

Pandacap presents a single actor object (at the root URL, with an Accept header that prefers application/activity+json over HTML). The actor also exposes:

  • a single inbox URL (no shared inbox)
  • a paginated outbox (whose underlying implementation is shared with the "all" Atom/RSS feed)
  • a followers list stub (count only, no actor IDs are provided)
  • an unpaginated following list (including actor IDs)
  • a paginated "liked" collection (whose underlying implementation is shared with the Favorites page)

Pandacap artwork posts, status updates, community posts, and replies are federated as type Note, while journal entries are federated as type Article. However, artwork posts (and, optionally, community posts) include a name field for the post title, which will be ignored by Mastodon.

As of this writing, artwork updates, journal entries, and status updates are converted to ActivityStreams as follows (where pair denotes a key followed by a value, and id is the ActivityPub ID and public URL of the post):

pair "id" id
pair "url" id

pair "type" (if post.Type = PostType.JournalEntry then "Article" else "Note")

if not (isNull post.Title) then
pair "name" post.Title

if not (isNull post.Html) then
pair "content" post.Html

pair "attributedTo" actor_id
pair "tag" [
for tag in post.Tags do dict [
pair "type" "Hashtag"
pair "name" $"#{tag}"
pair "href" $"https://{appInfo.ApplicationHostname}/Profile/Search?q=%%23{Uri.EscapeDataString(tag)}"
]
]
pair "published" post.PublishedTime
pair "to" "https://www.w3.org/ns/activitystreams#Public"
pair "cc" [followers_collection_id]

if post.Images.Count > 0 then
pair "attachment" [
for image in post.Images do
dict [
pair "type" "Image"
pair "url" (mapper.GetImageUrl(post, image.Blob))
pair "mediaType" image.Blob.ContentType
if not (String.IsNullOrEmpty(image.AltText)) then
pair "name" image.AltText
if not (isNull image.FocalPoint) then
pair "focalPoint" [image.FocalPoint.Horizontal; image.FocalPoint.Vertical]
]
]

Community posts (for Lemmy) and replies are converted as follows (where audience is the ID / URL of the Lemmy community, if any):

let isReply = not (isNull post.InReplyTo)
let communities = Option.toList post.Audience
let addressing = {|
To = [
"https://www.w3.org/ns/activitystreams#Public"
if not isReply then
yield! communities
]
Cc = [
yield! post.Users
if isReply then
yield! communities
]
|}

pair "id" id
pair "url" id

pair "type" "Note"
if not (isNull post.Title) then
pair "name" post.Title

pair "content" post.HtmlContent

pair "inReplyTo" post.InReplyTo

pair "attributedTo" actor_id
pair "published" post.PublishedTime

pair "to" post.Addressing.To
pair "cc" post.Addressing.Cc

match post.Audience with
| Some id -> pair "audience" id
| _ -> ()

The inbox performs the following steps:

  • Expand the content using JSON-LD (which may require a remote fetch, if a context is used which is not yet cached)
    • If this fails, replace the JSON-LD context with https://www.w3.org/ns/activitystreams and https://w3id.org/security/v1 and try again
  • Retrieve the actor mentioned in the activity, and verify the request against that actor's public key
  • Process the activity based on its type

The following activity types are supported:

  • Follow
  • Like, Dislike, Flag, Listen, Read, View, Announce (against a post created by the Pandacap actor)
  • Undo (for any of the above)
  • Accept, Reject (for Follow)
  • Create (for a post that is a reply to one of the Pandacap actor's posts, is addressed to the Pandacap actor, or is from a followed user and is not addressed to any specific user)
  • Update (for a follow, follower, or reply)
  • Delete (for an inbox post, favorite, or reply)

Outgoing activities are sent every 10 minutes, and are used for:

  • Accepting follow requests (sent to the follower)
  • Creating top-level posts to Lemmy communities (sent to the community)
  • Creating or deleting replies (sent to all addressed users, and to the community, if any)
  • Adding or removing favorites (sent to the creator of the favorited post)
  • Following and unfollowing users (sent to the user)
  • Uploading a new avatar (sent to all followers and all users you follow)
  • Creating artwork posts, journal entries, or status updates (sent to all followers, except for Bridgy Fed, which is disabled in my deployment because I now crosspost to Bluesky manually)
  • Deleting artwork posts, journal entries, or status updates (sent to all followers and all users you follow)

Snail#

A programming blog where the gimmick is that I pretend to be a snail.

Expand Cut Tags

No cut tags

Style Credit

Page generated Jun. 4th, 2025 01:28 pm
Powered by Dreamwidth Studios