diff --git a/src/database/key_value/rooms/alias.rs b/src/database/key_value/rooms/alias.rs index 2f7df781..0114d322 100644 --- a/src/database/key_value/rooms/alias.rs +++ b/src/database/key_value/rooms/alias.rs @@ -75,4 +75,28 @@ impl service::rooms::alias::Data for KeyValueDatabase { }) .transpose() } + + 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 583bfcd1..f85a5622 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -1,10 +1,11 @@ use std::{collections::BTreeMap, convert::TryFrom, sync::Arc, time::Instant}; -use clap::Parser; +use clap::{Parser, Subcommand}; use regex::Regex; use ruma::{ api::appservice::Registration, events::{ + relation::InReplyTo, room::{ canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, @@ -12,15 +13,15 @@ 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, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, - RoomVersionId, ServerName, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, + RoomId, RoomVersionId, ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::{mpsc, Mutex, RwLock}; @@ -34,10 +35,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 /// @@ -51,39 +85,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, - - /// Removes an alias from the server - RemoveAlias { - /// The alias to be removed - alias: Box, + /// 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, @@ -112,6 +159,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) @@ -135,6 +287,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, @@ -144,46 +303,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, - }, - - /// Temporarily toggle user registration by passing either true or false as an argument, does not persist between restarts - AllowRegistration { status: Option }, - - /// Disables incoming federation handling for a room. - DisableRoom { room_id: Box }, - /// Enables incoming federation handling for a room again. - EnableRoom { room_id: Box }, - - /// Sign a json object using Conduit's signing keys, putting the json in a codeblock - SignJson, - - /// Verify json signatures, putting the json in a codeblock - VerifyJson, - - /// Parses a JSON object as an event then creates a hash and signs it, putting a room - /// version as an argument, and the json in a codeblock - HashAndSignEvent { room_version_id: RoomVersionId }, } #[derive(Debug)] pub enum AdminRoomEvent { - ProcessMessage(String), + ProcessMessage(String, Arc, OwnedUserId), SendMessage(RoomMessageEventContent), } @@ -219,9 +343,9 @@ 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 (mut message_content, reply) = match event { + AdminRoomEvent::SendMessage(content) => (content, None), + AdminRoomEvent::ProcessMessage(room_message, reply_id, sender) => (self.process_admin_message(room_message, &sender).await, Some(reply_id)) }; let mutex_state = Arc::clone( @@ -235,6 +359,10 @@ impl Service { let state_lock = mutex_state.lock().await; + if let Some(reply) = reply { + message_content.relates_to = Some(Reply { in_reply_to: InReplyTo { event_id: reply.into() } }) + } + services() .rooms .timeline @@ -248,20 +376,32 @@ impl Service { redacts: None, timestamp: None, }, - conduit_user, + &conduit_user, &conduit_room, &state_lock, ) - .await.unwrap(); + .await + .unwrap(); + + drop(state_lock); } } } } } - pub fn process_message(&self, room_message: String) { + pub fn process_message( + &self, + room_message: String, + event_id: Arc, + sender: OwnedUserId, + ) { self.sender - .send(AdminRoomEvent::ProcessMessage(room_message)) + .send(AdminRoomEvent::ProcessMessage( + room_message, + event_id, + sender, + )) .unwrap(); } @@ -272,7 +412,11 @@ impl Service { } // Parse and process a message from the admin room - async fn process_admin_message(&self, room_message: String) -> RoomMessageEventContent { + async fn process_admin_message( + &self, + room_message: String, + sender: &UserId, + ) -> RoomMessageEventContent { let mut lines = room_message.lines().filter(|l| !l.trim().is_empty()); let command_line = lines.next().expect("each string has at least one line"); let body: Vec<_> = lines.collect(); @@ -288,7 +432,10 @@ impl Service { } }; - match self.process_admin_command(admin_command, body).await { + match self + .process_admin_command(admin_command, body, sender) + .await + { Ok(reply_message) => reply_message, Err(error) => { let markdown_message = format!( @@ -331,645 +478,823 @@ impl Service { &self, command: AdminCommand, body: Vec<&str>, + sender: &UserId, ) -> 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).await { - 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) - .await - { - Ok(()) => RoomMessageEventContent::text_plain("Appservice unregistered."), - Err(e) => RoomMessageEventContent::text_plain(format!( - "Failed to unregister appservice: {e}" - )), - }, - AdminCommand::ListAppservices => { - let appservices = services().appservice.iter_ids().await; - let output = format!( - "Appservices ({}): {}", - appservices.len(), - appservices.join(", ") - ); - RoomMessageEventContent::text_plain(output) - } - 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().await; - 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).await + { + 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}" + )), } - 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().await; - 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).await; - - 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}" - ))) - } - }; - - // Checks if user is local - if user_id.server_name() != services().globals.server_name() { - return Ok(RoomMessageEventContent::text_plain( - "The specified user is not from this 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") - { - 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}" - ))) - } - }; - - // Checks if user is local - if user_id.server_name() != services().globals.server_name() { - return Ok(RoomMessageEventContent::text_plain( - "The specified user is not from this server!", - )); - }; - - 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::AllowRegistration { status } => { - if let Some(status) = status { - services().globals.set_registration(status).await; - RoomMessageEventContent::text_plain(if status { - "Registration is now enabled" } else { - "Registration is now disabled" - }) - } else { - RoomMessageEventContent::text_plain( - if services().globals.allow_registration().await { - "Registration is currently enabled" - } else { - "Registration is currently disabled" - }, - ) - } - } - 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!( - "User {user_id} doesn't exist on this server" - )) - } else if user_id.server_name() != services().globals.server_name() { - RoomMessageEventContent::text_plain(format!( - "User {user_id} is not from this server" - )) - } else { - 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( + "Expected code block in command body. Add --help for details.", + ) } - - RoomMessageEventContent::text_plain(format!( - "User {user_id} has been deactivated" - )) } - } - AdminCommand::DeactivateAll { leave_rooms, force } => { - if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" + AppserviceCommand::Unregister { + appservice_identifier, + } => match services() + .appservice + .unregister_appservice(&appservice_identifier) + .await { - let users = body.clone().drain(1..body.len() - 1).collect::>(); - - let mut user_ids = Vec::new(); - let mut remote_ids = Vec::new(); - let mut non_existant_ids = Vec::new(); - let mut invalid_users = Vec::new(); - - for &user in &users { - match <&UserId>::try_from(user) { - Ok(user_id) => { - if user_id.server_name() != services().globals.server_name() { - remote_ids.push(user_id) - } else if !services().users.exists(user_id)? { - non_existant_ids.push(user_id) - } else { - user_ids.push(user_id) - } - } - Err(_) => { - invalid_users.push(user); - } + 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) + .await + { + Some(registration) => { + let config_str = serde_yaml::to_string(®istration) + .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) } + None => RoomMessageEventContent::text_plain("Appservice does not exist."), + } + } + AppserviceCommand::List => { + let appservices = services().appservice.iter_ids().await; + let count = appservices.len(); + let output = format!("Appservices ({}): {}", count, appservices.join(", ")); + RoomMessageEventContent::text_plain(output) + } + }, + 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(" ⚡️"); } - let mut markdown_message = String::new(); - let mut html_message = String::new(); - if !invalid_users.is_empty() { - markdown_message.push_str("The following user ids are not valid:\n```\n"); - html_message.push_str("The following user ids are not valid:\n
\n");
-                        for invalid_user in invalid_users {
-                            markdown_message.push_str(&format!("{invalid_user}\n"));
-                            html_message.push_str(&format!("{invalid_user}\n"));
+                    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}"
+                    ))
+                }
+                UserCommand::Deactivate {
+                    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?;
                         }
