diff --git a/src/database/key_value/rooms/alias.rs b/src/database/key_value/rooms/alias.rs index 6f230323..0d3e2b10 100644 --- a/src/database/key_value/rooms/alias.rs +++ b/src/database/key_value/rooms/alias.rs @@ -57,4 +57,28 @@ impl service::rooms::alias::Data for KeyValueDatabase { .map_err(|_| Error::bad_database("Invalid alias in aliasid_alias.")) })) } + + fn all_local_aliases<'a>( + &'a self, + ) -> Box> + 'a> { + Box::new( + self.alias_roomid + .iter() + .map(|(room_alias_bytes, room_id_bytes)| { + let room_alias_localpart = utils::string_from_bytes(&room_alias_bytes) + .map_err(|_| { + Error::bad_database("Invalid alias bytes in aliasid_alias.") + })?; + + let room_id = utils::string_from_bytes(&room_id_bytes) + .map_err(|_| { + Error::bad_database("Invalid room_id bytes in aliasid_alias.") + })? + .try_into() + .map_err(|_| Error::bad_database("Invalid room_id in aliasid_alias."))?; + + Ok((room_id, room_alias_localpart)) + }), + ) + } } diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index b22f8ed4..8b31aa50 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -5,10 +5,11 @@ use std::{ time::Instant, }; -use clap::Parser; +use clap::{Parser, Subcommand}; use regex::Regex; use ruma::{ events::{ + relation::InReplyTo, room::{ canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, @@ -16,14 +17,14 @@ use ruma::{ history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, - message::RoomMessageEventContent, + message::{Relation::Reply, RoomMessageEventContent}, name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, topic::RoomTopicEventContent, }, TimelineEventType, }, - EventId, OwnedRoomAliasId, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, + EventId, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::{mpsc, Mutex, MutexGuard}; @@ -37,10 +38,43 @@ use crate::{ use super::pdu::PduBuilder; +const PAGE_SIZE: usize = 100; + #[cfg_attr(test, derive(Debug))] #[derive(Parser)] #[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))] enum AdminCommand { + #[command(subcommand)] + /// Commands for managing appservices + Appservices(AppserviceCommand), + + #[command(subcommand)] + /// Commands for managing local users + Users(UserCommand), + + #[command(subcommand)] + /// Commands for managing rooms + Rooms(RoomCommand), + + #[command(subcommand)] + /// Commands for managing federation + Federation(FederationCommand), + + #[command(subcommand)] + /// Commands for managing the server + Server(ServerCommand), + + #[command(subcommand)] + // TODO: should i split out debug commands to a separate thing? the + // debug commands seem like they could fit in the other categories fine + // this is more like a "miscellaneous" category than a debug one + /// Commands for debugging things + Debug(DebugCommand), +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum AppserviceCommand { #[command(verbatim_doc_comment)] /// Register an appservice using its registration YAML /// @@ -54,33 +88,52 @@ enum AdminCommand { /// # ``` /// # yaml content here /// # ``` - RegisterAppservice, + Register, + #[command(verbatim_doc_comment)] /// Unregister an appservice using its ID /// /// You can find the ID using the `list-appservices` command. - UnregisterAppservice { + Unregister { /// The appservice to unregister appservice_identifier: String, }, + #[command(verbatim_doc_comment)] + /// Show an appservice's config using its ID + /// + /// You can find the ID using the `list-appservices` command. + Show { + /// The appservice to show + appservice_identifier: String, + }, + /// List all the currently registered appservices - ListAppservices, + List, +} - /// List all rooms the server knows about - ListRooms, +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum UserCommand { + /// Create a new user + Create { + /// Username of the new user + username: String, + /// Password of the new user, if unspecified one is generated + password: Option, + }, - /// List users in the database - ListLocalUsers, - - /// List all rooms we are currently handling an incoming pdu from - IncomingFederation, + /// Reset user password + ResetPassword { + /// Username of the user for whom the password should be reset + username: String, + }, /// Deactivate a user /// /// User will not be removed from all rooms by default. /// Use --leave-rooms to force the user to leave all rooms - DeactivateUser { + Deactivate { #[arg(short, long)] leave_rooms: bool, user_id: Box, @@ -109,6 +162,111 @@ enum AdminCommand { force: bool, }, + /// List local users in the database + List, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum RoomCommand { + /// List all rooms the server knows about + List { page: Option }, + + #[command(subcommand)] + /// Manage rooms' aliases + Alias(RoomAliasCommand), + + #[command(subcommand)] + /// Manage the room directory + Directory(RoomDirectoryCommand), +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum RoomAliasCommand { + /// Make an alias point to a room. + Set { + #[arg(short, long)] + /// Set the alias even if a room is already using it + force: bool, + + /// The room id to set the alias on + room_id: Box, + + /// The alias localpart to use (`alias`, not `#alias:servername.tld`) + room_alias_localpart: Box, + }, + + /// Remove an alias + Remove { + /// The alias localpart to remove (`alias`, not `#alias:servername.tld`) + room_alias_localpart: Box, + }, + + /// Show which room is using an alias + Which { + /// The alias localpart to look up (`alias`, not `#alias:servername.tld`) + room_alias_localpart: Box, + }, + + /// List aliases currently being used + List { + /// If set, only list the aliases for this room + room_id: Option>, + }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum RoomDirectoryCommand { + /// Publish a room to the room directory + Publish { + /// The room id of the room to publish + room_id: Box, + }, + + /// Unpublish a room to the room directory + Unpublish { + /// The room id of the room to unpublish + room_id: Box, + }, + + /// List rooms that are published + List { page: Option }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum FederationCommand { + /// List all rooms we are currently handling an incoming pdu from + IncomingFederation, + + /// Disables incoming federation handling for a room. + DisableRoom { room_id: Box }, + + /// Enables incoming federation handling for a room again. + EnableRoom { room_id: Box }, + + #[command(verbatim_doc_comment)] + /// Verify json signatures + /// [commandbody] + /// # ``` + /// # json here + /// # ``` + SignJson, + + #[command(verbatim_doc_comment)] + /// Verify json signatures + /// [commandbody] + /// # ``` + /// # json here + /// # ``` + VerifyJson, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum DebugCommand { /// Get the auth_chain of a PDU GetAuthChain { /// An event ID (the $ character followed by the base64 reference hash) @@ -132,6 +290,13 @@ enum AdminCommand { /// An event ID (a $ followed by the base64 reference hash) event_id: Box, }, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Subcommand)] +enum ServerCommand { + /// Show configuration values + ShowConfig, /// Print database memory usage statistics MemoryUsage, @@ -141,47 +306,11 @@ enum AdminCommand { /// Clears all of Conduit's service caches with index smaller than the amount ClearServiceCaches { amount: u32 }, - - /// Show configuration values - ShowConfig, - - /// Reset user password - ResetPassword { - /// Username of the user for whom the password should be reset - username: String, - }, - - /// Create a new user - CreateUser { - /// Username of the new user - username: String, - /// Password of the new user, if unspecified one is generated - password: Option, - }, - - /// Disables incoming federation handling for a room. - DisableRoom { room_id: Box }, - /// Enables incoming federation handling for a room again. - EnableRoom { room_id: Box }, - - /// Verify json signatures - /// [commandbody] - /// # ``` - /// # json here - /// # ``` - SignJson, - - /// Verify json signatures - /// [commandbody] - /// # ``` - /// # json here - /// # ``` - VerifyJson, } #[derive(Debug)] pub enum AdminRoomEvent { - ProcessMessage(String), + ProcessMessage(String, Arc), SendMessage(RoomMessageEventContent), } @@ -226,7 +355,17 @@ impl Service { .expect("Database data for admin room alias must be valid") .expect("Admin room must exist"); - let send_message = |message: RoomMessageEventContent, mutex_lock: &MutexGuard<'_, ()>| { + let send_message = |mut message: RoomMessageEventContent, + reply: Option>, + mutex_lock: &MutexGuard<'_, ()>| { + if let Some(reply) = reply { + message.relates_to = Some(Reply { + in_reply_to: InReplyTo { + event_id: reply.into(), + }, + }) + } + services() .rooms .timeline @@ -249,9 +388,11 @@ impl Service { loop { tokio::select! { Some(event) = receiver.recv() => { - let message_content = match event { - AdminRoomEvent::SendMessage(content) => content, - AdminRoomEvent::ProcessMessage(room_message) => self.process_admin_message(room_message).await + let (message_content, reply) = match event { + AdminRoomEvent::SendMessage(content) => (content, None), + AdminRoomEvent::ProcessMessage(room_message, reply_id) => { + (self.process_admin_message(room_message).await, Some(reply_id)) + } }; let mutex_state = Arc::clone( @@ -265,7 +406,7 @@ impl Service { let state_lock = mutex_state.lock().await; - send_message(message_content, &state_lock); + send_message(message_content, reply, &state_lock); drop(state_lock); } @@ -273,9 +414,9 @@ impl Service { } } - pub fn process_message(&self, room_message: String) { + pub fn process_message(&self, room_message: String, event_id: Arc) { self.sender - .send(AdminRoomEvent::ProcessMessage(room_message)) + .send(AdminRoomEvent::ProcessMessage(room_message, event_id)) .unwrap(); } @@ -347,486 +488,809 @@ impl Service { body: Vec<&str>, ) -> Result { let reply_message_content = match command { - AdminCommand::RegisterAppservice => { - if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" - { - let appservice_config = body[1..body.len() - 1].join("\n"); - let parsed_config = - serde_yaml::from_str::(&appservice_config); - match parsed_config { - Ok(yaml) => match services().appservice.register_appservice(yaml) { - Ok(id) => RoomMessageEventContent::text_plain(format!( - "Appservice registered with ID: {id}." - )), - Err(e) => RoomMessageEventContent::text_plain(format!( - "Failed to register appservice: {e}" - )), - }, - Err(e) => RoomMessageEventContent::text_plain(format!( - "Could not parse appservice config: {e}" - )), - } - } else { - RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - ) - } - } - AdminCommand::UnregisterAppservice { - appservice_identifier, - } => match services() - .appservice - .unregister_appservice(&appservice_identifier) - { - Ok(()) => RoomMessageEventContent::text_plain("Appservice unregistered."), - Err(e) => RoomMessageEventContent::text_plain(format!( - "Failed to unregister appservice: {e}" - )), - }, - AdminCommand::ListAppservices => { - if let Ok(appservices) = services() - .appservice - .iter_ids() - .map(|ids| ids.collect::>()) - { - let count = appservices.len(); - let output = format!( - "Appservices ({}): {}", - count, - appservices - .into_iter() - .filter_map(|r| r.ok()) - .collect::>() - .join(", ") - ); - RoomMessageEventContent::text_plain(output) - } else { - RoomMessageEventContent::text_plain("Failed to get appservices.") - } - } - AdminCommand::ListRooms => { - let room_ids = services().rooms.metadata.iter_ids(); - let output = format!( - "Rooms:\n{}", - room_ids - .filter_map(|r| r.ok()) - .map(|id| id.to_string() - + "\tMembers: " - + &services() - .rooms - .state_cache - .room_joined_count(&id) - .ok() - .flatten() - .unwrap_or(0) - .to_string()) - .collect::>() - .join("\n") - ); - RoomMessageEventContent::text_plain(output) - } - AdminCommand::ListLocalUsers => match services().users.list_local_users() { - Ok(users) => { - let mut msg: String = format!("Found {} local user account(s):\n", users.len()); - msg += &users.join("\n"); - RoomMessageEventContent::text_plain(&msg) - } - Err(e) => RoomMessageEventContent::text_plain(e.to_string()), - }, - AdminCommand::IncomingFederation => { - let map = services() - .globals - .roomid_federationhandletime - .read() - .unwrap(); - let mut msg: String = format!("Handling {} incoming pdus:\n", map.len()); - - for (r, (e, i)) in map.iter() { - let elapsed = i.elapsed(); - msg += &format!( - "{} {}: {}m{}s\n", - r, - e, - elapsed.as_secs() / 60, - elapsed.as_secs() % 60 - ); - } - RoomMessageEventContent::text_plain(&msg) - } - AdminCommand::GetAuthChain { event_id } => { - let event_id = Arc::::from(event_id); - if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? { - let room_id_str = event - .get("room_id") - .and_then(|val| val.as_str()) - .ok_or_else(|| Error::bad_database("Invalid event in database"))?; - - let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| { - Error::bad_database("Invalid room id field in event in database") - })?; - let start = Instant::now(); - let count = services() - .rooms - .auth_chain - .get_auth_chain(room_id, vec![event_id]) - .await? - .count(); - let elapsed = start.elapsed(); - RoomMessageEventContent::text_plain(format!( - "Loaded auth chain with length {count} in {elapsed:?}" - )) - } else { - RoomMessageEventContent::text_plain("Event not found.") - } - } - AdminCommand::ParsePdu => { - if body.len() > 2 && body[0].trim() == "```" && 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::( - serde_json::to_value(value).expect("value is json"), - ) { - Ok(pdu) => RoomMessageEventContent::text_plain(format!( - "EventId: {event_id:?}\n{pdu:#?}" - )), - Err(e) => RoomMessageEventContent::text_plain(format!( - "EventId: {event_id:?}\nCould not parse event: {e}" - )), - } - } - Err(e) => RoomMessageEventContent::text_plain(format!( - "Could not parse PDU JSON: {e:?}" + AdminCommand::Appservices(command) => match command { + AppserviceCommand::Register => { + if body.len() > 2 + && body[0].trim().starts_with("```") + && body.last().unwrap().trim() == "```" + { + let appservice_config = body[1..body.len() - 1].join("\n"); + let parsed_config = + serde_yaml::from_str::(&appservice_config); + match parsed_config { + Ok(yaml) => match services().appservice.register_appservice(yaml) { + Ok(id) => RoomMessageEventContent::text_plain(format!( + "Appservice registered with ID: {id}." + )), + Err(e) => RoomMessageEventContent::text_plain(format!( + "Failed to register appservice: {e}" )), - } - } - Err(e) => RoomMessageEventContent::text_plain(format!( - "Invalid json in command body: {e}" - )), - } - } else { - RoomMessageEventContent::text_plain("Expected code block in command body.") - } - } - AdminCommand::GetPdu { event_id } => { - let mut outlier = false; - let mut pdu_json = services() - .rooms - .timeline - .get_non_outlier_pdu_json(&event_id)?; - if pdu_json.is_none() { - outlier = true; - pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?; - } - match pdu_json { - Some(json) => { - let json_text = serde_json::to_string_pretty(&json) - .expect("canonical json is valid json"); - RoomMessageEventContent::text_html( - format!( - "{}\n```json\n{}\n```", - if outlier { - "PDU is outlier" - } else { - "PDU was accepted" - }, - json_text - ), - format!( - "

