tuwunel_api/client/admin/users/
redact_status.rs1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use ruma::OwnedEventId;
5use serde::Deserialize;
6use serde_json::Value as JsonValue;
7use synapse_admin_api::users::redact_status::v1::{RedactStatus, Request, Response};
8use tuwunel_core::{Result, err};
9use tuwunel_service::tasks::{Status, TaskInfo};
10
11use crate::{Ruma, client::admin::require_admin};
12
13#[derive(Default, Deserialize)]
15struct RedactOutcome {
16 #[serde(default)]
17 failed_redactions: BTreeMap<OwnedEventId, String>,
18}
19
20pub(crate) async fn admin_redact_status_route(
25 State(services): State<crate::State>,
26 body: Ruma<Request>,
27) -> Result<Response> {
28 require_admin(&services, body.sender_user()).await?;
29
30 let task = services
31 .tasks
32 .get(&body.redact_id)
33 .filter(|task| task.action == super::REDACT_USER_ACTION)
34 .ok_or_else(|| err!(Request(NotFound("Unknown redact task"))))?;
35
36 Ok(redact_response(task))
37}
38
39fn redact_response(task: TaskInfo) -> Response {
40 let failed_redactions = (task.status == Status::Complete).then(|| {
41 task.result
42 .map(completed_failures)
43 .unwrap_or_default()
44 });
45
46 Response {
47 status: redact_status(task.status),
48 failed_redactions,
49 error: task.error,
50 }
51}
52
53fn completed_failures(result: JsonValue) -> BTreeMap<OwnedEventId, String> {
54 let outcome: RedactOutcome = serde_json::from_value(result).unwrap_or_default();
55
56 outcome.failed_redactions
57}
58
59fn redact_status(status: Status) -> RedactStatus {
60 match status {
61 | Status::Scheduled => RedactStatus::Scheduled,
62 | Status::Active => RedactStatus::Active,
63 | Status::Complete => RedactStatus::Complete,
64 | Status::Failed => RedactStatus::Failed,
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use serde_json::json;
71 use tuwunel_service::tasks::Status;
72
73 use super::{completed_failures, redact_status};
74
75 #[test]
76 fn maps_each_stage_and_emits_complete() {
77 let status = |status| serde_json::to_value(redact_status(status)).unwrap();
78
79 assert_eq!(status(Status::Scheduled), json!("scheduled"));
80 assert_eq!(status(Status::Active), json!("active"));
81 assert_eq!(status(Status::Complete), json!("complete"));
82 assert_eq!(status(Status::Failed), json!("failed"));
83 }
84
85 #[test]
86 fn folds_unexpected_result_shapes_to_no_failures() {
87 assert!(completed_failures(json!(null)).is_empty());
88 assert!(completed_failures(json!({})).is_empty());
89 assert!(completed_failures(json!({ "purged": 7 })).is_empty());
90
91 let failures =
92 completed_failures(json!({ "failed_redactions": { "$f:example.com": "boom" } }));
93
94 assert_eq!(failures.len(), 1);
95 }
96}