mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
docs: add CLAUDE.md for AI agents and README.md for humans
CLAUDE.md covers architecture, 18 critical gotchas distilled from bug fixes (Fedify bridge, objectId vs getObject, template collisions, Express 5 redirect, date handling, author fallback chain, etc.), MongoDB collections, route table, and publishing workflow. README.md covers features, installation, configuration, nginx setup, how syndication/inbox/content negotiation work, Mastodon migration, admin UI reference, and known limitations.
This commit is contained in:
303
CLAUDE.md
Normal file
303
CLAUDE.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# CLAUDE.md — @rmdes/indiekit-endpoint-activitypub
|
||||
|
||||
AI agent instructions for working on this codebase. Read this entire file before making any changes.
|
||||
|
||||
## What This Is
|
||||
|
||||
An Indiekit plugin that adds full ActivityPub federation via [Fedify](https://fedify.dev). It turns an Indiekit-powered IndieWeb site into a fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, Lemmy, etc.
|
||||
|
||||
**npm:** `@rmdes/indiekit-endpoint-activitypub`
|
||||
**Version:** See `package.json`
|
||||
**Node:** >=22
|
||||
**Module system:** ESM (`"type": "module"`)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
index.js ← Plugin entry, route registration, syndicator
|
||||
├── lib/federation-setup.js ← Fedify Federation instance, dispatchers, collections
|
||||
├── lib/federation-bridge.js ← Express ↔ Fedify request/response bridge
|
||||
├── lib/inbox-listeners.js ← Handlers for Follow, Undo, Like, Announce, Create, Delete, etc.
|
||||
├── lib/jf2-to-as2.js ← JF2 → ActivityStreams conversion (plain JSON + Fedify vocab)
|
||||
├── lib/kv-store.js ← MongoDB-backed KvStore for Fedify
|
||||
├── lib/activity-log.js ← Activity logging to ap_activities
|
||||
├── lib/timeline-store.js ← Timeline item extraction + sanitization
|
||||
├── lib/timeline-cleanup.js ← Retention-based timeline pruning
|
||||
├── lib/batch-refollow.js ← Gradual re-follow for imported Mastodon accounts
|
||||
├── lib/migration.js ← CSV parsing + WebFinger resolution for Mastodon import
|
||||
├── lib/csrf.js ← CSRF token generation/validation
|
||||
├── lib/storage/
|
||||
│ ├── timeline.js ← Timeline CRUD with cursor pagination
|
||||
│ ├── notifications.js ← Notification CRUD with read/unread tracking
|
||||
│ └── moderation.js ← Mute/block storage
|
||||
├── lib/controllers/ ← Express route handlers (admin UI)
|
||||
│ ├── dashboard.js, reader.js, compose.js, profile.js, profile.remote.js
|
||||
│ ├── followers.js, following.js, activities.js
|
||||
│ ├── featured.js, featured-tags.js
|
||||
│ ├── interactions.js, interactions-like.js, interactions-boost.js
|
||||
│ ├── moderation.js, migrate.js, refollow.js
|
||||
├── views/ ← Nunjucks templates
|
||||
│ ├── activitypub-*.njk ← Page templates
|
||||
│ ├── layouts/ap-reader.njk ← Reader layout (NOT reader.njk — see gotcha below)
|
||||
│ └── partials/ ← Shared components
|
||||
├── assets/
|
||||
│ ├── reader.css ← Reader UI styles
|
||||
│ └── icon.svg ← Plugin icon
|
||||
└── locales/en.json ← i18n strings
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
Outbound: Indiekit post → syndicator.syndicate() → jf2ToAS2Activity() → ctx.sendActivity() → follower inboxes
|
||||
Inbound: Remote inbox POST → Fedify → inbox-listeners.js → MongoDB collections → admin UI
|
||||
Reader: Followed account posts → Create inbox → timeline-store → ap_timeline → reader UI
|
||||
```
|
||||
|
||||
## MongoDB Collections
|
||||
|
||||
| Collection | Purpose | Key fields |
|
||||
|---|---|---|
|
||||
| `ap_followers` | Accounts following us | `actorUrl` (unique), `inbox`, `sharedInbox`, `source` |
|
||||
| `ap_following` | Accounts we follow | `actorUrl` (unique), `source`, `acceptedAt` |
|
||||
| `ap_activities` | Activity log (TTL-indexed) | `direction`, `type`, `actorUrl`, `objectUrl`, `receivedAt` |
|
||||
| `ap_keys` | Cryptographic key pairs | `type` ("rsa" or "ed25519"), key material |
|
||||
| `ap_kv` | Fedify KvStore + job state | `_id` (key path), `value` |
|
||||
| `ap_profile` | Actor profile (single doc) | `name`, `summary`, `icon`, `attachments`, `actorType` |
|
||||
| `ap_featured` | Pinned posts | `postUrl`, `pinnedAt` |
|
||||
| `ap_featured_tags` | Featured hashtags | `tag`, `addedAt` |
|
||||
| `ap_timeline` | Reader timeline items | `uid` (unique), `published`, `author`, `content` |
|
||||
| `ap_notifications` | Likes, boosts, follows, mentions | `uid` (unique), `type`, `read` |
|
||||
| `ap_muted` | Muted actors/keywords | `url` or `keyword` |
|
||||
| `ap_blocked` | Blocked actors | `url` |
|
||||
| `ap_interactions` | Like/boost tracking per post | `objectUrl`, `type` |
|
||||
|
||||
## Critical Patterns and Gotchas
|
||||
|
||||
### 1. Express ↔ Fedify Bridge (CUSTOM — NOT @fedify/express)
|
||||
|
||||
We **cannot** use `@fedify/express`'s `integrateFederation()` because Indiekit mounts plugins at sub-paths. Express strips the mount prefix from `req.url`, breaking Fedify's URI template matching. Instead, `federation-bridge.js` uses `req.originalUrl` to build the full URL.
|
||||
|
||||
**If you see path-matching issues with Fedify, check that `req.originalUrl` is being used, not `req.url`.**
|
||||
|
||||
### 2. Content Negotiation Route — GET Only
|
||||
|
||||
The `contentNegotiationRoutes` router is mounted at `/` (root). It MUST only pass `GET`/`HEAD` requests to Fedify. Passing `POST`/`PUT`/`DELETE` would cause `fromExpressRequest()` to consume the body stream via `Readable.toWeb(req)`, breaking Express body-parsed routes downstream (admin forms, Micropub, etc.).
|
||||
|
||||
### 3. Skip Fedify for Admin Routes
|
||||
|
||||
In `routesPublic`, the middleware skips paths starting with `/admin`. Without this, Fedify would intercept admin UI requests and return 404/406 responses instead of letting Express serve the authenticated pages.
|
||||
|
||||
### 4. Use .objectId/.actorId — NOT .getObject()/.getActor() in Inbox Handlers
|
||||
|
||||
Fedify's `.getObject()` and `.getActor()` trigger HTTP fetches to remote servers. This fails silently or retries ~10 times when:
|
||||
- Remote server has **Authorized Fetch** enabled (returns 401)
|
||||
- Server is down or unreachable
|
||||
- Object has been deleted
|
||||
|
||||
**Always prefer** `.objectId?.href` and `.actorId?.href` (zero network requests) for Like, Announce, Undo, and Delete handlers. Only use `.getObject()` / `.getActor()` when you need the full object, and **always wrap in try-catch**.
|
||||
|
||||
### 5. Accept(Follow) Matching — Don't Check Inner Object Type
|
||||
|
||||
Fedify often resolves the inner object of `Accept` to a `Person` (the Follow's target) rather than the `Follow` itself. The Accept handler matches against `ap_following` by actor URL instead of inspecting `inner instanceof Follow`.
|
||||
|
||||
### 6. Filter Inbound Likes/Announces to Our Content Only
|
||||
|
||||
Without filtering, the inbox logs every Like/Announce from every federated server — including reactions to other people's content that happens to flow through shared inboxes. Check `objectId.startsWith(publicationUrl)` before logging.
|
||||
|
||||
### 7. Nunjucks Template Name Collisions
|
||||
|
||||
Template names resolve across ALL registered plugin view directories. If two plugins have `views/layouts/reader.njk`, Nunjucks loads whichever it finds first (often wrong). The reader layout is named `ap-reader.njk` to avoid collision with `@rmdes/indiekit-endpoint-microsub`'s `reader.njk`.
|
||||
|
||||
**Never name a layout/template with a generic name that another plugin might use.**
|
||||
|
||||
### 8. Express 5 — No redirect("back")
|
||||
|
||||
Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's treated as a literal URL, causing 404s at paths like `/admin/featured/back`. Always use explicit redirect paths.
|
||||
|
||||
### 9. Fedify Endpoints Type Bug (Workaround)
|
||||
|
||||
Fedify serializes `endpoints` with `"type": "as:Endpoints"` which is not a real ActivityStreams type. `sendFedifyResponse()` in `federation-bridge.js` strips this from actor JSON responses. Remove the workaround when [fedify#576](https://github.com/fedify-dev/fedify/issues/576) is fixed upstream.
|
||||
|
||||
### 10. Profile Links — Express qs Body Parser Key Mismatch
|
||||
|
||||
`express.urlencoded({ extended: true })` uses `qs` which strips `[]` from array field names. HTML fields named `link_name[]` arrive as `request.body.link_name` (not `request.body["link_name[]"]`). The profile controller reads `link_name` and `link_value`, NOT `link_name[]`.
|
||||
|
||||
### 11. Author Resolution Fallback Chain
|
||||
|
||||
`extractObjectData()` in `timeline-store.js` uses a multi-strategy fallback:
|
||||
1. `object.getAttributedTo()` — async, may fail with Authorized Fetch
|
||||
2. `options.actorFallback` — the activity's actor (passed from Create handler)
|
||||
3. `object.attribution` / `object.attributedTo` — plain object properties
|
||||
4. `object.attributionIds` — non-fetching URL array with username extraction from common patterns (`/@name`, `/users/name`)
|
||||
|
||||
Without this chain, many timeline items show "Unknown" as the author.
|
||||
|
||||
### 12. Username Extraction from Actor URLs
|
||||
|
||||
When extracting usernames from attribution IDs, handle multiple URL patterns:
|
||||
- `/@username` (Mastodon)
|
||||
- `/users/username` (Mastodon, Indiekit)
|
||||
- `/ap/users/12345/` (numeric IDs on some platforms)
|
||||
|
||||
The regex was previously matching "users" instead of the actual username from `/users/NatalieDavis`.
|
||||
|
||||
### 13. Empty Boost Filtering
|
||||
|
||||
Lemmy/PieFed send Announce activities where the boosted object resolves to an activity ID instead of a Note/Article with actual content. Check `object.content || object.name` before storing to avoid empty cards in the timeline.
|
||||
|
||||
### 14. Temporal.Instant for Fedify Dates
|
||||
|
||||
Fedify uses `@js-temporal/polyfill` for dates. When setting `published` on Fedify objects, use `Temporal.Instant.from(isoString)`. When reading Fedify dates in inbox handlers, use `String(object.published)` to get ISO strings — NOT `new Date(object.published)` which causes `TypeError`.
|
||||
|
||||
### 15. LogTape — Configure Once Only
|
||||
|
||||
`@logtape/logtape`'s `configure()` can only be called once per process. The module-level `_logtapeConfigured` flag prevents duplicate configuration. If configure fails (e.g., another plugin already configured it), catch the error silently.
|
||||
|
||||
### 16. .authorize() Intentionally NOT Chained on Actor Dispatcher
|
||||
|
||||
Fedify's `.authorize()` triggers HTTP Signature verification on every GET to the actor endpoint. Servers requiring Authorized Fetch cause infinite loops: Fedify tries to fetch their key → they return 401 → Fedify retries → 500 errors. Re-enable when Fedify supports authenticated document loading for outgoing fetches.
|
||||
|
||||
### 17. Delivery Queue Must Be Started
|
||||
|
||||
`federation.startQueue()` MUST be called after setup. Without it, `ctx.sendActivity()` enqueues tasks but the message queue never processes them — activities are never delivered.
|
||||
|
||||
### 18. Shared Key Dispatcher for Shared Inbox
|
||||
|
||||
`inboxChain.setSharedKeyDispatcher()` tells Fedify to use our actor's key pair when verifying HTTP Signatures on the shared inbox. Without this, servers like hachyderm.io (which requires Authorized Fetch) have their signatures rejected.
|
||||
|
||||
## Date Handling Convention
|
||||
|
||||
**All dates MUST be stored as ISO 8601 strings.** This is mandatory across all Indiekit plugins.
|
||||
|
||||
```javascript
|
||||
// CORRECT
|
||||
followedAt: new Date().toISOString()
|
||||
published: String(fedifyObject.published) // Temporal → string
|
||||
|
||||
// WRONG — crashes Nunjucks | date filter
|
||||
followedAt: new Date()
|
||||
published: new Date(fedifyObject.published)
|
||||
```
|
||||
|
||||
The Nunjucks `| date` filter calls `date-fns parseISO()` which only accepts ISO strings. `Date` objects cause `"dateString.split is not a function"` crashes.
|
||||
|
||||
## Batch Re-follow State Machine
|
||||
|
||||
```
|
||||
import → refollow:pending → refollow:sent → federation (happy path: Accept received)
|
||||
import → refollow:pending → refollow:sent → refollow:failed (after 3 retries)
|
||||
```
|
||||
|
||||
- `import`: Imported from Mastodon CSV, no Follow sent yet
|
||||
- `refollow:pending`: Claimed by batch processor, being processed
|
||||
- `refollow:sent`: Follow activity sent, awaiting Accept
|
||||
- `federation`: Accept received, fully federated
|
||||
- `refollow:failed`: Max retries exceeded
|
||||
|
||||
On restart, `refollow:pending` entries are reset to `import` to prevent stale claims.
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
1. `constructor()` — Merges options with defaults
|
||||
2. `init(Indiekit)` — Called by Indiekit during startup:
|
||||
- Stores `publication.me` as `_publicationUrl`
|
||||
- Registers 13 MongoDB collections with indexes
|
||||
- Seeds actor profile from config (first run only)
|
||||
- Calls `setupFederation()` which creates Fedify instance + starts queue
|
||||
- Registers endpoint (mounts routes) and syndicator
|
||||
- Starts batch re-follow processor (10s delay)
|
||||
- Schedules timeline cleanup (on startup + every 24h)
|
||||
|
||||
## Route Structure
|
||||
|
||||
| Method | Path | Handler | Auth |
|
||||
|---|---|---|---|
|
||||
| `*` | `/.well-known/*` | Fedify (WebFinger, NodeInfo) | No |
|
||||
| `*` | `{mount}/users/*`, `{mount}/inbox` | Fedify (actor, inbox, outbox, collections) | No (HTTP Sig) |
|
||||
| `GET` | `{mount}/` | Dashboard | Yes (IndieAuth) |
|
||||
| `GET` | `{mount}/admin/reader` | Timeline reader | Yes |
|
||||
| `GET` | `{mount}/admin/reader/notifications` | Notifications | Yes |
|
||||
| `POST` | `{mount}/admin/reader/compose` | Compose reply | Yes |
|
||||
| `POST` | `{mount}/admin/reader/like,unlike,boost,unboost` | Interactions | Yes |
|
||||
| `POST` | `{mount}/admin/reader/follow,unfollow` | Follow/unfollow | Yes |
|
||||
| `GET` | `{mount}/admin/reader/profile` | Remote profile view | Yes |
|
||||
| `GET` | `{mount}/admin/reader/moderation` | Moderation dashboard | Yes |
|
||||
| `POST` | `{mount}/admin/reader/mute,unmute,block,unblock` | Moderation actions | Yes |
|
||||
| `GET` | `{mount}/admin/followers,following,activities` | Lists | Yes |
|
||||
| `GET/POST` | `{mount}/admin/profile` | Actor profile editor | Yes |
|
||||
| `GET/POST` | `{mount}/admin/featured` | Pinned posts | Yes |
|
||||
| `GET/POST` | `{mount}/admin/tags` | Featured tags | Yes |
|
||||
| `GET/POST` | `{mount}/admin/migrate` | Mastodon migration | Yes |
|
||||
| `*` | `{mount}/admin/refollow/*` | Batch refollow control | Yes |
|
||||
| `GET` | `/*` (root) | Content negotiation (AP clients only) | No |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---|---|
|
||||
| `@fedify/fedify` | ActivityPub federation framework |
|
||||
| `@fedify/express` | Express integration utilities (types only — bridge is custom) |
|
||||
| `@fedify/redis` | Redis message queue for delivery |
|
||||
| `@js-temporal/polyfill` | Temporal API for Fedify date handling |
|
||||
| `ioredis` | Redis client |
|
||||
| `sanitize-html` | XSS prevention for timeline/notification content |
|
||||
| `express` | Route handling (peer: Indiekit provides it) |
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```javascript
|
||||
{
|
||||
mountPath: "/activitypub", // URL prefix for all routes
|
||||
actor: {
|
||||
handle: "rick", // Fediverse username
|
||||
name: "Ricardo Mendes", // Display name (seeds profile)
|
||||
summary: "", // Bio (seeds profile)
|
||||
icon: "", // Avatar URL (seeds profile)
|
||||
},
|
||||
checked: true, // Syndicator checked by default
|
||||
alsoKnownAs: "", // Mastodon migration alias
|
||||
activityRetentionDays: 90, // TTL for ap_activities (0 = forever)
|
||||
storeRawActivities: false, // Store full JSON of inbound activities
|
||||
redisUrl: "", // Redis for delivery queue (empty = in-process)
|
||||
parallelWorkers: 5, // Parallel delivery workers (with Redis)
|
||||
actorType: "Person", // Person | Service | Organization | Group
|
||||
timelineRetention: 1000, // Max timeline items (0 = unlimited)
|
||||
}
|
||||
```
|
||||
|
||||
## Publishing Workflow
|
||||
|
||||
1. Edit code in this repo
|
||||
2. Bump version in `package.json` (npm rejects duplicate versions)
|
||||
3. Commit and push
|
||||
4. **STOP** — user must run `npm publish` manually (requires OTP)
|
||||
5. After publish confirmation, update Dockerfile version in `indiekit-cloudron/`
|
||||
6. `cloudron build --no-cache && cloudron update --app rmendes.net --no-backup`
|
||||
|
||||
## Testing
|
||||
|
||||
No automated test suite. Manual testing against real fediverse servers:
|
||||
|
||||
```bash
|
||||
# WebFinger
|
||||
curl -s "https://rmendes.net/.well-known/webfinger?resource=acct:rick@rmendes.net" | jq .
|
||||
|
||||
# Actor document
|
||||
curl -s -H "Accept: application/activity+json" "https://rmendes.net/" | jq .
|
||||
|
||||
# NodeInfo
|
||||
curl -s "https://rmendes.net/nodeinfo/2.1" | jq .
|
||||
|
||||
# Search from Mastodon for @rick@rmendes.net
|
||||
```
|
||||
|
||||
## CSS Conventions
|
||||
|
||||
The reader CSS (`assets/reader.css`) uses Indiekit's theme custom properties for automatic dark mode support:
|
||||
- `--color-on-background` (not `--color-text`)
|
||||
- `--color-on-offset` (not `--color-text-muted`)
|
||||
- `--border-radius-small` (not `--border-radius`)
|
||||
- `--color-red45`, `--color-green50`, etc. (not hardcoded hex)
|
||||
|
||||
Post types are differentiated by left border color: purple (notes), green (articles), yellow (boosts), primary (replies).
|
||||
267
README.md
Normal file
267
README.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# @rmdes/indiekit-endpoint-activitypub
|
||||
|
||||
ActivityPub federation endpoint for [Indiekit](https://getindiekit.com). Makes your IndieWeb site a full fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, and any ActivityPub-compatible platform.
|
||||
|
||||
## Features
|
||||
|
||||
**Federation**
|
||||
- Full ActivityPub actor with WebFinger, NodeInfo, HTTP Signatures, and Object Integrity Proofs (Ed25519)
|
||||
- Outbox syndication — posts created via Micropub are automatically delivered to followers
|
||||
- Inbox processing — receives follows, likes, boosts, replies, mentions, deletes, and account moves
|
||||
- Content negotiation — ActivityPub clients requesting your site get JSON-LD; browsers get HTML
|
||||
- Reply delivery — replies are addressed to and delivered directly to the original post's author
|
||||
- Shared inbox support with collection sync (FEP-8fcf)
|
||||
- Configurable actor type (Person, Service, Organization, Group)
|
||||
|
||||
**Reader**
|
||||
- Timeline view showing posts from followed accounts
|
||||
- Notifications for likes, boosts, follows, mentions, and replies
|
||||
- Compose form with dual-path posting (quick AP reply or Micropub blog post)
|
||||
- Native interactions (like, boost, reply, follow/unfollow from the reader)
|
||||
- Remote actor profile pages
|
||||
- Content warnings and sensitive content handling
|
||||
- Media display (images, video, audio)
|
||||
- Configurable timeline retention
|
||||
|
||||
**Moderation**
|
||||
- Mute actors or keywords
|
||||
- Block actors (also removes from followers)
|
||||
- All moderation actions available from the reader UI
|
||||
|
||||
**Mastodon Migration**
|
||||
- Import following/followers lists from Mastodon CSV exports
|
||||
- Set `alsoKnownAs` alias for account Move verification
|
||||
- Batch re-follow processor — gradually sends Follow activities to imported accounts
|
||||
- Progress tracking with pause/resume controls
|
||||
|
||||
**Admin UI**
|
||||
- Dashboard with follower/following counts and recent activity
|
||||
- Profile editor (name, bio, avatar, header, profile links with rel="me" verification)
|
||||
- Pinned posts (featured collection)
|
||||
- Featured tags (hashtag collection)
|
||||
- Activity log (inbound/outbound)
|
||||
- Follower and following lists with source tracking
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Indiekit](https://getindiekit.com) v1.0.0-beta.25+
|
||||
- Node.js >= 22
|
||||
- MongoDB (used by Indiekit)
|
||||
- Redis (recommended for production delivery queue; in-process queue available for development)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @rmdes/indiekit-endpoint-activitypub
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the plugin to your Indiekit config:
|
||||
|
||||
```javascript
|
||||
// indiekit.config.js
|
||||
export default {
|
||||
plugins: [
|
||||
"@rmdes/indiekit-endpoint-activitypub",
|
||||
],
|
||||
"@rmdes/indiekit-endpoint-activitypub": {
|
||||
mountPath: "/activitypub",
|
||||
actor: {
|
||||
handle: "yourname",
|
||||
name: "Your Name",
|
||||
summary: "A short bio",
|
||||
icon: "https://example.com/avatar.jpg",
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### All Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `mountPath` | string | `"/activitypub"` | URL prefix for all plugin routes |
|
||||
| `actor.handle` | string | `"rick"` | Fediverse username (e.g. `@handle@yourdomain.com`) |
|
||||
| `actor.name` | string | `""` | Display name (used to seed profile on first run) |
|
||||
| `actor.summary` | string | `""` | Bio text (used to seed profile on first run) |
|
||||
| `actor.icon` | string | `""` | Avatar URL (used to seed profile on first run) |
|
||||
| `checked` | boolean | `true` | Whether the syndicator is checked by default in the post editor |
|
||||
| `alsoKnownAs` | string | `""` | Mastodon migration alias URL |
|
||||
| `activityRetentionDays` | number | `90` | Days to keep activity log entries (0 = forever) |
|
||||
| `storeRawActivities` | boolean | `false` | Store full raw JSON of inbound activities |
|
||||
| `redisUrl` | string | `""` | Redis connection URL for delivery queue |
|
||||
| `parallelWorkers` | number | `5` | Number of parallel delivery workers (requires Redis) |
|
||||
| `actorType` | string | `"Person"` | Actor type: `Person`, `Service`, `Organization`, or `Group` |
|
||||
| `timelineRetention` | number | `1000` | Maximum timeline items to keep (0 = unlimited) |
|
||||
|
||||
### Redis (Recommended for Production)
|
||||
|
||||
Without Redis, the plugin uses an in-process message queue. This works for development but won't survive restarts and has limited throughput.
|
||||
|
||||
```javascript
|
||||
"@rmdes/indiekit-endpoint-activitypub": {
|
||||
redisUrl: "redis://localhost:6379",
|
||||
parallelWorkers: 5,
|
||||
},
|
||||
```
|
||||
|
||||
### Nginx Configuration (Reverse Proxy)
|
||||
|
||||
If you serve a static site alongside Indiekit (e.g. with Eleventy), you need nginx rules to route ActivityPub requests to Indiekit while serving HTML to browsers:
|
||||
|
||||
```nginx
|
||||
# ActivityPub content negotiation — detect AP clients
|
||||
map $http_accept $is_activitypub {
|
||||
default 0;
|
||||
"~*application/activity\+json" 1;
|
||||
"~*application/ld\+json" 1;
|
||||
}
|
||||
|
||||
# Proxy /activitypub to Indiekit
|
||||
location /activitypub {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
|
||||
# Default: static site, but AP clients get proxied
|
||||
location / {
|
||||
if ($is_activitypub) {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Syndication (Outbound)
|
||||
|
||||
When you create a post via Micropub, Indiekit's syndication system calls this plugin's syndicator. The plugin:
|
||||
|
||||
1. Converts the JF2 post properties to an ActivityStreams 2.0 `Create(Note)` or `Create(Article)` activity
|
||||
2. For replies, resolves the original post's author to include them in CC and deliver directly to their inbox
|
||||
3. Sends the activity to all followers via shared inboxes using Fedify's delivery queue
|
||||
4. Appends a permalink to the content so fediverse clients link back to your canonical post
|
||||
|
||||
### Inbox Processing (Inbound)
|
||||
|
||||
When remote servers send activities to your inbox:
|
||||
|
||||
- **Follow** → Auto-accepted, stored in `ap_followers`, notification created
|
||||
- **Undo(Follow)** → Removed from `ap_followers`
|
||||
- **Like** → Logged in activity log, notification created (only for reactions to your own posts)
|
||||
- **Announce (Boost)** → Logged + notification (your content) or stored in timeline (followed account)
|
||||
- **Create (Note/Article)** → Stored in timeline if from a followed account; notification if it's a reply or mention
|
||||
- **Update** → Updates timeline item content or refreshes follower profile data
|
||||
- **Delete** → Removes from activity log and timeline
|
||||
- **Move** → Updates follower's actor URL
|
||||
- **Accept(Follow)** → Marks our follow as accepted
|
||||
- **Reject(Follow)** → Marks our follow as rejected
|
||||
- **Block** → Removes actor from our followers
|
||||
|
||||
### Content Negotiation
|
||||
|
||||
The plugin mounts a root-level router that intercepts requests from ActivityPub clients (detected by `Accept: application/activity+json` or `application/ld+json`):
|
||||
|
||||
- Root URL (`/`) → Redirects to the Fedify actor document
|
||||
- Post URLs → Looks up the post in MongoDB, converts to AS2 JSON
|
||||
- NodeInfo (`/nodeinfo/2.1`) → Delegated to Fedify
|
||||
|
||||
Regular browser requests pass through unmodified.
|
||||
|
||||
### Mastodon Migration
|
||||
|
||||
The plugin supports migrating from a Mastodon account:
|
||||
|
||||
1. **Set alias** — Configure `alsoKnownAs` with your old Mastodon profile URL. This is verified by Mastodon before allowing a Move.
|
||||
2. **Import social graph** — Upload Mastodon's `following_accounts.csv` and `followers.csv` exports. Following entries are resolved via WebFinger and stored locally.
|
||||
3. **Trigger Move** — From Mastodon's settings, initiate a Move to `@handle@yourdomain.com`. Mastodon notifies your followers, and compatible servers auto-refollow.
|
||||
4. **Batch re-follow** — The plugin gradually sends Follow activities to all imported accounts (10 per batch, 30s between batches) so remote servers start delivering content to your inbox.
|
||||
|
||||
## Verification
|
||||
|
||||
After deployment, verify federation is working:
|
||||
|
||||
```bash
|
||||
# WebFinger discovery
|
||||
curl -s "https://yourdomain.com/.well-known/webfinger?resource=acct:handle@yourdomain.com" | jq .
|
||||
|
||||
# Actor document
|
||||
curl -s -H "Accept: application/activity+json" "https://yourdomain.com/" | jq .
|
||||
|
||||
# NodeInfo
|
||||
curl -s "https://yourdomain.com/nodeinfo/2.1" | jq .
|
||||
```
|
||||
|
||||
Then search for `@handle@yourdomain.com` from any Mastodon instance — your profile should appear.
|
||||
|
||||
## Admin UI Pages
|
||||
|
||||
All admin pages are behind IndieAuth authentication:
|
||||
|
||||
| Page | Path | Description |
|
||||
|---|---|---|
|
||||
| Dashboard | `/activitypub` | Overview with follower/following counts, recent activity |
|
||||
| Reader | `/activitypub/admin/reader` | Timeline from followed accounts |
|
||||
| Notifications | `/activitypub/admin/reader/notifications` | Likes, boosts, follows, mentions, replies |
|
||||
| Compose | `/activitypub/admin/reader/compose` | Reply composer (quick AP or Micropub) |
|
||||
| Moderation | `/activitypub/admin/reader/moderation` | Muted/blocked accounts and keywords |
|
||||
| Profile | `/activitypub/admin/profile` | Edit actor display name, bio, avatar, links |
|
||||
| Followers | `/activitypub/admin/followers` | List of accounts following you |
|
||||
| Following | `/activitypub/admin/following` | List of accounts you follow |
|
||||
| Activity Log | `/activitypub/admin/activities` | Inbound/outbound activity history |
|
||||
| Pinned Posts | `/activitypub/admin/featured` | Pin/unpin posts to your featured collection |
|
||||
| Featured Tags | `/activitypub/admin/tags` | Add/remove featured hashtags |
|
||||
| Migration | `/activitypub/admin/migrate` | Mastodon import wizard |
|
||||
|
||||
## MongoDB Collections
|
||||
|
||||
The plugin creates these collections automatically:
|
||||
|
||||
| Collection | Description |
|
||||
|---|---|
|
||||
| `ap_followers` | Accounts following your actor |
|
||||
| `ap_following` | Accounts you follow |
|
||||
| `ap_activities` | Activity log with automatic TTL cleanup |
|
||||
| `ap_keys` | RSA and Ed25519 key pairs for HTTP Signatures |
|
||||
| `ap_kv` | Fedify key-value store and batch job state |
|
||||
| `ap_profile` | Actor profile (single document) |
|
||||
| `ap_featured` | Pinned/featured posts |
|
||||
| `ap_featured_tags` | Featured hashtags |
|
||||
| `ap_timeline` | Reader timeline items from followed accounts |
|
||||
| `ap_notifications` | Interaction notifications |
|
||||
| `ap_muted` | Muted actors and keywords |
|
||||
| `ap_blocked` | Blocked actors |
|
||||
| `ap_interactions` | Per-post like/boost tracking |
|
||||
|
||||
## Supported Post Types
|
||||
|
||||
The JF2-to-ActivityStreams converter handles these Indiekit post types:
|
||||
|
||||
| Post Type | ActivityStreams |
|
||||
|---|---|
|
||||
| note, reply, bookmark, jam, rsvp, checkin | `Create(Note)` |
|
||||
| article | `Create(Article)` |
|
||||
| like | `Like` |
|
||||
| repost | `Announce` |
|
||||
| photo, video, audio | Attachments on Note/Article |
|
||||
|
||||
Categories are converted to `Hashtag` tags. Bookmarks include a bookmark emoji and link.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **No automated tests** — Manual testing against real fediverse servers
|
||||
- **Single actor** — One fediverse identity per Indiekit instance
|
||||
- **No Authorized Fetch enforcement** — Disabled due to Fedify's current limitation with authenticated outgoing fetches (causes infinite loops with servers that require it)
|
||||
- **No image upload in reader** — Compose form is text-only
|
||||
- **In-process queue without Redis** — Activities may be lost on restart
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
[Ricardo Mendes](https://rmendes.net) ([@rick@rmendes.net](https://rmendes.net))
|
||||
Reference in New Issue
Block a user