use std::{fmt::Write as _, sync::Arc}; use itertools::Itertools; use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, UserId}; use tracing::{error, info, warn}; use crate::{ api::client_server::{join_room_by_id_helper, leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH}, service::admin::{escape_html, get_room_info}, services, utils, Result, }; pub(super) async fn list(_body: Vec<&str>) -> Result { match services().users.list_local_users() { Ok(users) => { let mut msg = format!("Found {} local user account(s):\n", users.len()); msg += &users.join("\n"); Ok(RoomMessageEventContent::text_plain(&msg)) }, Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())), } } pub(super) async fn create( _body: Vec<&str>, username: String, password: Option, ) -> Result { let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); // Validate user id let user_id = match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) { Ok(id) => id, Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" ))) }, }; if user_id.is_historical() { return Ok(RoomMessageEventContent::text_plain(format!( "Userid {user_id} is not allowed due to historical" ))); } if services().users.exists(&user_id)? { return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists"))); } // Create user services().users.create(&user_id, Some(password.as_str()))?; // Default to pretty displayname let mut displayname = user_id.localpart().to_owned(); // If `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 !services().globals.new_user_displayname_suffix().is_empty() { displayname.push_str(&(" ".to_owned() + services().globals.new_user_displayname_suffix())); } services() .users .set_displayname(&user_id, Some(displayname)) .await?; // Initial account data services().account_data.update( None, &user_id, ruma::events::GlobalAccountDataEventType::PushRules .to_string() .into(), &serde_json::to_value(ruma::events::push_rules::PushRulesEvent { content: ruma::events::push_rules::PushRulesEventContent { global: ruma::push::Ruleset::server_default(&user_id), }, }) .expect("to json value always works"), )?; if !services().globals.config.auto_join_rooms.is_empty() { for room in &services().globals.config.auto_join_rooms { if !services() .rooms .state_cache .server_in_room(services().globals.server_name(), room)? { warn!("Skipping room {room} to automatically join as we have never joined before."); continue; } if let Some(room_id_server_name) = room.server_name() { match join_room_by_id_helper( Some(&user_id), room, Some("Automatically joining this room upon registration".to_owned()), &[room_id_server_name.to_owned(), services().globals.server_name().to_owned()], None, ) .await { Ok(_) => { 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}"); }, }; } } } // we dont add a device since we're not the user, just the creator // Inhibit login does not work for guests Ok(RoomMessageEventContent::text_plain(format!( "Created user with user_id: {user_id} and password: `{password}`" ))) } pub(super) async fn deactivate( _body: Vec<&str>, leave_rooms: bool, user_id: String, ) -> Result { // Validate user id let user_id = match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) { Ok(id) => Arc::::from(id), Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" ))) }, }; // check if user belongs to our server if user_id.server_name() != services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain(format!( "User {user_id} does not belong to our server." ))); } // don't deactivate the conduit service account if user_id == UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists") { return Ok(RoomMessageEventContent::text_plain( "Not allowed to deactivate the Conduit service account.", )); } if services().users.exists(&user_id)? { RoomMessageEventContent::text_plain(format!("Making {user_id} leave all rooms before deactivation...")); services().users.deactivate_account(&user_id)?; if leave_rooms { leave_all_rooms(&user_id).await?; } Ok(RoomMessageEventContent::text_plain(format!( "User {user_id} has been deactivated" ))) } else { Ok(RoomMessageEventContent::text_plain(format!( "User {user_id} doesn't exist on this server" ))) } } pub(super) async fn reset_password(_body: Vec<&str>, username: String) -> Result { // Validate user id let user_id = match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) { Ok(id) => Arc::::from(id), Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" ))) }, }; // check if user belongs to our server if user_id.server_name() != services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain(format!( "User {user_id} does not belong to our server." ))); } // Check if the specified user is valid if !services().users.exists(&user_id)? || user_id == UserId::parse_with_server_name("conduit", services().globals.server_name()).expect("conduit user exists") { return Ok(RoomMessageEventContent::text_plain("The specified user does not exist!")); } let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH); match services() .users .set_password(&user_id, Some(new_password.as_str())) { Ok(()) => Ok(RoomMessageEventContent::text_plain(format!( "Successfully reset the password for user {user_id}: `{new_password}`" ))), Err(e) => Ok(RoomMessageEventContent::text_plain(format!( "Couldn't reset the password for user {user_id}: {e}" ))), } } pub(super) async fn deactivate_all(body: Vec<&str>, leave_rooms: bool, force: bool) -> Result { if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" { let usernames = body.clone().drain(1..body.len() - 1).collect::>(); let mut user_ids: Vec<&UserId> = Vec::new(); for &username in &usernames { match <&UserId>::try_from(username) { Ok(user_id) => user_ids.push(user_id), Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "{username} is not a valid username: {e}" ))) }, } } let mut deactivation_count = 0; let mut admins = Vec::new(); if !force { user_ids.retain(|&user_id| match services().users.is_admin(user_id) { Ok(is_admin) => { if is_admin { admins.push(user_id.localpart()); false } else { true } }, Err(_) => false, }); } for &user_id in &user_ids { // check if user belongs to our server and skips over non-local users if user_id.server_name() != services().globals.server_name() { continue; } // don't deactivate the conduit service account if user_id == UserId::parse_with_server_name("conduit", services().globals.server_name()) .expect("conduit user exists") { continue; } // user does not exist on our server if !services().users.exists(user_id)? { continue; } if services().users.deactivate_account(user_id).is_ok() { deactivation_count += 1; } } if leave_rooms { for &user_id in &user_ids { _ = leave_all_rooms(user_id).await; } } if admins.is_empty() { Ok(RoomMessageEventContent::text_plain(format!( "Deactivated {deactivation_count} accounts." ))) } else { Ok(RoomMessageEventContent::text_plain(format!( "Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", ") ))) } } else { Ok(RoomMessageEventContent::text_plain( "Expected code block in command body. Add --help for details.", )) } } pub(super) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result { // Validate user id let user_id = match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) { Ok(id) => Arc::::from(id), Err(e) => { return Ok(RoomMessageEventContent::text_plain(format!( "The supplied username is not a valid username: {e}" ))) }, }; if user_id.server_name() != services().globals.server_name() { return Ok(RoomMessageEventContent::text_plain("User does not belong to our server.")); } if !services().users.exists(&user_id)? { return Ok(RoomMessageEventContent::text_plain("User does not exist on this server.")); } let mut rooms: Vec<(OwnedRoomId, u64, String)> = services() .rooms .state_cache .rooms_joined(&user_id) .filter_map(Result::ok) .map(|room_id| get_room_info(&room_id)) .sorted_unstable() .dedup() .collect(); if rooms.is_empty() { return Ok(RoomMessageEventContent::text_plain("User is not in any rooms.")); } rooms.sort_by_key(|r| r.1); rooms.reverse(); let output_plain = format!( "Rooms {user_id} Joined:\n{}", rooms .iter() .map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}")) .collect::>() .join("\n") ); let output_html = format!( "\n\t\t\n{}
Rooms {user_id} \ Joined
idmembersname
", rooms .iter() .fold(String::new(), |mut output, (id, members, name)| { writeln!( output, "{}\t{}\t{}", escape_html(id.as_ref()), members, escape_html(name) ) .unwrap(); output }) ); Ok(RoomMessageEventContent::text_html(output_plain, output_html)) }