mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-06-27 00:56:36 +02:00
369 lines
10 KiB
Rust
369 lines
10 KiB
Rust
use std::{fmt::Write as _, sync::Arc};
|
|
|
|
use api::client_server::{join_room_by_id_helper, leave_all_rooms};
|
|
use conduit::utils;
|
|
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, UserId};
|
|
use tracing::{error, info, warn};
|
|
|
|
use crate::{escape_html, get_room_info, services, user_is_local, Result};
|
|
|
|
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
|
|
|
|
pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
|
|
match services().users.list_local_users() {
|
|
Ok(users) => {
|
|
let mut msg = format!("Found {} local user account(s):\n", users.len());
|
|
msg += &users.join("\n");
|
|
Ok(RoomMessageEventContent::text_plain(&msg))
|
|
},
|
|
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn create(
|
|
_body: Vec<&str>, username: String, password: Option<String>,
|
|
) -> Result<RoomMessageEventContent> {
|
|
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
|
|
|
// Validate user id
|
|
let user_id =
|
|
match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) {
|
|
Ok(id) => id,
|
|
Err(e) => {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"The supplied username is not a valid username: {e}"
|
|
)))
|
|
},
|
|
};
|
|
|
|
if !user_is_local(&user_id) {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"User {user_id} does not belong to our server."
|
|
)));
|
|
}
|
|
|
|
if user_id.is_historical() {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"Userid {user_id} is not allowed due to historical"
|
|
)));
|
|
}
|
|
|
|
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
|
|
.config
|
|
.new_user_displayname_suffix
|
|
.is_empty()
|
|
{
|
|
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
|
|
.expect("should be able to write to string buffer");
|
|
}
|
|
|
|
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(_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}");
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(crate) async fn deactivate(
|
|
_body: Vec<&str>, leave_rooms: bool, user_id: String,
|
|
) -> Result<RoomMessageEventContent> {
|
|
// Validate user id
|
|
let user_id =
|
|
match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) {
|
|
Ok(id) => Arc::<UserId>::from(id),
|
|
Err(e) => {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"The supplied username is not a valid username: {e}"
|
|
)))
|
|
},
|
|
};
|
|
|
|
// check if user belongs to our server
|
|
if user_id.server_name() != services().globals.server_name() {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"User {user_id} does not belong to our server."
|
|
)));
|
|
}
|
|
|
|
// 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(crate) async fn reset_password(_body: Vec<&str>, username: String) -> Result<RoomMessageEventContent> {
|
|
// Validate user id
|
|
let user_id =
|
|
match UserId::parse_with_server_name(username.as_str().to_lowercase(), services().globals.server_name()) {
|
|
Ok(id) => Arc::<UserId>::from(id),
|
|
Err(e) => {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"The supplied username is not a valid username: {e}"
|
|
)))
|
|
},
|
|
};
|
|
|
|
// 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(crate) async fn deactivate_all(body: Vec<&str>, leave_rooms: bool, force: bool) -> Result<RoomMessageEventContent> {
|
|
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
|
|
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
|
|
|
|
let mut user_ids: Vec<&UserId> = Vec::new();
|
|
|
|
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: u16 = 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 = deactivation_count.saturating_add(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(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result<RoomMessageEventContent> {
|
|
// Validate user id
|
|
let user_id =
|
|
match UserId::parse_with_server_name(user_id.as_str().to_lowercase(), services().globals.server_name()) {
|
|
Ok(id) => Arc::<UserId>::from(id),
|
|
Err(e) => {
|
|
return Ok(RoomMessageEventContent::text_plain(format!(
|
|
"The supplied username is not a valid username: {e}"
|
|
)))
|
|
},
|
|
};
|
|
|
|
if !user_is_local(&user_id) {
|
|
return Ok(RoomMessageEventContent::text_plain("User does not belong to our server."));
|
|
}
|
|
|
|
if !services().users.exists(&user_id)? {
|
|
return Ok(RoomMessageEventContent::text_plain("User does not exist on this server."));
|
|
}
|
|
|
|
let 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))
|
|
.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.len(),
|
|
rooms
|
|
.iter()
|
|
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
);
|
|
let output_html = format!(
|
|
"<table><caption>Rooms {user_id} Joined \
|
|
({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
|
rooms.len(),
|
|
rooms
|
|
.iter()
|
|
.fold(String::new(), |mut output, (id, members, name)| {
|
|
writeln!(
|
|
output,
|
|
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
|
escape_html(id.as_ref()),
|
|
members,
|
|
escape_html(name)
|
|
)
|
|
.unwrap();
|
|
output
|
|
})
|
|
);
|
|
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
|
}
|