Skip to main content

netget/server/activitypub/
instance.rs

1//! One small ActivityPub instance, used by both roles: its local actors (each with an RSA
2//! key), WebFinger and actor documents, followers and an outbox per actor, fetching and
3//! caching remote actors, signed delivery, and verifying what arrives at an inbox. What the
4//! actors *say* comes from the model; this is the federation machinery around it.
5use super::sig;
6use anyhow::{bail, ensure, Context, Result};
7use rsa::RsaPrivateKey;
8use serde_json::{json, Value};
9use std::collections::{BTreeSet, HashMap, VecDeque};
10use std::sync::Mutex;
11use std::time::Duration;
12
13pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
14pub const SECURITY_CONTEXT: &str = "https://w3id.org/security/v1";
15pub const PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public";
16/// A fetched document or an inbox POST is at most this long.
17pub const MAX_DOCUMENT: usize = 1024 * 1024;
18pub const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
19pub const MAX_FOLLOWERS: usize = 10_000;
20pub const OUTBOX_KEPT: usize = 50;
21pub const MAX_OBJECTS: usize = 1000;
22pub const MAX_REMOTE_ACTORS: usize = 1000;
23/// Inboxes one post is delivered to at most.
24pub const MAX_DELIVERIES: usize = 100;
25
26pub struct LocalActor {
27    pub name: String,
28    key: RsaPrivateKey,
29    pem: String,
30    pub followers: BTreeSet<String>,
31    /// Remote actors this one follows: id → the Follow activity's id, and whether accepted.
32    pub following: HashMap<String, (String, bool)>,
33    pub outbox: VecDeque<Value>,
34}
35
36#[derive(Clone, Debug)]
37pub struct Remote {
38    pub id: String,
39    pub inbox: String,
40    pub shared_inbox: Option<String>,
41    pub preferred_username: Option<String>,
42    pub name: Option<String>,
43    pub key_id: Option<String>,
44    pub key_pem: Option<String>,
45}
46
47/// An activity that arrived at an inbox with a valid signature.
48pub struct Inbound {
49    /// The local actor whose inbox it was, or None for the shared inbox.
50    pub to: Option<String>,
51    pub signer: String,
52    pub activity: Value,
53}
54
55pub struct Instance {
56    /// `http://host:port`, no trailing slash.
57    pub base: String,
58    /// The host part of handles: `host:port`.
59    pub host: String,
60    pub actors: Mutex<HashMap<String, LocalActor>>,
61    objects: Mutex<(HashMap<String, Value>, VecDeque<String>)>,
62    remote: Mutex<HashMap<String, Remote>>,
63    proxied: reqwest::Client,
64    direct: reqwest::Client,
65}
66
67fn loopback(url: &str) -> bool {
68    reqwest::Url::parse(url)
69        .ok()
70        .and_then(|u| u.host_str().map(str::to_string))
71        .is_some_and(|h| {
72            h == "localhost"
73                || h.parse::<std::net::IpAddr>()
74                    .is_ok_and(|ip| ip.is_loopback())
75                || h.trim_matches(['[', ']'])
76                    .parse::<std::net::IpAddr>()
77                    .is_ok_and(|ip| ip.is_loopback())
78        })
79}
80
81pub fn escape_html(s: &str) -> String {
82    s.replace('&', "&amp;")
83        .replace('<', "&lt;")
84        .replace('>', "&gt;")
85        .replace('"', "&quot;")
86}
87
88/// HTML content as the model reads it: tags dropped, the common entities decoded.
89pub fn plain_text(html: &str) -> String {
90    let mut out = String::new();
91    let mut in_tag = false;
92    let mut tag = String::new();
93    for c in html.chars() {
94        match c {
95            '<' => {
96                in_tag = true;
97                tag.clear();
98            }
99            '>' if in_tag => {
100                in_tag = false;
101                let t = tag.trim_start_matches('/').to_ascii_lowercase();
102                if (t.starts_with("br") || t.starts_with('p'))
103                    && !out.is_empty()
104                    && !out.ends_with('\n')
105                {
106                    out.push('\n');
107                }
108            }
109            c if in_tag => tag.push(c),
110            c => out.push(c),
111        }
112    }
113    out.replace("&lt;", "<")
114        .replace("&gt;", ">")
115        .replace("&quot;", "\"")
116        .replace("&#39;", "'")
117        .replace("&amp;", "&")
118        .trim()
119        .to_string()
120}
121
122impl Instance {
123    pub fn new(base: &str, names: &[String]) -> Result<Self> {
124        let base = base.trim_end_matches('/').to_string();
125        let host = reqwest::Url::parse(&base)
126            .ok()
127            .and_then(|u| {
128                u.host_str().map(|h| match u.port() {
129                    Some(p) => format!("{h}:{p}"),
130                    None => h.to_string(),
131                })
132            })
133            .context("base_url must be an http(s) URL")?;
134        let mut actors = HashMap::new();
135        for name in names {
136            ensure!(
137                !name.is_empty()
138                    && name.len() <= 64
139                    && name
140                        .chars()
141                        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'),
142                "actor name {name:?} must be letters, digits, '_', '-' or '.'"
143            );
144            let key = sig::generate_key()?;
145            let pem = sig::public_pem(&key)?;
146            actors.insert(
147                name.clone(),
148                LocalActor {
149                    name: name.clone(),
150                    key,
151                    pem,
152                    followers: BTreeSet::new(),
153                    following: HashMap::new(),
154                    outbox: VecDeque::new(),
155                },
156            );
157        }
158        let build = |proxy: bool| {
159            let b = reqwest::Client::builder()
160                .timeout(HTTP_TIMEOUT)
161                .redirect(reqwest::redirect::Policy::limited(3))
162                .user_agent(concat!(
163                    "NetGet/",
164                    env!("CARGO_PKG_VERSION"),
165                    " (ActivityPub)"
166                ));
167            if proxy { b } else { b.no_proxy() }.build()
168        };
169        Ok(Self {
170            base,
171            host,
172            actors: Mutex::new(actors),
173            objects: Mutex::new(Default::default()),
174            remote: Mutex::new(HashMap::new()),
175            proxied: build(true)?,
176            direct: build(false)?,
177        })
178    }
179
180    fn http(&self, url: &str) -> &reqwest::Client {
181        if loopback(url) {
182            &self.direct
183        } else {
184            &self.proxied
185        }
186    }
187
188    pub fn actor_url(&self, name: &str) -> String {
189        format!("{}/users/{name}", self.base)
190    }
191
192    pub fn handle(&self, name: &str) -> String {
193        format!("{name}@{}", self.host)
194    }
195
196    pub fn has_actor(&self, name: &str) -> bool {
197        self.actors
198            .lock()
199            .unwrap_or_else(|e| e.into_inner())
200            .contains_key(name)
201    }
202
203    pub fn first_actor(&self) -> Option<String> {
204        self.actors
205            .lock()
206            .unwrap_or_else(|e| e.into_inner())
207            .keys()
208            .min()
209            .cloned()
210    }
211
212    /// The local actor an `acct:` resource or actor URL names.
213    pub fn local_name(&self, s: &str) -> Option<String> {
214        let s = s.trim_start_matches("acct:").trim_start_matches('@');
215        let name = if let Some(rest) = s.strip_prefix(&format!("{}/users/", self.base)) {
216            rest.to_string()
217        } else {
218            let (user, host) = s.split_once('@')?;
219            if !host.eq_ignore_ascii_case(&self.host) {
220                return None;
221            }
222            user.to_string()
223        };
224        self.has_actor(&name).then_some(name)
225    }
226
227    pub fn webfinger(&self, resource: &str) -> Option<Value> {
228        let name = self.local_name(resource)?;
229        Some(json!({
230            "subject": format!("acct:{}", self.handle(&name)),
231            "aliases": [self.actor_url(&name)],
232            "links": [
233                {"rel": "self", "type": "application/activity+json", "href": self.actor_url(&name)},
234                {"rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": self.actor_url(&name)}
235            ]
236        }))
237    }
238
239    pub fn actor_document(&self, name: &str) -> Option<Value> {
240        let actors = self.actors.lock().unwrap_or_else(|e| e.into_inner());
241        let a = actors.get(name)?;
242        let id = self.actor_url(name);
243        Some(json!({
244            "@context": [AS_CONTEXT, SECURITY_CONTEXT],
245            "id": id,
246            "type": "Person",
247            "preferredUsername": name,
248            "name": name,
249            "summary": "<p>An actor served by NetGet</p>",
250            "url": id,
251            "inbox": format!("{id}/inbox"),
252            "outbox": format!("{id}/outbox"),
253            "followers": format!("{id}/followers"),
254            "following": format!("{id}/following"),
255            "endpoints": {"sharedInbox": format!("{}/inbox", self.base)},
256            "manuallyApprovesFollowers": true,
257            "publicKey": {"id": format!("{id}#main-key"), "owner": id, "publicKeyPem": a.pem}
258        }))
259    }
260
261    pub fn collection(&self, name: &str, which: &str) -> Option<Value> {
262        let actors = self.actors.lock().unwrap_or_else(|e| e.into_inner());
263        let a = actors.get(name)?;
264        let id = format!("{}/{which}", self.actor_url(name));
265        let items: Vec<Value> = match which {
266            "followers" => a.followers.iter().map(|f| json!(f)).collect(),
267            "following" => a
268                .following
269                .iter()
270                .filter(|(_, (_, ok))| *ok)
271                .map(|(f, _)| json!(f))
272                .collect(),
273            "outbox" => a.outbox.iter().rev().cloned().collect(),
274            _ => return None,
275        };
276        Some(
277            json!({"@context": AS_CONTEXT, "id": id, "type": "OrderedCollection",
278                    "totalItems": items.len(), "orderedItems": items}),
279        )
280    }
281
282    pub fn object(&self, id: &str) -> Option<Value> {
283        self.objects
284            .lock()
285            .unwrap_or_else(|e| e.into_inner())
286            .0
287            .get(id)
288            .cloned()
289    }
290
291    fn remember_object(&self, id: &str, v: &Value) {
292        let mut o = self.objects.lock().unwrap_or_else(|e| e.into_inner());
293        if o.0.insert(id.to_string(), v.clone()).is_none() {
294            o.1.push_back(id.to_string());
295        }
296        while o.1.len() > MAX_OBJECTS {
297            if let Some(old) = o.1.pop_front() {
298                o.0.remove(&old);
299            }
300        }
301    }
302
303    pub fn new_id(&self, kind: &str) -> String {
304        let n: u64 = rand::random();
305        format!("{}/{kind}/{n:016x}", self.base)
306    }
307
308    // -----------------------------------------------------------------------------------
309    // Outgoing HTTP
310
311    fn signed_headers(
312        &self,
313        from: &str,
314        method: &str,
315        url: &str,
316        body: Option<&[u8]>,
317    ) -> Result<Vec<(String, String)>> {
318        let u = reqwest::Url::parse(url).context("not a URL")?;
319        let host = match u.port() {
320            Some(p) => format!("{}:{p}", u.host_str().unwrap_or_default()),
321            None => u.host_str().unwrap_or_default().to_string(),
322        };
323        let path = match u.query() {
324            Some(q) => format!("{}?{q}", u.path()),
325            None => u.path().to_string(),
326        };
327        let mut values = HashMap::new();
328        values.insert("host".to_string(), host.clone());
329        values.insert("date".to_string(), sig::http_date());
330        if let Some(b) = body {
331            values.insert("digest".to_string(), sig::digest(b));
332        }
333        let key = {
334            let actors = self.actors.lock().unwrap_or_else(|e| e.into_inner());
335            actors.get(from).context("no such local actor")?.key.clone()
336        };
337        let signature = sig::sign(
338            &key,
339            &format!("{}#main-key", self.actor_url(from)),
340            method,
341            &path,
342            &values,
343        )?;
344        let mut out: Vec<(String, String)> = values.into_iter().collect();
345        out.push(("signature".into(), signature));
346        Ok(out)
347    }
348
349    /// GET an ActivityStreams document, signed by `from` when given (for servers that
350    /// require authorized fetch).
351    pub async fn fetch(&self, url: &str, from: Option<&str>) -> Result<Value> {
352        ensure!(
353            url.starts_with("http://") || url.starts_with("https://"),
354            "only http(s) URLs can be fetched"
355        );
356        let mut req = self
357            .http(url)
358            .get(url)
359            .header("accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/jrd+json, application/json");
360        if let Some(from) = from {
361            for (k, v) in self.signed_headers(from, "get", url, None)? {
362                if k != "host" {
363                    req = req.header(k, v);
364                }
365            }
366        }
367        let resp = req.send().await.with_context(|| format!("GET {url}"))?;
368        let status = resp.status();
369        ensure!(
370            resp.content_length().unwrap_or(0) as usize <= MAX_DOCUMENT,
371            "{url} is larger than {MAX_DOCUMENT} bytes"
372        );
373        let mut body = Vec::new();
374        let mut resp = resp;
375        while let Some(chunk) = resp.chunk().await? {
376            body.extend_from_slice(&chunk);
377            ensure!(
378                body.len() <= MAX_DOCUMENT,
379                "{url} is larger than {MAX_DOCUMENT} bytes"
380            );
381        }
382        ensure!(status.is_success(), "GET {url}: {status}");
383        serde_json::from_slice(&body).with_context(|| format!("{url} is not JSON"))
384    }
385
386    /// A remote actor, fetched once and cached.
387    pub async fn remote_actor(&self, id: &str, from: Option<&str>) -> Result<Remote> {
388        if let Some(r) = self
389            .remote
390            .lock()
391            .unwrap_or_else(|e| e.into_inner())
392            .get(id)
393        {
394            return Ok(r.clone());
395        }
396        let doc = self.fetch(id, from).await?;
397        let r = Remote {
398            id: doc["id"].as_str().unwrap_or(id).to_string(),
399            inbox: doc["inbox"]
400                .as_str()
401                .context("the actor has no inbox")?
402                .to_string(),
403            shared_inbox: doc["endpoints"]["sharedInbox"].as_str().map(str::to_string),
404            preferred_username: doc["preferredUsername"].as_str().map(str::to_string),
405            name: doc["name"].as_str().map(str::to_string),
406            key_id: doc["publicKey"]["id"].as_str().map(str::to_string),
407            key_pem: doc["publicKey"]["publicKeyPem"]
408                .as_str()
409                .map(str::to_string),
410        };
411        let mut cache = self.remote.lock().unwrap_or_else(|e| e.into_inner());
412        if cache.len() >= MAX_REMOTE_ACTORS {
413            cache.clear();
414        }
415        cache.insert(id.to_string(), r.clone());
416        Ok(r)
417    }
418
419    /// An actor id from a handle (`user@host`, `@user@host`, `acct:…`) via WebFinger, or a URL.
420    pub async fn resolve(&self, target: &str) -> Result<String> {
421        if target.starts_with("http://") || target.starts_with("https://") {
422            return Ok(target.to_string());
423        }
424        let acct = target.trim_start_matches("acct:").trim_start_matches('@');
425        let (_, host) = acct.split_once('@').context("a handle is user@host")?;
426        let scheme = if loopback(&format!("http://{host}/")) {
427            "http"
428        } else {
429            "https"
430        };
431        let url = format!(
432            "{scheme}://{host}/.well-known/webfinger?resource=acct:{}",
433            urlencoding::encode(acct)
434        );
435        let jrd = self.fetch(&url, None).await?;
436        jrd["links"]
437            .as_array()
438            .into_iter()
439            .flatten()
440            .find(|l| {
441                l["rel"] == "self"
442                    && l["type"]
443                        .as_str()
444                        .is_some_and(|t| t.contains("activity+json") || t.contains("ld+json"))
445            })
446            .and_then(|l| l["href"].as_str())
447            .map(str::to_string)
448            .with_context(|| format!("WebFinger for {acct} names no ActivityPub actor"))
449    }
450
451    /// POST an activity to an inbox, signed by `from`; the HTTP status.
452    pub async fn deliver(&self, from: &str, inbox: &str, activity: &Value) -> Result<u16> {
453        let body = serde_json::to_vec(activity)?;
454        let mut req = self
455            .http(inbox)
456            .post(inbox)
457            .header("content-type", "application/activity+json");
458        for (k, v) in self.signed_headers(from, "post", inbox, Some(&body))? {
459            if k != "host" {
460                req = req.header(k, v);
461            }
462        }
463        let resp = req
464            .body(body)
465            .send()
466            .await
467            .with_context(|| format!("POST {inbox}"))?;
468        Ok(resp.status().as_u16())
469    }
470
471    // -----------------------------------------------------------------------------------
472    // What actors do
473
474    pub fn add_follower(&self, name: &str, follower: &str) -> bool {
475        let mut actors = self.actors.lock().unwrap_or_else(|e| e.into_inner());
476        match actors.get_mut(name) {
477            Some(a) if a.followers.len() < MAX_FOLLOWERS || a.followers.contains(follower) => {
478                a.followers.insert(follower.to_string());
479                true
480            }
481            _ => false,
482        }
483    }
484
485    pub fn remove_follower(&self, name: &str, follower: &str) {
486        if let Some(a) = self
487            .actors
488            .lock()
489            .unwrap_or_else(|e| e.into_inner())
490            .get_mut(name)
491        {
492            a.followers.remove(follower);
493        }
494    }
495
496    pub fn followers(&self, name: &str) -> Vec<String> {
497        self.actors
498            .lock()
499            .unwrap_or_else(|e| e.into_inner())
500            .get(name)
501            .map(|a| a.followers.iter().cloned().collect())
502            .unwrap_or_default()
503    }
504
505    /// Answer a Follow with Accept or Reject, delivered to the follower.
506    pub async fn answer_follow(&self, name: &str, follow: &Value, accept: bool) -> Result<u16> {
507        let follower = follow["actor"]
508            .as_str()
509            .context("the Follow has no actor")?
510            .to_string();
511        if accept {
512            ensure!(
513                self.add_follower(name, &follower),
514                "{name} has too many followers"
515            );
516        }
517        let activity = json!({
518            "@context": AS_CONTEXT,
519            "id": self.new_id(if accept { "accepts" } else { "rejects" }),
520            "type": if accept { "Accept" } else { "Reject" },
521            "actor": self.actor_url(name),
522            "object": follow,
523        });
524        let remote = self.remote_actor(&follower, Some(name)).await?;
525        self.deliver(name, &remote.inbox, &activity).await
526    }
527
528    /// Follow a remote actor; the Follow's id.
529    pub async fn follow(&self, name: &str, target: &str) -> Result<(String, u16)> {
530        let id = self.resolve(target).await?;
531        let remote = self.remote_actor(&id, Some(name)).await?;
532        let follow_id = self.new_id("follows");
533        let activity = json!({"@context": AS_CONTEXT, "id": follow_id, "type": "Follow",
534                              "actor": self.actor_url(name), "object": remote.id});
535        if let Some(a) = self
536            .actors
537            .lock()
538            .unwrap_or_else(|e| e.into_inner())
539            .get_mut(name)
540        {
541            a.following
542                .insert(remote.id.clone(), (follow_id.clone(), false));
543        }
544        let status = self.deliver(name, &remote.inbox, &activity).await?;
545        Ok((remote.id, status))
546    }
547
548    pub async fn unfollow(&self, name: &str, target: &str) -> Result<(String, u16)> {
549        let id = self.resolve(target).await?;
550        let follow_id = self
551            .actors
552            .lock()
553            .unwrap_or_else(|e| e.into_inner())
554            .get_mut(name)
555            .and_then(|a| a.following.remove(&id))
556            .map(|(f, _)| f)
557            .with_context(|| format!("{name} does not follow {id}"))?;
558        let remote = self.remote_actor(&id, Some(name)).await?;
559        let activity = json!({"@context": AS_CONTEXT, "id": self.new_id("undos"), "type": "Undo",
560                              "actor": self.actor_url(name),
561                              "object": {"id": follow_id, "type": "Follow", "actor": self.actor_url(name), "object": id}});
562        Ok((id, self.deliver(name, &remote.inbox, &activity).await?))
563    }
564
565    /// Mark a Follow accepted (an Accept arrived for it).
566    pub fn follow_answered(&self, name: &str, remote: &str, accepted: bool) {
567        if let Some(a) = self
568            .actors
569            .lock()
570            .unwrap_or_else(|e| e.into_inner())
571            .get_mut(name)
572        {
573            if accepted {
574                if let Some(f) = a.following.get_mut(remote) {
575                    f.1 = true;
576                }
577            } else {
578                a.following.remove(remote);
579            }
580        }
581    }
582
583    /// Publish a Note as `name`: to its followers and to `to` (actor ids or handles); the
584    /// Create activity and each delivery's status.
585    pub async fn post(
586        &self,
587        name: &str,
588        content: &str,
589        to: &[String],
590        public: bool,
591        in_reply_to: Option<&str>,
592    ) -> Result<(Value, Vec<(String, Result<u16, String>)>)> {
593        let me = self.actor_url(name);
594        let mut recipients = Vec::new();
595        for t in to {
596            recipients.push(self.resolve(t).await?);
597        }
598        let mut audience: Vec<Value> = recipients.iter().map(|r| json!(r)).collect();
599        if public {
600            audience.insert(0, json!(PUBLIC));
601        }
602        let note_id = self.new_id("notes");
603        let html = content
604            .split('\n')
605            .map(|l| format!("<p>{}</p>", escape_html(l)))
606            .collect::<String>();
607        let mut note = json!({
608            "id": note_id, "type": "Note", "attributedTo": me, "content": html,
609            "published": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
610            "to": audience, "cc": [format!("{me}/followers")],
611        });
612        if let Some(r) = in_reply_to {
613            note["inReplyTo"] = json!(r);
614        }
615        let create = json!({
616            "@context": AS_CONTEXT, "id": format!("{note_id}/activity"), "type": "Create",
617            "actor": me, "published": note["published"], "to": note["to"], "cc": note["cc"],
618            "object": note,
619        });
620        self.remember_object(&note_id, &{
621            let mut n = create["object"].clone();
622            n["@context"] = json!(AS_CONTEXT);
623            n
624        });
625        if let Some(a) = self
626            .actors
627            .lock()
628            .unwrap_or_else(|e| e.into_inner())
629            .get_mut(name)
630        {
631            a.outbox.push_back(create.clone());
632            while a.outbox.len() > OUTBOX_KEPT {
633                a.outbox.pop_front();
634            }
635        }
636        let mut targets: Vec<String> = self.followers(name);
637        targets.extend(recipients);
638        targets.sort();
639        targets.dedup();
640        let mut inboxes = Vec::new();
641        for t in targets.into_iter().take(MAX_DELIVERIES) {
642            match self.remote_actor(&t, Some(name)).await {
643                Ok(r) => inboxes.push(r.shared_inbox.unwrap_or(r.inbox)),
644                Err(e) => inboxes.push(format!("!{t}: {e:#}")),
645            }
646        }
647        inboxes.sort();
648        inboxes.dedup();
649        let mut results = Vec::new();
650        for inbox in inboxes {
651            if let Some(err) = inbox.strip_prefix('!') {
652                results.push((err.to_string(), Err("actor not reachable".to_string())));
653                continue;
654            }
655            let r = self
656                .deliver(name, &inbox, &create)
657                .await
658                .map_err(|e| format!("{e:#}"));
659            results.push((inbox, r));
660        }
661        Ok((create, results))
662    }
663
664    /// Like an object, delivered to its author (or `to`).
665    pub async fn like(&self, name: &str, object: &str, to: &str) -> Result<u16> {
666        let target = self.resolve(to).await?;
667        let remote = self.remote_actor(&target, Some(name)).await?;
668        let activity = json!({"@context": AS_CONTEXT, "id": self.new_id("likes"), "type": "Like",
669                              "actor": self.actor_url(name), "object": object});
670        self.deliver(name, &remote.inbox, &activity).await
671    }
672
673    // -----------------------------------------------------------------------------------
674    // Incoming
675
676    /// Verify a POST to an inbox: the signature (with the signer's key, fetched), the
677    /// digest, the date, and that the activity's actor is the signer.
678    pub async fn verify_inbound(
679        &self,
680        to: Option<String>,
681        path: &str,
682        headers: &HashMap<String, String>,
683        body: &[u8],
684    ) -> Result<Inbound> {
685        let header = headers
686            .get("signature")
687            .context("unsigned: the inbox needs an HTTP Signature")?;
688        let parsed = sig::parse(header)?;
689        let owner_hint = parsed
690            .key_id
691            .split('#')
692            .next()
693            .unwrap_or_default()
694            .to_string();
695        let (pem, owner) = self
696            .key_of(&parsed.key_id, &owner_hint, to.as_deref())
697            .await?;
698        sig::verify(&parsed, &pem, "post", path, headers, Some(body))?;
699        let activity: Value = serde_json::from_slice(body).context("the body is not JSON")?;
700        let actor = activity["actor"]
701            .as_str()
702            .or_else(|| activity["actor"]["id"].as_str())
703            .context("the activity has no actor")?;
704        if actor != owner {
705            bail!("signed by {owner} but the activity's actor is {actor}");
706        }
707        Ok(Inbound {
708            to,
709            signer: owner,
710            activity,
711        })
712    }
713
714    /// A key's PEM and owner: the actor document with that key, or a key document.
715    async fn key_of(
716        &self,
717        key_id: &str,
718        owner_hint: &str,
719        from: Option<&str>,
720    ) -> Result<(String, String)> {
721        if let Ok(r) = self.remote_actor(owner_hint, from).await {
722            if r.key_id.as_deref() == Some(key_id) {
723                if let Some(pem) = r.key_pem {
724                    return Ok((pem, r.id));
725                }
726            }
727        }
728        let doc = self.fetch(key_id, from).await?;
729        let key = if doc["publicKeyPem"].is_string() {
730            &doc
731        } else {
732            &doc["publicKey"]
733        };
734        let pem = key["publicKeyPem"]
735            .as_str()
736            .context("the key document has no publicKeyPem")?;
737        let owner = key["owner"]
738            .as_str()
739            .or_else(|| doc["id"].as_str())
740            .context("the key has no owner")?;
741        Ok((pem.to_string(), owner.to_string()))
742    }
743}
744
745/// What the model is told about an activity.
746pub fn summary(a: &Value) -> Value {
747    let kind = a["type"].as_str().unwrap_or("?");
748    let mut s = json!({"type": kind, "id": a["id"], "actor": a["actor"]});
749    let obj = &a["object"];
750    match kind {
751        "Create" | "Update" => {
752            s["object_type"] = obj["type"].clone();
753            s["object_id"] = obj["id"].clone();
754            if let Some(c) = obj["content"].as_str() {
755                s["content"] = json!(plain_text(c));
756            }
757            s["in_reply_to"] = obj["inReplyTo"].clone();
758            s["to"] = obj["to"].clone();
759        }
760        "Undo" | "Accept" | "Reject" => {
761            s["object_type"] = obj["type"].clone();
762            s["object_id"] = if obj.is_string() {
763                obj.clone()
764            } else {
765                obj["id"].clone()
766            };
767        }
768        _ => {
769            s["object_id"] = if obj.is_string() {
770                obj.clone()
771            } else {
772                obj["id"].clone()
773            };
774        }
775    }
776    s
777}