From 08ccc43f8d18554bb1517e0b4653b125cbd6b5bd Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 26 Apr 2023 17:36:51 +0100 Subject: [PATCH] feat(core/database): implement user operations --- Cargo.lock | 1 + crates/core/database/Cargo.toml | 1 + crates/core/database/src/models/bots/model.rs | 2 +- .../core/database/src/models/users/model.rs | 99 +++++- crates/core/database/src/models/users/ops.rs | 47 ++- .../database/src/models/users/ops/mongodb.rs | 288 +++++++++++++++++- .../src/models/users/ops/reference.rs | 105 ++++++- 7 files changed, 537 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45d652c0..462af954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2854,6 +2854,7 @@ dependencies = [ "log", "mongodb", "nanoid", + "once_cell", "revolt-permissions", "revolt-result", "revolt_optional_struct", diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 562718ca..f2be94ab 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -27,6 +27,7 @@ revolt-permissions = { version = "0.0.2", path = "../permissions" } # Utility log = "0.4" nanoid = "0.4.0" +once_cell = "1.17" # Serialisation serde_json = "1" diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs index 1a9fa17e..f57a12e1 100644 --- a/crates/core/database/src/models/bots/model.rs +++ b/crates/core/database/src/models/bots/model.rs @@ -84,7 +84,7 @@ impl Bot { /// Delete this bot pub async fn delete(&self, db: &Database) -> Result<()> { - // db.fetch_user(&self.id).await?.mark_deleted(db).await?; + db.fetch_user(&self.id).await?.mark_deleted(db).await?; db.delete_bot(&self.id).await } } diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index ebdc2ecb..eae6c927 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -1,4 +1,6 @@ -use crate::File; +use crate::{Database, File}; + +use revolt_result::{Error, ErrorType, Result}; auto_derived_partial!( /// # User @@ -96,4 +98,99 @@ auto_derived!( /// Id of the owner of this bot pub owner: String, } + + /// Optional fields on user object + pub enum FieldsUser { + Avatar, + StatusText, + StatusPresence, + ProfileContent, + ProfileBackground, + } ); + +impl User { + /// Check whether a username is already in use by another user + async fn is_username_taken(db: &Database, username: &str) -> Result { + match db.fetch_user_by_username(username).await { + Ok(_) => Ok(true), + Err(Error { + error_type: ErrorType::NotFound, + .. + }) => Ok(false), + Err(error) => Err(error), + } + } + + /// Update user data + pub async fn update<'a>( + &mut self, + db: &Database, + partial: PartialUser, + remove: Vec, + ) -> Result<()> { + for field in &remove { + self.remove_field(field); + } + + self.apply_options(partial.clone()); + db.update_user(&self.id, &partial, remove.clone()).await?; + + /* // TODO: EventV1::UserUpdate { + id: self.id.clone(), + data: partial, + clear: remove, + } + .p_user(self.id.clone(), db) + .await; */ + + Ok(()) + } + + /// Remove a field from User object + pub fn remove_field(&mut self, field: &FieldsUser) { + match field { + FieldsUser::Avatar => self.avatar = None, + FieldsUser::StatusText => { + if let Some(x) = self.status.as_mut() { + x.text = String::new(); + } + } + FieldsUser::StatusPresence => { + if let Some(x) = self.status.as_mut() { + x.presence = None; + } + } + FieldsUser::ProfileContent => { + if let Some(x) = self.profile.as_mut() { + x.content = String::new(); + } + } + FieldsUser::ProfileBackground => { + if let Some(x) = self.profile.as_mut() { + x.background = None; + } + } + } + } + + /// Mark as deleted + pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> { + self.update( + db, + PartialUser { + username: Some(format!("Deleted User {}", self.id)), + flags: Some(2), + ..Default::default() + }, + vec![ + FieldsUser::Avatar, + FieldsUser::StatusText, + FieldsUser::StatusPresence, + FieldsUser::ProfileContent, + FieldsUser::ProfileBackground, + ], + ) + .await + } +} diff --git a/crates/core/database/src/models/users/ops.rs b/crates/core/database/src/models/users/ops.rs index ca2662ed..28421ec0 100644 --- a/crates/core/database/src/models/users/ops.rs +++ b/crates/core/database/src/models/users/ops.rs @@ -1,12 +1,57 @@ use revolt_result::Result; -use crate::User; +use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; mod mongodb; mod reference; #[async_trait] pub trait AbstractUsers: Sync + Send { + /// Insert a new user into the database + async fn insert_user(&self, user: &User) -> Result<()>; + /// Fetch a user from the database async fn fetch_user(&self, id: &str) -> Result; + + /// Fetch a user from the database by their username + async fn fetch_user_by_username(&self, username: &str) -> Result; + + /// Fetch a user from the database by their session token + async fn fetch_user_by_token(&self, token: &str) -> Result; + + /// Fetch multiple users by their ids + async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result>; + + /// Fetch ids of users that both users are friends with + async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result>; + + /// Fetch ids of channels that both users are in + async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result>; + + /// Fetch ids of servers that both users share + async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result>; + + /// Update a user by their id given some data + async fn update_user( + &self, + id: &str, + user: &PartialUser, + remove: Vec, + ) -> Result<()>; + + /// Set relationship with another user + /// + /// This should use pull_relationship if relationship is None. + async fn set_relationship( + &self, + user_id: &str, + target_id: &str, + relationship: &RelationshipStatus, + ) -> Result<()>; + + /// Remove relationship with another user + async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()>; + + /// Delete a user by their id + async fn delete_user(&self, id: &str) -> Result<()>; } diff --git a/crates/core/database/src/models/users/ops/mongodb.rs b/crates/core/database/src/models/users/ops/mongodb.rs index c6e43c98..42cf4af1 100644 --- a/crates/core/database/src/models/users/ops/mongodb.rs +++ b/crates/core/database/src/models/users/ops/mongodb.rs @@ -1,16 +1,300 @@ +use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions}; +use authifier::models::Session; +use futures::StreamExt; use revolt_result::Result; +use crate::DocumentId; +use crate::IntoDocumentPath; use crate::MongoDb; -use crate::User; +use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use super::AbstractUsers; -static COL: &str = "bots"; +static COL: &str = "users"; #[async_trait] impl AbstractUsers for MongoDb { + /// Insert a new user into the database + async fn insert_user(&self, user: &User) -> Result<()> { + query!(self, insert_one, COL, &user).map(|_| ()) + } + /// Fetch a user from the database async fn fetch_user(&self, id: &str) -> Result { query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound)) } + + /// Fetch a user from the database by their username + async fn fetch_user_by_username(&self, username: &str) -> Result { + query!( + self, + find_one_with_options, + COL, + doc! { + "username": username + }, + FindOneOptions::builder() + .collation( + Collation::builder() + .locale("en") + .strength(CollationStrength::Secondary) + .build(), + ) + .build() + )? + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a user from the database by their session token + async fn fetch_user_by_token(&self, token: &str) -> Result { + let session = self + .col::("sessions") + .find_one( + doc! { + "token": token + }, + None, + ) + .await + .map_err(|_| create_database_error!("find_one", "sessions"))? + .ok_or_else(|| create_error!(InvalidSession))?; + + self.fetch_user(&session.id).await + } + + /// Fetch multiple users by their ids + async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "_id": { + "$in": ids + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("find", "user"))? + .filter_map(|s| async { + if cfg!(debug_assertions) { + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect() + .await) + } + + /// Fetch ids of users that both users are friends with + async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "$and": [ + { "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } }, + { "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } } + ] + }, + FindOptions::builder().projection(doc! { "_id": 1 }).build(), + ) + .await + .map_err(|_| create_database_error!("find", "users"))? + .filter_map(|s| async { s.ok() }) + .map(|user| user.id) + .collect() + .await) + } + + /// Fetch ids of channels that both users are in + async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result> { + Ok(self + .col::("channels") + .find( + doc! { + "channel_type": { + "$in": ["Group", "DirectMessage"] + }, + "recipients": { + "$all": [ user_a, user_b ] + } + }, + FindOptions::builder().projection(doc! { "_id": 1 }).build(), + ) + .await + .map_err(|_| create_database_error!("find", "channels"))? + .filter_map(|s| async { s.ok() }) + .map(|user| user.id) + .collect() + .await) + } + + /// Fetch ids of servers that both users share + async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result> { + Ok(self + .col::("server_members") + .aggregate( + vec![ + doc! { + "$match": { + "_id.user": user_a + } + }, + doc! { + "$lookup": { + "from": "server_members", + "as": "members", + "let": { + "server": "$_id.server" + }, + "pipeline": [ + { + "$match": { + "$expr": { + "$and": [ + { "$eq": [ "$_id.user", user_b ] }, + { "$eq": [ "$_id.server", "$$server" ] } + ] + } + } + } + ] + } + }, + doc! { + "$match": { + "members": { + "$size": 1_i32 + } + } + }, + doc! { + "$project": { + "_id": "$_id.server" + } + }, + ], + None, + ) + .await + .map_err(|_| create_database_error!("aggregate", "server_members"))? + .filter_map(|s| async { s.ok() }) + .filter_map(|doc| async move { doc.get_str("_id").map(|id| id.to_string()).ok() }) + .collect() + .await) + } + + /// Update a user by their id given some data + async fn update_user( + &self, + id: &str, + partial: &PartialUser, + remove: Vec, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None + ) + .map(|_| ()) + } + + /// Set relationship with another user + /// + /// This should use pull_relationship if relationship is None. + async fn set_relationship( + &self, + user_id: &str, + target_id: &str, + relationship: &RelationshipStatus, + ) -> Result<()> { + if let RelationshipStatus::None = relationship { + return self.pull_relationship(user_id, target_id).await; + } + + self.col::(COL) + .update_one( + doc! { + "_id": user_id + }, + vec![doc! { + "$set": { + "relations": { + "$concatArrays": [ + { + "$ifNull": [ + { + "$filter": { + "input": "$relations", + "cond": { + "$ne": [ + "$$this._id", + target_id + ] + } + } + }, + [] + ] + }, + [ + { + "_id": target_id, + "status": format!("{relationship:?}") + } + ] + ] + } + } + }], + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", "user")) + } + + /// Remove relationship with another user + async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": user_id + }, + doc! { + "$pull": { + "relations": { + "_id": target_id + } + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", "user")) + } + + /// Delete a user by their id + async fn delete_user(&self, id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, id).map(|_| ()) + } +} + +impl IntoDocumentPath for FieldsUser { + fn as_path(&self) -> Option<&'static str> { + Some(match self { + FieldsUser::Avatar => "avatar", + FieldsUser::ProfileBackground => "profile.background", + FieldsUser::ProfileContent => "profile.content", + FieldsUser::StatusPresence => "status.presence", + FieldsUser::StatusText => "status.text", + }) + } } diff --git a/crates/core/database/src/models/users/ops/reference.rs b/crates/core/database/src/models/users/ops/reference.rs index 034f4867..d75af7a6 100644 --- a/crates/core/database/src/models/users/ops/reference.rs +++ b/crates/core/database/src/models/users/ops/reference.rs @@ -1,12 +1,23 @@ use revolt_result::Result; use crate::ReferenceDb; -use crate::User; +use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use super::AbstractUsers; #[async_trait] impl AbstractUsers for ReferenceDb { + /// Insert a new user into the database + async fn insert_user(&self, user: &User) -> Result<()> { + let mut users = self.users.lock().await; + if users.contains_key(&user.id) { + Err(create_database_error!("insert", "user")) + } else { + users.insert(user.id.to_string(), user.clone()); + Ok(()) + } + } + /// Fetch a user from the database async fn fetch_user(&self, id: &str) -> Result { let users = self.users.lock().await; @@ -15,4 +26,96 @@ impl AbstractUsers for ReferenceDb { .cloned() .ok_or_else(|| create_error!(NotFound)) } + + /// Fetch a user from the database by their username + async fn fetch_user_by_username(&self, username: &str) -> Result { + let users = self.users.lock().await; + let lowercase = username.to_lowercase(); + users + .values() + .find(|user| user.username.to_lowercase() == lowercase) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a user from the database by their session token + async fn fetch_user_by_token(&self, _token: &str) -> Result { + todo!() + } + + /// Fetch multiple users by their ids + async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result> { + let users = self.users.lock().await; + ids.iter() + .map(|id| { + users + .get(id) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + }) + .collect() + } + + /// Fetch ids of users that both users are friends with + async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result> { + todo!() + } + + /// Fetch ids of channels that both users are in + async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result> { + todo!() + } + + /// Fetch ids of servers that both users share + async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result> { + todo!() + } + + /// Update a user by their id given some data + async fn update_user( + &self, + id: &str, + partial: &PartialUser, + remove: Vec, + ) -> Result<()> { + let mut users = self.users.lock().await; + if let Some(user) = users.get_mut(id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + user.remove_field(&field); + } + + user.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Set relationship with another user + /// + /// This should use pull_relationship if relationship is None. + async fn set_relationship( + &self, + _user_id: &str, + _target_id: &str, + _relationship: &RelationshipStatus, + ) -> Result<()> { + todo!() + } + + /// Remove relationship with another user + async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> { + todo!() + } + + /// Delete a user by their id + async fn delete_user(&self, id: &str) -> Result<()> { + let mut users = self.users.lock().await; + if users.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } }