docs: update README to v0.3.0; add docs/ to .gitignore
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
docs/
|
||||||
|
|||||||
179
README.md
179
README.md
@@ -10,39 +10,56 @@ Not domain-specific — no opinions about what your content type looks like.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[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
|
## 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
|
```rust
|
||||||
// Your database layer for follows, keypairs, remote actors, blocks
|
// Activity deduplication — idempotency for inbound deliveries
|
||||||
impl FederationRepository for MyFederationRepo { ... }
|
impl ActivityRepository for MyDb { ... }
|
||||||
|
|
||||||
// Your user lookup (id, username, bio, avatar, alsoKnownAs)
|
// Follower / following graph + account migration
|
||||||
impl ApUserRepository for MyUserRepo { ... }
|
impl FollowRepository for MyDb { ... }
|
||||||
|
|
||||||
// Dispatch incoming AP objects to the right handler
|
// Local keypairs, remote actor cache, boost (Announce) tracking
|
||||||
impl ApObjectHandler for MyObjectHandler { ... }
|
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
|
## Wire up the service
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use k_ap::{ActivityPubService, FederationRepository, ApUserRepository, ApObjectHandler};
|
use std::sync::Arc;
|
||||||
|
use k_ap::ActivityPubService;
|
||||||
|
|
||||||
let service = ActivityPubService::builder(
|
let db = Arc::new(MyDb::new(...));
|
||||||
Arc::new(my_federation_repo),
|
|
||||||
Arc::new(my_user_repo),
|
let service = ActivityPubService::builder("https://example.com")
|
||||||
Arc::new(my_object_handler),
|
.activity_repo(db.clone())
|
||||||
"https://example.com",
|
.follow_repo(db.clone())
|
||||||
)
|
.actor_repo(db.clone())
|
||||||
.allow_registration(true)
|
.blocklist_repo(db.clone())
|
||||||
.software_name("my-app")
|
.user_repo(db.clone())
|
||||||
.build()
|
.content_reader(db.clone())
|
||||||
.await?;
|
.object_handler(db.clone())
|
||||||
|
.allow_registration(false)
|
||||||
|
.software_name("my-app")
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Mount the AP routes onto your axum router
|
// Mount the AP routes onto your axum router
|
||||||
let router = Router::new().merge(service.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
|
## What the service handles for you
|
||||||
|
|
||||||
- **Actor** — `GET /users/:id` serves the AP Person object with public key
|
- **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`
|
- **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 OrderedCollection pagination
|
- **Outbox** — `GET /users/:id/outbox` with cursor-based `OrderedCollection` pagination
|
||||||
- **Followers / Following** — `GET /users/:id/followers` and `/following`
|
- **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`
|
- **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
|
```rust
|
||||||
// Fan out a new note to all accepted followers
|
ApUser {
|
||||||
service.broadcast_create_note(user_id, note_json).await?;
|
id: uuid,
|
||||||
service.broadcast_update_note(user_id, note_json).await?;
|
username: String,
|
||||||
|
display_name: Option<String>,
|
||||||
|
bio: Option<String>,
|
||||||
|
avatar_url: Option<Url>,
|
||||||
|
banner_url: Option<Url>,
|
||||||
|
also_known_as: Option<String>, // for account migration
|
||||||
|
profile_url: Option<Url>,
|
||||||
|
featured_url: Option<Url>, // pinned posts collection
|
||||||
|
attachment: Vec<ApProfileField>, // 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?;
|
service.broadcast_delete_to_followers(user_id, ap_id).await?;
|
||||||
|
|
||||||
// Announce / Undo Announce
|
// Announce / Undo Announce
|
||||||
@@ -84,7 +132,7 @@ service.broadcast_move(user_id, new_actor_url).await?;
|
|||||||
## Follow management
|
## Follow management
|
||||||
|
|
||||||
```rust
|
```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.follow(local_user_id, "@user@remote.example").await?;
|
||||||
service.unfollow(local_user_id, remote_actor_url).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?;
|
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
|
## Actor lookup
|
||||||
|
|
||||||
```rust
|
```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?;
|
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<Thought>`). 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
|
## Key public types
|
||||||
|
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `ActivityPubService` | Central service — build once, share via `Arc` |
|
| `ActivityPubService` | Central service — build once, share via `Arc` |
|
||||||
| `FederationData` | Request-scoped data passed through the federation layer |
|
| `ActivityRepository` | Trait: activity deduplication |
|
||||||
| `FederationRepository` | Trait: follows, keypairs, remote actors, blocks |
|
| `FollowRepository` | Trait: follower/following graph + migration |
|
||||||
| `ApUserRepository` | Trait: user lookup by id / username |
|
| `ActorRepository` | Trait: keypairs, remote actor cache, announce tracking |
|
||||||
| `ApObjectHandler` | Trait: dispatch incoming AP objects |
|
| `BlocklistRepository` | Trait: domain and actor blocklists |
|
||||||
| `LookedUpActor` | Resolved remote actor data from `lookup_actor_by_handle` |
|
| `ApUserRepository` | Trait: user lookup |
|
||||||
| `RemoteActor` | A federated actor record |
|
| `ApContentReader` | Trait: provides local content for outbox/backfill |
|
||||||
| `Follower` / `FollowerStatus` | Follower with pending/accepted/rejected state |
|
| `ApObjectHandler` | Trait: dispatches inbound AP activities |
|
||||||
| `ApUser` | AP-serializable local user (includes `also_known_as`) |
|
| `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 |
|
| `ApFederationConfig` | Wraps the `activitypub_federation` config |
|
||||||
| `Error` | AP-layer error type |
|
| `Error` | AP-layer error type |
|
||||||
|
|
||||||
## Inbound activity handling
|
## Local development
|
||||||
|
|
||||||
The library handles the following inbound AP activities out of the box:
|
```bash
|
||||||
|
make check # fmt --check + clippy -D warnings + tests (use before committing)
|
||||||
`Follow`, `Accept`, `Reject`, `Undo` (Follow, Like, Announce), `Create`, `Update`, `Delete`, `Announce`, `Like`, `Add`, `Block`, `Move`
|
make fmt # apply rustfmt
|
||||||
|
make fix # fmt + clippy --fix
|
||||||
`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`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user