diff --git a/Cargo.lock b/Cargo.lock index 1a41f52f..29ff7fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,7 +342,7 @@ dependencies = [ "chrono", "futures", "handlebars", - "iso8601-timestamp", + "iso8601-timestamp 0.1.10", "lazy_static", "lettre", "log", @@ -1605,6 +1605,17 @@ dependencies = [ "time 0.3.17", ] +[[package]] +name = "iso8601-timestamp" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368d05885f276773b79b4578869a60ab91e880415d17fb2a8d193aea74188a29" +dependencies = [ + "generic-array 0.14.5", + "serde", + "time 0.3.17", +] + [[package]] name = "itoa" version = "0.4.8" @@ -2851,6 +2862,7 @@ dependencies = [ "authifier", "bson", "futures", + "iso8601-timestamp 0.2.10", "log", "mongodb", "nanoid", @@ -2956,7 +2968,7 @@ dependencies = [ "futures", "impl_ops", "indexmap", - "iso8601-timestamp", + "iso8601-timestamp 0.1.10", "linkify 0.8.1", "log", "lru", @@ -3129,7 +3141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4554a81937341d8c3cd3e43431ac2d23624444dc3a88e7ffd3ff66593779293" dependencies = [ "authifier", - "iso8601-timestamp", + "iso8601-timestamp 0.1.10", "revolt_okapi", "revolt_rocket_okapi", "rocket", diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index a5d6487f..b92bb4fa 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -34,6 +34,7 @@ once_cell = "1.17" serde_json = "1" revolt_optional_struct = "0.2.0" serde = { version = "1", features = ["derive"] } +iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } # Database bson = { optional = true, version = "2.1.0" } diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index 6b5c7674..d58f58da 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use futures::lock::Mutex; -use crate::{Bot, File, Server, User, UserSettings}; +use crate::{Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; database_derived!( /// Reference implementation @@ -11,11 +11,11 @@ database_derived!( pub bots: Arc>>, pub user_settings: Arc>>, pub users: Arc>>, + pub server_members: Arc>>, pub servers: Arc>>, pub files: Arc>>, pub server_bans: Arc>>, - pub server_members: Arc>>, pub safety_reports: Arc>>, pub safety_snapshots: Arc>>, pub emoji: Arc>>, diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index 1a981ba6..d4ff1ad7 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 server_members; mod servers; mod user_settings; mod users; @@ -8,6 +9,7 @@ mod users; pub use admin_migrations::*; pub use bots::*; pub use files::*; +pub use server_members::*; pub use servers::*; pub use user_settings::*; pub use users::*; @@ -20,6 +22,7 @@ pub trait AbstractDatabase: + admin_migrations::AbstractMigrations + bots::AbstractBots + files::AbstractAttachments + + server_members::AbstractServerMembers + servers::AbstractServers + user_settings::AbstractUserSettings + users::AbstractUsers diff --git a/crates/core/database/src/models/server_members/mod.rs b/crates/core/database/src/models/server_members/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/server_members/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs new file mode 100644 index 00000000..629ffac4 --- /dev/null +++ b/crates/core/database/src/models/server_members/model.rs @@ -0,0 +1,118 @@ +use iso8601_timestamp::Timestamp; +use revolt_result::Result; + +use crate::{Database, File, Server}; + +auto_derived_partial!( + /// Server Member + pub struct Member { + /// Unique member id + #[serde(rename = "_id")] + pub id: MemberCompositeKey, + + /// Time at which this user joined the server + #[serde(skip_serializing_if = "Option::is_none")] + pub joined_at: Option, + + /// Member's nickname + #[serde(skip_serializing_if = "Option::is_none")] + pub nickname: Option, + /// Avatar attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + /// Member's roles + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub roles: Vec, + /// Timestamp this member is timed out until + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + }, + "PartialMember" +); + +auto_derived!( + /// Composite primary key consisting of server and user id + #[derive(Hash, Default)] + pub struct MemberCompositeKey { + /// Server Id + pub server: String, + /// User Id + pub user: String, + } + + /// Optional fields on server member object + pub enum FieldsMember { + Nickname, + Avatar, + Roles, + Timeout, + } + + /// Member removal intention + pub enum RemovalIntention { + Leave, + Kick, + Ban, + } +); + +impl Member { + /// Update member data + pub async fn update<'a>( + &mut self, + db: &Database, + partial: PartialMember, + remove: Vec, + ) -> Result<()> { + for field in &remove { + self.remove_field(field); + } + + self.apply_options(partial.clone()); + + db.update_member(&self.id, &partial, remove.clone()).await?; + + /* // TODO: EventV1::ServerMemberUpdate { + id: self.id.clone(), + data: partial, + clear: remove, + } + .p(self.id.server.clone()) + .await; */ + + Ok(()) + } + + pub fn remove_field(&mut self, field: &FieldsMember) { + match field { + FieldsMember::Avatar => self.avatar = None, + FieldsMember::Nickname => self.nickname = None, + FieldsMember::Roles => self.roles.clear(), + FieldsMember::Timeout => self.timeout = None, + } + } + + /// Get this user's current ranking + pub fn get_ranking(&self, server: &Server) -> i64 { + let mut value = i64::MAX; + for role in &self.roles { + if let Some(role) = server.roles.get(role) { + if role.rank < value { + value = role.rank; + } + } + } + + value + } + + /// Check whether this member is in timeout + pub fn in_timeout(&self) -> bool { + if let Some(timeout) = self.timeout { + *timeout > *Timestamp::now_utc() + } else { + false + } + } +} diff --git a/crates/core/database/src/models/server_members/ops.rs b/crates/core/database/src/models/server_members/ops.rs new file mode 100644 index 00000000..369f43e2 --- /dev/null +++ b/crates/core/database/src/models/server_members/ops.rs @@ -0,0 +1,41 @@ +use revolt_result::Result; + +use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; + +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractServerMembers: Sync + Send { + /// Insert a new server member into the database + async fn insert_member(&self, member: &Member) -> Result<()>; + + /// Fetch a server member by their id + async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result; + + /// Fetch all members in a server + async fn fetch_all_members<'a>(&self, server_id: &str) -> Result>; + + /// Fetch all memberships for a user + async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result>; + + /// Fetch multiple members by their ids + async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result>; + + /// Fetch member count of a server + async fn fetch_member_count(&self, server_id: &str) -> Result; + + /// Fetch server count of a user + async fn fetch_server_count(&self, user_id: &str) -> Result; + + /// Update information for a server member + async fn update_member( + &self, + id: &MemberCompositeKey, + partial: &PartialMember, + remove: Vec, + ) -> Result<()>; + + /// Delete a server member by their id + async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>; +} diff --git a/crates/core/database/src/models/server_members/ops/mongodb.rs b/crates/core/database/src/models/server_members/ops/mongodb.rs new file mode 100644 index 00000000..8ab52679 --- /dev/null +++ b/crates/core/database/src/models/server_members/ops/mongodb.rs @@ -0,0 +1,178 @@ +use futures::StreamExt; +use revolt_result::Result; + +use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; +use crate::{IntoDocumentPath, MongoDb}; + +use super::AbstractServerMembers; + +static COL: &str = "server_members"; + +#[async_trait] +impl AbstractServerMembers for MongoDb { + /// Insert a new server member into the database + async fn insert_member(&self, member: &Member) -> Result<()> { + query!(self, insert_one, COL, &member).map(|_| ()) + } + + /// Fetch a server member by their id + async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result { + query!( + self, + find_one, + COL, + doc! { + "_id.server": server_id, + "_id.user": user_id + } + )? + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch all members in a server + async fn fetch_all_members<'a>(&self, server_id: &str) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "_id.server": server_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) + } + + /// Fetch all memberships for a user + async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "_id.user": 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) + } + + /// Fetch multiple members by their ids + async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "_id.server": server_id, + "_id.user": { + "$in": ids + } + }, + 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) + } + + /// Fetch member count of a server + async fn fetch_member_count(&self, server_id: &str) -> Result { + self.col::(COL) + .count_documents( + doc! { + "_id.server": server_id + }, + None, + ) + .await + .map(|c| c as usize) + .map_err(|_| create_database_error!("count_documents", COL)) + } + + /// Fetch server count of a user + async fn fetch_server_count(&self, user_id: &str) -> Result { + self.col::(COL) + .count_documents( + doc! { + "_id.user": user_id + }, + None, + ) + .await + .map(|c| c as usize) + .map_err(|_| create_database_error!("count_documents", COL)) + } + + /// Update information for a server member + async fn update_member( + &self, + id: &MemberCompositeKey, + partial: &PartialMember, + remove: Vec, + ) -> Result<()> { + query!( + self, + update_one, + COL, + doc! { + "_id.server": &id.server, + "_id.user": &id.user + }, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None + ) + .map(|_| ()) + } + + /// Delete a server member by their id + async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> { + query!( + self, + delete_one, + COL, + doc! { + "_id.server": &id.server, + "_id.user": &id.user + } + ) + .map(|_| ()) + } +} + +impl IntoDocumentPath for FieldsMember { + fn as_path(&self) -> Option<&'static str> { + Some(match self { + FieldsMember::Avatar => "avatar", + FieldsMember::Nickname => "nickname", + FieldsMember::Roles => "roles", + FieldsMember::Timeout => "timeout", + }) + } +} diff --git a/crates/core/database/src/models/server_members/ops/reference.rs b/crates/core/database/src/models/server_members/ops/reference.rs new file mode 100644 index 00000000..88c9c7b2 --- /dev/null +++ b/crates/core/database/src/models/server_members/ops/reference.rs @@ -0,0 +1,117 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; + +use super::AbstractServerMembers; + +#[async_trait] +impl AbstractServerMembers for ReferenceDb { + /// Insert a new server member into the database + async fn insert_member(&self, member: &Member) -> Result<()> { + let mut server_members = self.server_members.lock().await; + if server_members.contains_key(&member.id) { + Err(create_database_error!("insert", "member")) + } else { + server_members.insert(member.id.clone(), member.clone()); + Ok(()) + } + } + + /// Fetch a server member by their id + async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result { + let server_members = self.server_members.lock().await; + server_members + .get(&MemberCompositeKey { + server: server_id.to_string(), + user: user_id.to_string(), + }) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch all members in a server + async fn fetch_all_members<'a>(&self, server_id: &str) -> Result> { + let server_members = self.server_members.lock().await; + Ok(server_members + .values() + .filter(|member| member.id.server == server_id) + .cloned() + .collect()) + } + + /// Fetch all memberships for a user + async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result> { + let server_members = self.server_members.lock().await; + Ok(server_members + .values() + .filter(|member| member.id.user == user_id) + .cloned() + .collect()) + } + + /// Fetch multiple members by their ids + async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result> { + let server_members = self.server_members.lock().await; + ids.iter() + .map(|id| { + server_members + .get(&MemberCompositeKey { + server: server_id.to_string(), + user: id.to_string(), + }) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + }) + .collect() + } + + /// Fetch member count of a server + async fn fetch_member_count(&self, server_id: &str) -> Result { + let server_members = self.server_members.lock().await; + Ok(server_members + .values() + .filter(|member| member.id.server == server_id) + .count()) + } + + /// Fetch server count of a user + async fn fetch_server_count(&self, user_id: &str) -> Result { + let server_members = self.server_members.lock().await; + Ok(server_members + .values() + .filter(|member| member.id.user == user_id) + .count()) + } + + /// Update information for a server member + async fn update_member( + &self, + id: &MemberCompositeKey, + partial: &PartialMember, + remove: Vec, + ) -> Result<()> { + let mut server_members = self.server_members.lock().await; + if let Some(member) = server_members.get_mut(id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + member.remove_field(&field); + } + + member.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a server member by their id + async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> { + let mut server_members = self.server_members.lock().await; + if server_members.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } +} diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index 43ba859e..a650509d 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -406,6 +406,13 @@ impl Role { Ok(()) } + /// Remove field from Role + pub fn remove_field(&mut self, field: &FieldsRole) { + match field { + FieldsRole::Colour => self.colour = None, + } + } + /// Delete a role pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> { /* // TODO: EventV1::ServerRoleDelete { @@ -417,13 +424,6 @@ impl Role { db.delete_role(server_id, role_id).await } - - /// Remove field from Role - pub fn remove_field(&mut self, field: &FieldsRole) { - match field { - FieldsRole::Colour => self.colour = None, - } - } } impl SystemMessageChannels { diff --git a/crates/core/database/src/models/users/ops/mongodb.rs b/crates/core/database/src/models/users/ops/mongodb.rs index cfb27016..6bea5077 100644 --- a/crates/core/database/src/models/users/ops/mongodb.rs +++ b/crates/core/database/src/models/users/ops/mongodb.rs @@ -75,7 +75,7 @@ impl AbstractUsers for MongoDb { None, ) .await - .map_err(|_| create_database_error!("find", "users"))? + .map_err(|_| create_database_error!("find", COL))? .filter_map(|s| async { if cfg!(debug_assertions) { Some(s.unwrap()) @@ -101,7 +101,7 @@ impl AbstractUsers for MongoDb { FindOptions::builder().projection(doc! { "_id": 1 }).build(), ) .await - .map_err(|_| create_database_error!("find", "users"))? + .map_err(|_| create_database_error!("find", COL))? .filter_map(|s| async { s.ok() }) .map(|user| user.id) .collect() @@ -278,7 +278,7 @@ impl AbstractUsers for MongoDb { ) .await .map(|_| ()) - .map_err(|_| create_database_error!("update_one", "user")) + .map_err(|_| create_database_error!("update_one", COL)) } /// Delete a user by their id