Skip to main content

tuwunel_api/client/admin/users/
redact_status.rs

1use 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/// The payload the redaction task records; an unexpected shape folds to empty.
14#[derive(Default, Deserialize)]
15struct RedactOutcome {
16	#[serde(default)]
17	failed_redactions: BTreeMap<OwnedEventId, String>,
18}
19
20/// # `GET /_synapse/admin/v1/user/redact_status/{redact_id}`
21///
22/// Reports the stage of a bulk user-event redaction by its task id, including
23/// the per-event failures once the task has completed.
24pub(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}