From 6fdeadc356028c7366608b7d5272f5b7fc7a5066 Mon Sep 17 00:00:00 2001 From: timedout Date: Fri, 26 Jun 2026 07:20:20 +0100 Subject: [PATCH] style: Refactor admin crate, drop `admin_command` macro `admin_command` depended on `implement`, which is now gone. --- src/admin/appservice/commands.rs | 122 +- src/admin/check/commands.rs | 2 +- src/admin/debug/commands.rs | 2001 +++++++++++++------------- src/admin/debug/tester.rs | 51 +- src/admin/federation/commands.rs | 228 +-- src/admin/media/commands.rs | 670 ++++----- src/admin/mod.rs | 2 +- src/admin/query/account_data.rs | 86 +- src/admin/query/raw.rs | 484 ++++--- src/admin/query/resolver.rs | 95 +- src/admin/query/room_timeline.rs | 60 +- src/admin/query/short.rs | 32 +- src/admin/query/users.rs | 514 ++++--- src/admin/room/commands.rs | 144 +- src/admin/room/info.rs | 108 +- src/admin/room/moderation.rs | 703 ++++----- src/admin/server/commands.rs | 407 +++--- src/admin/token/commands.rs | 132 +- src/admin/user/commands.rs | 2313 +++++++++++++++--------------- src/macros/admin.rs | 11 +- src/macros/mod.rs | 5 - src/macros/utils.rs | 4 +- 22 files changed, 4068 insertions(+), 4106 deletions(-) diff --git a/src/admin/appservice/commands.rs b/src/admin/appservice/commands.rs index 2c1bb7c10..6afd9f468 100644 --- a/src/admin/appservice/commands.rs +++ b/src/admin/appservice/commands.rs @@ -1,80 +1,76 @@ use conduwuit::{Err, Result, checked}; use futures::{FutureExt, StreamExt, TryFutureExt}; -use crate::admin_command; +impl crate::Context<'_> { + pub(super) async fn register(&self) -> Result { + let body = &self.body; + let body_len = self.body.len(); + if body_len < 2 + || !body[0].trim().starts_with("```") + || body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details."); + } -#[admin_command] -pub(super) async fn register(&self) -> Result { - let body = &self.body; - let body_len = self.body.len(); - if body_len < 2 - || !body[0].trim().starts_with("```") - || body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details."); + let range = 1..checked!(body_len - 1)?; + let appservice_config_body = body[range].join("\n"); + let parsed_config = serde_saphyr::from_str(&appservice_config_body); + match parsed_config { + | Err(e) => return Err!("Could not parse appservice config as YAML: {e}"), + | Ok(registration) => match self + .services + .appservice + .register_appservice(®istration, &appservice_config_body) + .await + .map(|()| registration.id) + { + | Err(e) => return Err!("Failed to register appservice: {e}"), + | Ok(id) => write!(self, "Appservice registered with ID: {id}"), + }, + } + .await } - let range = 1..checked!(body_len - 1)?; - let appservice_config_body = body[range].join("\n"); - let parsed_config = serde_saphyr::from_str(&appservice_config_body); - match parsed_config { - | Err(e) => return Err!("Could not parse appservice config as YAML: {e}"), - | Ok(registration) => match self + pub(super) async fn unregister(&self, appservice_identifier: String) -> Result { + match self .services .appservice - .register_appservice(®istration, &appservice_config_body) + .unregister_appservice(&appservice_identifier) .await - .map(|()| registration.id) { - | Err(e) => return Err!("Failed to register appservice: {e}"), - | Ok(id) => write!(self, "Appservice registered with ID: {id}"), - }, - } - .await -} - -#[admin_command] -pub(super) async fn unregister(&self, appservice_identifier: String) -> Result { - match self - .services - .appservice - .unregister_appservice(&appservice_identifier) + | Err(e) => return Err!("Failed to unregister appservice: {e}"), + | Ok(()) => write!(self, "Appservice unregistered."), + } .await - { - | Err(e) => return Err!("Failed to unregister appservice: {e}"), - | Ok(()) => write!(self, "Appservice unregistered."), } - .await -} -#[admin_command] -pub(super) async fn show_appservice_config(&self, appservice_identifier: String) -> Result { - match self - .services - .appservice - .get_registration(&appservice_identifier) + pub(super) async fn show_appservice_config(&self, appservice_identifier: String) -> Result { + match self + .services + .appservice + .get_registration(&appservice_identifier) + .await + { + | None => return Err!("Appservice does not exist."), + | Some(config) => { + let config_str = serde_saphyr::to_string(&config)?; + write!(self, "Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```") + }, + } .await - { - | None => return Err!("Appservice does not exist."), - | Some(config) => { - let config_str = serde_saphyr::to_string(&config)?; - write!(self, "Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```") - }, } - .await -} -#[admin_command] -pub(super) async fn list_registered(&self) -> Result { - self.services - .appservice - .iter_ids() - .collect() - .map(Ok) - .and_then(|appservices: Vec<_>| { - let len = appservices.len(); - let list = appservices.join(", "); - write!(self, "Appservices ({len}): {list}") - }) - .await + pub(super) async fn list_registered(&self) -> Result { + self.services + .appservice + .iter_ids() + .collect() + .map(Ok) + .and_then(|appservices: Vec<_>| { + let len = appservices.len(); + let list = appservices.join(", "); + write!(self, "Appservices ({len}): {list}") + }) + .await + } } diff --git a/src/admin/check/commands.rs b/src/admin/check/commands.rs index 96dd9e4b3..14ef4f78f 100644 --- a/src/admin/check/commands.rs +++ b/src/admin/check/commands.rs @@ -3,7 +3,7 @@ use futures::StreamExt; use crate::Context; -impl Context { +impl Context<'_> { pub(super) async fn check_all_users(&self) -> Result { let timer = tokio::time::Instant::now(); let users = self.services.users.stream().collect::>().await; diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs index 1aa9cb062..1fdc9509a 100644 --- a/src/admin/debug/commands.rs +++ b/src/admin/debug/commands.rs @@ -31,44 +31,6 @@ use service::rooms::{ }; use tracing_subscriber::EnvFilter; -use crate::admin_command; - -#[admin_command] -pub(super) async fn echo(&self, message: Vec) -> Result { - let message = message.join(" "); - self.write_str(&message).await -} - -#[admin_command] -pub(super) async fn get_auth_chain(&self, event_id: OwnedEventId) -> Result { - let Ok(event) = self.services.rooms.timeline.get_pdu_json(&event_id).await else { - return Err!("Event not found."); - }; - - let room_id_str = event - .get("room_id") - .and_then(CanonicalJsonValue::as_str) - .ok_or_else(|| err!(Database("Invalid event in database")))?; - - let room_id = <&RoomId>::try_from(room_id_str) - .map_err(|_| err!(Database("Invalid room id field in event in database")))?; - - let start = Instant::now(); - let count = self - .services - .rooms - .auth_chain - .event_ids_iter(room_id, once(event_id.as_ref())) - .ready_filter_map(Result::ok) - .count() - .await; - - let elapsed = start.elapsed(); - let out = format!("Loaded auth chain with length {count} in {elapsed:?}"); - - self.write_str(&out).await -} - #[derive(Clone, Copy, Eq, PartialEq)] enum NodeStatus { Normal(bool), @@ -115,1064 +77,1093 @@ fn render_node( Ok(()) } -#[admin_command] -pub(super) async fn show_auth_chain(&self, event_id: OwnedEventId) -> Result { - let node_status = async |event_id: &EventId, missing: bool| -> NodeStatus { - if self +impl crate::Context<'_> { + pub(super) async fn echo(&self, message: Vec) -> Result { + let message = message.join(" "); + self.write_str(&message).await + } + + pub(super) async fn get_auth_chain(&self, event_id: OwnedEventId) -> Result { + let Ok(event) = self.services.rooms.timeline.get_pdu_json(&event_id).await else { + return Err!("Event not found."); + }; + + let room_id_str = event + .get("room_id") + .and_then(CanonicalJsonValue::as_str) + .ok_or_else(|| err!(Database("Invalid event in database")))?; + + let room_id = <&RoomId>::try_from(room_id_str) + .map_err(|_| err!(Database("Invalid room id field in event in database")))?; + + let start = Instant::now(); + let count = self .services .rooms - .pdu_metadata - .is_event_rejected(event_id) - .await - { - NodeStatus::Rejected(missing) - } else if self - .services - .rooms - .pdu_metadata - .is_event_soft_failed(event_id) - .await - { - NodeStatus::SoftFailed(missing) - } else { - NodeStatus::Normal(missing) - } - }; + .auth_chain + .event_ids_iter(room_id, once(event_id.as_ref())) + .ready_filter_map(Result::ok) + .count() + .await; - let Ok(root) = self.services.rooms.timeline.get_pdu(&event_id).await else { - return Err!("Event not found."); - }; + let elapsed = start.elapsed(); + let out = format!("Loaded auth chain with length {count} in {elapsed:?}"); - let mut graph = String::from( - "```mermaid\n%% This is a mermaid graph. You can plug this output into\n\ + self.write_str(&out).await + } + + pub(super) async fn show_auth_chain(&self, event_id: OwnedEventId) -> Result { + let node_status = async |event_id: &EventId, missing: bool| -> NodeStatus { + if self + .services + .rooms + .pdu_metadata + .is_event_rejected(event_id) + .await + { + NodeStatus::Rejected(missing) + } else if self + .services + .rooms + .pdu_metadata + .is_event_soft_failed(event_id) + .await + { + NodeStatus::SoftFailed(missing) + } else { + NodeStatus::Normal(missing) + } + }; + + let Ok(root) = self.services.rooms.timeline.get_pdu(&event_id).await else { + return Err!("Event not found."); + }; + + let mut graph = String::from( + "```mermaid\n%% This is a mermaid graph. You can plug this output into\n\ %% https://mermaid.live/edit to visualise it on-the-fly.\nflowchart TD\n\ classDef rejected fill:#ffe5e5,stroke:#cc0000,stroke-width:2px,color:#000;\n\ classDef soft_failed fill:#fff6cc,stroke:#c9a400,stroke-width:2px,color:#000;\n" - ); + ); - let mut node_ids: HashMap = HashMap::new(); - let mut cached_events: HashMap = - HashMap::from([(event_id.clone(), root.clone())]); - let mut scheduled: HashSet = HashSet::from([event_id.clone()]); - let mut visited: HashSet = HashSet::new(); - let mut stack = vec![root]; - let mut next_node_id = 0_usize; + let mut node_ids: HashMap = HashMap::new(); + let mut cached_events: HashMap = + HashMap::from([(event_id.clone(), root.clone())]); + let mut scheduled: HashSet = HashSet::from([event_id.clone()]); + let mut visited: HashSet = HashSet::new(); + let mut stack = vec![root]; + let mut next_node_id = 0_usize; - let node_id_for = |event_id: &OwnedEventId, - node_ids: &mut HashMap, - next_node_id: &mut usize| { - node_ids - .entry(event_id.clone()) - .or_insert_with(|| { - let id = format!("n{}", *next_node_id); - *next_node_id = next_node_id.saturating_add(1); - id - }) - .clone() - }; - let node_name = |e: &PduEvent| { - if let Some(state_key) = e.state_key() { - format!("{},'{}'", e.event_type(), state_key) - } else { - format!("{}", e.event_type()) - } - }; - - while let Some(event) = stack.pop() { - let current_event_id = event.event_id().to_owned(); - if !visited.insert(current_event_id.clone()) { - continue; - } - - let current_node_id = node_id_for(¤t_event_id, &mut node_ids, &mut next_node_id); - let current_status = node_status(¤t_event_id, false).await; - - render_node( - &mut graph, - ¤t_node_id, - ¤t_event_id, - &node_name(&event), - current_status, - )?; - - let mut children = Vec::with_capacity(event.auth_events.len()); - for auth_event_id in event.auth_events().rev() { - let auth_event_id = auth_event_id.to_owned(); - let auth_node_id = node_id_for(&auth_event_id, &mut node_ids, &mut next_node_id); - writeln!(graph, "{current_node_id} --> {auth_node_id}")?; - - let first_seen = scheduled.insert(auth_event_id.clone()); - let auth_pdu = if let Some(auth_pdu) = cached_events.get(&auth_event_id) { - // NOTE: events might be referenced multiple times (like the create event) - // so this saves some cheeky db lookup time - Some(auth_pdu.clone()) - } else if first_seen { - match self.services.rooms.timeline.get_pdu(&auth_event_id).await { - | Ok(auth_event) => { - cached_events.insert(auth_event_id.clone(), auth_event.clone()); - Some(auth_event) - }, - | Err(_) => None, - } + let node_id_for = |event_id: &OwnedEventId, + node_ids: &mut HashMap, + next_node_id: &mut usize| { + node_ids + .entry(event_id.clone()) + .or_insert_with(|| { + let id = format!("n{}", *next_node_id); + *next_node_id = next_node_id.saturating_add(1); + id + }) + .clone() + }; + let node_name = |e: &PduEvent| { + if let Some(state_key) = e.state_key() { + format!("{},'{}'", e.event_type(), state_key) } else { - None - }; + format!("{}", e.event_type()) + } + }; - // NOTE: Depth is used as the primary sorting key here, even though it has no - // bearing on state resolution or anything. Timestamp is used as a - // tiebreaker, failing back to lexicographical comparison. - let (depth, ts) = auth_pdu - .as_ref() - .map_or((UInt::MAX, UInt::MAX), |pdu| (pdu.depth, pdu.origin_server_ts)); - - children.push(AuthChild { - node_id: auth_node_id, - event_id: auth_event_id, - depth, - ts, - first_seen, - pdu: auth_pdu, - }); - } - - children.sort_by(|a, b| { - a.depth - .cmp(&b.depth) - .then(a.ts.cmp(&b.ts)) - .then(a.event_id.as_str().cmp(b.event_id.as_str())) - }); - - for child in children.into_iter().rev() { - if !child.first_seen { + while let Some(event) = stack.pop() { + let current_event_id = event.event_id().to_owned(); + if !visited.insert(current_event_id.clone()) { continue; } - if let Some(child_pdu) = child.pdu { - // We have this PDU so will want to traverse it. - stack.push(child_pdu); - } else { - // We don't have this PDU locally so we can't traverse its auth events, - // but we can still render it as a node. - render_node( - &mut graph, - &child.node_id, - &child.event_id, - "", - node_status(&child.event_id, true).await, - )?; - } - } - } + let current_node_id = + node_id_for(¤t_event_id, &mut node_ids, &mut next_node_id); + let current_status = node_status(¤t_event_id, false).await; - graph.push_str("```\n"); - self.write_str(&graph).await -} + render_node( + &mut graph, + ¤t_node_id, + ¤t_event_id, + &node_name(&event), + current_status, + )?; -#[admin_command] -pub(super) async fn parse_pdu(&self) -> Result { - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&EMPTY).trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details."); - } + let mut children = Vec::with_capacity(event.auth_events.len()); + for auth_event_id in event.auth_events().rev() { + let auth_event_id = auth_event_id.to_owned(); + let auth_node_id = node_id_for(&auth_event_id, &mut node_ids, &mut next_node_id); + writeln!(graph, "{current_node_id} --> {auth_node_id}")?; - let string = self.body[1..self.body.len().saturating_sub(1)].join("\n"); - let room_version_rules = RoomVersionId::V12.rules().unwrap(); - - match serde_json::from_str(&string) { - | Err(e) => return Err!("Invalid json in command body: {e}"), - | Ok(value) => match ruma::signatures::reference_hash(&value, &room_version_rules) { - | Err(e) => return Err!("Could not parse PDU JSON: {e:?}"), - | Ok(hash) => { - let event_id = EventId::parse(format!("${hash}")); - match serde_json::from_value::(serde_json::to_value(value)?) { - | Err(e) => return Err!("EventId: {event_id:?}\nCould not parse event: {e}"), - | Ok(pdu) => write!(self, "EventId: {event_id:?}\n{pdu:#?}"), - } - }, - }, - } - .await -} - -#[admin_command] -pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result { - let mut outlier = false; - let mut pdu_json = self - .services - .rooms - .timeline - .get_non_outlier_pdu_json(&event_id) - .await; - - if pdu_json.is_err() { - outlier = true; - pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id).await; - } - let rejected = self - .services - .rooms - .pdu_metadata - .is_event_rejected(&event_id) - .await; - let soft_failed = self - .services - .rooms - .pdu_metadata - .is_event_soft_failed(&event_id) - .await; - - match pdu_json { - | Err(_) => return Err!("PDU not found locally."), - | Ok(json) => { - let text = serde_json::to_string_pretty(&json)?; - let msg = if rejected { - "Rejected PDU:" - } else if soft_failed { - "Soft-failed PDU:" - } else if outlier { - "Outlier PDU:" - } else { - "PDU:" - }; - write!(self, "{msg}\n```json\n{text}\n```") - }, - } - .await -} - -#[admin_command] -pub(super) async fn get_short_pdu( - &self, - shortroomid: ShortRoomId, - shorteventid: ShortEventId, -) -> Result { - let pdu_id: RawPduId = PduId { - shortroomid, - shorteventid: shorteventid.into(), - } - .into(); - - let pdu_json = self - .services - .rooms - .timeline - .get_pdu_json_from_id(&pdu_id) - .await; - - match pdu_json { - | Err(_) => return Err!("PDU not found locally."), - | Ok(json) => { - let json_text = serde_json::to_string_pretty(&json)?; - write!(self, "```json\n{json_text}\n```") - }, - } - .await -} - -#[admin_command] -pub(super) async fn get_remote_pdu_list(&self, server: OwnedServerName, force: bool) -> Result { - if !self.services.server.config.allow_federation { - return Err!("Federation is disabled on this homeserver.",); - } - - if server == self.services.globals.server_name() { - return Err!( - "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ - fetching local PDUs from the database.", - ); - } - - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&EMPTY).trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details.",); - } - - let list = self - .body - .iter() - .collect::>() - .drain(1..self.body.len().saturating_sub(1)) - .filter_map(|pdu| EventId::parse(pdu).ok()) - .collect::>(); - - let mut failed_count: usize = 0; - let mut success_count: usize = 0; - - for event_id in list { - if force { - match self.get_remote_pdu(event_id.clone(), server.clone()).await { - | Err(e) => { - failed_count = failed_count.saturating_add(1); - self.services - .admin - .send_text(&format!("Failed to get remote PDU, ignoring error: {e}")) - .await; - - warn!("Failed to get remote PDU, ignoring error: {e}"); - }, - | _ => { - success_count = success_count.saturating_add(1); - }, - } - } else { - self.get_remote_pdu(event_id.clone(), server.clone()) - .await?; - success_count = success_count.saturating_add(1); - } - } - - let out = - format!("Fetched {success_count} remote PDUs successfully with {failed_count} failures"); - - self.write_str(&out).await -} - -#[admin_command] -pub(super) async fn get_remote_pdu( - &self, - event_id: OwnedEventId, - server: OwnedServerName, -) -> Result { - if !self.services.server.config.allow_federation { - return Err!("Federation is disabled on this homeserver."); - } - - if server == self.services.globals.server_name() { - return Err!( - "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ - fetching local PDUs.", - ); - } - - match self - .services - .sending - .send_federation_request( - &server, - ruma::api::federation::event::get_event::v1::Request::new(event_id.clone()), - ) - .await - { - | Err(e) => { - return Err!( - "Remote server did not have PDU or failed sending request to remote server: {e}" - ); - }, - | Ok(response) => { - let json: CanonicalJsonObject = - serde_json::from_str(response.pdu.get()).map_err(|e| { - warn!( - "Requested event ID {event_id} from server but failed to convert from \ - RawValue to CanonicalJsonObject (malformed event/response?): {e}" - ); - err!(Request(Unknown( - "Received response from server but failed to parse PDU" - ))) - })?; - - trace!("Attempting to parse PDU: {:?}", &response.pdu); - let _parsed_pdu = { - let parsed_result = self - .services - .rooms - .event_handler - .parse_incoming_pdu(&response.pdu) - .boxed() - .await; - - let (event_id, value, room_id) = match parsed_result { - | Ok(t) => t, - | Err(e) => { - warn!("Failed to parse PDU: {e}"); - info!("Full PDU: {:?}", &response.pdu); - return Err!("Failed to parse PDU remote server {server} sent us: {e}"); - }, + let first_seen = scheduled.insert(auth_event_id.clone()); + let auth_pdu = if let Some(auth_pdu) = cached_events.get(&auth_event_id) { + // NOTE: events might be referenced multiple times (like the create event) + // so this saves some cheeky db lookup time + Some(auth_pdu.clone()) + } else if first_seen { + match self.services.rooms.timeline.get_pdu(&auth_event_id).await { + | Ok(auth_event) => { + cached_events.insert(auth_event_id.clone(), auth_event.clone()); + Some(auth_event) + }, + | Err(_) => None, + } + } else { + None }; - vec![(event_id, value, room_id)] - }; + // NOTE: Depth is used as the primary sorting key here, even though it has no + // bearing on state resolution or anything. Timestamp is used as a + // tiebreaker, failing back to lexicographical comparison. + let (depth, ts) = auth_pdu + .as_ref() + .map_or((UInt::MAX, UInt::MAX), |pdu| (pdu.depth, pdu.origin_server_ts)); - let text = serde_json::to_string_pretty(&json)?; - let msg = "Got PDU from specified server:"; - write!(self, "{msg}. Event body:\n```json\n{text}\n```") - }, - } - .await -} + children.push(AuthChild { + node_id: auth_node_id, + event_id: auth_event_id, + depth, + ts, + first_seen, + pdu: auth_pdu, + }); + } -#[admin_command] -pub(super) async fn get_state_at(&self, event_id: OwnedEventId) -> Result { - self.bail_restricted()?; + children.sort_by(|a, b| { + a.depth + .cmp(&b.depth) + .then(a.ts.cmp(&b.ts)) + .then(a.event_id.as_str().cmp(b.event_id.as_str())) + }); - let shortstatehash = self - .services - .rooms - .state_accessor - .pdu_shortstatehash(&event_id) - .await?; + for child in children.into_iter().rev() { + if !child.first_seen { + continue; + } - let state_ids: Vec = self - .services - .rooms - .state_accessor - .state_full_ids(shortstatehash) - .map(at!(1)) - .collect() - .await; + if let Some(child_pdu) = child.pdu { + // We have this PDU so will want to traverse it. + stack.push(child_pdu); + } else { + // We don't have this PDU locally so we can't traverse its auth events, + // but we can still render it as a node. + render_node( + &mut graph, + &child.node_id, + &child.event_id, + "", + node_status(&child.event_id, true).await, + )?; + } + } + } - let pdus: Vec = state_ids - .iter() - .try_stream() - .and_then(|id| self.services.rooms.timeline.get_pdu_json(id)) - .try_collect() - .await?; - - let json = serde_json::to_string_pretty(&pdus).map_err(|e| { - err!(Database( - "Failed to convert room state events to pretty JSON, possible invalid room state \ - events in our database {e}", - )) - })?; - - let out = format!("```json\n{json}\n```"); - self.write_str(&out).await -} - -#[admin_command] -pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result { - self.bail_restricted()?; - - let room_id = self.services.rooms.alias.resolve(&room).await?; - let room_state: Vec> = self - .services - .rooms - .state_accessor - .room_state_full_pdus(&room_id) - .map_ok(Event::into_format) - .try_collect() - .await?; - - if room_state.is_empty() { - return Err!("Unable to find room state in our database (vector is empty)",); + graph.push_str("```\n"); + self.write_str(&graph).await } - let json = serde_json::to_string_pretty(&room_state).map_err(|e| { - err!(Database( - "Failed to convert room state events to pretty JSON, possible invalid room state \ - events in our database {e}", - )) - })?; + pub(super) async fn parse_pdu(&self) -> Result { + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&EMPTY).trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details."); + } - let out = format!("```json\n{json}\n```"); - self.write_str(&out).await -} + let string = self.body[1..self.body.len().saturating_sub(1)].join("\n"); + let room_version_rules = RoomVersionId::V12.rules().unwrap(); -#[admin_command] -pub(super) async fn ping(&self, server: OwnedServerName) -> Result { - if server == self.services.globals.server_name() { - return Err!("Not allowed to send federation requests to ourselves."); - } - - let timer = tokio::time::Instant::now(); - - match self - .services - .sending - .send_unauthenticated_request( - &server, - ruma::api::federation::discovery::get_server_version::v1::Request::new(), - ) + match serde_json::from_str(&string) { + | Err(e) => return Err!("Invalid json in command body: {e}"), + | Ok(value) => match ruma::signatures::reference_hash(&value, &room_version_rules) { + | Err(e) => return Err!("Could not parse PDU JSON: {e:?}"), + | Ok(hash) => { + let event_id = EventId::parse(format!("${hash}")); + match serde_json::from_value::(serde_json::to_value(value)?) { + | Err(e) => + return Err!("EventId: {event_id:?}\nCould not parse event: {e}"), + | Ok(pdu) => write!(self, "EventId: {event_id:?}\n{pdu:#?}"), + } + }, + }, + } .await - { - | Err(e) => { - return Err!("Failed sending federation request to specified server:\n\n{e}"); - }, - | Ok(response) => { - let ping_time = timer.elapsed(); - let json_text_res = serde_json::to_string_pretty(&response.server); + } - let out = if let Ok(json) = json_text_res { - format!("Got response which took {ping_time:?} time:\n```json\n{json}\n```") + pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result { + let mut outlier = false; + let mut pdu_json = self + .services + .rooms + .timeline + .get_non_outlier_pdu_json(&event_id) + .await; + + if pdu_json.is_err() { + outlier = true; + pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id).await; + } + let rejected = self + .services + .rooms + .pdu_metadata + .is_event_rejected(&event_id) + .await; + let soft_failed = self + .services + .rooms + .pdu_metadata + .is_event_soft_failed(&event_id) + .await; + + match pdu_json { + | Err(_) => return Err!("PDU not found locally."), + | Ok(json) => { + let text = serde_json::to_string_pretty(&json)?; + let msg = if rejected { + "Rejected PDU:" + } else if soft_failed { + "Soft-failed PDU:" + } else if outlier { + "Outlier PDU:" + } else { + "PDU:" + }; + write!(self, "{msg}\n```json\n{text}\n```") + }, + } + .await + } + + pub(super) async fn get_short_pdu( + &self, + shortroomid: ShortRoomId, + shorteventid: ShortEventId, + ) -> Result { + let pdu_id: RawPduId = PduId { + shortroomid, + shorteventid: shorteventid.into(), + } + .into(); + + let pdu_json = self + .services + .rooms + .timeline + .get_pdu_json_from_id(&pdu_id) + .await; + + match pdu_json { + | Err(_) => return Err!("PDU not found locally."), + | Ok(json) => { + let json_text = serde_json::to_string_pretty(&json)?; + write!(self, "```json\n{json_text}\n```") + }, + } + .await + } + + pub(super) async fn get_remote_pdu_list( + &self, + server: OwnedServerName, + force: bool, + ) -> Result { + if !self.services.server.config.allow_federation { + return Err!("Federation is disabled on this homeserver.",); + } + + if server == self.services.globals.server_name() { + return Err!( + "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ + fetching local PDUs from the database.", + ); + } + + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&EMPTY).trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details.",); + } + + let list = self + .body + .iter() + .collect::>() + .drain(1..self.body.len().saturating_sub(1)) + .filter_map(|pdu| EventId::parse(pdu).ok()) + .collect::>(); + + let mut failed_count: usize = 0; + let mut success_count: usize = 0; + + for event_id in list { + if force { + match self.get_remote_pdu(event_id.clone(), server.clone()).await { + | Err(e) => { + failed_count = failed_count.saturating_add(1); + self.services + .admin + .send_text(&format!("Failed to get remote PDU, ignoring error: {e}")) + .await; + + warn!("Failed to get remote PDU, ignoring error: {e}"); + }, + | _ => { + success_count = success_count.saturating_add(1); + }, + } } else { - format!("Got non-JSON response which took {ping_time:?} time:\n{response:?}") - }; + self.get_remote_pdu(event_id.clone(), server.clone()) + .await?; + success_count = success_count.saturating_add(1); + } + } - write!(self, "{out}") - }, + let out = format!( + "Fetched {success_count} remote PDUs successfully with {failed_count} failures" + ); + + self.write_str(&out).await } - .await -} -#[admin_command] -pub(super) async fn force_device_list_updates(&self) -> Result { - // Force E2EE device list updates for all users - self.services - .users - .stream() - .for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await) - .await; + pub(super) async fn get_remote_pdu( + &self, + event_id: OwnedEventId, + server: OwnedServerName, + ) -> Result { + if !self.services.server.config.allow_federation { + return Err!("Federation is disabled on this homeserver."); + } - write!(self, "Marked all devices for all users as having new keys to update").await -} - -#[admin_command] -pub(super) async fn change_log_level(&self, filter: Option, reset: bool) -> Result { - let handles = &["console"]; - - if reset { - let old_filter_layer = match EnvFilter::try_new(&self.services.server.config.log) { - | Ok(s) => s, - | Err(e) => return Err!("Log level from config appears to be invalid now: {e}"), - }; + if server == self.services.globals.server_name() { + return Err!( + "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ + fetching local PDUs.", + ); + } match self .services - .server - .log - .reload - .reload(&old_filter_layer, Some(handles)) - { - | Err(e) => { - return Err!("Failed to modify and reload the global tracing log level: {e}"); - }, - | Ok(()) => { - let value = &self.services.server.config.log; - let out = format!("Successfully changed log level back to config value {value}"); - return self.write_str(&out).await; - }, - } - } - - if let Some(filter) = filter { - let new_filter_layer = match EnvFilter::try_new(filter) { - | Ok(s) => s, - | Err(e) => return Err!("Invalid log level filter specified: {e}"), - }; - - match self - .services - .server - .log - .reload - .reload(&new_filter_layer, Some(handles)) - { - | Ok(()) => { - return self.write_str("Successfully changed log level").await; - }, - | Err(e) => { - return Err!("Failed to modify and reload the global tracing log level: {e}"); - }, - } - } - - Err!("No log level was specified.") -} - -#[admin_command] -pub(super) async fn verify_json(&self) -> Result { - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details."); - } - - let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n"); - let room_version_rules = RoomVersionId::V12.rules().unwrap(); - - match serde_json::from_str::(&string) { - | Err(e) => return Err!("Invalid json: {e}"), - | Ok(value) => match self - .services - .server_keys - .verify_json(&value, &room_version_rules) + .sending + .send_federation_request( + &server, + ruma::api::federation::event::get_event::v1::Request::new(event_id.clone()), + ) .await { - | Err(e) => return Err!("Signature verification failed: {e}"), - | Ok(()) => write!(self, "Signature correct"), - }, - } - .await -} + | Err(e) => { + return Err!( + "Remote server did not have PDU or failed sending request to remote server: \ + {e}" + ); + }, + | Ok(response) => { + let json: CanonicalJsonObject = serde_json::from_str(response.pdu.get()) + .map_err(|e| { + warn!( + "Requested event ID {event_id} from server but failed to convert \ + from RawValue to CanonicalJsonObject (malformed event/response?): \ + {e}" + ); + err!(Request(Unknown( + "Received response from server but failed to parse PDU" + ))) + })?; -#[admin_command] -pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result { - use ruma::signatures::Verified; + trace!("Attempting to parse PDU: {:?}", &response.pdu); + let _parsed_pdu = { + let parsed_result = self + .services + .rooms + .event_handler + .parse_incoming_pdu(&response.pdu) + .boxed() + .await; - let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?; - let room_version_rules = RoomVersionId::V12.rules().unwrap(); + let (event_id, value, room_id) = match parsed_result { + | Ok(t) => t, + | Err(e) => { + warn!("Failed to parse PDU: {e}"); + info!("Full PDU: {:?}", &response.pdu); + return Err!( + "Failed to parse PDU remote server {server} sent us: {e}" + ); + }, + }; - event.remove("event_id"); - let msg = match self - .services - .server_keys - .verify_event(&event, &room_version_rules) + vec![(event_id, value, room_id)] + }; + + let text = serde_json::to_string_pretty(&json)?; + let msg = "Got PDU from specified server:"; + write!(self, "{msg}. Event body:\n```json\n{text}\n```") + }, + } .await - { - | Err(e) => return Err(e), - | Ok(Verified::Signatures) => "signatures OK, but content hash failed (redaction).", - | Ok(Verified::All) => "signatures and hashes OK.", - }; - - self.write_str(msg).await -} - -#[admin_command] -#[tracing::instrument(skip(self), level = "info")] -pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result { - self.bail_restricted()?; - - if !self - .services - .rooms - .state_cache - .server_in_room(&self.services.server.name, &room_id) - .await - { - return Err!("We are not participating in the room / we don't know about the room ID.",); } - let first_pdu = self - .services - .rooms - .timeline - .first_pdu_in_room(&room_id) - .await - .map_err(|_| err!(Database("Failed to find the first PDU in database")))?; + pub(super) async fn get_state_at(&self, event_id: OwnedEventId) -> Result { + self.bail_restricted()?; - let out = format!("{first_pdu:?}"); - self.write_str(&out).await -} + let shortstatehash = self + .services + .rooms + .state_accessor + .pdu_shortstatehash(&event_id) + .await?; -#[admin_command] -#[tracing::instrument(skip(self), level = "info")] -pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result { - self.bail_restricted()?; + let state_ids: Vec = self + .services + .rooms + .state_accessor + .state_full_ids(shortstatehash) + .map(at!(1)) + .collect() + .await; - if !self - .services - .rooms - .state_cache - .server_in_room(&self.services.server.name, &room_id) - .await - { - return Err!("We are not participating in the room / we don't know about the room ID."); + let pdus: Vec = state_ids + .iter() + .try_stream() + .and_then(|id| self.services.rooms.timeline.get_pdu_json(id)) + .try_collect() + .await?; + + let json = serde_json::to_string_pretty(&pdus).map_err(|e| { + err!(Database( + "Failed to convert room state events to pretty JSON, possible invalid room state \ + events in our database {e}", + )) + })?; + + let out = format!("```json\n{json}\n```"); + self.write_str(&out).await } - let latest_pdu = self - .services - .rooms - .timeline - .latest_pdu_in_room(&room_id) - .await - .map_err(|_| err!(Database("Failed to find the latest PDU in database")))?; + pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result { + self.bail_restricted()?; - let out = format!("{latest_pdu:?}"); - self.write_str(&out).await -} + let room_id = self.services.rooms.alias.resolve(&room).await?; + let room_state: Vec> = self + .services + .rooms + .state_accessor + .room_state_full_pdus(&room_id) + .map_ok(Event::into_format) + .try_collect() + .await?; -#[admin_command] -#[tracing::instrument(skip(self), level = "info")] -pub(super) async fn force_set_room_state_from_server( - &self, - room_id: OwnedRoomId, - server_name: OwnedServerName, - at_event: Option, -) -> Result { - self.bail_restricted()?; + if room_state.is_empty() { + return Err!("Unable to find room state in our database (vector is empty)",); + } - if !self - .services - .rooms - .state_cache - .server_in_room(&self.services.server.name, &room_id) - .await - { - return Err!("We are not participating in the room / we don't know about the room ID."); + let json = serde_json::to_string_pretty(&room_state).map_err(|e| { + err!(Database( + "Failed to convert room state events to pretty JSON, possible invalid room \ + state events in our database {e}", + )) + })?; + + let out = format!("```json\n{json}\n```"); + self.write_str(&out).await } - let at_event_id = match at_event { - | Some(event_id) => event_id, - | None => self + pub(super) async fn ping(&self, server: OwnedServerName) -> Result { + if server == self.services.globals.server_name() { + return Err!("Not allowed to send federation requests to ourselves."); + } + + let timer = tokio::time::Instant::now(); + + match self + .services + .sending + .send_unauthenticated_request( + &server, + ruma::api::federation::discovery::get_server_version::v1::Request::new(), + ) + .await + { + | Err(e) => { + return Err!("Failed sending federation request to specified server:\n\n{e}"); + }, + | Ok(response) => { + let ping_time = timer.elapsed(); + let json_text_res = serde_json::to_string_pretty(&response.server); + + let out = if let Ok(json) = json_text_res { + format!("Got response which took {ping_time:?} time:\n```json\n{json}\n```") + } else { + format!("Got non-JSON response which took {ping_time:?} time:\n{response:?}") + }; + + write!(self, "{out}") + }, + } + .await + } + + pub(super) async fn force_device_list_updates(&self) -> Result { + // Force E2EE device list updates for all users + self.services + .users + .stream() + .for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await) + .await; + + write!(self, "Marked all devices for all users as having new keys to update").await + } + + pub(super) async fn change_log_level(&self, filter: Option, reset: bool) -> Result { + let handles = &["console"]; + + if reset { + let old_filter_layer = match EnvFilter::try_new(&self.services.server.config.log) { + | Ok(s) => s, + | Err(e) => return Err!("Log level from config appears to be invalid now: {e}"), + }; + + match self + .services + .server + .log + .reload + .reload(&old_filter_layer, Some(handles)) + { + | Err(e) => { + return Err!("Failed to modify and reload the global tracing log level: {e}"); + }, + | Ok(()) => { + let value = &self.services.server.config.log; + let out = + format!("Successfully changed log level back to config value {value}"); + return self.write_str(&out).await; + }, + } + } + + if let Some(filter) = filter { + let new_filter_layer = match EnvFilter::try_new(filter) { + | Ok(s) => s, + | Err(e) => return Err!("Invalid log level filter specified: {e}"), + }; + + match self + .services + .server + .log + .reload + .reload(&new_filter_layer, Some(handles)) + { + | Ok(()) => { + return self.write_str("Successfully changed log level").await; + }, + | Err(e) => { + return Err!("Failed to modify and reload the global tracing log level: {e}"); + }, + } + } + + Err!("No log level was specified.") + } + + pub(super) async fn verify_json(&self) -> Result { + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details."); + } + + let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n"); + let room_version_rules = RoomVersionId::V12.rules().unwrap(); + + match serde_json::from_str::(&string) { + | Err(e) => return Err!("Invalid json: {e}"), + | Ok(value) => match self + .services + .server_keys + .verify_json(&value, &room_version_rules) + .await + { + | Err(e) => return Err!("Signature verification failed: {e}"), + | Ok(()) => write!(self, "Signature correct"), + }, + } + .await + } + + pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result { + use ruma::signatures::Verified; + + let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?; + let room_version_rules = RoomVersionId::V12.rules().unwrap(); + + event.remove("event_id"); + let msg = match self + .services + .server_keys + .verify_event(&event, &room_version_rules) + .await + { + | Err(e) => return Err(e), + | Ok(Verified::Signatures) => "signatures OK, but content hash failed (redaction).", + | Ok(Verified::All) => "signatures and hashes OK.", + }; + + self.write_str(msg).await + } + + #[tracing::instrument(skip(self), level = "info")] + pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result { + self.bail_restricted()?; + + if !self + .services + .rooms + .state_cache + .server_in_room(&self.services.server.name, &room_id) + .await + { + return Err!( + "We are not participating in the room / we don't know about the room ID.", + ); + } + + let first_pdu = self + .services + .rooms + .timeline + .first_pdu_in_room(&room_id) + .await + .map_err(|_| err!(Database("Failed to find the first PDU in database")))?; + + let out = format!("{first_pdu:?}"); + self.write_str(&out).await + } + + #[tracing::instrument(skip(self), level = "info")] + pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result { + self.bail_restricted()?; + + if !self + .services + .rooms + .state_cache + .server_in_room(&self.services.server.name, &room_id) + .await + { + return Err!( + "We are not participating in the room / we don't know about the room ID." + ); + } + + let latest_pdu = self .services .rooms .timeline .latest_pdu_in_room(&room_id) .await - .map_err(|_| err!(Database("Failed to find the latest PDU in database")))? - .event_id() - .to_owned(), - }; + .map_err(|_| err!(Database("Failed to find the latest PDU in database")))?; - let room_version = self.services.rooms.state.get_room_version(&room_id).await?; - let room_version_rules = room_version.rules().unwrap(); - - let mut state: HashMap = HashMap::new(); - - let remote_state_response = self - .services - .sending - .send_federation_request( - &server_name, - get_room_state::v1::Request::new(at_event_id, room_id.clone()), - ) - .await?; - - for pdu in remote_state_response.pdus.clone() { - match self - .services - .rooms - .event_handler - .parse_incoming_pdu(&pdu) - .await - { - | Ok(t) => t, - | Err(e) => { - warn!("Could not parse PDU, ignoring: {e}"); - continue; - }, - }; + let out = format!("{latest_pdu:?}"); + self.write_str(&out).await } - info!("Going through room_state response PDUs"); - for result in remote_state_response.pdus.iter().map(|pdu| { - self.services - .server_keys - .validate_and_add_event_id(pdu, &room_version_rules) - }) { - let Ok((event_id, value)) = result.await else { - continue; - }; + #[tracing::instrument(skip(self), level = "info")] + pub(super) async fn force_set_room_state_from_server( + &self, + room_id: OwnedRoomId, + server_name: OwnedServerName, + at_event: Option, + ) -> Result { + self.bail_restricted()?; - let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| { - debug_error!("Invalid PDU in fetching remote room state PDUs response: {value:#?}"); - err!(BadServerResponse(debug_error!("Invalid PDU in send_join response: {e:?}"))) - })?; - - self.services + if !self + .services .rooms - .outlier - .add_pdu_outlier(&event_id, &value); + .state_cache + .server_in_room(&self.services.server.name, &room_id) + .await + { + return Err!( + "We are not participating in the room / we don't know about the room ID." + ); + } - if let Some(state_key) = &pdu.state_key { - let shortstatekey = self + let at_event_id = match at_event { + | Some(event_id) => event_id, + | None => self .services .rooms - .short - .get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key) - .await; + .timeline + .latest_pdu_in_room(&room_id) + .await + .map_err(|_| err!(Database("Failed to find the latest PDU in database")))? + .event_id() + .to_owned(), + }; - state.insert(shortstatekey, pdu.event_id.clone()); + let room_version = self.services.rooms.state.get_room_version(&room_id).await?; + let room_version_rules = room_version.rules().unwrap(); + + let mut state: HashMap = HashMap::new(); + + let remote_state_response = self + .services + .sending + .send_federation_request( + &server_name, + get_room_state::v1::Request::new(at_event_id, room_id.clone()), + ) + .await?; + + for pdu in remote_state_response.pdus.clone() { + match self + .services + .rooms + .event_handler + .parse_incoming_pdu(&pdu) + .await + { + | Ok(t) => t, + | Err(e) => { + warn!("Could not parse PDU, ignoring: {e}"); + continue; + }, + }; + } + + info!("Going through room_state response PDUs"); + for result in remote_state_response.pdus.iter().map(|pdu| { + self.services + .server_keys + .validate_and_add_event_id(pdu, &room_version_rules) + }) { + let Ok((event_id, value)) = result.await else { + continue; + }; + + let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| { + debug_error!( + "Invalid PDU in fetching remote room state PDUs response: {value:#?}" + ); + err!(BadServerResponse(debug_error!("Invalid PDU in send_join response: {e:?}"))) + })?; + + self.services + .rooms + .outlier + .add_pdu_outlier(&event_id, &value); + + if let Some(state_key) = &pdu.state_key { + let shortstatekey = self + .services + .rooms + .short + .get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key) + .await; + + state.insert(shortstatekey, pdu.event_id.clone()); + self.services + .rooms + .pdu_metadata + .clear_pdu_markers(pdu.event_id()); + } + } + + info!("Going through auth_chain response"); + for result in remote_state_response.auth_chain.iter().map(|pdu| { + self.services + .server_keys + .validate_and_add_event_id(pdu, &room_version_rules) + }) { + let Ok((event_id, value)) = result.await else { + continue; + }; + + self.services + .rooms + .outlier + .add_pdu_outlier(&event_id, &value); self.services .rooms .pdu_metadata - .clear_pdu_markers(pdu.event_id()); + .clear_pdu_markers(&event_id); } - } - info!("Going through auth_chain response"); - for result in remote_state_response.auth_chain.iter().map(|pdu| { - self.services - .server_keys - .validate_and_add_event_id(pdu, &room_version_rules) - }) { - let Ok((event_id, value)) = result.await else { - continue; - }; - - self.services - .rooms - .outlier - .add_pdu_outlier(&event_id, &value); - self.services - .rooms - .pdu_metadata - .clear_pdu_markers(&event_id); - } - - info!("Resolving new room state"); - let new_room_state = self - .services - .rooms - .event_handler - .resolve_state(&room_id, &room_version_rules, state) - .await?; - - info!("Compressing new room state"); - let HashSetCompressStateEvent { - shortstatehash: short_state_hash, - added, - removed, - } = self - .services - .rooms - .state_compressor - .save_state(room_id.clone().as_ref(), new_room_state) - .await?; - - let state_lock = self.services.rooms.state.mutex.lock(&*room_id).await; - - info!("Forcing new room state"); - self.services - .rooms - .state - .force_state(room_id.clone().as_ref(), short_state_hash, added, removed, &state_lock) - .await?; - - info!("Updating joined counts for room"); - self.services - .rooms - .state_cache - .update_joined_count(&room_id) - .await; - - self.write_str("Successfully forced the room state from the requested remote server.") - .await -} - -#[admin_command] -pub(super) async fn get_signing_keys( - &self, - server_name: Option, - notary: Option, - query: bool, -) -> Result { - let server_name = server_name.unwrap_or_else(|| self.services.server.name.clone()); - - if let Some(notary) = notary { - let signing_keys = self + info!("Resolving new room state"); + let new_room_state = self .services - .server_keys - .notary_request(¬ary, &server_name) + .rooms + .event_handler + .resolve_state(&room_id, &room_version_rules, state) .await?; + info!("Compressing new room state"); + let HashSetCompressStateEvent { + shortstatehash: short_state_hash, + added, + removed, + } = self + .services + .rooms + .state_compressor + .save_state(room_id.clone().as_ref(), new_room_state) + .await?; + + let state_lock = self.services.rooms.state.mutex.lock(&*room_id).await; + + info!("Forcing new room state"); + self.services + .rooms + .state + .force_state(room_id.clone().as_ref(), short_state_hash, added, removed, &state_lock) + .await?; + + info!("Updating joined counts for room"); + self.services + .rooms + .state_cache + .update_joined_count(&room_id) + .await; + + self.write_str("Successfully forced the room state from the requested remote server.") + .await + } + + pub(super) async fn get_signing_keys( + &self, + server_name: Option, + notary: Option, + query: bool, + ) -> Result { + let server_name = server_name.unwrap_or_else(|| self.services.server.name.clone()); + + if let Some(notary) = notary { + let signing_keys = self + .services + .server_keys + .notary_request(¬ary, &server_name) + .await?; + + let out = format!("```rs\n{signing_keys:#?}\n```"); + return self.write_str(&out).await; + } + + let signing_keys = if query { + self.services + .server_keys + .server_request(&server_name) + .await? + } else { + self.services + .server_keys + .signing_keys_for(&server_name) + .await? + }; + let out = format!("```rs\n{signing_keys:#?}\n```"); - return self.write_str(&out).await; + self.write_str(&out).await } - let signing_keys = if query { - self.services + pub(super) async fn get_verify_keys(&self, server_name: Option) -> Result { + let server_name = server_name.unwrap_or_else(|| self.services.server.name.clone()); + + let keys = self + .services .server_keys - .server_request(&server_name) - .await? - } else { - self.services - .server_keys - .signing_keys_for(&server_name) - .await? - }; + .verify_keys_for(&server_name) + .await; - let out = format!("```rs\n{signing_keys:#?}\n```"); - self.write_str(&out).await -} + let mut out = String::new(); + writeln!(out, "| Key ID | Public Key |")?; + writeln!(out, "| --- | --- |")?; + for (key_id, key) in keys { + writeln!(out, "| {key_id} | {key:?} |")?; + } -#[admin_command] -pub(super) async fn get_verify_keys(&self, server_name: Option) -> Result { - let server_name = server_name.unwrap_or_else(|| self.services.server.name.clone()); - - let keys = self - .services - .server_keys - .verify_keys_for(&server_name) - .await; - - let mut out = String::new(); - writeln!(out, "| Key ID | Public Key |")?; - writeln!(out, "| --- | --- |")?; - for (key_id, key) in keys { - writeln!(out, "| {key_id} | {key:?} |")?; + self.write_str(&out).await } - self.write_str(&out).await -} + pub(super) async fn resolve_true_destination( + &self, + server_name: OwnedServerName, + no_cache: bool, + ) -> Result { + if !self.services.server.config.allow_federation { + return Err!("Federation is disabled on this homeserver.",); + } -#[admin_command] -pub(super) async fn resolve_true_destination( - &self, - server_name: OwnedServerName, - no_cache: bool, -) -> Result { - if !self.services.server.config.allow_federation { - return Err!("Federation is disabled on this homeserver.",); + if server_name == self.services.server.name { + return Err!( + "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ + fetching local PDUs.", + ); + } + + let actual = self + .services + .resolver + .resolve_actual_dest(&server_name, !no_cache) + .await?; + + let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host); + self.write_str(&msg).await } - if server_name == self.services.server.name { - return Err!( - "Not allowed to send federation requests to ourselves. Please use `get-pdu` for \ - fetching local PDUs.", + pub(super) async fn memory_stats(&self, opts: Option) -> Result { + const OPTS: &str = "abcdefghijklmnopqrstuvwxyz"; + + let opts: String = OPTS + .chars() + .filter(|&c| { + let allow_any = opts.as_ref().is_some_and(|opts| opts == "*"); + + let allow = allow_any || opts.as_ref().is_some_and(|opts| opts.contains(c)); + + !allow + }) + .collect(); + + let stats = conduwuit::alloc::memory_stats(&opts).unwrap_or_default(); + + self.write_str("```\n").await?; + self.write_str(&stats).await?; + self.write_str("\n```").await?; + Ok(()) + } + + #[cfg(tokio_unstable)] + + pub(super) async fn runtime_metrics(&self) -> Result { + let out = self.services.server.metrics.runtime_metrics().map_or_else( + || "Runtime metrics are not available.".to_owned(), + |metrics| { + format!( + "```rs\nnum_workers: {}\nnum_alive_tasks: {}\nglobal_queue_depth: {}\n```", + metrics.num_workers(), + metrics.num_alive_tasks(), + metrics.global_queue_depth() + ) + }, ); + + self.write_str(&out).await } - let actual = self - .services - .resolver - .resolve_actual_dest(&server_name, !no_cache) - .await?; + #[cfg(not(tokio_unstable))] + pub(super) async fn runtime_metrics(&self) -> Result { + self.write_str("Runtime metrics require building with `tokio_unstable`.") + .await + } - let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host); - self.write_str(&msg).await -} - -#[admin_command] -pub(super) async fn memory_stats(&self, opts: Option) -> Result { - const OPTS: &str = "abcdefghijklmnopqrstuvwxyz"; - - let opts: String = OPTS - .chars() - .filter(|&c| { - let allow_any = opts.as_ref().is_some_and(|opts| opts == "*"); - - let allow = allow_any || opts.as_ref().is_some_and(|opts| opts.contains(c)); - - !allow - }) - .collect(); - - let stats = conduwuit::alloc::memory_stats(&opts).unwrap_or_default(); - - self.write_str("```\n").await?; - self.write_str(&stats).await?; - self.write_str("\n```").await?; - Ok(()) -} - -#[cfg(tokio_unstable)] -#[admin_command] -pub(super) async fn runtime_metrics(&self) -> Result { - let out = self.services.server.metrics.runtime_metrics().map_or_else( - || "Runtime metrics are not available.".to_owned(), - |metrics| { - format!( - "```rs\nnum_workers: {}\nnum_alive_tasks: {}\nglobal_queue_depth: {}\n```", - metrics.num_workers(), - metrics.num_alive_tasks(), - metrics.global_queue_depth() - ) - }, - ); - - self.write_str(&out).await -} - -#[cfg(not(tokio_unstable))] -#[admin_command] -pub(super) async fn runtime_metrics(&self) -> Result { - self.write_str("Runtime metrics require building with `tokio_unstable`.") - .await -} - -#[cfg(tokio_unstable)] -#[admin_command] -pub(super) async fn runtime_interval(&self) -> Result { - let out = self.services.server.metrics.runtime_interval().map_or_else( - || "Runtime metrics are not available.".to_owned(), - |metrics| format!("```rs\n{metrics:#?}\n```"), - ); - - self.write_str(&out).await -} - -#[cfg(not(tokio_unstable))] -#[admin_command] -pub(super) async fn runtime_interval(&self) -> Result { - self.write_str("Runtime metrics require building with `tokio_unstable`.") - .await -} - -#[admin_command] -pub(super) async fn time(&self) -> Result { - let now = SystemTime::now(); - let now = utils::time::format(now, "%+"); - - self.write_str(&now).await -} - -#[admin_command] -pub(super) async fn database_stats( - &self, - property: Option, - map: Option, -) -> Result { - let map_name = map.as_ref().map_or(EMPTY, String::as_str); - let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned()); - self.services - .db - .iter() - .filter(|&(&name, _)| map_name.is_empty() || map_name == name) - .try_stream() - .try_for_each(|(&name, map)| { - let res = map.property(&property).expect("invalid property"); - writeln!(self, "##### {name}:\n```\n{}\n```", res.trim()) - }) - .await -} - -#[admin_command] -pub(super) async fn database_files(&self, map: Option, level: Option) -> Result { - let mut files: Vec<_> = self.services.db.db.file_list().collect::>()?; - - files.sort_by_key(|f| f.name.clone()); - - writeln!(self, "| lev | sst | keys | dels | size | column |").await?; - writeln!(self, "| ---: | :--- | ---: | ---: | ---: | :--- |").await?; - files - .into_iter() - .filter(|file| { - map.as_deref() - .is_none_or(|map| map == file.column_family_name) - }) - .filter(|file| level.as_ref().is_none_or(|&level| level == file.level)) - .try_stream() - .try_for_each(|file| { - writeln!( - self, - "| {} | {:<13} | {:7}+ | {:4}- | {:9} | {} |", - file.level, - file.name, - file.num_entries, - file.num_deletions, - file.size, - file.column_family_name, - ) - }) - .await -} - -#[admin_command] -pub(super) async fn trim_memory(&self) -> Result { - conduwuit::alloc::trim(None)?; - - writeln!(self, "done").await -} - -#[admin_command] -pub(super) async fn send_test_email(&self) -> Result { - self.bail_restricted()?; - - let mailer = self.services.mailer.expect_mailer()?; - let Some(sender) = self.sender else { - return Err!("No sender user provided in context"); - }; - - let Some(email) = self - .services - .threepid - .get_email_for_localpart(sender.localpart()) - .await - else { - return Err!("{} has no associated email address", sender); - }; - - mailer - .send(Mailbox::new(None, email.clone()), service::mailer::messages::Test) - .await?; - - self.write_str(&format!("Test email successfully sent to {email}")) - .await?; - - Ok(()) + #[cfg(tokio_unstable)] + + pub(super) async fn runtime_interval(&self) -> Result { + let out = self.services.server.metrics.runtime_interval().map_or_else( + || "Runtime metrics are not available.".to_owned(), + |metrics| format!("```rs\n{metrics:#?}\n```"), + ); + + self.write_str(&out).await + } + + #[cfg(not(tokio_unstable))] + pub(super) async fn runtime_interval(&self) -> Result { + self.write_str("Runtime metrics require building with `tokio_unstable`.") + .await + } + + pub(super) async fn time(&self) -> Result { + let now = SystemTime::now(); + let now = utils::time::format(now, "%+"); + + self.write_str(&now).await + } + + pub(super) async fn database_stats( + &self, + property: Option, + map: Option, + ) -> Result { + let map_name = map.as_ref().map_or(EMPTY, String::as_str); + let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned()); + self.services + .db + .iter() + .filter(|&(&name, _)| map_name.is_empty() || map_name == name) + .try_stream() + .try_for_each(|(&name, map)| { + let res = map.property(&property).expect("invalid property"); + writeln!(self, "##### {name}:\n```\n{}\n```", res.trim()) + }) + .await + } + + pub(super) async fn database_files(&self, map: Option, level: Option) -> Result { + let mut files: Vec<_> = self.services.db.db.file_list().collect::>()?; + + files.sort_by_key(|f| f.name.clone()); + + writeln!(self, "| lev | sst | keys | dels | size | column |").await?; + writeln!(self, "| ---: | :--- | ---: | ---: | ---: | :--- |").await?; + files + .into_iter() + .filter(|file| { + map.as_deref() + .is_none_or(|map| map == file.column_family_name) + }) + .filter(|file| level.as_ref().is_none_or(|&level| level == file.level)) + .try_stream() + .try_for_each(|file| { + writeln!( + self, + "| {} | {:<13} | {:7}+ | {:4}- | {:9} | {} |", + file.level, + file.name, + file.num_entries, + file.num_deletions, + file.size, + file.column_family_name, + ) + }) + .await + } + + pub(super) async fn trim_memory(&self) -> Result { + conduwuit::alloc::trim(None)?; + + writeln!(self, "done").await + } + + pub(super) async fn send_test_email(&self) -> Result { + self.bail_restricted()?; + + let mailer = self.services.mailer.expect_mailer()?; + let Some(sender) = self.sender else { + return Err!("No sender user provided in context"); + }; + + let Some(email) = self + .services + .threepid + .get_email_for_localpart(sender.localpart()) + .await + else { + return Err!("{} has no associated email address", sender); + }; + + mailer + .send(Mailbox::new(None, email.clone()), service::mailer::messages::Test) + .await?; + + self.write_str(&format!("Test email successfully sent to {email}")) + .await?; + + Ok(()) + } } diff --git a/src/admin/debug/tester.rs b/src/admin/debug/tester.rs index 75da382bc..80f590f4c 100644 --- a/src/admin/debug/tester.rs +++ b/src/admin/debug/tester.rs @@ -1,6 +1,6 @@ use conduwuit::{Err, Result}; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, clap::Subcommand)] @@ -11,37 +11,32 @@ pub enum TesterCommand { Timer, } -#[rustfmt::skip] -#[admin_command] -async fn panic(&self) -> Result { +impl crate::Context<'_> { + #[rustfmt::skip] + async fn panic(&self) -> Result { + panic!("panicked") + } - panic!("panicked") -} + #[rustfmt::skip] + async fn failure(&self) -> Result { + Err!("failed") + } -#[rustfmt::skip] -#[admin_command] -async fn failure(&self) -> Result { + #[inline(never)] + #[rustfmt::skip] + async fn tester(&self) -> Result { + self.write_str("Ok").await + } - Err!("failed") -} + #[inline(never)] + #[rustfmt::skip] + async fn timer(&self) -> Result { + let started = std::time::Instant::now(); + timed(self.body); -#[inline(never)] -#[rustfmt::skip] -#[admin_command] -async fn tester(&self) -> Result { - - self.write_str("Ok").await -} - -#[inline(never)] -#[rustfmt::skip] -#[admin_command] -async fn timer(&self) -> Result { - let started = std::time::Instant::now(); - timed(self.body); - - let elapsed = started.elapsed(); - self.write_str(&format!("completed in {elapsed:#?}")).await + let elapsed = started.elapsed(); + self.write_str(&format!("completed in {elapsed:#?}")).await + } } #[inline(never)] diff --git a/src/admin/federation/commands.rs b/src/admin/federation/commands.rs index bad1c403b..64a8e0dac 100644 --- a/src/admin/federation/commands.rs +++ b/src/admin/federation/commands.rs @@ -4,127 +4,131 @@ use conduwuit::{Err, Result, utils::response::LimitReadExt}; use futures::StreamExt; use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId}; -use crate::{admin_command, get_room_info}; +use crate::get_room_info; -#[admin_command] -pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result { - self.bail_restricted()?; - self.services.rooms.metadata.disable_room(&room_id, true); - self.write_str("Room disabled.").await -} +impl crate::Context<'_> { + pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result { + self.bail_restricted()?; + self.services.rooms.metadata.disable_room(&room_id, true); + self.write_str("Room disabled.").await + } -#[admin_command] -pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result { - self.bail_restricted()?; - self.services.rooms.metadata.disable_room(&room_id, false); - self.write_str("Room enabled.").await -} + pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result { + self.bail_restricted()?; + self.services.rooms.metadata.disable_room(&room_id, false); + self.write_str("Room enabled.").await + } -#[admin_command] -pub(super) async fn incoming_federation(&self) -> Result { - let msg = { - let map = self + pub(super) async fn incoming_federation(&self) -> Result { + let msg = { + let map = self + .services + .rooms + .event_handler + .federation_handletime + .read(); + + let mut msg = format!( + "Handling {} incoming PDUs across {} active transactions:\n", + map.len(), + self.services.transactions.txn_active_handle_count() + ); + for (r, (e, i)) in map.iter() { + let elapsed = i.elapsed(); + writeln!( + msg, + "{} {}: {}m{}s", + r, + e, + elapsed.as_secs() / 60, + elapsed.as_secs() % 60 + )?; + } + msg + }; + + self.write_str(&msg).await + } + + pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName) -> Result { + let response = self + .services + .client + .default + .get(format!("https://{server_name}/.well-known/matrix/support")) + .send() + .await?; + + let text = response + .limit_read_text( + self.services + .config + .max_request_size + .try_into() + .expect("u64 fits into usize"), + ) + .await?; + + if text.is_empty() { + return Err!("Response text/body is empty."); + } + + if text.len() > 1500 { + return Err!( + "Response text/body is over 1500 characters, assuming no support well-known.", + ); + } + + let json: serde_json::Value = match serde_json::from_str(&text) { + | Ok(json) => json, + | Err(_) => { + return Err!("Response text/body is not valid JSON.",); + }, + }; + + let pretty_json: String = match serde_json::to_string_pretty(&json) { + | Ok(json) => json, + | Err(_) => { + return Err!("Response text/body is not valid JSON.",); + }, + }; + + self.write_str(&format!("Got JSON response:\n\n```json\n{pretty_json}\n```")) + .await + } + + pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result { + if user_id.server_name() == self.services.server.name { + return Err!( + "User belongs to our server, please use `list-joined-rooms` user admin command \ + instead.", + ); + } + + let mut rooms: Vec<(OwnedRoomId, u64, String)> = self .services .rooms - .event_handler - .federation_handletime - .read(); + .state_cache + .rooms_joined(&user_id) + .then(async |room_id| get_room_info(self.services, &room_id).await) + .collect() + .await; - let mut msg = format!( - "Handling {} incoming PDUs across {} active transactions:\n", - map.len(), - self.services.transactions.txn_active_handle_count() - ); - for (r, (e, i)) in map.iter() { - let elapsed = i.elapsed(); - writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?; + if rooms.is_empty() { + return Err!("User is not in any rooms."); } - msg - }; - self.write_str(&msg).await -} + rooms.sort_by_key(|r| r.1); + rooms.reverse(); -#[admin_command] -pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName) -> Result { - let response = self - .services - .client - .default - .get(format!("https://{server_name}/.well-known/matrix/support")) - .send() - .await?; + let num = rooms.len(); + let body = rooms + .iter() + .map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}")) + .collect::>() + .join("\n"); - let text = response - .limit_read_text( - self.services - .config - .max_request_size - .try_into() - .expect("u64 fits into usize"), - ) - .await?; - - if text.is_empty() { - return Err!("Response text/body is empty."); - } - - if text.len() > 1500 { - return Err!( - "Response text/body is over 1500 characters, assuming no support well-known.", - ); - } - - let json: serde_json::Value = match serde_json::from_str(&text) { - | Ok(json) => json, - | Err(_) => { - return Err!("Response text/body is not valid JSON.",); - }, - }; - - let pretty_json: String = match serde_json::to_string_pretty(&json) { - | Ok(json) => json, - | Err(_) => { - return Err!("Response text/body is not valid JSON.",); - }, - }; - - self.write_str(&format!("Got JSON response:\n\n```json\n{pretty_json}\n```")) - .await -} - -#[admin_command] -pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result { - if user_id.server_name() == self.services.server.name { - return Err!( - "User belongs to our server, please use `list-joined-rooms` user admin command \ - instead.", - ); - } - - let mut rooms: Vec<(OwnedRoomId, u64, String)> = self - .services - .rooms - .state_cache - .rooms_joined(&user_id) - .then(async |room_id| get_room_info(self.services, &room_id).await) - .collect() - .await; - - if rooms.is_empty() { - return Err!("User is not in any rooms."); + self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```")) + .await } - - rooms.sort_by_key(|r| r.1); - rooms.reverse(); - - let num = rooms.len(); - let body = rooms - .iter() - .map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}")) - .collect::>() - .join("\n"); - - self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```")) - .await } diff --git a/src/admin/media/commands.rs b/src/admin/media/commands.rs index 6943d82d5..b2809ecee 100644 --- a/src/admin/media/commands.rs +++ b/src/admin/media/commands.rs @@ -9,401 +9,403 @@ use conduwuit_service::media::Dim; use ruma::{OwnedEventId, OwnedMxcUri, OwnedServerName}; use service::media::mxc::Mxc; -use crate::{admin_command, utils::parse_local_user_id}; +use crate::utils::parse_local_user_id; -#[admin_command] -pub(super) async fn delete( - &self, - mxc: Option, - event_id: Option, -) -> Result { - self.bail_restricted()?; +impl crate::Context<'_> { + pub(super) async fn delete( + &self, + mxc: Option, + event_id: Option, + ) -> Result { + self.bail_restricted()?; - if event_id.is_some() && mxc.is_some() { - return Err!("Please specify either an MXC or an event ID, not both.",); - } + if event_id.is_some() && mxc.is_some() { + return Err!("Please specify either an MXC or an event ID, not both.",); + } - if let Some(mxc) = mxc { - trace!("Got MXC URL: {mxc}"); - self.services - .media - .delete(&mxc.as_str().try_into()?) - .await?; + if let Some(mxc) = mxc { + trace!("Got MXC URL: {mxc}"); + self.services + .media + .delete(&mxc.as_str().try_into()?) + .await?; - return self - .write_str("Deleted the MXC from our database and on our filesystem.") - .await; - } + return self + .write_str("Deleted the MXC from our database and on our filesystem.") + .await; + } - if let Some(event_id) = event_id { - trace!("Got event ID to delete media from: {event_id}"); + if let Some(event_id) = event_id { + trace!("Got event ID to delete media from: {event_id}"); - let mut mxc_urls = Vec::with_capacity(4); + let mut mxc_urls = Vec::with_capacity(4); - // parsing the PDU for any MXC URLs begins here - match self.services.rooms.timeline.get_pdu_json(&event_id).await { - | Ok(event_json) => { - if let Some(content_key) = event_json.get("content") { - debug!("Event ID has \"content\"."); - let content_obj = content_key.as_object(); + // parsing the PDU for any MXC URLs begins here + match self.services.rooms.timeline.get_pdu_json(&event_id).await { + | Ok(event_json) => { + if let Some(content_key) = event_json.get("content") { + debug!("Event ID has \"content\"."); + let content_obj = content_key.as_object(); - if let Some(content) = content_obj { - // 1. attempts to parse the "url" key - debug!("Attempting to go into \"url\" key for main media file"); - if let Some(url) = content.get("url") { - debug!("Got a URL in the event ID {event_id}: {url}"); + if let Some(content) = content_obj { + // 1. attempts to parse the "url" key + debug!("Attempting to go into \"url\" key for main media file"); + if let Some(url) = content.get("url") { + debug!("Got a URL in the event ID {event_id}: {url}"); - if url.to_string().starts_with("\"mxc://") { - debug!("Pushing URL {url} to list of MXCs to delete"); - let final_url = url.to_string().replace('"', ""); - mxc_urls.push(final_url); - } else { - info!( - "Found a URL in the event ID {event_id} but did not start \ - with mxc://, ignoring" - ); - } - } - - // 2. attempts to parse the "info" key - debug!("Attempting to go into \"info\" key for thumbnails"); - if let Some(info_key) = content.get("info") { - debug!("Event ID has \"info\"."); - let info_obj = info_key.as_object(); - - if let Some(info) = info_obj { - if let Some(thumbnail_url) = info.get("thumbnail_url") { - debug!("Found a thumbnail_url in info key: {thumbnail_url}"); - - if thumbnail_url.to_string().starts_with("\"mxc://") { - debug!( - "Pushing thumbnail URL {thumbnail_url} to list of \ - MXCs to delete" - ); - let final_thumbnail_url = - thumbnail_url.to_string().replace('"', ""); - mxc_urls.push(final_thumbnail_url); - } else { - info!( - "Found a thumbnail URL in the event ID {event_id} \ - but did not start with mxc://, ignoring" - ); - } + if url.to_string().starts_with("\"mxc://") { + debug!("Pushing URL {url} to list of MXCs to delete"); + let final_url = url.to_string().replace('"', ""); + mxc_urls.push(final_url); } else { info!( - "No \"thumbnail_url\" key in \"info\" key, assuming no \ - thumbnails." + "Found a URL in the event ID {event_id} but did not \ + start with mxc://, ignoring" ); } } - } - // 3. attempts to parse the "file" key - debug!("Attempting to go into \"file\" key"); - if let Some(file_key) = content.get("file") { - debug!("Event ID has \"file\"."); - let file_obj = file_key.as_object(); + // 2. attempts to parse the "info" key + debug!("Attempting to go into \"info\" key for thumbnails"); + if let Some(info_key) = content.get("info") { + debug!("Event ID has \"info\"."); + let info_obj = info_key.as_object(); - if let Some(file) = file_obj { - if let Some(url) = file.get("url") { - debug!("Found url in file key: {url}"); + if let Some(info) = info_obj { + if let Some(thumbnail_url) = info.get("thumbnail_url") { + debug!( + "Found a thumbnail_url in info key: {thumbnail_url}" + ); - if url.to_string().starts_with("\"mxc://") { - debug!("Pushing URL {url} to list of MXCs to delete"); - let final_url = url.to_string().replace('"', ""); - mxc_urls.push(final_url); + if thumbnail_url.to_string().starts_with("\"mxc://") { + debug!( + "Pushing thumbnail URL {thumbnail_url} to list \ + of MXCs to delete" + ); + let final_thumbnail_url = + thumbnail_url.to_string().replace('"', ""); + mxc_urls.push(final_thumbnail_url); + } else { + info!( + "Found a thumbnail URL in the event ID \ + {event_id} but did not start with mxc://, \ + ignoring" + ); + } } else { - warn!( - "Found a URL in the event ID {event_id} but did not \ - start with mxc://, ignoring" + info!( + "No \"thumbnail_url\" key in \"info\" key, assuming \ + no thumbnails." ); } - } else { - error!("No \"url\" key in \"file\" key."); } } + + // 3. attempts to parse the "file" key + debug!("Attempting to go into \"file\" key"); + if let Some(file_key) = content.get("file") { + debug!("Event ID has \"file\"."); + let file_obj = file_key.as_object(); + + if let Some(file) = file_obj { + if let Some(url) = file.get("url") { + debug!("Found url in file key: {url}"); + + if url.to_string().starts_with("\"mxc://") { + debug!("Pushing URL {url} to list of MXCs to delete"); + let final_url = url.to_string().replace('"', ""); + mxc_urls.push(final_url); + } else { + warn!( + "Found a URL in the event ID {event_id} but did \ + not start with mxc://, ignoring" + ); + } + } else { + error!("No \"url\" key in \"file\" key."); + } + } + } + } else { + return Err!( + "Event ID does not have a \"content\" key or failed parsing the \ + event ID JSON.", + ); } } else { return Err!( - "Event ID does not have a \"content\" key or failed parsing the \ - event ID JSON.", + "Event ID does not have a \"content\" key, this is not a message or \ + an event type that contains media.", ); } - } else { - return Err!( - "Event ID does not have a \"content\" key, this is not a message or an \ - event type that contains media.", - ); + }, + | _ => { + return Err!("Event ID does not exist or is not known to us.",); + }, + } + + if mxc_urls.is_empty() { + return Err!("Parsed event ID but found no MXC URLs.",); + } + + let mut mxc_deletion_count: usize = 0; + + for mxc_url in mxc_urls { + match self + .services + .media + .delete(&mxc_url.as_str().try_into()?) + .await + { + | Ok(()) => { + debug_info!( + "Successfully deleted {mxc_url} from filesystem and database" + ); + mxc_deletion_count = mxc_deletion_count.saturating_add(1); + }, + | Err(e) => { + debug_warn!( + "Failed to delete {mxc_url}, ignoring error and skipping: {e}" + ); + continue; + }, } - }, - | _ => { - return Err!("Event ID does not exist or is not known to us.",); - }, + } + + return self + .write_str(&format!( + "Deleted {mxc_deletion_count} total MXCs from our database and the \ + filesystem from event ID {event_id}." + )) + .await; } - if mxc_urls.is_empty() { - return Err!("Parsed event ID but found no MXC URLs.",); + Err!( + "Please specify either an MXC using --mxc or an event ID using --event-id of the \ + message containing an image. See --help for details." + ) + } + + pub(super) async fn delete_list(&self) -> Result { + self.bail_restricted()?; + + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details.",); } + let mut failed_parsed_mxcs: usize = 0; + + let mxc_list = self + .body + .to_vec() + .drain(1..self.body.len().checked_sub(1).unwrap()) + .filter_map(|mxc_s| { + mxc_s + .try_into() + .inspect_err(|e| { + debug_warn!("Failed to parse user-provided MXC URI: {e}"); + failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1); + }) + .ok() + }) + .collect::>>(); + let mut mxc_deletion_count: usize = 0; - for mxc_url in mxc_urls { - match self - .services - .media - .delete(&mxc_url.as_str().try_into()?) - .await - { + for mxc in &mxc_list { + trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk"); + match self.services.media.delete(mxc).await { | Ok(()) => { - debug_info!("Successfully deleted {mxc_url} from filesystem and database"); + debug_info!("Successfully deleted {mxc} from filesystem and database"); mxc_deletion_count = mxc_deletion_count.saturating_add(1); }, | Err(e) => { - debug_warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}"); + debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); continue; }, } } - return self - .write_str(&format!( - "Deleted {mxc_deletion_count} total MXCs from our database and the filesystem \ - from event ID {event_id}." - )) - .await; + self.write_str(&format!( + "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our \ + database and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from \ + the database.", + )) + .await } - Err!( - "Please specify either an MXC using --mxc or an event ID using --event-id of the \ - message containing an image. See --help for details." - ) -} + pub(super) async fn delete_past_remote_media( + &self, + duration: String, + before: bool, + after: bool, + yes_i_want_to_delete_local_media: bool, + ) -> Result { + self.bail_restricted()?; -#[admin_command] -pub(super) async fn delete_list(&self) -> Result { - self.bail_restricted()?; - - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details.",); - } - - let mut failed_parsed_mxcs: usize = 0; - - let mxc_list = self - .body - .to_vec() - .drain(1..self.body.len().checked_sub(1).unwrap()) - .filter_map(|mxc_s| { - mxc_s - .try_into() - .inspect_err(|e| { - debug_warn!("Failed to parse user-provided MXC URI: {e}"); - failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1); - }) - .ok() - }) - .collect::>>(); - - let mut mxc_deletion_count: usize = 0; - - for mxc in &mxc_list { - trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk"); - match self.services.media.delete(mxc).await { - | Ok(()) => { - debug_info!("Successfully deleted {mxc} from filesystem and database"); - mxc_deletion_count = mxc_deletion_count.saturating_add(1); - }, - | Err(e) => { - debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); - continue; - }, + if before && after { + return Err!("Please only pick one argument, --before or --after.",); } - } + assert!(!(before && after), "--before and --after should not be specified together"); - self.write_str(&format!( - "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database \ - and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from the database.", - )) - .await -} - -#[admin_command] -pub(super) async fn delete_past_remote_media( - &self, - duration: String, - before: bool, - after: bool, - yes_i_want_to_delete_local_media: bool, -) -> Result { - self.bail_restricted()?; - - if before && after { - return Err!("Please only pick one argument, --before or --after.",); - } - assert!(!(before && after), "--before and --after should not be specified together"); - - let direction = if after { - TimeDirection::After - } else { - TimeDirection::Before - }; - - let time_boundary = parse_timepoint_ago(&duration)?; - let deleted_count = self - .services - .media - .delete_all_media_within_timeframe( - time_boundary, - direction, - yes_i_want_to_delete_local_media, - ) - .await?; - - self.write_str(&format!("Deleted {deleted_count} total files.")) - .await -} - -#[admin_command] -pub(super) async fn delete_all_from_user(&self, username: String) -> Result { - let user_id = parse_local_user_id(self.services, &username)?; - - let deleted_count = self.services.media.delete_from_user(&user_id).await?; - - self.write_str(&format!("Deleted {deleted_count} total files.")) - .await -} - -#[admin_command] -pub(super) async fn delete_all_from_server( - &self, - server_name: OwnedServerName, - yes_i_want_to_delete_local_media: bool, -) -> Result { - self.bail_restricted()?; - - if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media { - return Err!("This command only works for remote media by default.",); - } - - let Ok(all_mxcs) = self - .services - .media - .get_all_mxcs() - .await - .inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}")) - else { - return Err!("Failed to get MXC URIs from our database",); - }; - - let mut deleted_count: usize = 0; - - for mxc in all_mxcs { - let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| { - debug_warn!( - "Failed to parse MXC {mxc} server name from database, ignoring error and \ - skipping: {e}" - ); - }) else { - continue; + let direction = if after { + TimeDirection::After + } else { + TimeDirection::Before }; - if mxc_server_name != server_name - || (self.services.globals.server_is_ours(mxc_server_name) - && !yes_i_want_to_delete_local_media) + let time_boundary = parse_timepoint_ago(&duration)?; + let deleted_count = self + .services + .media + .delete_all_media_within_timeframe( + time_boundary, + direction, + yes_i_want_to_delete_local_media, + ) + .await?; + + self.write_str(&format!("Deleted {deleted_count} total files.")) + .await + } + + pub(super) async fn delete_all_from_user(&self, username: String) -> Result { + let user_id = parse_local_user_id(self.services, &username)?; + + let deleted_count = self.services.media.delete_from_user(&user_id).await?; + + self.write_str(&format!("Deleted {deleted_count} total files.")) + .await + } + + pub(super) async fn delete_all_from_server( + &self, + server_name: OwnedServerName, + yes_i_want_to_delete_local_media: bool, + ) -> Result { + self.bail_restricted()?; + + if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media { - trace!("skipping MXC URI {mxc}"); - continue; + return Err!("This command only works for remote media by default.",); } - let mxc: Mxc<'_> = mxc.as_str().try_into()?; + let Ok(all_mxcs) = self + .services + .media + .get_all_mxcs() + .await + .inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}")) + else { + return Err!("Failed to get MXC URIs from our database",); + }; - match self.services.media.delete(&mxc).await { - | Ok(()) => { - deleted_count = deleted_count.saturating_add(1); - }, - | Err(e) => { - debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); + let mut deleted_count: usize = 0; + + for mxc in all_mxcs { + let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| { + debug_warn!( + "Failed to parse MXC {mxc} server name from database, ignoring error and \ + skipping: {e}" + ); + }) else { continue; - }, + }; + + if mxc_server_name != server_name + || (self.services.globals.server_is_ours(mxc_server_name) + && !yes_i_want_to_delete_local_media) + { + trace!("skipping MXC URI {mxc}"); + continue; + } + + let mxc: Mxc<'_> = mxc.as_str().try_into()?; + + match self.services.media.delete(&mxc).await { + | Ok(()) => { + deleted_count = deleted_count.saturating_add(1); + }, + | Err(e) => { + debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); + continue; + }, + } } + + self.write_str(&format!("Deleted {deleted_count} total files.")) + .await } - self.write_str(&format!("Deleted {deleted_count} total files.")) - .await -} + pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result { + let mxc: Mxc<'_> = mxc.as_str().try_into()?; + let metadata = self.services.media.get_metadata(&mxc).await; -#[admin_command] -pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result { - let mxc: Mxc<'_> = mxc.as_str().try_into()?; - let metadata = self.services.media.get_metadata(&mxc).await; - - self.write_str(&format!("```\n{metadata:#?}\n```")).await -} - -#[admin_command] -pub(super) async fn get_remote_file( - &self, - mxc: OwnedMxcUri, - server: Option, - timeout: u32, -) -> Result { - let mxc: Mxc<'_> = mxc.as_str().try_into()?; - let timeout = Duration::from_millis(timeout.into()); - let mut result = self - .services - .media - .fetch_remote_content(&mxc, None, server.as_deref(), timeout) - .await?; - - // Grab the length of the content before clearing it to not flood the output - let len = result.content.as_ref().expect("content").len(); - result.content.as_mut().expect("content").clear(); - - self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) - .await -} - -#[admin_command] -pub(super) async fn get_remote_thumbnail( - &self, - mxc: OwnedMxcUri, - server: Option, - timeout: u32, - width: u32, - height: u32, -) -> Result { - let mxc: Mxc<'_> = mxc.as_str().try_into()?; - let timeout = Duration::from_millis(timeout.into()); - let dim = Dim::new(width, height, None); - let mut result = self - .services - .media - .fetch_remote_thumbnail(&mxc, None, server.as_deref(), timeout, &dim) - .await?; - - // Grab the length of the content before clearing it to not flood the output - let len = result.content.as_ref().expect("content").len(); - result.content.as_mut().expect("content").clear(); - - self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) - .await -} - -#[admin_command] -pub(super) async fn delete_url_preview(&self, url: Option, all: bool) -> Result { - if all { - self.services.media.clear_url_previews().await; - - return self.write_str("Deleted all cached URL previews.").await; + self.write_str(&format!("```\n{metadata:#?}\n```")).await } - let url = url.expect("clap enforces url is required unless --all"); + pub(super) async fn get_remote_file( + &self, + mxc: OwnedMxcUri, + server: Option, + timeout: u32, + ) -> Result { + let mxc: Mxc<'_> = mxc.as_str().try_into()?; + let timeout = Duration::from_millis(timeout.into()); + let mut result = self + .services + .media + .fetch_remote_content(&mxc, None, server.as_deref(), timeout) + .await?; - self.services.media.remove_url_preview(&url).await?; + // Grab the length of the content before clearing it to not flood the output + let len = result.content.as_ref().expect("content").len(); + result.content.as_mut().expect("content").clear(); - self.write_str(&format!("Deleted cached URL preview for: {url}")) - .await + self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) + .await + } + + pub(super) async fn get_remote_thumbnail( + &self, + mxc: OwnedMxcUri, + server: Option, + timeout: u32, + width: u32, + height: u32, + ) -> Result { + let mxc: Mxc<'_> = mxc.as_str().try_into()?; + let timeout = Duration::from_millis(timeout.into()); + let dim = Dim::new(width, height, None); + let mut result = self + .services + .media + .fetch_remote_thumbnail(&mxc, None, server.as_deref(), timeout, &dim) + .await?; + + // Grab the length of the content before clearing it to not flood the output + let len = result.content.as_ref().expect("content").len(); + result.content.as_mut().expect("content").clear(); + + self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```")) + .await + } + + pub(super) async fn delete_url_preview(&self, url: Option, all: bool) -> Result { + if all { + self.services.media.clear_url_previews().await; + + return self.write_str("Deleted all cached URL previews.").await; + } + + let url = url.expect("clap enforces url is required unless --all"); + + self.services.media.remove_url_preview(&url).await?; + + self.write_str(&format!("Deleted cached URL preview for: {url}")) + .await + } } diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 3c47f4855..1bc33691c 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -26,7 +26,7 @@ extern crate conduwuit_api as api; extern crate conduwuit_core as conduwuit; extern crate conduwuit_service as service; -pub(crate) use conduwuit_macros::{admin_command, admin_command_dispatch}; +pub(crate) use conduwuit_macros::admin_command_dispatch; pub(crate) use crate::{context::Context, utils::get_room_info}; diff --git a/src/admin/query/account_data.rs b/src/admin/query/account_data.rs index 746f72031..82e881e0b 100644 --- a/src/admin/query/account_data.rs +++ b/src/admin/query/account_data.rs @@ -4,7 +4,7 @@ use conduwuit_database::Deserialized as _; use futures::StreamExt; use ruma::{OwnedRoomId, OwnedUserId, exports::serde::Serialize}; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -31,50 +31,50 @@ pub enum AccountDataCommand { }, } -#[admin_command] -async fn changes_since( - &self, - user_id: OwnedUserId, - since: u64, - room_id: Option, -) -> Result { - let timer = tokio::time::Instant::now(); - let results: Vec<_> = self - .services - .account_data - .changes_since(room_id.as_deref(), &user_id, Some(since), None) - .collect() - .await; - let query_time = timer.elapsed(); +impl crate::Context<'_> { + async fn changes_since( + &self, + user_id: OwnedUserId, + since: u64, + room_id: Option, + ) -> Result { + let timer = tokio::time::Instant::now(); + let results: Vec<_> = self + .services + .account_data + .changes_since(room_id.as_deref(), &user_id, Some(since), None) + .collect() + .await; + let query_time = timer.elapsed(); - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")) - .await -} + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")) + .await + } -#[admin_command] -async fn account_data_get( - &self, - user_id: OwnedUserId, - kind: String, - room_id: Option, -) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .account_data - .get_raw(room_id.as_deref(), &user_id, &kind) - .await; - let query_time = timer.elapsed(); + async fn account_data_get( + &self, + user_id: OwnedUserId, + kind: String, + room_id: Option, + ) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .account_data + .get_raw(room_id.as_deref(), &user_id, &kind) + .await; + let query_time = timer.elapsed(); - let json = serde_json::to_string_pretty(&match room_id { - | None => result - .deserialized::>()? - .serialize(serde_json::value::Serializer)?, - | Some(_) => result - .deserialized::>()? - .serialize(serde_json::value::Serializer)?, - })?; + let json = serde_json::to_string_pretty(&match room_id { + | None => result + .deserialized::>()? + .serialize(serde_json::value::Serializer)?, + | Some(_) => result + .deserialized::>()? + .serialize(serde_json::value::Serializer)?, + })?; - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{json}\n```")) - .await + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{json}\n```")) + .await + } } diff --git a/src/admin/query/raw.rs b/src/admin/query/raw.rs index 97579d844..557c6d844 100644 --- a/src/admin/query/raw.rs +++ b/src/admin/query/raw.rs @@ -13,7 +13,7 @@ use conduwuit_service::Services; use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; use tokio::time::Instant; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -159,279 +159,287 @@ pub enum RawCommand { }, } -#[admin_command] -pub(super) async fn compact( - &self, - map: Option>, - start: Option, - stop: Option, - from: Option, - into: Option, - parallelism: Option, - exhaustive: bool, -) -> Result { - use conduwuit_database::compact::Options; +impl crate::Context<'_> { + pub(super) async fn compact( + &self, + map: Option>, + start: Option, + stop: Option, + from: Option, + into: Option, + parallelism: Option, + exhaustive: bool, + ) -> Result { + use conduwuit_database::compact::Options; - let default_all_maps: Option<_> = map.is_none().then(|| { - self.services - .db - .keys() - .map(Deref::deref) - .map(ToOwned::to_owned) - }); + let default_all_maps: Option<_> = map.is_none().then(|| { + self.services + .db + .keys() + .map(Deref::deref) + .map(ToOwned::to_owned) + }); - let maps: Vec<_> = map - .unwrap_or_default() - .into_iter() - .chain(default_all_maps.into_iter().flatten()) - .map(|map| self.services.db.get(&map)) - .filter_map(Result::ok) - .cloned() - .collect(); + let maps: Vec<_> = map + .unwrap_or_default() + .into_iter() + .chain(default_all_maps.into_iter().flatten()) + .map(|map| self.services.db.get(&map)) + .filter_map(Result::ok) + .cloned() + .collect(); - if maps.is_empty() { - return Err!("--map argument invalid. not found in database"); + if maps.is_empty() { + return Err!("--map argument invalid. not found in database"); + } + + let range = ( + start.as_ref().map(String::as_bytes).map(Into::into), + stop.as_ref().map(String::as_bytes).map(Into::into), + ); + + let options = Options { + range, + level: (from, into), + exclusive: parallelism.is_some_and(is_zero!()), + exhaustive, + }; + + let runtime = self.services.server.runtime().clone(); + let parallelism = parallelism.unwrap_or(1); + let results = maps + .into_iter() + .try_stream::() + .paralleln_and_then(runtime, parallelism, move |map| { + map.compact_blocking(options.clone())?; + Ok(map.name().to_owned()) + }) + .collect::>(); + + let timer = Instant::now(); + let results = results.await; + let query_time = timer.elapsed(); + self.write_str(&format!("Jobs completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")) + .await } - let range = ( - start.as_ref().map(String::as_bytes).map(Into::into), - stop.as_ref().map(String::as_bytes).map(Into::into), - ); + pub(super) async fn raw_count(&self, map: Option, prefix: Option) -> Result { + let prefix = prefix.as_deref().unwrap_or(EMPTY); - let options = Options { - range, - level: (from, into), - exclusive: parallelism.is_some_and(is_zero!()), - exhaustive, - }; + let timer = Instant::now(); + let count = with_maps_or(map.as_deref(), self.services) + .then(|map| map.raw_count_prefix(&prefix)) + .ready_fold(0_usize, usize::saturating_add) + .await; - let runtime = self.services.server.runtime().clone(); - let parallelism = parallelism.unwrap_or(1); - let results = maps - .into_iter() - .try_stream::() - .paralleln_and_then(runtime, parallelism, move |map| { - map.compact_blocking(options.clone())?; - Ok(map.name().to_owned()) - }) - .collect::>(); + let query_time = timer.elapsed(); + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{count:#?}\n```")) + .await + } - let timer = Instant::now(); - let results = results.await; - let query_time = timer.elapsed(); - self.write_str(&format!("Jobs completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")) - .await -} + pub(super) async fn raw_keys(&self, map: String, prefix: Option) -> Result { + writeln!(self, "```").boxed().await?; -#[admin_command] -pub(super) async fn raw_count(&self, map: Option, prefix: Option) -> Result { - let prefix = prefix.as_deref().unwrap_or(EMPTY); + let map = self.services.db.get(map.as_str())?; + let timer = Instant::now(); + prefix + .as_deref() + .map_or_else(|| map.raw_keys().boxed(), |prefix| map.raw_keys_prefix(prefix).boxed()) + .map_ok(String::from_utf8_lossy) + .try_for_each(|str| writeln!(self, "{str:?}")) + .boxed() + .await?; - let timer = Instant::now(); - let count = with_maps_or(map.as_deref(), self.services) - .then(|map| map.raw_count_prefix(&prefix)) - .ready_fold(0_usize, usize::saturating_add) - .await; + let query_time = timer.elapsed(); + self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{count:#?}\n```")) - .await -} + pub(super) async fn raw_keys_sizes( + &self, + map: Option, + prefix: Option, + ) -> Result { + let prefix = prefix.as_deref().unwrap_or(EMPTY); -#[admin_command] -pub(super) async fn raw_keys(&self, map: String, prefix: Option) -> Result { - writeln!(self, "```").boxed().await?; + let timer = Instant::now(); + let result = with_maps_or(map.as_deref(), self.services) + .map(|map| map.raw_keys_prefix(&prefix)) + .flatten() + .ignore_err() + .map(<[u8]>::len) + .ready_fold_default(|mut map: BTreeMap<_, usize>, len| { + let entry = map.entry(len).or_default(); + *entry = entry.saturating_add(1); + map + }) + .await; - let map = self.services.db.get(map.as_str())?; - let timer = Instant::now(); - prefix - .as_deref() - .map_or_else(|| map.raw_keys().boxed(), |prefix| map.raw_keys_prefix(prefix).boxed()) - .map_ok(String::from_utf8_lossy) - .try_for_each(|str| writeln!(self, "{str:?}")) - .boxed() - .await?; + let query_time = timer.elapsed(); + self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_keys_total( + &self, + map: Option, + prefix: Option, + ) -> Result { + let prefix = prefix.as_deref().unwrap_or(EMPTY); -#[admin_command] -pub(super) async fn raw_keys_sizes(&self, map: Option, prefix: Option) -> Result { - let prefix = prefix.as_deref().unwrap_or(EMPTY); + let timer = Instant::now(); + let result = with_maps_or(map.as_deref(), self.services) + .map(|map| map.raw_keys_prefix(&prefix)) + .flatten() + .ignore_err() + .map(<[u8]>::len) + .ready_fold_default(|acc: usize, len| acc.saturating_add(len)) + .await; - let timer = Instant::now(); - let result = with_maps_or(map.as_deref(), self.services) - .map(|map| map.raw_keys_prefix(&prefix)) - .flatten() - .ignore_err() - .map(<[u8]>::len) - .ready_fold_default(|mut map: BTreeMap<_, usize>, len| { - let entry = map.entry(len).or_default(); - *entry = entry.saturating_add(1); - map - }) - .await; + let query_time = timer.elapsed(); + self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_vals_sizes( + &self, + map: Option, + prefix: Option, + ) -> Result { + let prefix = prefix.as_deref().unwrap_or(EMPTY); -#[admin_command] -pub(super) async fn raw_keys_total(&self, map: Option, prefix: Option) -> Result { - let prefix = prefix.as_deref().unwrap_or(EMPTY); + let timer = Instant::now(); + let result = with_maps_or(map.as_deref(), self.services) + .map(|map| map.raw_stream_prefix(&prefix)) + .flatten() + .ignore_err() + .map(at!(1)) + .map(<[u8]>::len) + .ready_fold_default(|mut map: BTreeMap<_, usize>, len| { + let entry = map.entry(len).or_default(); + *entry = entry.saturating_add(1); + map + }) + .await; - let timer = Instant::now(); - let result = with_maps_or(map.as_deref(), self.services) - .map(|map| map.raw_keys_prefix(&prefix)) - .flatten() - .ignore_err() - .map(<[u8]>::len) - .ready_fold_default(|acc: usize, len| acc.saturating_add(len)) - .await; + let query_time = timer.elapsed(); + self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_vals_total( + &self, + map: Option, + prefix: Option, + ) -> Result { + let prefix = prefix.as_deref().unwrap_or(EMPTY); -#[admin_command] -pub(super) async fn raw_vals_sizes(&self, map: Option, prefix: Option) -> Result { - let prefix = prefix.as_deref().unwrap_or(EMPTY); + let timer = Instant::now(); + let result = with_maps_or(map.as_deref(), self.services) + .map(|map| map.raw_stream_prefix(&prefix)) + .flatten() + .ignore_err() + .map(at!(1)) + .map(<[u8]>::len) + .ready_fold_default(|acc: usize, len| acc.saturating_add(len)) + .await; - let timer = Instant::now(); - let result = with_maps_or(map.as_deref(), self.services) - .map(|map| map.raw_stream_prefix(&prefix)) - .flatten() - .ignore_err() - .map(at!(1)) - .map(<[u8]>::len) - .ready_fold_default(|mut map: BTreeMap<_, usize>, len| { - let entry = map.entry(len).or_default(); - *entry = entry.saturating_add(1); - map - }) - .await; + let query_time = timer.elapsed(); + self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_iter(&self, map: String, prefix: Option) -> Result { + writeln!(self, "```").await?; -#[admin_command] -pub(super) async fn raw_vals_total(&self, map: Option, prefix: Option) -> Result { - let prefix = prefix.as_deref().unwrap_or(EMPTY); + let map = self.services.db.get(&map)?; + let timer = Instant::now(); + prefix + .as_deref() + .map_or_else( + || map.raw_stream().boxed(), + |prefix| map.raw_stream_prefix(prefix).boxed(), + ) + .map_ok(apply!(2, String::from_utf8_lossy)) + .map_ok(apply!(2, Cow::into_owned)) + .try_for_each(|keyval| writeln!(self, "{keyval:?}")) + .boxed() + .await?; - let timer = Instant::now(); - let result = with_maps_or(map.as_deref(), self.services) - .map(|map| map.raw_stream_prefix(&prefix)) - .flatten() - .ignore_err() - .map(at!(1)) - .map(<[u8]>::len) - .ready_fold_default(|acc: usize, len| acc.saturating_add(len)) - .await; + let query_time = timer.elapsed(); + self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_keys_from( + &self, + map: String, + start: String, + limit: Option, + ) -> Result { + writeln!(self, "```").await?; -#[admin_command] -pub(super) async fn raw_iter(&self, map: String, prefix: Option) -> Result { - writeln!(self, "```").await?; + let map = self.services.db.get(&map)?; + let timer = Instant::now(); + map.raw_keys_from(&start) + .map_ok(String::from_utf8_lossy) + .take(limit.unwrap_or(usize::MAX)) + .try_for_each(|str| writeln!(self, "{str:?}")) + .boxed() + .await?; - let map = self.services.db.get(&map)?; - let timer = Instant::now(); - prefix - .as_deref() - .map_or_else(|| map.raw_stream().boxed(), |prefix| map.raw_stream_prefix(prefix).boxed()) - .map_ok(apply!(2, String::from_utf8_lossy)) - .map_ok(apply!(2, Cow::into_owned)) - .try_for_each(|keyval| writeln!(self, "{keyval:?}")) - .boxed() - .await?; + let query_time = timer.elapsed(); + self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) + .await + } - let query_time = timer.elapsed(); - self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) - .await -} + pub(super) async fn raw_iter_from( + &self, + map: String, + start: String, + limit: Option, + ) -> Result { + let map = self.services.db.get(&map)?; + let timer = Instant::now(); + let result = map + .raw_stream_from(&start) + .map_ok(apply!(2, String::from_utf8_lossy)) + .map_ok(apply!(2, Cow::into_owned)) + .take(limit.unwrap_or(usize::MAX)) + .try_collect::>() + .await?; -#[admin_command] -pub(super) async fn raw_keys_from( - &self, - map: String, - start: String, - limit: Option, -) -> Result { - writeln!(self, "```").await?; + let query_time = timer.elapsed(); + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } - let map = self.services.db.get(&map)?; - let timer = Instant::now(); - map.raw_keys_from(&start) - .map_ok(String::from_utf8_lossy) - .take(limit.unwrap_or(usize::MAX)) - .try_for_each(|str| writeln!(self, "{str:?}")) - .boxed() - .await?; + pub(super) async fn raw_del(&self, map: String, key: String) -> Result { + let map = self.services.db.get(&map)?; + let timer = Instant::now(); + map.remove(&key); - let query_time = timer.elapsed(); - self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}")) - .await -} + let query_time = timer.elapsed(); + self.write_str(&format!("Operation completed in {query_time:?}")) + .await + } -#[admin_command] -pub(super) async fn raw_iter_from( - &self, - map: String, - start: String, - limit: Option, -) -> Result { - let map = self.services.db.get(&map)?; - let timer = Instant::now(); - let result = map - .raw_stream_from(&start) - .map_ok(apply!(2, String::from_utf8_lossy)) - .map_ok(apply!(2, Cow::into_owned)) - .take(limit.unwrap_or(usize::MAX)) - .try_collect::>() - .await?; + pub(super) async fn raw_get(&self, map: String, key: String) -> Result { + let map = self.services.db.get(&map)?; + let timer = Instant::now(); + let handle = map.get(&key).await?; - let query_time = timer.elapsed(); - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} + let query_time = timer.elapsed(); + let result = String::from_utf8_lossy(&handle); + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```")) + .await + } -#[admin_command] -pub(super) async fn raw_del(&self, map: String, key: String) -> Result { - let map = self.services.db.get(&map)?; - let timer = Instant::now(); - map.remove(&key); + pub(super) async fn raw_maps(&self) -> Result { + let list: Vec<_> = self.services.db.iter().map(at!(0)).copied().collect(); - let query_time = timer.elapsed(); - self.write_str(&format!("Operation completed in {query_time:?}")) - .await -} - -#[admin_command] -pub(super) async fn raw_get(&self, map: String, key: String) -> Result { - let map = self.services.db.get(&map)?; - let timer = Instant::now(); - let handle = map.get(&key).await?; - - let query_time = timer.elapsed(); - let result = String::from_utf8_lossy(&handle); - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```")) - .await -} - -#[admin_command] -pub(super) async fn raw_maps(&self) -> Result { - let list: Vec<_> = self.services.db.iter().map(at!(0)).copied().collect(); - - self.write_str(&format!("{list:#?}")).await + self.write_str(&format!("{list:#?}")).await + } } fn with_maps_or<'a>( diff --git a/src/admin/query/resolver.rs b/src/admin/query/resolver.rs index d651e5bff..7e5a4b40d 100644 --- a/src/admin/query/resolver.rs +++ b/src/admin/query/resolver.rs @@ -3,7 +3,7 @@ use conduwuit::{Err, Result, utils::time}; use futures::StreamExt; use ruma::OwnedServerName; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -39,67 +39,68 @@ pub enum ResolverCommand { }, } -#[admin_command] -async fn destinations_cache(&self, server_name: Option) -> Result { - use service::resolver::cache::CachedDest; +impl crate::Context<'_> { + async fn destinations_cache(&self, server_name: Option) -> Result { + use service::resolver::cache::CachedDest; - writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?; - writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?; + writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?; + writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?; - let mut destinations = self.services.resolver.cache.destinations().boxed(); + let mut destinations = self.services.resolver.cache.destinations().boxed(); - while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await { - if let Some(server_name) = server_name.as_ref() { - if name != *server_name { - continue; + while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await { + if let Some(server_name) = server_name.as_ref() { + if name != *server_name { + continue; + } } + + let expire = time::format(expire, "%+"); + self.write_str(&format!("| {name} | {dest} | {host} | {expire} |\n")) + .await?; } - let expire = time::format(expire, "%+"); - self.write_str(&format!("| {name} | {dest} | {host} | {expire} |\n")) - .await?; + Ok(()) } - Ok(()) -} + async fn overrides_cache(&self, server_name: Option) -> Result { + use service::resolver::cache::CachedOverride; -#[admin_command] -async fn overrides_cache(&self, server_name: Option) -> Result { - use service::resolver::cache::CachedOverride; + writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?; + writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?; - writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?; - writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?; + let mut overrides = self.services.resolver.cache.overrides().boxed(); - let mut overrides = self.services.resolver.cache.overrides().boxed(); - - while let Some((name, CachedOverride { ips, port, expire, overriding })) = - overrides.next().await - { - if let Some(server_name) = server_name.as_ref() { - if name != *server_name { - continue; + while let Some((name, CachedOverride { ips, port, expire, overriding })) = + overrides.next().await + { + if let Some(server_name) = server_name.as_ref() { + if name != *server_name { + continue; + } } + + let expire = time::format(expire, "%+"); + self.write_str(&format!( + "| {name} | {ips:?} | {port} | {expire} | {overriding:?} |\n" + )) + .await?; } - let expire = time::format(expire, "%+"); - self.write_str(&format!("| {name} | {ips:?} | {port} | {expire} | {overriding:?} |\n")) - .await?; + Ok(()) } - Ok(()) -} - -#[admin_command] -async fn flush_cache(&self, name: Option, all: bool) -> Result { - if all { - self.services.resolver.cache.clear().await; - writeln!(self, "Resolver caches cleared!").await - } else if let Some(name) = name { - self.services.resolver.cache.del_destination(&name); - self.services.resolver.cache.del_override(&name); - self.write_str(&format!("Cleared {name} from resolver caches!")) - .await - } else { - Err!("Missing name. Supply a name or use --all to flush the whole cache.") + async fn flush_cache(&self, name: Option, all: bool) -> Result { + if all { + self.services.resolver.cache.clear().await; + writeln!(self, "Resolver caches cleared!").await + } else if let Some(name) = name { + self.services.resolver.cache.del_destination(&name); + self.services.resolver.cache.del_override(&name); + self.write_str(&format!("Cleared {name} from resolver caches!")) + .await + } else { + Err!("Missing name. Supply a name or use --all to flush the whole cache.") + } } } diff --git a/src/admin/query/room_timeline.rs b/src/admin/query/room_timeline.rs index 2ca46944e..2b2a9b8d4 100644 --- a/src/admin/query/room_timeline.rs +++ b/src/admin/query/room_timeline.rs @@ -3,7 +3,7 @@ use conduwuit::{PduCount, Result, utils::stream::TryTools}; use futures::TryStreamExt; use ruma::OwnedRoomOrAliasId; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -23,39 +23,39 @@ pub enum RoomTimelineCommand { }, } -#[admin_command] -pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result { - let room_id = self.services.rooms.alias.resolve(&room_id).await?; +impl crate::Context<'_> { + pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result { + let room_id = self.services.rooms.alias.resolve(&room_id).await?; - let result = self - .services - .rooms - .timeline - .last_timeline_count(&room_id) - .await?; + let result = self + .services + .rooms + .timeline + .last_timeline_count(&room_id) + .await?; - self.write_str(&format!("{result:#?}")).await -} + self.write_str(&format!("{result:#?}")).await + } -#[admin_command] -pub(super) async fn pdus( - &self, - room_id: OwnedRoomOrAliasId, - from: Option, - limit: Option, -) -> Result { - let room_id = self.services.rooms.alias.resolve(&room_id).await?; + pub(super) async fn pdus( + &self, + room_id: OwnedRoomOrAliasId, + from: Option, + limit: Option, + ) -> Result { + let room_id = self.services.rooms.alias.resolve(&room_id).await?; - let from: Option = from.as_deref().map(str::parse).transpose()?; + let from: Option = from.as_deref().map(str::parse).transpose()?; - let result: Vec<_> = self - .services - .rooms - .timeline - .pdus_rev(&room_id, from) - .try_take(limit.unwrap_or(3)) - .try_collect() - .await?; + let result: Vec<_> = self + .services + .rooms + .timeline + .pdus_rev(&room_id, from) + .try_take(limit.unwrap_or(3)) + .try_collect() + .await?; - self.write_str(&format!("```\n{result:#?}\n```")).await + self.write_str(&format!("```\n{result:#?}\n```")).await + } } diff --git a/src/admin/query/short.rs b/src/admin/query/short.rs index 3ebfbbcf8..6bcd2d4c0 100644 --- a/src/admin/query/short.rs +++ b/src/admin/query/short.rs @@ -2,7 +2,7 @@ use clap::Subcommand; use conduwuit::Result; use ruma::{OwnedEventId, OwnedRoomOrAliasId}; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -17,23 +17,23 @@ pub enum ShortCommand { }, } -#[admin_command] -pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result { - let shortid = self - .services - .rooms - .short - .get_shorteventid(&event_id) - .await?; +impl crate::Context<'_> { + pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result { + let shortid = self + .services + .rooms + .short + .get_shorteventid(&event_id) + .await?; - self.write_str(&format!("{shortid:#?}")).await -} + self.write_str(&format!("{shortid:#?}")).await + } -#[admin_command] -pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result { - let room_id = self.services.rooms.alias.resolve(&room_id).await?; + pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result { + let room_id = self.services.rooms.alias.resolve(&room_id).await?; - let shortid = self.services.rooms.short.get_shortroomid(&room_id).await?; + let shortid = self.services.rooms.short.get_shortroomid(&room_id).await?; - self.write_str(&format!("{shortid:#?}")).await + self.write_str(&format!("{shortid:#?}")).await + } } diff --git a/src/admin/query/users.rs b/src/admin/query/users.rs index dd49415f7..13fe9a888 100644 --- a/src/admin/query/users.rs +++ b/src/admin/query/users.rs @@ -3,7 +3,7 @@ use conduwuit::Result; use futures::stream::StreamExt; use ruma::{OwnedDeviceId, OwnedRoomId, OwnedUserId}; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -92,263 +92,259 @@ pub enum UsersCommand { }, } -#[admin_command] -async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let result: Vec<_> = self - .services - .rooms - .state_cache - .get_shared_rooms(&user_a, &user_b) - .collect() - .await; - let query_time = timer.elapsed(); +impl crate::Context<'_> { + async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let result: Vec<_> = self + .services + .rooms + .state_cache + .get_shared_rooms(&user_a, &user_b) + .collect() + .await; + let query_time = timer.elapsed(); - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_backup_session( - &self, - user_id: OwnedUserId, - version: String, - room_id: OwnedRoomId, - session_id: String, -) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .key_backups - .get_session(&user_id, &version, &room_id, &session_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_room_backups( - &self, - user_id: OwnedUserId, - version: String, - room_id: OwnedRoomId, -) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .key_backups - .get_room(&user_id, &version, &room_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result { - let timer = tokio::time::Instant::now(); - let result = self.services.key_backups.get_all(&user_id, &version).await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .key_backups - .get_backup(&user_id, &version) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .key_backups - .get_latest_backup_version(&user_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self.services.key_backups.get_latest_backup(&user_id).await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn iter_users(&self) -> Result { - let timer = tokio::time::Instant::now(); - let result: Vec = self.services.users.stream().map(Into::into).collect().await; - - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn iter_users2(&self) -> Result { - let timer = tokio::time::Instant::now(); - let result: Vec<_> = self.services.users.stream().collect().await; - let result: Vec<_> = result - .into_iter() - .map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned()) - .collect(); - - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```")) - .await -} - -#[admin_command] -async fn count_users(&self) -> Result { - let timer = tokio::time::Instant::now(); - let result = self.services.users.count().await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn list_devices(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let devices = self - .services - .users - .all_device_ids(&user_id) - .collect::>() - .await; - - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```")) - .await -} - -#[admin_command] -async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let devices = self - .services - .users - .all_devices_metadata(&user_id) - .collect::>() - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```")) - .await -} - -#[admin_command] -async fn get_device_metadata(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result { - let timer = tokio::time::Instant::now(); - let device = self - .services - .users - .get_device_metadata(&user_id, &device_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```")) - .await -} - -#[admin_command] -async fn get_devices_version(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let device = self.services.users.get_devicelist_version(&user_id).await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```")) - .await -} - -#[admin_command] -async fn count_one_time_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .users - .count_one_time_keys(&user_id, &device_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .users - .get_device_keys(&user_id, &device_id) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self.services.users.get_user_signing_key(&user_id).await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_master_key(&self, user_id: OwnedUserId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .users - .get_master_key(None, &user_id, &|_| true) - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await -} - -#[admin_command] -async fn get_to_device_events(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result { - let timer = tokio::time::Instant::now(); - let result = self - .services - .users - .get_to_device_events(&user_id, &device_id, None, None) - .collect::>() - .await; - let query_time = timer.elapsed(); - - self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) - .await + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_backup_session( + &self, + user_id: OwnedUserId, + version: String, + room_id: OwnedRoomId, + session_id: String, + ) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .key_backups + .get_session(&user_id, &version, &room_id, &session_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_room_backups( + &self, + user_id: OwnedUserId, + version: String, + room_id: OwnedRoomId, + ) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .key_backups + .get_room(&user_id, &version, &room_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result { + let timer = tokio::time::Instant::now(); + let result = self.services.key_backups.get_all(&user_id, &version).await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .key_backups + .get_backup(&user_id, &version) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .key_backups + .get_latest_backup_version(&user_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let result = self.services.key_backups.get_latest_backup(&user_id).await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn iter_users(&self) -> Result { + let timer = tokio::time::Instant::now(); + let result: Vec = + self.services.users.stream().map(Into::into).collect().await; + + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn iter_users2(&self) -> Result { + let timer = tokio::time::Instant::now(); + let result: Vec<_> = self.services.users.stream().collect().await; + let result: Vec<_> = result + .into_iter() + .map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned()) + .collect(); + + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```")) + .await + } + + async fn count_users(&self) -> Result { + let timer = tokio::time::Instant::now(); + let result = self.services.users.count().await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn list_devices(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let devices = self + .services + .users + .all_device_ids(&user_id) + .collect::>() + .await; + + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```")) + .await + } + + async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let devices = self + .services + .users + .all_devices_metadata(&user_id) + .collect::>() + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```")) + .await + } + + async fn get_device_metadata( + &self, + user_id: OwnedUserId, + device_id: OwnedDeviceId, + ) -> Result { + let timer = tokio::time::Instant::now(); + let device = self + .services + .users + .get_device_metadata(&user_id, &device_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```")) + .await + } + + async fn get_devices_version(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let device = self.services.users.get_devicelist_version(&user_id).await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```")) + .await + } + + async fn count_one_time_keys( + &self, + user_id: OwnedUserId, + device_id: OwnedDeviceId, + ) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .users + .count_one_time_keys(&user_id, &device_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .users + .get_device_keys(&user_id, &device_id) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let result = self.services.users.get_user_signing_key(&user_id).await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_master_key(&self, user_id: OwnedUserId) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .users + .get_master_key(None, &user_id, &|_| true) + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } + + async fn get_to_device_events( + &self, + user_id: OwnedUserId, + device_id: OwnedDeviceId, + ) -> Result { + let timer = tokio::time::Instant::now(); + let result = self + .services + .users + .get_to_device_events(&user_id, &device_id, None, None) + .collect::>() + .await; + let query_time = timer.elapsed(); + + self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```")) + .await + } } diff --git a/src/admin/room/commands.rs b/src/admin/room/commands.rs index 06061311c..0cee2ebcc 100644 --- a/src/admin/room/commands.rs +++ b/src/admin/room/commands.rs @@ -2,83 +2,83 @@ use conduwuit::{Err, Result}; use futures::StreamExt; use ruma::OwnedRoomId; -use crate::{PAGE_SIZE, admin_command, get_room_info}; +use crate::{PAGE_SIZE, get_room_info}; -#[allow(clippy::fn_params_excessive_bools)] -#[admin_command] -pub(super) async fn list_rooms( - &self, - page: Option, - exclude_disabled: bool, - exclude_banned: bool, - include_empty: bool, - no_details: bool, -) -> Result { - // 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 = self - .services - .rooms - .metadata - .iter_ids() - .filter_map(|room_id| async move { - (!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await) - .then_some(room_id) - }) - .filter_map(|room_id| async move { - (!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await) - .then_some(room_id) - }) - .then(async |room_id| get_room_info(self.services, &room_id).await) - .then(|(room_id, total_members, name)| async move { - let local_members: Vec<_> = self - .services - .rooms - .state_cache - .active_local_users_in_room(&room_id) - .collect() - .await; - let local_members = local_members.len(); - (room_id, total_members, local_members, name) - }) - .filter_map(|(room_id, total_members, local_members, name)| async move { - (include_empty || local_members > 0).then_some((room_id, total_members, name)) - }) - .collect::>() - .await; +impl crate::Context<'_> { + #[allow(clippy::fn_params_excessive_bools)] + pub(super) async fn list_rooms( + &self, + page: Option, + exclude_disabled: bool, + exclude_banned: bool, + include_empty: bool, + no_details: bool, + ) -> Result { + // 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 = self + .services + .rooms + .metadata + .iter_ids() + .filter_map(|room_id| async move { + (!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await) + .then_some(room_id) + }) + .filter_map(|room_id| async move { + (!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await) + .then_some(room_id) + }) + .then(async |room_id| get_room_info(self.services, &room_id).await) + .then(|(room_id, total_members, name)| async move { + let local_members: Vec<_> = self + .services + .rooms + .state_cache + .active_local_users_in_room(&room_id) + .collect() + .await; + let local_members = local_members.len(); + (room_id, total_members, local_members, name) + }) + .filter_map(|(room_id, total_members, local_members, name)| async move { + (include_empty || local_members > 0).then_some((room_id, total_members, name)) + }) + .collect::>() + .await; - rooms.sort_by_key(|r| r.1); - rooms.reverse(); + rooms.sort_by_key(|r| r.1); + rooms.reverse(); - let rooms = rooms - .into_iter() - .skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE)) - .take(PAGE_SIZE) - .collect::>(); + let rooms = rooms + .into_iter() + .skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE)) + .take(PAGE_SIZE) + .collect::>(); - if rooms.is_empty() { - return Err!("No more rooms."); + if rooms.is_empty() { + return Err!("No more rooms."); + } + + let body = rooms + .iter() + .map(|(id, members, name)| { + if no_details { + format!("{id}") + } else { + format!("{id}\tMembers: {members}\tName: {name}") + } + }) + .collect::>() + .join("\n"); + + self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len())) + .await } - let body = rooms - .iter() - .map(|(id, members, name)| { - if no_details { - format!("{id}") - } else { - format!("{id}\tMembers: {members}\tName: {name}") - } - }) - .collect::>() - .join("\n"); + pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result { + let result = self.services.rooms.metadata.exists(&room_id).await; - self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len())) - .await -} - -#[admin_command] -pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result { - let result = self.services.rooms.metadata.exists(&room_id).await; - - self.write_str(&format!("{result}")).await + self.write_str(&format!("{result}")).await + } } diff --git a/src/admin/room/info.rs b/src/admin/room/info.rs index cfb7a8a83..a035027c7 100644 --- a/src/admin/room/info.rs +++ b/src/admin/room/info.rs @@ -3,7 +3,7 @@ use conduwuit::{Err, Result, utils::ReadyExt}; use futures::StreamExt; use ruma::OwnedRoomId; -use crate::{admin_command, admin_command_dispatch}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -26,62 +26,62 @@ pub enum RoomInfoCommand { }, } -#[admin_command] -async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> Result { - let room_name = self - .services - .rooms - .state_accessor - .get_name(&room_id) - .await - .unwrap_or_else(|_| room_id.to_string()); +impl crate::Context<'_> { + async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> Result { + let room_name = self + .services + .rooms + .state_accessor + .get_name(&room_id) + .await + .unwrap_or_else(|_| room_id.to_string()); - let member_info: Vec<_> = self - .services - .rooms - .state_cache - .room_members(&room_id) - .ready_filter(|user_id| { - local_only - .then(|| self.services.globals.user_is_local(user_id)) - .unwrap_or(true) - }) - .filter_map(|user_id| async move { - Some(( - self.services - .users - .displayname(&user_id) - .await - .unwrap_or_else(|_| user_id.to_string()), - user_id, - )) - }) - .collect() - .await; + let member_info: Vec<_> = self + .services + .rooms + .state_cache + .room_members(&room_id) + .ready_filter(|user_id| { + local_only + .then(|| self.services.globals.user_is_local(user_id)) + .unwrap_or(true) + }) + .filter_map(|user_id| async move { + Some(( + self.services + .users + .displayname(&user_id) + .await + .unwrap_or_else(|_| user_id.to_string()), + user_id, + )) + }) + .collect() + .await; - let num = member_info.len(); - let body = member_info - .into_iter() - .map(|(displayname, mxid)| format!("{mxid} | {displayname}")) - .collect::>() - .join("\n"); + let num = member_info.len(); + let body = member_info + .into_iter() + .map(|(displayname, mxid)| format!("{mxid} | {displayname}")) + .collect::>() + .join("\n"); - self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```")) - .await -} + self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```")) + .await + } -#[admin_command] -async fn view_room_topic(&self, room_id: OwnedRoomId) -> Result { - let Ok(room_topic) = self - .services - .rooms - .state_accessor - .get_room_topic(&room_id) - .await - else { - return Err!("Room does not have a room topic set."); - }; + async fn view_room_topic(&self, room_id: OwnedRoomId) -> Result { + let Ok(room_topic) = self + .services + .rooms + .state_accessor + .get_room_topic(&room_id) + .await + else { + return Err!("Room does not have a room topic set."); + }; - self.write_str(&format!("Room topic:\n```\n{room_topic}\n```")) - .await + self.write_str(&format!("Room topic:\n```\n{room_topic}\n```")) + .await + } } diff --git a/src/admin/room/moderation.rs b/src/admin/room/moderation.rs index 3ae9d978e..7b6758c01 100644 --- a/src/admin/room/moderation.rs +++ b/src/admin/room/moderation.rs @@ -8,7 +8,7 @@ use conduwuit::{ use futures::{FutureExt, StreamExt}; use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId}; -use crate::{admin_command, admin_command_dispatch, get_room_info}; +use crate::{admin_command_dispatch, get_room_info}; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -45,238 +45,72 @@ pub enum RoomModerationCommand { }, } -#[admin_command] -async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result { - debug!("Got room alias or ID: {}", room); +impl crate::Context<'_> { + async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result { + debug!("Got room alias or ID: {}", room); - let admin_room_alias = &self.services.globals.admin_alias; + let admin_room_alias = &self.services.globals.admin_alias; - if let Ok(admin_room_id) = self.services.admin.get_admin_room().await { - if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) { - return Err!("Not allowed to ban the admin room."); + if let Ok(admin_room_id) = self.services.admin.get_admin_room().await { + if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) { + return Err!("Not allowed to ban the admin room."); + } } - } - let room_id = if room.is_room_id() { - let room_id = match RoomId::parse(&room) { - | Ok(room_id) => room_id, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, + let room_id = if room.is_room_id() { + let room_id = match RoomId::parse(&room) { + | Ok(room_id) => room_id, + | Err(e) => { + return Err!( + "Failed to parse room ID {room}. Please note that this requires a full \ + room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`): {e}" + ); + }, + }; + + debug!("Room specified is a room ID, banning room ID"); + + room_id.clone() + } else if room.is_room_alias_id() { + let room_alias = match RoomAliasId::parse(&room) { + | Ok(room_alias) => room_alias, + | Err(e) => { + return Err!( + "Failed to parse room ID {room}. Please note that this requires a full \ + room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`): {e}" + ); + }, + }; + + debug!( + "Room specified is not a room ID, attempting to resolve room alias to a room ID \ + locally, if not using get_alias_helper to fetch room ID remotely" + ); + + match self.services.rooms.alias.resolve_alias(&room_alias).await { + | Ok((room_id, servers)) => { + debug!( + %room_id, + ?servers, + "Got federation response fetching room ID for room {room}" + ); + room_id + }, + | Err(e) => { + return Err!("Failed to resolve room alias {room} to a room ID: {e}"); + }, + } + } else { + return Err!( + "Room specified is not a room ID or room alias. Please note that this requires \ + a full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`)", + ); }; - debug!("Room specified is a room ID, banning room ID"); - - room_id.clone() - } else if room.is_room_alias_id() { - let room_alias = match RoomAliasId::parse(&room) { - | Ok(room_alias) => room_alias, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; - - debug!( - "Room specified is not a room ID, attempting to resolve room alias to a room ID \ - locally, if not using get_alias_helper to fetch room ID remotely" - ); - - match self.services.rooms.alias.resolve_alias(&room_alias).await { - | Ok((room_id, servers)) => { - debug!( - %room_id, - ?servers, - "Got federation response fetching room ID for room {room}" - ); - room_id - }, - | Err(e) => { - return Err!("Failed to resolve room alias {room} to a room ID: {e}"); - }, - } - } else { - return Err!( - "Room specified is not a room ID or room alias. Please note that this requires a \ - full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`)", - ); - }; - - info!("Making all users leave the room {room_id} and forgetting it"); - let mut users = self - .services - .rooms - .state_cache - .room_members(&room_id) - .ready_filter(|user| self.services.globals.user_is_local(user)) - .boxed(); - - while let Some(ref user_id) = users.next().await { - info!( - "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ - evicting admins too)", - ); - - if let Err(e) = leave_room(self.services, user_id, &room_id, None) - .boxed() - .await - { - warn!("Failed to leave room: {e}"); - } - - self.services.rooms.state_cache.forget(&room_id, user_id); - } - - self.services - .rooms - .alias - .local_aliases_for_room(&room_id) - .for_each(|local_alias| async move { - self.services - .rooms - .alias - .remove_alias(&local_alias, &self.services.globals.server_user) - .await - .ok(); - }) - .await; - - self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory - self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins - self.services.rooms.metadata.disable_room(&room_id, true); // disable federation - - self.write_str( - "Room banned, removed all our local users, and disabled incoming federation with room.", - ) - .await -} - -#[admin_command] -async fn ban_list_of_rooms(&self) -> Result { - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details.",); - } - - let rooms_s = self - .body - .to_vec() - .drain(1..self.body.len().saturating_sub(1)) - .collect::>(); - - let admin_room_alias = &self.services.globals.admin_alias; - - let mut room_ban_count: usize = 0; - let mut room_ids: Vec = Vec::new(); - - for &room in &rooms_s { - match <&RoomOrAliasId>::try_from(room) { - | Ok(room_alias_or_id) => { - if let Ok(admin_room_id) = self.services.admin.get_admin_room().await { - if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(admin_room_alias) - { - warn!("User specified admin room in bulk ban list, ignoring"); - continue; - } - } - - if room_alias_or_id.is_room_id() { - let room_id = match RoomId::parse(room_alias_or_id) { - | Ok(room_id) => room_id, - | Err(e) => { - // ignore rooms we failed to parse - warn!( - "Error parsing room \"{room}\" during bulk room banning, \ - ignoring error and logging here: {e}" - ); - continue; - }, - }; - - room_ids.push(room_id.clone()); - } - - if room_alias_or_id.is_room_alias_id() { - match RoomAliasId::parse(room_alias_or_id) { - | Ok(room_alias) => { - let room_id = match self - .services - .rooms - .alias - .resolve_local_alias(&room_alias) - .await - { - | Ok(room_id) => room_id, - | _ => { - debug!( - "We don't have this room alias to a room ID locally, \ - attempting to fetch room ID over federation" - ); - - match self - .services - .rooms - .alias - .resolve_alias(&room_alias) - .await - { - | Ok((room_id, servers)) => { - debug!( - %room_id, - ?servers, - "Got federation response fetching room ID for \ - {room}", - ); - room_id - }, - | Err(e) => { - warn!( - "Failed to resolve room alias {room} to a room \ - ID: {e}" - ); - continue; - }, - } - }, - }; - - room_ids.push(room_id); - }, - | Err(e) => { - warn!( - "Error parsing room \"{room}\" during bulk room banning, \ - ignoring error and logging here: {e}" - ); - continue; - }, - } - } - }, - | Err(e) => { - warn!( - "Error parsing room \"{room}\" during bulk room banning, ignoring error and \ - logging here: {e}" - ); - continue; - }, - } - } - - for room_id in room_ids { - debug!("Banned {room_id} successfully"); - room_ban_count = room_ban_count.saturating_add(1); - - debug!("Making all users leave the room {room_id} and forgetting it"); + info!("Making all users leave the room {room_id} and forgetting it"); let mut users = self .services .rooms @@ -286,7 +120,7 @@ async fn ban_list_of_rooms(&self) -> Result { .boxed(); while let Some(ref user_id) = users.next().await { - debug!( + info!( "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ evicting admins too)", ); @@ -301,7 +135,6 @@ async fn ban_list_of_rooms(&self) -> Result { self.services.rooms.state_cache.forget(&room_id, user_id); } - // remove any local aliases, ignore errors self.services .rooms .alias @@ -316,139 +149,307 @@ async fn ban_list_of_rooms(&self) -> Result { }) .await; - self.services.rooms.metadata.ban_room(&room_id, true); - // unpublish from room directory, ignore errors - self.services.rooms.directory.set_not_public(&room_id); - self.services.rooms.metadata.disable_room(&room_id, true); + self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory + self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins + self.services.rooms.metadata.disable_room(&room_id, true); // disable federation + + self.write_str( + "Room banned, removed all our local users, and disabled incoming federation with \ + room.", + ) + .await } - self.write_str(&format!( - "Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and \ - disabled incoming federation with the room." - )) - .await -} + async fn ban_list_of_rooms(&self) -> Result { + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details.",); + } -#[admin_command] -async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result { - let room_id = if room.is_room_id() { - let room_id = match RoomId::parse(&room) { - | Ok(room_id) => room_id, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" + let rooms_s = self + .body + .to_vec() + .drain(1..self.body.len().saturating_sub(1)) + .collect::>(); + + let admin_room_alias = &self.services.globals.admin_alias; + + let mut room_ban_count: usize = 0; + let mut room_ids: Vec = Vec::new(); + + for &room in &rooms_s { + match <&RoomOrAliasId>::try_from(room) { + | Ok(room_alias_or_id) => { + if let Ok(admin_room_id) = self.services.admin.get_admin_room().await { + if room.to_owned().eq(&admin_room_id) + || room.to_owned().eq(admin_room_alias) + { + warn!("User specified admin room in bulk ban list, ignoring"); + continue; + } + } + + if room_alias_or_id.is_room_id() { + let room_id = match RoomId::parse(room_alias_or_id) { + | Ok(room_id) => room_id, + | Err(e) => { + // ignore rooms we failed to parse + warn!( + "Error parsing room \"{room}\" during bulk room banning, \ + ignoring error and logging here: {e}" + ); + continue; + }, + }; + + room_ids.push(room_id.clone()); + } + + if room_alias_or_id.is_room_alias_id() { + match RoomAliasId::parse(room_alias_or_id) { + | Ok(room_alias) => { + let room_id = match self + .services + .rooms + .alias + .resolve_local_alias(&room_alias) + .await + { + | Ok(room_id) => room_id, + | _ => { + debug!( + "We don't have this room alias to a room ID \ + locally, attempting to fetch room ID over \ + federation" + ); + + match self + .services + .rooms + .alias + .resolve_alias(&room_alias) + .await + { + | Ok((room_id, servers)) => { + debug!( + %room_id, + ?servers, + "Got federation response fetching room ID for \ + {room}", + ); + room_id + }, + | Err(e) => { + warn!( + "Failed to resolve room alias {room} to a \ + room ID: {e}" + ); + continue; + }, + } + }, + }; + + room_ids.push(room_id); + }, + | Err(e) => { + warn!( + "Error parsing room \"{room}\" during bulk room banning, \ + ignoring error and logging here: {e}" + ); + continue; + }, + } + } + }, + | Err(e) => { + warn!( + "Error parsing room \"{room}\" during bulk room banning, ignoring error \ + and logging here: {e}" + ); + continue; + }, + } + } + + for room_id in room_ids { + debug!("Banned {room_id} successfully"); + room_ban_count = room_ban_count.saturating_add(1); + + debug!("Making all users leave the room {room_id} and forgetting it"); + let mut users = self + .services + .rooms + .state_cache + .room_members(&room_id) + .ready_filter(|user| self.services.globals.user_is_local(user)) + .boxed(); + + while let Some(ref user_id) = users.next().await { + debug!( + "Attempting leave for user {user_id} in room {room_id} (ignoring all \ + errors, evicting admins too)", ); - }, + + if let Err(e) = leave_room(self.services, user_id, &room_id, None) + .boxed() + .await + { + warn!("Failed to leave room: {e}"); + } + + self.services.rooms.state_cache.forget(&room_id, user_id); + } + + // remove any local aliases, ignore errors + self.services + .rooms + .alias + .local_aliases_for_room(&room_id) + .for_each(|local_alias| async move { + self.services + .rooms + .alias + .remove_alias(&local_alias, &self.services.globals.server_user) + .await + .ok(); + }) + .await; + + self.services.rooms.metadata.ban_room(&room_id, true); + // unpublish from room directory, ignore errors + self.services.rooms.directory.set_not_public(&room_id); + self.services.rooms.metadata.disable_room(&room_id, true); + } + + self.write_str(&format!( + "Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, \ + and disabled incoming federation with the room." + )) + .await + } + + async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result { + let room_id = if room.is_room_id() { + let room_id = match RoomId::parse(&room) { + | Ok(room_id) => room_id, + | Err(e) => { + return Err!( + "Failed to parse room ID {room}. Please note that this requires a full \ + room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`): {e}" + ); + }, + }; + + debug!("Room specified is a room ID, unbanning room ID"); + self.services.rooms.metadata.ban_room(&room_id, false); + + room_id.clone() + } else if room.is_room_alias_id() { + let room_alias = match RoomAliasId::parse(&room) { + | Ok(room_alias) => room_alias, + | Err(e) => { + return Err!( + "Failed to parse room ID {room}. Please note that this requires a full \ + room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`): {e}" + ); + }, + }; + + debug!( + "Room specified is not a room ID, attempting to resolve room alias to a room ID \ + locally, if not using get_alias_helper to fetch room ID remotely" + ); + + let room_id = match self + .services + .rooms + .alias + .resolve_local_alias(&room_alias) + .await + { + | Ok(room_id) => room_id, + | _ => { + debug!( + "We don't have this room alias to a room ID locally, attempting to \ + fetch room ID over federation" + ); + + match self.services.rooms.alias.resolve_alias(&room_alias).await { + | Ok((room_id, servers)) => { + debug!( + %room_id, + ?servers, + "Got federation response fetching room ID for room {room}" + ); + room_id + }, + | Err(e) => { + return Err!("Failed to resolve room alias {room} to a room ID: {e}"); + }, + } + }, + }; + + self.services.rooms.metadata.ban_room(&room_id, false); + + room_id + } else { + return Err!( + "Room specified is not a room ID or room alias. Please note that this requires \ + a full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ + (`#roomalias:example.com`)", + ); }; - debug!("Room specified is a room ID, unbanning room ID"); - self.services.rooms.metadata.ban_room(&room_id, false); + self.services.rooms.metadata.disable_room(&room_id, false); + self.write_str("Room unbanned and federation re-enabled.") + .await + } - room_id.clone() - } else if room.is_room_alias_id() { - let room_alias = match RoomAliasId::parse(&room) { - | Ok(room_alias) => room_alias, - | Err(e) => { - return Err!( - "Failed to parse room ID {room}. Please note that this requires a full room \ - ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`): {e}" - ); - }, - }; - - debug!( - "Room specified is not a room ID, attempting to resolve room alias to a room ID \ - locally, if not using get_alias_helper to fetch room ID remotely" - ); - - let room_id = match self + async fn list_banned_rooms(&self, no_details: bool) -> Result { + let room_ids: Vec = self .services .rooms - .alias - .resolve_local_alias(&room_alias) - .await - { - | Ok(room_id) => room_id, - | _ => { - debug!( - "We don't have this room alias to a room ID locally, attempting to fetch \ - room ID over federation" - ); + .metadata + .list_banned_rooms() + .map(Into::into) + .collect() + .await; - match self.services.rooms.alias.resolve_alias(&room_alias).await { - | Ok((room_id, servers)) => { - debug!( - %room_id, - ?servers, - "Got federation response fetching room ID for room {room}" - ); - room_id - }, - | Err(e) => { - return Err!("Failed to resolve room alias {room} to a room ID: {e}"); - }, + if room_ids.is_empty() { + return Err!("No rooms are banned."); + } + + let mut rooms = room_ids + .iter() + .stream() + .then(|room_id| get_room_info(self.services, room_id)) + .collect::>() + .await; + + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let num = rooms.len(); + + let body = rooms + .iter() + .map(|(id, members, name)| { + if no_details { + format!("{id}") + } else { + format!("{id}\tMembers: {members}\tName: {name}") } - }, - }; + }) + .collect::>() + .join("\n"); - self.services.rooms.metadata.ban_room(&room_id, false); - - room_id - } else { - return Err!( - "Room specified is not a room ID or room alias. Please note that this requires a \ - full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \ - (`#roomalias:example.com`)", - ); - }; - - self.services.rooms.metadata.disable_room(&room_id, false); - self.write_str("Room unbanned and federation re-enabled.") - .await -} - -#[admin_command] -async fn list_banned_rooms(&self, no_details: bool) -> Result { - let room_ids: Vec = self - .services - .rooms - .metadata - .list_banned_rooms() - .map(Into::into) - .collect() - .await; - - if room_ids.is_empty() { - return Err!("No rooms are banned."); + self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```")) + .await } - - let mut rooms = room_ids - .iter() - .stream() - .then(|room_id| get_room_info(self.services, room_id)) - .collect::>() - .await; - - rooms.sort_by_key(|r| r.1); - rooms.reverse(); - - let num = rooms.len(); - - let body = rooms - .iter() - .map(|(id, members, name)| { - if no_details { - format!("{id}") - } else { - format!("{id}\tMembers: {members}\tName: {name}") - } - }) - .collect::>() - .join("\n"); - - self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```")) - .await } diff --git a/src/admin/server/commands.rs b/src/admin/server/commands.rs index 462c047d6..744dde43e 100644 --- a/src/admin/server/commands.rs +++ b/src/admin/server/commands.rs @@ -7,243 +7,230 @@ use conduwuit::{ }; use futures::TryStreamExt; -use crate::admin_command; +impl crate::Context<'_> { + pub(super) async fn uptime(&self) -> Result { + let elapsed = self + .services + .server + .started + .elapsed() + .expect("standard duration"); -#[admin_command] -pub(super) async fn uptime(&self) -> Result { - let elapsed = self - .services - .server - .started - .elapsed() - .expect("standard duration"); + let result = time::pretty(elapsed); + self.write_str(&format!("{result}.")).await + } - let result = time::pretty(elapsed); - self.write_str(&format!("{result}.")).await -} + pub(super) async fn show_config(&self) -> Result { + self.bail_restricted()?; -#[admin_command] -pub(super) async fn show_config(&self) -> Result { - self.bail_restricted()?; + self.write_str(&format!("{}", *self.services.server.config)) + .await + } - self.write_str(&format!("{}", *self.services.server.config)) - .await -} + pub(super) async fn reload_config(&self, path: Option) -> Result { + // The path argument is only what's optionally passed via the admin command, + // so we need to merge it with the existing paths if any were given at startup. + let mut paths = Vec::new(); -#[admin_command] -pub(super) async fn reload_config(&self, path: Option) -> Result { - // The path argument is only what's optionally passed via the admin command, - // so we need to merge it with the existing paths if any were given at startup. - let mut paths = Vec::new(); + // Add previously saved paths to the argument list + self.services + .config + .config_paths + .clone() + .unwrap_or_default() + .iter() + .for_each(|p| paths.push(p.to_owned())); - // Add previously saved paths to the argument list - self.services - .config - .config_paths - .clone() - .unwrap_or_default() - .iter() - .for_each(|p| paths.push(p.to_owned())); - - // If a path is given, and it's not already in the list, - // add it last, so that it overrides earlier files - if let Some(p) = path { - if !paths.contains(&p) { - paths.push(p); + // If a path is given, and it's not already in the list, + // add it last, so that it overrides earlier files + if let Some(p) = path { + if !paths.contains(&p) { + paths.push(p); + } } + + self.services.config.reload(&paths)?; + + self.write_str(&format!("Successfully reconfigured from paths: {paths:?}")) + .await } - self.services.config.reload(&paths)?; + pub(super) async fn memory_usage(&self) -> Result { + let services_usage = self.services.memory_usage().await?; + let database_usage = self.services.db.db.memory_usage()?; + let allocator_usage = conduwuit::alloc::memory_usage() + .map_or(String::new(), |s| format!("\nAllocator:\n{s}")); - self.write_str(&format!("Successfully reconfigured from paths: {paths:?}")) + self.write_str(&format!( + "Services:\n{services_usage}\nDatabase:\n{database_usage}{allocator_usage}", + )) .await -} - -#[admin_command] -pub(super) async fn memory_usage(&self) -> Result { - let services_usage = self.services.memory_usage().await?; - let database_usage = self.services.db.db.memory_usage()?; - let allocator_usage = - conduwuit::alloc::memory_usage().map_or(String::new(), |s| format!("\nAllocator:\n{s}")); - - self.write_str(&format!( - "Services:\n{services_usage}\nDatabase:\n{database_usage}{allocator_usage}", - )) - .await -} - -#[admin_command] -pub(super) async fn clear_caches(&self) -> Result { - self.services.clear_cache().await; - - self.write_str("Done.").await -} - -#[admin_command] -pub(super) async fn list_backups(&self) -> Result { - self.services - .db - .db - .backup_list()? - .try_stream() - .try_for_each(|result| writeln!(self, "{result}")) - .await -} - -#[admin_command] -pub(super) async fn backup_database(&self) -> Result { - self.bail_restricted()?; - - let db = Arc::clone(&self.services.db); - let result = self - .services - .server - .runtime() - .spawn_blocking(move || match db.db.backup() { - | Ok(()) => "Done".to_owned(), - | Err(e) => format!("Failed: {e}"), - }) - .await?; - - let count = self.services.db.db.backup_count()?; - self.write_str(&format!("{result}. Currently have {count} backups.")) - .await -} - -#[admin_command] -pub(super) async fn admin_notice(&self, message: Vec) -> Result { - let message = message.join(" "); - self.services.admin.send_text(&message).await; - - self.write_str("Notice was sent to #admins").await -} - -#[admin_command] -pub(super) async fn reload_mods(&self) -> Result { - self.bail_restricted()?; - - self.services.server.reload()?; - - self.write_str("Reloading server...").await -} - -#[admin_command] -#[cfg(unix)] -pub(super) async fn restart(&self, force: bool) -> Result { - use conduwuit::utils::sys::current_exe_deleted; - - if !force && current_exe_deleted() { - return Err!( - "The server cannot be restarted because the executable changed. If this is expected \ - use --force to override." - ); } - self.services.server.restart()?; + pub(super) async fn clear_caches(&self) -> Result { + self.services.clear_cache().await; - self.write_str("Restarting server...").await -} - -#[admin_command] -pub(super) async fn shutdown(&self) -> Result { - self.bail_restricted()?; - - warn!("shutdown command"); - self.services.server.shutdown()?; - - self.write_str("Shutting down server...").await -} - -#[admin_command] -pub(super) async fn list_features(&self) -> Result { - let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES - .lock() - .expect("locked") - .values() - .flat_map(|f| f.iter()) - .collect::>(); - - enabled_features.sort_unstable(); - enabled_features.dedup(); - - let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES - .iter() - .flat_map(|(_, f)| f.iter()) - .collect::>(); - - available_features.sort_unstable(); - available_features.dedup(); - - let mut features = String::new(); - - for feature in available_features { - let active = enabled_features.contains(&feature); - let emoji = if active { "✅" } else { "❌" }; - let remark = if active { "[enabled]" } else { "" }; - writeln!(features, "{emoji} {feature} {remark}")?; + self.write_str("Done.").await } - self.write_str(&features).await -} - -#[admin_command] -pub(super) async fn build_info(&self) -> Result { - use conduwuit::build_metadata::built; + pub(super) async fn list_backups(&self) -> Result { + self.services + .db + .db + .backup_list()? + .try_stream() + .try_for_each(|result| writeln!(self, "{result}")) + .await + } - let mut info = String::new(); + pub(super) async fn backup_database(&self) -> Result { + self.bail_restricted()?; + + let db = Arc::clone(&self.services.db); + let result = self + .services + .server + .runtime() + .spawn_blocking(move || match db.db.backup() { + | Ok(()) => "Done".to_owned(), + | Err(e) => format!("Failed: {e}"), + }) + .await?; + + let count = self.services.db.db.backup_count()?; + self.write_str(&format!("{result}. Currently have {count} backups.")) + .await + } - // Version information - writeln!(info, "# Build Information\n")?; - writeln!(info, "**Version:** {}", built::PKG_VERSION)?; - writeln!(info, "**Package:** {}", built::PKG_NAME)?; - writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?; + pub(super) async fn admin_notice(&self, message: Vec) -> Result { + let message = message.join(" "); + self.services.admin.send_text(&message).await; - // Git information - writeln!(info, "\n## Git Information\n")?; - if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH { - writeln!(info, "**Commit Hash:** {hash}")?; - } - if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT { - writeln!(info, "**Commit Hash (short):** {hash}")?; - } - if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL { - writeln!(info, "**Repository:** {url}")?; + self.write_str("Notice was sent to #admins").await } - if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL { - writeln!(info, "**Commit URL:** {url}")?; + + pub(super) async fn reload_mods(&self) -> Result { + self.bail_restricted()?; + + self.services.server.reload()?; + + self.write_str("Reloading server...").await } - // Build environment - writeln!(info, "\n## Build Environment\n")?; - writeln!(info, "**Profile:** {}", built::PROFILE)?; - writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?; - writeln!(info, "**Debug:** {}", built::DEBUG)?; - writeln!(info, "**Target:** {}", built::TARGET)?; - writeln!(info, "**Host:** {}", built::HOST)?; + #[cfg(unix)] + pub(super) async fn restart(&self, force: bool) -> Result { + use conduwuit::utils::sys::current_exe_deleted; - // Rust compiler information - writeln!(info, "\n## Compiler Information\n")?; - writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?; - if !built::RUSTDOC_VERSION.is_empty() { - writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?; + if !force && current_exe_deleted() { + return Err!( + "The server cannot be restarted because the executable changed. If this is \ + expected use --force to override." + ); + } + + self.services.server.restart()?; + + self.write_str("Restarting server...").await } - // Target configuration - writeln!(info, "\n## Target Configuration\n")?; - writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?; - writeln!(info, "**OS:** {}", built::CFG_OS)?; - writeln!(info, "**Family:** {}", built::CFG_FAMILY)?; - writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?; - writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?; - if !built::CFG_ENV.is_empty() { - writeln!(info, "**Environment:** {}", built::CFG_ENV)?; + pub(super) async fn shutdown(&self) -> Result { + self.bail_restricted()?; + + warn!("shutdown command"); + self.services.server.shutdown()?; + + self.write_str("Shutting down server...").await } - // CI information - if let Some(ci) = built::CI_PLATFORM { - writeln!(info, "\n## CI Platform\n")?; - writeln!(info, "**Platform:** {ci}")?; + pub(super) async fn list_features(&self) -> Result { + let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES + .lock() + .expect("locked") + .values() + .flat_map(|f| f.iter()) + .collect::>(); + + enabled_features.sort_unstable(); + enabled_features.dedup(); + + let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES + .iter() + .flat_map(|(_, f)| f.iter()) + .collect::>(); + + available_features.sort_unstable(); + available_features.dedup(); + + let mut features = String::new(); + + for feature in available_features { + let active = enabled_features.contains(&feature); + let emoji = if active { "✅" } else { "❌" }; + let remark = if active { "[enabled]" } else { "" }; + writeln!(features, "{emoji} {feature} {remark}")?; + } + + self.write_str(&features).await } - self.write_str(&info).await + pub(super) async fn build_info(&self) -> Result { + use conduwuit::build_metadata::built; + + let mut info = String::new(); + + // Version information + writeln!(info, "# Build Information\n")?; + writeln!(info, "**Version:** {}", built::PKG_VERSION)?; + writeln!(info, "**Package:** {}", built::PKG_NAME)?; + writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?; + + // Git information + writeln!(info, "\n## Git Information\n")?; + if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH { + writeln!(info, "**Commit Hash:** {hash}")?; + } + if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT { + writeln!(info, "**Commit Hash (short):** {hash}")?; + } + if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL { + writeln!(info, "**Repository:** {url}")?; + } + if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL { + writeln!(info, "**Commit URL:** {url}")?; + } + + // Build environment + writeln!(info, "\n## Build Environment\n")?; + writeln!(info, "**Profile:** {}", built::PROFILE)?; + writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?; + writeln!(info, "**Debug:** {}", built::DEBUG)?; + writeln!(info, "**Target:** {}", built::TARGET)?; + writeln!(info, "**Host:** {}", built::HOST)?; + + // Rust compiler information + writeln!(info, "\n## Compiler Information\n")?; + writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?; + if !built::RUSTDOC_VERSION.is_empty() { + writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?; + } + + // Target configuration + writeln!(info, "\n## Target Configuration\n")?; + writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?; + writeln!(info, "**OS:** {}", built::CFG_OS)?; + writeln!(info, "**Family:** {}", built::CFG_FAMILY)?; + writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?; + writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?; + if !built::CFG_ENV.is_empty() { + writeln!(info, "**Environment:** {}", built::CFG_ENV)?; + } + + // CI information + if let Some(ci) = built::CI_PLATFORM { + writeln!(info, "\n## CI Platform\n")?; + writeln!(info, "**Platform:** {ci}")?; + } + + self.write_str(&info).await + } } diff --git a/src/admin/token/commands.rs b/src/admin/token/commands.rs index 4ff74a200..d6890e2cc 100644 --- a/src/admin/token/commands.rs +++ b/src/admin/token/commands.rs @@ -1,76 +1,76 @@ use conduwuit::{Err, Result, utils}; -use conduwuit_macros::admin_command; use futures::StreamExt; use service::registration_tokens::TokenExpires; -#[admin_command] -pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result { - let expires = { - if expires.immortal { - None - } else if let Some(max_uses) = expires.max_uses { - Some(TokenExpires::AfterUses(max_uses)) - } else if expires.once { - Some(TokenExpires::AfterUses(1)) - } else if let Some(max_age) = expires - .max_age - .as_deref() - .map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?)) - .transpose()? - { - Some(TokenExpires::AfterTime(max_age)) - } else { - unreachable!(); - } - }; +impl crate::Context<'_> { + pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result { + let expires = { + if expires.immortal { + None + } else if let Some(max_uses) = expires.max_uses { + Some(TokenExpires::AfterUses(max_uses)) + } else if expires.once { + Some(TokenExpires::AfterUses(1)) + } else if let Some(max_age) = expires + .max_age + .as_deref() + .map(|max_age| { + utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?) + }) + .transpose()? + { + Some(TokenExpires::AfterTime(max_age)) + } else { + unreachable!(); + } + }; - let (token, info) = self - .services - .registration_tokens - .issue_token(self.sender_or_service_user().into(), expires); + let (token, info) = self + .services + .registration_tokens + .issue_token(self.sender_or_service_user().into(), expires); - self.write_str(&format!( - "New registration token issued: `{token}`. {}.", - if let Some(expires) = info.expires { - format!("{expires}") - } else { - "Never expires".to_owned() - } - )) - .await -} - -#[admin_command] -pub(super) async fn revoke_token(&self, token: String) -> Result { - let Some(token) = self - .services - .registration_tokens - .validate_token(token) + self.write_str(&format!( + "New registration token issued: `{token}`. {}.", + if let Some(expires) = info.expires { + format!("{expires}") + } else { + "Never expires".to_owned() + } + )) .await - else { - return Err!("This token does not exist or has already expired."); - }; - - self.services.registration_tokens.revoke_token(token)?; - - self.write_str("Token revoked successfully.").await -} - -#[admin_command] -pub(super) async fn list_tokens(&self) -> Result { - let tokens: Vec<_> = self - .services - .registration_tokens - .iterate_tokens() - .collect() - .await; - - self.write_str(&format!("Found {} registration tokens:\n", tokens.len())) - .await?; - - for token in tokens { - self.write_str(&format!("- {token}\n")).await?; } - Ok(()) + pub(super) async fn revoke_token(&self, token: String) -> Result { + let Some(token) = self + .services + .registration_tokens + .validate_token(token) + .await + else { + return Err!("This token does not exist or has already expired."); + }; + + self.services.registration_tokens.revoke_token(token)?; + + self.write_str("Token revoked successfully.").await + } + + pub(super) async fn list_tokens(&self) -> Result { + let tokens: Vec<_> = self + .services + .registration_tokens + .iterate_tokens() + .collect() + .await; + + self.write_str(&format!("Found {} registration tokens:\n", tokens.len())) + .await?; + + for token in tokens { + self.write_str(&format!("- {token}\n")).await?; + } + + Ok(()) + } } diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 6a1ab7d3f..66589b4b6 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -26,266 +26,1093 @@ use ruma::{ use service::users::HashedPassword; use crate::{ - admin_command, get_room_info, + get_room_info, utils::{parse_active_local_user_id, parse_local_user_id}, }; const AUTO_GEN_PASSWORD_LENGTH: usize = 25; const BULK_JOIN_REASON: &str = "Bulk force joining this room as initiated by the server admin."; -#[admin_command] -pub(super) async fn list_users(&self) -> Result { - let users: Vec<_> = self - .services - .users - .list_local_users() - .map(|id| id.as_str().to_owned()) - .collect() - .await; +impl crate::Context<'_> { + pub(super) async fn list_users(&self) -> Result { + let users: Vec<_> = self + .services + .users + .list_local_users() + .map(|id| id.as_str().to_owned()) + .collect() + .await; - let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len()); - plain_msg += users.join("\n").as_str(); - plain_msg += "\n```"; + let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len()); + plain_msg += users.join("\n").as_str(); + plain_msg += "\n```"; - self.write_str(&plain_msg).await -} - -#[admin_command] -pub(super) async fn create_user(&self, username: String, password: Option) -> Result { - // Validate user id - let user_id = parse_local_user_id(self.services, &username)?; - - if let Err(e) = user_id.validate_strict() { - if self.services.config.emergency_password.is_none() { - return Err!("Username {user_id} contains disallowed characters or spaces: {e}"); - } + self.write_str(&plain_msg).await } - if self.services.users.exists(&user_id).await { - return Err!("User {user_id} already exists"); - } + pub(super) async fn create_user(&self, username: String, password: Option) -> Result { + // Validate user id + let user_id = parse_local_user_id(self.services, &username)?; - let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); - - // Create user - self.services - .users - .create(&user_id, Some(HashedPassword::new(&password)?)) - .await?; - - // Default to pretty displayname - let mut displayname = user_id.localpart().to_owned(); - - // If `new_user_displayname_suffix` is set, registration will push whatever - // content is set to the user's display name with a space before it - if !self - .services - .server - .config - .new_user_displayname_suffix - .is_empty() - { - write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?; - } - - self.services - .users - .set_displayname(&user_id, Some(displayname)); - - // Initial account data - self.services - .account_data - .update( - None, - &user_id, - ruma::events::GlobalAccountDataEventType::PushRules - .to_string() - .into(), - &serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new( - ruma::events::push_rules::PushRulesEventContent::new( - ruma::push::Ruleset::server_default(&user_id), - ), - )) - .unwrap(), - ) - .await?; - - if !self.services.server.config.auto_join_rooms.is_empty() { - for room in &self.services.server.config.auto_join_rooms { - let Ok(room_id) = self.services.rooms.alias.resolve(room).await else { - error!( - %user_id, - "Failed to resolve room alias to room ID when attempting to auto join {room}, skipping" - ); - continue; - }; - - if !self - .services - .rooms - .state_cache - .server_in_room(self.services.globals.server_name(), &room_id) - .await - { - warn!( - "Skipping room {room} to automatically join as we have never joined before." - ); - continue; + if let Err(e) = user_id.validate_strict() { + if self.services.config.emergency_password.is_none() { + return Err!("Username {user_id} contains disallowed characters or spaces: {e}"); } + } - if let Some(room_server_name) = room.server_name() { - match self + if self.services.users.exists(&user_id).await { + return Err!("User {user_id} already exists"); + } + + let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); + + // Create user + self.services + .users + .create(&user_id, Some(HashedPassword::new(&password)?)) + .await?; + + // Default to pretty displayname + let mut displayname = user_id.localpart().to_owned(); + + // If `new_user_displayname_suffix` is set, registration will push whatever + // content is set to the user's display name with a space before it + if !self + .services + .server + .config + .new_user_displayname_suffix + .is_empty() + { + write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?; + } + + self.services + .users + .set_displayname(&user_id, Some(displayname)); + + // Initial account data + self.services + .account_data + .update( + None, + &user_id, + ruma::events::GlobalAccountDataEventType::PushRules + .to_string() + .into(), + &serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new( + ruma::events::push_rules::PushRulesEventContent::new( + ruma::push::Ruleset::server_default(&user_id), + ), + )) + .unwrap(), + ) + .await?; + + if !self.services.server.config.auto_join_rooms.is_empty() { + for room in &self.services.server.config.auto_join_rooms { + let Ok(room_id) = self.services.rooms.alias.resolve(room).await else { + error!( + %user_id, + "Failed to resolve room alias to room ID when attempting to auto join {room}, skipping" + ); + continue; + }; + + if !self .services .rooms - .membership - .join_room( - &user_id, - &room_id, - Some("Automatically joining this room upon registration".to_owned()), - &[ - self.services.globals.server_name().to_owned(), - room_server_name.to_owned(), - ], - ) + .state_cache + .server_in_room(self.services.globals.server_name(), &room_id) .await { - | Ok(_response) => { - info!("Automatically joined room {room} for user {user_id}"); - }, - | Err(e) => { - // don't return this error so we don't fail registrations - error!( - "Failed to automatically join room {room} for user {user_id}: {e}" - ); - self.services - .admin - .send_text(&format!( + warn!( + "Skipping room {room} to automatically join as we have never joined \ + before." + ); + continue; + } + + if let Some(room_server_name) = room.server_name() { + match self + .services + .rooms + .membership + .join_room( + &user_id, + &room_id, + Some("Automatically joining this room upon registration".to_owned()), + &[ + self.services.globals.server_name().to_owned(), + room_server_name.to_owned(), + ], + ) + .await + { + | Ok(_response) => { + info!("Automatically joined room {room} for user {user_id}"); + }, + | Err(e) => { + // don't return this error so we don't fail registrations + error!( "Failed to automatically join room {room} for user {user_id}: \ {e}" - )) - .await; - }, + ); + self.services + .admin + .send_text(&format!( + "Failed to automatically join room {room} for user \ + {user_id}: {e}" + )) + .await; + }, + } } } } - } - // we dont add a device since we're not the user, just the creator + // we dont add a device since we're not the user, just the creator - // Make the first user to register an administrator and disable first-run mode. - self.services.firstrun.empower_first_user(&user_id).await?; + // Make the first user to register an administrator and disable first-run mode. + self.services.firstrun.empower_first_user(&user_id).await?; - self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`")) + self.write_str(&format!( + "Created user with user_id: {user_id} and password: `{password}`" + )) .await -} - -#[admin_command] -pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result { - // Validate user id - let user_id = parse_local_user_id(self.services, &user_id)?; - - // don't deactivate the server service account - if user_id == self.services.globals.server_user { - return Err!("Not allowed to deactivate the server service account.",); } - self.services.users.deactivate_account(&user_id).await?; + pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result { + // Validate user id + let user_id = parse_local_user_id(self.services, &user_id)?; - if !no_leave_rooms { + // don't deactivate the server service account + if user_id == self.services.globals.server_user { + return Err!("Not allowed to deactivate the server service account.",); + } + + self.services.users.deactivate_account(&user_id).await?; + + if !no_leave_rooms { + self.services + .admin + .send_text(&format!("Making {user_id} leave all rooms after deactivation...")) + .await; + + let all_joined_rooms: Vec = self + .services + .rooms + .state_cache + .rooms_joined(&user_id) + .map(Into::into) + .collect() + .await; + + full_user_deactivate(self.services, &user_id, &all_joined_rooms) + .boxed() + .await?; + } + + self.write_str(&format!("User {user_id} has been deactivated")) + .await + } + + pub(super) async fn suspend(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + + if user_id == self.services.globals.server_user { + return Err!("Not allowed to suspend the server service account.",); + } + + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + if self.services.users.is_admin(&user_id).await { + return Err!("Admin users cannot be suspended."); + } + // TODO: Record the actual user that sent the suspension where possible self.services - .admin - .send_text(&format!("Making {user_id} leave all rooms after deactivation...")) + .users + .suspend_account(&user_id, self.sender_or_service_user()) .await; - let all_joined_rooms: Vec = self + self.write_str(&format!("User {user_id} has been suspended.")) + .await + } + + pub(super) async fn unsuspend(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + + if user_id == self.services.globals.server_user { + return Err!("Not allowed to unsuspend the server service account.",); + } + + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + self.services.users.unsuspend_account(&user_id).await; + + self.write_str(&format!("User {user_id} has been unsuspended.")) + .await + } + + pub(super) async fn reset_password( + &self, + logout: bool, + username: String, + password: Option, + ) -> Result { + let user_id = parse_local_user_id(self.services, &username)?; + + if user_id == self.services.globals.server_user { + return Err!( + "Not allowed to set the password for the server account. Please use the \ + emergency password config option.", + ); + } + + let new_password = + password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); + + self.services + .users + .set_password(&user_id, Some(HashedPassword::new(&new_password)?)); + + self.write_str(&format!( + "Successfully reset the password for user {user_id}: `{new_password}`" + )) + .await?; + + if logout { + self.services + .users + .all_device_ids(&user_id) + .for_each(async |device_id| { + self.services + .users + .remove_device(&user_id, &device_id) + .await; + }) + .await; + write!(self, "\nAll existing sessions have been logged out.").await?; + } + + Ok(()) + } + + pub(super) async fn issue_password_reset_link(&self, username: String) -> Result { + use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM}; + + self.bail_restricted()?; + + let mut reset_url = self + .services + .config + .get_client_domain() + .join(PASSWORD_RESET_PATH) + .unwrap(); + + let user_id = parse_local_user_id(self.services, &username)?; + let token = self.services.password_reset.issue_token(user_id).await?; + reset_url + .query_pairs_mut() + .append_pair(RESET_TOKEN_QUERY_PARAM, &token.token); + + self.write_str(&format!("Password reset link issued for {username}: {reset_url}")) + .await?; + + Ok(()) + } + + pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result { + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details.",); + } + + let usernames = self + .body + .to_vec() + .drain(1..self.body.len().saturating_sub(1)) + .collect::>(); + + let mut user_ids: Vec = Vec::with_capacity(usernames.len()); + let mut admins = Vec::new(); + + for username in usernames { + match parse_active_local_user_id(self.services, username).await { + | Err(e) => { + self.services + .admin + .send_text(&format!( + "{username} is not a valid username, skipping over: {e}" + )) + .await; + + continue; + }, + | Ok(user_id) => { + if self.services.users.is_admin(&user_id).await && !force { + self.services + .admin + .send_text(&format!( + "{username} is an admin and --force is not set, skipping over" + )) + .await; + + admins.push(username); + continue; + } + + // don't deactivate the server service account + if user_id == self.services.globals.server_user { + self.services + .admin + .send_text(&format!( + "{username} is the server service account, skipping over" + )) + .await; + + continue; + } + + user_ids.push(user_id); + }, + } + } + + let mut deactivation_count: usize = 0; + + for user_id in user_ids { + match self.services.users.deactivate_account(&user_id).await { + | Err(e) => { + self.services + .admin + .send_text(&format!("Failed deactivating user: {e}")) + .await; + }, + | Ok(()) => { + deactivation_count = deactivation_count.saturating_add(1); + if !no_leave_rooms { + info!( + "Forcing user {user_id} to leave all rooms apart of deactivate-all" + ); + let all_joined_rooms: Vec = self + .services + .rooms + .state_cache + .rooms_joined(&user_id) + .map(Into::into) + .collect() + .await; + + full_user_deactivate(self.services, &user_id, &all_joined_rooms) + .boxed() + .await?; + } + }, + } + } + + if admins.is_empty() { + write!(self, "Deactivated {deactivation_count} accounts.") + } else { + write!( + self, + "Deactivated {deactivation_count} accounts.\nSkipped admin accounts: {}. Use \ + --force to deactivate admin accounts", + admins.join(", ") + ) + } + .await + } + + pub(super) async fn list_invited_rooms(&self, user_id: String) -> Result { + // Validate user id + let user_id = parse_local_user_id(self.services, &user_id)?; + + let mut rooms: Vec<((OwnedRoomId, u64, String), Result)> = self + .services + .rooms + .state_cache + .rooms_invited(&user_id) + .then(async |(room_id, _)| { + let sender = self + .services + .rooms + .state_cache + .invite_sender(&user_id, &room_id) + .await; + (get_room_info(self.services, &room_id).await, sender) + }) + .collect() + .await; + + if rooms.is_empty() { + return Err!("User is not invited to any rooms."); + } + + rooms.sort_by_key(|r| r.0.1); + rooms.reverse(); + + let body = rooms + .iter() + .map(|((id, members, name), sender)| match sender { + | Ok(user_id) => + format!("{id}\tInviter: {user_id}\tMembers: {members}\tName: {name}"), + | Err(_) => format!("{id}\tMembers: {members}\tName: {name}"), + }) + .collect::>() + .join("\n"); + + self.write_str(&format!( + "Rooms {user_id} is Invited to ({}):\n```\n{body}\n```", + rooms.len() + )) + .await + } + + pub(super) async fn reject_all_invites(&self, user_id: String) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + + let fails = self + .services + .rooms + .state_cache + .rooms_invited(&user_id) + .filter_map(async |(room_id, _)| { + match leave_room(self.services, &user_id, &room_id, None).await { + | Err(ref e) => { + warn!(%user_id, "Failed to leave {room_id}: {e}"); + Some(()) + }, + | Ok(()) => None, + } + }) + .count() + .await; + + if fails > 0 { + return Err!("{fails} invites could not be rejected"); + } + + self.write_str("Successfully rejected all invites.").await + } + + pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result { + // Validate user id + let user_id = parse_local_user_id(self.services, &user_id)?; + + let mut rooms: Vec<(OwnedRoomId, u64, String)> = self .services .rooms .state_cache .rooms_joined(&user_id) - .map(Into::into) + .then(async |room_id| get_room_info(self.services, &room_id).await) .collect() .await; - full_user_deactivate(self.services, &user_id, &all_joined_rooms) + if rooms.is_empty() { + return Err!("User is not in any rooms."); + } + + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + + let body = rooms + .iter() + .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) + .collect::>() + .join("\n"); + + self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len())) + .await + } + + pub(super) async fn force_join_list_of_local_users( + &self, + room_id: OwnedRoomOrAliasId, + yes_i_want_to_do_this: bool, + ) -> Result { + if self.body.len() < 2 + || !self.body[0].trim().starts_with("```") + || self.body.last().unwrap_or(&"").trim() != "```" + { + return Err!("Expected code block in command body. Add --help for details.",); + } + + if !yes_i_want_to_do_this { + return Err!( + "You must pass the --yes-i-want-to-do-this-flag to ensure you really want to \ + force bulk join all specified local users.", + ); + } + + let server_admins = self.services.admin.get_admins().await; + + if server_admins.is_empty() { + return Err!("There are no admins set for this server."); + } + + let (room_id, servers) = self + .services + .rooms + .alias + .resolve_with_servers(&room_id, None) + .await?; + + if !self + .services + .rooms + .state_cache + .server_in_room(self.services.globals.server_name(), &room_id) + .await + { + return Err!("We are not joined in this room."); + } + + if !self + .services + .rooms + .state_cache + .room_members(&room_id) + .ready_any(|user_id| server_admins.contains(&user_id)) + .await + { + return Err!("There is not a single server admin in the room.",); + } + + let usernames = self + .body + .to_vec() + .drain(1..self.body.len().saturating_sub(1)) + .collect::>(); + + let mut user_ids: Vec = Vec::with_capacity(usernames.len()); + + for username in usernames { + match parse_active_local_user_id(self.services, username).await { + | Ok(user_id) => { + // don't make the server service account join + if user_id == self.services.globals.server_user { + self.services + .admin + .send_text(&format!( + "{username} is the server service account, skipping over" + )) + .await; + + continue; + } + + user_ids.push(user_id); + }, + | Err(e) => { + self.services + .admin + .send_text(&format!( + "{username} is not a valid username, skipping over: {e}" + )) + .await; + + continue; + }, + } + } + + let mut failed_joins: usize = 0; + let mut successful_joins: usize = 0; + + for user_id in user_ids { + match self + .services + .rooms + .membership + .join_room(&user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers) + .await + { + | Ok(_res) => { + successful_joins = successful_joins.saturating_add(1); + }, + | Err(e) => { + debug_warn!( + "Failed force joining {user_id} to {room_id} during bulk join: {e}" + ); + failed_joins = failed_joins.saturating_add(1); + }, + } + } + + self.write_str(&format!( + "{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \ + failed.", + )) + .await + } + + pub(super) async fn force_join_all_local_users( + &self, + room_id: OwnedRoomOrAliasId, + yes_i_want_to_do_this: bool, + ) -> Result { + if !yes_i_want_to_do_this { + return Err!( + "You must pass the --yes-i-want-to-do-this-flag to ensure you really want to \ + force bulk join all local users.", + ); + } + + let server_admins = self.services.admin.get_admins().await; + + if server_admins.is_empty() { + return Err!("There are no admins set for this server."); + } + + let (room_id, servers) = self + .services + .rooms + .alias + .resolve_with_servers(&room_id, None) + .await?; + + if !self + .services + .rooms + .state_cache + .server_in_room(self.services.globals.server_name(), &room_id) + .await + { + return Err!("We are not joined in this room."); + } + + if !self + .services + .rooms + .state_cache + .room_members(&room_id) + .ready_any(|user_id| server_admins.contains(&user_id)) + .await + { + return Err!("There is not a single server admin in the room.",); + } + + let mut failed_joins: usize = 0; + let mut successful_joins: usize = 0; + + for user_id in &self + .services + .users + .list_local_users() + .collect::>() + .await + { + match self + .services + .rooms + .membership + .join_room(user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers) + .await + { + | Ok(_res) => { + successful_joins = successful_joins.saturating_add(1); + }, + | Err(e) => { + debug_warn!( + "Failed force joining {user_id} to {room_id} during bulk join: {e}" + ); + failed_joins = failed_joins.saturating_add(1); + }, + } + } + + self.write_str(&format!( + "{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \ + failed.", + )) + .await + } + + pub(super) async fn force_join_room( + &self, + user_id: String, + room_id: OwnedRoomOrAliasId, + via: Option, + ) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + let (room_id, mut servers) = self + .services + .rooms + .alias + .resolve_with_servers(&room_id, None) + .await?; + if let Some(via) = via.map(ServerName::parse).transpose()? { + servers.retain(|n| *n != via); + servers.insert(0, via); + } + + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + self.services + .rooms + .membership + .join_room(&user_id, &room_id, None, &servers) + .await?; + + self.write_str(&format!("{user_id} has been joined to {room_id}.")) + .await + } + + pub(super) async fn force_leave_room( + &self, + user_id: String, + room_id: OwnedRoomOrAliasId, + ) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + let room_id = self.services.rooms.alias.resolve(&room_id).await?; + + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + + if !self + .services + .rooms + .state_cache + .is_joined(&user_id, &room_id) + .await + { + return Err!("{user_id} is not joined in the room"); + } + + leave_room(self.services, &user_id, &room_id, None) .boxed() .await?; + + self.write_str(&format!("{user_id} has left {room_id}.")) + .await } - self.write_str(&format!("User {user_id} has been deactivated")) - .await -} + pub(super) async fn force_demote( + &self, + user_id: String, + room_id: OwnedRoomOrAliasId, + ) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + let room_id = self.services.rooms.alias.resolve(&room_id).await?; -#[admin_command] -pub(super) async fn suspend(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - - if user_id == self.services.globals.server_user { - return Err!("Not allowed to suspend the server service account.",); - } - - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - if self.services.users.is_admin(&user_id).await { - return Err!("Admin users cannot be suspended."); - } - // TODO: Record the actual user that sent the suspension where possible - self.services - .users - .suspend_account(&user_id, self.sender_or_service_user()) - .await; - - self.write_str(&format!("User {user_id} has been suspended.")) - .await -} - -#[admin_command] -pub(super) async fn unsuspend(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - - if user_id == self.services.globals.server_user { - return Err!("Not allowed to unsuspend the server service account.",); - } - - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - self.services.users.unsuspend_account(&user_id).await; - - self.write_str(&format!("User {user_id} has been unsuspended.")) - .await -} - -#[admin_command] -pub(super) async fn reset_password( - &self, - logout: bool, - username: String, - password: Option, -) -> Result { - let user_id = parse_local_user_id(self.services, &username)?; - - if user_id == self.services.globals.server_user { - return Err!( - "Not allowed to set the password for the server account. Please use the emergency \ - password config option.", + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" ); + + let state_lock = self.services.rooms.state.mutex.lock(room_id.as_str()).await; + + let mut room_power_levels = self + .services + .rooms + .state_accessor + .get_room_power_levels(&room_id) + .await; + + let user_can_demote_self = + room_power_levels.user_can_change_user_power_level(&user_id, &user_id); + + if !user_can_demote_self { + return Err!("User is not allowed to modify their own power levels in the room.",); + } + + room_power_levels.users.remove(&user_id); + + let event_id = self + .services + .rooms + .timeline + .build_and_append_pdu( + PartialPdu::state( + String::new(), + &RoomPowerLevelsEventContent::try_from(room_power_levels) + .expect("PLs should be valid for room version"), + ), + &user_id, + Some(&room_id), + &state_lock, + ) + .await?; + + self.write_str(&format!( + "User {user_id} demoted themselves to the room default power level in {room_id} - \ + {event_id}" + )) + .await } - let new_password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); + pub(super) async fn make_user_admin(&self, user_id: String) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); - self.services - .users - .set_password(&user_id, Some(HashedPassword::new(&new_password)?)); + self.services + .admin + .make_user_admin(&user_id) + .boxed() + .await?; - self.write_str(&format!( - "Successfully reset the password for user {user_id}: `{new_password}`" - )) - .await?; + self.write_str(&format!("{user_id} has been granted admin privileges.")) + .await + } - if logout { + pub(super) async fn put_room_tag( + &self, + user_id: String, + room_id: OwnedRoomId, + tag: String, + ) -> Result { + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + + let mut tags_event = self + .services + .account_data + .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) + .await + .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); + + tags_event + .content + .tags + .insert(tag.clone().into(), TagInfo::new()); + + self.services + .account_data + .update( + Some(&room_id), + &user_id, + RoomAccountDataEventType::Tag, + &serde_json::to_value(tags_event).expect("to json value always works"), + ) + .await?; + + self.write_str(&format!( + "Successfully updated room account data for {user_id} and room {room_id} with tag \ + {tag}" + )) + .await + } + + pub(super) async fn delete_room_tag( + &self, + user_id: String, + room_id: OwnedRoomId, + tag: String, + ) -> Result { + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + + let mut tags_event = self + .services + .account_data + .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) + .await + .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); + + tags_event.content.tags.remove(&tag.clone().into()); + + self.services + .account_data + .update( + Some(&room_id), + &user_id, + RoomAccountDataEventType::Tag, + &serde_json::to_value(tags_event).expect("to json value always works"), + ) + .await?; + + self.write_str(&format!( + "Successfully updated room account data for {user_id} and room {room_id}, deleting \ + room tag {tag}" + )) + .await + } + + pub(super) async fn get_room_tags(&self, user_id: String, room_id: OwnedRoomId) -> Result { + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + + let tags_event = self + .services + .account_data + .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) + .await + .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); + + self.write_str(&format!("```\n{:#?}\n```", tags_event.content.tags)) + .await + } + + pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result { + let Ok(event) = self + .services + .rooms + .timeline + .get_non_outlier_pdu(&event_id) + .await + else { + return Err!("Event does not exist in our database."); + }; + + if event.is_redacted() { + return Err!("Event is already redacted."); + } + + if !self.services.globals.user_is_local(event.sender()) { + return Err!("This command only works on local users."); + } + + let reason = format!( + "The administrator(s) of {} has redacted this user's message.", + self.services.globals.server_name() + ); + + let redaction_event_id = { + let state_lock = self + .services + .rooms + .state + .mutex + .lock(event.room_id_or_hash().as_str()) + .await; + + self.services + .rooms + .timeline + .build_and_append_pdu( + PartialPdu { + redacts: Some(event.event_id().to_owned()), + ..PartialPdu::timeline(&assign!(RoomRedactionEventContent::new_v1(), { + redacts: Some(event.event_id().to_owned()), + reason: Some(reason), + })) + }, + event.sender(), + Some(&event.room_id_or_hash()), + &state_lock, + ) + .await? + }; + + self.write_str(&format!( + "Successfully redacted event. Redaction event ID: {redaction_event_id}" + )) + .await + } + + pub(super) async fn force_leave_remote_room( + &self, + user_id: String, + room_id: OwnedRoomOrAliasId, + via: Option, + ) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + let (room_id, vias_raw) = self + .services + .rooms + .alias + .resolve_with_servers( + &room_id, + if let Some(v) = via.clone() { + Some(vec![ServerName::parse(v)?]) + } else { + None + }, + ) + .await?; + + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + let mut vias: HashSet = HashSet::new(); + if let Some(via) = via { + vias.insert(ServerName::parse(via)?); + } + for server in vias_raw { + vias.insert(server); + } + remote_leave_room(self.services, &user_id, &room_id, None, vias) + .boxed() + .await?; + + self.write_str(&format!("{user_id} successfully left {room_id} via remote server.")) + .await + } + + pub(super) async fn lock(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + if user_id == self.services.globals.server_user { + return Err!("Not allowed to lock the server service account.",); + } + + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + if self.services.users.is_admin(&user_id).await { + return Err!("Admin users cannot be locked."); + } + self.services + .users + .lock_account(&user_id, self.sender_or_service_user()) + .await; + + self.write_str(&format!("User {user_id} has been locked.")) + .await + } + + pub(super) async fn unlock(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + self.services.users.unlock_account(&user_id).await; + + self.write_str(&format!("User {user_id} has been unlocked.")) + .await + } + + pub(super) async fn logout(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + if user_id == self.services.globals.server_user { + return Err!("Not allowed to log out the server service account.",); + } + + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + if self.services.users.is_admin(&user_id).await { + return Err!("You cannot forcefully log out admin users."); + } self.services .users .all_device_ids(&user_id) @@ -296,988 +1123,158 @@ pub(super) async fn reset_password( .await; }) .await; - write!(self, "\nAll existing sessions have been logged out.").await?; + self.write_str(&format!("User {user_id} has been logged out from all devices.")) + .await } - Ok(()) -} - -#[admin_command] -pub(super) async fn issue_password_reset_link(&self, username: String) -> Result { - use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM}; - - self.bail_restricted()?; - - let mut reset_url = self - .services - .config - .get_client_domain() - .join(PASSWORD_RESET_PATH) - .unwrap(); - - let user_id = parse_local_user_id(self.services, &username)?; - let token = self.services.password_reset.issue_token(user_id).await?; - reset_url - .query_pairs_mut() - .append_pair(RESET_TOKEN_QUERY_PARAM, &token.token); - - self.write_str(&format!("Password reset link issued for {username}: {reset_url}")) - .await?; - - Ok(()) -} - -#[admin_command] -pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result { - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details.",); - } - - let usernames = self - .body - .to_vec() - .drain(1..self.body.len().saturating_sub(1)) - .collect::>(); - - let mut user_ids: Vec = Vec::with_capacity(usernames.len()); - let mut admins = Vec::new(); - - for username in usernames { - match parse_active_local_user_id(self.services, username).await { - | Err(e) => { - self.services - .admin - .send_text(&format!("{username} is not a valid username, skipping over: {e}")) - .await; - - continue; - }, - | Ok(user_id) => { - if self.services.users.is_admin(&user_id).await && !force { - self.services - .admin - .send_text(&format!( - "{username} is an admin and --force is not set, skipping over" - )) - .await; - - admins.push(username); - continue; - } - - // don't deactivate the server service account - if user_id == self.services.globals.server_user { - self.services - .admin - .send_text(&format!( - "{username} is the server service account, skipping over" - )) - .await; - - continue; - } - - user_ids.push(user_id); - }, - } - } - - let mut deactivation_count: usize = 0; - - for user_id in user_ids { - match self.services.users.deactivate_account(&user_id).await { - | Err(e) => { - self.services - .admin - .send_text(&format!("Failed deactivating user: {e}")) - .await; - }, - | Ok(()) => { - deactivation_count = deactivation_count.saturating_add(1); - if !no_leave_rooms { - info!("Forcing user {user_id} to leave all rooms apart of deactivate-all"); - let all_joined_rooms: Vec = self - .services - .rooms - .state_cache - .rooms_joined(&user_id) - .map(Into::into) - .collect() - .await; - - full_user_deactivate(self.services, &user_id, &all_joined_rooms) - .boxed() - .await?; - } - }, - } - } - - if admins.is_empty() { - write!(self, "Deactivated {deactivation_count} accounts.") - } else { - write!( - self, - "Deactivated {deactivation_count} accounts.\nSkipped admin accounts: {}. Use \ - --force to deactivate admin accounts", - admins.join(", ") - ) - } - .await -} - -#[admin_command] -pub(super) async fn list_invited_rooms(&self, user_id: String) -> Result { - // Validate user id - let user_id = parse_local_user_id(self.services, &user_id)?; - - let mut rooms: Vec<((OwnedRoomId, u64, String), Result)> = self - .services - .rooms - .state_cache - .rooms_invited(&user_id) - .then(async |(room_id, _)| { - let sender = self - .services - .rooms - .state_cache - .invite_sender(&user_id, &room_id) - .await; - (get_room_info(self.services, &room_id).await, sender) - }) - .collect() - .await; - - if rooms.is_empty() { - return Err!("User is not invited to any rooms."); - } - - rooms.sort_by_key(|r| r.0.1); - rooms.reverse(); - - let body = rooms - .iter() - .map(|((id, members, name), sender)| match sender { - | Ok(user_id) => - format!("{id}\tInviter: {user_id}\tMembers: {members}\tName: {name}"), - | Err(_) => format!("{id}\tMembers: {members}\tName: {name}"), - }) - .collect::>() - .join("\n"); - - self.write_str(&format!("Rooms {user_id} is Invited to ({}):\n```\n{body}\n```", rooms.len())) - .await -} - -#[admin_command] -pub(super) async fn reject_all_invites(&self, user_id: String) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - - let fails = self - .services - .rooms - .state_cache - .rooms_invited(&user_id) - .filter_map(async |(room_id, _)| { - match leave_room(self.services, &user_id, &room_id, None).await { - | Err(ref e) => { - warn!(%user_id, "Failed to leave {room_id}: {e}"); - Some(()) - }, - | Ok(()) => None, - } - }) - .count() - .await; - - if fails > 0 { - return Err!("{fails} invites could not be rejected"); - } - - self.write_str("Successfully rejected all invites.").await -} - -#[admin_command] -pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result { - // Validate user id - let user_id = parse_local_user_id(self.services, &user_id)?; - - let mut rooms: Vec<(OwnedRoomId, u64, String)> = self - .services - .rooms - .state_cache - .rooms_joined(&user_id) - .then(async |room_id| get_room_info(self.services, &room_id).await) - .collect() - .await; - - if rooms.is_empty() { - return Err!("User is not in any rooms."); - } - - rooms.sort_by_key(|r| r.1); - rooms.reverse(); - - let body = rooms - .iter() - .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) - .collect::>() - .join("\n"); - - self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len())) - .await -} - -#[admin_command] -pub(super) async fn force_join_list_of_local_users( - &self, - room_id: OwnedRoomOrAliasId, - yes_i_want_to_do_this: bool, -) -> Result { - if self.body.len() < 2 - || !self.body[0].trim().starts_with("```") - || self.body.last().unwrap_or(&"").trim() != "```" - { - return Err!("Expected code block in command body. Add --help for details.",); - } - - if !yes_i_want_to_do_this { - return Err!( - "You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force \ - bulk join all specified local users.", + pub(super) async fn disable_login(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" ); - } - - let server_admins = self.services.admin.get_admins().await; - - if server_admins.is_empty() { - return Err!("There are no admins set for this server."); - } - - let (room_id, servers) = self - .services - .rooms - .alias - .resolve_with_servers(&room_id, None) - .await?; - - if !self - .services - .rooms - .state_cache - .server_in_room(self.services.globals.server_name(), &room_id) - .await - { - return Err!("We are not joined in this room."); - } - - if !self - .services - .rooms - .state_cache - .room_members(&room_id) - .ready_any(|user_id| server_admins.contains(&user_id)) - .await - { - return Err!("There is not a single server admin in the room.",); - } - - let usernames = self - .body - .to_vec() - .drain(1..self.body.len().saturating_sub(1)) - .collect::>(); - - let mut user_ids: Vec = Vec::with_capacity(usernames.len()); - - for username in usernames { - match parse_active_local_user_id(self.services, username).await { - | Ok(user_id) => { - // don't make the server service account join - if user_id == self.services.globals.server_user { - self.services - .admin - .send_text(&format!( - "{username} is the server service account, skipping over" - )) - .await; - - continue; - } - - user_ids.push(user_id); - }, - | Err(e) => { - self.services - .admin - .send_text(&format!("{username} is not a valid username, skipping over: {e}")) - .await; - - continue; - }, + if user_id == self.services.globals.server_user { + return Err!("Not allowed to disable login for the server service account.",); } + + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + if self.services.users.is_admin(&user_id).await { + return Err!("Admin users cannot have their login disallowed."); + } + self.services.users.disable_login(&user_id); + + self.write_str(&format!( + "{user_id} can no longer log in. Their existing sessions remain unaffected." + )) + .await } - let mut failed_joins: usize = 0; - let mut successful_joins: usize = 0; + pub(super) async fn enable_login(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; + assert!( + self.services.globals.user_is_local(&user_id), + "Parsed user_id must be a local user" + ); + if !self.services.users.exists(&user_id).await { + return Err!("User {user_id} does not exist."); + } + self.services.users.enable_login(&user_id); + + self.write_str(&format!("{user_id} can now log in.")).await + } + + pub(super) async fn get_email(&self, user_id: String) -> Result { + self.bail_restricted()?; + let user_id = parse_local_user_id(self.services, &user_id)?; - for user_id in user_ids { match self .services - .rooms - .membership - .join_room(&user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers) + .threepid + .get_email_for_localpart(user_id.localpart()) .await { - | Ok(_res) => { - successful_joins = successful_joins.saturating_add(1); - }, - | Err(e) => { - debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}"); - failed_joins = failed_joins.saturating_add(1); - }, + | Some(email) => + self.write_str(&format!("{user_id} has the associated email address {email}.")) + .await, + | None => + self.write_str(&format!("{user_id} has no associated email address.")) + .await, } } - self.write_str(&format!( - "{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \ - failed.", - )) - .await -} + pub(super) async fn get_user_by_email(&self, email: String) -> Result { + self.bail_restricted()?; -#[admin_command] -pub(super) async fn force_join_all_local_users( - &self, - room_id: OwnedRoomOrAliasId, - yes_i_want_to_do_this: bool, -) -> Result { - if !yes_i_want_to_do_this { - return Err!( - "You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force \ - bulk join all local users.", - ); - } + let Ok(email) = Address::try_from(email) else { + return Err!("Invalid email address."); + }; - let server_admins = self.services.admin.get_admins().await; + match self.services.threepid.get_localpart_for_email(&email).await { + | Some(localpart) => { + let user_id = UserId::parse(format!( + "@{localpart}:{}", + self.services.globals.server_name() + )) + .unwrap(); - if server_admins.is_empty() { - return Err!("There are no admins set for this server."); - } - - let (room_id, servers) = self - .services - .rooms - .alias - .resolve_with_servers(&room_id, None) - .await?; - - if !self - .services - .rooms - .state_cache - .server_in_room(self.services.globals.server_name(), &room_id) - .await - { - return Err!("We are not joined in this room."); - } - - if !self - .services - .rooms - .state_cache - .room_members(&room_id) - .ready_any(|user_id| server_admins.contains(&user_id)) - .await - { - return Err!("There is not a single server admin in the room.",); - } - - let mut failed_joins: usize = 0; - let mut successful_joins: usize = 0; - - for user_id in &self - .services - .users - .list_local_users() - .collect::>() - .await - { - match self - .services - .rooms - .membership - .join_room(user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers) - .await - { - | Ok(_res) => { - successful_joins = successful_joins.saturating_add(1); - }, - | Err(e) => { - debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}"); - failed_joins = failed_joins.saturating_add(1); + self.write_str(&format!("{email} belongs to {user_id}.")) + .await }, + | None => + self.write_str(&format!("No user has {email} as their email address.")) + .await, } } - self.write_str(&format!( - "{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \ - failed.", - )) - .await -} + pub(super) async fn change_email(&self, user_id: String, email: Option) -> Result { + self.bail_restricted()?; -#[admin_command] -pub(super) async fn force_join_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, - via: Option, -) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let (room_id, mut servers) = self - .services - .rooms - .alias - .resolve_with_servers(&room_id, None) - .await?; - if let Some(via) = via.map(ServerName::parse).transpose()? { - servers.retain(|n| *n != via); - servers.insert(0, via); - } + let user_id = parse_local_user_id(self.services, &user_id)?; + let Ok(new_email) = email.map(Address::try_from).transpose() else { + return Err!("Invalid email address."); + }; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - self.services - .rooms - .membership - .join_room(&user_id, &room_id, None, &servers) - .await?; + if self.services.mailer.mailer().is_none() { + warn!("SMTP has not been configured on this server, emails cannot be sent."); + } - self.write_str(&format!("{user_id} has been joined to {room_id}.")) - .await -} - -#[admin_command] -pub(super) async fn force_leave_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, -) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let room_id = self.services.rooms.alias.resolve(&room_id).await?; - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - - if !self - .services - .rooms - .state_cache - .is_joined(&user_id, &room_id) - .await - { - return Err!("{user_id} is not joined in the room"); - } - - leave_room(self.services, &user_id, &room_id, None) - .boxed() - .await?; - - self.write_str(&format!("{user_id} has left {room_id}.")) - .await -} - -#[admin_command] -pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAliasId) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let room_id = self.services.rooms.alias.resolve(&room_id).await?; - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - - let state_lock = self.services.rooms.state.mutex.lock(room_id.as_str()).await; - - let mut room_power_levels = self - .services - .rooms - .state_accessor - .get_room_power_levels(&room_id) - .await; - - let user_can_demote_self = - room_power_levels.user_can_change_user_power_level(&user_id, &user_id); - - if !user_can_demote_self { - return Err!("User is not allowed to modify their own power levels in the room.",); - } - - room_power_levels.users.remove(&user_id); - - let event_id = self - .services - .rooms - .timeline - .build_and_append_pdu( - PartialPdu::state( - String::new(), - &RoomPowerLevelsEventContent::try_from(room_power_levels) - .expect("PLs should be valid for room version"), - ), - &user_id, - Some(&room_id), - &state_lock, - ) - .await?; - - self.write_str(&format!( - "User {user_id} demoted themselves to the room default power level in {room_id} - \ - {event_id}" - )) - .await -} - -#[admin_command] -pub(super) async fn make_user_admin(&self, user_id: String) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - - self.services - .admin - .make_user_admin(&user_id) - .boxed() - .await?; - - self.write_str(&format!("{user_id} has been granted admin privileges.")) - .await -} - -#[admin_command] -pub(super) async fn put_room_tag( - &self, - user_id: String, - room_id: OwnedRoomId, - tag: String, -) -> Result { - let user_id = parse_active_local_user_id(self.services, &user_id).await?; - - let mut tags_event = self - .services - .account_data - .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) - .await - .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); - - tags_event - .content - .tags - .insert(tag.clone().into(), TagInfo::new()); - - self.services - .account_data - .update( - Some(&room_id), - &user_id, - RoomAccountDataEventType::Tag, - &serde_json::to_value(tags_event).expect("to json value always works"), - ) - .await?; - - self.write_str(&format!( - "Successfully updated room account data for {user_id} and room {room_id} with tag {tag}" - )) - .await -} - -#[admin_command] -pub(super) async fn delete_room_tag( - &self, - user_id: String, - room_id: OwnedRoomId, - tag: String, -) -> Result { - let user_id = parse_active_local_user_id(self.services, &user_id).await?; - - let mut tags_event = self - .services - .account_data - .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) - .await - .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); - - tags_event.content.tags.remove(&tag.clone().into()); - - self.services - .account_data - .update( - Some(&room_id), - &user_id, - RoomAccountDataEventType::Tag, - &serde_json::to_value(tags_event).expect("to json value always works"), - ) - .await?; - - self.write_str(&format!( - "Successfully updated room account data for {user_id} and room {room_id}, deleting room \ - tag {tag}" - )) - .await -} - -#[admin_command] -pub(super) async fn get_room_tags(&self, user_id: String, room_id: OwnedRoomId) -> Result { - let user_id = parse_active_local_user_id(self.services, &user_id).await?; - - let tags_event = self - .services - .account_data - .get_room(&room_id, &user_id, RoomAccountDataEventType::Tag) - .await - .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); - - self.write_str(&format!("```\n{:#?}\n```", tags_event.content.tags)) - .await -} - -#[admin_command] -pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result { - let Ok(event) = self - .services - .rooms - .timeline - .get_non_outlier_pdu(&event_id) - .await - else { - return Err!("Event does not exist in our database."); - }; - - if event.is_redacted() { - return Err!("Event is already redacted."); - } - - if !self.services.globals.user_is_local(event.sender()) { - return Err!("This command only works on local users."); - } - - let reason = format!( - "The administrator(s) of {} has redacted this user's message.", - self.services.globals.server_name() - ); - - let redaction_event_id = { - let state_lock = self + let current_email = self .services - .rooms - .state - .mutex - .lock(event.room_id_or_hash().as_str()) + .threepid + .get_email_for_localpart(user_id.localpart()) .await; - self.services - .rooms - .timeline - .build_and_append_pdu( - PartialPdu { - redacts: Some(event.event_id().to_owned()), - ..PartialPdu::timeline(&assign!(RoomRedactionEventContent::new_v1(), { - redacts: Some(event.event_id().to_owned()), - reason: Some(reason), - })) - }, - event.sender(), - Some(&event.room_id_or_hash()), - &state_lock, - ) - .await? - }; + match (current_email, new_email) { + | (None, None) => + self.write_str(&format!( + "{user_id} already had no associated email. No changes have been made." + )) + .await, + | (current_email, Some(new_email)) => { + self.services + .threepid + .associate_localpart_email(user_id.localpart(), &new_email) + .await?; - self.write_str(&format!( - "Successfully redacted event. Redaction event ID: {redaction_event_id}" - )) - .await -} - -#[admin_command] -pub(super) async fn force_leave_remote_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, - via: Option, -) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let (room_id, vias_raw) = self - .services - .rooms - .alias - .resolve_with_servers( - &room_id, - if let Some(v) = via.clone() { - Some(vec![ServerName::parse(v)?]) - } else { - None + if let Some(current_email) = current_email { + self.write_str(&format!( + "The associated email of {user_id} has been changed from \ + {current_email} to {new_email}." + )) + .await + } else { + self.write_str(&format!( + "{user_id} has been associated with the email {new_email}." + )) + .await + } }, - ) - .await?; + | (Some(current_email), None) => { + self.services + .threepid + .disassociate_localpart_email(user_id.localpart()) + .await; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - let mut vias: HashSet = HashSet::new(); - if let Some(via) = via { - vias.insert(ServerName::parse(via)?); - } - for server in vias_raw { - vias.insert(server); - } - remote_leave_room(self.services, &user_id, &room_id, None, vias) - .boxed() - .await?; - - self.write_str(&format!("{user_id} successfully left {room_id} via remote server.")) - .await -} - -#[admin_command] -pub(super) async fn lock(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - if user_id == self.services.globals.server_user { - return Err!("Not allowed to lock the server service account.",); - } - - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - if self.services.users.is_admin(&user_id).await { - return Err!("Admin users cannot be locked."); - } - self.services - .users - .lock_account(&user_id, self.sender_or_service_user()) - .await; - - self.write_str(&format!("User {user_id} has been locked.")) - .await -} - -#[admin_command] -pub(super) async fn unlock(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - self.services.users.unlock_account(&user_id).await; - - self.write_str(&format!("User {user_id} has been unlocked.")) - .await -} - -#[admin_command] -pub(super) async fn logout(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - if user_id == self.services.globals.server_user { - return Err!("Not allowed to log out the server service account.",); - } - - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - if self.services.users.is_admin(&user_id).await { - return Err!("You cannot forcefully log out admin users."); - } - self.services - .users - .all_device_ids(&user_id) - .for_each(async |device_id| { - self.services - .users - .remove_device(&user_id, &device_id) - .await; - }) - .await; - self.write_str(&format!("User {user_id} has been logged out from all devices.")) - .await -} - -#[admin_command] -pub(super) async fn disable_login(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - if user_id == self.services.globals.server_user { - return Err!("Not allowed to disable login for the server service account.",); - } - - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - if self.services.users.is_admin(&user_id).await { - return Err!("Admin users cannot have their login disallowed."); - } - self.services.users.disable_login(&user_id); - - self.write_str(&format!( - "{user_id} can no longer log in. Their existing sessions remain unaffected." - )) - .await -} - -#[admin_command] -pub(super) async fn enable_login(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } - self.services.users.enable_login(&user_id); - - self.write_str(&format!("{user_id} can now log in.")).await -} - -#[admin_command] -pub(super) async fn get_email(&self, user_id: String) -> Result { - self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - - match self - .services - .threepid - .get_email_for_localpart(user_id.localpart()) - .await - { - | Some(email) => - self.write_str(&format!("{user_id} has the associated email address {email}.")) - .await, - | None => - self.write_str(&format!("{user_id} has no associated email address.")) - .await, - } -} - -#[admin_command] -pub(super) async fn get_user_by_email(&self, email: String) -> Result { - self.bail_restricted()?; - - let Ok(email) = Address::try_from(email) else { - return Err!("Invalid email address."); - }; - - match self.services.threepid.get_localpart_for_email(&email).await { - | Some(localpart) => { - let user_id = - UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name())) - .unwrap(); - - self.write_str(&format!("{email} belongs to {user_id}.")) - .await - }, - | None => - self.write_str(&format!("No user has {email} as their email address.")) - .await, - } -} - -#[admin_command] -pub(super) async fn change_email(&self, user_id: String, email: Option) -> Result { - self.bail_restricted()?; - - let user_id = parse_local_user_id(self.services, &user_id)?; - let Ok(new_email) = email.map(Address::try_from).transpose() else { - return Err!("Invalid email address."); - }; - - if self.services.mailer.mailer().is_none() { - warn!("SMTP has not been configured on this server, emails cannot be sent."); - } - - let current_email = self - .services - .threepid - .get_email_for_localpart(user_id.localpart()) - .await; - - match (current_email, new_email) { - | (None, None) => - self.write_str(&format!( - "{user_id} already had no associated email. No changes have been made." - )) - .await, - | (current_email, Some(new_email)) => { - self.services - .threepid - .associate_localpart_email(user_id.localpart(), &new_email) - .await?; - - if let Some(current_email) = current_email { self.write_str(&format!( - "The associated email of {user_id} has been changed from {current_email} to \ - {new_email}." + "The associated email of {user_id} has been removed (it was \ + {current_email})." )) .await - } else { - self.write_str(&format!( - "{user_id} has been associated with the email {new_email}." - )) - .await - } - }, - | (Some(current_email), None) => { - self.services - .threepid - .disassociate_localpart_email(user_id.localpart()) - .await; + }, + } + } - self.write_str(&format!( - "The associated email of {user_id} has been removed (it was {current_email})." - )) + pub(super) async fn reset_push_rules(&self, user_id: String) -> Result { + let user_id = parse_local_user_id(self.services, &user_id)?; + if !self.services.users.is_active(&user_id).await { + return Err!("User is not active."); + } + recreate_push_rules_and_return(self.services, &user_id).await?; + self.write_str("Reset user's push rules to the server default.") .await - }, } } - -#[admin_command] -pub(super) async fn reset_push_rules(&self, user_id: String) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - if !self.services.users.is_active(&user_id).await { - return Err!("User is not active."); - } - recreate_push_rules_and_return(self.services, &user_id).await?; - self.write_str("Reset user's push rules to the server default.") - .await -} diff --git a/src/macros/admin.rs b/src/macros/admin.rs index fe227b436..365d593c4 100644 --- a/src/macros/admin.rs +++ b/src/macros/admin.rs @@ -2,19 +2,10 @@ use itertools::Itertools; use proc_macro::{Span, TokenStream}; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; -use syn::{Attribute, Error, Fields, Ident, ItemEnum, ItemFn, Meta, Variant, parse_quote}; +use syn::{Error, Fields, Ident, ItemEnum, Meta, Variant}; use crate::{Result, utils::camel_to_snake_string}; -pub(super) fn command(mut item: ItemFn, _args: &[Meta]) -> Result { - let attr: Attribute = parse_quote! { - #[conduwuit_macros::implement(crate::Context, params = "<'_>")] - }; - - item.attrs.push(attr); - Ok(item.into_token_stream().into()) -} - pub(super) fn command_dispatch(item: ItemEnum, _args: &[Meta]) -> Result { let name = &item.ident; let arm: Vec = item.variants.iter().map(dispatch_arm).try_collect()?; diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 843cd4e82..b533230b3 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -14,11 +14,6 @@ use syn::{ pub(crate) type Result = std::result::Result; -#[proc_macro_attribute] -pub fn admin_command(args: TokenStream, input: TokenStream) -> TokenStream { - attribute_macro::(args, input, admin::command) -} - #[proc_macro_attribute] pub fn admin_command_dispatch(args: TokenStream, input: TokenStream) -> TokenStream { attribute_macro::(args, input, admin::command_dispatch) diff --git a/src/macros/utils.rs b/src/macros/utils.rs index 598f60ffa..a345fc673 100644 --- a/src/macros/utils.rs +++ b/src/macros/utils.rs @@ -1,8 +1,6 @@ use std::collections::HashMap; -use syn::{Expr, ExprLit, Generics, Lit, Meta, MetaNameValue, parse_str}; - -use crate::Result; +use syn::{Expr, ExprLit, Lit, Meta, MetaNameValue}; pub(crate) fn get_simple_settings(args: &[Meta]) -> HashMap { args.iter().fold(HashMap::new(), |mut map, arg| {