feat(core/database, bindings/node): suspend user

This commit is contained in:
Paul Makles
2024-08-31 17:10:47 +01:00
parent ae1d5d07e3
commit 01368960f3
16 changed files with 856 additions and 25 deletions

View File

@@ -2,12 +2,15 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use authifier::config::{EmailVerificationConfig, Template};
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0;
use revolt_models::v0::{self, UserFlags};
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use serde_json::json;
use ulid::Ulid;
auto_derived_partial!(
@@ -49,6 +52,10 @@ auto_derived_partial!(
/// Bot information
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
/// Time until user is unsuspended
#[serde(skip_serializing_if = "Option::is_none")]
pub suspended_until: Option<Timestamp>,
},
"PartialUser"
);
@@ -62,6 +69,10 @@ auto_derived!(
ProfileContent,
ProfileBackground,
DisplayName,
// internal fields
Suspension,
None,
}
/// User's relationship with another user (or themselves)
@@ -165,6 +176,7 @@ impl Default for User {
flags: Default::default(),
privileged: Default::default(),
bot: Default::default(),
suspended_until: Default::default(),
}
}
}
@@ -659,17 +671,106 @@ impl User {
}
}
FieldsUser::DisplayName => self.display_name = None,
FieldsUser::Suspension => self.suspended_until = None,
FieldsUser::None => {}
}
}
/// Suspend the user
pub async fn suspend(&mut self, db: &Database) -> Result<()> {
// Remove sessions (logout all)
// Mark user as suspended
// Disable account
///
/// - If a duration is specified, the user will be automatically unsuspended after the given time.
/// - If a reason is specified, an email will be sent.
pub async fn suspend(
&mut self,
db: &Database,
duration_days: Option<usize>,
reason: Option<Vec<String>>,
) -> Result<()> {
let authifier = db.clone().to_authifier().await;
let mut account = authifier
.database
.find_account(&self.id)
.await
.map_err(|_| create_error!(InternalError))?;
account
.disable(&authifier)
.await
.map_err(|_| create_error!(InternalError))?;
account
.delete_all_sessions(&authifier, None)
.await
.map_err(|_| create_error!(InternalError))?;
self.update(
db,
PartialUser {
flags: Some(UserFlags::SuspendedUntil as i32),
suspended_until: duration_days.and_then(|dur| {
Timestamp::now_utc().checked_add(iso8601_timestamp::Duration::days(dur as i64))
}),
..Default::default()
},
vec![],
)
.await?;
if let Some(reason) = reason {
if let EmailVerificationConfig::Enabled { smtp, .. } =
authifier.config.email_verification
{
smtp.send_email(
account.email.clone(),
// maybe move this to common area?
&Template {
title: "Account Suspension".to_string(),
html: Some(include_str!("../../../templates/suspension.html").to_owned()),
text: include_str!("../../../templates/suspension.txt").to_owned(),
url: Default::default(),
},
json!({
"email": account.email,
"list": reason.join(", "),
"duration": duration_days,
"duration_diplay": if duration_days.is_some() {
"block"
} else {
"none"
}
}),
)
.map_err(|_| create_error!(InternalError))?;
}
}
Ok(())
}
/// Unsuspend the user
pub async fn unsuspend(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
flags: Some(0),
suspended_until: None,
..Default::default()
},
vec![],
)
.await?;
unimplemented!()
}
/// Permanently ban the user
///
/// - If a reason is specified, an email will be sent.
pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
// Send ban email (if reason provided)
unimplemented!()
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
@@ -685,6 +786,7 @@ impl User {
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
FieldsUser::Suspension,
],
)
.await

View File

@@ -346,6 +346,8 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
FieldsUser::Suspension => "suspended_until",
FieldsUser::None => "none",
})
}
}

View File

@@ -8,7 +8,7 @@ static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(|
/// Get sender
pub fn sender() -> Sender<AuthifierEvent> {
Q.0
Q.0.clone()
}
/// Start a new worker

View File

@@ -15,7 +15,7 @@ pub mod process_embeds;
pub mod web_push;
/// Spawn background workers
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
pub fn start_workers(db: Database, authifier_db: authifier::Database) {
task::spawn(authifier_relay::worker());
task::spawn(apple_notifications::worker(db.clone()));

View File

@@ -1171,6 +1171,7 @@ impl From<User> for crate::User {
flags: Some(value.flags as i32),
privileged: value.privileged,
bot: value.bot.map(Into::into),
suspended_until: None,
}
}
}
@@ -1209,6 +1210,8 @@ impl From<FieldsUser> for crate::FieldsUser {
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
FieldsUser::Internal => crate::FieldsUser::None,
}
}
}
@@ -1222,6 +1225,9 @@ impl From<crate::FieldsUser> for FieldsUser {
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
crate::FieldsUser::Suspension => FieldsUser::Internal,
crate::FieldsUser::None => FieldsUser::Internal,
}
}
}
@@ -1338,4 +1344,4 @@ impl From<FieldsMessage> for crate::FieldsMessage {
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
}
}
}
}