From 279d9ef1b5d0d915f3711f1827e18cae860ceddc Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 3 Sep 2023 15:52:18 +0100 Subject: [PATCH] refactor(core): remove quark references from webhook routes --- clippy.toml | 1 + crates/core/config/Revolt.toml | 3 +- crates/core/config/src/lib.rs | 1 + .../database/src/models/messages/model.rs | 221 +++++++++++++++++- crates/core/database/src/util/bridge/v0.rs | 21 ++ crates/core/database/src/util/reference.rs | 7 +- crates/core/models/src/v0/channel_webhooks.rs | 16 +- crates/core/models/src/v0/messages.rs | 62 ++++- crates/core/models/src/v0/server_members.rs | 21 ++ crates/core/result/src/lib.rs | 3 + crates/core/result/src/rocket.rs | 1 + .../src/routes/channels/webhook_create.rs | 56 ++--- .../src/routes/channels/webhook_fetch_all.rs | 27 ++- .../src/routes/webhooks/webhook_delete.rs | 27 ++- .../delta/src/routes/webhooks/webhook_edit.rs | 34 +-- .../src/routes/webhooks/webhook_execute.rs | 67 ++++-- .../routes/webhooks/webhook_execute_github.rs | 44 ++-- .../src/routes/webhooks/webhook_fetch.rs | 21 +- 18 files changed, 494 insertions(+), 139 deletions(-) diff --git a/clippy.toml b/clippy.toml index d4739dfa..f0c8ae98 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,6 +1,7 @@ disallowed-methods = [ # Shouldn't need to access these directly "revolt_database::models::bots::model::Bot::remove_field", + "revolt_database::models::messages::model::Message::attach_sendable_embed", # Prefer to use Object::create() "revolt_database::models::bots::ops::AbstractBots::insert_bot", diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 213f1fd0..7b8626a2 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -48,9 +48,10 @@ max_concurrent_connections = 50 [features.limits.default] group_size = 100 bots = 5 +message_length = 2000 +message_embeds = 5 message_replies = 5 message_attachments = 5 -message_embeds = 5 servers = 100 server_emoji = 100 server_roles = 200 diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 3bd5c019..934114e1 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -97,6 +97,7 @@ pub struct Api { pub struct FeaturesLimits { pub group_size: usize, pub bots: usize, + pub message_length: usize, pub message_replies: usize, pub message_attachments: usize, pub message_embeds: usize, diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index a2d3aefd..c0d39402 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -1,12 +1,19 @@ +use std::collections::HashSet; + use indexmap::{IndexMap, IndexSet}; use iso8601_timestamp::Timestamp; -use revolt_models::v0::{Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification}; +use revolt_config::config; +use revolt_models::v0::{ + self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification, + ReplyIntent, SendableEmbed, RE_MENTION, +}; use revolt_result::Result; use ulid::Ulid; use crate::{ events::client::EventV1, tasks::{self, ack::AckEvent}, + util::idempotency::IdempotencyKey, Channel, Database, File, }; @@ -196,6 +203,160 @@ impl Default for Message { #[allow(clippy::disallowed_methods)] impl Message { + /// Create message from API data + pub async fn create_from_api( + db: &Database, + channel: Channel, + data: DataMessageSend, + author: MessageAuthor<'_>, + mut idempotency: IdempotencyKey, + generate_embeds: bool, + ) -> Result { + let config = config().await; + + Message::validate_sum( + &data.content, + data.embeds.as_deref().unwrap_or_default(), + config.features.limits.default.message_length, + )?; + + idempotency + .consume_nonce(data.nonce) + .await + .map_err(|_| create_error!(InvalidOperation))?; + + // Check the message is not empty + if (data.content.as_ref().map_or(true, |v| v.is_empty())) + && (data.attachments.as_ref().map_or(true, |v| v.is_empty())) + && (data.embeds.as_ref().map_or(true, |v| v.is_empty())) + { + return Err(create_error!(EmptyMessage)); + } + + // Ensure restrict_reactions is not specified without reactions list + if let Some(interactions) = &data.interactions { + if interactions.restrict_reactions { + let disallowed = if let Some(list) = &interactions.reactions { + list.is_empty() + } else { + true + }; + + if disallowed { + return Err(create_error!(InvalidProperty)); + } + } + } + + let (author_id, webhook) = match &author { + MessageAuthor::User(user) => (user.id.clone(), None), + MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())), + MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None), + }; + + // Start constructing the message + let message_id = Ulid::new().to_string(); + let mut message = Message { + id: message_id.clone(), + channel: channel.id(), + masquerade: data.masquerade.map(|masquerade| masquerade.into()), + interactions: data + .interactions + .map(|interactions| interactions.into()) + .unwrap_or_default(), + author: author_id, + webhook: webhook.map(|w| w.into()), + ..Default::default() + }; + + // Parse mentions in message. + let mut mentions = HashSet::new(); + if let Some(content) = &data.content { + for capture in RE_MENTION.captures_iter(content) { + if let Some(mention) = capture.get(1) { + mentions.insert(mention.as_str().to_string()); + } + } + } + + // Verify replies are valid. + let mut replies = HashSet::new(); + if let Some(entries) = data.replies { + if entries.len() > config.features.limits.default.message_replies { + return Err(create_error!(TooManyReplies { + max: config.features.limits.default.message_replies, + })); + } + + for ReplyIntent { id, mention } in entries { + let message = db.fetch_message(&id).await?; + + if mention { + mentions.insert(message.author.to_owned()); + } + + replies.insert(message.id); + } + } + + if !mentions.is_empty() { + message.mentions.replace(mentions.into_iter().collect()); + } + + if !replies.is_empty() { + message + .replies + .replace(replies.into_iter().collect::>()); + } + + // Add attachments to message. + let mut attachments = vec![]; + if data + .attachments + .as_ref() + .is_some_and(|v| v.len() > config.features.limits.default.message_attachments) + { + return Err(create_error!(TooManyAttachments { + max: config.features.limits.default.message_attachments, + })); + } + + if data + .embeds + .as_ref() + .is_some_and(|v| v.len() > config.features.limits.default.message_embeds) + { + return Err(create_error!(TooManyEmbeds { + max: config.features.limits.default.message_embeds, + })); + } + + for attachment_id in data.attachments.as_deref().unwrap_or_default() { + attachments.push( + db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id) + .await?, + ); + } + + if !attachments.is_empty() { + message.attachments.replace(attachments); + } + + // Process included embeds. + for sendable_embed in data.embeds.unwrap_or_default() { + message.attach_sendable_embed(db, sendable_embed).await?; + } + + // Set content + message.content = data.content; + + // Pass-through nonce value for clients + message.nonce = Some(idempotency.into_key()); + + message.send(db, author, &channel, generate_embeds).await?; + Ok(message) + } + /// Send a message without any notifications pub async fn send_without_notifications( &mut self, @@ -293,6 +454,64 @@ impl Message { Ok(()) } + + /// Convert sendable embed to text embed and attach to message + pub async fn attach_sendable_embed( + &mut self, + db: &Database, + embed: v0::SendableEmbed, + ) -> Result<()> { + let media: Option = if let Some(id) = embed.media { + Some( + db.find_and_use_attachment(&id, "attachments", "message", &self.id) + .await? + .into(), + ) + } else { + None + }; + + let embed = v0::Embed::Text(v0::Text { + icon_url: embed.icon_url, + url: embed.url, + title: embed.title, + description: embed.description, + media, + colour: embed.colour, + }); + + if let Some(embeds) = &mut self.embeds { + embeds.push(embed); + } else { + self.embeds = Some(vec![embed]); + } + + Ok(()) + } + + /// Validate the sum of content of a message is under threshold + pub fn validate_sum( + content: &Option, + embeds: &[SendableEmbed], + max_length: usize, + ) -> Result<()> { + let mut running_total = 0; + if let Some(content) = content { + running_total += content.len(); + } + + for embed in embeds { + if let Some(desc) = &embed.description { + running_total += desc.len(); + } + } + + if running_total <= max_length { + Ok(()) + } else { + Err(create_error!(PayloadTooLarge)) + } + } } impl SystemMessage { diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 4439134d..88cf7bdb 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -402,6 +402,17 @@ impl From for Interactions { } } +impl From for crate::Interactions { + fn from(value: Interactions) -> Self { + crate::Interactions { + reactions: value + .reactions + .map(|reactions| reactions.into_iter().collect()), + restrict_reactions: value.restrict_reactions, + } + } +} + impl From for AppendMessage { fn from(value: crate::AppendMessage) -> Self { AppendMessage { @@ -420,6 +431,16 @@ impl From for Masquerade { } } +impl From for crate::Masquerade { + fn from(value: Masquerade) -> Self { + crate::Masquerade { + name: value.name, + avatar: value.avatar, + colour: value.colour, + } + } +} + impl From for ServerBan { fn from(value: crate::ServerBan) -> Self { ServerBan { diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index 0b155f91..204aff0d 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -7,7 +7,7 @@ use schemars::{ JsonSchema, }; -use crate::{Bot, Database, Webhook}; +use crate::{Bot, Channel, Database, Webhook}; /// Reference to some object in the database #[derive(Serialize, Deserialize)] @@ -27,6 +27,11 @@ impl Reference { db.fetch_bot(&self.id).await } + /// Fetch channel from Ref + pub async fn as_channel(&self, db: &Database) -> Result { + db.fetch_channel(&self.id).await + } + /// Fetch webhook from Ref pub async fn as_webhook(&self, db: &Database) -> Result { db.fetch_webhook(&self.id).await diff --git a/crates/core/models/src/v0/channel_webhooks.rs b/crates/core/models/src/v0/channel_webhooks.rs index 8e7ae042..dc1fd588 100644 --- a/crates/core/models/src/v0/channel_webhooks.rs +++ b/crates/core/models/src/v0/channel_webhooks.rs @@ -1,3 +1,5 @@ +use validator::Validate; + use super::File; auto_derived_partial!( @@ -69,13 +71,23 @@ auto_derived!( pub channel_id: String, /// The permissions for the webhook - pub permissions: u64 + pub permissions: u64, } /// Optional fields on webhook object pub enum FieldsWebhook { Avatar, } + + /// Information for the webhook + #[derive(Validate)] + pub struct CreateWebhookBody { + #[validate(length(min = 1, max = 32))] + pub name: String, + + #[validate(length(min = 1, max = 128))] + pub avatar: Option, + } ); impl From for MessageWebhook { @@ -94,7 +106,7 @@ impl From for ResponseWebhook { name: value.name, avatar: value.avatar.map(|file| file.id), channel_id: value.channel_id, - permissions: value.permissions + permissions: value.permissions, } } } diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index b5ac5e6b..6676147f 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -3,11 +3,17 @@ use std::{ time::SystemTime, }; +use once_cell::sync::Lazy; +use regex::Regex; use revolt_config::config; +use validator::Validate; use iso8601_timestamp::Timestamp; -use super::{Embed, File, MessageWebhook, User, Webhook}; +use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR}; + +pub static RE_MENTION: Lazy = + Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap()); auto_derived_partial!( /// Message @@ -88,7 +94,9 @@ auto_derived!( } /// Name and / or avatar override information + #[derive(Validate)] pub struct Masquerade { + // FIXME: missing validation /// Replace the display name shown on this message #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, @@ -154,6 +162,58 @@ auto_derived!( /// URL to open when clicking notification pub url: String, } + + /// Representation of a text embed before it is sent. + #[derive(Default, Validate)] + pub struct SendableEmbed { + #[validate(length(min = 1, max = 128))] + pub icon_url: Option, + #[validate(length(min = 1, max = 256))] + pub url: Option, + #[validate(length(min = 1, max = 100))] + pub title: Option, + #[validate(length(min = 1, max = 2000))] + pub description: Option, + pub media: Option, + #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")] + pub colour: Option, + } + + /// What this message should reply to and how + pub struct ReplyIntent { + /// Message Id + pub id: String, + /// Whether this reply should mention the message's author + pub mention: bool, + } + + /// Message to send + #[derive(Validate)] + pub struct DataMessageSend { + /// Unique token to prevent duplicate message sending + /// + /// **This is deprecated and replaced by `Idempotency-Key`!** + #[validate(length(min = 1, max = 64))] + pub nonce: Option, + + /// Message content to send + #[validate(length(min = 0, max = 2000))] + pub content: Option, + /// Attachments to include in message + pub attachments: Option>, + /// Messages to reply to + pub replies: Option>, + /// Embeds to include in message + /// + /// Text embed content contributes to the content length cap + #[validate] + pub embeds: Option>, + /// Masquerade to apply to this message + #[validate] + pub masquerade: Option, + /// Information about how this message should be interacted with + pub interactions: Option, + } ); /// Message Author Abstraction diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index 3fbacce9..62b929b6 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -1,6 +1,27 @@ use super::File; use iso8601_timestamp::Timestamp; +use once_cell::sync::Lazy; +use regex::Regex; + +/// Regex for valid role colours +/// +/// Allows the use of named colours, rgb(a), variables and all gradients. +/// +/// Flags: +/// - Case-insensitive (`i`) +/// +/// Source: +/// ```regex +/// VALUE = [a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+ +/// ADDITIONAL_VALUE = \d+deg +/// STOP = ([ ]+(\d{1,3}%|0))? +/// +/// ^(?:VALUE|(repeating-)?(linear|conic|radial)-gradient\((VALUE|ADDITIONAL_VALUE)STOP(,[ ]*(VALUE)STOP)+\))$ +/// ``` +pub static RE_COLOUR: Lazy = Lazy::new(|| { + Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap() +}); auto_derived_partial!( /// Server Member diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 140a7a09..104f98f5 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -60,6 +60,9 @@ pub enum ErrorType { TooManyAttachments { max: usize, }, + TooManyEmbeds { + max: usize, + }, TooManyReplies { max: usize, }, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index b6efd8cb..d5ca3493 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -46,6 +46,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::AlreadyInServer => Status::Conflict, ErrorType::TooManyServers { .. } => Status::BadRequest, + ErrorType::TooManyEmbeds { .. } => Status::BadRequest, ErrorType::TooManyEmoji { .. } => Status::BadRequest, ErrorType::TooManyChannels { .. } => Status::BadRequest, ErrorType::TooManyRoles { .. } => Status::BadRequest, diff --git a/crates/delta/src/routes/channels/webhook_create.rs b/crates/delta/src/routes/channels/webhook_create.rs index 4956c675..d03ae0e5 100644 --- a/crates/delta/src/routes/channels/webhook_create.rs +++ b/crates/delta/src/routes/channels/webhook_create.rs @@ -1,23 +1,16 @@ -use revolt_database::{Database, Webhook}; -use revolt_quark::{ - models::{Channel, User}, - perms, Db, Error, Permission, Ref, Result, - DEFAULT_WEBHOOK_PERMISSIONS, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, User, Webhook, }; +use revolt_models::v0; +use revolt_permissions::{ + calculate_channel_permissions, ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS, +}; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; -use serde::{Deserialize, Serialize}; use ulid::Ulid; use validator::Validate; -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct CreateWebhookBody { - #[validate(length(min = 1, max = 32))] - name: String, - - #[validate(length(min = 1, max = 128))] - avatar: Option, -} - /// # Creates a webhook /// /// Creates a webhook which 3rd party platforms can use to send messages @@ -25,33 +18,34 @@ pub struct CreateWebhookBody { #[post("//webhooks", data = "")] pub async fn req( db: &State, - legacy_db: &Db, user: User, - target: Ref, - data: Json, -) -> Result> { + target: Reference, + data: Json, +) -> Result> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - let channel = target.as_channel(legacy_db).await?; + let channel = target.as_channel(db).await?; if !matches!(channel, Channel::TextChannel { .. } | Channel::Group { .. }) { - return Err(Error::InvalidOperation); + return Err(create_error!(InvalidOperation)); } - let mut permissions = perms(&user).channel(&channel); - permissions - .has_permission(legacy_db, Permission::ManageWebhooks) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?; let webhook_id = Ulid::new().to_string(); let avatar = match &data.avatar { Some(id) => Some( db.find_and_use_attachment(id, "avatars", "user", &webhook_id) - .await - .map_err(Error::from_core)?, + .await?, ), None => None, }; @@ -60,12 +54,12 @@ pub async fn req( id: webhook_id, name: data.name, avatar, - channel_id: channel.id().to_string(), + channel_id: channel.id(), permissions: *DEFAULT_WEBHOOK_PERMISSIONS, token: Some(nanoid::nanoid!(64)), }; - webhook.create(db).await.map_err(Error::from_core)?; + webhook.create(db).await?; Ok(Json(webhook.into())) } diff --git a/crates/delta/src/routes/channels/webhook_fetch_all.rs b/crates/delta/src/routes/channels/webhook_fetch_all.rs index 4c03b128..5727537f 100644 --- a/crates/delta/src/routes/channels/webhook_fetch_all.rs +++ b/crates/delta/src/routes/channels/webhook_fetch_all.rs @@ -1,6 +1,10 @@ -use revolt_database::Database; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; use revolt_models::v0::Webhook; -use revolt_quark::{models::User, perms, Db, Error, Permission, Ref, Result}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; use rocket::{serde::json::Json, State}; /// # Gets all webhooks @@ -10,20 +14,19 @@ use rocket::{serde::json::Json, State}; #[get("//webhooks")] pub async fn req( db: &State, - legacy_db: &Db, user: User, - channel_id: Ref, + channel_id: Reference, ) -> Result>> { - let channel = channel_id.as_channel(legacy_db).await?; - let mut permissions = perms(&user).channel(&channel); - permissions - .has_permission(legacy_db, Permission::ManageWebhooks) - .await?; + let channel = channel_id.as_channel(db).await?; + + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; Ok(Json( - db.fetch_webhooks_for_channel(channel.id()) - .await - .map_err(Error::from_core)? + db.fetch_webhooks_for_channel(&channel.id()) + .await? .into_iter() .map(|v| v.into()) .collect::>(), diff --git a/crates/delta/src/routes/webhooks/webhook_delete.rs b/crates/delta/src/routes/webhooks/webhook_delete.rs index 7de6f5f5..2a5bbc35 100644 --- a/crates/delta/src/routes/webhooks/webhook_delete.rs +++ b/crates/delta/src/routes/webhooks/webhook_delete.rs @@ -1,5 +1,9 @@ -use revolt_database::{util::reference::Reference, Database}; -use revolt_quark::{models::User, perms, Db, Error, Permission, 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; @@ -10,21 +14,16 @@ use rocket_empty::EmptyResponse; #[delete("/")] pub async fn webhook_delete( db: &State, - legacy_db: &Db, user: User, webhook_id: Reference, ) -> Result { - let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?; - let channel = legacy_db.fetch_channel(&webhook.channel_id).await?; + let webhook = webhook_id.as_webhook(db).await?; + let channel = db.fetch_channel(&webhook.channel_id).await?; - perms(&user) - .channel(&channel) - .throw_permission(legacy_db, Permission::ManageWebhooks) - .await?; - - webhook - .delete(db) + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) .await - .map(|_| EmptyResponse) - .map_err(Error::from_core) + .throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?; + + webhook.delete(db).await.map(|_| EmptyResponse) } diff --git a/crates/delta/src/routes/webhooks/webhook_edit.rs b/crates/delta/src/routes/webhooks/webhook_edit.rs index e1769e8e..e87468eb 100644 --- a/crates/delta/src/routes/webhooks/webhook_edit.rs +++ b/crates/delta/src/routes/webhooks/webhook_edit.rs @@ -1,6 +1,10 @@ -use revolt_database::{util::reference::Reference, Database, PartialWebhook}; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, PartialWebhook, User, +}; use revolt_models::v0::{DataEditWebhook, Webhook}; -use revolt_quark::{models::User, perms, Db, Error, Permission, Result}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; use validator::Validate; @@ -11,22 +15,24 @@ use validator::Validate; #[patch("/", data = "")] pub async fn webhook_edit( db: &State, - legacy_db: &Db, webhook_id: Reference, user: User, data: Json, ) -> Result> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - let mut webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?; - let channel = legacy_db.fetch_channel(&webhook.channel_id).await?; + let mut webhook = webhook_id.as_webhook(db).await?; + let channel = db.fetch_channel(&webhook.channel_id).await?; - perms(&user) - .channel(&channel) - .throw_permission(legacy_db, Permission::ManageWebhooks) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?; if data.name.is_none() && data.avatar.is_none() && data.remove.is_empty() { return Ok(Json(webhook.into())); @@ -48,16 +54,14 @@ pub async fn webhook_edit( if let Some(avatar) = avatar { let file = db .find_and_use_attachment(&avatar, "avatars", "user", &webhook.id) - .await - .map_err(Error::from_core)?; + .await?; partial.avatar = Some(file) } webhook .update(db, partial, remove.into_iter().map(|v| v.into()).collect()) - .await - .map_err(Error::from_core)?; + .await?; Ok(Json(webhook.into())) } diff --git a/crates/delta/src/routes/webhooks/webhook_execute.rs b/crates/delta/src/routes/webhooks/webhook_execute.rs index fb31fa56..506ee099 100644 --- a/crates/delta/src/routes/webhooks/webhook_execute.rs +++ b/crates/delta/src/routes/webhooks/webhook_execute.rs @@ -1,12 +1,10 @@ use revolt_database::{ util::{idempotency::IdempotencyKey, reference::Reference}, - Database, -}; -use revolt_quark::{ - models::message::{DataMessageSend, Message}, - types::push::MessageAuthor, - Db, Error, Result, + Database, Message, }; +use revolt_models::v0; +use revolt_permissions::{ChannelPermission, PermissionValue}; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; use validator::Validate; @@ -18,31 +16,50 @@ use validator::Validate; #[post("//", data = "")] pub async fn webhook_execute( db: &State, - legacy_db: &Db, webhook_id: Reference, token: String, - data: Json, + data: Json, idempotency: IdempotencyKey, -) -> Result> { +) -> Result> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?; - webhook.assert_token(&token).map_err(Error::from_core)?; + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; - data.validate_webhook_permissions(webhook.permissions)?; + let mut value: PermissionValue = webhook.permissions.into(); + value.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?; - let channel = legacy_db.fetch_channel(&webhook.channel_id).await?; - let message = channel - .send_message( - legacy_db, - data, - MessageAuthor::Webhook(&webhook.into()), - idempotency, - true, - ) - .await?; + if data.attachments.as_ref().map_or(false, |v| !v.is_empty()) { + value.throw_if_lacking_channel_permission(ChannelPermission::UploadFiles)?; + } - Ok(Json(message)) + if data.embeds.as_ref().map_or(false, |v| !v.is_empty()) { + value.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?; + } + + if data.masquerade.is_some() { + value.throw_if_lacking_channel_permission(ChannelPermission::Masquerade)?; + } + + if data.interactions.is_some() { + value.throw_if_lacking_channel_permission(ChannelPermission::React)?; + } + + let channel = db.fetch_channel(&webhook.channel_id).await?; + let message = Message::create_from_api( + db, + channel, + data, + v0::MessageAuthor::Webhook(&webhook.into()), + idempotency, + true, + ) + .await?; + + Ok(Json(message.into())) } diff --git a/crates/delta/src/routes/webhooks/webhook_execute_github.rs b/crates/delta/src/routes/webhooks/webhook_execute_github.rs index 0ca0b8c8..16142bef 100644 --- a/crates/delta/src/routes/webhooks/webhook_execute_github.rs +++ b/crates/delta/src/routes/webhooks/webhook_execute_github.rs @@ -1,10 +1,6 @@ -use revolt_database::{util::reference::Reference, Database}; -use revolt_models::v0::Webhook; -use revolt_quark::{ - models::{message::SendableEmbed, Message}, - types::push::MessageAuthor, - Db, Error, Result, -}; +use revolt_database::{util::reference::Reference, Database, Message}; +use revolt_models::v0::{MessageAuthor, SendableEmbed, Webhook}; +use revolt_result::{create_error, Error, Result}; use revolt_rocket_okapi::{ gen::OpenApiGenerator, request::{OpenApiFromRequest, RequestHeaderInput}, @@ -639,7 +635,7 @@ impl<'r> FromRequest<'r> for EventHeader<'r> { async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { let headers = request.headers(); let Some(event) = headers.get_one("X-GitHub-Event") else { - return rocket::request::Outcome::Failure((Status::BadRequest, Error::InvalidOperation)) + return rocket::request::Outcome::Failure((Status::BadRequest, create_error!(InvalidOperation))) }; rocket::request::Outcome::Success(Self(event)) @@ -702,7 +698,7 @@ fn safe_from_str Deserialize<'de>>(data: &str) -> Result { Ok(output) => Ok(output), Err(err) => { log::error!("{err:?}"); - Err(Error::InvalidOperation) + Err(create_error!(InvalidOperation)) } } } @@ -727,7 +723,7 @@ fn convert_event(data: &str, event_name: &str) -> Result { "issue_comment" => BaseEvent::IssueComment(safe_from_str(data)?), "issues" => BaseEvent::Issues(safe_from_str(data)?), "pull_request" => BaseEvent::PullRequest(safe_from_str(data)?), - _ => return Err(Error::InvalidOperation), + _ => return Err(create_error!(InvalidOperation)), }; let _Event { @@ -751,16 +747,15 @@ fn convert_event(data: &str, event_name: &str) -> Result { #[post("///github", data = "")] pub async fn webhook_execute_github( db: &State, - legacy_db: &Db, webhook_id: Reference, token: String, event: EventHeader<'_>, data: String, ) -> Result<()> { - let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?; - webhook.assert_token(&token).map_err(Error::from_core)?; + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; - let channel = legacy_db.fetch_channel(&webhook.channel_id).await?; + let channel = db.fetch_channel(&webhook.channel_id).await?; let event = convert_event(&data, &event)?; let sendable_embed = match event.event { @@ -1058,30 +1053,25 @@ pub async fn webhook_execute_github( }, }; - sendable_embed - .validate() - .map_err(|error| Error::FailedValidation { error })?; + sendable_embed.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; let message_id = Ulid::new().to_string(); - let embed = sendable_embed - .into_embed(legacy_db, &message_id) - .await?; - let mut message = Message { id: message_id, author: webhook.id.clone(), channel: webhook.channel_id.clone(), - embeds: Some(vec![embed]), webhook: Some(std::convert::Into::::into(webhook.clone()).into()), ..Default::default() }; + #[allow(clippy::disallowed_methods)] + message.attach_sendable_embed(db, sendable_embed).await?; message - .create( - legacy_db, - &channel, - Some(MessageAuthor::Webhook(&webhook.into())), - ) + .send(db, MessageAuthor::Webhook(&webhook.into()), &channel, false) .await } diff --git a/crates/delta/src/routes/webhooks/webhook_fetch.rs b/crates/delta/src/routes/webhooks/webhook_fetch.rs index 9f54d66e..28afad2f 100644 --- a/crates/delta/src/routes/webhooks/webhook_fetch.rs +++ b/crates/delta/src/routes/webhooks/webhook_fetch.rs @@ -1,6 +1,10 @@ -use revolt_database::{util::reference::Reference, Database}; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; use revolt_models::v0::{ResponseWebhook, Webhook}; -use revolt_quark::{models::User, perms, Db, Error, Permission, Result}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; use rocket::{serde::json::Json, State}; /// # Gets a webhook @@ -10,17 +14,16 @@ use rocket::{serde::json::Json, State}; #[get("/")] pub async fn webhook_fetch( db: &State, - legacy_db: &Db, webhook_id: Reference, user: User, ) -> Result> { - let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?; - let channel = legacy_db.fetch_channel(&webhook.channel_id).await?; + let webhook = webhook_id.as_webhook(db).await?; + let channel = db.fetch_channel(&webhook.channel_id).await?; - perms(&user) - .channel(&channel) - .throw_permission(legacy_db, Permission::ViewChannel) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; Ok(Json(std::convert::Into::::into(webhook).into())) }