tuwunel_api_admin/rooms/
purge_history.rs1use std::pin::pin;
2
3use axum::extract::State;
4use futures::StreamExt;
5use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, api::Direction};
6use synapse_admin_api::purge_history::{
7 purge::{
8 by_event::{Request as PurgeByEventRequest, Response as PurgeByEventResponse},
9 v1::{Request as PurgeRequest, Response as PurgeResponse},
10 },
11 status::v1::{PurgeStatus, Request as StatusRequest, Response as StatusResponse},
12};
13use tuwunel_core::{Err, Result, err};
14use tuwunel_matrix::pdu::PduCount;
15use tuwunel_service::tasks::Status;
16
17use crate::{Ruma, require_admin};
18
19pub(crate) async fn admin_purge_history_route(
24 State(services): State<crate::State>,
25 body: Ruma<PurgeRequest>,
26) -> Result<PurgeResponse> {
27 require_admin(&services, body.sender_user()).await?;
28
29 let boundary = resolve_boundary(
30 &services,
31 &body.room_id,
32 body.purge_up_to_event_id.as_deref(),
33 body.purge_up_to_ts,
34 )
35 .await?;
36
37 let purge_id =
38 schedule_purge(services, body.room_id.clone(), boundary, body.delete_local_events);
39
40 Ok(PurgeResponse { purge_id })
41}
42
43pub(crate) async fn admin_purge_history_by_event_route(
47 State(services): State<crate::State>,
48 body: Ruma<PurgeByEventRequest>,
49) -> Result<PurgeByEventResponse> {
50 require_admin(&services, body.sender_user()).await?;
51
52 let boundary = resolve_boundary(&services, &body.room_id, Some(&body.event_id), None).await?;
53
54 let purge_id =
55 schedule_purge(services, body.room_id.clone(), boundary, body.delete_local_events);
56
57 Ok(PurgeByEventResponse { purge_id })
58}
59
60pub(crate) async fn admin_purge_history_status_route(
65 State(services): State<crate::State>,
66 body: Ruma<StatusRequest>,
67) -> Result<StatusResponse> {
68 require_admin(&services, body.sender_user()).await?;
69
70 let task = services
71 .tasks
72 .get(&body.purge_id)
73 .filter(|task| task.action == super::PURGE_HISTORY_ACTION)
74 .ok_or_else(|| err!(Request(NotFound("Unknown purge task"))))?;
75
76 Ok(StatusResponse {
77 status: purge_status(task.status),
78 error: task.error,
79 })
80}
81
82async fn resolve_boundary(
86 services: &crate::State,
87 room_id: &RoomId,
88 event_id: Option<&EventId>,
89 ts: Option<MilliSecondsSinceUnixEpoch>,
90) -> Result<PduCount> {
91 if let Some(event_id) = event_id {
92 let pdu = services
93 .timeline
94 .get_pdu(event_id)
95 .await
96 .map_err(|_| err!(Request(NotFound("Event not found"))))?;
97
98 if pdu.room_id != *room_id {
99 return Err!(Request(BadJson("Event is for wrong room")));
100 }
101
102 return services
103 .timeline
104 .get_pdu_count(event_id)
105 .await
106 .map_err(|_| err!(Request(NotFound("Event not found"))));
107 }
108
109 let Some(ts) = ts else {
110 return Err!(Request(BadJson(
111 "One of purge_up_to_event_id or purge_up_to_ts must be provided"
112 )));
113 };
114
115 let events = services
116 .timeline
117 .pdus_near_ts(None, room_id, ts, Direction::Backward);
118
119 let mut events = pin!(events);
120
121 events
122 .next()
123 .await
124 .transpose()?
125 .map(|(count, _)| count)
126 .ok_or_else(|| err!(Request(NotFound("No event found before the given timestamp"))))
127}
128
129fn schedule_purge(
132 services: crate::State,
133 room_id: OwnedRoomId,
134 boundary: PduCount,
135 delete_local_events: bool,
136) -> String {
137 let resource_id = room_id.to_string();
138
139 let work = async move {
140 let purged = services
141 .timeline
142 .purge_history(&room_id, boundary, delete_local_events)
143 .await?;
144
145 Ok(serde_json::json!({ "purged": purged }))
146 };
147
148 services
149 .tasks
150 .spawn(super::PURGE_HISTORY_ACTION, resource_id, work)
151 .to_string()
152}
153
154fn purge_status(status: Status) -> PurgeStatus {
155 match status {
156 | Status::Scheduled | Status::Active => PurgeStatus::Active,
157 | Status::Complete => PurgeStatus::Complete,
158 | Status::Failed => PurgeStatus::Failed,
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use serde_json::json;
165 use tuwunel_service::tasks::Status;
166
167 use super::purge_status;
168
169 #[test]
170 fn collapses_scheduled_into_active() {
171 let status = |status| serde_json::to_value(purge_status(status)).unwrap();
172
173 assert_eq!(status(Status::Scheduled), json!("active"));
174 assert_eq!(status(Status::Active), json!("active"));
175 assert_eq!(status(Status::Complete), json!("complete"));
176 assert_eq!(status(Status::Failed), json!("failed"));
177 }
178}