Skip to main content

solid_pod_rs_activitypub/
inbox.rs

1//! Inbox handler — dispatches verified inbound AP activities.
2//!
3//! JSS parity: mirrors `src/ap/routes/inbox.js` semantics. The handler
4//! dispatches `Follow`, `Undo(Follow)`, `Accept(Follow)`, `Create`,
5//! `Delete`, `Like`, and `Announce`; unknown activity types are
6//! recorded but not acted on.
7//!
8//! Design: the transport (HTTP framework) is the caller's concern. We
9//! take an already-verified request (see [`crate::http_sig`]) plus the
10//! decoded JSON activity and return an outcome the caller can map to
11//! an HTTP status.
12
13use serde::{Deserialize, Serialize};
14
15use crate::{error::InboxError, http_sig::VerifiedActor, store::Store};
16
17/// The outcome of processing an inbox activity. The HTTP layer maps
18/// this to status codes and any extra response side-effects
19/// (e.g. enqueue an Accept reply).
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum InboxOutcome {
22    /// Activity accepted and stored. Return 202.
23    Accepted,
24    /// Activity was a duplicate (already stored). Return 202.
25    Duplicate,
26    /// Activity was ignored (unknown type, or no-op). Return 202.
27    Ignored,
28    /// Follow request accepted — the caller should queue and deliver
29    /// an `Accept` activity back to `follower_inbox`.
30    FollowAccepted {
31        follower_id: String,
32        follower_inbox: Option<String>,
33        accept_object: serde_json::Value,
34    },
35    /// Undo(Follow) — follower removed.
36    FollowRemoved { follower_id: String },
37    /// Accept(Follow) — our follow was accepted.
38    FollowAcknowledged { target_id: String },
39}
40
41/// Handle a single inbound activity.
42///
43/// `local_actor_id` is the pod's own Actor ID (e.g.
44/// `https://pod.example/profile/card.jsonld#me`).
45///
46/// The `verified_actor` argument must already have passed HTTP
47/// Signature verification. If signature verification is being
48/// soft-logged (JSS's default posture) callers may still hand through
49/// an attested [`VerifiedActor`].
50pub async fn handle_inbox(
51    store: &Store,
52    local_actor_id: &str,
53    verified_actor: &VerifiedActor,
54    activity: &serde_json::Value,
55) -> Result<InboxOutcome, InboxError> {
56    let activity_type = activity
57        .get("type")
58        .and_then(|v| v.as_str())
59        .ok_or(InboxError::MissingType)?;
60
61    let was_new = store.record_inbox(local_actor_id, activity).await?;
62    if !was_new {
63        return Ok(InboxOutcome::Duplicate);
64    }
65
66    match activity_type {
67        "Follow" => {
68            let follower_id = activity
69                .get("actor")
70                .and_then(|v| v.as_str())
71                .unwrap_or(&verified_actor.actor_url)
72                .to_string();
73            // Per JSS — the follower's inbox is looked up from the
74            // actor document. That fetch is the caller's responsibility
75            // (we'd pull it in via [`crate::http_sig::ActorKeyResolver`]
76            // if this were a blocking op); we surface the follower_id
77            // here and let the caller hydrate the inbox URL before
78            // persisting if they want to.
79            let follower_inbox = activity
80                .get("actorInbox")
81                .and_then(|v| v.as_str())
82                .map(String::from);
83            store
84                .add_follower(
85                    local_actor_id,
86                    &follower_id,
87                    follower_inbox.as_deref(),
88                )
89                .await?;
90            let accept = build_accept(local_actor_id, activity);
91            Ok(InboxOutcome::FollowAccepted {
92                follower_id,
93                follower_inbox,
94                accept_object: accept,
95            })
96        }
97        "Undo" => {
98            let inner_type = activity
99                .get("object")
100                .and_then(|v| v.get("type"))
101                .and_then(|v| v.as_str());
102            if inner_type == Some("Follow") {
103                let follower_id = activity
104                    .get("actor")
105                    .and_then(|v| v.as_str())
106                    .unwrap_or(&verified_actor.actor_url)
107                    .to_string();
108                store.remove_follower(local_actor_id, &follower_id).await?;
109                return Ok(InboxOutcome::FollowRemoved { follower_id });
110            }
111            Ok(InboxOutcome::Ignored)
112        }
113        "Accept" => {
114            let inner = activity.get("object");
115            let inner_type = inner.and_then(|v| v.get("type")).and_then(|v| v.as_str());
116            if inner_type == Some("Follow") {
117                let target_id = inner
118                    .and_then(|v| v.get("object"))
119                    .and_then(|v| v.as_str())
120                    .unwrap_or(local_actor_id)
121                    .to_string();
122                store.accept_following(local_actor_id, &target_id).await?;
123                return Ok(InboxOutcome::FollowAcknowledged { target_id });
124            }
125            Ok(InboxOutcome::Ignored)
126        }
127        "Create" | "Like" | "Announce" | "Delete" => Ok(InboxOutcome::Accepted),
128        _ => Ok(InboxOutcome::Ignored),
129    }
130}
131
132/// Build an `Accept(Follow)` activity to send back to a follower,
133/// matching JSS's `outbox.createAccept` structure.
134pub fn build_accept(local_actor_id: &str, follow: &serde_json::Value) -> serde_json::Value {
135    serde_json::json!({
136        "@context": "https://www.w3.org/ns/activitystreams",
137        "id": format!("{}/accept/{}", local_actor_id.trim_end_matches("#me"), uuid::Uuid::new_v4()),
138        "type": "Accept",
139        "actor": local_actor_id,
140        "object": follow,
141    })
142}
143
144// ---------------------------------------------------------------------------
145// Tests
146// ---------------------------------------------------------------------------
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn sample_verified(actor_url: &str) -> VerifiedActor {
153        VerifiedActor {
154            key_id: format!("{actor_url}#main-key"),
155            actor_url: actor_url.to_string(),
156            public_key_pem: "PEM".to_string(),
157        }
158    }
159
160    #[tokio::test]
161    async fn inbox_follow_accepts_and_stores_follower() {
162        let store = Store::in_memory().await.unwrap();
163        let me = "https://pod.example/profile/card.jsonld#me";
164        let follow = serde_json::json!({
165            "id": "https://remote.example/follows/1",
166            "type": "Follow",
167            "actor": "https://remote.example/actor",
168            "actorInbox": "https://remote.example/inbox",
169            "object": me
170        });
171        let outcome = handle_inbox(
172            &store,
173            me,
174            &sample_verified("https://remote.example/actor"),
175            &follow,
176        )
177        .await
178        .unwrap();
179        match outcome {
180            InboxOutcome::FollowAccepted {
181                follower_id,
182                follower_inbox,
183                accept_object,
184            } => {
185                assert_eq!(follower_id, "https://remote.example/actor");
186                assert_eq!(
187                    follower_inbox.as_deref(),
188                    Some("https://remote.example/inbox")
189                );
190                assert_eq!(accept_object["type"], "Accept");
191                assert_eq!(accept_object["object"]["id"], follow["id"]);
192            }
193            other => panic!("expected FollowAccepted, got {other:?}"),
194        }
195        assert!(store
196            .is_follower(me, "https://remote.example/actor")
197            .await
198            .unwrap());
199    }
200
201    #[tokio::test]
202    async fn inbox_undo_follow_removes_follower() {
203        let store = Store::in_memory().await.unwrap();
204        let me = "https://pod.example/profile/card.jsonld#me";
205        store
206            .add_follower(me, "https://remote.example/actor", Some("https://r/inbox"))
207            .await
208            .unwrap();
209        let undo = serde_json::json!({
210            "id": "https://remote.example/undos/1",
211            "type": "Undo",
212            "actor": "https://remote.example/actor",
213            "object": {"type": "Follow", "actor": "https://remote.example/actor", "object": me}
214        });
215        let outcome = handle_inbox(
216            &store,
217            me,
218            &sample_verified("https://remote.example/actor"),
219            &undo,
220        )
221        .await
222        .unwrap();
223        assert!(matches!(outcome, InboxOutcome::FollowRemoved { .. }));
224        assert!(!store
225            .is_follower(me, "https://remote.example/actor")
226            .await
227            .unwrap());
228    }
229
230    #[tokio::test]
231    async fn inbox_accept_marks_following() {
232        let store = Store::in_memory().await.unwrap();
233        let me = "https://pod.example/profile/card.jsonld#me";
234        store
235            .add_following(me, "https://remote.example/actor")
236            .await
237            .unwrap();
238        let accept = serde_json::json!({
239            "id": "https://remote.example/accepts/1",
240            "type": "Accept",
241            "actor": "https://remote.example/actor",
242            "object": {
243                "type": "Follow",
244                "actor": me,
245                "object": "https://remote.example/actor"
246            }
247        });
248        let outcome = handle_inbox(
249            &store,
250            me,
251            &sample_verified("https://remote.example/actor"),
252            &accept,
253        )
254        .await
255        .unwrap();
256        assert!(matches!(outcome, InboxOutcome::FollowAcknowledged { .. }));
257        assert!(store
258            .is_following(me, "https://remote.example/actor")
259            .await
260            .unwrap());
261    }
262
263    #[tokio::test]
264    async fn inbox_create_is_idempotent_by_id() {
265        let store = Store::in_memory().await.unwrap();
266        let me = "https://pod.example/profile/card.jsonld#me";
267        let create = serde_json::json!({
268            "id": "https://remote.example/notes/42/activity",
269            "type": "Create",
270            "actor": "https://remote.example/actor",
271            "object": {"type": "Note", "content": "hi"}
272        });
273        let first = handle_inbox(
274            &store,
275            me,
276            &sample_verified("https://remote.example/actor"),
277            &create,
278        )
279        .await
280        .unwrap();
281        assert_eq!(first, InboxOutcome::Accepted);
282        let second = handle_inbox(
283            &store,
284            me,
285            &sample_verified("https://remote.example/actor"),
286            &create,
287        )
288        .await
289        .unwrap();
290        assert_eq!(second, InboxOutcome::Duplicate);
291    }
292
293    #[tokio::test]
294    async fn inbox_unknown_type_is_ignored() {
295        let store = Store::in_memory().await.unwrap();
296        let me = "https://pod.example/profile/card.jsonld#me";
297        let weird = serde_json::json!({
298            "id": "https://remote.example/x/1",
299            "type": "Move",
300            "actor": "https://remote.example/actor"
301        });
302        let outcome = handle_inbox(
303            &store,
304            me,
305            &sample_verified("https://remote.example/actor"),
306            &weird,
307        )
308        .await
309        .unwrap();
310        assert_eq!(outcome, InboxOutcome::Ignored);
311    }
312
313    #[tokio::test]
314    async fn inbox_missing_type_errors() {
315        let store = Store::in_memory().await.unwrap();
316        let me = "https://pod.example/profile/card.jsonld#me";
317        let bad = serde_json::json!({ "id": "x" });
318        let err = handle_inbox(
319            &store,
320            me,
321            &sample_verified("https://remote.example/actor"),
322            &bad,
323        )
324        .await
325        .unwrap_err();
326        assert!(matches!(err, InboxError::MissingType));
327    }
328
329    #[test]
330    fn build_accept_has_expected_shape() {
331        let follow = serde_json::json!({
332            "id": "https://r/f/1",
333            "type": "Follow",
334            "actor": "https://r/a"
335        });
336        let accept = build_accept("https://pod.example/profile/card.jsonld#me", &follow);
337        assert_eq!(accept["type"], "Accept");
338        assert_eq!(accept["object"]["id"], "https://r/f/1");
339        assert_eq!(
340            accept["actor"],
341            "https://pod.example/profile/card.jsonld#me"
342        );
343        assert!(accept.get("id").is_some());
344    }
345}