5 Commits

Author SHA1 Message Date
62c9bf2e4e fix: add missing RemoteActor fields in get_blocked_actors fallback 2026-05-29 04:04:08 +02:00
485c407edb feat(RemoteActor): add bio, banner_url, followers_url, following_url, also_known_as fields
Bump to 0.3.1. These fields are available on DbActor at follow/ingest
time but were discarded when constructing RemoteActor. Now populated
in from_json and follow(), so consuming repos can store and return
rich actor profiles without extra queries.
2026-05-29 04:03:23 +02:00
fad95f0550 docs: update README with Gitea registry installation 2026-05-29 03:36:22 +02:00
c1c8a37d0d 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
2026-05-29 03:28:17 +02:00
757c6d14ec fix 2026-05-29 03:20:21 +02:00
8 changed files with 105 additions and 19 deletions

View File

@@ -1,6 +1,24 @@
# Changelog
## [0.3.0] — unreleased
## [0.3.1] — 2026-05-29
### Breaking changes
**`RemoteActor` has five new required fields** — struct literals must include them:
| Field | Type | Description |
|-------|------|-------------|
| `bio` | `Option<String>` | Actor biography/summary |
| `banner_url` | `Option<String>` | Banner/header image URL |
| `followers_url` | `Option<String>` | AP followers collection URL |
| `following_url` | `Option<String>` | AP following collection URL |
| `also_known_as` | `Vec<String>` | Account aliases (for Move verification) |
These are populated automatically when k-ap fetches a remote actor (via `from_json`) and when the local `follow()` method constructs a `RemoteActor` from the fetched `DbActor`. Consuming applications only need to add the new fields to their `upsert_remote_actor` / `get_remote_actor` SQL and any custom `RemoteActor` construction sites.
---
## [0.3.0] — 2026-05-28
### Breaking changes
@@ -86,6 +104,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.

2
Cargo.lock generated
View File

@@ -1368,7 +1368,7 @@ dependencies = [
[[package]]
name = "k-ap"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"activitypub_federation",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "k-ap"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
description = "Generic ActivityPub protocol layer"
license = "MIT"

View File

@@ -8,6 +8,22 @@ Not domain-specific — no opinions about what your content type looks like.
## Add as dependency
Via the private Gitea registry (recommended):
```toml
[dependencies]
k-ap = { version = "0.3.0", registry = "gitea" }
```
Configure the registry in `.cargo/config.toml`:
```toml
[registries.gitea]
index = "sparse+https://git.gabrielkaszewski.dev/api/packages/GKaszewski/cargo/"
```
Or via git if you don't have registry access:
```toml
[dependencies]
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.3.0" }
@@ -67,19 +83,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<String>, 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:

View File

@@ -355,6 +355,11 @@ impl Object for DbActor {
display_name: json.name.clone(),
avatar_url: json.icon.as_ref().map(|i| i.url.to_string()),
outbox_url: json.outbox.as_ref().map(|u| u.to_string()),
bio: json.summary.clone(),
banner_url: json.image.as_ref().map(|i| i.url.to_string()),
followers_url: json.followers.as_ref().map(|u| u.to_string()),
following_url: json.following.as_ref().map(|u| u.to_string()),
also_known_as: json.also_known_as.clone(),
};
data.actor_repo.upsert_remote_actor(actor).await?;

View File

@@ -30,6 +30,11 @@ pub struct RemoteActor {
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub outbox_url: Option<String>,
pub bio: Option<String>,
pub banner_url: Option<String>,
pub followers_url: Option<String>,
pub following_url: Option<String>,
pub also_known_as: Vec<String>,
}
#[derive(Debug, Clone)]

View File

@@ -37,9 +37,14 @@ impl ActivityPubService {
.shared_inbox_url
.as_ref()
.map(|u| u.to_string()),
display_name: Some(remote_actor.username.clone()),
display_name: remote_actor.display_name.clone().or_else(|| Some(remote_actor.username.clone())),
avatar_url: remote_actor.avatar_url.as_ref().map(|u| u.to_string()),
outbox_url: Some(remote_actor.outbox_url.to_string()),
bio: remote_actor.bio.clone(),
banner_url: remote_actor.banner_url.as_ref().map(|u| u.to_string()),
followers_url: Some(remote_actor.followers_url.to_string()),
following_url: Some(remote_actor.following_url.to_string()),
also_known_as: remote_actor.also_known_as.clone(),
};
// Save BEFORE delivering — prevents lost state on process restart.
data.follow_repo
@@ -343,6 +348,11 @@ impl ActivityPubService {
display_name: None,
avatar_url: None,
outbox_url: None,
bio: None,
banner_url: None,
followers_url: None,
following_url: None,
also_known_as: vec![],
},
};
actors.push(actor);
@@ -382,9 +392,14 @@ impl ActivityPubService {
handle: format!("{}@{}", target.username, data.domain),
inbox_url: format!("{}/inbox", target_actor_url),
shared_inbox_url: None,
display_name: Some(target.username),
avatar_url: None,
outbox_url: None,
display_name: target.display_name.or(Some(target.username)),
avatar_url: target.avatar_url.as_ref().map(|u| u.to_string()),
outbox_url: Some(format!("{}/outbox", target_actor_url)),
bio: target.bio,
banner_url: target.banner_url.as_ref().map(|u| u.to_string()),
followers_url: Some(format!("{}/followers", target_actor_url)),
following_url: Some(format!("{}/following", target_actor_url)),
also_known_as: target.also_known_as,
};
data.follow_repo
.add_following(local_user_id, target_as_remote, &follow_id)

View File

@@ -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,7 +188,19 @@ impl ActivityPubService {
&self.base_url
}
/// Returns the ActivityPub router. Inbox routes enforce a 1 MB body limit.
/// Returns the ActivityPub router.
///
/// 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<S>(&self) -> Router<S>
where
S: Clone + Send + Sync + 'static,
@@ -203,14 +213,11 @@ impl ActivityPubService {
"/inbox",
post(inbox_handler).layer(DefaultBodyLimit::max(1024 * 1024)),
)
.route("/users/{id}", get(actor_handler))
.route(
"/users/{id}/inbox",
post(inbox_handler).layer(DefaultBodyLimit::max(1024 * 1024)),
)
.route("/users/{id}/outbox", get(outbox_handler))
.route("/users/{id}/followers", get(followers_handler))
.route("/users/{id}/following", get(following_handler))
.route("/users/{id}/featured", get(featured_handler))
.layer(self.federation_config.middleware())
}