c768b12c05b0fc8c40b3fec6daf3c6b5cda7d097 / frogue / src / activitypub / mod.rs
Raw
//! ActivityPub federation for users and repositories.
//!
//! Each user gets an ActivityPub actor at `/{base}/~{user}`. Notes can be
//! published to the outbox and delivered to followers. Reactions (Like,
//! Announce, EmojiReact) are accepted on local notes.
//!
//! Federation is gated by `FROGUE_PUBLIC_URL`: when empty, all AP endpoints
//! return 404, no delivery worker runs, and no handle is shown in the WebUI.
//! The server still works as a standalone forge.
use url::Url;
use crate::AppState;
pub mod actor;
pub mod delivery;
pub mod feed;
pub mod following;
pub mod forge;
pub mod inbox;
pub mod keys;
pub mod notes;
pub mod outbox;
pub mod remote;
pub mod signatures;
pub mod types;
pub mod webfinger;
/// Returns true if federation is enabled (public_url is non-empty).
pub fn enabled(state: &AppState) -> bool {
!state.public_url().is_empty()
}
/// URL builders for ActivityPub endpoints.
///
/// Constructed from `state.public_url()` once per handler; every URL in every
/// AP document comes from here, never hand-formatted elsewhere.
pub struct ApUrls {
base: String,
}
impl ApUrls {
/// Build from the public URL (trailing slash already stripped by AppState).
pub fn new(public_url: &str) -> Self {
Self {
base: public_url.to_string(),
}
}
/// Actor ID: `{base}/~{user}`.
pub fn actor(&self, user: &str) -> String {
format!("{}/~{}", self.base, user)
}
/// Key ID: `{base}/~{user}#main-key`.
pub fn key_id(&self, user: &str) -> String {
format!("{}#main-key", self.actor(user))
}
/// Inbox: `{base}/~{user}/inbox`.
pub fn inbox(&self, user: &str) -> String {
format!("{}/inbox", self.actor(user))
}
/// Outbox: `{base}/~{user}/outbox`.
pub fn outbox(&self, user: &str) -> String {
format!("{}/outbox", self.actor(user))
}
/// Followers: `{base}/~{user}/followers`.
pub fn followers(&self, user: &str) -> String {
format!("{}/followers", self.actor(user))
}
/// Note: `{base}/~{user}/notes/{uuid}`.
pub fn note(&self, user: &str, uuid: uuid::Uuid) -> String {
format!("{}/notes/{}", self.actor(user), uuid)
}
/// Activity: `{base}/~{user}/activities/{uuid}`.
pub fn activity(&self, user: &str, uuid: uuid::Uuid) -> String {
format!("{}/activities/{}", self.actor(user), uuid)
}
/// Repository actor ID: `{base}/~{owner}/{slug}`.
pub fn repo_actor(&self, owner: &str, slug: &str) -> String {
format!("{}/~{}/{}", self.base, owner, slug)
}
/// Repository key ID: `{base}/~{owner}/{slug}#main-key`.
pub fn repo_key_id(&self, owner: &str, slug: &str) -> String {
format!("{}#main-key", self.repo_actor(owner, slug))
}
/// Repository inbox: `{base}/~{owner}/{slug}/inbox`.
pub fn repo_inbox(&self, owner: &str, slug: &str) -> String {
format!("{}/inbox", self.repo_actor(owner, slug))
}
/// Repository outbox: `{base}/~{owner}/{slug}/outbox`.
pub fn repo_outbox(&self, owner: &str, slug: &str) -> String {
format!("{}/outbox", self.repo_actor(owner, slug))
}
/// Repository followers: `{base}/~{owner}/{slug}/followers`.
pub fn repo_followers(&self, owner: &str, slug: &str) -> String {
format!("{}/followers", self.repo_actor(owner, slug))
}
/// Repository activity: `{base}/~{owner}/{slug}/activities/{uuid}`.
pub fn repo_activity(&self, owner: &str, slug: &str, uuid: uuid::Uuid) -> String {
format!("{}/activities/{}", self.repo_actor(owner, slug), uuid)
}
/// Repository commit document: `{base}/~{owner}/{slug}/commits/{hash}`.
pub fn repo_commit(&self, owner: &str, slug: &str, hash: &str) -> String {
format!("{}/commits/{}", self.repo_actor(owner, slug), hash)
}
/// Repository HTTP clone URI: `{base}/{owner}/{slug}.git`.
pub fn repo_clone_uri(&self, owner: &str, slug: &str) -> String {
format!("{}/{}/{}.git", self.base, owner, slug)
}
/// Repository push URIs: HTTP clone URL + SSH URL.
/// The SSH advertise address comes from SSH settings, not public_url.
pub fn repo_push_uris(&self, owner: &str, slug: &str, ssh_host: &str) -> Vec<String> {
vec![
self.repo_clone_uri(owner, slug),
format!("ssh://git@{}/{}/{}", ssh_host, owner, slug),
]
}
/// Ticket: `{base}/~{owner}/{slug}/tickets/{number}`.
pub fn repo_ticket(&self, owner: &str, slug: &str, number: i64) -> String {
format!("{}/tickets/{}", self.repo_actor(owner, slug), number)
}
/// Patch: `{base}/~{owner}/{slug}/patches/{number}`.
pub fn repo_patch(&self, owner: &str, slug: &str, number: i64) -> String {
format!("{}/patches/{}", self.repo_actor(owner, slug), number)
}
/// Ticket comment: `{base}/~{owner}/{slug}/tickets/{number}/comments/{uuid}`.
pub fn repo_ticket_comment(
&self,
owner: &str,
slug: &str,
number: i64,
uuid: uuid::Uuid,
) -> String {
format!(
"{}/comments/{}",
self.repo_ticket(owner, slug, number),
uuid
)
}
/// Patch comment: `{base}/~{owner}/{slug}/patches/{number}/comments/{uuid}`.
pub fn repo_patch_comment(
&self,
owner: &str,
slug: &str,
number: i64,
uuid: uuid::Uuid,
) -> String {
format!("{}/comments/{}", self.repo_patch(owner, slug, number), uuid)
}
/// Ticket replies: `{base}/~{owner}/{slug}/tickets/{number}/replies`.
pub fn repo_ticket_replies(&self, owner: &str, slug: &str, number: i64) -> String {
format!("{}/replies", self.repo_ticket(owner, slug, number))
}
/// Patch replies: `{base}/~{owner}/{slug}/patches/{number}/replies`.
pub fn repo_patch_replies(&self, owner: &str, slug: &str, number: i64) -> String {
format!("{}/replies", self.repo_patch(owner, slug, number))
}
/// Host (authority) of the public URL, for webfinger.
pub fn host(&self) -> String {
Url::parse(&self.base)
.ok()
.and_then(|u| {
u.host_str().map(|h| {
if let Some(port) = u.port() {
format!("{}:{}", h, port)
} else {
h.to_string()
}
})
})
.unwrap_or_default()
}
/// The base URL itself.
pub fn base(&self) -> &str {
&self.base
}
}