diff --git a/.gitignore b/.gitignore index 96ef6c0..0bf37c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +docs/ diff --git a/README.md b/README.md index 142af17..08b90ea 100644 --- a/README.md +++ b/README.md @@ -10,39 +10,56 @@ Not domain-specific — no opinions about what your content type looks like. ```toml [dependencies] -k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.10" } +k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.3.0" } ``` ## What you implement -Three traits wire your data layer into `k-ap`: +Seven focused traits wire your data layer into `k-ap`. You can implement them all on a single database struct by cloning the `Arc`. ```rust -// Your database layer for follows, keypairs, remote actors, blocks -impl FederationRepository for MyFederationRepo { ... } +// Activity deduplication — idempotency for inbound deliveries +impl ActivityRepository for MyDb { ... } -// Your user lookup (id, username, bio, avatar, alsoKnownAs) -impl ApUserRepository for MyUserRepo { ... } +// Follower / following graph + account migration +impl FollowRepository for MyDb { ... } -// Dispatch incoming AP objects to the right handler -impl ApObjectHandler for MyObjectHandler { ... } +// Local keypairs, remote actor cache, boost (Announce) tracking +impl ActorRepository for MyDb { ... } + +// Domain and per-user actor blocklists +impl BlocklistRepository for MyDb { ... } + +// User lookup by id / username +impl ApUserRepository for MyDb { ... } + +// Read side — provides local content to the library (outbox, backfill) +impl ApContentReader for MyDb { ... } + +// Write side — called when the inbox receives AP activities +impl ApObjectHandler for MyDb { ... } ``` ## Wire up the service ```rust -use k_ap::{ActivityPubService, FederationRepository, ApUserRepository, ApObjectHandler}; +use std::sync::Arc; +use k_ap::ActivityPubService; -let service = ActivityPubService::builder( - Arc::new(my_federation_repo), - Arc::new(my_user_repo), - Arc::new(my_object_handler), - "https://example.com", -) -.allow_registration(true) -.software_name("my-app") -.build() -.await?; +let db = Arc::new(MyDb::new(...)); + +let service = ActivityPubService::builder("https://example.com") + .activity_repo(db.clone()) + .follow_repo(db.clone()) + .actor_repo(db.clone()) + .blocklist_repo(db.clone()) + .user_repo(db.clone()) + .content_reader(db.clone()) + .object_handler(db.clone()) + .allow_registration(false) + .software_name("my-app") + .build() + .await?; // Mount the AP routes onto your axum router let router = Router::new().merge(service.router()); @@ -50,19 +67,50 @@ 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 -- **Inbox** — `POST /users/:id/inbox` + `POST /inbox` (shared), verifies HTTP signatures, dispatches to your `ApObjectHandler` -- **Outbox** — `GET /users/:id/outbox` with OrderedCollection pagination +- **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` +- **WebFinger** — `GET /.well-known/webfinger` with `aliases` field - **NodeInfo** — `GET /.well-known/nodeinfo` + `GET /nodeinfo/2.0` -## Broadcast from your domain layer +## ApUser fields + +Your `ApUserRepository` returns `ApUser`. All fields control how the actor is serialized: ```rust -// Fan out a new note to all accepted followers -service.broadcast_create_note(user_id, note_json).await?; -service.broadcast_update_note(user_id, note_json).await?; +ApUser { + id: uuid, + username: String, + display_name: Option, + bio: Option, + avatar_url: Option, + banner_url: Option, + also_known_as: Option, // for account migration + profile_url: Option, + featured_url: Option, // pinned posts collection + attachment: Vec, // profile metadata fields + manually_approves_followers: bool, // controls manuallyApprovesFollowers in AP JSON + discoverable: bool, // controls discoverable in AP JSON + actor_type: ApActorType, // Person / Service / Application / Organization / Group +} +``` + +## Broadcast with visibility + +```rust +use k_ap::ApVisibility; + +// Public — to: [AS_PUBLIC], cc: [followers] +service.broadcast_create_note(user_id, note_json, ApVisibility::Public).await?; + +// Followers only — to: [followers], cc: [] +service.broadcast_create_note(user_id, note_json, ApVisibility::FollowersOnly).await?; + +// Private — no delivery, library returns immediately +service.broadcast_create_note(user_id, note_json, ApVisibility::Private).await?; + +service.broadcast_update_note(user_id, note_json, ApVisibility::Public).await?; service.broadcast_delete_to_followers(user_id, ap_id).await?; // Announce / Undo Announce @@ -84,7 +132,7 @@ service.broadcast_move(user_id, new_actor_url).await?; ## Follow management ```rust -// Outbound follows (resolves handle via WebFinger) +// Outbound follows (resolves handle via signed WebFinger request) service.follow(local_user_id, "@user@remote.example").await?; service.unfollow(local_user_id, remote_actor_url).await?; @@ -98,6 +146,30 @@ service.mark_follower_accepted(local_user_id, remote_actor_url).await?; service.mark_follower_rejected(local_user_id, remote_actor_url).await?; ``` +## Async delivery via EventPublisher + +By default, outbound activities are sent via `tokio::spawn` (fire-and-forget). Implement `EventPublisher` to route deliveries through your job queue instead: + +```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?; + } + FederationEvent::DeliveryFailed { inbox, error, .. } => { + // Log or alert on permanent failure + } + } + Ok(()) + } +} + +// When your worker processes the queue item: +service.deliver_to_inbox(inbox, activity_json, signing_actor_id).await?; +``` + ## Actor lookup ```rust @@ -106,32 +178,47 @@ service.mark_follower_rejected(local_user_id, remote_actor_url).await?; let actor: LookedUpActor = service.lookup_actor_by_handle("@user@remote.example").await?; ``` -## Project-specific ports +## Inbound activity handling -`k-ap` does not define port traits tied to your domain (e.g. `OutboundFederationPort`, `ActivityPubRepository`). Those belong in your adapter layer and are wired up there. See `crates/adapters/activitypub/src/port.rs` in `thoughts` for a reference implementation. +The library handles the following inbound AP activities out of the box: + +`Follow`, `Accept`, `Reject`, `Undo` (Follow, Like, Announce, Add), `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`. ## Key public types | Type | Description | |------|-------------| | `ActivityPubService` | Central service — build once, share via `Arc` | -| `FederationData` | Request-scoped data passed through the federation layer | -| `FederationRepository` | Trait: follows, keypairs, remote actors, blocks | -| `ApUserRepository` | Trait: user lookup by id / username | -| `ApObjectHandler` | Trait: dispatch incoming AP objects | -| `LookedUpActor` | Resolved remote actor data from `lookup_actor_by_handle` | -| `RemoteActor` | A federated actor record | -| `Follower` / `FollowerStatus` | Follower with pending/accepted/rejected state | -| `ApUser` | AP-serializable local user (includes `also_known_as`) | +| `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 | +| `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 | +| `LookedUpActor` | Resolved remote actor from `lookup_actor_by_handle` | +| `RemoteActor` | A cached federated actor record | +| `Follower` / `FollowerStatus` | Follower with `Pending`/`Accepted`/`Rejected` state | +| `ApUser` | AP-serializable local user | | `ApFederationConfig` | Wraps the `activitypub_federation` config | | `Error` | AP-layer error type | -## Inbound activity handling +## Local development -The library handles the following inbound AP activities out of the box: - -`Follow`, `Accept`, `Reject`, `Undo` (Follow, Like, Announce), `Create`, `Update`, `Delete`, `Announce`, `Like`, `Add`, `Block`, `Move` - -`Move` is fully handled: verifies `alsoKnownAs` cross-reference on the target actor, migrates all local following records, and re-follows the new actor on behalf of affected users. - -Actor types accepted: `Person`, `Service`, `Application`, `Organization`, `Group`. +```bash +make check # fmt --check + clippy -D warnings + tests (use before committing) +make fmt # apply rustfmt +make fix # fmt + clippy --fix +```