1use 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";
16pub 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;
23pub 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 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
47pub struct Inbound {
49 pub to: Option<String>,
51 pub signer: String,
52 pub activity: Value,
53}
54
55pub struct Instance {
56 pub base: String,
58 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('&', "&")
83 .replace('<', "<")
84 .replace('>', ">")
85 .replace('"', """)
86}
87
88pub 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("<", "<")
114 .replace(">", ">")
115 .replace(""", "\"")
116 .replace("'", "'")
117 .replace("&", "&")
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 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 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 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 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 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 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 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 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 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 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 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(¬e_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 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 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 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
745pub 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}