{}

\n
{}\n
\n", - if outlier { - "PDU is outlier" - } else { - "PDU was accepted" - }, - HtmlEscape(&json_text) - ), - ) - } - None => RoomMessageEventContent::text_plain("PDU not found."), - } - } - AdminCommand::MemoryUsage => { - let response1 = services().memory_usage(); - let response2 = services().globals.db.memory_usage(); - - RoomMessageEventContent::text_plain(format!( - "Services:\n{response1}\n\nDatabase:\n{response2}" - )) - } - AdminCommand::ClearDatabaseCaches { amount } => { - services().globals.db.clear_caches(amount); - - RoomMessageEventContent::text_plain("Done.") - } - AdminCommand::ClearServiceCaches { amount } => { - services().clear_caches(amount); - - RoomMessageEventContent::text_plain("Done.") - } - AdminCommand::ShowConfig => { - // Construct and send the response - RoomMessageEventContent::text_plain(format!("{}", services().globals.config)) - } - AdminCommand::ResetPassword { username } => { - 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}" - ))) - } - }; - - // 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") - { - return Ok(RoomMessageEventContent::text_plain( - "The specified user does not exist!", - )); - } - - let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); - - match services() - .users - .set_password(&user_id, Some(new_password.as_str())) - { - Ok(()) => RoomMessageEventContent::text_plain(format!( - "Successfully reset the password for user {user_id}: {new_password}" - )), - Err(e) => RoomMessageEventContent::text_plain(format!( - "Couldn't reset the password for user {user_id}: {e}" - )), - } - } - AdminCommand::CreateUser { username, password } => { - 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_id.is_historical() { - return Ok(RoomMessageEventContent::text_plain(format!( - "Userid {user_id} is not allowed due to historical" - ))); - } - if services().users.exists(&user_id)? { - return Ok(RoomMessageEventContent::text_plain(format!( - "Userid {user_id} already exists" - ))); - } - // Create user - services().users.create(&user_id, Some(password.as_str()))?; - - // Default to pretty displayname - let mut displayname = user_id.localpart().to_owned(); - - // If enabled append lightning bolt to display name (default true) - if services().globals.enable_lightning_bolt() { - displayname.push_str(" ⚡️"); - } - - services() - .users - .set_displayname(&user_id, Some(displayname))?; - - // Initial account data - services().account_data.update( - None, - &user_id, - ruma::events::GlobalAccountDataEventType::PushRules - .to_string() - .into(), - &serde_json::to_value(ruma::events::push_rules::PushRulesEvent { - content: ruma::events::push_rules::PushRulesEventContent { - global: ruma::push::Ruleset::server_default(&user_id), - }, - }) - .expect("to json value always works"), - )?; - - // we dont add a device since we're not the user, just the creator - - // Inhibit login does not work for guests - RoomMessageEventContent::text_plain(format!( - "Created user with user_id: {user_id} and password: {password}" - )) - } - AdminCommand::DisableRoom { room_id } => { - services().rooms.metadata.disable_room(&room_id, true)?; - RoomMessageEventContent::text_plain("Room disabled.") - } - AdminCommand::EnableRoom { room_id } => { - services().rooms.metadata.disable_room(&room_id, false)?; - RoomMessageEventContent::text_plain("Room enabled.") - } - AdminCommand::DeactivateUser { - leave_rooms, - user_id, - } => { - let user_id = Arc::::from(user_id); - 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?; - } - - RoomMessageEventContent::text_plain(format!( - "User {user_id} has been deactivated" - )) - } else { - RoomMessageEventContent::text_plain(format!( - "User {user_id} doesn't exist on this server" - )) - } - } - AdminCommand::DeactivateAll { leave_rooms, force } => { - if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" - { - let usernames = body.clone().drain(1..body.len() - 1).collect::>(); - - let mut user_ids: Vec<&UserId> = Vec::new(); - - for &username in &usernames { - match <&UserId>::try_from(username) { - Ok(user_id) => user_ids.push(user_id), - Err(_) => { - return Ok(RoomMessageEventContent::text_plain(format!( - "{username} is not a valid username" - ))) - } - } - } - - let mut deactivation_count = 0; - let mut admins = Vec::new(); - - if !force { - user_ids.retain(|&user_id| match services().users.is_admin(user_id) { - Ok(is_admin) => match is_admin { - true => { - admins.push(user_id.localpart()); - false - } - false => true, }, - Err(_) => false, + Err(e) => RoomMessageEventContent::text_plain(format!( + "Could not parse appservice config: {e}" + )), + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } + AppserviceCommand::Unregister { + appservice_identifier, + } => match services() + .appservice + .unregister_appservice(&appservice_identifier) + { + Ok(()) => RoomMessageEventContent::text_plain("Appservice unregistered."), + Err(e) => RoomMessageEventContent::text_plain(format!( + "Failed to unregister appservice: {e}" + )), + }, + AppserviceCommand::Show { + appservice_identifier, + } => { + match services() + .appservice + .get_registration(&appservice_identifier) + { + Ok(Some(config)) => { + let config_str = serde_yaml::to_string(&config) + .expect("config should've been validated on register"); + let output = format!( + "Config for {}:\n\n```yaml\n{}\n```", + appservice_identifier, config_str, + ); + let output_html = format!( + "Config for {}:\n\n
{}
", + escape_html(&appservice_identifier), + escape_html(&config_str), + ); + RoomMessageEventContent::text_html(output, output_html) + } + Ok(None) => { + RoomMessageEventContent::text_plain("Appservice does not exist.") + } + Err(_) => RoomMessageEventContent::text_plain("Failed to get appservice."), + } + } + AppserviceCommand::List => { + if let Ok(appservices) = services() + .appservice + .iter_ids() + .map(|ids| ids.collect::>()) + { + let count = appservices.len(); + let output = format!( + "Appservices ({}): {}", + count, + appservices + .into_iter() + .filter_map(|r| r.ok()) + .collect::>() + .join(", ") + ); + RoomMessageEventContent::text_plain(output) + } else { + RoomMessageEventContent::text_plain("Failed to get appservices.") + } + } + }, + AdminCommand::Users(command) => match command { + UserCommand::List => match services().users.list_local_users() { + Ok(users) => { + let mut msg: String = + format!("Found {} local user account(s):\n", users.len()); + msg += &users.join("\n"); + RoomMessageEventContent::text_plain(&msg) + } + Err(e) => RoomMessageEventContent::text_plain(e.to_string()), + }, + UserCommand::Create { username, password } => { + 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_id.is_historical() { + return Ok(RoomMessageEventContent::text_plain(format!( + "Userid {user_id} is not allowed due to historical" + ))); + } + if services().users.exists(&user_id)? { + return Ok(RoomMessageEventContent::text_plain(format!( + "Userid {user_id} already exists" + ))); + } + // Create user + services().users.create(&user_id, Some(password.as_str()))?; + + // Default to pretty displayname + let mut displayname = user_id.localpart().to_owned(); + + // If enabled append lightning bolt to display name (default true) + if services().globals.enable_lightning_bolt() { + displayname.push_str(" ⚡️"); + } + + services() + .users + .set_displayname(&user_id, Some(displayname))?; + + // Initial account data + services().account_data.update( + None, + &user_id, + ruma::events::GlobalAccountDataEventType::PushRules + .to_string() + .into(), + &serde_json::to_value(ruma::events::push_rules::PushRulesEvent { + content: ruma::events::push_rules::PushRulesEventContent { + global: ruma::push::Ruleset::server_default(&user_id), + }, }) - } + .expect("to json value always works"), + )?; - for &user_id in &user_ids { - if services().users.deactivate_account(user_id).is_ok() { - deactivation_count += 1 - } - } + // we dont add a device since we're not the user, just the creator - if leave_rooms { - for &user_id in &user_ids { - let _ = leave_all_rooms(user_id).await; - } - } - - if admins.is_empty() { + // Inhibit login does not work for guests + RoomMessageEventContent::text_plain(format!( + "Created user with user_id: {user_id} and password: {password}" + )) + } + UserCommand::Deactivate { + leave_rooms, + user_id, + } => { + let user_id = Arc::::from(user_id); + if services().users.exists(&user_id)? { RoomMessageEventContent::text_plain(format!( - "Deactivated {deactivation_count} accounts." + "Making {user_id} leave all rooms before deactivation..." + )); + + services().users.deactivate_account(&user_id)?; + + if leave_rooms { + leave_all_rooms(&user_id).await?; + } + + RoomMessageEventContent::text_plain(format!( + "User {user_id} has been deactivated" )) } else { - RoomMessageEventContent::text_plain(format!("Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", "))) + RoomMessageEventContent::text_plain(format!( + "User {user_id} doesn't exist on this server" + )) } - } else { - RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - ) } - } - AdminCommand::SignJson => { - if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" - { - let string = body[1..body.len() - 1].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"); - RoomMessageEventContent::text_plain(json_text) + UserCommand::ResetPassword { username } => { + 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}" + ))) } - Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + }; + + // 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") + { + return Ok(RoomMessageEventContent::text_plain( + "The specified user does not exist!", + )); + } + + let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); + + match services() + .users + .set_password(&user_id, Some(new_password.as_str())) + { + Ok(()) => RoomMessageEventContent::text_plain(format!( + "Successfully reset the password for user {user_id}: {new_password}" + )), + Err(e) => RoomMessageEventContent::text_plain(format!( + "Couldn't reset the password for user {user_id}: {e}" + )), } - } else { - RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - ) } - } - AdminCommand::VerifyJson => { - if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" - { - let string = body[1..body.len() - 1].join("\n"); - match serde_json::from_str(&string) { - Ok(value) => { - let pub_key_map = RwLock::new(BTreeMap::new()); + UserCommand::DeactivateAll { leave_rooms, force } => { + if body.len() > 2 + && body[0].trim().starts_with("```") + && body.last().unwrap().trim() == "```" + { + let usernames = body.clone().drain(1..body.len() - 1).collect::>(); - services() + let mut user_ids: Vec<&UserId> = Vec::new(); + + for &username in &usernames { + match <&UserId>::try_from(username) { + Ok(user_id) => user_ids.push(user_id), + Err(_) => { + return Ok(RoomMessageEventContent::text_plain(format!( + "{username} is not a valid username" + ))) + } + } + } + + let mut deactivation_count = 0; + let mut admins = Vec::new(); + + if !force { + user_ids.retain(|&user_id| match services().users.is_admin(user_id) { + Ok(is_admin) => match is_admin { + true => { + admins.push(user_id.localpart()); + false + } + false => true, + }, + Err(_) => false, + }) + } + + for &user_id in &user_ids { + if services().users.deactivate_account(user_id).is_ok() { + deactivation_count += 1 + } + } + + if leave_rooms { + for &user_id in &user_ids { + let _ = leave_all_rooms(user_id).await; + } + } + + if admins.is_empty() { + RoomMessageEventContent::text_plain(format!( + "Deactivated {deactivation_count} accounts." + )) + } else { + RoomMessageEventContent::text_plain(format!("Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", "))) + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } + }, + AdminCommand::Rooms(command) => match command { + RoomCommand::List { page } => { + // TODO: i know there's a way to do this with clap, but i can't seem to find it + let page = page.unwrap_or(1); + let mut rooms = services() + .rooms + .metadata + .iter_ids() + .filter_map(|r| r.ok()) + .map(Self::get_room_info) + .collect::>(); + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let rooms: Vec<_> = rooms + .into_iter() + .skip(page.saturating_sub(1) * PAGE_SIZE) + .take(PAGE_SIZE) + .collect(); + + if rooms.is_empty() { + return Ok(RoomMessageEventContent::text_plain("No more rooms.")); + }; + + let output_plain = format!( + "Rooms:\n{}", + rooms + .iter() + .map(|(id, members, name)| format!( + "{id}\tMembers: {members}\tName: {name}" + )) + .collect::>() + .join("\n") + ); + let output_html = format!( + "\n\t\t\n{}
Room list - page {page}
idmembersname
", + rooms + .iter() + .map(|(id, members, name)| format!( + "{}\t{}\t{}\n", + escape_html(&id.to_string()), + members, + escape_html(name), + )) + .collect::() + ); + RoomMessageEventContent::text_html(output_plain, output_html) + } + RoomCommand::Alias(command) => match command { + RoomAliasCommand::Set { + ref room_alias_localpart, + .. + } + | RoomAliasCommand::Remove { + ref room_alias_localpart, + } + | RoomAliasCommand::Which { + ref room_alias_localpart, + } => { + let room_alias_str = format!( + "#{}:{}", + room_alias_localpart, + services().globals.server_name() + ); + let room_alias = match RoomAliasId::parse_box(room_alias_str) { + Ok(alias) => alias, + Err(err) => { + return Ok(RoomMessageEventContent::text_plain(format!( + "Failed to parse alias: {}", + err + ))) + } + }; + + match command { + RoomAliasCommand::Set { force, room_id, .. } => { + match (force, services().rooms.alias.resolve_local_alias(&room_alias)) { + (true, Ok(Some(id))) => match services().rooms.alias.set_alias(&room_alias, &room_id) { + Ok(()) => RoomMessageEventContent::text_plain(format!("Successfully overwrote alias (formerly {})", id)), + Err(err) => RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err)), + } + (false, Ok(Some(id))) => { + RoomMessageEventContent::text_plain(format!("Refusing to overwrite in use alias for {}, use -f or --force to overwrite", id)) + } + (_, Ok(None)) => match services().rooms.alias.set_alias(&room_alias, &room_id) { + Ok(()) => RoomMessageEventContent::text_plain("Successfully set alias"), + Err(err) => RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err)), + } + (_, Err(err)) => RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {}", err)), + } + } + RoomAliasCommand::Remove { .. } => { + match services().rooms.alias.resolve_local_alias(&room_alias) { + Ok(Some(id)) => { + match services().rooms.alias.remove_alias(&room_alias) { + Ok(()) => RoomMessageEventContent::text_plain(format!( + "Removed alias from {}", + id + )), + Err(err) => RoomMessageEventContent::text_plain( + format!("Failed to remove alias: {}", err), + ), + } + } + Ok(None) => { + RoomMessageEventContent::text_plain("Alias isn't in use.") + } + Err(err) => RoomMessageEventContent::text_plain(format!( + "Unable to lookup alias: {}", + err + )), + } + } + RoomAliasCommand::Which { .. } => { + match services().rooms.alias.resolve_local_alias(&room_alias) { + Ok(Some(id)) => RoomMessageEventContent::text_plain(format!( + "Alias resolves to {}", + id + )), + Ok(None) => { + RoomMessageEventContent::text_plain("Alias isn't in use.") + } + Err(err) => RoomMessageEventContent::text_plain(&format!( + "Unable to lookup alias: {}", + err + )), + } + } + RoomAliasCommand::List { .. } => unreachable!(), + } + } + RoomAliasCommand::List { room_id } => match room_id { + Some(room_id) => { + let aliases: Result, _> = services() .rooms - .event_handler - .fetch_required_signing_keys(&value, &pub_key_map) - .await?; + .alias + .local_aliases_for_room(&room_id) + .collect(); + match aliases { + Ok(aliases) => { + let plain_list: String = aliases + .iter() + .map(|alias| format!("- {}\n", alias)) + .collect(); - let pub_key_map = pub_key_map.read().unwrap(); - match ruma::signatures::verify_json(&pub_key_map, &value) { - Ok(_) => RoomMessageEventContent::text_plain("Signature correct"), - Err(e) => RoomMessageEventContent::text_plain(format!( - "Signature verification failed: {e}" + let html_list: String = aliases + .iter() + .map(|alias| { + format!( + "
  • {}
  • \n", + escape_html(&alias.to_string()) + ) + }) + .collect(); + + let plain = format!("Aliases for {}:\n{}", room_id, plain_list); + let html = + format!("Aliases for {}:\n
      {}
    ", room_id, html_list); + RoomMessageEventContent::text_html(plain, html) + } + Err(err) => RoomMessageEventContent::text_plain(&format!( + "Unable to list aliases: {}", + err )), } } - Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + None => { + let aliases: Result, _> = + services().rooms.alias.all_local_aliases().collect(); + match aliases { + Ok(aliases) => { + let server_name = services().globals.server_name(); + let plain_list: String = aliases + .iter() + .map(|(id, alias)| { + format!("- #{}:{} -> {}\n", alias, server_name, id) + }) + .collect(); + + let html_list: String = aliases + .iter() + .map(|(id, alias)| { + format!( + "
  • #{}:{} -> {}
  • \n", + escape_html(&alias.to_string()), + server_name, + escape_html(&id.to_string()) + ) + }) + .collect(); + + let plain = format!("Aliases:\n{}", plain_list); + let html = format!("Aliases:\n
      {}
    ", html_list); + RoomMessageEventContent::text_html(plain, html) + } + Err(err) => RoomMessageEventContent::text_plain(&format!( + "Unable to list aliases: {}", + err + )), + } + } + }, + }, + RoomCommand::Directory(command) => match command { + RoomDirectoryCommand::Publish { room_id } => { + match services().rooms.directory.set_public(&room_id) { + Ok(()) => RoomMessageEventContent::text_plain("Room published"), + Err(err) => RoomMessageEventContent::text_plain(&format!( + "Unable to update room: {}", + err + )), + } } - } else { - RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - ) + RoomDirectoryCommand::Unpublish { room_id } => { + match services().rooms.directory.set_not_public(&room_id) { + Ok(()) => RoomMessageEventContent::text_plain("Room unpublished"), + Err(err) => RoomMessageEventContent::text_plain(&format!( + "Unable to update room: {}", + err + )), + } + } + RoomDirectoryCommand::List { page } => { + let page = page.unwrap_or(1); + let mut rooms = services() + .rooms + .directory + .public_rooms() + .filter_map(|r| r.ok()) + .map(Self::get_room_info) + .collect::>(); + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let rooms: Vec<_> = rooms + .into_iter() + .skip(page.saturating_sub(1) * PAGE_SIZE) + .take(PAGE_SIZE) + .collect(); + + if rooms.is_empty() { + return Ok(RoomMessageEventContent::text_plain("No more rooms.")); + }; + + let output_plain = format!( + "Rooms:\n{}", + rooms + .iter() + .map(|(id, members, name)| format!( + "{id}\tMembers: {members}\tName: {name}" + )) + .collect::>() + .join("\n") + ); + let output_html = format!( + "\n\t\t\n{}
    Room directory - page {page}
    idmembersname
    ", + rooms + .iter() + .map(|(id, members, name)| format!( + "{}\t{}\t{}\n", + escape_html(&id.to_string()), + members, + escape_html(name), + )) + .collect::() + ); + RoomMessageEventContent::text_html(output_plain, output_html) + } + }, + }, + AdminCommand::Federation(command) => match command { + FederationCommand::DisableRoom { room_id } => { + services().rooms.metadata.disable_room(&room_id, true)?; + RoomMessageEventContent::text_plain("Room disabled.") } - } + FederationCommand::EnableRoom { room_id } => { + services().rooms.metadata.disable_room(&room_id, false)?; + RoomMessageEventContent::text_plain("Room enabled.") + } + FederationCommand::IncomingFederation => { + let map = services() + .globals + .roomid_federationhandletime + .read() + .unwrap(); + let mut msg: String = format!("Handling {} incoming pdus:\n", map.len()); + + for (r, (e, i)) in map.iter() { + let elapsed = i.elapsed(); + msg += &format!( + "{} {}: {}m{}s\n", + r, + e, + elapsed.as_secs() / 60, + elapsed.as_secs() % 60 + ); + } + RoomMessageEventContent::text_plain(&msg) + } + FederationCommand::SignJson => { + 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(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"); + RoomMessageEventContent::text_plain(json_text) + } + Err(e) => { + RoomMessageEventContent::text_plain(format!("Invalid json: {e}")) + } + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } + FederationCommand::VerifyJson => { + 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) => { + 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().unwrap(); + match ruma::signatures::verify_json(&pub_key_map, &value) { + Ok(_) => { + RoomMessageEventContent::text_plain("Signature correct") + } + Err(e) => RoomMessageEventContent::text_plain(format!( + "Signature verification failed: {e}" + )), + } + } + Err(e) => { + RoomMessageEventContent::text_plain(format!("Invalid json: {e}")) + } + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } + }, + AdminCommand::Server(command) => match command { + ServerCommand::ShowConfig => { + // Construct and send the response + RoomMessageEventContent::text_plain(format!("{}", services().globals.config)) + } + ServerCommand::MemoryUsage => { + let response1 = services().memory_usage(); + let response2 = services().globals.db.memory_usage(); + + RoomMessageEventContent::text_plain(format!( + "Services:\n{response1}\n\nDatabase:\n{response2}" + )) + } + ServerCommand::ClearDatabaseCaches { amount } => { + services().globals.db.clear_caches(amount); + + RoomMessageEventContent::text_plain("Done.") + } + ServerCommand::ClearServiceCaches { amount } => { + services().clear_caches(amount); + + RoomMessageEventContent::text_plain("Done.") + } + }, + AdminCommand::Debug(command) => match command { + DebugCommand::GetAuthChain { event_id } => { + let event_id = Arc::::from(event_id); + if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? { + let room_id_str = event + .get("room_id") + .and_then(|val| val.as_str()) + .ok_or_else(|| Error::bad_database("Invalid event in database"))?; + + let room_id = <&RoomId>::try_from(room_id_str).map_err(|_| { + Error::bad_database("Invalid room id field in event in database") + })?; + let start = Instant::now(); + let count = services() + .rooms + .auth_chain + .get_auth_chain(room_id, vec![event_id]) + .await? + .count(); + let elapsed = start.elapsed(); + RoomMessageEventContent::text_plain(format!( + "Loaded auth chain with length {count} in {elapsed:?}" + )) + } else { + RoomMessageEventContent::text_plain("Event not found.") + } + } + DebugCommand::ParsePdu => { + 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::( + serde_json::to_value(value).expect("value is json"), + ) { + Ok(pdu) => RoomMessageEventContent::text_plain( + format!("EventId: {event_id:?}\n{pdu:#?}"), + ), + Err(e) => RoomMessageEventContent::text_plain(format!( + "EventId: {event_id:?}\nCould not parse event: {e}" + )), + } + } + Err(e) => RoomMessageEventContent::text_plain(format!( + "Could not parse PDU JSON: {e:?}" + )), + } + } + Err(e) => RoomMessageEventContent::text_plain(format!( + "Invalid json in command body: {e}" + )), + } + } else { + RoomMessageEventContent::text_plain("Expected code block in command body.") + } + } + DebugCommand::GetPdu { event_id } => { + let mut outlier = false; + let mut pdu_json = services() + .rooms + .timeline + .get_non_outlier_pdu_json(&event_id)?; + if pdu_json.is_none() { + outlier = true; + pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?; + } + match pdu_json { + Some(json) => { + let json_text = serde_json::to_string_pretty(&json) + .expect("canonical json is valid json"); + RoomMessageEventContent::text_html( + format!( + "{}\n```json\n{}\n```", + if outlier { + "PDU is outlier" + } else { + "PDU was accepted" + }, + json_text + ), + format!( + "

    {}

    \n
    {}\n
    \n", + if outlier { + "PDU is outlier" + } else { + "PDU was accepted" + }, + HtmlEscape(&json_text) + ), + ) + } + None => RoomMessageEventContent::text_plain("PDU not found."), + } + } + }, }; Ok(reply_message_content) } + fn get_room_info(id: OwnedRoomId) -> (OwnedRoomId, u64, String) { + ( + id.clone(), + services() + .rooms + .state_cache + .room_joined_count(&id) + .ok() + .flatten() + .unwrap_or(0), + services() + .rooms + .state_accessor + .get_name(&id) + .ok() + .flatten() + .unwrap_or(id.to_string()), + ) + } + // Utility to turn clap's `--help` text to HTML. fn usage_to_html(&self, text: &str, server_name: &ServerName) -> String { // Replace `@conduit:servername:-subcmdname` with `@conduit:servername: subcmdname` @@ -840,7 +1304,7 @@ impl Service { let text = text.replace("subcommand", "command"); // Escape option names (e.g. ``) since they look like HTML tags - let text = text.replace('<', "<").replace('>', ">"); + let text = escape_html(&text); // Italicize the first line (command name and version text) let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail"); @@ -1226,6 +1690,12 @@ impl Service { } } +fn escape_html(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + #[cfg(test)] mod test { use super::*; diff --git a/src/service/rooms/alias/data.rs b/src/service/rooms/alias/data.rs index 629b1ee1..e2647ffc 100644 --- a/src/service/rooms/alias/data.rs +++ b/src/service/rooms/alias/data.rs @@ -16,4 +16,9 @@ pub trait Data: Send + Sync { &'a self, room_id: &RoomId, ) -> Box> + 'a>; + + /// Returns all local aliases on the server + fn all_local_aliases<'a>( + &'a self, + ) -> Box> + 'a>; } diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index d26030c0..34a5732b 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -32,4 +32,11 @@ impl Service { ) -> Box> + 'a> { self.db.local_aliases_for_room(room_id) } + + #[tracing::instrument(skip(self))] + pub fn all_local_aliases<'a>( + &'a self, + ) -> Box> + 'a> { + self.db.all_local_aliases() + } } diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 25e1c54d..52947887 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -466,7 +466,7 @@ impl Service { && services().globals.emergency_password().is_none(); if to_conduit && !from_conduit && admin_room.as_ref() == Some(&pdu.room_id) { - services().admin.process_message(body); + services().admin.process_message(body, pdu.event_id.clone()); } } }