Files
tuwunel/src/admin/user/commands.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

929 lines
21 KiB
Rust
Raw Normal View History

use std::{cmp, collections::BTreeMap};
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
Int, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
2025-08-28 02:56:52 +05:00
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, UserPowerLevel},
redaction::RoomRedactionEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
},
uint,
};
use tuwunel_core::{
Err, Result, debug_warn, info,
matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt, stream::IterStream},
};
use tuwunel_service::{Services, users::Register};
use crate::{
admin_command, get_room_info,
2025-08-28 02:56:52 +05:00
utils::{parse_active_local_user_id, parse_local_user_id, parse_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
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
.services
.users
.list_local_users()
.map(ToString::to_string)
.collect()
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
.await;
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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<String>) -> Result {
// Validate user id
let user_id = parse_local_user_id(self.services, &username)?;
if let Err(e) = user_id.validate_strict()
&& self.services.config.emergency_password.is_none()
{
return Err!("Username {user_id} contains disallowed characters or spaces: {e}");
}
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));
self.services
.users
.full_register(Register {
user_id: Some(&user_id),
password: Some(&password),
grant_first_user_admin: true,
..Default::default()
})
.await?;
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.",);
}
deactivate_user(self.services, &user_id, no_leave_rooms).await?;
self.write_str(&format!("User {user_id} has been deactivated"))
.await
}
#[admin_command]
pub(super) async fn delete_device(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
if !self.services.globals.user_is_local(&user_id) {
return Err!("Cannot delete device of remote user");
}
self.services
.users
.remove_device(&user_id, &device_id)
.await;
self.write_str(&format!("User {user_id}'s device {device_id} removed."))
.await
}
#[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> 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));
match self
.services
.users
.set_password(&user_id, Some(new_password.as_str()))
.await
{
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
| Ok(()) => {
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await
}
#[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::<Vec<_>>();
let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len());
let mut admins = Vec::new();
for username in usernames {
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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.admin.user_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 deactivate_user(self.services, &user_id, no_leave_rooms).await {
| Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
},
| Err(e) => {
self.services
.admin
.send_text(&format!("Failed deactivating user: {e}"))
.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
}
async fn deactivate_user(services: &Services, user_id: &UserId, no_leave_rooms: bool) -> Result {
if !no_leave_rooms {
services
.deactivate
.full_deactivate(user_id)
.boxed()
.await?;
} else {
services.users.deactivate_account(user_id).await?;
}
Ok(())
}
#[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
.state_cache
.rooms_joined(&user_id)
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
.then(|room_id| get_room_info(self.services, room_id))
.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::<Vec<_>>()
.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: 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 Ok(admin_room) = self.services.admin.get_admin_room().await else {
return Err!("There is not an admin room to check for server admins.",);
};
let (room_id, servers) = self
.services
.alias
.maybe_resolve_with_servers(&room, None)
.await?;
if !self
.services
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
return Err!("We are not joined in this room.");
}
let server_admins: Vec<_> = self
.services
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
.collect()
.await;
if !self
.services
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.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::<Vec<_>>();
let mut user_ids: Vec<OwnedUserId> = 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;
let state_lock = self.services.state.mutex.lock(&room_id).await;
for user_id in user_ids {
match self
.services
.membership
.join(
&user_id,
&room_id,
Some(&room),
Some(String::from(BULK_JOIN_REASON)),
&servers,
false,
&state_lock,
)
.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);
},
}
}
drop(state_lock);
self.write_str(&format!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \
failed.",
))
.await
}
#[admin_command]
pub(super) async fn force_join_all_local_users(
&self,
room: 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(admin_room) = self.services.admin.get_admin_room().await else {
return Err!("There is not an admin room to check for server admins.",);
};
let (room_id, servers) = self
.services
.alias
.maybe_resolve_with_servers(&room, None)
.await?;
if !self
.services
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
return Err!("We are not joined in this room.");
}
let server_admins: Vec<_> = self
.services
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
.collect()
.await;
if !self
.services
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.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;
let state_lock = self.services.state.mutex.lock(&room_id).await;
for user_id in &self
.services
.users
.list_local_users()
.map(UserId::to_owned)
.collect::<Vec<_>>()
.await
{
if user_id == &self.services.globals.server_user {
continue;
}
match self
.services
.membership
.join(
user_id,
&room_id,
Some(&room),
Some(String::from(BULK_JOIN_REASON)),
&servers,
false,
&state_lock,
)
.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);
},
}
}
drop(state_lock);
self.write_str(&format!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \
failed.",
))
.await
}
#[admin_command]
pub(super) async fn force_join_room(&self, user_id: String, room: OwnedRoomOrAliasId) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let (room_id, servers) = self
.services
.alias
.maybe_resolve_with_servers(&room, None)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let state_lock = self.services.state.mutex.lock(&room_id).await;
self.services
.membership
.join(&user_id, &room_id, Some(&room), None, &servers, false, &state_lock)
.await?;
drop(state_lock);
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
.alias
.maybe_resolve(&room_id)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self
.services
.state_cache
.is_joined(&user_id, &room_id)
.await
{
return Err!("{user_id} is not joined in the room");
}
let state_lock = self.services.state.mutex.lock(&room_id).await;
self.services
.membership
.leave(&user_id, &room_id, None, false, &state_lock)
.boxed()
.await?;
drop(state_lock);
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
.alias
.maybe_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.state.mutex.lock(&room_id).await;
let room_power_levels: Option<RoomPowerLevels> = self
.services
.state_accessor
.get_power_levels(&room_id)
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
.await
.ok();
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(&user_id, &user_id)
});
let user_can_demote_self = user_can_change_self
|| self
.services
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.");
}
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(&user_id);
let event_id = self
.services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
&user_id,
&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
}
2025-08-28 02:56:52 +05:00
#[admin_command]
pub(super) async fn force_promote(
&self,
target_id: String,
room_id: OwnedRoomOrAliasId,
) -> Result {
let target_id = parse_user_id(self.services, &target_id)?;
let room_id = self
.services
.alias
.maybe_resolve(&room_id)
.await?;
2025-08-28 02:56:52 +05:00
let state_lock = self.services.state.mutex.lock(&room_id).await;
let room_power_levels = self
.services
.state_accessor
.get_power_levels(&room_id)
.await?;
let privileged_member = self
.services
.state_cache
.room_members(&room_id)
.ready_filter(|member_id| {
self.services.globals.user_is_local(member_id)
&& room_power_levels.user_can_change_user_power_level(member_id, &target_id)
})
.map(ToOwned::to_owned)
.ready_fold_default(|selected_user, member_id| match selected_user {
| None => Some(member_id),
| Some(selected_user) => Some(
if room_power_levels.for_user(&selected_user)
> room_power_levels.for_user(&member_id)
{
selected_user
} else {
member_id
},
),
})
.await;
let Some(privileged_member) = privileged_member else {
return Err!("No privileged user exists in room, cannot promote.");
};
info!("Selected privileged member {privileged_member}");
let power_level: Int = match room_power_levels.for_user(&privileged_member) {
| UserPowerLevel::Infinite => Int::MAX,
| UserPowerLevel::Int(x) => x,
};
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels.try_into()?;
power_levels_content
.users
.insert(target_id.clone(), power_level);
let event_id = self
.services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
&privileged_member,
&room_id,
&state_lock,
)
.await?;
drop(state_lock);
self.write_str(&format!(
"User {privileged_member} promoted {target_id} to {power_level} power level in \
{room_id} - {event_id}"
))
.await?;
Ok(())
}
#[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 {
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event
.content
.tags
.insert(tag.clone().into(), TagInfo::new());
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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 {
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event
.content
.tags
.remove(&tag.clone().into());
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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 {
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
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(TagEvent {
content: TagEventContent { tags: 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 {
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
let Ok(event) = self
.services
.timeline
Database Refactor combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
2024-08-08 17:18:30 +00:00
.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
.state
.mutex
.lock(event.room_id())
.await;
self.services
.timeline
.build_and_append_pdu(
PduBuilder {
redacts: Some(event.event_id().to_owned()),
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(event.event_id().to_owned()),
reason: Some(reason),
})
},
event.sender(),
event.room_id(),
&state_lock,
)
.await?
};
self.write_str(&format!(
"Successfully redacted event. Redaction event ID: {redaction_event_id}"
))
.await
}
#[admin_command]
pub(super) async fn last_active(&self, limit: Option<usize>) -> Result {
self.services
.users
.list_local_users()
.map(ToOwned::to_owned)
.then(async |user_id| {
self.services
.users
.all_devices_metadata(&user_id)
.ready_filter_map(|device| device.last_seen_ts)
.ready_fold_default(cmp::max)
.map(|last_seen_ts| (last_seen_ts, user_id.clone()))
.await
})
.ready_filter(|(ts, _)| ts.get() > uint!(0))
.collect::<Vec<_>>()
.map(|mut vec| {
vec.sort_by_key(|k| cmp::Reverse(k.0));
vec
})
.map(Vec::into_iter)
.map(IterStream::try_stream)
.flatten_stream()
.take(limit.unwrap_or(48))
.try_for_each(async |(last_seen_ts, user_id)| {
let ago = last_seen_ts;
let user_id = user_id.localpart();
let line = format!("{ago:?} {user_id}\n");
self.write_str(&line).await
})
.boxed()
.await
}