diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index d58f58da..9f562b36 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -2,12 +2,13 @@ use std::{collections::HashMap, sync::Arc}; use futures::lock::Mutex; -use crate::{Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; +use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; database_derived!( /// Reference implementation #[derive(Default)] pub struct ReferenceDb { + pub account_strikes: Arc>>, pub bots: Arc>>, pub user_settings: Arc>>, pub users: Arc>>, diff --git a/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs index 2649635d..352d04ad 100644 --- a/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs @@ -64,6 +64,10 @@ pub async fn create_database(db: &MongoDb) { .await .expect("Failed to create safety_snapshots collection."); + db.create_collection("safety_strikes", None) + .await + .expect("Failed to create safety_strikes collection."); + db.create_collection("bots", None) .await .expect("Failed to create bots collection."); diff --git a/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs index 8c5aa874..417749d9 100644 --- a/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs @@ -16,7 +16,7 @@ struct MigrationInfo { revision: i32, } -pub const LATEST_REVISION: i32 = 21; +pub const LATEST_REVISION: i32 = 22; pub async fn migrate_database(db: &MongoDb) { let migrations = db.col::("migrations"); @@ -696,26 +696,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { if revision <= 19 { info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable."); - // TODO: make these fail once production is migrated - if db - .db() + db.db() .create_collection("safety_reports", None) .await - .is_err() - { - info!("Failed to create safety_reports collection but this is expected in production."); - } + .unwrap(); - if db - .db() + db.db() .create_collection("safety_snapshots", None) .await - .is_err() - { - info!( - "Failed to create safety_snapshots collection but this is expected in production." - ); - } + .unwrap(); db.col::("safety_reports") .update_many( @@ -753,6 +742,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { .expect("Failed to create safety snapshot index."); } + if revision <= 21 { + info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`."); + + db.db() + .create_collection("safety_strikes", None) + .await + .unwrap(); + } + // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Reminder to update LATEST_REVISION when adding new migrations. diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index d4ff1ad7..badc2c24 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -1,6 +1,7 @@ mod admin_migrations; mod bots; mod files; +mod safety_strikes; mod server_members; mod servers; mod user_settings; @@ -9,6 +10,7 @@ mod users; pub use admin_migrations::*; pub use bots::*; pub use files::*; +pub use safety_strikes::*; pub use server_members::*; pub use servers::*; pub use user_settings::*; @@ -22,6 +24,7 @@ pub trait AbstractDatabase: + admin_migrations::AbstractMigrations + bots::AbstractBots + files::AbstractAttachments + + safety_strikes::AbstractAccountStrikes + server_members::AbstractServerMembers + servers::AbstractServers + user_settings::AbstractUserSettings diff --git a/crates/core/database/src/models/safety_strikes/mod.rs b/crates/core/database/src/models/safety_strikes/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/safety_strikes/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/safety_strikes/model.rs b/crates/core/database/src/models/safety_strikes/model.rs new file mode 100644 index 00000000..09bb3d97 --- /dev/null +++ b/crates/core/database/src/models/safety_strikes/model.rs @@ -0,0 +1,104 @@ +use revolt_result::Result; + +use crate::Database; + +auto_derived_partial!( + /// Account Strike + pub struct AccountStrike { + /// Strike Id + #[serde(rename = "_id")] + pub id: String, + /// User Id of reported user + pub user_id: String, + + /// Attached reason + pub reason: String, + }, + "PartialAccountStrike" +); + +impl AccountStrike { + /// Update this strike + pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> { + db.update_account_strike(&self.id, &partial).await?; + self.apply_options(partial); + Ok(()) + } + + /// Delete this strike + pub async fn delete(&self, db: &Database) -> Result<()> { + db.delete_account_strike(&self.id).await + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use crate::{AccountStrike, PartialAccountStrike}; + + #[async_std::test] + async fn crud() { + database_test!(|db| async move { + let user_id = "user"; + let strike_a = "a"; + let strike_b = "b"; + + let strike = AccountStrike { + id: strike_a.to_string(), + user_id: user_id.to_string(), + reason: "reason 1".to_string(), + }; + + db.insert_account_strike(&strike).await.unwrap(); + + let mut updated_strike = strike.clone(); + updated_strike + .update( + &db, + PartialAccountStrike { + reason: Some("new reason".to_string()), + ..Default::default() + }, + ) + .await + .unwrap(); + + db.insert_account_strike(&AccountStrike { + id: strike_b.to_string(), + user_id: user_id.to_string(), + reason: "reason 2".to_string(), + }) + .await + .unwrap(); + + let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap(); + + let ids = strikes + .iter() + .cloned() + .map(|strike| strike.id) + .collect::>(); + + assert!(ids.contains(strike_a)); + assert!(ids.contains(strike_b)); + + let fetched_strike = strikes + .into_iter() + .find(|strike| strike.id == strike_a) + .unwrap(); + + assert_eq!(fetched_strike, updated_strike); + assert_ne!(fetched_strike, strike); + + strike.delete(&db).await.unwrap(); + assert_eq!( + 1, + db.fetch_account_strikes_by_user(user_id) + .await + .unwrap() + .len() + ) + }); + } +} diff --git a/crates/core/database/src/models/safety_strikes/ops.rs b/crates/core/database/src/models/safety_strikes/ops.rs new file mode 100644 index 00000000..d0dcb60f --- /dev/null +++ b/crates/core/database/src/models/safety_strikes/ops.rs @@ -0,0 +1,21 @@ +use revolt_result::Result; + +use crate::{AccountStrike, PartialAccountStrike}; + +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractAccountStrikes: Sync + Send { + /// Insert new strike into the database + async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>; + + /// Fetch strikes by user id + async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result>; + + /// Update strike with new information + async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>; + + /// Delete a strike from the database + async fn delete_account_strike(&self, id: &str) -> Result<()>; +} diff --git a/crates/core/database/src/models/safety_strikes/ops/mongodb.rs b/crates/core/database/src/models/safety_strikes/ops/mongodb.rs new file mode 100644 index 00000000..816b69b2 --- /dev/null +++ b/crates/core/database/src/models/safety_strikes/ops/mongodb.rs @@ -0,0 +1,50 @@ +use futures::StreamExt; +use revolt_result::Result; + +use crate::MongoDb; +use crate::{AccountStrike, PartialAccountStrike}; + +use super::AbstractAccountStrikes; + +static COL: &str = "bots"; + +#[async_trait] +impl AbstractAccountStrikes for MongoDb { + /// Insert new strike into the database + async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> { + query!(self, insert_one, COL, &strike).map(|_| ()) + } + + /// Fetch strikes by user id + async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "user_id": user_id, + }, + None, + ) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|s| async { + if cfg!(debug_assertions) { + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect() + .await) + } + + /// Update strike with new information + async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> { + query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ()) + } + + /// Delete a strike from the database + async fn delete_account_strike(&self, id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, id).map(|_| ()) + } +} diff --git a/crates/core/database/src/models/safety_strikes/ops/reference.rs b/crates/core/database/src/models/safety_strikes/ops/reference.rs new file mode 100644 index 00000000..8ec00a60 --- /dev/null +++ b/crates/core/database/src/models/safety_strikes/ops/reference.rs @@ -0,0 +1,51 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{AccountStrike, PartialAccountStrike}; + +use super::AbstractAccountStrikes; + +#[async_trait] +impl AbstractAccountStrikes for ReferenceDb { + /// Insert new strike into the database + async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> { + let mut strikes = self.account_strikes.lock().await; + if strikes.contains_key(&strike.id) { + Err(create_database_error!("insert", "strike")) + } else { + strikes.insert(strike.id.to_string(), strike.clone()); + Ok(()) + } + } + + /// Fetch strikes by user id + async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result> { + let strikes = self.account_strikes.lock().await; + Ok(strikes + .values() + .filter(|strike| strike.user_id == user_id) + .cloned() + .collect()) + } + + /// Update strike with new information + async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> { + let mut strikes = self.account_strikes.lock().await; + if let Some(strike) = strikes.get_mut(id) { + strike.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a strike from the database + async fn delete_account_strike(&self, id: &str) -> Result<()> { + let mut strikes = self.account_strikes.lock().await; + if strikes.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } +}