tuwunel_api/client/admin/media/
list_room_media.rs1use std::iter::once;
2
3use axum::extract::State;
4use ruma::{Mxc, OwnedMxcUri, ServerName};
5use serde::Deserialize;
6use serde_json::Value as JsonValue;
7use synapse_admin_api::media::list_room_media::v1::{Request, Response};
8use tuwunel_core::{
9 Result,
10 matrix::Event,
11 utils::{ReadyExt, stream::TryIgnore},
12};
13
14use crate::{Ruma, client::admin::require_admin};
15
16type MxcLists = (Vec<OwnedMxcUri>, Vec<OwnedMxcUri>);
17
18#[derive(Deserialize)]
19struct ExtractUrls {
20 url: Option<String>,
21 info: Option<JsonValue>,
22}
23
24pub(crate) async fn admin_list_room_media_route(
29 State(services): State<crate::State>,
30 body: Ruma<Request>,
31) -> Result<Response> {
32 require_admin(&services, body.sender_user()).await?;
33
34 let server_name = services.globals.server_name();
35
36 let (local, remote) = services
37 .timeline
38 .pdus_rev(None, &body.room_id, None)
39 .ignore_err()
40 .ready_filter_map(|(_, pdu)| pdu.get_content().ok())
41 .ready_fold_default(|lists, content: ExtractUrls| {
42 collect_urls(lists, server_name, &content)
43 })
44 .await;
45
46 Ok(Response { local, remote })
47}
48
49fn collect_urls(
54 (mut local, mut remote): MxcLists,
55 server_name: &ServerName,
56 content: &ExtractUrls,
57) -> MxcLists {
58 let Some(url) = content.url.as_deref() else {
59 return (local, remote);
60 };
61
62 let thumbnail_url = content
63 .info
64 .as_ref()
65 .and_then(JsonValue::as_object)
66 .and_then(|info| info.get("thumbnail_url"))
67 .and_then(JsonValue::as_str);
68
69 once(url)
70 .chain(thumbnail_url)
71 .filter_map(|url| Mxc::try_from(url).ok().map(|mxc| (url, mxc)))
72 .for_each(|(url, mxc)| {
73 if mxc.server_name == server_name {
74 local.push(url.into());
75 } else {
76 remote.push(url.into());
77 }
78 });
79
80 (local, remote)
81}
82
83#[cfg(test)]
84mod tests {
85 use ruma::server_name;
86 use serde_json::json;
87
88 use super::{ExtractUrls, MxcLists, collect_urls};
89
90 fn collect(content: serde_json::Value) -> MxcLists {
91 let content: ExtractUrls = serde_json::from_value(content).expect("valid ExtractUrls");
92
93 collect_urls(MxcLists::default(), server_name!("example.org"), &content)
94 }
95
96 #[test]
97 fn url_and_thumbnail_split_by_origin() {
98 let (local, remote) = collect(json!({
99 "url": "mxc://example.org/abc",
100 "info": { "thumbnail_url": "mxc://remote.example/def" },
101 }));
102
103 assert_eq!(local, ["mxc://example.org/abc"]);
104 assert_eq!(remote, ["mxc://remote.example/def"]);
105 }
106
107 #[test]
108 fn thumbnail_only_event_contributes_nothing() {
109 let (local, remote) = collect(json!({
110 "info": { "thumbnail_url": "mxc://example.org/xyz" },
111 }));
112
113 assert!(local.is_empty(), "{local:?}");
114 assert!(remote.is_empty(), "{remote:?}");
115 }
116
117 #[test]
118 fn non_object_info_still_lists_url() {
119 let (local, remote) = collect(json!({
120 "url": "mxc://example.org/abc",
121 "info": "weird",
122 }));
123
124 assert_eq!(local, ["mxc://example.org/abc"]);
125 assert!(remote.is_empty(), "{remote:?}");
126 }
127
128 #[test]
129 fn non_mxc_and_invalid_urls_skipped() {
130 let (local, remote) = collect(json!({
131 "url": "https://example.org/pic.png",
132 "info": { "thumbnail_url": "mxc://example.org/has/slash" },
133 }));
134
135 assert!(local.is_empty(), "{local:?}");
136 assert!(remote.is_empty(), "{remote:?}");
137 }
138
139 #[test]
140 fn empty_url_still_lists_thumbnail() {
141 let (local, remote) = collect(json!({
142 "url": "",
143 "info": { "thumbnail_url": "mxc://remote.example/def" },
144 }));
145
146 assert!(local.is_empty(), "{local:?}");
147 assert_eq!(remote, ["mxc://remote.example/def"]);
148 }
149
150 #[test]
151 fn duplicates_preserved_across_events() {
152 let content: ExtractUrls =
153 serde_json::from_value(json!({ "url": "mxc://example.org/abc" }))
154 .expect("valid ExtractUrls");
155
156 let lists = collect_urls(MxcLists::default(), server_name!("example.org"), &content);
157 let (local, remote) = collect_urls(lists, server_name!("example.org"), &content);
158
159 assert_eq!(local, ["mxc://example.org/abc", "mxc://example.org/abc"]);
160 assert!(remote.is_empty(), "{remote:?}");
161 }
162
163 #[test]
164 fn non_string_url_fails_the_row_filter() {
165 let result = serde_json::from_value::<ExtractUrls>(json!({ "url": 5 }));
166
167 assert!(result.is_err());
168 }
169}