Skip to main content

tuwunel_admin/media/
commands.rs

1use std::time::Duration;
2
3use ruma::{CanonicalJsonValue, Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName};
4use tuwunel_core::{
5	Err, Result, debug, err, error, info, trace,
6	utils::{math::Expected, time::parse_timepoint_ago},
7	warn,
8};
9use tuwunel_service::media::Dim;
10
11use crate::{admin_command, utils::parse_local_user_id};
12
13#[admin_command]
14pub(super) async fn delete(&self, mxc: OwnedMxcUri) -> Result {
15	self.services
16		.media
17		.delete(&mxc.as_str().try_into()?)
18		.await?;
19
20	Err!("Deleted the MXC from our database and on our filesystem.")
21}
22
23#[admin_command]
24pub(super) async fn delete_by_event(&self, event_id: OwnedEventId) -> Result {
25	let mut mxc_urls = Vec::with_capacity(3);
26
27	// parsing the PDU for any MXC URLs begins here
28	let event_json = self
29		.services
30		.timeline
31		.get_pdu_json(&event_id)
32		.await
33		.map_err(|_| err!("Event ID does not exist or is not known to us."))?;
34
35	let content = event_json
36		.get("content")
37		.and_then(CanonicalJsonValue::as_object)
38		.ok_or_else(|| {
39			err!(
40				"Event ID does not have a \"content\" key, this is not a message or an event \
41				 type that contains media.",
42			)
43		})?;
44
45	// 1. attempts to parse the "url" key
46	debug!("Attempting to go into \"url\" key for main media file");
47	if let Some(url) = content
48		.get("url")
49		.and_then(CanonicalJsonValue::as_str)
50	{
51		debug!("Got a URL in the event ID {event_id}: {url}");
52
53		mxc_urls.push(url.to_owned());
54	} else {
55		debug!("No main media found.");
56	}
57
58	// 2. attempts to parse the "info" key
59	debug!("Attempting to go into \"info\" key for thumbnails");
60	if let Some(thumbnail_url) = content
61		.get("info")
62		.and_then(CanonicalJsonValue::as_object)
63		.and_then(|info| info.get("thumbnail_url"))
64		.and_then(CanonicalJsonValue::as_str)
65	{
66		debug!("Found a thumbnail_url in info key: {thumbnail_url}");
67
68		mxc_urls.push(thumbnail_url.to_owned());
69	} else {
70		debug!("No thumbnails found.");
71	}
72
73	// 3. attempts to parse the "file" key
74	debug!("Attempting to go into \"file\" key");
75	if let Some(url) = content
76		.get("file")
77		.and_then(CanonicalJsonValue::as_object)
78		.and_then(|file| file.get("url"))
79		.and_then(CanonicalJsonValue::as_str)
80	{
81		debug!("Found url in file key: {url}");
82
83		mxc_urls.push(url.to_owned());
84	} else {
85		debug!("No \"url\" key in \"file\" key.");
86	}
87
88	if mxc_urls.is_empty() {
89		return Err!("Parsed event ID but found no MXC URLs.",);
90	}
91
92	let mut mxc_deletion_count: usize = 0;
93
94	for mxc_url in mxc_urls {
95		if !mxc_url.starts_with("mxc://") {
96			warn!("Ignoring non-mxc url {mxc_url}");
97			continue;
98		}
99
100		let mxc: Mxc<'_> = mxc_url.as_str().try_into()?;
101
102		match self.services.media.delete(&mxc).await {
103			| Ok(()) => {
104				info!("Successfully deleted {mxc_url} from filesystem and database");
105				mxc_deletion_count = mxc_deletion_count.saturating_add(1);
106			},
107			| Err(e) => {
108				warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}");
109			},
110		}
111	}
112
113	self.write_str(&format!(
114		"Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from \
115		 event ID {event_id}."
116	))
117	.await
118}
119
120#[admin_command]
121pub(super) async fn delete_list(&self) -> Result {
122	if self.body.len() < 2
123		|| !self.body[0].trim().starts_with("```")
124		|| self.body.last().unwrap_or(&"").trim() != "```"
125	{
126		return Err!("Expected code block in command body. Add --help for details.",);
127	}
128
129	let mut failed_parsed_mxcs: usize = 0;
130
131	let mxc_list = self
132		.body
133		.to_vec()
134		.drain(1..self.body.len().expected_sub(1))
135		.filter_map(|mxc_s| {
136			mxc_s
137				.try_into()
138				.inspect_err(|e| {
139					warn!("Failed to parse user-provided MXC URI: {e}");
140					failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1);
141				})
142				.ok()
143		})
144		.collect::<Vec<Mxc<'_>>>();
145
146	let mut mxc_deletion_count: usize = 0;
147
148	for mxc in &mxc_list {
149		trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk");
150		match self.services.media.delete(mxc).await {
151			| Ok(()) => {
152				info!("Successfully deleted {mxc} from filesystem and database");
153				mxc_deletion_count = mxc_deletion_count.saturating_add(1);
154			},
155			| Err(e) => {
156				warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
157				continue;
158			},
159		}
160	}
161
162	self.write_str(&format!(
163		"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database \
164		 and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from the database.",
165	))
166	.await
167}
168
169#[admin_command]
170pub(super) async fn delete_range(
171	&self,
172	duration: String,
173	older_than: bool,
174	newer_than: bool,
175	yes_i_want_to_delete_local_media: bool,
176) -> Result {
177	if older_than == newer_than {
178		return Err!("Please pick only one of --older_than or --newer_than.",);
179	}
180
181	let duration = parse_timepoint_ago(&duration)?;
182	let deleted_count = self
183		.services
184		.media
185		.delete_range(duration, older_than, newer_than, yes_i_want_to_delete_local_media)
186		.await?;
187
188	self.write_str(&format!("Deleted {deleted_count} total files."))
189		.await
190}
191
192#[admin_command]
193pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
194	let user_id = parse_local_user_id(self.services, &username)?;
195
196	let deleted_count = self
197		.services
198		.media
199		.delete_from_user(&user_id)
200		.await?;
201
202	self.write_str(&format!("Deleted {deleted_count} total files."))
203		.await
204}
205
206#[admin_command]
207pub(super) async fn delete_all_from_server(
208	&self,
209	server_name: OwnedServerName,
210	yes_i_want_to_delete_local_media: bool,
211) -> Result {
212	if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
213		return Err!("This command only works for remote media by default.",);
214	}
215
216	let Ok(all_mxcs) = self
217		.services
218		.media
219		.get_all_mxcs()
220		.await
221		.inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}"))
222	else {
223		return Err!("Failed to get MXC URIs from our database",);
224	};
225
226	let mut deleted_count: usize = 0;
227
228	for mxc in all_mxcs {
229		let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| {
230			warn!(
231				"Failed to parse MXC {mxc} server name from database, ignoring error and \
232				 skipping: {e}"
233			);
234		}) else {
235			continue;
236		};
237
238		if mxc_server_name != server_name {
239			trace!("skipping MXC URI {mxc}");
240			continue;
241		}
242
243		let mxc: Mxc<'_> = mxc.as_str().try_into()?;
244
245		match self.services.media.delete(&mxc).await {
246			| Ok(()) => {
247				deleted_count = deleted_count.saturating_add(1);
248			},
249			| Err(e) => {
250				warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
251			},
252		}
253	}
254
255	self.write_str(&format!("Deleted {deleted_count} total files."))
256		.await
257}
258
259#[admin_command]
260pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result {
261	let mxc: Mxc<'_> = mxc.as_str().try_into()?;
262	let metadata = self.services.media.get_metadata(&mxc).await;
263
264	self.write_str(&format!("```\n{metadata:#?}\n```"))
265		.await
266}
267
268#[admin_command]
269pub(super) async fn get_remote_file(
270	&self,
271	mxc: OwnedMxcUri,
272	server: Option<OwnedServerName>,
273	timeout: u32,
274) -> Result {
275	let mxc: Mxc<'_> = mxc.as_str().try_into()?;
276	let timeout = Duration::from_millis(timeout.into());
277	let mut result = self
278		.services
279		.media
280		.fetch_remote_content(&mxc, server.as_deref(), timeout)
281		.await?;
282
283	// Grab the length of the content before clearing it to not flood the output
284	let len = result.content.len();
285	result.content.clear();
286
287	self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
288		.await
289}
290
291#[admin_command]
292pub(super) async fn get_remote_thumbnail(
293	&self,
294	mxc: OwnedMxcUri,
295	server: Option<OwnedServerName>,
296	timeout: u32,
297	width: u32,
298	height: u32,
299) -> Result {
300	let mxc: Mxc<'_> = mxc.as_str().try_into()?;
301	let timeout = Duration::from_millis(timeout.into());
302	let dim = Dim::new(width, height, None);
303	let mut result = self
304		.services
305		.media
306		.fetch_remote_thumbnail(&mxc, server.as_deref(), timeout, &dim)
307		.await?;
308
309	// Grab the length of the content before clearing it to not flood the output
310	let len = result.content.len();
311	result.content.clear();
312
313	self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
314		.await
315}