// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! Instance actor endpoints.
//!
//! The instance actor is a singleton `Application` actor at `/actor` used for
//! admin-level federation activities (signing Blocks, receiving forwarded
//! Flags).
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, web};
use serde_json::json;
use tokio::sync::RwLock;
use tracing::error;
use crate::activitypub::{self, ApUrls};
use crate::{AppState, state_read};
/// GET /actor with ActivityPub Accept header
///
/// Serves the instance actor as an `Application` type. Returns 404 for browsers
/// (non-AP requests) to avoid confusion with a regular web page.
#[tracing::instrument(skip_all)]
pub async fn get_instance_actor(data: web::Data<Arc<RwLock<AppState>>>) -> HttpResponse {
let state = state_read(&data).await;
if !activitypub::enabled(&state) {
return HttpResponse::NotFound().finish();
}
// Fetch or create the instance actor (keypair generated lazily).
let actor = match activitypub::actor::get_or_create_instance(&state.db).await {
Ok(actor) => actor,
Err(err) => {
error!(%err, "fetching or creating instance actor failed");
return HttpResponse::InternalServerError().finish();
}
};
let urls = ApUrls::new(state.public_url());
let domain = urls
.base()
.strip_prefix("http://")
.or_else(|| urls.base().strip_prefix("https://"))
.unwrap_or(urls.base());
// Build the Application actor document
let actor_doc = json!({
"@context": activitypub::types::CONTEXT,
"id": urls.instance_actor(),
"type": "Application",
"preferredUsername": domain,
"inbox": urls.instance_actor_inbox(),
"outbox": urls.instance_actor_outbox(),
"publicKey": {
"id": urls.instance_actor_key_id(),
"owner": urls.instance_actor(),
"publicKeyPem": actor.public_key_pem,
}
});
HttpResponse::Ok()
.content_type("application/activity+json; charset=utf-8")
.json(actor_doc)
}
/// POST /actor/inbox
///
/// Receives activities addressed to the instance actor. Processes Flag
/// activities for moderation reports and accepts other activities with 202.
#[tracing::instrument(skip_all)]
pub async fn post_instance_actor_inbox(
data: web::Data<Arc<RwLock<AppState>>>,
req: HttpRequest,
body: web::Bytes,
) -> HttpResponse {
let state = state_read(&data).await;
if !activitypub::enabled(&state) {
return HttpResponse::NotFound().finish();
}
// Fetch or create the instance actor (needed for signature verification)
let actor = match activitypub::actor::get_or_create_instance(&state.db).await {
Ok(actor) => actor,
Err(err) => {
error!(%err, "fetching or creating instance actor failed");
return HttpResponse::InternalServerError().finish();
}
};
activitypub::inbox::handle(
&state.db,
state.http_client(),
state.public_url(),
&actor,
&req,
&body,
)
.await
}