-                        markdown_message.push_str("```\n\n");
-                        html_message.push_str("
\n\n"); + + RoomMessageEventContent::text_plain(format!( + "User {user_id} has been deactivated" + )) + } else { + RoomMessageEventContent::text_plain(format!( + "User {user_id} doesn't exist on this server" + )) } - if !remote_ids.is_empty() { - markdown_message - .push_str("The following users are not from this server:\n```\n"); - html_message - .push_str("The following users are not from this server:\n
\n");
-                        for remote_id in remote_ids {
-                            markdown_message.push_str(&format!("{remote_id}\n"));
-                            html_message.push_str(&format!("{remote_id}\n"));
+                }
+                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}"
+                            )))
                         }
-                        markdown_message.push_str("```\n\n");
-                        html_message.push_str("
\n\n"); - } - if !non_existant_ids.is_empty() { - markdown_message.push_str("The following users do not exist:\n```\n"); - html_message.push_str("The following users do not exist:\n
\n");
-                        for non_existant_id in non_existant_ids {
-                            markdown_message.push_str(&format!("{non_existant_id}\n"));
-                            html_message.push_str(&format!("{non_existant_id}\n"));
-                        }
-                        markdown_message.push_str("```\n\n");
-                        html_message.push_str("
\n\n"); - } - if !markdown_message.is_empty() { - return Ok(RoomMessageEventContent::text_html( - markdown_message, - html_message, + }; + + // 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 mut deactivation_count = 0; - let mut admins = Vec::new(); + let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); - 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 + 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}" + )), + } + } + 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::>(); + + 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" + ))) } - false => true, - }, - Err(_) => false, - }) - } - - for &user_id in &user_ids { - if services().users.deactivate_account(user_id).is_ok() { - deactivation_count += 1 + } + } + + 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, + }) } - } - if leave_rooms { for &user_id in &user_ids { - let _ = leave_all_rooms(user_id).await; + if services().users.deactivate_account(user_id).is_ok() { + deactivation_count += 1 + } } - } - if admins.is_empty() { - RoomMessageEventContent::text_plain(format!( - "Deactivated {deactivation_count} accounts." - )) + 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(format!("Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", "))) + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) } - } 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) - } - Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + }, + 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, + .. } - } 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()); + | 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 + ))) + } + }; - services() - .rooms - .event_handler - // Generally we shouldn't be checking against expired keys unless required, so in the admin - // room it might be best to not allow expired keys - .fetch_required_signing_keys(&value, &pub_key_map) - .await?; - - let mut expired_key_map = BTreeMap::new(); - let mut valid_key_map = BTreeMap::new(); - - for (server, keys) in pub_key_map.into_inner().into_iter() { - if keys.valid_until_ts > MilliSecondsSinceUnixEpoch::now() { - valid_key_map.insert( - server, - keys.verify_keys - .into_iter() - .map(|(id, key)| (id, key.key)) - .collect(), - ); - } else { - expired_key_map.insert( - server, - keys.verify_keys - .into_iter() - .map(|(id, key)| (id, key.key)) - .collect(), - ); + 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, sender) { + 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, sender) { + 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, sender) + { + 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 + .alias + .local_aliases_for_room(&room_id) + .collect(); + match aliases { + Ok(aliases) => { + let plain_list: String = aliases + .iter() + .map(|alias| format!("- {}\n", alias)) + .collect(); - if ruma::signatures::verify_json(&valid_key_map, &value).is_ok() { - RoomMessageEventContent::text_plain("Signature correct") - } else if let Err(e) = - ruma::signatures::verify_json(&expired_key_map, &value) - { - RoomMessageEventContent::text_plain(format!( - "Signature verification failed: {e}" - )) - } else { - RoomMessageEventContent::text_plain( - "Signature correct (with expired keys)", - ) + 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.") } - } - AdminCommand::HashAndSignEvent { room_version_id } => { - if body.len() > 2 - // Language may be specified as part of the codeblock (e.g. "```json") - && 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) => { - if let Err(e) = ruma::signatures::hash_and_sign_event( - services().globals.server_name().as_str(), - services().globals.keypair(), - &mut value, - &room_version_id, - ) { - RoomMessageEventContent::text_plain(format!("Invalid event: {e}")) - } else { + 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().await; + 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}")) + } } - Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) } - } else { - RoomMessageEventContent::text_plain( - "Expected code block in command body. Add --help for details.", - ) } - } - AdminCommand::RemoveAlias { alias } => { - if alias.server_name() != services().globals.server_name() { - RoomMessageEventContent::text_plain( - "Cannot remove alias which is not from this server", - ) - } else if services() - .rooms - .alias - .resolve_local_alias(&alias)? - .is_none() - { - RoomMessageEventContent::text_plain("No such alias exists") - } else { - // We execute this as the server user for two reasons - // 1. If the user can execute commands in the admin room, they can always remove the alias. - // 2. In the future, we are likely going to be able to allow users to execute commands via - // other methods, such as IPC, which would lead to us not knowing their user id + 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 - .alias - .remove_alias(&alias, services().globals.server_user())?; - RoomMessageEventContent::text_plain("Alias removed sucessfully") + services() + .rooms + .event_handler + .fetch_required_signing_keys(&value, &pub_key_map) + .await?; + + let mut expired_key_map = BTreeMap::new(); + let mut valid_key_map = BTreeMap::new(); + + for (server, keys) in pub_key_map.into_inner().into_iter() { + if keys.valid_until_ts > MilliSecondsSinceUnixEpoch::now() { + valid_key_map.insert( + server, + keys.verify_keys + .into_iter() + .map(|(id, key)| (id, key.key)) + .collect(), + ); + } else { + expired_key_map.insert( + server, + keys.verify_keys + .into_iter() + .map(|(id, key)| (id, key.key)) + .collect(), + ); + } + } + + if ruma::signatures::verify_json(&valid_key_map, &value).is_ok() { + RoomMessageEventContent::text_plain("Signature correct") + } else if let Err(e) = + ruma::signatures::verify_json(&expired_key_map, &value) + { + RoomMessageEventContent::text_plain(format!( + "Signature verification failed: {e}" + )) + } else { + RoomMessageEventContent::text_plain( + "Signature correct (with expired keys)", + ) + } + } + 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().await; + 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).await; + + 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` @@ -983,7 +1308,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"); @@ -1456,6 +1781,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 dd514072..61164147 100644 --- a/src/service/rooms/alias/data.rs +++ b/src/service/rooms/alias/data.rs @@ -19,4 +19,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 95d52ad3..ca494abf 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -98,4 +98,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 b9c177bd..1b8e1fbb 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -506,7 +506,11 @@ impl Service { .state_cache .is_joined(server_user, &admin_room)? { - services().admin.process_message(body); + services().admin.process_message( + body, + pdu.event_id.clone(), + pdu.sender.clone(), + ); } } }