From 5cb2320760fb6ca4b18094138d83ad2deab1076f Mon Sep 17 00:00:00 2001 From: Zomatree Date: Sat, 26 Nov 2022 23:07:22 +0000 Subject: [PATCH] inital webhook support --- .../src/routes/channels/message_delete.rs | 2 +- .../delta/src/routes/channels/message_edit.rs | 2 +- .../delta/src/routes/channels/message_send.rs | 28 ++-- crates/delta/src/routes/channels/mod.rs | 8 +- .../src/routes/channels/webhook_create.rs | 50 +++++++ .../src/routes/channels/webhook_fetch_all.rs | 19 +++ crates/delta/src/routes/mod.rs | 12 +- crates/delta/src/routes/webhooks/mod.rs | 16 ++ .../src/routes/webhooks/webhook_delete.rs | 18 +++ .../delta/src/routes/webhooks/webhook_edit.rs | 57 ++++++++ .../src/routes/webhooks/webhook_execute.rs | 138 ++++++++++++++++++ .../src/routes/webhooks/webhook_fetch.rs | 17 +++ crates/quark/src/events/client.rs | 18 ++- .../quark/src/impl/dummy/channels/channel.rs | 2 +- .../quark/src/impl/dummy/channels/message.rs | 2 +- .../quark/src/impl/dummy/channels/webhook.rs | 40 +++++ crates/quark/src/impl/dummy/mod.rs | 1 + .../src/impl/generic/channels/message.rs | 20 ++- .../src/impl/generic/channels/webhook.rs | 58 ++++++++ crates/quark/src/impl/generic/mod.rs | 1 + .../quark/src/impl/mongo/channels/channel.rs | 17 ++- .../quark/src/impl/mongo/channels/webhook.rs | 57 ++++++++ crates/quark/src/impl/mongo/mod.rs | 1 + crates/quark/src/models/channels/message.rs | 9 +- crates/quark/src/models/channels/webhook.rs | 33 +++++ crates/quark/src/models/mod.rs | 2 + crates/quark/src/traits/channels/channel.rs | 2 +- crates/quark/src/traits/channels/webhook.rs | 11 ++ crates/quark/src/traits/mod.rs | 3 + crates/quark/src/types/push.rs | 40 ++++- crates/quark/src/util/ref.rs | 7 +- 31 files changed, 653 insertions(+), 38 deletions(-) create mode 100644 crates/delta/src/routes/channels/webhook_create.rs create mode 100644 crates/delta/src/routes/channels/webhook_fetch_all.rs create mode 100644 crates/delta/src/routes/webhooks/mod.rs create mode 100644 crates/delta/src/routes/webhooks/webhook_delete.rs create mode 100644 crates/delta/src/routes/webhooks/webhook_edit.rs create mode 100644 crates/delta/src/routes/webhooks/webhook_execute.rs create mode 100644 crates/delta/src/routes/webhooks/webhook_fetch.rs create mode 100644 crates/quark/src/impl/dummy/channels/webhook.rs create mode 100644 crates/quark/src/impl/generic/channels/webhook.rs create mode 100644 crates/quark/src/impl/mongo/channels/webhook.rs create mode 100644 crates/quark/src/models/channels/webhook.rs create mode 100644 crates/quark/src/traits/channels/webhook.rs diff --git a/crates/delta/src/routes/channels/message_delete.rs b/crates/delta/src/routes/channels/message_delete.rs index 6a427316..917d1367 100644 --- a/crates/delta/src/routes/channels/message_delete.rs +++ b/crates/delta/src/routes/channels/message_delete.rs @@ -11,7 +11,7 @@ pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result, + pub nonce: Option, /// Message content to send #[validate(length(min = 0, max = 2000))] - content: Option, + pub content: Option, /// Attachments to include in message #[validate(length(min = 1, max = 128))] - attachments: Option>, + pub attachments: Option>, /// Messages to reply to - replies: Option>, + pub replies: Option>, /// Embeds to include in message /// /// Text embed content contributes to the content length cap #[validate(length(min = 1, max = 10))] - embeds: Option>, + pub embeds: Option>, /// Masquerade to apply to this message #[validate] - masquerade: Option, + pub masquerade: Option, /// Information about how this message should be interacted with - interactions: Option, + pub interactions: Option, } lazy_static! { // ignoring I L O and U is intentional - static ref RE_MENTION: Regex = Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap(); + pub static ref RE_MENTION: Regex = Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap(); } /// # Send Message @@ -86,7 +86,7 @@ pub async fn message_send( let mut message = Message { id: message_id.clone(), channel: channel.id().to_string(), - author: user.id.clone(), + author: Some(user.id.clone()), masquerade: data.masquerade, interactions: data.interactions.unwrap_or_default(), ..Default::default() @@ -128,11 +128,11 @@ pub async fn message_send( for Reply { id, mention } in entries { let message = Ref::from_unchecked(id).as_message(db).await?; - replies.insert(message.id); - if mention { - mentions.insert(message.author); + mentions.insert(message.author_id().to_owned()); } + + replies.insert(message.id); } } @@ -195,7 +195,7 @@ pub async fn message_send( // 8. Pass-through nonce value for clients message.nonce = Some(idempotency.into_key()); - message.create(db, &channel, Some(&user)).await?; + message.create(db, &channel, Some(MessageAuthor::User(&user))).await?; // Queue up a task for processing embeds if let Some(content) = &message.content { diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index 255660d4..6b914ad2 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -19,11 +19,13 @@ mod message_query; mod message_query_stale; mod message_react; mod message_search; -mod message_send; +pub mod message_send; mod message_unreact; mod permissions_set; mod permissions_set_default; mod voice_join; +mod webhook_create; +mod webhook_fetch_all; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -49,6 +51,8 @@ pub fn routes() -> (Vec, OpenApi) { permissions_set_default::req, message_react::react_message, message_unreact::unreact_message, - message_clear_reactions::clear_reactions + message_clear_reactions::clear_reactions, + webhook_create::req, + webhook_fetch_all::req, ] } diff --git a/crates/delta/src/routes/channels/webhook_create.rs b/crates/delta/src/routes/channels/webhook_create.rs new file mode 100644 index 00000000..10d1a40f --- /dev/null +++ b/crates/delta/src/routes/channels/webhook_create.rs @@ -0,0 +1,50 @@ +use revolt_quark::{models::{User, Webhook, File}, perms, Db, Error, Permission, Ref, Result}; +use rocket::serde::json::Json; +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 +#[openapi(tag = "Webhooks")] +#[post("//webhooks", data = "")] +pub async fn req(db: &Db, user: User, target: Ref, data: Json) -> Result> { + let data = data.into_inner(); + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + let channel = target.as_channel(db).await?; + let mut permissions = perms(&user).channel(&channel); + permissions + .has_permission(db, Permission::ManageWebhooks) + .await?; + + let webhook_id = Ulid::new().to_string(); + + let avatar = match &data.avatar { + Some(id) => Some(File::use_avatar(db, id, &webhook_id).await?), + None => None + }; + + let webhook = Webhook { + id: webhook_id, + name: data.name, + avatar, + channel: channel.id().to_string(), + token: nanoid::nanoid!(64) + }; + + webhook.create(db).await?; + + Ok(Json(webhook)) +} diff --git a/crates/delta/src/routes/channels/webhook_fetch_all.rs b/crates/delta/src/routes/channels/webhook_fetch_all.rs new file mode 100644 index 00000000..8f058187 --- /dev/null +++ b/crates/delta/src/routes/channels/webhook_fetch_all.rs @@ -0,0 +1,19 @@ +use revolt_quark::{models::{User, Webhook}, perms, Db, Permission, Ref, Result}; +use rocket::serde::json::Json; + +/// # Gets all webhooks +/// +/// gets all webhooks inside the channel +#[openapi(tag = "Webhooks")] +#[get("//webhooks")] +pub async fn req(db: &Db, user: User, target: Ref) -> Result>> { + let channel = target.as_channel(db).await?; + let mut permissions = perms(&user).channel(&channel); + permissions + .has_permission(db, Permission::ManageWebhooks) + .await?; + + let webhooks = db.fetch_webhooks_for_channel(channel.id()).await?; + + Ok(Json(webhooks)) +} diff --git a/crates/delta/src/routes/mod.rs b/crates/delta/src/routes/mod.rs index f50ef91f..111cc668 100644 --- a/crates/delta/src/routes/mod.rs +++ b/crates/delta/src/routes/mod.rs @@ -13,6 +13,7 @@ mod root; mod servers; mod sync; mod users; +mod webhooks; pub fn mount(mut rocket: Rocket) -> Rocket { let settings = OpenApiSettings::default(); @@ -33,6 +34,7 @@ pub fn mount(mut rocket: Rocket) -> Rocket { "/onboard" => onboard::routes(), "/push" => push::routes(), "/sync" => sync::routes(), + "/webhooks" => webhooks::routes() }; rocket @@ -82,7 +84,8 @@ fn custom_openapi_spec() -> OpenApi { "Messaging", "Interactions", "Groups", - "Voice" + "Voice", + "Webhooks", ] }, { @@ -277,6 +280,13 @@ fn custom_openapi_spec() -> OpenApi { ), ..Default::default() }, + Tag { + name: "Webhooks".to_owned(), + description: Some( + "Send messages from 3rd party services".to_owned(), + ), + ..Default::default() + } ], ..Default::default() } diff --git a/crates/delta/src/routes/webhooks/mod.rs b/crates/delta/src/routes/webhooks/mod.rs new file mode 100644 index 00000000..c1013c42 --- /dev/null +++ b/crates/delta/src/routes/webhooks/mod.rs @@ -0,0 +1,16 @@ +use rocket::Route; +use rocket_okapi::okapi::openapi3::OpenApi; + +mod webhook_delete; +mod webhook_edit; +mod webhook_execute; +mod webhook_fetch; + +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![ + webhook_delete::req, + webhook_edit::req, + webhook_execute::req, + webhook_fetch::req, + ] +} diff --git a/crates/delta/src/routes/webhooks/webhook_delete.rs b/crates/delta/src/routes/webhooks/webhook_delete.rs new file mode 100644 index 00000000..84e401d1 --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_delete.rs @@ -0,0 +1,18 @@ +use revolt_quark::{Db, Ref, Result, EmptyResponse, Error}; + +/// # Deletes a webhook +/// +/// deletes a webhook +#[openapi(tag = "Webhooks")] +#[delete("//")] +pub async fn req(db: &Db, target: Ref, token: String) -> Result { + let webhook = target.as_webhook(db).await?; + + (webhook.token == token) + .then_some(()) + .ok_or(Error::InvalidCredentials)?; + + webhook.delete(db).await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/webhooks/webhook_edit.rs b/crates/delta/src/routes/webhooks/webhook_edit.rs new file mode 100644 index 00000000..0d5e1d81 --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_edit.rs @@ -0,0 +1,57 @@ +use revolt_quark::{Db, Ref, Result, Error, models::{webhook::{FieldsWebhook, Webhook, PartialWebhook}, File}}; +use serde::{Serialize, Deserialize}; +use validator::Validate; +use rocket::serde::json::Json; + +#[derive(Serialize, Deserialize, Validate, JsonSchema)] +pub struct WebhookEditBody { + #[validate(length(min = 1, max = 32))] + name: Option, + + #[validate(length(min = 1, max = 128))] + avatar: Option, + + #[serde(default)] + remove: Vec +} + +/// # Edits a webhook +/// +/// edits a webhook +#[openapi(tag = "Webhooks")] +#[patch("//", data="")] +pub async fn req(db: &Db, target: Ref, token: String, data: Json) -> Result> { + let data = data.into_inner(); + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + let mut webhook = target.as_webhook(db).await?; + + (webhook.token == token) + .then_some(()) + .ok_or(Error::InvalidCredentials)?; + + if data.name.is_none() + && data.avatar.is_none() + && data.remove.is_empty() + { + return Ok(Json(webhook)) + }; + + let mut partial = PartialWebhook::default(); + + let WebhookEditBody { name, avatar, remove } = data; + + if let Some(name) = name { + partial.name = Some(name) + } + + if let Some(avatar) = avatar { + let file = File::use_avatar(db, &avatar, &webhook.id).await?; + partial.avatar = Some(file) + } + + webhook.update(db, partial, remove).await?; + + Ok(Json(webhook)) +} diff --git a/crates/delta/src/routes/webhooks/webhook_execute.rs b/crates/delta/src/routes/webhooks/webhook_execute.rs new file mode 100644 index 00000000..8d1e8fe3 --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_execute.rs @@ -0,0 +1,138 @@ +use std::collections::HashSet; + +use revolt_quark::{Db, Ref, Result, Error, models::{Message, message::Reply}, web::idempotency::IdempotencyKey, types::push::MessageAuthor}; +use rocket::serde::json::Json; +use ulid::Ulid; +use crate::routes::channels::message_send::{DataMessageSend, RE_MENTION}; +use validator::Validate; + +/// # Executes a webhook +/// +/// executes a webhook and sends a message +#[openapi(tag = "Webhooks")] +#[post("//", data="")] +pub async fn req(db: &Db, target: Ref, token: String, data: Json, mut idempotency: IdempotencyKey) -> Result> { + let data = data.into_inner(); + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + let webhook = target.as_webhook(db).await?; + + (webhook.token == token) + .then_some(()) + .ok_or(Error::InvalidCredentials)?; + + Message::validate_sum(&data.content, &data.embeds)?; + + idempotency.consume_nonce(data.nonce).await?; + + let channel = Ref::from_unchecked(webhook.channel.clone()).as_channel(db).await?; + + 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(Error::EmptyMessage); + } + + let message_id = Ulid::new().to_string(); + let mut message = Message { + id: message_id.clone(), + channel: channel.id().to_string(), + webhook: Some(webhook.id.clone()), + masquerade: data.masquerade, + interactions: data.interactions.unwrap_or_default(), + ..Default::default() + }; + + // 1. 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()); + } + } + } + + // 4. Verify replies are valid. + let mut replies = HashSet::new(); + if let Some(entries) = data.replies { + if entries.len() > 5 { + return Err(Error::TooManyReplies); + } + + for Reply { id, mention } in entries { + let message = Ref::from_unchecked(id).as_message(db).await?; + + if mention { + mentions.insert(message.author_id().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::>()); + } + + // 5. Process included embeds. + let mut embeds = vec![]; + if let Some(sendable_embeds) = data.embeds { + for sendable_embed in sendable_embeds { + embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?) + } + } + + if !embeds.is_empty() { + message.embeds.replace(embeds); + } + + // 6. Add attachments to message. + let mut attachments = vec![]; + if let Some(ids) = &data.attachments { + // ! FIXME: move this to app config + if ids.len() > 5 { + return Err(Error::TooManyAttachments); + } + + for attachment_id in ids { + attachments.push( + db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id) + .await?, + ); + } + } + + if !attachments.is_empty() { + message.attachments.replace(attachments); + } + + // 7. Set content + message.content = data.content; + + // 8. Pass-through nonce value for clients + message.nonce = Some(idempotency.into_key()); + + message.create(db, &channel, Some(MessageAuthor::Webhook(&webhook))).await?; + + // Queue up a task for processing embeds + if let Some(content) = &message.content { + revolt_quark::tasks::process_embeds::queue( + channel.id().to_string(), + message.id.to_string(), + content.clone(), + ) + .await; + } + + Ok(Json(message)) + +} diff --git a/crates/delta/src/routes/webhooks/webhook_fetch.rs b/crates/delta/src/routes/webhooks/webhook_fetch.rs new file mode 100644 index 00000000..6472ba0e --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_fetch.rs @@ -0,0 +1,17 @@ +use revolt_quark::{Db, Ref, Result, Error, models::Webhook}; +use rocket::serde::json::Json; + +/// # gets a webhook +/// +/// gets a webhook +#[openapi(tag = "Webhooks")] +#[get("//")] +pub async fn req(db: &Db, target: Ref, token: String) -> Result> { + let webhook = target.as_webhook(db).await?; + + (webhook.token == token) + .then_some(()) + .ok_or(Error::InvalidCredentials)?; + + Ok(Json(webhook)) +} diff --git a/crates/quark/src/events/client.rs b/crates/quark/src/events/client.rs index 28f7eb6a..0fff286d 100644 --- a/crates/quark/src/events/client.rs +++ b/crates/quark/src/events/client.rs @@ -1,11 +1,12 @@ use serde::{Deserialize, Serialize}; use crate::models::channel::{FieldsChannel, PartialChannel}; +use crate::models::webhook::{PartialWebhook, FieldsWebhook}; use crate::models::message::{AppendMessage, PartialMessage}; use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer}; use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember}; use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus}; -use crate::models::{Channel, Emoji, Member, Message, Server, User, UserSettings}; +use crate::models::{Channel, Emoji, Member, Message, Server, User, UserSettings, Webhook}; use crate::Error; /// WebSocket Client Errors @@ -200,4 +201,19 @@ pub enum EventV1 { /// Delete emoji EmojiDelete { id: String }, + + /// New webhook + WebhookCreate(Webhook), + + /// Update existing webhook + WebhookUpdate { + id: String, + data: PartialWebhook, + remove: Vec + }, + + /// Delete webhook + WebhookDelete { + id: String + } } diff --git a/crates/quark/src/impl/dummy/channels/channel.rs b/crates/quark/src/impl/dummy/channels/channel.rs index b25406de..ebcbead6 100644 --- a/crates/quark/src/impl/dummy/channels/channel.rs +++ b/crates/quark/src/impl/dummy/channels/channel.rs @@ -1,4 +1,4 @@ -use crate::models::channel::{Channel, FieldsChannel, PartialChannel}; +use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}}; use crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result}; use super::super::DummyDb; diff --git a/crates/quark/src/impl/dummy/channels/message.rs b/crates/quark/src/impl/dummy/channels/message.rs index a01a32a1..4547c4ad 100644 --- a/crates/quark/src/impl/dummy/channels/message.rs +++ b/crates/quark/src/impl/dummy/channels/message.rs @@ -9,7 +9,7 @@ impl AbstractMessage for DummyDb { Ok(Message { id: id.into(), channel: "channel".into(), - author: "author".into(), + author: Some("author".into()), content: Some("message content".into()), ..Default::default() diff --git a/crates/quark/src/impl/dummy/channels/webhook.rs b/crates/quark/src/impl/dummy/channels/webhook.rs new file mode 100644 index 00000000..13bcfb2c --- /dev/null +++ b/crates/quark/src/impl/dummy/channels/webhook.rs @@ -0,0 +1,40 @@ +use crate::models::webhook::{Webhook, FieldsWebhook, PartialWebhook}; +use crate::{AbstractWebhook, Result}; + +use super::super::DummyDb; + +#[async_trait] +impl AbstractWebhook for DummyDb { + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> { + info!("Created webhook {} in {}", webhook.name, webhook.channel); + Ok(()) + } + + async fn delete_webhook(&self, webhook_id: &str) -> Result<()> { + info!("deleting webhook {webhook_id}"); + + Ok(()) + } + + async fn fetch_webhook(&self, webhook_id: &str) -> Result { + Ok(Webhook { + id: webhook_id.to_string(), + name: "".to_string(), + avatar: None, + channel: "0".to_string(), + token: "".to_owned(), + }) + } + + async fn update_webook(&self, webhook_id: &str, partial_webhook: &PartialWebhook, remove: &[FieldsWebhook]) -> Result<()> { + info!("updating webhook {webhook_id}, {partial_webhook:?}, removing {remove:?}"); + + Ok(()) + } + + async fn fetch_webhooks_for_channel(&self, channel: &str) -> Result> { + info!("fetching webhooks for channel {channel}"); + + Ok(vec![self.fetch_webhook("0").await?]) + } +} diff --git a/crates/quark/src/impl/dummy/mod.rs b/crates/quark/src/impl/dummy/mod.rs index cfb9b8ea..dcb41249 100644 --- a/crates/quark/src/impl/dummy/mod.rs +++ b/crates/quark/src/impl/dummy/mod.rs @@ -14,6 +14,7 @@ pub mod channels { pub mod channel_invite; pub mod channel_unread; pub mod message; + pub mod webhook; } pub mod servers { diff --git a/crates/quark/src/impl/generic/channels/message.rs b/crates/quark/src/impl/generic/channels/message.rs index 20318da6..d8843612 100644 --- a/crates/quark/src/impl/generic/channels/message.rs +++ b/crates/quark/src/impl/generic/channels/message.rs @@ -18,7 +18,7 @@ use crate::{ tasks::ack::AckEvent, types::{ january::{Embed, Text}, - push::PushNotification, + push::{PushNotification, MessageAuthor}, }, Database, Error, Permission, Result, }; @@ -66,7 +66,7 @@ impl Message { &mut self, db: &Database, channel: &Channel, - sender: Option<&User>, + sender: Option>, ) -> Result<()> { self.create_no_web_push(db, channel.id(), channel.is_direct_dm()) .await?; @@ -271,6 +271,14 @@ impl Message { // Write to database db.clear_reaction(&self.id, emoji).await } + + /// Gets either the user or the webhook id which sent this message + pub fn author_id(&self) -> &str { + match &self.author { + Some(id) => id, + None => self.webhook.as_deref().unwrap() + } + } } pub trait IntoUsers { @@ -279,7 +287,11 @@ pub trait IntoUsers { impl IntoUsers for Message { fn get_user_ids(&self) -> Vec { - let mut ids = vec![self.author.clone()]; + let mut ids = Vec::new(); + + if let Some(author) = &self.author { + ids.push(author.clone()); + }; if let Some(msg) = &self.system { match msg { @@ -319,7 +331,7 @@ impl SystemMessage { Message { id: Ulid::new().to_string(), channel, - author: "00000000000000000000000000".to_string(), + author: Some("00000000000000000000000000".to_string()), system: Some(self), ..Default::default() diff --git a/crates/quark/src/impl/generic/channels/webhook.rs b/crates/quark/src/impl/generic/channels/webhook.rs new file mode 100644 index 00000000..940c91c5 --- /dev/null +++ b/crates/quark/src/impl/generic/channels/webhook.rs @@ -0,0 +1,58 @@ +use crate::{ + events::client::EventV1, + models::{ + webhook::{Webhook, PartialWebhook, FieldsWebhook} + }, + Database, Result +}; + +impl Webhook { + pub async fn create(&self, db: &Database) -> Result<()> { + db.insert_webhook(self).await?; + + EventV1::WebhookCreate(self.clone()) + .p(self.channel.clone()).await; + + Ok(()) + } + + pub async fn delete(&self, db: &Database) -> Result<()> { + db.delete_webhook(&self.id).await?; + + EventV1::WebhookDelete { id: self.id.clone() } + .p(self.channel.clone()).await; + + Ok(()) + } + + pub async fn update( + &mut self, + db: &Database, + partial: PartialWebhook, + remove: Vec + ) -> Result<()> { + for field in &remove { + self.remove(field) + }; + + self.apply_options(partial.clone()); + + db.update_webook(&self.id, &partial, &remove).await?; + + EventV1::WebhookUpdate { + id: self.id.clone(), + data: partial, + remove + } + .p(self.channel.clone()) + .await; + + Ok(()) + } + + fn remove(&mut self, field: &FieldsWebhook) { + match field { + FieldsWebhook::Avatar => self.avatar = None + } + } +} diff --git a/crates/quark/src/impl/generic/mod.rs b/crates/quark/src/impl/generic/mod.rs index 4c963b06..5cf905df 100644 --- a/crates/quark/src/impl/generic/mod.rs +++ b/crates/quark/src/impl/generic/mod.rs @@ -14,6 +14,7 @@ pub mod channels { pub mod channel_invite; pub mod channel_unread; pub mod message; + pub mod webhook; } pub mod servers { diff --git a/crates/quark/src/impl/mongo/channels/channel.rs b/crates/quark/src/impl/mongo/channels/channel.rs index f0802a11..33aaa25f 100644 --- a/crates/quark/src/impl/mongo/channels/channel.rs +++ b/crates/quark/src/impl/mongo/channels/channel.rs @@ -37,9 +37,24 @@ impl MongoDb { operation: "delete_many", with: "channel_unreads", }) - .map(|_| ()) + .map(|_| ())?; // update many attachments with parent id + + // Delete all webhooks on this channel. + self.col::("webhooks") + .delete_many( + doc! { + "channel": &id + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "delete_many", + with: "webhooks", + }) + .map(|_| ()) } } diff --git a/crates/quark/src/impl/mongo/channels/webhook.rs b/crates/quark/src/impl/mongo/channels/webhook.rs new file mode 100644 index 00000000..f7f58fe9 --- /dev/null +++ b/crates/quark/src/impl/mongo/channels/webhook.rs @@ -0,0 +1,57 @@ +use crate::models::{webhook::{Webhook, PartialWebhook, FieldsWebhook}}; +use crate::r#impl::mongo::IntoDocumentPath; +use crate::{Result, AbstractWebhook}; + +use super::super::MongoDb; + +static COL: &str = "webhooks"; + +#[async_trait] +impl AbstractWebhook for MongoDb { + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> { + self.insert_one(COL, webhook).await?; + + Ok(()) + } + + async fn fetch_webhook(&self, webhook_id: &str) -> Result { + info!("{COL} {webhook_id}"); + + self.find_one_by_id(COL, webhook_id).await + } + + async fn delete_webhook(&self, webhook_id: &str) -> Result<()> { + self.delete_one_by_id(COL, webhook_id).await?; + + Ok(()) + } + + async fn update_webook(&self, webhook_id: &str, partial_webhook: &PartialWebhook, remove: &[FieldsWebhook]) -> Result<()> { + self.update_one_by_id( + COL, + webhook_id, + partial_webhook, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None, + ) + .await + .map(|_| ()) + } + + async fn fetch_webhooks_for_channel(&self, channel: &str) -> Result> { + self.find( + COL, + doc! { + "channel": channel + }) + .await + } +} + +impl IntoDocumentPath for FieldsWebhook { + fn as_path(&self) -> Option<&'static str> { + match self { + FieldsWebhook::Avatar => Some("avatar"), + } + } +} diff --git a/crates/quark/src/impl/mongo/mod.rs b/crates/quark/src/impl/mongo/mod.rs index fc4d7a2e..8c766219 100644 --- a/crates/quark/src/impl/mongo/mod.rs +++ b/crates/quark/src/impl/mongo/mod.rs @@ -25,6 +25,7 @@ pub mod channels { pub mod channel_invite; pub mod channel_unread; pub mod message; + pub mod webhook; } pub mod servers { diff --git a/crates/quark/src/models/channels/message.rs b/crates/quark/src/models/channels/message.rs index af66c473..cc90e99f 100644 --- a/crates/quark/src/models/channels/message.rs +++ b/crates/quark/src/models/channels/message.rs @@ -117,9 +117,12 @@ pub struct Message { pub nonce: Option, /// Id of the channel this message was sent in pub channel: String, - /// Id of the user that sent this message - pub author: String, - + /// Id of the user that sent this message, will be none when a webhook sent the message + #[serde(skip_serializing_if = "Option::is_none")] + pub author: Option, + /// Id of the webhook that sent this message, mutually exclusive with `author` + #[serde(skip_serializing_if = "Option::is_none")] + pub webhook: Option, /// Message content #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, diff --git a/crates/quark/src/models/channels/webhook.rs b/crates/quark/src/models/channels/webhook.rs new file mode 100644 index 00000000..b97ea52e --- /dev/null +++ b/crates/quark/src/models/channels/webhook.rs @@ -0,0 +1,33 @@ +use crate::models::File; +use serde::{Deserialize, Serialize}; + +/// Respresents a webhook +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct)] +#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)] +#[optional_name = "PartialWebhook"] +#[opt_skip_serializing_none] +#[opt_some_priority] + +pub struct Webhook { + /// Unique Id + #[serde(rename = "_id")] + pub id: String, + + /// The name of the webhook + pub name: String, + + /// The avatar of the webhook + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + /// The channel this webhook belongs to + pub channel: String, + + /// The private token for the webhook + pub token: String +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +pub enum FieldsWebhook { + Avatar +} diff --git a/crates/quark/src/models/mod.rs b/crates/quark/src/models/mod.rs index 20bf360e..f6a4b629 100644 --- a/crates/quark/src/models/mod.rs +++ b/crates/quark/src/models/mod.rs @@ -13,6 +13,7 @@ mod channels { pub mod channel_invite; pub mod channel_unread; pub mod message; + pub mod webhook; } mod servers { @@ -47,3 +48,4 @@ pub use server_member::Member; pub use simple::SimpleModel; pub use user::User; pub use user_settings::UserSettings; +pub use webhook::Webhook; diff --git a/crates/quark/src/traits/channels/channel.rs b/crates/quark/src/traits/channels/channel.rs index e1fa6140..e51c5ce0 100644 --- a/crates/quark/src/traits/channels/channel.rs +++ b/crates/quark/src/traits/channels/channel.rs @@ -1,4 +1,4 @@ -use crate::models::channel::{Channel, FieldsChannel, PartialChannel}; +use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}}; use crate::{OverrideField, Result}; #[async_trait] diff --git a/crates/quark/src/traits/channels/webhook.rs b/crates/quark/src/traits/channels/webhook.rs new file mode 100644 index 00000000..1836cf62 --- /dev/null +++ b/crates/quark/src/traits/channels/webhook.rs @@ -0,0 +1,11 @@ +use crate::models::webhook::{Webhook, PartialWebhook, FieldsWebhook}; +use crate::Result; + +#[async_trait] +pub trait AbstractWebhook: Sync + Send { + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>; + async fn fetch_webhook(&self, webhook_id: &str) -> Result; + async fn delete_webhook(&self, webhook_id: &str) -> Result<()>; + async fn update_webook(&self, webhook_id: &str, partial_webhook: &PartialWebhook, remove: &[FieldsWebhook]) -> Result<()>; + async fn fetch_webhooks_for_channel(&self, channel: &str) -> Result>; +} diff --git a/crates/quark/src/traits/mod.rs b/crates/quark/src/traits/mod.rs index 93764ed8..4691a072 100644 --- a/crates/quark/src/traits/mod.rs +++ b/crates/quark/src/traits/mod.rs @@ -12,6 +12,7 @@ mod channels { pub mod channel_invite; pub mod channel_unread; pub mod message; + pub mod webhook; } mod servers { @@ -35,6 +36,7 @@ pub use channels::channel::AbstractChannel; pub use channels::channel_invite::AbstractChannelInvite; pub use channels::channel_unread::AbstractChannelUnread; pub use channels::message::AbstractMessage; +pub use channels::webhook::AbstractWebhook; pub use servers::server::AbstractServer; pub use servers::server_ban::AbstractServerBan; @@ -60,5 +62,6 @@ pub trait AbstractDatabase: + AbstractBot + AbstractUser + AbstractUserSettings + + AbstractWebhook { } diff --git a/crates/quark/src/types/push.rs b/crates/quark/src/types/push.rs index d75e74f1..5a2a299e 100644 --- a/crates/quark/src/types/push.rs +++ b/crates/quark/src/types/push.rs @@ -2,7 +2,7 @@ use std::time::SystemTime; use serde::{Deserialize, Serialize}; -use crate::models::{Message, User}; +use crate::models::{Message, User, Webhook, File}; use crate::variables::delta::{APP_URL, AUTUMN_URL, PUBLIC_URL}; /// Push Notification @@ -25,14 +25,42 @@ pub struct PushNotification { pub url: String, } +pub enum MessageAuthor<'a> { + User(&'a User), + Webhook(&'a Webhook) +} + +impl<'a> MessageAuthor<'a> { + pub fn id(&self) -> &str { + match self { + MessageAuthor::User(user) => &user.id, + MessageAuthor::Webhook(webhook) => &webhook.id, + } + } + + pub fn avatar(&self) -> Option<&File> { + match self { + MessageAuthor::User(user) => user.avatar.as_ref(), + MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref(), + } + } + + pub fn username(&self) -> &str { + match self { + MessageAuthor::User(user) => &user.username, + MessageAuthor::Webhook(webhook) => &webhook.name, + } + } +} + impl PushNotification { /// Create a new notification from a given message, author and channel ID - pub fn new(msg: Message, author: Option<&User>, channel_id: &str) -> Self { - let icon = if let Some(author) = author { - if let Some(avatar) = &author.avatar { + pub fn new(msg: Message, author: Option>, channel_id: &str) -> Self { + let icon = if let Some(author) = &author { + if let Some(avatar) = author.avatar() { format!("{}/avatars/{}", &*AUTUMN_URL, avatar.id) } else { - format!("{}/users/{}/default_avatar", &*PUBLIC_URL, msg.author) + format!("{}/users/{}/default_avatar", &*PUBLIC_URL, author.id()) } } else { format!("{}/assets/logo.png", &*APP_URL) @@ -59,7 +87,7 @@ impl PushNotification { Self { author: author - .map(|x| x.username.to_string()) + .map(|x| x.username().to_string()) .unwrap_or_else(|| "Revolt".to_string()), icon, image, diff --git a/crates/quark/src/util/ref.rs b/crates/quark/src/util/ref.rs index d010b4c1..b75de9f2 100644 --- a/crates/quark/src/util/ref.rs +++ b/crates/quark/src/util/ref.rs @@ -4,7 +4,7 @@ use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User}; +use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook}; use crate::presence::presence_is_online; use crate::{Database, Error, Result}; @@ -78,6 +78,11 @@ impl Ref { pub async fn as_emoji(&self, db: &Database) -> Result { db.fetch_emoji(&self.id).await } + + /// Fetches webhook from Ref + pub async fn as_webhook(&self, db: &Database) -> Result { + db.fetch_webhook(&self.id).await + } } impl<'r> FromParam<'r> for Ref {