From 4f07c3f69a129149fd07813b1744d1a374054e22 Mon Sep 17 00:00:00 2001 From: Restioson Date: Tue, 7 Jul 2020 10:19:38 +0200 Subject: [PATCH] basic webhook support --- Cargo.toml | 2 +- src/error.rs | 4 ++++ src/gateway/mod.rs | 2 +- src/http/mod.rs | 55 +++++++++++++++++++++++++++++++++++++++++-- src/http/routing.rs | 49 ++++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 3 +++ src/models/webhook.rs | 48 +++++++++++++++++++++++++++++++++++++ 7 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/models/webhook.rs diff --git a/Cargo.toml b/Cargo.toml index 676a760..4cd3286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ futures = "0.3.5" isahc = { version = "0.9.3", features = ["json"] } flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } -url = "2.1.1" +url = { version = "2.1.1", features = ["serde"] } log = "0.4.8" [dependencies.tokio] diff --git a/src/error.rs b/src/error.rs index ea3b52d..6d07be0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -76,6 +76,9 @@ pub enum PandaError { /// tungstenite TungsteniteError(TungsteniteError), + /// An invalid webhook id was given + InvalidWebhook, + RuntimeError, } @@ -102,6 +105,7 @@ impl fmt::Display for PandaError { Self::TungsteniteError(e) => write!(f, "Tungstenite Error: {}", e), Self::UnknownOpcodeSent => write!(f, "panda sent an invalid Opcode, please report the bug"), Self::InvalidDecodeSent => write!(f, "panda sent an invalid payload, please report the bug"), + Self::InvalidWebhook => write!(f, "invalid webhook id"), Self::RuntimeError => write!(f, "runtime error") } } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ff75aa2..bec4a07 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -84,7 +84,7 @@ impl GatewayConnection { log::error!("Disconnected from the gateway, starting reconnect..."); match GatewayConnection::new().await { Ok(g) => { - std::mem::replace(self, g); + *self = g; log::info!("Connected succesfully"); break; } diff --git a/src/http/mod.rs b/src/http/mod.rs index 88e3910..7790a04 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -9,6 +9,7 @@ use crate::{ models::{ channel::{Channel, Embed, Message}, user::User, + webhook::{Webhook, ExecuteWebhook}, }, }; @@ -427,8 +428,6 @@ impl HttpClient { let route = Route::get_channel_invite(channel_id); let res = self._make_request(route).await?; - dbg!(res); - Ok(()) } // // pub async fn create_channel_invite() {} @@ -488,4 +487,56 @@ impl HttpClient { // PUT/channels/{channel.id}/recipients/{user.id} // DELETE/channels/{channel.id}/recipients/{user.id} + + /// Creates a new webook, and returns the [`Webhook`]. For the format of the avatar data, see + /// [the Discord API docs](https://discord.com/developers/docs/reference#image-data). + /// + /// [`Webhook`]: ../../panda/models/webhook/struct.Webhook.html + pub async fn create_webhook( + &self, + channel_id: impl AsRef, + name: impl AsRef, + avatar_data: Option> + ) -> Result { + // Create body + let body = match avatar_data { + Some(data) => serde_json::json!({ "name": name.as_ref(), "avatar": data.as_ref() }), + None => serde_json::json!({ "name": name.as_ref() }), + }; + + // Parse to a valid Body, isahc::Body + let body = serde_json::to_string(&body)?; + + // Create route + let route = Route::create_webhook(channel_id, body); + let mut res = self._make_request(route).await?; + Ok(res.json()?) + } + + /// Deletes a webhook. + pub async fn delete_webhook(&self, webhook_id: impl AsRef) -> Result<()> { + // Create route + let route = Route::delete_webhook(webhook_id); + if self._make_request(route).await?.status() == StatusCode::NO_CONTENT { + Ok(()) + } else { + Err(PandaError::InvalidWebhook) + } + } + + /// Executes a webhook with the given parameters. + pub async fn execute_webhook( + &self, + webhook_id: impl AsRef, + token: impl AsRef, + params: ExecuteWebhook + ) -> Result<()> { + // Parse to a valid Body, isahc::Body + let body = serde_json::to_string(¶ms)?; + + // Create route + let route = Route::execute_webhook(webhook_id, token, body); + let _ = self._make_request(route).await?; + Ok(()) + } } diff --git a/src/http/routing.rs b/src/http/routing.rs index 19f29d1..08e438e 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -12,6 +12,9 @@ macro_rules! bucket_key { (emoji: $id: expr) => { format!("emoji:{}", $id.as_ref()); }; + (webhook: $id: expr) => { + format!("webhooks:{}", $id.as_ref()); + }; } macro_rules! api_request { @@ -322,6 +325,20 @@ impl Route<()> { body: (), } } + + pub(crate) fn delete_webhook(webhook_id: impl AsRef) -> Self { + let method = Method::DELETE; + let uri = api_request!("/webhooks/{}", webhook_id.as_ref()); + + let bucket_key = bucket_key!(webhook: webhook_id); + + Route { + method, + uri, + bucket_key, + body: (), + } + } } // Routes with body @@ -446,6 +463,38 @@ impl> Route { // body, // } // } + + pub(crate) fn create_webhook(channel_id: impl AsRef, body: B) -> Self { + let method = Method::POST; + let uri = api_request!("/channels/{}/webhooks", channel_id.as_ref()); + + let bucket_key = bucket_key!(channel: channel_id); + + Route { + method, + uri, + bucket_key, + body, + } + } + + pub(crate) fn execute_webhook( + webhook_id: impl AsRef, + token: impl AsRef, + body: B + ) -> Self { + let method = Method::POST; + let uri = api_request!("/webhooks/{}/{}", webhook_id.as_ref(), token.as_ref()); + + let bucket_key = bucket_key!(webhook: webhook_id); + + Route { + method, + uri, + bucket_key, + body, + } + } } /// Used to encode emoji as a valid char in URL diff --git a/src/models/mod.rs b/src/models/mod.rs index f82a56b..71b8a5d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,6 +16,8 @@ pub mod user; pub mod voice; #[doc(inline)] pub mod invite; +#[doc(inline)] +pub mod webhook; // Re-export all models pub use channel::*; @@ -25,3 +27,4 @@ pub use guild::*; pub use user::*; pub use voice::*; pub use invite::*; +pub use webhook::*; diff --git a/src/models/webhook.rs b/src/models/webhook.rs new file mode 100644 index 0000000..1d7f4a4 --- /dev/null +++ b/src/models/webhook.rs @@ -0,0 +1,48 @@ +use crate::models::user::User; +use crate::models::channel::Embed; +use serde::{Deserialize, Serialize}; +use serde_repr::*; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Webhook { + pub id: String, + #[serde(rename = "type")] + pub kind: WebhookKind, + pub guild_id: Option, + pub channel_id: String, + /// The user this webhook was created by. `None` when getting a webhook by its token. + pub user: Option, + pub name: Option, + pub avatar: Option, + /// The secure token of the webhook. Only `Some` for incoming webhooks ([`WebhookKind::Incoming`]) + /// + /// [`WebhookKind::Incoming`]: enum.WebhookKind.html#variant.Incoming + pub token: Option, +} + +#[derive(Debug, Serialize)] +pub struct ExecuteWebhook { + #[serde(flatten)] + pub payload: WebhookPayload, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tts: Option, +} + +#[derive(Debug, Serialize)] +pub enum WebhookPayload { + #[serde(rename = "content")] + MessageContent(String), + #[serde(rename = "embeds")] + Embeds(Vec), +} + +#[derive(Clone, Debug, Deserialize_repr, Serialize_repr, PartialEq)] +#[repr(u8)] +pub enum WebhookKind { + Incoming = 1, + ChannelFollower = 2, +}