docs: update README to reflect current API state
- also_known_as: Vec<String> (was Option<String>) - broadcast_create_note/update_note: add mentioned_inboxes param + example - EventPublisher: add BackfillRequested variant to match/example - Add run_backfill_for_follower and import_remote_outbox sections - Add featured collection section (get_featured_objects override) - Expand routes table to include /featured endpoint - Add count_accepted_followers + get_accepted_followers_page to follow section - FederationEvent table: add BackfillRequested - ApObjectHandler/ApContentReader: note which methods have defaults - Inbound section: mention Undo(Announce) and Move improvements
This commit is contained in:
151
README.md
151
README.md
@@ -15,7 +15,7 @@ k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0
|
||||
|
||||
## What you implement
|
||||
|
||||
Seven focused traits wire your data layer into `k-ap`. You can implement them all on a single database struct by cloning the `Arc`.
|
||||
Seven focused traits wire your data layer into `k-ap`. Implement them all on a single database struct by cloning the `Arc`, or use separate structs for different backends.
|
||||
|
||||
```rust
|
||||
// Activity deduplication — idempotency for inbound deliveries
|
||||
@@ -33,7 +33,7 @@ impl BlocklistRepository for MyDb { ... }
|
||||
// User lookup by id / username
|
||||
impl ApUserRepository for MyDb { ... }
|
||||
|
||||
// Read side — provides local content to the library (outbox, backfill)
|
||||
// Read side — provides local content to the library (outbox, backfill, featured)
|
||||
impl ApContentReader for MyDb { ... }
|
||||
|
||||
// Write side — called when the inbox receives AP activities
|
||||
@@ -67,12 +67,18 @@ let router = Router::new().merge(service.router());
|
||||
|
||||
## What the service handles for you
|
||||
|
||||
- **Actor** — `GET /users/:id` serves the AP Person object with public key and security vocabulary `@context`
|
||||
- **Inbox** — `POST /users/:id/inbox` + `POST /inbox` (shared), verifies HTTP signatures, dispatches to your `ApObjectHandler`. 1 MB body limit enforced.
|
||||
- **Outbox** — `GET /users/:id/outbox` with cursor-based `OrderedCollection` pagination
|
||||
- **Followers / Following** — `GET /users/:id/followers` and `/following`
|
||||
- **WebFinger** — `GET /.well-known/webfinger` with `aliases` field
|
||||
- **NodeInfo** — `GET /.well-known/nodeinfo` + `GET /nodeinfo/2.0`
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `GET /users/{id}` | AP actor JSON with public key and security `@context` |
|
||||
| `POST /users/{id}/inbox` | Per-user inbox — verifies HTTP signatures, 1 MB limit |
|
||||
| `POST /inbox` | Shared inbox — same verification |
|
||||
| `GET /users/{id}/outbox` | Cursor-based `OrderedCollection` |
|
||||
| `GET /users/{id}/followers` | Offset-paginated follower collection |
|
||||
| `GET /users/{id}/following` | Offset-paginated following collection |
|
||||
| `GET /users/{id}/featured` | Pinned posts `OrderedCollection` (from `get_featured_objects`) |
|
||||
| `GET /.well-known/webfinger` | JRD with `aliases` field |
|
||||
| `GET /.well-known/nodeinfo` | NodeInfo well-known redirect |
|
||||
| `GET /nodeinfo/2.0` | NodeInfo 2.0 |
|
||||
|
||||
## ApUser fields
|
||||
|
||||
@@ -86,10 +92,10 @@ ApUser {
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<Url>,
|
||||
banner_url: Option<Url>,
|
||||
also_known_as: Option<String>, // for account migration
|
||||
also_known_as: Vec<String>, // all known aliases, for account migration
|
||||
profile_url: Option<Url>,
|
||||
featured_url: Option<Url>, // pinned posts collection
|
||||
attachment: Vec<ApProfileField>, // profile metadata fields
|
||||
featured_url: Option<Url>, // pinned posts collection URL
|
||||
attachment: Vec<ApProfileField>, // profile metadata (PropertyValue)
|
||||
manually_approves_followers: bool, // controls manuallyApprovesFollowers in AP JSON
|
||||
discoverable: bool, // controls discoverable in AP JSON
|
||||
actor_type: ApActorType, // Person / Service / Application / Organization / Group
|
||||
@@ -101,31 +107,37 @@ ApUser {
|
||||
```rust
|
||||
use k_ap::ApVisibility;
|
||||
|
||||
// Public — to: [AS_PUBLIC], cc: [followers]
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Public).await?;
|
||||
// Resolve the inboxes of any mentioned non-followers first, then pass them in.
|
||||
// The library delivers to followers + mentioned actors, deduplicated.
|
||||
let mentioned = vec![
|
||||
service.lookup_actor_by_handle("@bob@mastodon.social").await?.outbox_url.unwrap(),
|
||||
// ...or resolve inbox URLs directly
|
||||
];
|
||||
|
||||
// Followers only — to: [followers], cc: []
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::FollowersOnly).await?;
|
||||
// Public — to: [AS_PUBLIC], cc: [followers]; delivered to followers + mentioned
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Public, mentioned).await?;
|
||||
|
||||
// Private — no delivery, library returns immediately
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Private).await?;
|
||||
// Followers only — to: [followers], cc: []; delivered to followers + mentioned
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::FollowersOnly, vec![]).await?;
|
||||
|
||||
service.broadcast_update_note(user_id, note_json, ApVisibility::Public).await?;
|
||||
// Private — no delivery at all; library returns immediately
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Private, vec![]).await?;
|
||||
|
||||
service.broadcast_update_note(user_id, note_json, ApVisibility::Public, vec![]).await?;
|
||||
service.broadcast_delete_to_followers(user_id, ap_id).await?;
|
||||
|
||||
// Announce / Undo Announce
|
||||
service.broadcast_announce_to_followers(user_id, object_ap_id).await?;
|
||||
service.broadcast_undo_announce_to_followers(user_id, object_ap_id).await?;
|
||||
|
||||
// Like / Unlike to a remote inbox
|
||||
// Like / Unlike to a single remote inbox
|
||||
service.broadcast_like_to_inbox(user_id, object_ap_id, inbox_url).await?;
|
||||
service.broadcast_undo_like_to_inbox(user_id, object_ap_id, inbox_url).await?;
|
||||
|
||||
// Actor profile update
|
||||
// Actor profile update to all followers
|
||||
service.broadcast_actor_update(user_id).await?;
|
||||
|
||||
// Account migration — sends a Move activity to all followers
|
||||
// Pre-condition: set alsoKnownAs on the local actor before calling this
|
||||
// Account migration — sends Move to all followers; set alsoKnownAs first
|
||||
service.broadcast_move(user_id, new_actor_url).await?;
|
||||
```
|
||||
|
||||
@@ -141,33 +153,55 @@ service.accept_follower(local_user_id, remote_actor_url).await?;
|
||||
service.reject_follower(local_user_id, remote_actor_url).await?;
|
||||
|
||||
// Inbound follow requests — DB only (no AP delivery)
|
||||
// Use these when delivering Accept/Reject from a separate worker process
|
||||
// Use when delivering Accept/Reject from a separate worker process
|
||||
service.mark_follower_accepted(local_user_id, remote_actor_url).await?;
|
||||
service.mark_follower_rejected(local_user_id, remote_actor_url).await?;
|
||||
|
||||
// Querying followers (DB-side filtering — efficient for large accounts)
|
||||
let count: usize = service.count_accepted_followers(user_id).await?;
|
||||
let page: Vec<RemoteActor> = service.get_accepted_followers_page(user_id, 0, 20).await?;
|
||||
```
|
||||
|
||||
## Async delivery via EventPublisher
|
||||
## Async delivery and backfill via EventPublisher
|
||||
|
||||
By default, outbound activities are sent via `tokio::spawn` (fire-and-forget). Implement `EventPublisher` to route deliveries through your job queue instead:
|
||||
By default, outbound delivery and backfill run in the same process via `tokio::spawn`.
|
||||
Implement `EventPublisher` to route them through your job queue so workers can process them separately:
|
||||
|
||||
```rust
|
||||
impl EventPublisher for MyQueue {
|
||||
async fn publish(&self, event: FederationEvent) -> anyhow::Result<()> {
|
||||
match event {
|
||||
FederationEvent::DeliveryRequested { inbox, activity, signing_actor_id } => {
|
||||
// Persist and enqueue the delivery task
|
||||
self.enqueue(inbox, activity, signing_actor_id).await?;
|
||||
// Persist and enqueue; your worker calls deliver_to_inbox
|
||||
self.enqueue_delivery(inbox, activity, signing_actor_id).await?;
|
||||
}
|
||||
FederationEvent::DeliveryFailed { inbox, error, .. } => {
|
||||
// Log or alert on permanent failure
|
||||
tracing::error!(%inbox, %error, "permanent delivery failure");
|
||||
}
|
||||
FederationEvent::BackfillRequested { owner_user_id, follower_inbox_url } => {
|
||||
// Enqueue; your worker calls run_backfill_for_follower
|
||||
self.enqueue_backfill(owner_user_id, follower_inbox_url).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// When your worker processes the queue item:
|
||||
// Worker: execute a delivery task from the queue
|
||||
service.deliver_to_inbox(inbox, activity_json, signing_actor_id).await?;
|
||||
|
||||
// Worker: send local content to a new follower's inbox
|
||||
service.run_backfill_for_follower(owner_user_id, follower_inbox_url).await?;
|
||||
```
|
||||
|
||||
## Remote outbox import
|
||||
|
||||
Import a remote actor's post history (e.g. after a local user follows them):
|
||||
|
||||
```rust
|
||||
// Fetches pages from outbox_url and calls ApObjectHandler::on_create for each.
|
||||
// Distinct from run_backfill_for_follower which sends YOUR content TO a follower.
|
||||
service.import_remote_outbox(outbox_url, actor_url).await?;
|
||||
```
|
||||
|
||||
## Actor lookup
|
||||
@@ -178,38 +212,59 @@ service.deliver_to_inbox(inbox, activity_json, signing_actor_id).await?;
|
||||
let actor: LookedUpActor = service.lookup_actor_by_handle("@user@remote.example").await?;
|
||||
```
|
||||
|
||||
## Pinned posts (featured collection)
|
||||
|
||||
Override `get_featured_objects` in your `ApContentReader` to expose pinned posts.
|
||||
The library serves them at `GET /users/{id}/featured` automatically. Default is empty.
|
||||
|
||||
```rust
|
||||
impl ApContentReader for MyDb {
|
||||
async fn get_featured_objects(&self, user_id: uuid::Uuid) -> anyhow::Result<Vec<Url>> {
|
||||
Ok(self.get_pinned_post_urls(user_id).await?)
|
||||
}
|
||||
// ...other methods
|
||||
}
|
||||
```
|
||||
|
||||
Set `featured_url` in `ApUser` to point to the endpoint — the library includes it in actor JSON:
|
||||
|
||||
```rust
|
||||
ApUser {
|
||||
featured_url: Some("https://example.com/users/{id}/featured".parse()?),
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Inbound activity handling
|
||||
|
||||
The library handles the following inbound AP activities out of the box:
|
||||
Handled out of the box:
|
||||
|
||||
`Follow`, `Accept`, `Reject`, `Undo` (Follow, Like, Announce, Add), `Create`, `Update`, `Delete`, `Announce`, `Like`, `Add`, `Block`, `Move`
|
||||
`Follow`, `Accept`, `Reject`, `Undo` (Follow, Like, Announce, Add, Block), `Create`, `Update`, `Delete`, `Announce`, `Like`, `Add`, `Block`, `Move`
|
||||
|
||||
All activities are deduplicated by activity `id` — safe against retried deliveries.
|
||||
|
||||
Mentions are extracted from `tag` arrays in `Create`/`Update` and dispatched via `ApObjectHandler::on_mention`.
|
||||
|
||||
`Move` is fully handled: verifies `alsoKnownAs` cross-reference on the target, migrates all local follower records, and re-follows the new actor on behalf of affected users.
|
||||
|
||||
Actor types accepted on inbound: `Person`, `Service`, `Application`, `Organization`, `Group`.
|
||||
- All activities are deduplicated by `id` — safe against retried deliveries.
|
||||
- Mentions are extracted from `tag` arrays and dispatched via `ApObjectHandler::on_mention`.
|
||||
- `Undo(Announce)` removes the boost record from `ActorRepository` and calls `on_announce_removed`.
|
||||
- `Move` verifies all `alsoKnownAs` aliases on the target, migrates follower records, and re-follows in a background task (non-blocking).
|
||||
- Actor types accepted: `Person`, `Service`, `Application`, `Organization`, `Group`.
|
||||
|
||||
## Key public types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `ActivityPubService` | Central service — build once, share via `Arc` |
|
||||
| `ActivityRepository` | Trait: activity deduplication |
|
||||
| `FollowRepository` | Trait: follower/following graph + migration |
|
||||
| `ActorRepository` | Trait: keypairs, remote actor cache, announce tracking |
|
||||
| `BlocklistRepository` | Trait: domain and actor blocklists |
|
||||
| `ApUserRepository` | Trait: user lookup |
|
||||
| `ApContentReader` | Trait: provides local content for outbox/backfill |
|
||||
| `ApObjectHandler` | Trait: dispatches inbound AP activities |
|
||||
| `ActivityRepository` | Trait: activity ID deduplication (2 methods) |
|
||||
| `FollowRepository` | Trait: follower/following graph + migration (18 methods) |
|
||||
| `ActorRepository` | Trait: keypairs, remote actor cache, announce tracking (6 methods) |
|
||||
| `BlocklistRepository` | Trait: domain and actor blocklists (8 methods) |
|
||||
| `ApUserRepository` | Trait: user lookup (3 methods) |
|
||||
| `ApContentReader` | Trait: outbox/backfill/featured content (3 methods, 1 with default) |
|
||||
| `ApObjectHandler` | Trait: inbound activity callbacks (9 methods, 2 with defaults) |
|
||||
| `ApVisibility` | `Public` / `FollowersOnly` / `Private` |
|
||||
| `ApActorType` | `Person` / `Service` / `Application` / `Organization` / `Group` |
|
||||
| `FederationEvent` | `DeliveryRequested` / `DeliveryFailed` — for job queue integration |
|
||||
| `EventPublisher` | Trait: hook for async delivery via external queue |
|
||||
| `FederationEvent` | `DeliveryRequested` / `DeliveryFailed` / `BackfillRequested` |
|
||||
| `EventPublisher` | Trait: hook for job queue integration |
|
||||
| `LookedUpActor` | Resolved remote actor from `lookup_actor_by_handle` |
|
||||
| `RemoteActor` | A cached federated actor record |
|
||||
| `RemoteActor` | Cached federated actor record |
|
||||
| `Follower` / `FollowerStatus` | Follower with `Pending`/`Accepted`/`Rejected` state |
|
||||
| `ApUser` | AP-serializable local user |
|
||||
| `ApFederationConfig` | Wraps the `activitypub_federation` config |
|
||||
|
||||
Reference in New Issue
Block a user