From a189b66ca6eaeef2c51cd249c7074a06f1ec27ec Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Tue, 6 May 2025 00:47:20 +0100 Subject: [PATCH] feat(admin): show media command --- Cargo.lock | 27 ++++- Cargo.toml | 1 + src/api/client_server/media.rs | 2 +- src/api/client_server/mod.rs | 2 +- src/service/admin/mod.rs | 175 ++++++++++++++++++++++----------- 5 files changed, 145 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bd25569..af950d48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -396,6 +396,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -1401,12 +1407,23 @@ dependencies = [ "byteorder", "color_quant", "gif", + "image-webp", "num-traits", "png", "zune-core", "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2107,6 +2124,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.36" @@ -2259,7 +2282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", - "quick-error", + "quick-error 1.2.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 511a6f4c..f883be9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ image = { version = "0.25", default-features = false, features = [ "gif", "jpeg", "png", + "webp", ] } # Used for creating media filenames hex = "0.4" diff --git a/src/api/client_server/media.rs b/src/api/client_server/media.rs index d4c8738d..4b55a511 100644 --- a/src/api/client_server/media.rs +++ b/src/api/client_server/media.rs @@ -193,7 +193,7 @@ pub async fn get_content_auth_route( get_content(&body.server_name, body.media_id.clone(), true, true).await } -async fn get_content( +pub async fn get_content( server_name: &ServerName, media_id: String, allow_remote: bool, diff --git a/src/api/client_server/mod.rs b/src/api/client_server/mod.rs index e5d0a5d5..e99bc048 100644 --- a/src/api/client_server/mod.rs +++ b/src/api/client_server/mod.rs @@ -9,7 +9,7 @@ mod device; mod directory; mod filter; mod keys; -mod media; +pub mod media; mod membership; mod message; mod openid; diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 06e6d047..ba58044a 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -9,6 +9,7 @@ use std::{ use bytesize::ByteSize; use chrono::DateTime; use clap::{Args, Parser}; +use image::GenericImageView; use regex::Regex; use ruma::{ api::appservice::Registration, @@ -20,21 +21,25 @@ use ruma::{ history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, - message::RoomMessageEventContent, + message::{ + FileMessageEventContent, ImageMessageEventContent, MessageType, + RoomMessageEventContent, + }, name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, topic::RoomTopicEventContent, + MediaSource, }, TimelineEventType, }, - EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, - RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, + EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, + OwnedServerName, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::{mpsc, Mutex, RwLock}; use crate::{ - api::client_server::{leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH}, + api::client_server::{self, leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH}, services, utils::{self, HtmlEscape}, Error, PduEvent, Result, @@ -42,7 +47,7 @@ use crate::{ use super::{ media::{ - BlockedMediaInfo, FileInfo, MediaListItem, MediaQuery, MediaQueryFileInfo, + size, BlockedMediaInfo, FileInfo, MediaListItem, MediaQuery, MediaQueryFileInfo, MediaQueryThumbInfo, ServerNameOrUserId, }, pdu::PduBuilder, @@ -138,6 +143,12 @@ enum AdminCommand { mxc: Box, }, + /// Sends a message with the requested media attached, so that you can view it easily + ShowMedia { + /// The MXC URI of the media you want to view + mxc: Box, + }, + /// Lists all the media matching the specified requirements ListMedia { #[command(flatten)] @@ -439,8 +450,8 @@ impl Service { tokio::select! { Some(event) = receiver.recv() => { let message_content = match event { - AdminRoomEvent::SendMessage(content) => content, - AdminRoomEvent::ProcessMessage(room_message) => self.process_admin_message(room_message).await + AdminRoomEvent::SendMessage(content) => content.into(), + AdminRoomEvent::ProcessMessage(room_message) => self.process_admin_message(room_message).await, }; let mutex_state = Arc::clone( @@ -491,7 +502,7 @@ impl Service { } // Parse and process a message from the admin room - async fn process_admin_message(&self, room_message: String) -> RoomMessageEventContent { + async fn process_admin_message(&self, room_message: String) -> MessageType { let mut lines = room_message.lines().filter(|l| !l.trim().is_empty()); let command_line = lines.next().expect("each string has at least one line"); let body: Vec<_> = lines.collect(); @@ -503,7 +514,7 @@ impl Service { let message = error.replace("server.name", server_name.as_str()); let html_message = self.usage_to_html(&message, server_name); - return RoomMessageEventContent::text_html(message, html_message); + return RoomMessageEventContent::text_html(message, html_message).into(); } }; @@ -519,7 +530,7 @@ impl Service {
\n{error}\n
", ); - RoomMessageEventContent::text_html(markdown_message, html_message) + RoomMessageEventContent::text_html(markdown_message, html_message).into() } } } @@ -550,7 +561,7 @@ impl Service { &self, command: AdminCommand, body: Vec<&str>, - ) -> Result { + ) -> Result { let reply_message_content = match command { AdminCommand::RegisterAppservice => { if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" @@ -574,7 +585,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::UnregisterAppservice { appservice_identifier, @@ -587,7 +598,7 @@ impl Service { Err(e) => RoomMessageEventContent::text_plain(format!( "Failed to unregister appservice: {e}" )), - }, + }.into(), AdminCommand::ListAppservices => { let appservices = services().appservice.iter_ids().await; let output = format!( @@ -595,7 +606,7 @@ impl Service { appservices.len(), appservices.join(", ") ); - RoomMessageEventContent::text_plain(output) + RoomMessageEventContent::text_plain(output).into() } AdminCommand::ListRooms => { let room_ids = services().rooms.metadata.iter_ids(); @@ -616,7 +627,7 @@ impl Service { .collect::>() .join("\n") ); - RoomMessageEventContent::text_plain(output) + RoomMessageEventContent::text_plain(output).into() } AdminCommand::ListLocalUsers => match services().users.list_local_users() { Ok(users) => { @@ -625,7 +636,7 @@ impl Service { RoomMessageEventContent::text_plain(&msg) } Err(e) => RoomMessageEventContent::text_plain(e.to_string()), - }, + }.into(), AdminCommand::IncomingFederation => { let map = services().globals.roomid_federationhandletime.read().await; let mut msg: String = format!("Handling {} incoming pdus:\n", map.len()); @@ -640,7 +651,7 @@ impl Service { elapsed.as_secs() % 60 ); } - RoomMessageEventContent::text_plain(&msg) + RoomMessageEventContent::text_plain(&msg).into() } AdminCommand::GetAuthChain { event_id } => { let event_id = Arc::::from(event_id); @@ -666,7 +677,7 @@ impl Service { )) } else { RoomMessageEventContent::text_plain("Event not found.") - } + }.into() } AdminCommand::ParsePdu => { if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" @@ -700,7 +711,7 @@ impl Service { } } else { RoomMessageEventContent::text_plain("Expected code block in command body.") - } + }.into() } AdminCommand::GetPdu { event_id } => { let mut outlier = false; @@ -738,7 +749,7 @@ impl Service { ) } None => RoomMessageEventContent::text_plain("PDU not found."), - } + }.into() } AdminCommand::MemoryUsage => { let response1 = services().memory_usage().await; @@ -746,21 +757,21 @@ impl Service { RoomMessageEventContent::text_plain(format!( "Services:\n{response1}\n\nDatabase:\n{response2}" - )) + )).into() } AdminCommand::ClearDatabaseCaches { amount } => { services().globals.db.clear_caches(amount); - RoomMessageEventContent::text_plain("Done.") + RoomMessageEventContent::text_plain("Done.").into() } AdminCommand::ClearServiceCaches { amount } => { services().clear_caches(amount).await; - RoomMessageEventContent::text_plain("Done.") + RoomMessageEventContent::text_plain("Done.").into() } AdminCommand::ShowConfig => { // Construct and send the response - RoomMessageEventContent::text_plain(format!("{}", services().globals.config)) + RoomMessageEventContent::text_plain(format!("{}", services().globals.config)).into() } AdminCommand::ResetPassword { username } => { let user_id = match UserId::parse_with_server_name( @@ -771,7 +782,7 @@ impl Service { Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" - ))) + )).into()) } }; @@ -779,7 +790,7 @@ impl Service { if user_id.server_name() != services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain( "The specified user is not from this server!", - )); + ).into()); }; // Check if the specified user is valid @@ -793,7 +804,7 @@ impl Service { { return Ok(RoomMessageEventContent::text_plain( "The specified user does not exist!", - )); + ).into()); } let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); @@ -808,7 +819,7 @@ impl Service { Err(e) => RoomMessageEventContent::text_plain(format!( "Couldn't reset the password for user {user_id}: {e}" )), - } + }.into() } AdminCommand::CreateUser { username, password } => { let password = @@ -822,7 +833,7 @@ impl Service { Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" - ))) + )).into()) } }; @@ -830,18 +841,18 @@ impl Service { if user_id.server_name() != services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain( "The specified user is not from this server!", - )); + ).into()); }; if user_id.is_historical() { return Ok(RoomMessageEventContent::text_plain(format!( "Userid {user_id} is not allowed due to historical" - ))); + )).into()); } if services().users.exists(&user_id)? { return Ok(RoomMessageEventContent::text_plain(format!( "Userid {user_id} already exists" - ))); + )).into()); } // Create user services().users.create(&user_id, Some(password.as_str()))?; @@ -878,7 +889,7 @@ impl Service { // Inhibit login does not work for guests RoomMessageEventContent::text_plain(format!( "Created user with user_id: {user_id} and password: {password}" - )) + )).into() } AdminCommand::AllowRegistration { status } => { if let Some(status) = status { @@ -896,15 +907,15 @@ impl Service { "Registration is currently disabled" }, ) - } + }.into() } AdminCommand::DisableRoom { room_id } => { services().rooms.metadata.disable_room(&room_id, true)?; - RoomMessageEventContent::text_plain("Room disabled.") + RoomMessageEventContent::text_plain("Room disabled.").into() } AdminCommand::EnableRoom { room_id } => { services().rooms.metadata.disable_room(&room_id, false)?; - RoomMessageEventContent::text_plain("Room enabled.") + RoomMessageEventContent::text_plain("Room enabled.").into() } AdminCommand::DeactivateUser { leave_rooms, @@ -954,7 +965,7 @@ impl Service { "User {user_id} has been deactivated, but {failed_purged_media} media failed to be purged, check the logs for more details" )) } - } + }.into() } AdminCommand::DeactivateAll { leave_rooms, @@ -1027,11 +1038,11 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::QueryMedia { mxc } => { let Ok((server_name, media_id)) = mxc.parts() else { - return Ok(RoomMessageEventContent::text_plain("Invalid media MXC")); + return Ok(RoomMessageEventContent::text_plain("Invalid media MXC").into()); }; let MediaQuery{ is_blocked, source_file, thumbnails } = services().media.query(server_name, media_id)?; @@ -1118,7 +1129,55 @@ impl Service { } } - RoomMessageEventContent::text_plain(message) + RoomMessageEventContent::text_plain(message).into() + } + AdminCommand::ShowMedia { mxc } => { + let Ok((server_name, media_id)) = mxc.parts() else { + return Ok(RoomMessageEventContent::text_plain("Invalid media MXC").into()); + }; + + // TODO: Bypass blocking once MSC3911 is implemented (linking media to events) + let ruma::api::client::authenticated_media::get_content::v1::Response { + file, + content_type, + content_disposition, + } = client_server::media::get_content(server_name, media_id.to_owned(), true, true).await?; + + if let Ok(image) = image::load_from_memory(&file) { + let filename = content_disposition.and_then(|cd| cd.filename); + let (width, height) = image.dimensions(); + + MessageType::Image(ImageMessageEventContent { + body: filename.clone().unwrap_or_default(), + formatted: None, + filename, + source: MediaSource::Plain(OwnedMxcUri::from(mxc.to_owned())), + info: Some(Box::new(ruma::events::room::ImageInfo { + height: Some(height.into()), + width: Some(width.into()), + mimetype: content_type, + size: size(&file)?.try_into().ok(), + thumbnail_info: None, + thumbnail_source: None, + blurhash: None, + })), + }) + } else { + let filename = content_disposition.and_then(|cd| cd.filename); + + MessageType::File(FileMessageEventContent { + body: filename.clone().unwrap_or_default(), + formatted: None, + filename, + source: MediaSource::Plain(OwnedMxcUri::from(mxc.to_owned())), + info: Some(Box::new(ruma::events::room::message::FileInfo { + mimetype: content_type, + size: size(&file)?.try_into().ok(), + thumbnail_info: None, + thumbnail_source: None, + })), + }) + } } AdminCommand::ListMedia { user_server_filter: ListMediaArgs { @@ -1183,7 +1242,7 @@ impl Service { html_message.push_str(""); - RoomMessageEventContent::text_html(markdown_message, html_message) + RoomMessageEventContent::text_html(markdown_message, html_message).into() }, AdminCommand::PurgeMedia => media_from_body(body).map_or_else( |message| message, @@ -1196,7 +1255,7 @@ impl Service { RoomMessageEventContent::text_plain(format!( "Failed to delete {failed_count} media, check logs for more details" )) - } + }.into() }, ), AdminCommand::PurgeMediaFromUsers { @@ -1232,7 +1291,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::PurgeMediaFromServer { server_id: server_name, @@ -1260,7 +1319,7 @@ impl Service { RoomMessageEventContent::text_plain(format!( "Failed to purge {failed_count} media, check logs for more details" )) - } + }.into() } AdminCommand::BlockMedia { and_purge, reason } => media_from_body(body).map_or_else( |message| message, @@ -1283,7 +1342,7 @@ impl Service { (false, false) => RoomMessageEventContent::text_plain(format!( "Failed to block {failed_count}, and purge {failed_purge_count} media, check logs for more details" )) - } + }.into() }, ), AdminCommand::BlockMediaFromUsers { from_last, reason } => { @@ -1321,7 +1380,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::ListBlockedMedia => { let mut markdown_message = String::from( @@ -1361,7 +1420,7 @@ impl Service { html_message.push_str(""); - RoomMessageEventContent::text_html(markdown_message, html_message) + RoomMessageEventContent::text_html(markdown_message, html_message).into() } AdminCommand::UnblockMedia => media_from_body(body).map_or_else( |message| message, @@ -1374,7 +1433,7 @@ impl Service { RoomMessageEventContent::text_plain(format!( "Failed to unblock {failed_count} media, check logs for more details" )) - } + }.into() }, ), AdminCommand::SignJson => { @@ -1399,7 +1458,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::VerifyJson => { if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" @@ -1460,7 +1519,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::HashAndSignEvent { room_version_id } => { if body.len() > 2 @@ -1490,7 +1549,7 @@ impl Service { RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", ) - } + }.into() } AdminCommand::RemoveAlias { alias } => { if alias.server_name() != services().globals.server_name() { @@ -1515,7 +1574,7 @@ impl Service { .alias .remove_alias(&alias, services().globals.server_user())?; RoomMessageEventContent::text_plain("Alias removed successfully") - } + }.into() } }; @@ -2010,7 +2069,7 @@ impl Service { fn userids_from_body<'a>( body: &'a [&'a str], -) -> Result, RoomMessageEventContent>, Error> { +) -> Result, MessageType>, Error> { let users = body.to_owned().drain(1..body.len() - 1).collect::>(); let mut user_ids = Vec::new(); @@ -2071,15 +2130,14 @@ fn userids_from_body<'a>( return Ok(Err(RoomMessageEventContent::text_html( markdown_message, html_message, - ))); + ) + .into())); } Ok(Ok(user_ids)) } -fn media_from_body( - body: Vec<&str>, -) -> Result, RoomMessageEventContent> { +fn media_from_body(body: Vec<&str>) -> Result, MessageType> { if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" { Ok(body .clone() @@ -2094,7 +2152,8 @@ fn media_from_body( } else { Err(RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", - )) + ) + .into()) } }