solid_pod_rs_activitypub/
inbox.rs1use serde::{Deserialize, Serialize};
14
15use crate::{error::InboxError, http_sig::VerifiedActor, store::Store};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum InboxOutcome {
22 Accepted,
24 Duplicate,
26 Ignored,
28 FollowAccepted {
31 follower_id: String,
32 follower_inbox: Option<String>,
33 accept_object: serde_json::Value,
34 },
35 FollowRemoved { follower_id: String },
37 FollowAcknowledged { target_id: String },
39}
40
41pub 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 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
132pub 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#[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}