netget/server/activitypub/
mod.rs1pub mod actions;
5pub mod http;
6pub mod instance;
7pub mod sig;
8
9use crate::llm::action_helper::call_llm;
10use crate::llm::actions::protocol_trait::ActionResult;
11use crate::logging::emit::Log;
12use crate::protocol::{Event, SpawnContext};
13use crate::server::{
14 accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS},
15 connection::ConnectionId,
16};
17use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
18use anyhow::{Context, Result};
19use hyper::{server::conn::http1, service::service_fn};
20use hyper_util::rt::{TokioIo, TokioTimer};
21use instance::{Inbound, Instance};
22use serde_json::{json, Value};
23use std::convert::Infallible;
24use std::net::SocketAddr;
25use std::sync::Arc;
26use std::time::Duration;
27use tokio::sync::mpsc;
28
29pub const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
30pub const INBOX_QUEUE: usize = 256;
32
33pub fn base_url(configured: Option<String>, local: SocketAddr) -> String {
35 configured.unwrap_or_else(|| {
36 let host = if local.ip().is_unspecified() {
37 "127.0.0.1".to_string()
38 } else {
39 match local.ip() {
40 std::net::IpAddr::V6(v6) => format!("[{v6}]"),
41 ip => ip.to_string(),
42 }
43 };
44 format!("http://{host}:{}", local.port())
45 })
46}
47
48pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
49 let params = ctx.startup_params.as_ref();
50 let names: Vec<String> = match params
51 .map(|p| p.get_optional_array("actors"))
52 .transpose()?
53 .flatten()
54 {
55 Some(list) => list
56 .iter()
57 .map(|v| {
58 v.as_str()
59 .map(str::to_string)
60 .context("actors must be usernames")
61 })
62 .collect::<Result<_>>()?,
63 None => actions::DEFAULT_ACTORS
64 .iter()
65 .map(|s| s.to_string())
66 .collect(),
67 };
68 let configured = params
69 .map(|p| p.get_optional_string("base_url"))
70 .transpose()?
71 .flatten();
72 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
73 let local = listener.local_addr()?;
74 let base = base_url(configured, local);
75 let instance = Arc::new(
76 tokio::task::spawn_blocking(move || Instance::new(&base, &names))
77 .await
78 .context("key generation stopped")??,
79 );
80 Log::new(Some(&ctx.status_tx)).info(format!(
81 "ActivityPub instance {} on {local}, actors: {}",
82 instance.base,
83 instance
84 .actors
85 .lock()
86 .unwrap_or_else(|e| e.into_inner())
87 .keys()
88 .map(|n| instance.handle(n))
89 .collect::<Vec<_>>()
90 .join(", ")
91 ));
92 let (tx, rx) = mpsc::channel(INBOX_QUEUE);
93 let server_id = ctx.server_id;
94 let consumer_ctx = ctx.clone();
95 let consumer_instance = instance.clone();
96 ctx.state
97 .spawn_server_task(server_id, consume(consumer_ctx, consumer_instance, rx))
98 .await;
99 let state = ctx.state.clone();
100 let accept = tokio::spawn(async move {
101 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
102 loop {
103 let (stream, peer, permit) =
104 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 {
105 Ok(v) => v,
106 Err(_) => break,
107 };
108 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
109 let now = crate::utils::clock::Instant::now();
110 ctx.state
111 .add_connection_to_server(
112 server_id,
113 ConnectionState {
114 id,
115 remote_addr: peer,
116 local_addr: local,
117 bytes_sent: 0,
118 bytes_received: 0,
119 packets_sent: 0,
120 packets_received: 0,
121 last_activity: now,
122 status: ConnectionStatus::Active,
123 status_changed_at: now,
124 protocol_info: ProtocolConnectionInfo::empty(),
125 },
126 )
127 .await;
128 let child = ctx.clone();
129 let instance = instance.clone();
130 let tx = tx.clone();
131 ctx.state
132 .spawn_server_task(server_id, async move {
133 let _permit = permit;
134 let log_ctx = child.clone();
135 let service = service_fn(move |request| {
136 let instance = instance.clone();
137 let tx = tx.clone();
138 let ctx = log_ctx.clone();
139 async move {
140 let line = format!("{} {}", request.method(), request.uri().path());
141 let (reply, outcome) = http::handle(&instance, request, &tx).await;
142 let log = Log::new(Some(&ctx.status_tx));
143 match outcome {
144 http::Outcome::Refused(why) => {
145 log.warn(format!("ActivityPub {line} refused: {why}"))
146 }
147 http::Outcome::Queued => {
148 log.info(format!("ActivityPub {line} queued for the model"))
149 }
150 http::Outcome::Served => {
151 log.debug(format!("ActivityPub {line} {}", reply.status()))
152 }
153 }
154 Ok::<_, Infallible>(reply)
155 }
156 });
157 let _ = http1::Builder::new()
158 .timer(TokioTimer::new())
159 .header_read_timeout(HEADER_TIMEOUT)
160 .max_buf_size(64 * 1024)
161 .serve_connection(TokioIo::new(stream), service)
162 .await;
163 child
164 .state
165 .update_connection_status(child.server_id, id, ConnectionStatus::Closed)
166 .await;
167 let _ = child.status_tx.send("__UPDATE_UI__".into());
168 })
169 .await;
170 }
171 });
172 state.register_server_task(server_id, accept).await;
173 Ok(local)
174}
175
176pub fn acting(instance: &Instance, action: &Value, inbox_owner: Option<&str>) -> Result<String> {
178 let name = action["as"]
179 .as_str()
180 .map(str::to_string)
181 .or_else(|| inbox_owner.map(str::to_string))
182 .or_else(|| instance.first_actor())
183 .context("the instance has no actors")?;
184 anyhow::ensure!(instance.has_actor(&name), "no local actor {name}");
185 Ok(name)
186}
187
188pub async fn apply_shared(
190 instance: &Instance,
191 me: &str,
192 action: &Value,
193 sender: Option<&str>,
194) -> Result<Value> {
195 Ok(match action["type"].as_str().unwrap_or_default() {
196 actions::POST => {
197 let to: Vec<String> = action["to"]
198 .as_array()
199 .map(|a| {
200 a.iter()
201 .filter_map(|v| v.as_str().map(str::to_string))
202 .collect()
203 })
204 .unwrap_or_default();
205 let (create, results) = instance
206 .post(
207 me,
208 action["content"].as_str().unwrap_or_default(),
209 &to,
210 action["public"].as_bool().unwrap_or(true),
211 action["in_reply_to"].as_str(),
212 )
213 .await?;
214 json!({"note_id": create["object"]["id"], "deliveries": results.iter().map(|(inbox, r)| match r {
215 Ok(s) => json!({"inbox": inbox, "status": s}),
216 Err(e) => json!({"inbox": inbox, "error": e}),
217 }).collect::<Vec<_>>()})
218 }
219 actions::LIKE => {
220 let to = action["to"]
221 .as_str()
222 .or(sender)
223 .context("whom to tell: give to")?;
224 json!({"status": instance.like(me, action["object"].as_str().unwrap_or_default(), to).await?})
225 }
226 actions::FOLLOW => {
227 let (id, status) = instance
228 .follow(me, action["target"].as_str().unwrap_or_default())
229 .await?;
230 json!({"actor": id, "status": status})
231 }
232 actions::UNFOLLOW => {
233 let (id, status) = instance
234 .unfollow(me, action["target"].as_str().unwrap_or_default())
235 .await?;
236 json!({"actor": id, "status": status})
237 }
238 other => anyhow::bail!("{other} is not something an actor does on its own"),
239 })
240}
241
242pub fn bookkeeping(instance: &Instance, inb: &Inbound) {
244 let a = &inb.activity;
245 let obj = &a["object"];
246 let to = inb.to.clone().or_else(|| {
247 obj["object"]
248 .as_str()
249 .or(obj.as_str())
250 .and_then(|o| instance.local_name(o))
251 });
252 match (a["type"].as_str(), obj["type"].as_str()) {
253 (Some("Undo"), Some("Follow")) => {
254 if let Some(name) = obj["object"].as_str().and_then(|o| instance.local_name(o)) {
255 instance.remove_follower(&name, &inb.signer);
256 }
257 }
258 (Some(t @ ("Accept" | "Reject")), _) => {
259 if let Some(name) = to {
260 instance.follow_answered(&name, &inb.signer, t == "Accept");
261 }
262 }
263 _ => {}
264 }
265}
266
267pub async fn event_data(instance: &Instance, inb: &Inbound) -> Value {
269 let mut data = instance::summary(&inb.activity);
270 data["to_actor"] = json!(inb.to);
271 data["actor"] = json!(inb.signer);
272 if let Ok(r) = instance.remote_actor(&inb.signer, inb.to.as_deref()).await {
273 if let (Some(user), Ok(u)) = (r.preferred_username, reqwest::Url::parse(&r.id)) {
274 let host = match u.port() {
275 Some(p) => format!("{}:{p}", u.host_str().unwrap_or_default()),
276 None => u.host_str().unwrap_or_default().to_string(),
277 };
278 data["actor_handle"] = json!(format!("{user}@{host}"));
279 }
280 }
281 data
282}
283
284async fn consume(ctx: SpawnContext, instance: Arc<Instance>, mut rx: mpsc::Receiver<Inbound>) {
285 let log = Log::new(Some(&ctx.status_tx));
286 while let Some(inb) = rx.recv().await {
287 bookkeeping(&instance, &inb);
288 let data = event_data(&instance, &inb).await;
289 let kind = data["type"].as_str().unwrap_or("?").to_string();
290 let event = Event::new(&actions::ACTIVITY_EVENT, data);
291 let result = match call_llm(
292 &ctx.llm_client,
293 &ctx.state,
294 ctx.server_id,
295 None,
296 &event,
297 &actions::ActivityPubProtocol,
298 )
299 .await
300 {
301 Ok(r) if r.failures.is_empty() => r,
302 _ => {
303 log.error(format!(
304 "ActivityPub {kind} from {} decision=fail_closed_llm_error (nothing sent)",
305 inb.signer
306 ));
307 continue;
308 }
309 };
310 let mut todo = Vec::new();
311 let mut stack = result.protocol_results;
312 while let Some(r) = stack.pop() {
313 match r {
314 ActionResult::Custom { data, .. } => todo.push(data),
315 ActionResult::Multiple(items) => stack.extend(items),
316 _ => {}
317 }
318 }
319 todo.reverse();
320 log.info(format!(
321 "ActivityPub {kind} from {} decision={}",
322 inb.signer,
323 if todo.is_empty() {
324 "model_silent"
325 } else {
326 "model_answer"
327 }
328 ));
329 for action in todo {
330 let outcome: Result<Value> = async {
331 let me = acting(&instance, &action, inb.to.as_deref())?;
332 match action["type"].as_str().unwrap_or_default() {
333 t @ (actions::ACCEPT | actions::REJECT) => {
334 anyhow::ensure!(kind == "Follow", "{t} answers a Follow, not a {kind}");
335 let me = inb.activity["object"]
336 .as_str()
337 .and_then(|o| instance.local_name(o))
338 .unwrap_or(me);
339 let status = instance
340 .answer_follow(&me, &inb.activity, t == actions::ACCEPT)
341 .await?;
342 Ok(json!({"status": status}))
343 }
344 _ => apply_shared(&instance, &me, &action, Some(&inb.signer)).await,
345 }
346 }
347 .await;
348 match outcome {
349 Ok(v) => log.info(format!("ActivityPub {}: {v}", action["type"])),
350 Err(e) => log.warn(format!("ActivityPub {} failed: {e:#}", action["type"])),
351 }
352 }
353 }
354}