159 lines
7.8 KiB
Rust
159 lines
7.8 KiB
Rust
//! Removing an account removes everything the panel knows about its player.
|
|
//!
|
|
//! Rows that belong to the player are deleted. Rows that belong to *other*
|
|
//! people but mention the player (the other side of an economy transfer, who
|
|
//! performed a guild-bank deposit) are kept with the player anonymised.
|
|
|
|
use crate::auth::UserRow;
|
|
use crate::error::AppResult;
|
|
use crate::state::AppState;
|
|
use serde::Serialize;
|
|
use sqlx::SqliteConnection;
|
|
|
|
/// Stand-in UUID for deleted players in other people's records.
|
|
pub const DELETED_UUID: &str = "00000000-0000-0000-0000-000000000000";
|
|
pub const DELETED_NAME: &str = "Deleted player";
|
|
|
|
#[derive(Debug, Default, Serialize)]
|
|
pub struct PurgeReport {
|
|
/// Rows deleted, by area.
|
|
pub removed: std::collections::BTreeMap<&'static str, u64>,
|
|
/// Guilds dissolved because the player was their only member.
|
|
pub guilds_dissolved: u64,
|
|
/// Guilds whose leadership was handed to someone else.
|
|
pub guilds_transferred: u64,
|
|
pub skin_file_removed: bool,
|
|
}
|
|
|
|
impl PurgeReport {
|
|
fn add(&mut self, area: &'static str, n: u64) {
|
|
if n > 0 {
|
|
*self.removed.entry(area).or_default() += n;
|
|
}
|
|
}
|
|
pub fn total(&self) -> u64 {
|
|
self.removed.values().sum()
|
|
}
|
|
}
|
|
|
|
async fn run(conn: &mut SqliteConnection, sql: &str, binds: &[&str]) -> AppResult<u64> {
|
|
let mut q = sqlx::query(sql);
|
|
for b in binds {
|
|
q = q.bind(*b);
|
|
}
|
|
Ok(q.execute(&mut *conn).await?.rows_affected())
|
|
}
|
|
|
|
/// Delete an account and all data tied to it. Runs in one transaction.
|
|
pub async fn purge_user(state: &AppState, user: &UserRow) -> AppResult<PurgeReport> {
|
|
let uuid = user.uuid.as_str();
|
|
let name = user.username.as_str();
|
|
let mut report = PurgeReport::default();
|
|
let mut tx = state.db.begin().await?;
|
|
let c = &mut *tx;
|
|
|
|
// ---- progression ----
|
|
report.add("levels", run(c, "DELETE FROM user_levels WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("levels", run(c, "DELETE FROM server_levels WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("rewards", run(c, "DELETE FROM granted_rewards WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("quests", run(c, "DELETE FROM user_quests WHERE user_uuid = ?", &[uuid]).await?);
|
|
report.add("quests", run(c, "DELETE FROM quest_assignments WHERE user_uuid = ?", &[uuid]).await?);
|
|
report.add("achievements", run(c, "DELETE FROM user_achievements WHERE user_uuid = ?", &[uuid]).await?);
|
|
|
|
// ---- gameplay records ----
|
|
report.add("stats", run(c, "DELETE FROM player_stats WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("stats", run(c, "DELETE FROM server_online WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("activity", run(c, "DELETE FROM server_events WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("activity", run(c, "DELETE FROM events WHERE uuid = ? OR (uuid IS NULL AND username = ? COLLATE NOCASE)", &[uuid, name]).await?);
|
|
|
|
// ---- social ----
|
|
report.add("friends", run(c, "DELETE FROM friendships WHERE user_uuid = ? OR friend_uuid = ?", &[uuid, uuid]).await?);
|
|
report.add("messages", run(c, "DELETE FROM direct_messages WHERE sender_uuid = ? OR recipient_uuid = ?", &[uuid, uuid]).await?);
|
|
report.add("invites", run(c, "DELETE FROM game_invites WHERE sender_uuid = ? OR recipient_uuid = ?", &[uuid, uuid]).await?);
|
|
report.add("profile", run(c, "DELETE FROM user_profiles WHERE uuid = ?", &[uuid]).await?);
|
|
// Likes they gave come off other people's post counters.
|
|
run(
|
|
c,
|
|
"UPDATE user_posts SET likes_count = MAX(0, likes_count - 1) WHERE id IN (SELECT post_id FROM user_post_likes WHERE user_uuid = ?)",
|
|
&[uuid],
|
|
)
|
|
.await?;
|
|
report.add("posts", run(c, "DELETE FROM user_post_likes WHERE user_uuid = ?", &[uuid]).await?);
|
|
report.add("posts", run(c, "DELETE FROM user_posts WHERE user_uuid = ?", &[uuid]).await?);
|
|
|
|
// ---- guilds ----
|
|
let led: Vec<String> = sqlx::query_scalar("SELECT id FROM guilds WHERE leader_uuid = ?").bind(uuid).fetch_all(&mut *c).await?;
|
|
for guild in led {
|
|
// Next in line: an officer, then whoever has been a member longest.
|
|
let heir: Option<(String, String)> = sqlx::query_as(
|
|
"SELECT uuid, name FROM guild_members WHERE guild_id = ? AND uuid <> ?
|
|
ORDER BY (role = 'officer') DESC, joined_at ASC LIMIT 1",
|
|
)
|
|
.bind(&guild)
|
|
.bind(uuid)
|
|
.fetch_optional(&mut *c)
|
|
.await?;
|
|
match heir {
|
|
Some((heir_uuid, _)) => {
|
|
run(c, "UPDATE guilds SET leader_uuid = ? WHERE id = ?", &[&heir_uuid, &guild]).await?;
|
|
run(c, "UPDATE guild_members SET role = 'leader' WHERE guild_id = ? AND uuid = ?", &[&guild, &heir_uuid]).await?;
|
|
report.guilds_transferred += 1;
|
|
}
|
|
None => {
|
|
// Claims, wallet, posts and roles go with the guild.
|
|
report.add("guild_data", run(c, "DELETE FROM guilds WHERE id = ?", &[&guild]).await?);
|
|
report.guilds_dissolved += 1;
|
|
}
|
|
}
|
|
}
|
|
report.add("guild_membership", run(c, "DELETE FROM guild_members WHERE uuid = ?", &[uuid]).await?);
|
|
report.add("guild_posts", run(c, "DELETE FROM guild_posts WHERE author_uuid = ?", &[uuid]).await?);
|
|
// Territory they claimed stays with the guild.
|
|
run(c, "UPDATE guild_claims SET claimed_by_uuid = ? WHERE claimed_by_uuid = ?", &[DELETED_UUID, uuid]).await?;
|
|
run(c, "UPDATE guild_wallet_transactions SET actor_uuid = ? WHERE actor_uuid = ?", &[DELETED_UUID, uuid]).await?;
|
|
|
|
// ---- economy ----
|
|
report.add("economy", run(c, "DELETE FROM server_economy WHERE uuid = ?", &[uuid]).await?);
|
|
// Items a guild had listed stay for the guild; only the lister is anonymised.
|
|
run(c, "UPDATE server_market SET seller_uuid = ?, seller_name = ? WHERE seller_uuid = ? AND seller_guild_id IS NOT NULL", &[DELETED_UUID, DELETED_NAME, uuid]).await?;
|
|
report.add("economy", run(c, "DELETE FROM server_market WHERE seller_uuid = ?", &[uuid]).await?);
|
|
run(c, "UPDATE economy_transactions SET from_uuid = ?, from_name = ? WHERE from_uuid = ?", &[DELETED_UUID, DELETED_NAME, uuid]).await?;
|
|
run(c, "UPDATE economy_transactions SET to_uuid = ?, to_name = ? WHERE to_uuid = ?", &[DELETED_UUID, DELETED_NAME, uuid]).await?;
|
|
|
|
// ---- identity ----
|
|
// `reserved_usernames` is kept on purpose: a deleted player's name stays
|
|
// taken so nobody can impersonate them. It holds only the name and UUID.
|
|
// Explicit, in case a foreign key is ever relaxed.
|
|
let id = user.id.to_string();
|
|
for (area, sql) in [
|
|
("sessions", "DELETE FROM ygg_tokens WHERE user_id = ?"),
|
|
("sessions", "DELETE FROM ygg_sessions WHERE user_id = ?"),
|
|
("sessions", "DELETE FROM player_keys WHERE user_id = ?"),
|
|
("sessions", "DELETE FROM launcher_sessions WHERE user_id = ?"),
|
|
("connections", "DELETE FROM account_connections WHERE user_id = ?"),
|
|
("connections", "DELETE FROM oauth_attempts WHERE user_id = ?"),
|
|
("connections", "DELETE FROM password_resets WHERE user_id = ?"),
|
|
("groups", "DELETE FROM user_groups WHERE user_id = ?"),
|
|
] {
|
|
report.add(area, run(c, sql, &[&id]).await?);
|
|
}
|
|
report.add("account", run(c, "DELETE FROM users WHERE id = ?", &[&id]).await?);
|
|
|
|
// A skin nobody else uses is deleted from disk.
|
|
let skin = user.skin_hash.clone();
|
|
let skin_shared: bool = match &skin {
|
|
Some(h) => sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE skin_hash = ?)").bind(h).fetch_one(&mut *c).await?,
|
|
None => true,
|
|
};
|
|
tx.commit().await?;
|
|
|
|
if let (Some(hash), false) = (skin, skin_shared) {
|
|
if let Some(path) = crate::textures::path(&state.cfg.textures_dir(), &hash) {
|
|
report.skin_file_removed = tokio::fs::remove_file(path).await.is_ok();
|
|
}
|
|
}
|
|
// Live map: stop showing them.
|
|
state.livemap.forget_player(uuid);
|
|
Ok(report)
|
|
}
|