feat(core/database): implement server member

This commit is contained in:
Paul Makles
2023-04-26 18:58:31 +01:00
parent 1d228ff76e
commit 8b8aec4dc1
11 changed files with 490 additions and 15 deletions

18
Cargo.lock generated
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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<Mutex<HashMap<String, Bot>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub server_members: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,

View File

@@ -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

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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<Timestamp>,
/// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
/// Avatar attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// Member's roles
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
},
"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<FieldsMember>,
) -> 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
}
}
}

View File

@@ -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<Member>;
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize>;
/// Update information for a server member
async fn update_member(
&self,
id: &MemberCompositeKey,
partial: &PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}

View File

@@ -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<Member> {
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<Vec<Member>> {
Ok(self
.col::<Member>(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<Vec<Member>> {
Ok(self
.col::<Member>(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<Vec<Member>> {
Ok(self
.col::<Member>(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<usize> {
self.col::<Member>(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<usize> {
self.col::<Member>(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<FieldsMember>,
) -> 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",
})
}
}

View File

@@ -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<Member> {
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<Vec<Member>> {
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<Vec<Member>> {
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<Vec<Member>> {
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<usize> {
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<usize> {
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<FieldsMember>,
) -> 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))
}
}
}

View File

@@ -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 {

View File

@@ -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