feat(core/database): implement server model

This commit is contained in:
Paul Makles
2023-04-26 18:32:29 +01:00
parent b5863c0104
commit 1d228ff76e
11 changed files with 831 additions and 6 deletions

View File

@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{Bot, File, User, UserSettings};
use crate::{Bot, File, Server, User, UserSettings};
database_derived!(
/// Reference implementation
@@ -11,9 +11,9 @@ 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 servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub servers: Arc<Mutex<HashMap<String, ()>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub server_members: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,

View File

@@ -1,12 +1,14 @@
mod admin_migrations;
mod bots;
mod files;
mod servers;
mod user_settings;
mod users;
pub use admin_migrations::*;
pub use bots::*;
pub use files::*;
pub use servers::*;
pub use user_settings::*;
pub use users::*;
@@ -18,6 +20,7 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ files::AbstractAttachments
+ 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,451 @@
use std::collections::{HashMap, HashSet};
use revolt_permissions::OverrideField;
use revolt_result::Result;
use ulid::Ulid;
use crate::{Database, File};
auto_derived_partial!(
/// Server
pub struct Server {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// User id of the owner
pub owner: String,
/// Name of the server
pub name: String,
/// Description for the server
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
pub channels: Vec<String>,
/// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
/// Configuration for sending system event messages
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
/// Roles for this server
#[serde(
default = "HashMap::<String, Role>::new",
skip_serializing_if = "HashMap::<String, Role>::is_empty"
)]
pub roles: HashMap<String, Role>,
/// Default set of server and channel permissions
pub default_permissions: i64,
/// Icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
/// Banner attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>,
/// Bitfield of server flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
/// Whether this server is flagged as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
/// Whether to enable analytics
#[serde(skip_serializing_if = "crate::if_false", default)]
pub analytics: bool,
/// Whether this server should be publicly discoverable
#[serde(skip_serializing_if = "crate::if_false", default)]
pub discoverable: bool,
},
"PartialServer"
);
auto_derived_partial!(
/// Role
pub struct Role {
/// Role name
pub name: String,
/// Permissions available to this role
pub permissions: OverrideField,
/// Colour used for this role
///
/// This can be any valid CSS colour
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
/// Whether this role should be shown separately on the member sidebar
#[serde(skip_serializing_if = "crate::if_false", default)]
pub hoist: bool,
/// Ranking of this role
#[serde(default)]
pub rank: i64,
},
"PartialRole"
);
auto_derived!(
/// Channel category
pub struct Category {
/// Unique ID for this category
pub id: String,
/// Title for this category
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
}
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_joined: Option<String>,
/// ID of channel to send user left messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_left: Option<String>,
/// ID of channel to send user kicked messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_kicked: Option<String>,
/// ID of channel to send user banned messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_banned: Option<String>,
}
/// Optional fields on server object
pub enum FieldsServer {
Description,
Categories,
SystemMessages,
Icon,
Banner,
}
/// Optional fields on server object
pub enum FieldsRole {
Colour,
}
);
impl Server {
/// Create a server
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_server(self).await
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_server(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p(self.id.clone())
.await; */
Ok(())
}
/// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> {
/* // TODO: EventV1::ServerDelete {
id: self.id.clone(),
}
.p(self.id.clone())
.await; */
db.delete_server(&self.id).await
}
/// Remove a field from Server
pub fn remove_field(&mut self, field: &FieldsServer) {
match field {
FieldsServer::Description => self.description = None,
FieldsServer::Categories => self.categories = None,
FieldsServer::SystemMessages => self.system_messages = None,
FieldsServer::Icon => self.icon = None,
FieldsServer::Banner => self.banner = None,
}
}
/// Set role permission on a server
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
if let Some(role) = self.roles.get_mut(role_id) {
role.update(
db,
&self.id,
role_id,
PartialRole {
permissions: Some(permissions),
..Default::default()
},
vec![],
)
.await?;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/* /// Create a new member in a server
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server
pub async fn remove_member(
&self,
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
/* // TODO: EventV1::ServerMemberLeave {
id: self.id.to_string(),
user: member.id.user.clone(),
}
.p(member.id.server)
.await; */
if !silent {
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
}
Ok(())
}
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server
pub async fn ban_member(
self,
db: &Database,
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
.await?;
self.ban_user(db, member.id, reason).await
} */
}
impl Role {
/// Into optional struct
pub fn into_optional(self) -> PartialRole {
PartialRole {
name: Some(self.name),
permissions: Some(self.permissions),
colour: self.colour,
hoist: Some(self.hoist),
rank: Some(self.rank),
}
}
/// Create a role
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?;
/* // TODO: EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: self.clone().into_optional(),
clear: vec![],
}
.p(server_id.to_string())
.await; */
Ok(role_id)
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
server_id: &str,
role_id: &str,
partial: PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_role(server_id, role_id, &partial, remove.clone())
.await?;
/* // TODO: EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: partial,
clear: vec![],
}
.p(server_id.to_string())
.await; */
Ok(())
}
/// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
/* // TODO: EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: role_id.to_string(),
}
.p(server_id.to_string())
.await; */
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 {
pub fn into_channel_ids(self) -> HashSet<String> {
let mut ids = HashSet::new();
if let Some(id) = self.user_joined {
ids.insert(id);
}
if let Some(id) = self.user_left {
ids.insert(id);
}
if let Some(id) = self.user_kicked {
ids.insert(id);
}
if let Some(id) = self.user_banned {
ids.insert(id);
}
ids
}
}

