Files
tuwunel/src/service/deactivate/mod.rs
2025-09-23 05:16:29 +00:00

165 lines
3.6 KiB
Rust

use std::sync::Arc;
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomId, UserId,
events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent},
};
use tuwunel_core::{Event, Result, info, pdu::PduBuilder, utils::ReadyExt, warn};
pub struct Service {
services: Arc<crate::services::OnceServices>,
}
impl crate::Service for Service {
fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self { services: args.services.clone() }))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Runs through all the deactivation steps:
///
/// - Mark as deactivated
/// - Removing display name
/// - Removing avatar URL and blurhash
/// - Removing all profile data
/// - Leaving all rooms (and forgets all of them)
pub async fn full_deactivate(&self, user_id: &UserId) -> Result {
self.services
.users
.deactivate_account(user_id)
.await?;
let all_joined_rooms: Vec<OwnedRoomId> = self
.services
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
self.services
.users
.update_displayname(user_id, None, &all_joined_rooms)
.await;
self.services
.users
.update_avatar_url(user_id, None, None, &all_joined_rooms)
.await;
self.services
.users
.all_profile_keys(user_id)
.ready_for_each(|(profile_key, _)| {
self.services
.users
.set_profile_key(user_id, &profile_key, None);
})
.await;
for room_id in all_joined_rooms {
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
.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 {
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail
match self
.services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
user_id,
&room_id,
&state_lock,
)
.await
{
| Err(e) => {
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
},
| _ => {
info!("Demoted {user_id} in {room_id} as part of account deactivation");
},
}
}
}
let rooms_joined = self
.services
.state_cache
.rooms_joined(user_id)
.map(ToOwned::to_owned);
let rooms_invited = self
.services
.state_cache
.rooms_invited(user_id)
.map(|(r, _)| r);
let rooms_knocked = self
.services
.state_cache
.rooms_knocked(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined
.chain(rooms_invited)
.chain(rooms_knocked)
.collect()
.await;
for room_id in all_rooms {
let state_lock = self.services.state.mutex.lock(&room_id).await;
// ignore errors
if let Err(e) = self
.services
.membership
.leave(user_id, &room_id, None, false, &state_lock)
.boxed()
.await
{
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
}
drop(state_lock);
self.services
.state_cache
.forget(&room_id, user_id);
}
Ok(())
}
}