diff --git a/src/admin/appservice/appservice_command.rs b/src/admin/appservice/appservice_command.rs index d15cb7a6da3e0749a9db4a25b44cb3f4254abf18..6f36ebf73b327fad7b5b7f0c1755b4bae0b7f1f7 100644 --- a/src/admin/appservice/appservice_command.rs +++ b/src/admin/appservice/appservice_command.rs @@ -3,26 +3,26 @@ use crate::{escape_html, services, Result}; pub(crate) async fn register(body: Vec<&str>) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let appservice_config = body[1..body.len().checked_sub(1).unwrap()].join("\n"); - let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config); - match parsed_config { - Ok(yaml) => match services().appservice.register_appservice(yaml).await { - Ok(id) => Ok(RoomMessageEventContent::text_plain(format!( - "Appservice registered with ID: {id}." - ))), - Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "Failed to register appservice: {e}" - ))), - }, + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } + + let appservice_config = body[1..body.len().checked_sub(1).unwrap()].join("\n"); + let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config); + match parsed_config { + Ok(yaml) => match services().appservice.register_appservice(yaml).await { + Ok(id) => Ok(RoomMessageEventContent::text_plain(format!( + "Appservice registered with ID: {id}." + ))), Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "Could not parse appservice config: {e}" + "Failed to register appservice: {e}" ))), - } - } else { - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!( + "Could not parse appservice config: {e}" + ))), } } diff --git a/src/admin/debug/debug_commands.rs b/src/admin/debug/debug_commands.rs index 4522b678be0038f420acb44192a55738e8590891..bd15ba50aab513c2ddf8be6c2aeab76b3ef92c05 100644 --- a/src/admin/debug/debug_commands.rs +++ b/src/admin/debug/debug_commands.rs @@ -1,9 +1,15 @@ -use std::{collections::BTreeMap, sync::Arc, time::Instant}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, + time::Instant, +}; +use api::client::validate_and_add_event_id; use conduit::{utils::HtmlEscape, Error, Result}; use ruma::{ - api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId, - RoomId, RoomVersionId, ServerName, + api::{client::error::ErrorKind, federation::event::get_room_state}, + events::room::message::RoomMessageEventContent, + CanonicalJsonObject, EventId, RoomId, RoomVersionId, ServerName, }; use service::{rooms::event_handler::parse_incoming_pdu, sending::resolve::resolve_actual_dest, services, PduEvent}; use tokio::sync::RwLock; @@ -37,28 +43,30 @@ pub(crate) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) -> } pub(crate) async fn parse_pdu(body: Vec<&str>) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let string = body[1..body.len() - 1].join("\n"); - match serde_json::from_str(&string) { - Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) { - Ok(hash) => { - let event_id = EventId::parse(format!("${hash}")); - - match serde_json::from_value::<PduEvent>(serde_json::to_value(value).expect("value is json")) { - Ok(pdu) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))), - Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "EventId: {event_id:?}\nCould not parse event: {e}" - ))), - } - }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))), + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } + + let string = body[1..body.len() - 1].join("\n"); + match serde_json::from_str(&string) { + Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) { + Ok(hash) => { + let event_id = EventId::parse(format!("${hash}")); + + match serde_json::from_value::<PduEvent>(serde_json::to_value(value).expect("value is json")) { + Ok(pdu) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))), + Err(e) => Ok(RoomMessageEventContent::text_plain(format!( + "EventId: {event_id:?}\nCould not parse event: {e}" + ))), + } }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "Invalid json in command body: {e}" - ))), - } - } else { - Ok(RoomMessageEventContent::text_plain("Expected code block in command body.")) + Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))), + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!( + "Invalid json in command body: {e}" + ))), } } @@ -111,33 +119,40 @@ pub(crate) async fn get_remote_pdu_list( if server == services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain( - "Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs.", + "Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs from \ + the database.", )); } - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let list = body - .clone() - .drain(1..body.len().checked_sub(1).unwrap()) - .filter_map(|pdu| EventId::parse(pdu).ok()) - .collect::<Vec<_>>(); - - for pdu in list { - if force { - if let Err(e) = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await { - warn!(%e, "Failed to get remote PDU, ignoring error"); - } - } else { - get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?; + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } + + let list = body + .clone() + .drain(1..body.len().checked_sub(1).unwrap()) + .filter_map(|pdu| EventId::parse(pdu).ok()) + .collect::<Vec<_>>(); + + for pdu in list { + if force { + if let Err(e) = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "Failed to get remote PDU, ignoring error: {e}" + ))) + .await; + warn!(%e, "Failed to get remote PDU, ignoring error"); } + } else { + get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?; } - - return Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs.")); } - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) + Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs.")) } pub(crate) async fn get_remote_pdu( @@ -378,55 +393,55 @@ pub(crate) async fn change_log_level( } pub(crate) async fn sign_json(body: Vec<&str>) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let string = body[1..body.len().checked_sub(1).unwrap()].join("\n"); - match serde_json::from_str(&string) { - Ok(mut value) => { - ruma::signatures::sign_json( - services().globals.server_name().as_str(), - services().globals.keypair(), - &mut value, - ) - .expect("our request json is what ruma expects"); - let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json"); - Ok(RoomMessageEventContent::text_plain(json_text)) - }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), - } - } else { - Ok(RoomMessageEventContent::text_plain( + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", - )) + )); + } + + let string = body[1..body.len().checked_sub(1).unwrap()].join("\n"); + match serde_json::from_str(&string) { + Ok(mut value) => { + ruma::signatures::sign_json( + services().globals.server_name().as_str(), + services().globals.keypair(), + &mut value, + ) + .expect("our request json is what ruma expects"); + let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json"); + Ok(RoomMessageEventContent::text_plain(json_text)) + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), } } pub(crate) async fn verify_json(body: Vec<&str>) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let string = body[1..body.len().checked_sub(1).unwrap()].join("\n"); - match serde_json::from_str(&string) { - Ok(value) => { - let pub_key_map = RwLock::new(BTreeMap::new()); - - services() - .rooms - .event_handler - .fetch_required_signing_keys([&value], &pub_key_map) - .await?; - - let pub_key_map = pub_key_map.read().await; - match ruma::signatures::verify_json(&pub_key_map, &value) { - Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")), - Err(e) => Ok(RoomMessageEventContent::text_plain(format!( - "Signature verification failed: {e}" - ))), - } - }, - Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), - } - } else { - Ok(RoomMessageEventContent::text_plain( + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", - )) + )); + } + + let string = body[1..body.len().checked_sub(1).unwrap()].join("\n"); + match serde_json::from_str(&string) { + Ok(value) => { + let pub_key_map = RwLock::new(BTreeMap::new()); + + services() + .rooms + .event_handler + .fetch_required_signing_keys([&value], &pub_key_map) + .await?; + + let pub_key_map = pub_key_map.read().await; + match ruma::signatures::verify_json(&pub_key_map, &value) { + Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")), + Err(e) => Ok(RoomMessageEventContent::text_plain(format!( + "Signature verification failed: {e}" + ))), + } + }, + Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))), } } @@ -472,6 +487,147 @@ pub(crate) async fn latest_pdu_in_room(_body: Vec<&str>, room_id: Box<RoomId>) - Ok(RoomMessageEventContent::text_plain(format!("{latest_pdu:?}"))) } +#[tracing::instrument(skip(_body))] +pub(crate) async fn force_set_room_state_from_server( + _body: Vec<&str>, server_name: Box<ServerName>, room_id: Box<RoomId>, +) -> Result<RoomMessageEventContent> { + if !services() + .rooms + .state_cache + .server_in_room(&services().globals.config.server_name, &room_id)? + { + return Ok(RoomMessageEventContent::text_plain( + "We are not participating in the room / we don't know about the room ID.", + )); + } + + let first_pdu = services() + .rooms + .timeline + .latest_pdu_in_room(&room_id)? + .ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?; + + let room_version = services().rooms.state.get_room_version(&room_id)?; + + let mut state: HashMap<u64, Arc<EventId>> = HashMap::new(); + let pub_key_map = RwLock::new(BTreeMap::new()); + + let remote_state_response = services() + .sending + .send_federation_request( + &server_name, + get_room_state::v1::Request { + room_id: room_id.clone().into(), + event_id: first_pdu.event_id.clone().into(), + }, + ) + .await?; + + let mut events = Vec::with_capacity(remote_state_response.pdus.len()); + + for pdu in remote_state_response.pdus.clone() { + events.push(match parse_incoming_pdu(&pdu) { + Ok(t) => t, + Err(e) => { + warn!("Could not parse PDU, ignoring: {e}"); + continue; + }, + }); + } + + info!("Fetching required signing keys for all the state events we got"); + services() + .rooms + .event_handler + .fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map) + .await?; + + info!("Going through room_state response PDUs"); + for result in remote_state_response + .pdus + .iter() + .map(|pdu| validate_and_add_event_id(pdu, &room_version, &pub_key_map)) + { + let Ok((event_id, value)) = result.await else { + continue; + }; + + let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| { + warn!("Invalid PDU in fetching remote room state PDUs response: {} {:?}", e, value); + Error::BadServerResponse("Invalid PDU in send_join response.") + })?; + + services() + .rooms + .outlier + .add_pdu_outlier(&event_id, &value)?; + if let Some(state_key) = &pdu.state_key { + let shortstatekey = services() + .rooms + .short + .get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)?; + state.insert(shortstatekey, pdu.event_id.clone()); + } + } + + info!("Going through auth_chain response"); + for result in remote_state_response + .auth_chain + .iter() + .map(|pdu| validate_and_add_event_id(pdu, &room_version, &pub_key_map)) + { + let Ok((event_id, value)) = result.await else { + continue; + }; + + services() + .rooms + .outlier + .add_pdu_outlier(&event_id, &value)?; + } + + let new_room_state = services() + .rooms + .event_handler + .resolve_state(room_id.clone().as_ref(), &room_version, state) + .await?; + + info!("Forcing new room state"); + let (short_state_hash, new, removed) = services() + .rooms + .state_compressor + .save_state(room_id.clone().as_ref(), new_room_state)?; + + let mutex_state = Arc::clone( + services() + .globals + .roomid_mutex_state + .write() + .await + .entry(room_id.clone().into()) + .or_default(), + ); + let state_lock = mutex_state.lock().await; + + services() + .rooms + .state + .force_state(room_id.clone().as_ref(), short_state_hash, new, removed, &state_lock) + .await?; + + info!( + "Updating joined counts for room just in case (e.g. we may have found a difference in the room's \ + m.room.member state" + ); + services().rooms.state_cache.update_joined_count(&room_id)?; + + drop(state_lock); + + Ok(RoomMessageEventContent::text_plain( + "Successfully forced the room state from the requested remote server.", + )) +} + pub(crate) async fn resolve_true_destination( _body: Vec<&str>, server_name: Box<ServerName>, no_cache: bool, ) -> Result<RoomMessageEventContent> { diff --git a/src/admin/debug/mod.rs b/src/admin/debug/mod.rs index 614a4ae5542006369876fb9ac6664db04a626cf0..afcc49b00bb2f52947972296037bc91666dac761 100644 --- a/src/admin/debug/mod.rs +++ b/src/admin/debug/mod.rs @@ -1,5 +1,5 @@ use clap::Subcommand; -use debug_commands::{first_pdu_in_room, latest_pdu_in_room}; +use debug_commands::{first_pdu_in_room, force_set_room_state_from_server, latest_pdu_in_room}; use ruma::{events::room::message::RoomMessageEventContent, EventId, RoomId, ServerName}; use self::debug_commands::{ @@ -123,6 +123,27 @@ pub(crate) enum DebugCommand { room_id: Box<RoomId>, }, + /// - Forcefully replaces the room state of our local copy of the specified + /// room, with the copy (auth chain and room state events) the specified + /// remote server says. + /// + /// A common desire for room deletion is to simply "reset" our copy of the + /// room. While this admin command is not a replacement for that, if you + /// know you have split/broken room state and you know another server in the + /// room that has the best/working room state, this command can let you use + /// their room state. Such example is your server saying users are in a + /// room, but other servers are saying they're not in the room in question. + /// + /// This command will get the latest PDU in the room we know about, and + /// request the room state at that point in time via + /// `/_matrix/federation/v1/state/{roomId}`. + ForceSetRoomStateFromServer { + /// The impacted room ID + room_id: Box<RoomId>, + /// The server we will use to query the room state for + server_name: Box<ServerName>, + }, + /// - Runs a server name through conduwuit's true destination resolution /// process /// @@ -174,6 +195,10 @@ pub(crate) async fn process(command: DebugCommand, body: Vec<&str>) -> Result<Ro server, force, } => get_remote_pdu_list(body, server, force).await?, + DebugCommand::ForceSetRoomStateFromServer { + room_id, + server_name, + } => force_set_room_state_from_server(body, server_name, room_id).await?, DebugCommand::ResolveTrueDestination { server_name, no_cache, diff --git a/src/admin/federation/federation_commands.rs b/src/admin/federation/federation_commands.rs index 3405b6fab539e809cd475aaf2fc41cb682dbadce..b52a62f29c3fe1707b5ba1e64abb52fdeef7f438 100644 --- a/src/admin/federation/federation_commands.rs +++ b/src/admin/federation/federation_commands.rs @@ -14,7 +14,7 @@ pub(crate) async fn enable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Resul Ok(RoomMessageEventContent::text_plain("Room enabled.")) } -pub(crate) async fn incoming_federeation(_body: Vec<&str>) -> Result<RoomMessageEventContent> { +pub(crate) async fn incoming_federation(_body: Vec<&str>) -> Result<RoomMessageEventContent> { let map = services().globals.roomid_federationhandletime.read().await; let mut msg = format!("Handling {} incoming pdus:\n", map.len()); @@ -101,7 +101,8 @@ pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>) rooms.reverse(); let output_plain = format!( - "Rooms {user_id} shares with us:\n{}", + "Rooms {user_id} shares with us ({}):\n{}", + rooms.len(), rooms .iter() .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) @@ -109,15 +110,16 @@ pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>) .join("\n") ); let output_html = format!( - "<table><caption>Rooms {user_id} shares with \ - us</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>", + "<table><caption>Rooms {user_id} shares with us \ + ({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>", + rooms.len(), rooms .iter() .fold(String::new(), |mut output, (id, members, name)| { writeln!( output, "<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>", - escape_html(id.as_ref()), + id, members, escape_html(name) ) diff --git a/src/admin/federation/mod.rs b/src/admin/federation/mod.rs index b3da33f2a782954e944e730754a2992758ad502f..81469feb46bcb5c7a9cd22b014c0a113e901a9b2 100644 --- a/src/admin/federation/mod.rs +++ b/src/admin/federation/mod.rs @@ -2,7 +2,7 @@ use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId}; use self::federation_commands::{ - disable_room, enable_room, fetch_support_well_known, incoming_federeation, remote_user_in_rooms, + disable_room, enable_room, fetch_support_well_known, incoming_federation, remote_user_in_rooms, }; use crate::Result; @@ -51,7 +51,7 @@ pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Resu FederationCommand::EnableRoom { room_id, } => enable_room(body, room_id).await?, - FederationCommand::IncomingFederation => incoming_federeation(body).await?, + FederationCommand::IncomingFederation => incoming_federation(body).await?, FederationCommand::FetchSupportWellKnown { server_name, } => fetch_support_well_known(body, server_name).await?, diff --git a/src/admin/media/media_commands.rs b/src/admin/media/media_commands.rs index 956f5743d1b7a8bcf6daee1fe18f3a4bcc018eab..a78ee3873b2d17119a6c871818ad57ada316fa46 100644 --- a/src/admin/media/media_commands.rs +++ b/src/admin/media/media_commands.rs @@ -138,30 +138,30 @@ pub(crate) async fn delete( } pub(crate) async fn delete_list(body: Vec<&str>) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let mxc_list = body - .clone() - .drain(1..body.len().checked_sub(1).unwrap()) - .collect::<Vec<_>>(); + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } - let mut mxc_deletion_count: usize = 0; + let mxc_list = body + .clone() + .drain(1..body.len().checked_sub(1).unwrap()) + .collect::<Vec<_>>(); - for mxc in mxc_list { - debug!("Deleting MXC {mxc} in bulk"); - services().media.delete(mxc.to_owned()).await?; - mxc_deletion_count = mxc_deletion_count - .checked_add(1) - .expect("mxc_deletion_count should not get this high"); - } + let mut mxc_deletion_count: usize = 0; - return Ok(RoomMessageEventContent::text_plain(format!( - "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.", - ))); + for mxc in mxc_list { + debug!("Deleting MXC {mxc} in bulk"); + services().media.delete(mxc.to_owned()).await?; + mxc_deletion_count = mxc_deletion_count + .checked_add(1) + .expect("mxc_deletion_count should not get this high"); } - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) + Ok(RoomMessageEventContent::text_plain(format!( + "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.", + ))) } pub(crate) async fn delete_past_remote_media( diff --git a/src/admin/query/mod.rs b/src/admin/query/mod.rs index bebbb7711e01d0e14efef02212d3f5840677e4f6..2e5b0bb805ca81fba176182111d426bccd705c21 100644 --- a/src/admin/query/mod.rs +++ b/src/admin/query/mod.rs @@ -3,10 +3,12 @@ pub(crate) mod globals; pub(crate) mod presence; pub(crate) mod room_alias; +pub(crate) mod room_state_cache; pub(crate) mod sending; pub(crate) mod users; use clap::Subcommand; +use room_state_cache::room_state_cache; use ruma::{ events::{room::message::RoomMessageEventContent, RoomAccountDataEventType}, RoomAliasId, RoomId, ServerName, UserId, @@ -38,6 +40,10 @@ pub(crate) enum QueryCommand { #[command(subcommand)] RoomAlias(RoomAlias), + /// - rooms/state_cache iterators and getters + #[command(subcommand)] + RoomStateCache(RoomStateCache), + /// - globals.rs iterators and getters #[command(subcommand)] Globals(Globals), @@ -127,6 +133,78 @@ pub(crate) enum RoomAlias { AllLocalAliases, } +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomStateCache { + ServerInRoom { + server: Box<ServerName>, + room_id: Box<RoomId>, + }, + + RoomServers { + room_id: Box<RoomId>, + }, + + ServerRooms { + server: Box<ServerName>, + }, + + RoomMembers { + room_id: Box<RoomId>, + }, + + LocalUsersInRoom { + room_id: Box<RoomId>, + }, + + ActiveLocalUsersInRoom { + room_id: Box<RoomId>, + }, + + RoomJoinedCount { + room_id: Box<RoomId>, + }, + + RoomInvitedCount { + room_id: Box<RoomId>, + }, + + RoomUserOnceJoined { + room_id: Box<RoomId>, + }, + + RoomMembersInvited { + room_id: Box<RoomId>, + }, + + GetInviteCount { + room_id: Box<RoomId>, + user_id: Box<UserId>, + }, + + GetLeftCount { + room_id: Box<RoomId>, + user_id: Box<UserId>, + }, + + RoomsJoined { + user_id: Box<UserId>, + }, + + RoomsLeft { + user_id: Box<UserId>, + }, + + RoomsInvited { + user_id: Box<UserId>, + }, + + InviteState { + user_id: Box<UserId>, + room_id: Box<RoomId>, + }, +} + #[cfg_attr(test, derive(Debug))] #[derive(Subcommand)] /// All the getters and iterators from src/database/key_value/globals.rs @@ -216,6 +294,7 @@ pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<R QueryCommand::Appservice(command) => appservice(command).await?, QueryCommand::Presence(command) => presence(command).await?, QueryCommand::RoomAlias(command) => room_alias(command).await?, + QueryCommand::RoomStateCache(command) => room_state_cache(command).await?, QueryCommand::Globals(command) => globals(command).await?, QueryCommand::Sending(command) => sending(command).await?, QueryCommand::Users(command) => users(command).await?, diff --git a/src/admin/query/room_state_cache.rs b/src/admin/query/room_state_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..c062497ed9546fb2090d9bb8438ea7df6bf93162 --- /dev/null +++ b/src/admin/query/room_state_cache.rs @@ -0,0 +1,249 @@ +use ruma::events::room::message::RoomMessageEventContent; + +use super::RoomStateCache; +use crate::{services, Result}; + +pub(crate) async fn room_state_cache(subcommand: RoomStateCache) -> Result<RoomMessageEventContent> { + match subcommand { + RoomStateCache::ServerInRoom { + server, + room_id, + } => { + let timer = tokio::time::Instant::now(); + let result = services() + .rooms + .state_cache + .server_in_room(&server, &room_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{result:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{result:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomServers { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .room_servers(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::ServerRooms { + server, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services().rooms.state_cache.server_rooms(&server).collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomMembers { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .room_members(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::LocalUsersInRoom { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Vec<_> = services() + .rooms + .state_cache + .local_users_in_room(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::ActiveLocalUsersInRoom { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Vec<_> = services() + .rooms + .state_cache + .active_local_users_in_room(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomJoinedCount { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results = services().rooms.state_cache.room_joined_count(&room_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomInvitedCount { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results = services().rooms.state_cache.room_invited_count(&room_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomUserOnceJoined { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .room_useroncejoined(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomMembersInvited { + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .room_members_invited(&room_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::GetInviteCount { + room_id, + user_id, + } => { + let timer = tokio::time::Instant::now(); + let results = services() + .rooms + .state_cache + .get_invite_count(&room_id, &user_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::GetLeftCount { + room_id, + user_id, + } => { + let timer = tokio::time::Instant::now(); + let results = services() + .rooms + .state_cache + .get_left_count(&room_id, &user_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomsJoined { + user_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .rooms_joined(&user_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomsInvited { + user_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services() + .rooms + .state_cache + .rooms_invited(&user_id) + .collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::RoomsLeft { + user_id, + } => { + let timer = tokio::time::Instant::now(); + let results: Result<Vec<_>> = services().rooms.state_cache.rooms_left(&user_id).collect(); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + RoomStateCache::InviteState { + user_id, + room_id, + } => { + let timer = tokio::time::Instant::now(); + let results = services() + .rooms + .state_cache + .invite_state(&user_id, &room_id); + let query_time = timer.elapsed(); + + Ok(RoomMessageEventContent::text_html( + format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"), + format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"), + )) + }, + } +} diff --git a/src/admin/room/mod.rs b/src/admin/room/mod.rs index ccc8c8bec9cecbbd443235881750513e0fd35bf2..ced96c95281b6529ec289d532e64e337cac9132a 100644 --- a/src/admin/room/mod.rs +++ b/src/admin/room/mod.rs @@ -7,6 +7,7 @@ pub(crate) mod room_alias_commands; pub(crate) mod room_commands; pub(crate) mod room_directory_commands; +pub(crate) mod room_info_commands; pub(crate) mod room_moderation_commands; #[cfg_attr(test, derive(Debug))] @@ -17,6 +18,10 @@ pub(crate) enum RoomCommand { page: Option<usize>, }, + #[command(subcommand)] + /// - View information about a room we know about + Info(RoomInfoCommand), + #[command(subcommand)] /// - Manage moderation of remote or local rooms Moderation(RoomModerationCommand), @@ -30,6 +35,23 @@ pub(crate) enum RoomCommand { Directory(RoomDirectoryCommand), } +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +pub(crate) enum RoomInfoCommand { + /// - List joined members in a room + ListJoinedMembers { + room_id: Box<RoomId>, + }, + + /// - Displays room topic + /// + /// Room topics can be huge, so this is in its + /// own separate command + ViewRoomTopic { + room_id: Box<RoomId>, + }, +} + #[cfg_attr(test, derive(Debug))] #[derive(Subcommand)] pub(crate) enum RoomAliasCommand { @@ -147,6 +169,8 @@ pub(crate) enum RoomModerationCommand { pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { Ok(match command { + RoomCommand::Info(command) => room_info_commands::process(command, body).await?, + RoomCommand::Alias(command) => room_alias_commands::process(command, body).await?, RoomCommand::Directory(command) => room_directory_commands::process(command, body).await?, diff --git a/src/admin/room/room_info_commands.rs b/src/admin/room/room_info_commands.rs new file mode 100644 index 0000000000000000000000000000000000000000..0bcbc7aed15547d88ded9a356c69c791056d46cf --- /dev/null +++ b/src/admin/room/room_info_commands.rs @@ -0,0 +1,93 @@ +use std::fmt::Write; + +use ruma::{events::room::message::RoomMessageEventContent, RoomId}; +use service::services; + +use super::RoomInfoCommand; +use crate::{escape_html, Result}; + +pub(crate) async fn process(command: RoomInfoCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { + match command { + RoomInfoCommand::ListJoinedMembers { + room_id, + } => list_joined_members(body, room_id).await, + RoomInfoCommand::ViewRoomTopic { + room_id, + } => view_room_topic(body, room_id).await, + } +} + +async fn list_joined_members(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> { + let room_name = services() + .rooms + .state_accessor + .get_name(&room_id) + .ok() + .flatten() + .unwrap_or_else(|| room_id.to_string()); + + let members = services() + .rooms + .state_cache + .room_members(&room_id) + .filter_map(Result::ok); + + let member_info = members + .into_iter() + .map(|user_id| { + ( + user_id.clone(), + services() + .users + .displayname(&user_id) + .unwrap_or(None) + .unwrap_or_else(|| user_id.to_string()), + ) + }) + .collect::<Vec<_>>(); + + let output_plain = format!( + "{} Members in Room \"{}\":\n```\n{}\n```", + member_info.len(), + room_name, + member_info + .iter() + .map(|(mxid, displayname)| format!("{mxid} | {displayname}")) + .collect::<Vec<_>>() + .join("\n") + ); + + let output_html = format!( + "<table><caption>{} Members in Room \"{}\" </caption>\n<tr><th>MXID</th>\t<th>Display \ + Name</th></tr>\n{}</table>", + member_info.len(), + room_name, + member_info + .iter() + .fold(String::new(), |mut output, (mxid, displayname)| { + writeln!( + output, + "<tr><td>{}</td>\t<td>{}</td></tr>", + mxid, + escape_html(displayname.as_ref()) + ) + .expect("should be able to write to string buffer"); + output + }) + ); + + Ok(RoomMessageEventContent::text_html(output_plain, output_html)) +} + +async fn view_room_topic(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> { + let Some(room_topic) = services().rooms.state_accessor.get_room_topic(&room_id)? else { + return Ok(RoomMessageEventContent::text_plain("Room does not have a room topic set.")); + }; + + let output_html = format!("<p>Room topic:</p>\n<hr>\n{}<hr>", escape_html(&room_topic)); + + Ok(RoomMessageEventContent::text_html( + format!("Room topic:\n\n```{room_topic}\n```"), + output_html, + )) +} diff --git a/src/admin/room/room_moderation_commands.rs b/src/admin/room/room_moderation_commands.rs index 6b759c9bb7ee5a43369ffcbb6d7420cfbf636008..9886756b6106bea940e745f22fbb898f67dd3151 100644 --- a/src/admin/room/room_moderation_commands.rs +++ b/src/admin/room/room_moderation_commands.rs @@ -6,11 +6,8 @@ }; use tracing::{debug, error, info, warn}; -use super::{ - super::{escape_html, Service}, - RoomModerationCommand, -}; -use crate::{services, user_is_local, Result}; +use super::{super::Service, RoomModerationCommand}; +use crate::{escape_html, get_room_info, services, user_is_local, Result}; pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { match command { @@ -187,135 +184,137 @@ async fn ban_room( } async fn ban_list_of_rooms(body: Vec<&str>, force: bool, disable_federation: bool) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let rooms_s = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>(); - - let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name()) - .try_into() - .expect("#admins:server_name is a valid alias name"); - - let mut room_ban_count: usize = 0; - let mut room_ids: Vec<OwnedRoomId> = Vec::new(); - - for &room in &rooms_s { - match <&RoomOrAliasId>::try_from(room) { - Ok(room_alias_or_id) => { - if let Some(admin_room_id) = Service::get_admin_room().await? { - if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(&admin_room_alias) { - info!("User specified admin room in bulk ban list, ignoring"); - continue; - } - } + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } - if room_alias_or_id.is_room_id() { - let room_id = match RoomId::parse(room_alias_or_id) { - Ok(room_id) => room_id, - Err(e) => { - if force { - // ignore rooms we failed to parse if we're force banning - warn!( - "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ - logging here: {e}" - ); - continue; - } + let rooms_s = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>(); + + let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name()) + .try_into() + .expect("#admins:server_name is a valid alias name"); - return Ok(RoomMessageEventContent::text_plain(format!( - "{room} is not a valid room ID or room alias, please fix the list and try again: \ - {e}" - ))); - }, - }; + let mut room_ban_count: usize = 0; + let mut room_ids: Vec<OwnedRoomId> = Vec::new(); - room_ids.push(room_id); + for &room in &rooms_s { + match <&RoomOrAliasId>::try_from(room) { + Ok(room_alias_or_id) => { + if let Some(admin_room_id) = Service::get_admin_room().await? { + if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(&admin_room_alias) { + info!("User specified admin room in bulk ban list, ignoring"); + continue; } + } - if room_alias_or_id.is_room_alias_id() { - match RoomAliasId::parse(room_alias_or_id) { - Ok(room_alias) => { - let room_id = - if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? { - room_id - } else { - debug!( - "We don't have this room alias to a room ID locally, attempting to fetch \ - room ID over federation" - ); - - match get_alias_helper(room_alias, None).await { - Ok(response) => { - debug!( - "Got federation response fetching room ID for room {room}: {:?}", - response - ); - response.room_id - }, - Err(e) => { - // don't fail if force blocking - if force { - warn!("Failed to resolve room alias {room} to a room ID: {e}"); - continue; - } - - return Ok(RoomMessageEventContent::text_plain(format!( - "Failed to resolve room alias {room} to a room ID: {e}" - ))); - }, - } - }; - - room_ids.push(room_id); - }, - Err(e) => { - if force { - // ignore rooms we failed to parse if we're force deleting - error!( - "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ - logging here: {e}" + if room_alias_or_id.is_room_id() { + let room_id = match RoomId::parse(room_alias_or_id) { + Ok(room_id) => room_id, + Err(e) => { + if force { + // ignore rooms we failed to parse if we're force banning + warn!( + "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ + logging here: {e}" + ); + continue; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "{room} is not a valid room ID or room alias, please fix the list and try again: {e}" + ))); + }, + }; + + room_ids.push(room_id); + } + + if room_alias_or_id.is_room_alias_id() { + match RoomAliasId::parse(room_alias_or_id) { + Ok(room_alias) => { + let room_id = + if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? { + room_id + } else { + debug!( + "We don't have this room alias to a room ID locally, attempting to fetch room \ + ID over federation" ); - continue; - } - - return Ok(RoomMessageEventContent::text_plain(format!( - "{room} is not a valid room ID or room alias, please fix the list and try again: \ - {e}" - ))); - }, - } - } - }, - Err(e) => { - if force { - // ignore rooms we failed to parse if we're force deleting - error!( - "Error parsing room \"{room}\" during bulk room banning, ignoring error and logging here: \ - {e}" - ); - continue; + + match get_alias_helper(room_alias, None).await { + Ok(response) => { + debug!( + "Got federation response fetching room ID for room {room}: {:?}", + response + ); + response.room_id + }, + Err(e) => { + // don't fail if force blocking + if force { + warn!("Failed to resolve room alias {room} to a room ID: {e}"); + continue; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "Failed to resolve room alias {room} to a room ID: {e}" + ))); + }, + } + }; + + room_ids.push(room_id); + }, + Err(e) => { + if force { + // ignore rooms we failed to parse if we're force deleting + error!( + "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ + logging here: {e}" + ); + continue; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "{room} is not a valid room ID or room alias, please fix the list and try again: {e}" + ))); + }, } + } + }, + Err(e) => { + if force { + // ignore rooms we failed to parse if we're force deleting + error!( + "Error parsing room \"{room}\" during bulk room banning, ignoring error and logging here: {e}" + ); + continue; + } - return Ok(RoomMessageEventContent::text_plain(format!( - "{room} is not a valid room ID or room alias, please fix the list and try again: {e}" - ))); - }, - } + return Ok(RoomMessageEventContent::text_plain(format!( + "{room} is not a valid room ID or room alias, please fix the list and try again: {e}" + ))); + }, } + } - for room_id in room_ids { - if services().rooms.metadata.ban_room(&room_id, true).is_ok() { - debug!("Banned {room_id} successfully"); - room_ban_count = room_ban_count.saturating_add(1); - } + for room_id in room_ids { + if services().rooms.metadata.ban_room(&room_id, true).is_ok() { + debug!("Banned {room_id} successfully"); + room_ban_count = room_ban_count.saturating_add(1); + } - debug!("Making all users leave the room {}", &room_id); - if force { - for local_user in services() - .rooms - .state_cache - .room_members(&room_id) - .filter_map(|user| { - user.ok().filter(|local_user| { - local_user.server_name() == services().globals.server_name() + debug!("Making all users leave the room {}", &room_id); + if force { + for local_user in services() + .rooms + .state_cache + .room_members(&room_id) + .filter_map(|user| { + user.ok().filter(|local_user| { + local_user.server_name() == services().globals.server_name() // additional wrapped check here is to avoid adding remote users // who are in the admin room to the list of local users (would fail auth check) && (local_user.server_name() @@ -324,31 +323,31 @@ async fn ban_list_of_rooms(body: Vec<&str>, force: bool, disable_federation: boo .users .is_admin(local_user) .unwrap_or(true)) // since this is a - // force operation, - // assume user is - // an admin if - // somehow this - // fails - }) + // force operation, + // assume user is + // an admin if + // somehow this + // fails }) - .collect::<Vec<OwnedUserId>>() - { - debug!( - "Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)", - &local_user, room_id - ); - if let Err(e) = leave_room(&local_user, &room_id, None).await { - warn!(%e, "Failed to leave room"); - } + }) + .collect::<Vec<OwnedUserId>>() + { + debug!( + "Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)", + &local_user, room_id + ); + if let Err(e) = leave_room(&local_user, &room_id, None).await { + warn!(%e, "Failed to leave room"); } - } else { - for local_user in services() - .rooms - .state_cache - .room_members(&room_id) - .filter_map(|user| { - user.ok().filter(|local_user| { - local_user.server_name() == services().globals.server_name() + } + } else { + for local_user in services() + .rooms + .state_cache + .room_members(&room_id) + .filter_map(|user| { + user.ok().filter(|local_user| { + local_user.server_name() == services().globals.server_name() // additional wrapped check here is to avoid adding remote users // who are in the admin room to the list of local users (would fail auth check) && (local_user.server_name() @@ -357,45 +356,41 @@ async fn ban_list_of_rooms(body: Vec<&str>, force: bool, disable_federation: boo .users .is_admin(local_user) .unwrap_or(false)) - }) }) - .collect::<Vec<OwnedUserId>>() - { - debug!("Attempting leave for user {} in room {}", &local_user, &room_id); - if let Err(e) = leave_room(&local_user, &room_id, None).await { - error!( - "Error attempting to make local user {} leave room {} during bulk room banning: {}", - &local_user, &room_id, e - ); - return Ok(RoomMessageEventContent::text_plain(format!( - "Error attempting to make local user {} leave room {} during room banning (room is still \ - banned but not removing any more users and not banning any more rooms): {}\nIf you would \ - like to ignore errors, use --force", - &local_user, &room_id, e - ))); - } + }) + .collect::<Vec<OwnedUserId>>() + { + debug!("Attempting leave for user {} in room {}", &local_user, &room_id); + if let Err(e) = leave_room(&local_user, &room_id, None).await { + error!( + "Error attempting to make local user {} leave room {} during bulk room banning: {}", + &local_user, &room_id, e + ); + return Ok(RoomMessageEventContent::text_plain(format!( + "Error attempting to make local user {} leave room {} during room banning (room is still \ + banned but not removing any more users and not banning any more rooms): {}\nIf you would \ + like to ignore errors, use --force", + &local_user, &room_id, e + ))); } } - - if disable_federation { - services().rooms.metadata.disable_room(&room_id, true)?; - } } if disable_federation { - return Ok(RoomMessageEventContent::text_plain(format!( - "Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and disabled \ - incoming federation with the room." - ))); + services().rooms.metadata.disable_room(&room_id, true)?; } - return Ok(RoomMessageEventContent::text_plain(format!( - "Finished bulk room ban, banned {room_ban_count} total rooms and evicted all users." - ))); } - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) + if disable_federation { + Ok(RoomMessageEventContent::text_plain(format!( + "Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and disabled incoming \ + federation with the room." + ))) + } else { + Ok(RoomMessageEventContent::text_plain(format!( + "Finished bulk room ban, banned {room_ban_count} total rooms and evicted all users." + ))) + } } async fn unban_room( @@ -481,26 +476,51 @@ async fn list_banned_rooms(_body: Vec<&str>) -> Result<RoomMessageEventContent> match rooms { Ok(room_ids) => { - // TODO: add room name from our state cache if available, default to the room ID - // as the room name if we dont have it TODO: do same if we have a room alias for - // this - let plain_list = room_ids.iter().fold(String::new(), |mut output, room_id| { - writeln!(output, "- `{room_id}`").unwrap(); - output - }); - - let html_list = room_ids.iter().fold(String::new(), |mut output, room_id| { - writeln!(output, "<li><code>{}</code></li>", escape_html(room_id.as_ref())).unwrap(); - output - }); - - let plain = format!("Rooms:\n{plain_list}"); - let html = format!("Rooms:\n<ul>{html_list}</ul>"); - Ok(RoomMessageEventContent::text_html(plain, html)) + if room_ids.is_empty() { + return Ok(RoomMessageEventContent::text_plain("No rooms are banned.")); + } + + let mut rooms = room_ids + .into_iter() + .map(|room_id| get_room_info(&room_id)) + .collect::<Vec<_>>(); + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let output_plain = format!( + "Rooms Banned ({}):\n```\n{}```", + rooms.len(), + rooms + .iter() + .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) + .collect::<Vec<_>>() + .join("\n") + ); + + let output_html = format!( + "<table><caption>Rooms Banned ({}) \ + </caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>", + rooms.len(), + rooms + .iter() + .fold(String::new(), |mut output, (id, members, name)| { + writeln!( + output, + "<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>", + id, + members, + escape_html(name.as_ref()) + ) + .expect("should be able to write to string buffer"); + output + }) + ); + + Ok(RoomMessageEventContent::text_html(output_plain, output_html)) }, Err(e) => { error!("Failed to list banned rooms: {}", e); - Ok(RoomMessageEventContent::text_plain(format!("Unable to list room aliases: {e}"))) + Ok(RoomMessageEventContent::text_plain(format!("Unable to list banned rooms: {e}"))) }, } } diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index 031dd97a862bdd3e7ab814b44459fbbbdea7d36c..e11429deb2c0edf7f46445d39dbe8ffa265c9755 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -1,7 +1,8 @@ pub(crate) mod user_commands; use clap::Subcommand; -use ruma::events::room::message::RoomMessageEventContent; +use ruma::{events::room::message::RoomMessageEventContent, RoomId}; +use user_commands::{delete_room_tag, get_room_tags, put_room_tag}; use self::user_commands::{create, deactivate, deactivate_all, list, list_joined_rooms, reset_password}; use crate::Result; @@ -25,11 +26,11 @@ pub(crate) enum UserCommand { /// - Deactivate a user /// - /// User will not be removed from all rooms by default. - /// Use --leave-rooms to force the user to leave all rooms + /// User will be removed from all rooms by default. + /// Use --no-leave-rooms to not leave all rooms by default. Deactivate { #[arg(short, long)] - leave_rooms: bool, + no_leave_rooms: bool, user_id: String, }, @@ -37,8 +38,10 @@ pub(crate) enum UserCommand { /// /// Recommended to use in conjunction with list-local-users. /// - /// Users will not be removed from joined rooms by default. - /// Can be overridden with --leave-rooms OR the --force flag. + /// Users will be removed from joined rooms by default. + /// + /// Can be overridden with --no-leave-rooms. + /// /// Removing a mass amount of users from a room may cause a significant /// amount of leave events. The time to leave rooms may depend significantly /// on joined rooms and servers. @@ -48,7 +51,7 @@ pub(crate) enum UserCommand { DeactivateAll { #[arg(short, long)] /// Remove users from their joined rooms - leave_rooms: bool, + no_leave_rooms: bool, #[arg(short, long)] /// Also deactivate admin accounts and will assume leave all rooms too force: bool, @@ -62,6 +65,32 @@ pub(crate) enum UserCommand { ListJoinedRooms { user_id: String, }, + + /// - Puts a room tag for the specified user and room ID. + /// + /// This is primarily useful if you'd like to set your admin room + /// to the special "System Alerts" section in Element as a way to + /// permanently see your admin room without it being buried away in your + /// favourites or rooms. To do this, you would pass your user, your admin + /// room's internal ID, and the tag name `m.server_notice`. + PutRoomTag { + user_id: String, + room_id: Box<RoomId>, + tag: String, + }, + + /// - Deletes the room tag for the specified user and room ID + DeleteRoomTag { + user_id: String, + room_id: Box<RoomId>, + tag: String, + }, + + /// - Gets all the room tags for the specified user and room ID + GetRoomTags { + user_id: String, + room_id: Box<RoomId>, + }, } pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> { @@ -72,18 +101,32 @@ pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<Roo password, } => create(body, username, password).await?, UserCommand::Deactivate { - leave_rooms, + no_leave_rooms, user_id, - } => deactivate(body, leave_rooms, user_id).await?, + } => deactivate(body, no_leave_rooms, user_id).await?, UserCommand::ResetPassword { username, } => reset_password(body, username).await?, UserCommand::DeactivateAll { - leave_rooms, + no_leave_rooms, force, - } => deactivate_all(body, leave_rooms, force).await?, + } => deactivate_all(body, no_leave_rooms, force).await?, UserCommand::ListJoinedRooms { user_id, } => list_joined_rooms(body, user_id).await?, + UserCommand::PutRoomTag { + user_id, + room_id, + tag, + } => put_room_tag(body, user_id, room_id, tag).await?, + UserCommand::DeleteRoomTag { + user_id, + room_id, + tag, + } => delete_room_tag(body, user_id, room_id, tag).await?, + UserCommand::GetRoomTags { + user_id, + room_id, + } => get_room_tags(body, user_id, room_id).await?, }) } diff --git a/src/admin/user/user_commands.rs b/src/admin/user/user_commands.rs index 24f30931384428805b198484a382f9ae150fbd47..8b90894e940f2881a7f930b9247ae3a92c975647 100644 --- a/src/admin/user/user_commands.rs +++ b/src/admin/user/user_commands.rs @@ -1,20 +1,36 @@ -use std::{fmt::Write as _, sync::Arc}; +use std::{collections::BTreeMap, fmt::Write as _}; use api::client::{join_room_by_id_helper, leave_all_rooms}; use conduit::utils; -use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, UserId}; +use ruma::{ + events::{ + room::message::RoomMessageEventContent, + tag::{TagEvent, TagEventContent, TagInfo}, + RoomAccountDataEventType, + }, + OwnedRoomId, OwnedUserId, RoomId, UserId, +}; use tracing::{error, info, warn}; -use crate::{escape_html, get_room_info, services, user_is_local, Result}; +use crate::{ + escape_html, get_room_info, services, + utils::{parse_active_local_user_id, parse_local_user_id}, + Result, +}; const AUTO_GEN_PASSWORD_LENGTH: usize = 25; pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> { match services().users.list_local_users() { Ok(users) => { - let mut msg = format!("Found {} local user account(s):\n", users.len()); - msg += &users.join("\n"); - Ok(RoomMessageEventContent::text_plain(&msg)) + let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len()); + plain_msg += &users.join("\n"); + plain_msg += "\n```"; + + let mut html_msg = format!("<p>Found {} local user account(s):</p><pre><code>", users.len()); + html_msg += &users.join("\n"); + html_msg += "\n</code></pre>"; + Ok(RoomMessageEventContent::text_html(&plain_msg, &html_msg)) }, Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())), } @@ -23,34 +39,15 @@ pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> { pub(crate) async fn create( _body: Vec<&str>, username: String, password: Option<String>, ) -> Result<RoomMessageEventContent> { - let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); - // Validate user id - let user_id = - match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) { - Ok(id) => id, - Err(e) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "The supplied username is not a valid username: {e}" - ))) - }, - }; - - if !user_is_local(&user_id) { - return Ok(RoomMessageEventContent::text_plain(format!( - "User {user_id} does not belong to our server." - ))); - } - - if user_id.is_historical() { - return Ok(RoomMessageEventContent::text_plain(format!( - "Userid {user_id} is not allowed due to historical" - ))); - } + let user_id = parse_local_user_id(&username)?; if services().users.exists(&user_id)? { return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists"))); } + + let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); + // Create user services().users.create(&user_id, Some(password.as_str()))?; @@ -131,79 +128,46 @@ pub(crate) async fn create( } pub(crate) async fn deactivate( - _body: Vec<&str>, leave_rooms: bool, user_id: String, + _body: Vec<&str>, no_leave_rooms: bool, user_id: String, ) -> Result<RoomMessageEventContent> { // Validate user id - let user_id = - match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) { - Ok(id) => Arc::<UserId>::from(id), - Err(e) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "The supplied username is not a valid username: {e}" - ))) - }, - }; - - // check if user belongs to our server - if user_id.server_name() != services().globals.server_name() { - return Ok(RoomMessageEventContent::text_plain(format!( - "User {user_id} does not belong to our server." - ))); - } + let user_id = parse_local_user_id(&user_id)?; - // don't deactivate the conduit service account + // don't deactivate the server service account if user_id == UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists") { return Ok(RoomMessageEventContent::text_plain( - "Not allowed to deactivate the Conduit service account.", + "Not allowed to deactivate the server service account.", )); } - if services().users.exists(&user_id)? { - RoomMessageEventContent::text_plain(format!("Making {user_id} leave all rooms before deactivation...")); - - services().users.deactivate_account(&user_id)?; - - if leave_rooms { - leave_all_rooms(&user_id).await; - } + services().users.deactivate_account(&user_id)?; - Ok(RoomMessageEventContent::text_plain(format!( - "User {user_id} has been deactivated" - ))) - } else { - Ok(RoomMessageEventContent::text_plain(format!( - "User {user_id} doesn't exist on this server" - ))) + if !no_leave_rooms { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "Making {user_id} leave all rooms after deactivation..." + ))) + .await; + leave_all_rooms(&user_id).await; } + + Ok(RoomMessageEventContent::text_plain(format!( + "User {user_id} has been deactivated" + ))) } pub(crate) async fn reset_password(_body: Vec<&str>, username: String) -> Result<RoomMessageEventContent> { - // Validate user id - let user_id = - match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) { - Ok(id) => Arc::<UserId>::from(id), - Err(e) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "The supplied username is not a valid username: {e}" - ))) - }, - }; + let user_id = parse_local_user_id(&username)?; - // check if user belongs to our server - if user_id.server_name() != services().globals.server_name() { - return Ok(RoomMessageEventContent::text_plain(format!( - "User {user_id} does not belong to our server." - ))); - } - - // Check if the specified user is valid - if !services().users.exists(&user_id)? - || user_id - == UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists") + if user_id + == UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists") { - return Ok(RoomMessageEventContent::text_plain("The specified user does not exist!")); + return Ok(RoomMessageEventContent::text_plain( + "Not allowed to set the password for the server account. Please use the emergency password config option.", + )); } let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); @@ -221,107 +185,98 @@ pub(crate) async fn reset_password(_body: Vec<&str>, username: String) -> Result } } -pub(crate) async fn deactivate_all(body: Vec<&str>, leave_rooms: bool, force: bool) -> Result<RoomMessageEventContent> { - if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { - let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>(); - - let mut user_ids: Vec<&UserId> = Vec::new(); +pub(crate) async fn deactivate_all( + body: Vec<&str>, no_leave_rooms: bool, force: bool, +) -> Result<RoomMessageEventContent> { + if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" { + return Ok(RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + )); + } - for &username in &usernames { - match <&UserId>::try_from(username) { - Ok(user_id) => user_ids.push(user_id), - Err(e) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "{username} is not a valid username: {e}" + let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>(); + + let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len()); + let mut admins = Vec::new(); + + for username in usernames { + match parse_active_local_user_id(username) { + Ok(user_id) => { + if services().users.is_admin(&user_id)? && !force { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "{username} is an admin and --force is not set, skipping over" + ))) + .await; + admins.push(username); + continue; + } + + // don't deactivate the server service account + if user_id + == UserId::parse_with_server_name("conduit", services().globals.server_name()) + .expect("server user exists") + { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "{username} is the server service account, skipping over" + ))) + .await; + continue; + } + + user_ids.push(user_id); + }, + Err(e) => { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "{username} is not a valid username, skipping over: {e}" ))) - }, - } - } - - let mut deactivation_count: usize = 0; - let mut admins = Vec::new(); - - if !force { - user_ids.retain(|&user_id| match services().users.is_admin(user_id) { - Ok(is_admin) => { - if is_admin { - admins.push(user_id.localpart()); - false - } else { - true - } - }, - Err(_) => false, - }); - } - - for &user_id in &user_ids { - // check if user belongs to our server and skips over non-local users - if user_id.server_name() != services().globals.server_name() { - continue; - } - - // don't deactivate the conduit service account - if user_id - == UserId::parse_with_server_name("conduit", services().globals.server_name()) - .expect("conduit user exists") - { + .await; continue; - } + }, + } + } - // user does not exist on our server - if !services().users.exists(user_id)? { - continue; - } + let mut deactivation_count: usize = 0; - if services().users.deactivate_account(user_id).is_ok() { + for user_id in user_ids { + match services().users.deactivate_account(&user_id) { + Ok(()) => { deactivation_count = deactivation_count.saturating_add(1); - } - } - - if leave_rooms || force { - for &user_id in &user_ids { - leave_all_rooms(user_id).await; - } + if !no_leave_rooms { + info!("Forcing user {user_id} to leave all rooms apart of deactivate-all"); + leave_all_rooms(&user_id).await; + } + }, + Err(e) => { + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!("Failed deactivating user: {e}"))) + .await; + }, } + } - if admins.is_empty() { - Ok(RoomMessageEventContent::text_plain(format!( - "Deactivated {deactivation_count} accounts." - ))) - } else { - Ok(RoomMessageEventContent::text_plain(format!( - "Deactivated {} accounts.\nSkipped admin accounts: {}. Use --force to deactivate admin accounts", - deactivation_count, - admins.join(", ") - ))) - } + if admins.is_empty() { + Ok(RoomMessageEventContent::text_plain(format!( + "Deactivated {deactivation_count} accounts." + ))) } else { - Ok(RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - )) + Ok(RoomMessageEventContent::text_plain(format!( + "Deactivated {deactivation_count} accounts.\nSkipped admin accounts: {}. Use --force to deactivate admin \ + accounts", + admins.join(", ") + ))) } } pub(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result<RoomMessageEventContent> { // Validate user id - let user_id = - match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) { - Ok(id) => Arc::<UserId>::from(id), - Err(e) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "The supplied username is not a valid username: {e}" - ))) - }, - }; - - if !user_is_local(&user_id) { - return Ok(RoomMessageEventContent::text_plain("User does not belong to our server.")); - } - - if !services().users.exists(&user_id)? { - return Ok(RoomMessageEventContent::text_plain("User does not exist on this server.")); - } + let user_id = parse_local_user_id(&user_id)?; let mut rooms: Vec<(OwnedRoomId, u64, String)> = services() .rooms @@ -347,6 +302,7 @@ pub(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Resu .collect::<Vec<_>>() .join("\n") ); + let output_html = format!( "<table><caption>Rooms {user_id} Joined \ ({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>", @@ -365,5 +321,97 @@ pub(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Resu output }) ); + Ok(RoomMessageEventContent::text_html(output_plain, output_html)) } + +pub(crate) async fn put_room_tag( + _body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String, +) -> Result<RoomMessageEventContent> { + let user_id = parse_active_local_user_id(&user_id)?; + + let event = services() + .account_data + .get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?; + + let mut tags_event = event.map_or_else( + || TagEvent { + content: TagEventContent { + tags: BTreeMap::new(), + }, + }, + |e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"), + ); + + tags_event + .content + .tags + .insert(tag.clone().into(), TagInfo::new()); + + services().account_data.update( + Some(&room_id), + &user_id, + RoomAccountDataEventType::Tag, + &serde_json::to_value(tags_event).expect("to json value always works"), + )?; + + Ok(RoomMessageEventContent::text_plain(format!( + "Successfully updated room account data for {user_id} and room {room_id} with tag {tag}" + ))) +} + +pub(crate) async fn delete_room_tag( + _body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String, +) -> Result<RoomMessageEventContent> { + let user_id = parse_active_local_user_id(&user_id)?; + + let event = services() + .account_data + .get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?; + + let mut tags_event = event.map_or_else( + || TagEvent { + content: TagEventContent { + tags: BTreeMap::new(), + }, + }, + |e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"), + ); + + tags_event.content.tags.remove(&tag.clone().into()); + + services().account_data.update( + Some(&room_id), + &user_id, + RoomAccountDataEventType::Tag, + &serde_json::to_value(tags_event).expect("to json value always works"), + )?; + + Ok(RoomMessageEventContent::text_plain(format!( + "Successfully updated room account data for {user_id} and room {room_id}, deleting room tag {tag}" + ))) +} + +pub(crate) async fn get_room_tags( + _body: Vec<&str>, user_id: String, room_id: Box<RoomId>, +) -> Result<RoomMessageEventContent> { + let user_id = parse_active_local_user_id(&user_id)?; + + let event = services() + .account_data + .get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?; + + let tags_event = event.map_or_else( + || TagEvent { + content: TagEventContent { + tags: BTreeMap::new(), + }, + }, + |e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"), + ); + + Ok(RoomMessageEventContent::text_html( + format!("<pre><code>\n{:?}\n</code></pre>", tags_event.content.tags), + format!("```\n{:?}\n```", tags_event.content.tags), + )) +} diff --git a/src/admin/utils.rs b/src/admin/utils.rs index 6e65f2b21e9641ba2c0784cb7a3c2a25862420fc..4c30e5a8b646308d69d1c76998f2a6ed12d1cf05 100644 --- a/src/admin/utils.rs +++ b/src/admin/utils.rs @@ -1,7 +1,9 @@ pub(crate) use conduit::utils::HtmlEscape; -use ruma::{OwnedRoomId, RoomId}; +use conduit_core::Error; +use ruma::{OwnedRoomId, OwnedUserId, RoomId, UserId}; +use service::user_is_local; -use crate::services; +use crate::{services, Result}; pub(crate) fn escape_html(s: &str) -> String { s.replace('&', "&") @@ -28,3 +30,35 @@ pub(crate) fn get_room_info(id: &RoomId) -> (OwnedRoomId, u64, String) { .unwrap_or_else(|| id.to_string()), ) } + +/// Parses user ID +pub(crate) fn parse_user_id(user_id: &str) -> Result<OwnedUserId> { + UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name()) + .map_err(|e| Error::Err(format!("The supplied username is not a valid username: {e}"))) +} + +/// Parses user ID as our local user +pub(crate) fn parse_local_user_id(user_id: &str) -> Result<OwnedUserId> { + let user_id = parse_user_id(user_id)?; + + if !user_is_local(&user_id) { + return Err(Error::Err(String::from("User does not belong to our server."))); + } + + Ok(user_id) +} + +/// Parses user ID that is an active (not guest or deactivated) local user +pub(crate) fn parse_active_local_user_id(user_id: &str) -> Result<OwnedUserId> { + let user_id = parse_local_user_id(user_id)?; + + if !services().users.exists(&user_id)? { + return Err(Error::Err(String::from("User does not exist on this server."))); + } + + if services().users.is_deactivated(&user_id)? { + return Err(Error::Err(String::from("User is deactivated."))); + } + + Ok(user_id) +} diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 72830f9d4f510bce34c71f1e4dae7a8ad01c9c25..dc384c4cfeaf2c40858e51748da255d66ae20ac8 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -1,5 +1,6 @@ use std::fmt::Write; +use axum_client_ip::InsecureClientIp; use conduit::debug_info; use register::RegistrationKind; use ruma::{ @@ -39,8 +40,9 @@ /// /// Note: This will not reserve the username, so the username might become /// invalid when trying to register +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn get_register_available_route( - body: Ruma<get_username_availability::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_username_availability::v3::Request>, ) -> Result<get_username_availability::v3::Response> { // Validate user id let user_id = UserId::parse_with_server_name(body.username.to_lowercase(), services().globals.server_name()) @@ -87,7 +89,10 @@ pub(crate) async fn get_register_available_route( /// - If `inhibit_login` is false: Creates a device and returns device id and /// access_token #[allow(clippy::doc_markdown)] -pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> { +#[tracing::instrument(skip_all, fields(%client_ip))] +pub(crate) async fn register_route( + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<register::v3::Request>, +) -> Result<register::v3::Response> { if !services().globals.allow_registration() && body.appservice_info.is_none() { info!( "Registration disabled and request not from known appservice, rejecting registration attempt for username \ @@ -104,8 +109,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< || (services().globals.allow_registration() && services().globals.config.registration_token.is_some())) { info!( - "Guest registration disabled / registration enabled with token configured, rejecting guest registration, \ - initial device name: {:?}", + "Guest registration disabled / registration enabled with token configured, rejecting guest registration \ + attempt, initial device name: {:?}", body.initial_device_display_name ); return Err(Error::BadRequest( @@ -297,14 +302,14 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( - "New user \"{user_id}\" registered on this server." + "New user \"{user_id}\" registered on this server from IP {client_ip}." ))) .await; } // log in conduit admin channel if a guest registered if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() { - info!("New guest user \"{user_id}\" registered on this server."); + info!("New guest user \"{user_id}\" registered on this server from IP."); if let Some(device_display_name) = &body.initial_device_display_name { if body @@ -316,14 +321,15 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< .admin .send_message(RoomMessageEventContent::notice_plain(format!( "Guest user \"{user_id}\" with device display name `{device_display_name}` registered on this \ - server." + server from IP {client_ip}." ))) .await; } else { services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( - "Guest user \"{user_id}\" with no device display name registered on this server.", + "Guest user \"{user_id}\" with no device display name registered on this server from IP \ + {client_ip}.", ))) .await; } @@ -331,7 +337,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( - "Guest user \"{user_id}\" with no device display name registered on this server.", + "Guest user \"{user_id}\" with no device display name registered on this server from IP \ + {client_ip}.", ))) .await; } @@ -352,7 +359,7 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< .make_user_admin(&user_id, displayname) .await?; - warn!("Granting {} admin privileges as the first user", user_id); + warn!("Granting {user_id} admin privileges as the first user"); } } } @@ -416,8 +423,9 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result< /// last seen ts) /// - Forgets to-device events /// - Triggers device list updates +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn change_password_route( - body: Ruma<change_password::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<change_password::v3::Request>, ) -> Result<change_password::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated"); @@ -466,7 +474,7 @@ pub(crate) async fn change_password_route( } } - info!("User {} changed their password.", sender_user); + info!("User {sender_user} changed their password."); services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( @@ -504,7 +512,10 @@ pub(crate) async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoa /// - Forgets all to-device events /// - Triggers device list updates /// - Removes ability to log in again -pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Result<deactivate::v3::Response> { +#[tracing::instrument(skip_all, fields(%client_ip))] +pub(crate) async fn deactivate_route( + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<deactivate::v3::Request>, +) -> Result<deactivate::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated"); @@ -542,7 +553,7 @@ pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Res // Remove devices and mark account as deactivated services().users.deactivate_account(sender_user)?; - info!("User {} deactivated their account.", sender_user); + info!("User {sender_user} deactivated their account."); services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( diff --git a/src/api/client/directory.rs b/src/api/client/directory.rs index 0e5b8afa9f94fbfc4de7baaf7c8e247cfb41c110..20f0ccca998d265b2221b52b277b05f6f3b8bbb0 100644 --- a/src/api/client/directory.rs +++ b/src/api/client/directory.rs @@ -1,3 +1,4 @@ +use axum_client_ip::InsecureClientIp; use ruma::{ api::{ client::{ @@ -11,11 +12,8 @@ events::{ room::{ avatar::RoomAvatarEventContent, - canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, - guest_access::{GuestAccess, RoomGuestAccessEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent}, - topic::RoomTopicEventContent, }, StateEventType, }, @@ -30,8 +28,9 @@ /// Lists the public rooms on this server. /// /// - Rooms are ordered by the number of joined members +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn get_public_rooms_filtered_route( - body: Ruma<get_public_rooms_filtered::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms_filtered::v3::Request>, ) -> Result<get_public_rooms_filtered::v3::Response> { if let Some(server) = &body.server { if services() @@ -55,8 +54,8 @@ pub(crate) async fn get_public_rooms_filtered_route( ) .await .map_err(|e| { - warn!("Failed to return our /publicRooms: {e}"); - Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.") + warn!(?body.server, "Failed to return /publicRooms: {e}"); + Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.") })?; Ok(response) @@ -67,8 +66,9 @@ pub(crate) async fn get_public_rooms_filtered_route( /// Lists the public rooms on this server. /// /// - Rooms are ordered by the number of joined members +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn get_public_rooms_route( - body: Ruma<get_public_rooms::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms::v3::Request>, ) -> Result<get_public_rooms::v3::Response> { if let Some(server) = &body.server { if services() @@ -92,8 +92,8 @@ pub(crate) async fn get_public_rooms_route( ) .await .map_err(|e| { - warn!("Failed to return our /publicRooms: {e}"); - Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.") + warn!(?body.server, "Failed to return /publicRooms: {e}"); + Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.") })?; Ok(get_public_rooms::v3::Response { @@ -109,8 +109,9 @@ pub(crate) async fn get_public_rooms_route( /// Sets the visibility of a given room in the room directory. /// /// - TODO: Access control checks +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn set_room_visibility_route( - body: Ruma<set_room_visibility::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<set_room_visibility::v3::Request>, ) -> Result<set_room_visibility::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); @@ -230,12 +231,7 @@ pub(crate) async fn get_public_rooms_filtered_helper( canonical_alias: services() .rooms .state_accessor - .room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")? - .map_or(Ok(None), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomCanonicalAliasEventContent| c.alias) - .map_err(|_| Error::bad_database("Invalid canonical alias event in database.")) - })?, + .get_canonical_alias(&room_id)?, name: services().rooms.state_accessor.get_name(&room_id)?, num_joined_members: services() .rooms @@ -250,26 +246,13 @@ pub(crate) async fn get_public_rooms_filtered_helper( topic: services() .rooms .state_accessor - .room_state_get(&room_id, &StateEventType::RoomTopic, "")? - .map_or(Ok(None), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomTopicEventContent| Some(c.topic)) - .map_err(|e| { - error!("Invalid room topic event in database for room {room_id}: {e}"); - Error::bad_database("Invalid room topic event in database.") - }) - }) + .get_room_topic(&room_id) .unwrap_or(None), world_readable: services().rooms.state_accessor.is_world_readable(&room_id)?, guest_can_join: services() .rooms .state_accessor - .room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")? - .map_or(Ok(false), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin) - .map_err(|_| Error::bad_database("Invalid room guest access event in database.")) - })?, + .guest_can_join(&room_id)?, avatar_url: services() .rooms .state_accessor diff --git a/src/api/client/membership.rs b/src/api/client/membership.rs index 2d04be2f2262f638420ed044d5a8f45276de4f41..79ea50bd10bbe9027defa55b0604b588cf312c88 100644 --- a/src/api/client/membership.rs +++ b/src/api/client/membership.rs @@ -5,6 +5,7 @@ time::{Duration, Instant}, }; +use axum_client_ip::InsecureClientIp; use ruma::{ api::{ client::{ @@ -141,8 +142,9 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na /// rules locally /// - If the server does not know about the room: asks other servers over /// federation +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn join_room_by_id_route( - body: Ruma<join_room_by_id::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<join_room_by_id::v3::Request>, ) -> Result<join_room_by_id::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); @@ -193,8 +195,9 @@ pub(crate) async fn join_room_by_id_route( /// - If the server does not know about the room: use the server name query /// param if specified. if not specified, asks other servers over federation /// via room alias server name and room ID server name +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn join_room_by_id_or_alias_route( - body: Ruma<join_room_by_id_or_alias::v3::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<join_room_by_id_or_alias::v3::Request>, ) -> Result<join_room_by_id_or_alias::v3::Response> { let sender_user = body.sender_user.as_deref().expect("user is authenticated"); let body = body.body; @@ -295,7 +298,10 @@ pub(crate) async fn leave_room_route(body: Ruma<leave_room::v3::Request>) -> Res /// # `POST /_matrix/client/r0/rooms/{roomId}/invite` /// /// Tries to send an invite event into the room. -pub(crate) async fn invite_user_route(body: Ruma<invite_user::v3::Request>) -> Result<invite_user::v3::Response> { +#[tracing::instrument(skip_all, fields(%client_ip))] +pub(crate) async fn invite_user_route( + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<invite_user::v3::Request>, +) -> Result<invite_user::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); if !services().users.is_admin(sender_user)? && services().globals.block_non_admin_invites() { @@ -1314,7 +1320,7 @@ async fn make_join_request( make_join_response_and_server } -async fn validate_and_add_event_id( +pub async fn validate_and_add_event_id( pdu: &RawJsonValue, room_version: &RoomVersionId, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>, ) -> Result<(OwnedEventId, CanonicalJsonObject)> { let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index ced2bde528c8f8575d0dd1ff186db0a7b0eab4a3..6a6f16a75226039a76706250d6b46b59545e34d5 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -47,7 +47,7 @@ pub(super) use keys::*; pub(super) use media::*; pub(super) use membership::*; -pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room}; +pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id}; pub(super) use message::*; pub(super) use presence::*; pub(super) use profile::*; diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 9ebc60ee41aaae84a5109ccde74f18e59e1d39cd..1b256b4a21bf8b4c233eef1f727497697c61ccb6 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -1,3 +1,4 @@ +use axum_client_ip::InsecureClientIp; use ruma::{ api::{client::error::ErrorKind, federation::membership::create_invite}, events::room::member::{MembershipState, RoomMemberEventContent}, @@ -16,7 +17,10 @@ /// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}` /// /// Invites a remote user to a room. -pub(crate) async fn create_invite_route(body: Ruma<create_invite::v2::Request>) -> Result<create_invite::v2::Response> { +#[tracing::instrument(skip_all, fields(%client_ip))] +pub(crate) async fn create_invite_route( + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<create_invite::v2::Request>, +) -> Result<create_invite::v2::Response> { let origin = body.origin.as_ref().expect("server is authenticated"); // ACL check origin diff --git a/src/api/server/publicrooms.rs b/src/api/server/publicrooms.rs index 5a91f018e1291d04fdcac50409ded46d6ec33a03..6b5010e5206c6d07eae2694bbc7fe1cc917331a0 100644 --- a/src/api/server/publicrooms.rs +++ b/src/api/server/publicrooms.rs @@ -1,3 +1,4 @@ +use axum_client_ip::InsecureClientIp; use ruma::{ api::{ client::error::ErrorKind, @@ -11,8 +12,9 @@ /// # `POST /_matrix/federation/v1/publicRooms` /// /// Lists the public rooms on this server. +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn get_public_rooms_filtered_route( - body: Ruma<get_public_rooms_filtered::v1::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms_filtered::v1::Request>, ) -> Result<get_public_rooms_filtered::v1::Response> { if !services() .globals @@ -42,8 +44,9 @@ pub(crate) async fn get_public_rooms_filtered_route( /// # `GET /_matrix/federation/v1/publicRooms` /// /// Lists the public rooms on this server. +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn get_public_rooms_route( - body: Ruma<get_public_rooms::v1::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms::v1::Request>, ) -> Result<get_public_rooms::v1::Response> { if !services() .globals diff --git a/src/api/server/send.rs b/src/api/server/send.rs index 592eb4f94f8d900b9beed4b2195956e6d9eff387..e2413a4abb6f949c5a8989a5b68c2a09a05adc10 100644 --- a/src/api/server/send.rs +++ b/src/api/server/send.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, sync::Arc, time::Instant}; +use axum_client_ip::InsecureClientIp; use conduit::debug_warn; use ruma::{ api::{ @@ -25,8 +26,9 @@ /// # `PUT /_matrix/federation/v1/send/{txnId}` /// /// Push EDUs and PDUs to this server. +#[tracing::instrument(skip_all, fields(%client_ip))] pub(crate) async fn send_transaction_message_route( - body: Ruma<send_transaction_message::v1::Request>, + InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<send_transaction_message::v1::Request>, ) -> Result<send_transaction_message::v1::Response> { let origin = body.origin.as_ref().expect("server is authenticated"); diff --git a/src/core/utils/mod.rs b/src/core/utils/mod.rs index f3a45e47c8dd08e751125a0cb62fa11dda8bf36c..9ffbbbd03dce3a767f3748993e6413e1d86e5062 100644 --- a/src/core/utils/mod.rs +++ b/src/core/utils/mod.rs @@ -7,8 +7,7 @@ pub mod sys; use std::{ - cmp, - cmp::Ordering, + cmp::{self, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; diff --git a/src/router/layers.rs b/src/router/layers.rs index b358a3e63462daf45be8c2044e29fedf0c4515d2..3c7a5387bacb9adc6d7594b4ef3b80d85460d8a0 100644 --- a/src/router/layers.rs +++ b/src/router/layers.rs @@ -37,7 +37,6 @@ pub(crate) fn build(server: &Arc<Server>) -> io::Result<Router> { let layers = layers .sensitive_headers([header::AUTHORIZATION]) - .sensitive_request_headers([HeaderName::from_static("x-forwarded-for")].into()) .layer(axum::middleware::from_fn_with_state(Arc::clone(server), request::spawn)) .layer( TraceLayer::new_for_http() diff --git a/src/service/rooms/spaces/mod.rs b/src/service/rooms/spaces/mod.rs index 6fcaccb9c61c055fc21707ed5bf4be7b24b1b3a0..ffe1935dc467850860da5514bf723f9dae0e0609 100644 --- a/src/service/rooms/spaces/mod.rs +++ b/src/service/rooms/spaces/mod.rs @@ -15,11 +15,8 @@ events::{ room::{ avatar::RoomAvatarEventContent, - canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, - guest_access::{GuestAccess, RoomGuestAccessEventContent}, join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent, RoomMembership}, - topic::RoomTopicEventContent, }, space::child::{HierarchySpaceChildEvent, SpaceChildEventContent}, StateEventType, @@ -559,12 +556,7 @@ fn get_room_summary( canonical_alias: services() .rooms .state_accessor - .room_state_get(room_id, &StateEventType::RoomCanonicalAlias, "")? - .map_or(Ok(None), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomCanonicalAliasEventContent| c.alias) - .map_err(|_| Error::bad_database("Invalid canonical alias event in database.")) - })?, + .get_canonical_alias(room_id)?, name: services().rooms.state_accessor.get_name(room_id)?, num_joined_members: services() .rooms @@ -580,18 +572,10 @@ fn get_room_summary( topic: services() .rooms .state_accessor - .room_state_get(room_id, &StateEventType::RoomTopic, "")? - .map_or(Ok(None), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomTopicEventContent| Some(c.topic)) - .map_err(|_| { - error!("Invalid room topic event in database for room {}", room_id); - Error::bad_database("Invalid room topic event in database.") - }) - }) + .get_room_topic(room_id) .unwrap_or(None), world_readable: services().rooms.state_accessor.is_world_readable(room_id)?, - guest_can_join: guest_can_join(room_id)?, + guest_can_join: services().rooms.state_accessor.guest_can_join(room_id)?, avatar_url: services() .rooms .state_accessor @@ -847,19 +831,6 @@ fn is_accessable_child_recurse( } } -/// Checks if guests are able to join a given room -fn guest_can_join(room_id: &RoomId) -> Result<bool, Error> { - services() - .rooms - .state_accessor - .room_state_get(room_id, &StateEventType::RoomGuestAccess, "")? - .map_or(Ok(false), |s| { - serde_json::from_str(s.content.get()) - .map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin) - .map_err(|_| Error::bad_database("Invalid room guest access event in database.")) - }) -} - /// Returns the join rule for a given room fn get_join_rule(current_room: &RoomId) -> Result<(SpaceRoomJoinRule, Vec<OwnedRoomId>), Error> { Ok(services() diff --git a/src/service/rooms/state_accessor/mod.rs b/src/service/rooms/state_accessor/mod.rs index 0681c298fc58ae34c4eb93c3cdc950346673edd9..d3dc92eff23f251b884404b9021c02e4d46a0539 100644 --- a/src/service/rooms/state_accessor/mod.rs +++ b/src/service/rooms/state_accessor/mod.rs @@ -10,13 +10,16 @@ events::{ room::{ avatar::RoomAvatarEventContent, + canonical_alias::RoomCanonicalAliasEventContent, + guest_access::{GuestAccess, RoomGuestAccessEventContent}, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, member::{MembershipState, RoomMemberEventContent}, name::RoomNameEventContent, + topic::RoomTopicEventContent, }, StateEventType, }, - EventId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, + EventId, OwnedRoomAliasId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::MutexGuard; @@ -304,8 +307,7 @@ pub async fn user_can_invite( /// Checks if guests are able to view room content without joining pub fn is_world_readable(&self, room_id: &RoomId) -> Result<bool, Error> { - Ok(self - .room_state_get(room_id, &StateEventType::RoomHistoryVisibility, "")? + self.room_state_get(room_id, &StateEventType::RoomHistoryVisibility, "")? .map_or(Ok(false), |s| { serde_json::from_str(s.content.get()) .map(|c: RoomHistoryVisibilityEventContent| { @@ -319,6 +321,38 @@ pub fn is_world_readable(&self, room_id: &RoomId) -> Result<bool, Error> { Error::bad_database("Invalid room history visibility event in database.") }) }) - .unwrap_or(false)) + } + + /// Checks if guests are able to join a given room + pub fn guest_can_join(&self, room_id: &RoomId) -> Result<bool, Error> { + self.room_state_get(room_id, &StateEventType::RoomGuestAccess, "")? + .map_or(Ok(false), |s| { + serde_json::from_str(s.content.get()) + .map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin) + .map_err(|_| Error::bad_database("Invalid room guest access event in database.")) + }) + } + + /// Gets the primary alias from canonical alias event + pub fn get_canonical_alias(&self, room_id: &RoomId) -> Result<Option<OwnedRoomAliasId>, Error> { + self.room_state_get(room_id, &StateEventType::RoomCanonicalAlias, "")? + .map_or(Ok(None), |s| { + serde_json::from_str(s.content.get()) + .map(|c: RoomCanonicalAliasEventContent| c.alias) + .map_err(|_| Error::bad_database("Invalid canonical alias event in database.")) + }) + } + + /// Gets the room topic + pub fn get_room_topic(&self, room_id: &RoomId) -> Result<Option<String>, Error> { + self.room_state_get(room_id, &StateEventType::RoomTopic, "")? + .map_or(Ok(None), |s| { + serde_json::from_str(s.content.get()) + .map(|c: RoomTopicEventContent| Some(c.topic)) + .map_err(|e| { + error!("Invalid room topic event in database for room {room_id}: {e}"); + Error::bad_database("Invalid room topic event in database.") + }) + }) } }