diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 61cebca7..e8ab5f65 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -18,7 +18,7 @@ use crate::{ events::client::EventV1, tasks::{self, ack::AckEvent}, util::idempotency::IdempotencyKey, - Channel, Database, Emoji, File, + Channel, Database, Emoji, File, User, }; auto_derived_partial!( @@ -542,6 +542,40 @@ impl Message { Ok(()) } + /// Add a reaction to a message + pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> { + // Check how many reactions are already on the message + let config = config().await; + if self.reactions.len() >= config.features.limits.default.message_reactions + && !self.reactions.contains_key(emoji) + { + return Err(create_error!(InvalidOperation)); + } + + // Check if the emoji is whitelisted + if !self.interactions.can_use(emoji) { + return Err(create_error!(InvalidOperation)); + } + + // Check if the emoji is usable by us + if !Emoji::can_use(db, emoji).await? { + return Err(create_error!(InvalidOperation)); + } + + // Send reaction event + EventV1::MessageReact { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + user_id: user.id.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + // Add emoji + db.add_reaction(&self.id, emoji, &user.id).await + } + /// Validate the sum of content of a message is under threshold pub fn validate_sum( content: &Option, @@ -607,6 +641,53 @@ impl Message { .await; Ok(()) } + + /// Remove a reaction from a message + pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> { + // Check if it actually exists + let empty = if let Some(users) = self.reactions.get(emoji) { + if !users.contains(user) { + return Err(create_error!(NotFound)); + } + + users.len() == 1 + } else { + return Err(create_error!(NotFound)); + }; + + // Send reaction event + EventV1::MessageUnreact { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + user_id: user.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + if empty { + // If empty, remove the reaction entirely + db.clear_reaction(&self.id, emoji).await + } else { + // Otherwise only remove that one reaction + db.remove_reaction(&self.id, emoji, user).await + } + } + + /// Remove a reaction from a message + pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> { + // Send reaction event + EventV1::MessageRemoveReaction { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + // Write to database + db.clear_reaction(&self.id, emoji).await + } } impl SystemMessage { diff --git a/crates/core/database/src/util/permissions.rs b/crates/core/database/src/util/permissions.rs index 3aaf09f6..3adb2ec3 100644 --- a/crates/core/database/src/util/permissions.rs +++ b/crates/core/database/src/util/permissions.rs @@ -464,6 +464,13 @@ impl<'a> DatabasePermissionQuery<'a> { pub fn member_ref(&self) -> &Option> { &self.member } + + /// Get the known member's current ranking + pub fn get_member_rank(&self) -> Option { + self.member + .as_ref() + .map(|member| member.get_ranking(self.server.as_ref().unwrap())) + } } /// Short-hand for creating a permission calculator diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index 8815fb8a..dfabb2af 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -1,6 +1,6 @@ use super::File; -use revolt_permissions::OverrideField; +use revolt_permissions::{Override, OverrideField}; use std::collections::{HashMap, HashSet}; #[cfg(feature = "rocket")] @@ -262,6 +262,25 @@ auto_derived!( pub nsfw: Option, } + /// New default permissions + #[serde(untagged)] + pub enum DataDefaultChannelPermissions { + Value { + /// Permission values to set for members in a `Group` + permissions: u64, + }, + Field { + /// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel` + permissions: Override, + }, + } + + /// New role permissions + pub struct DataSetRolePermissions { + /// Allow / deny values to set for this role + pub permissions: Override, + } + /// Options when deleting a channel #[cfg_attr(feature = "rocket", derive(FromForm))] pub struct OptionsChannelDelete { diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index 46fd4419..73ca4bd6 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -272,7 +272,7 @@ auto_derived!( } /// Options for searching for messages - pub struct OptionsMessageSearch { + pub struct DataMessageSearch { /// Full-text search query /// /// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information. @@ -318,6 +318,15 @@ auto_derived!( #[validate(length(min = 1, max = 100))] pub ids: Vec, } + + /// Options for removing reaction + #[cfg_attr(feature = "rocket", derive(FromForm))] + pub struct OptionsUnreact { + /// Remove a specific user's reaction + pub user_id: Option, + /// Remove all reactions + pub remove_all: Option, + } ); /// Message Author Abstraction diff --git a/crates/core/permissions/src/models/mod.rs b/crates/core/permissions/src/models/mod.rs index 3210a42e..13c2e3f1 100644 --- a/crates/core/permissions/src/models/mod.rs +++ b/crates/core/permissions/src/models/mod.rs @@ -63,6 +63,37 @@ impl PermissionValue { })) } } + + /// Throw an error if we cannot grant permissions on either allows or denies + /// going from the previous given value to the next given value. + /// + /// We need to check any: + /// - allows added (permissions now granted) + /// - denies removed (permissions now neutral or granted) + pub async fn throw_permission_override( + &self, + current_value: C, + next_value: &Override, + ) -> Result<()> + where + C: Into>, + { + let current_value = current_value.into(); + + if let Some(current_value) = current_value { + if !self.has(!current_value.allows() & next_value.allows()) + || !self.has(current_value.denies() & !next_value.denies()) + { + return Err(create_error!(CannotGiveMissingPermissions)); + } + } else { + if !self.has(next_value.allows()) { + return Err(create_error!(CannotGiveMissingPermissions)); + } + } + + Ok(()) + } } impl From for PermissionValue { diff --git a/crates/delta/src/routes/channels/message_react.rs b/crates/delta/src/routes/channels/message_react.rs index 4c76adcc..11933bc1 100644 --- a/crates/delta/src/routes/channels/message_react.rs +++ b/crates/delta/src/routes/channels/message_react.rs @@ -1,4 +1,11 @@ -use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result}; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Add Reaction to Message /// @@ -6,20 +13,20 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Resu #[openapi(tag = "Interactions")] #[put("//messages//reactions/")] pub async fn react_message( - db: &Db, + db: &State, user: User, - target: Ref, - msg: Ref, - emoji: Ref, + target: Reference, + msg: Reference, + emoji: Reference, ) -> Result { let channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission_and_view_channel(db, Permission::React) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::React)?; // Fetch relevant message - let message = msg.as_message_in(db, channel.id()).await?; + let message = msg.as_message_in_channel(db, &channel.id()).await?; // Add the reaction message diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 07531ad2..ab83be95 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -32,7 +32,6 @@ pub async fn message_send( // Ensure we have permissions to send a message let channel = target.as_channel(db).await?; - let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); let permissions = calculate_channel_permissions(&mut query).await; permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?; diff --git a/crates/delta/src/routes/channels/message_unreact.rs b/crates/delta/src/routes/channels/message_unreact.rs index 7a5d4dc0..4e2132da 100644 --- a/crates/delta/src/routes/channels/message_unreact.rs +++ b/crates/delta/src/routes/channels/message_unreact.rs @@ -1,14 +1,12 @@ -use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result}; -use serde::{Deserialize, Serialize}; - -/// # Query Parameters -#[derive(Serialize, Deserialize, JsonSchema, FromForm)] -pub struct OptionsUnreact { - /// Remove a specific user's reaction - user_id: Option, - /// Remove all reactions - remove_all: Option, -} +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Remove Reaction(s) to Message /// @@ -18,29 +16,27 @@ pub struct OptionsUnreact { #[openapi(tag = "Interactions")] #[delete("//messages//reactions/?")] pub async fn unreact_message( - db: &Db, + db: &State, user: User, - target: Ref, - msg: Ref, - emoji: Ref, - options: OptionsUnreact, + target: Reference, + msg: Reference, + emoji: Reference, + options: v0::OptionsUnreact, ) -> Result { let channel = target.as_channel(db).await?; - let mut permissions = perms(&user).channel(&channel); - permissions - .throw_permission_and_view_channel(db, Permission::React) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + let permissions = calculate_channel_permissions(&mut query).await; + + permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?; // Check if we need to escalate permissions let remove_all = options.remove_all.unwrap_or_default(); if options.user_id.is_some() || remove_all { - permissions - .throw_permission(db, Permission::ManageMessages) - .await?; + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; } // Fetch relevant message - let message = msg.as_message_in(db, channel.id()).await?; + let message = msg.as_message_in_channel(db, &channel.id()).await?; // Check if we should wipe all of this reaction if remove_all { diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index 28e1870b..a5134815 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -45,8 +45,8 @@ pub fn routes() -> (Vec, OpenApi) { group_add_member::add_member, group_remove_member::remove_member, voice_join::call, - permissions_set::req, - permissions_set_default::req, + permissions_set::set_role_permissions, + permissions_set_default::set_default_permissions, message_react::react_message, message_unreact::unreact_message, message_clear_reactions::clear_reactions, diff --git a/crates/delta/src/routes/channels/permissions_set.rs b/crates/delta/src/routes/channels/permissions_set.rs index 7705fbce..b4b3c3dc 100644 --- a/crates/delta/src/routes/channels/permissions_set.rs +++ b/crates/delta/src/routes/channels/permissions_set.rs @@ -1,17 +1,12 @@ -use rocket::serde::json::Json; -use serde::Deserialize; - -use revolt_quark::{ - models::{Channel, User}, - perms, Db, Error, Override, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, User, }; - -/// # Permission Value -#[derive(Deserialize, JsonSchema)] -pub struct Data { - /// Allow / deny values to set for this role - permissions: Override, -} +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use serde::Deserialize; /// # Set Role Permission /// @@ -20,40 +15,39 @@ pub struct Data { /// Channel must be a `TextChannel` or `VoiceChannel`. #[openapi(tag = "Channel Permissions")] #[put("//permissions/", data = "", rank = 2)] -pub async fn req( - db: &Db, +pub async fn set_role_permissions( + db: &State, user: User, - target: Ref, + target: Reference, role_id: String, - data: Json, -) -> Result> { + data: Json, +) -> Result> { let mut channel = target.as_channel(db).await?; - let mut permissions = perms(&user).channel(&channel); + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + let permissions = calculate_channel_permissions(&mut query).await; - permissions - .throw_permission_and_view_channel(db, Permission::ManagePermissions) - .await?; + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; - if let Some(server) = permissions.server.get() { + if let Some(server) = query.server_ref() { if let Some(role) = server.roles.get(&role_id) { - if role.rank <= permissions.get_member_rank().unwrap_or(i64::MIN) { - return Err(Error::NotElevated); + if role.rank <= query.get_member_rank().unwrap_or(i64::MIN) { + return Err(create_error!(NotElevated)); } let current_value: Override = role.permissions.into(); permissions - .throw_permission_override(db, current_value, data.permissions) + .throw_permission_override(current_value, &data.permissions) .await?; channel - .set_role_permission(db, &role_id, data.permissions.into()) + .set_role_permission(db, &role_id, data.permissions.clone().into()) .await?; - Ok(Json(channel)) + Ok(Json(channel.into())) } else { - Err(Error::NotFound) + Err(create_error!(NotFound)) } } else { - Err(Error::InvalidOperation) + Err(create_error!(InvalidOperation)) } } diff --git a/crates/delta/src/routes/channels/permissions_set_default.rs b/crates/delta/src/routes/channels/permissions_set_default.rs index befccff4..5957426a 100644 --- a/crates/delta/src/routes/channels/permissions_set_default.rs +++ b/crates/delta/src/routes/channels/permissions_set_default.rs @@ -1,24 +1,11 @@ -use rocket::serde::json::Json; -use serde::Deserialize; - -use revolt_quark::{ - models::{channel::PartialChannel, Channel, User}, - perms, Db, Error, Override, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, PartialChannel, User, }; - -/// # Permission Value -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum DataDefaultChannelPermissions { - Value { - /// Permission values to set for members in a `Group` - permissions: u64, - }, - Field { - /// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel` - permissions: Override, - }, -} +use revolt_models::v0::{self, DataDefaultChannelPermissions}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; /// # Set Default Permission /// @@ -27,19 +14,19 @@ pub enum DataDefaultChannelPermissions { /// Channel must be a `Group`, `TextChannel` or `VoiceChannel`. #[openapi(tag = "Channel Permissions")] #[put("//permissions/default", data = "", rank = 1)] -pub async fn req( - db: &Db, +pub async fn set_default_permissions( + db: &State, user: User, - target: Ref, - data: Json, -) -> Result> { + target: Reference, + data: Json, +) -> Result> { let data = data.into_inner(); let mut channel = target.as_channel(db).await?; - let mut perm = perms(&user).channel(&channel); + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + let permissions = calculate_channel_permissions(&mut query).await; - perm.throw_permission_and_view_channel(db, Permission::ManagePermissions) - .await?; + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; match &channel { Channel::Group { .. } => { @@ -55,7 +42,7 @@ pub async fn req( ) .await?; } else { - return Err(Error::InvalidOperation); + return Err(create_error!(InvalidOperation)); } } Channel::TextChannel { @@ -66,30 +53,27 @@ pub async fn req( default_permissions, .. } => { - if let DataDefaultChannelPermissions::Field { permissions } = data { - perm.throw_permission_override( - db, - default_permissions.map(|x| x.into()), - permissions, - ) - .await?; + if let DataDefaultChannelPermissions::Field { permissions: field } = data { + permissions + .throw_permission_override(default_permissions.map(|x| x.into()), &field) + .await?; channel .update( db, PartialChannel { - default_permissions: Some(permissions.into()), + default_permissions: Some(field.into()), ..Default::default() }, vec![], ) .await?; } else { - return Err(Error::InvalidOperation); + return Err(create_error!(InvalidOperation)); } } - _ => return Err(Error::InvalidOperation), + _ => return Err(create_error!(InvalidOperation)), } - Ok(Json(channel)) + Ok(Json(channel.into())) }