From 01368960f3a3d5380a373ff98baa0c54b1c7dfd1 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sat, 31 Aug 2024 17:10:47 +0100 Subject: [PATCH] feat(core/database, bindings/node): suspend user --- Cargo.lock | 1 + crates/bindings/node/Cargo.toml | 1 + crates/bindings/node/index.d.ts | 18 + crates/bindings/node/package.json | 2 +- crates/bindings/node/src/lib.rs | 35 +- crates/core/config/Revolt.toml | 6 +- .../core/database/src/models/users/model.rs | 112 +++- .../database/src/models/users/ops/mongodb.rs | 2 + .../database/src/tasks/authifier_relay.rs | 2 +- crates/core/database/src/tasks/mod.rs | 2 +- crates/core/database/src/util/bridge/v0.rs | 8 +- .../core/database/templates/suspension.html | 625 ++++++++++++++++++ .../templates/suspension.original.html | 38 ++ crates/core/database/templates/suspension.txt | 12 + crates/core/models/src/v0/users.rs | 5 +- crates/delta/src/main.rs | 12 +- 16 files changed, 856 insertions(+), 25 deletions(-) create mode 100644 crates/core/database/templates/suspension.html create mode 100644 crates/core/database/templates/suspension.original.html create mode 100644 crates/core/database/templates/suspension.txt diff --git a/Cargo.lock b/Cargo.lock index 7e99f8a4..b5f4cf98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4851,6 +4851,7 @@ dependencies = [ "async-std", "neon", "neon-serde4", + "revolt-config", "revolt-database", "revolt-result", "serde", diff --git a/crates/bindings/node/Cargo.toml b/crates/bindings/node/Cargo.toml index 457d7fcc..b3982fd2 100644 --- a/crates/bindings/node/Cargo.toml +++ b/crates/bindings/node/Cargo.toml @@ -20,5 +20,6 @@ serde = { version = "1", features = ["derive"] } async-std = "1.12.0" +revolt-config = { version = "0.7.16", path = "../../core/config" } revolt-result = { version = "0.7.16", path = "../../core/result" } revolt-database = { version = "0.7.16", path = "../../core/database" } diff --git a/crates/bindings/node/index.d.ts b/crates/bindings/node/index.d.ts index a9f9ba91..0417ba4d 100644 --- a/crates/bindings/node/index.d.ts +++ b/crates/bindings/node/index.d.ts @@ -18,6 +18,12 @@ export declare interface Err { location: string; } +/** + * Initialises background tasks and logging, must be called before anything else! + * Can be called multiple times! + */ +export declare function init(); + /** * Gets a new handle to the Revolt database * @returns {Database} Handle @@ -64,3 +70,15 @@ export declare function proc_channels_create_dm( userA: string, userB: string ): Promise; + +/** + * Suspend a user + * @param {string} user User + * @param {number} duration Duration (in days), set to 0 for indefinite + * @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3) + */ +export declare function proc_users_suspend( + user: OpaqueUser, + duration: number, + reason: string +): Promise<{ error: Err }>; diff --git a/crates/bindings/node/package.json b/crates/bindings/node/package.json index 1bcb0ee7..a0cc5575 100644 --- a/crates/bindings/node/package.json +++ b/crates/bindings/node/package.json @@ -1,6 +1,6 @@ { "name": "revolt-nodejs-bindings", - "version": "0.7.15", + "version": "0.7.15-rev0.0.2", "description": "Node.js bindings for the Revolt software", "main": "index.node", "scripts": { diff --git a/crates/bindings/node/src/lib.rs b/crates/bindings/node/src/lib.rs index 7a01f743..a2030d73 100644 --- a/crates/bindings/node/src/lib.rs +++ b/crates/bindings/node/src/lib.rs @@ -1,9 +1,35 @@ #[macro_use] extern crate serde; +use std::sync::OnceLock; + use neon::prelude::*; use revolt_database::{Database, DatabaseInfo}; +fn js_init(mut cx: FunctionContext) -> JsResult { + static INIT: OnceLock<()> = OnceLock::new(); + if INIT.get().is_none() { + INIT.get_or_init(|| { + async_std::task::block_on(async { + revolt_config::configure!(api); + + match DatabaseInfo::Auto.connect().await { + Ok(db) => { + let authifier_db = db.clone().to_authifier().await.database; + revolt_database::tasks::start_workers(db, authifier_db); + Ok(()) + } + Err(err) => Err(err), + } + }) + .or_else(|err| cx.throw_error(err)) + .unwrap(); + }); + } + + Ok(cx.undefined()) +} + struct DatabaseBinding(Database, Channel); impl Finalize for DatabaseBinding {} impl DatabaseBinding { @@ -136,6 +162,9 @@ macro_rules! shim { #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { + // initialise required background stuff + cx.export_function("init", js_init)?; + // database & model stuff cx.export_function("database", js_database)?; cx.export_function("model_data", js_data)?; @@ -178,10 +207,12 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { shim!( cx, proc_users_suspend, - , + duration JsNumber 1 + reason JsString 2, user User 0, |db| async move { - user.suspend(&db).await + let duration = duration as usize; + user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await }, &user, ); diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 648b65de..feaebaf4 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -34,9 +34,9 @@ host = "" username = "" password = "" from_address = "noreply@example.com" -reply_to = "noreply@example.com" -port = 587 -use_tls = true +# reply_to = "noreply@example.com" +# port = 587 +# use_tls = true [api.vapid] # Generate your own keys: diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index 6a2557ac..0b476ad1 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -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, + + /// Time until user is unsuspended + #[serde(skip_serializing_if = "Option::is_none")] + pub suspended_until: Option, }, "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, + reason: Option>, + ) -> 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) -> 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 diff --git a/crates/core/database/src/models/users/ops/mongodb.rs b/crates/core/database/src/models/users/ops/mongodb.rs index 8e132eb1..ae61b029 100644 --- a/crates/core/database/src/models/users/ops/mongodb.rs +++ b/crates/core/database/src/models/users/ops/mongodb.rs @@ -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", }) } } diff --git a/crates/core/database/src/tasks/authifier_relay.rs b/crates/core/database/src/tasks/authifier_relay.rs index 2f2d3ea2..9dac7020 100644 --- a/crates/core/database/src/tasks/authifier_relay.rs +++ b/crates/core/database/src/tasks/authifier_relay.rs @@ -8,7 +8,7 @@ static Q: Lazy<(Sender, Receiver)> = Lazy::new(| /// Get sender pub fn sender() -> Sender { - Q.0 + Q.0.clone() } /// Start a new worker diff --git a/crates/core/database/src/tasks/mod.rs b/crates/core/database/src/tasks/mod.rs index c7d3e02a..d8988756 100644 --- a/crates/core/database/src/tasks/mod.rs +++ b/crates/core/database/src/tasks/mod.rs @@ -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())); diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index da1e8f11..ba3f01d8 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -1171,6 +1171,7 @@ impl From 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 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 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 for crate::FieldsMessage { FieldsMessage::Pinned => crate::FieldsMessage::Pinned, } } -} \ No newline at end of file +} diff --git a/crates/core/database/templates/suspension.html b/crates/core/database/templates/suspension.html new file mode 100644 index 00000000..fafd2b40 --- /dev/null +++ b/crates/core/database/templates/suspension.html @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/core/database/templates/suspension.original.html b/crates/core/database/templates/suspension.original.html new file mode 100644 index 00000000..624801e9 --- /dev/null +++ b/crates/core/database/templates/suspension.original.html @@ -0,0 +1,38 @@ + + + + + + +
+ Revolt Logo +
+

Account Suspended

+

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. +

+

Ban evasion is prohibited and will be dealt with accordingly.

+
+
+ This email is intended for {{email}}
+ Sent from Revolt
+ Made in Europe +
+
+ + diff --git a/crates/core/database/templates/suspension.txt b/crates/core/database/templates/suspension.txt new file mode 100644 index 00000000..ace1d25c --- /dev/null +++ b/crates/core/database/templates/suspension.txt @@ -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 diff --git a/crates/core/models/src/v0/users.rs b/crates/core/models/src/v0/users.rs index 58a7f2c4..ba913dd0 100644 --- a/crates/core/models/src/v0/users.rs +++ b/crates/core/models/src/v0/users.rs @@ -88,6 +88,9 @@ auto_derived!( ProfileContent, ProfileBackground, DisplayName, + + /// Internal field, ignore this. + Internal, } /// User's relationship with another user (or themselves) @@ -190,7 +193,7 @@ auto_derived!( #[repr(u32)] pub enum UserFlags { /// User has been suspended from the platform - Suspended = 1, + SuspendedUntil = 1, /// User has deleted their account Deleted = 2, /// User was banned off the platform diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 3797368d..fc5b33d5 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -10,7 +10,6 @@ pub mod util; use revolt_config::config; use revolt_database::events::client::EventV1; -use revolt_database::{Database, MongoDb}; use rocket::{Build, Rocket}; use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_prometheus::PrometheusMetrics; @@ -18,11 +17,7 @@ use std::net::Ipv4Addr; use std::str::FromStr; use async_std::channel::unbounded; -use authifier::config::{ - Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield, - Template, Templates, -}; -use authifier::{Authifier, AuthifierEvent}; +use authifier::AuthifierEvent; use rocket::data::ToByteUnit; pub async fn web() -> Rocket { @@ -59,10 +54,7 @@ pub async fn web() -> Rocket { }); // Launch background task workers - async_std::task::spawn(revolt_database::tasks::start_workers( - db.clone(), - authifier.database.clone(), - )); + revolt_database::tasks::start_workers(db.clone(), authifier.database.clone()); // Configure CORS let cors = CorsOptions {