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,
}
}
}
}

View File

@@ -0,0 +1,625 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.5.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
</style>
</head>
<body
class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<tbody>
<tr>
<td
valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<tbody>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
</td>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Account Suspended
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Your account has been suspended, for one
or more reasons:
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<ul class="">
{{list}}
</ul>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;"
class=""
align="left"
>
You will be able to use your account again
in {{duration}} days.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Further violations may result in a
permanent ban depending on severity,
please abide by the
<a
href="https://revolt.chat/aup"
style="color: #0d6efd"
>Acceptable Usage Policy</a
>.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Ban evasion is prohibited and will be
dealt with accordingly.
</p>
</td>
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Suspended</h1>
<p>Your account has been suspended, for one or more reasons:</p>
<ul>
{{list}}
</ul>
<p style="display:{{duration_display}}">
You will be able to use your account again in {{duration}} days.
</p>
<p>
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup">Acceptable Usage Policy</a>.
</p>
<p>Ban evasion is prohibited and will be dealt with accordingly.</p>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
Your account has been suspended, for one or more reasons:
{{list}}
You will be able to use your account again in {{duration}} days.
Further violations may result in a permanent ban depending on severity, please abide by the Acceptable Usage Policy (https://revolt.chat/aup).
Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}}
Sent by Revolt
Made in Europe