From c1c8a37d0d7111926d4dd92f9874d2551334b8bc Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 29 May 2026 03:28:17 +0200 Subject: [PATCH] fix: remove actor/followers/following routes from router() These paths need content negotiation in real apps (AP JSON vs UI JSON). k-ap can't serve the UI half, so the consuming app owns the route and calls actor_json/followers_collection_json/following_collection_json to produce the AP response. The route conflict caused a panic when thoughts mounted its own /users/{username}/... routes alongside service.router(). router() now registers only what k-ap fully owns: - POST /inbox, POST /users/{id}/inbox (signature verification) - GET /users/{id}/outbox - GET /users/{id}/featured - GET /.well-known/webfinger, nodeinfo, /nodeinfo/2.0 --- CHANGELOG.md | 2 ++ README.md | 30 ++++++++++++++++++++++++------ src/service/mod.rs | 22 ++++++++++++---------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82a86d..7bdb06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ service.broadcast_create_note(user_id, note_json, ApVisibility::Public, mentione **`GET /users/{id}/featured` route** — serves an `OrderedCollection` of pinned posts via `ApContentReader::get_featured_objects`. Default is an empty collection. +**`router()` no longer registers `GET /users/{id}`, `/followers`, or `/following`** — these paths need content negotiation (AP JSON vs UI JSON) which k-ap can't do. Your application owns those routes and calls `actor_json`, `followers_collection_json`, `following_collection_json` to produce the AP response. See README for the pattern. + **`FederationEvent::BackfillRequested`** — when an `EventPublisher` is configured, `accept_follower` publishes this event instead of spawning an in-process task. Process it by calling `run_backfill_for_follower`. **`run_backfill_for_follower(owner_user_id, follower_inbox_url)`** — public method for workers processing `BackfillRequested` events. diff --git a/README.md b/README.md index 1369ba5..c3d1ddc 100644 --- a/README.md +++ b/README.md @@ -67,19 +67,37 @@ let router = Router::new().merge(service.router()); ## What the service handles for you +`service.router()` registers only the routes k-ap fully owns: + | 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 | +| `POST /inbox` | Shared inbox — HTTP signature verification + dispatch, 1 MB limit | +| `POST /users/{id}/inbox` | Per-user inbox — same | | `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 /users/{id}/featured` | Pinned posts `OrderedCollection` | | `GET /.well-known/webfinger` | JRD with `aliases` field | | `GET /.well-known/nodeinfo` | NodeInfo well-known redirect | | `GET /nodeinfo/2.0` | NodeInfo 2.0 | +**Not registered by `router()`:** `GET /users/{id}`, `GET /users/{id}/followers`, `GET /users/{id}/following`. + +These paths are dual-purpose in real applications — they must serve both AP JSON (`application/activity+json`) and the app's own UI JSON (content negotiation). k-ap can't do the UI half, so your application owns the route and calls k-ap's helper methods to produce the AP response: + +```rust +// In your axum actor handler — serve AP JSON or UI JSON based on Accept header +async fn actor_handler(Path(username): Path, headers: HeaderMap, ...) { + if wants_ap_json(&headers) { + let json = service.actor_json(&user.id.to_string()).await?; + return ap_json_response(json); + } + // ... serve UI response +} + +// Similarly for followers/following: +let json = service.followers_collection_json(user_id, page).await?; +let json = service.following_collection_json(user_id, page).await?; +``` + ## ApUser fields Your `ApUserRepository` returns `ApUser`. All fields control how the actor is serialized: diff --git a/src/service/mod.rs b/src/service/mod.rs index b8beffa..c75b4be 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -5,13 +5,11 @@ use axum::{Router, extract::DefaultBodyLimit, routing::get, routing::post}; use url::Url; use crate::{ - actor_handler::actor_handler, actors::{DbActor, get_local_actor}, content::{ApContentReader, ApObjectHandler}, data::FederationData, featured_handler::featured_handler, federation::ApFederationConfig, - followers_handler::{followers_handler, following_handler}, inbox::inbox_handler, nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler}, outbox::outbox_handler, @@ -190,15 +188,19 @@ impl ActivityPubService { &self.base_url } - /// Returns the ActivityPub router. Inbox routes enforce a 1 MB body limit. - /// Returns the ActivityPub router. Inbox routes enforce a 1 MB body limit. + /// Returns the ActivityPub router. /// - /// Does NOT register `GET /users/{id}`, `GET /users/{id}/followers`, - /// `GET /users/{id}/following`, or `GET /users/{id}/featured` — consuming - /// applications typically own those paths (often behind content negotiation) - /// and should wire the AP response themselves by calling `actor_json`, - /// `followers_collection_json`, `following_collection_json`, and - /// `get_featured_objects` from their own handlers. + /// Registers only routes that k-ap fully owns: + /// - `POST /inbox` + `POST /users/{id}/inbox` — signature verification + dispatch (1 MB limit) + /// - `GET /users/{id}/outbox` — cursor-paginated OrderedCollection + /// - `GET /users/{id}/featured` — pinned posts OrderedCollection + /// - `GET /.well-known/webfinger`, `GET /.well-known/nodeinfo`, `GET /nodeinfo/2.0` + /// + /// **Not registered:** `GET /users/{id}`, `GET /users/{id}/followers`, + /// `GET /users/{id}/following`. Real applications need those paths to serve + /// both AP JSON and their own UI JSON (content negotiation), so they must own + /// the route. Call `actor_json`, `followers_collection_json`, and + /// `following_collection_json` from your own handler to produce the AP response. pub fn router(&self) -> Router where S: Clone + Send + Sync + 'static,