pub mod actions;
pub mod http;
pub mod instance;
pub mod sig;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::logging::emit::Log;
use crate::protocol::{Event, SpawnContext};
use crate::server::{
accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS},
connection::ConnectionId,
};
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use anyhow::{Context, Result};
use hyper::{server::conn::http1, service::service_fn};
use hyper_util::rt::{TokioIo, TokioTimer};
use instance::{Inbound, Instance};
use serde_json::{json, Value};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
pub const INBOX_QUEUE: usize = 256;
pub fn base_url(configured: Option<String>, local: SocketAddr) -> String {
configured.unwrap_or_else(|| {
let host = if local.ip().is_unspecified() {
"127.0.0.1".to_string()
} else {
match local.ip() {
std::net::IpAddr::V6(v6) => format!("[{v6}]"),
ip => ip.to_string(),
}
};
format!("http://{host}:{}", local.port())
})
}
pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
let params = ctx.startup_params.as_ref();
let names: Vec<String> = match params
.map(|p| p.get_optional_array("actors"))
.transpose()?
.flatten()
{
Some(list) => list
.iter()
.map(|v| {
v.as_str()
.map(str::to_string)
.context("actors must be usernames")
})
.collect::<Result<_>>()?,
None => actions::DEFAULT_ACTORS
.iter()
.map(|s| s.to_string())
.collect(),
};
let configured = params
.map(|p| p.get_optional_string("base_url"))
.transpose()?
.flatten();
let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
let local = listener.local_addr()?;
let base = base_url(configured, local);
let instance = Arc::new(
tokio::task::spawn_blocking(move || Instance::new(&base, &names))
.await
.context("key generation stopped")??,
);
Log::new(Some(&ctx.status_tx)).info(format!(
"ActivityPub instance {} on {local}, actors: {}",
instance.base,
instance
.actors
.lock()
.unwrap_or_else(|e| e.into_inner())
.keys()
.map(|n| instance.handle(n))
.collect::<Vec<_>>()
.join(", ")
));
let (tx, rx) = mpsc::channel(INBOX_QUEUE);
let server_id = ctx.server_id;
let consumer_ctx = ctx.clone();
let consumer_instance = instance.clone();
ctx.state
.spawn_server_task(server_id, consume(consumer_ctx, consumer_instance, rx))
.await;
let state = ctx.state.clone();
let accept = tokio::spawn(async move {
let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
loop {
let (stream, peer, permit) =
match accept_bounded(&listener, &limiter, b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", "ActivityPub", Some(&ctx.status_tx)).await {
Ok(v) => v,
Err(_) => break,
};
let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
let now = crate::utils::clock::Instant::now();
ctx.state
.add_connection_to_server(
server_id,
ConnectionState {
id,
remote_addr: peer,
local_addr: local,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
},
)
.await;
let child = ctx.clone();
let instance = instance.clone();
let tx = tx.clone();
ctx.state
.spawn_server_task(server_id, async move {
let _permit = permit;
let log_ctx = child.clone();
let service = service_fn(move |request| {
let instance = instance.clone();
let tx = tx.clone();
let ctx = log_ctx.clone();
async move {
let line = format!("{} {}", request.method(), request.uri().path());
let (reply, outcome) = http::handle(&instance, request, &tx).await;
let log = Log::new(Some(&ctx.status_tx));
match outcome {
http::Outcome::Refused(why) => {
log.warn(format!("ActivityPub {line} refused: {why}"))
}
http::Outcome::Queued => {
log.info(format!("ActivityPub {line} queued for the model"))
}
http::Outcome::Served => {
log.debug(format!("ActivityPub {line} {}", reply.status()))
}
}
Ok::<_, Infallible>(reply)
}
});
let _ = http1::Builder::new()
.timer(TokioTimer::new())
.header_read_timeout(HEADER_TIMEOUT)
.max_buf_size(64 * 1024)
.serve_connection(TokioIo::new(stream), service)
.await;
child
.state
.update_connection_status(child.server_id, id, ConnectionStatus::Closed)
.await;
let _ = child.status_tx.send("__UPDATE_UI__".into());
})
.await;
}
});
state.register_server_task(server_id, accept).await;
Ok(local)
}
pub fn acting(instance: &Instance, action: &Value, inbox_owner: Option<&str>) -> Result<String> {
let name = action["as"]
.as_str()
.map(str::to_string)
.or_else(|| inbox_owner.map(str::to_string))
.or_else(|| instance.first_actor())
.context("the instance has no actors")?;
anyhow::ensure!(instance.has_actor(&name), "no local actor {name}");
Ok(name)
}
pub async fn apply_shared(
instance: &Instance,
me: &str,
action: &Value,
sender: Option<&str>,
) -> Result<Value> {
Ok(match action["type"].as_str().unwrap_or_default() {
actions::POST => {
let to: Vec<String> = action["to"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let (create, results) = instance
.post(
me,
action["content"].as_str().unwrap_or_default(),
&to,
action["public"].as_bool().unwrap_or(true),
action["in_reply_to"].as_str(),
)
.await?;
json!({"note_id": create["object"]["id"], "deliveries": results.iter().map(|(inbox, r)| match r {
Ok(s) => json!({"inbox": inbox, "status": s}),
Err(e) => json!({"inbox": inbox, "error": e}),
}).collect::<Vec<_>>()})
}
actions::LIKE => {
let to = action["to"]
.as_str()
.or(sender)
.context("whom to tell: give to")?;
json!({"status": instance.like(me, action["object"].as_str().unwrap_or_default(), to).await?})
}
actions::FOLLOW => {
let (id, status) = instance
.follow(me, action["target"].as_str().unwrap_or_default())
.await?;
json!({"actor": id, "status": status})
}
actions::UNFOLLOW => {
let (id, status) = instance
.unfollow(me, action["target"].as_str().unwrap_or_default())
.await?;
json!({"actor": id, "status": status})
}
other => anyhow::bail!("{other} is not something an actor does on its own"),
})
}
pub fn bookkeeping(instance: &Instance, inb: &Inbound) {
let a = &inb.activity;
let obj = &a["object"];
let to = inb.to.clone().or_else(|| {
obj["object"]
.as_str()
.or(obj.as_str())
.and_then(|o| instance.local_name(o))
});
match (a["type"].as_str(), obj["type"].as_str()) {
(Some("Undo"), Some("Follow")) => {
if let Some(name) = obj["object"].as_str().and_then(|o| instance.local_name(o)) {
instance.remove_follower(&name, &inb.signer);
}
}
(Some(t @ ("Accept" | "Reject")), _) => {
if let Some(name) = to {
instance.follow_answered(&name, &inb.signer, t == "Accept");
}
}
_ => {}
}
}
pub async fn event_data(instance: &Instance, inb: &Inbound) -> Value {
let mut data = instance::summary(&inb.activity);
data["to_actor"] = json!(inb.to);
data["actor"] = json!(inb.signer);
if let Ok(r) = instance.remote_actor(&inb.signer, inb.to.as_deref()).await {
if let (Some(user), Ok(u)) = (r.preferred_username, reqwest::Url::parse(&r.id)) {
let host = match u.port() {
Some(p) => format!("{}:{p}", u.host_str().unwrap_or_default()),
None => u.host_str().unwrap_or_default().to_string(),
};
data["actor_handle"] = json!(format!("{user}@{host}"));
}
}
data
}
async fn consume(ctx: SpawnContext, instance: Arc<Instance>, mut rx: mpsc::Receiver<Inbound>) {
let log = Log::new(Some(&ctx.status_tx));
while let Some(inb) = rx.recv().await {
bookkeeping(&instance, &inb);
let data = event_data(&instance, &inb).await;
let kind = data["type"].as_str().unwrap_or("?").to_string();
let event = Event::new(&actions::ACTIVITY_EVENT, data);
let result = match call_llm(
&ctx.llm_client,
&ctx.state,
ctx.server_id,
None,
&event,
&actions::ActivityPubProtocol,
)
.await
{
Ok(r) if r.failures.is_empty() => r,
_ => {
log.error(format!(
"ActivityPub {kind} from {} decision=fail_closed_llm_error (nothing sent)",
inb.signer
));
continue;
}
};
let mut todo = Vec::new();
let mut stack = result.protocol_results;
while let Some(r) = stack.pop() {
match r {
ActionResult::Custom { data, .. } => todo.push(data),
ActionResult::Multiple(items) => stack.extend(items),
_ => {}
}
}
todo.reverse();
log.info(format!(
"ActivityPub {kind} from {} decision={}",
inb.signer,
if todo.is_empty() {
"model_silent"
} else {
"model_answer"
}
));
for action in todo {
let outcome: Result<Value> = async {
let me = acting(&instance, &action, inb.to.as_deref())?;
match action["type"].as_str().unwrap_or_default() {
t @ (actions::ACCEPT | actions::REJECT) => {
anyhow::ensure!(kind == "Follow", "{t} answers a Follow, not a {kind}");
let me = inb.activity["object"]
.as_str()
.and_then(|o| instance.local_name(o))
.unwrap_or(me);
let status = instance
.answer_follow(&me, &inb.activity, t == actions::ACCEPT)
.await?;
Ok(json!({"status": status}))
}
_ => apply_shared(&instance, &me, &action, Some(&inb.signer)).await,
}
}
.await;
match outcome {
Ok(v) => log.info(format!("ActivityPub {}: {v}", action["type"])),
Err(e) => log.warn(format!("ActivityPub {} failed: {e:#}", action["type"])),
}
}
}
}