View File

@@ -0,0 +1,46 @@
use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServers: Sync + Send {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()>;
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server>;
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>>;
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()>;
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()>;
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>;
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()>;
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,183 @@
use bson::{to_document, Document};
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServers;
static COL: &str = "servers";
#[async_trait]
impl AbstractServers for MongoDb {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()> {
query!(self, insert_one, COL, &server).map(|_| ())
}
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self
.col::<Server>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": server_id
},
doc! {
"$set": {
"roles.".to_owned() + role_id: to_document(role)
.map_err(|_| create_database_error!("to_document", "role"))?
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "server"))
}
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
server_id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
"roles.".to_owned() + role_id + "."
)
.map(|_| ())
}
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
self.col::<Document>("server_members")
.update_many(
doc! {
"_id.server": server_id
},
doc! {
"$pull": {
"roles": &role_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "server_members"))?;
self.col::<Document>("channels")
.update_one(
doc! {
"server": server_id
},
doc! {
"$unset": {
"role_permissions.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", "channels"))?;
self.col::<Document>("servers")
.update_one(
doc! {
"_id": server_id
},
doc! {
"$unset": {
"roles.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "servers"))
}
}
impl IntoDocumentPath for FieldsServer {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsServer::Banner => "banner",
FieldsServer::Categories => "categories",
FieldsServer::Description => "description",
FieldsServer::Icon => "icon",
FieldsServer::SystemMessages => "system_messages",
})
}
}
impl IntoDocumentPath for FieldsRole {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsRole::Colour => "colour",
})
}
}

View File

@@ -0,0 +1,126 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
use super::AbstractServers;
#[async_trait]
impl AbstractServers for ReferenceDb {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()> {
let mut servers = self.servers.lock().await;
if servers.contains_key(&server.id) {
Err(create_database_error!("insert", "server"))
} else {
servers.insert(server.id.to_string(), server.clone());
Ok(())
}
}
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server> {
let servers = self.servers.lock().await;
servers
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
let servers = self.servers.lock().await;
ids.iter()
.map(|id| {
servers
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
server.remove_field(&field);
}
server.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
let mut servers = self.servers.lock().await;
if servers.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
server.roles.insert(role_id.to_string(), role.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
if let Some(role) = server.roles.get_mut(role_id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
role.remove_field(&field);
}
role.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
if server.roles.remove(role_id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -75,7 +75,7 @@ impl AbstractUsers for MongoDb {
None,
)
.await
.map_err(|_| create_database_error!("find", "user"))?
.map_err(|_| create_database_error!("find", "users"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())