From 304567f6319f277f3f0b8a25a82486d648fa8924 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sat, 12 Feb 2022 20:17:43 +0100 Subject: [PATCH 01/15] implement textbox component --- src/types/components.rs | 74 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/src/types/components.rs b/src/types/components.rs index e228d50..1856f6e 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -1,4 +1,4 @@ -1#[cfg(feature = "builder")] +#[cfg(feature = "builder")] use std::error; #[cfg(feature = "builder")] use std::fmt::{self, Display}; @@ -33,7 +33,6 @@ pub struct MessageComponent { components: Option>, // Text input specific - label: Option, min_length: Option, max_length: Option, required: Option, @@ -55,7 +54,6 @@ impl Default for MessageComponent { max_values: None, min_values: None, components: None, - label: None, min_length: None, max_length: None, required: None, @@ -130,7 +128,6 @@ pub struct ComponentSelectMenu { max_values: u8, } -#[cfg(feature = "builder")] impl Default for ComponentSelectMenu { fn default() -> Self { Self { @@ -145,7 +142,6 @@ impl Default for ComponentSelectMenu { } } -#[cfg(feature = "builder")] impl From for MessageComponent { fn from(t: ComponentSelectMenu) -> Self { MessageComponent { @@ -498,3 +494,71 @@ impl Builder for ComponentSelectMenuBuilder { Ok(self.obj) } } + +#[derive(Clone, Debug, Default)] +pub struct ComponentTextBox { + placeholder: Option, + label: Option, + min_length: Option, + max_length: Option, + required: Option, +} + +impl From for MessageComponent { + fn from(t: ComponentTextBox) -> Self { + MessageComponent { + r#type: ComponentType::TextInput, + label: t.label, + min_length: t.min_length, + max_length: t.max_length, + required: t.required, + ..Default::default() + } + } +} + +#[cfg(feature = "builder")] +pub struct ComponentTextBoxBuilder { + obj: ComponentTextBox, +} + +#[cfg(feature = "builder")] +impl ComponentTextBoxBuilder { + pub fn placeholder(mut self, placeholder: String) -> Self { + self.obj.placeholder = Some(placeholder); + self + } + + pub fn min_length(mut self, minimum_length: u16) -> Self { + if minimum_length < 1 || minimum_length > 4000 { + warn!("Minimum length for this text box exceeds the limits, ignoring"); + return self; + } + + self.obj.min_length = Some(minimum_length); + self + } + + pub fn max_length(mut self, maximum_length: u16) -> Self { + if maximum_length < 2 || maximum_length > 4000 { + warn!("Maximum length for this text box exceeds the limits, ignoring"); + return self; + } + + self.obj.max_length = Some(maximum_length); + self + } + + pub fn required(mut self, is_required: bool) -> Self { + self.obj.required = Some(is_required); + self + } +} + +impl Builder for ComponentTextBoxBuilder { + type Error = std::convert::Infallible; + + fn build(self) -> Result { + Ok(self.obj.into()) + } +} From 232cdeba802679eb1055fbcf106eeb875f2f6c9d Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sat, 12 Feb 2022 20:18:02 +0100 Subject: [PATCH 02/15] add modal base struct + fixes --- src/types/modal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/modal.rs b/src/types/modal.rs index 1a06d55..439b46b 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -1,10 +1,10 @@ -use serde::{Serialize, Deserialize} +use serde::{Deserialize, Serialize}; +use super::components::MessageComponent; use serde_with::*; -use super::Components; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Modal{ +pub struct Modal { custom_id: String, title: String, components: Vec, From 03ad261c3c1ba67c89972eab7addea2330c7b3a1 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sat, 12 Feb 2022 20:23:40 +0100 Subject: [PATCH 03/15] Add builder feature flag to builder impl --- src/types/components.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/components.rs b/src/types/components.rs index 1856f6e..6e3555f 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -555,6 +555,7 @@ impl ComponentTextBoxBuilder { } } +#[cfg(feature = "builder")] impl Builder for ComponentTextBoxBuilder { type Error = std::convert::Infallible; From 4364a101c2d3f71ca7c19a1463bb9b3dbeeaaec6 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sat, 12 Feb 2022 21:00:01 +0100 Subject: [PATCH 04/15] Add new interaction types (+ autocomplete types) --- src/types/interaction.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/types/interaction.rs b/src/types/interaction.rs index 50c6948..b5f2882 100644 --- a/src/types/interaction.rs +++ b/src/types/interaction.rs @@ -91,6 +91,12 @@ pub enum InteractionType { /// A message component MessageComponent = 3, + + /// An autocomplete interaction + ApplicationCommandAutocomplete = 4, + + /// A response coming from a modal. + ModalSubmit = 5, } #[serde_as] @@ -318,6 +324,12 @@ pub enum InteractionResponseType { /// For components, edit the message the component was attached to UpdateMessage = 7, + + /// For responding to autocomplete interactions + ApplicationCommandAutocompleteResult = 8, + + /// Respond with a Popup [`Modal`] + Modal = 9, } #[serde_as] From 7e31f2f3f5eee0794ab63a8337841c026d00d4ab Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 00:32:26 +0100 Subject: [PATCH 05/15] implement ModalBuilder --- src/types/modal.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/types/modal.rs b/src/types/modal.rs index 439b46b..7d3e116 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -1,11 +1,106 @@ +use std::{error::Error, fmt}; + use serde::{Deserialize, Serialize}; use super::components::MessageComponent; use serde_with::*; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg(feature = "builder")] +use crate::Builder; + +use log::{warn}; + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct Modal { custom_id: String, title: String, components: Vec, } + + +#[cfg(feature = "builder")] +#[derive(Clone, Debug, Default)] +pub struct ModalBuilder{ + obj: Modal, +} + +#[cfg(feature = "builder")] +impl ModalBuilder{ + pub fn custom_id(mut self, id: String) -> Self{ + if id.len() > 100{ + warn!("Exceeding maximum id char count (100), ignoring"); + return self; + } + self.obj.custom_id = id; + self + } + + pub fn title(mut self, title: String) -> Self{ + self.obj.title = title; + self + } + + pub fn add_component(mut self, component: impl Into) -> Self{ + if self.obj.components.len() >= 5 { + warn!("Exceeding maximum modal component count (5), ignoring"); + return self; + } + self.obj.components.push(component.into()); + self + } +} + +#[cfg(feature = "builder")] +#[derive(Clone, Debug)] +pub enum ModalConversionError{ + MissingCustomId, + MissingTitle, + MissingComponents, + TooMuchComponents, +} + +#[cfg(feature = "builder")] +impl fmt::Display for ModalConversionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self{ + ModalConversionError::MissingCustomId => { + write!(f, "Missing a custom id for modal!") + }, + ModalConversionError::MissingTitle =>{ + write!(f, "Missing a title for modal!") + }, + ModalConversionError::MissingComponents => { + write!(f, "Modal does not contain any components!") + }, + ModalConversionError::TooMuchComponents => { + write!(f, "Modal contains too much components!") + }, + } + } +} + +#[cfg(feature = "builder")] +impl Error for ModalConversionError {} + +#[cfg(feature = "builder")] +impl Builder for ModalBuilder{ + type Error = ModalConversionError; + + fn build(self) -> Result{ + if self.obj.custom_id.len() < 1{ + return Err(ModalConversionError::MissingCustomId); + } + if self.obj.title.len() < 1 { + return Err(ModalConversionError::MissingTitle); + } + if self.obj.components.len() < 1{ + return Err(ModalConversionError::MissingComponents); + } + if self.obj.components.len() > 5{ + return Err(ModalConversionError::TooMuchComponents); + } + + return Ok(self.obj); + + } +} \ No newline at end of file From 9006545f210642c1dd954ccddeb25563a9b64009 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 00:38:37 +0100 Subject: [PATCH 06/15] add unimplemented! macros to interactiontype match --- src/handler.rs | 9 +++++++++ src/types/modal.rs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/handler.rs b/src/handler.rs index 0498679..d39ad64 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -465,6 +465,7 @@ impl InteractionHandler { match serde_json::from_str::(&body) { Err(e) => { // It's probably bad on our end if this code is reached. + // Could be that new features were implemented and breaks code error!("Failed to decode interaction! Error: {}", e); debug!("Body sent: {}", body); return ERROR_RESPONSE!(400, format!("Bad body: {}", e)); @@ -545,6 +546,14 @@ impl InteractionHandler { ); ERROR_RESPONSE!(501, "No associated handler found") } + }, + InteractionType::ApplicationCommandAutocomplete => { + //TODO: Implement autocomplete stuff + unimplemented!(); + }, + InteractionType::ModalSubmit => { + //TODO: Implement Modal submit event + unimplemented!(); } } } diff --git a/src/types/modal.rs b/src/types/modal.rs index 7d3e116..8ba86b3 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -7,7 +7,7 @@ use serde_with::*; #[cfg(feature = "builder")] use crate::Builder; - +#[cfg(feature = "builder")] use log::{warn}; #[derive(Clone, Debug, Serialize, Deserialize, Default)] From 8686eeccdf00e8e9d3fdd0b44ae3c031a6b5352b Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 00:39:28 +0100 Subject: [PATCH 07/15] format handler and stuff --- src/handler.rs | 4 ++-- src/types/modal.rs | 42 ++++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index d39ad64..ef07b6d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -546,11 +546,11 @@ impl InteractionHandler { ); ERROR_RESPONSE!(501, "No associated handler found") } - }, + } InteractionType::ApplicationCommandAutocomplete => { //TODO: Implement autocomplete stuff unimplemented!(); - }, + } InteractionType::ModalSubmit => { //TODO: Implement Modal submit event unimplemented!(); diff --git a/src/types/modal.rs b/src/types/modal.rs index 8ba86b3..399f6a9 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -8,7 +8,7 @@ use serde_with::*; #[cfg(feature = "builder")] use crate::Builder; #[cfg(feature = "builder")] -use log::{warn}; +use log::warn; #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct Modal { @@ -17,17 +17,16 @@ pub struct Modal { components: Vec, } - #[cfg(feature = "builder")] #[derive(Clone, Debug, Default)] -pub struct ModalBuilder{ +pub struct ModalBuilder { obj: Modal, } #[cfg(feature = "builder")] -impl ModalBuilder{ - pub fn custom_id(mut self, id: String) -> Self{ - if id.len() > 100{ +impl ModalBuilder { + pub fn custom_id(mut self, id: String) -> Self { + if id.len() > 100 { warn!("Exceeding maximum id char count (100), ignoring"); return self; } @@ -35,12 +34,12 @@ impl ModalBuilder{ self } - pub fn title(mut self, title: String) -> Self{ + pub fn title(mut self, title: String) -> Self { self.obj.title = title; self } - pub fn add_component(mut self, component: impl Into) -> Self{ + pub fn add_component(mut self, component: impl Into) -> Self { if self.obj.components.len() >= 5 { warn!("Exceeding maximum modal component count (5), ignoring"); return self; @@ -52,7 +51,7 @@ impl ModalBuilder{ #[cfg(feature = "builder")] #[derive(Clone, Debug)] -pub enum ModalConversionError{ +pub enum ModalConversionError { MissingCustomId, MissingTitle, MissingComponents, @@ -62,19 +61,19 @@ pub enum ModalConversionError{ #[cfg(feature = "builder")] impl fmt::Display for ModalConversionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self{ + match self { ModalConversionError::MissingCustomId => { write!(f, "Missing a custom id for modal!") - }, - ModalConversionError::MissingTitle =>{ + } + ModalConversionError::MissingTitle => { write!(f, "Missing a title for modal!") - }, + } ModalConversionError::MissingComponents => { write!(f, "Modal does not contain any components!") - }, + } ModalConversionError::TooMuchComponents => { write!(f, "Modal contains too much components!") - }, + } } } } @@ -83,24 +82,23 @@ impl fmt::Display for ModalConversionError { impl Error for ModalConversionError {} #[cfg(feature = "builder")] -impl Builder for ModalBuilder{ +impl Builder for ModalBuilder { type Error = ModalConversionError; - fn build(self) -> Result{ - if self.obj.custom_id.len() < 1{ + fn build(self) -> Result { + if self.obj.custom_id.len() < 1 { return Err(ModalConversionError::MissingCustomId); } if self.obj.title.len() < 1 { return Err(ModalConversionError::MissingTitle); } - if self.obj.components.len() < 1{ + if self.obj.components.len() < 1 { return Err(ModalConversionError::MissingComponents); } - if self.obj.components.len() > 5{ + if self.obj.components.len() > 5 { return Err(ModalConversionError::TooMuchComponents); } return Ok(self.obj); - } -} \ No newline at end of file +} From 54daf27170906dbc54111a1f6401af3ee59e3652 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 15:04:11 +0100 Subject: [PATCH 08/15] add docs + error for builder --- src/handler.rs | 4 +-- src/types/components.rs | 67 +++++++++++++++++++++++++++++++++++++++-- src/types/modal.rs | 23 ++++++++++++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index ef07b6d..246f9ba 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -177,7 +177,7 @@ impl InteractionHandler { self.data.insert(data); } /// Binds an async function to a **global** command. - /// Your function must take a [`Context`] as an argument and must return a [`InteractionResponse`]. + /// Your function must take a [`Context`] and optionally an [`InteractionHandler`] as an argument and must return a [`InteractionResponse`]. /// Make sure to use the `#[slash_command]` procedural macro to make it usable for the handler. /// /// Like: @@ -222,7 +222,7 @@ impl InteractionHandler { } /// Binds an async function to a **component**. - /// Your function must take a [`Context`] as an argument and must return a [`InteractionResponse`]. + /// Your function must take a [`Context`] and optionally an [`InteractionHandler`] as an argument and must return a [`InteractionResponse`]. /// Use the `#[component_handler]` procedural macro for your own convinence.eprintln! /// /// # Example diff --git a/src/types/components.rs b/src/types/components.rs index a8a8f6d..7bc1996 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -33,7 +33,6 @@ pub struct MessageComponent { components: Option>, // Text input specific - min_length: Option, max_length: Option, required: Option, @@ -497,6 +496,7 @@ impl Builder for ComponentSelectMenuBuilder { } #[derive(Clone, Debug, Default)] +/// A textbox component, where users can put text in pub struct ComponentTextBox { placeholder: Option, label: Option, @@ -519,17 +519,19 @@ impl From for MessageComponent { } #[cfg(feature = "builder")] +/// Build a textbox component pub struct ComponentTextBoxBuilder { obj: ComponentTextBox, } #[cfg(feature = "builder")] impl ComponentTextBoxBuilder { + /// Sets the placeholder pub fn placeholder(mut self, placeholder: String) -> Self { self.obj.placeholder = Some(placeholder); self } - + /// Sets the minimum amount of characters that need to be inserted pub fn min_length(mut self, minimum_length: u16) -> Self { if minimum_length < 1 || minimum_length > 4000 { warn!("Minimum length for this text box exceeds the limits, ignoring"); @@ -540,6 +542,9 @@ impl ComponentTextBoxBuilder { self } + /// Sets the maximum amount of characters that may be inserted + /// + /// **The maximum settable length is 4000 characters** pub fn max_length(mut self, maximum_length: u16) -> Self { if maximum_length < 2 || maximum_length > 4000 { warn!("Maximum length for this text box exceeds the limits, ignoring"); @@ -550,6 +555,7 @@ impl ComponentTextBoxBuilder { self } + /// Sets if this textbox is required to fill pub fn required(mut self, is_required: bool) -> Self { self.obj.required = Some(is_required); self @@ -558,9 +564,64 @@ impl ComponentTextBoxBuilder { #[cfg(feature = "builder")] impl Builder for ComponentTextBoxBuilder { - type Error = std::convert::Infallible; + type Error = ComponentTextBoxBuilderError; fn build(self) -> Result { + if let Some(ml) = self.obj.min_length.as_ref(){ + if ml > &4000{ + return Err(ComponentTextBoxBuilderError::MinimumLengthTooHigh); + } + } + if let Some(ml) = self.obj.max_length.as_ref(){ + if ml < &1 { + return Err(ComponentTextBoxBuilderError::MaximumLengthTooLow); + } + if ml > &4000 { + return Err(ComponentTextBoxBuilderError::MaximumLengthTooHigh); + } + if let Some(minl) = self.obj.min_length.as_ref(){ + if minl > ml { + return Err(ComponentTextBoxBuilderError::MinimumGreaterThanMaximum); + } + } + } Ok(self.obj.into()) } } + +#[cfg(feature = "builder")] +#[derive(Clone, Debug)] +/// Errors that arise when building the textbox component. +pub enum ComponentTextBoxBuilderError{ + /// The minimum length is set too high (> 4000) + MinimumLengthTooHigh, + /// The maximum length is set too high (> 4000) + MaximumLengthTooHigh, + /// The maximum length is set too low (< 1) + MaximumLengthTooLow, + /// The minimum required length is set to a higher value than the set maximum. + MinimumGreaterThanMaximum, +} + +#[cfg(feature = "builder")] +impl error::Error for ComponentTextBoxBuilderError {} + +#[cfg(feature = "builder")] +impl Display for ComponentTextBoxBuilderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ComponentTextBoxBuilderError::MinimumLengthTooHigh => { + write!(f, "Minimum length exceeded 4000 characters") + }, + ComponentTextBoxBuilderError::MaximumLengthTooHigh => { + write!(f, "Maximum length exceeded 4000 characters") + } + ComponentTextBoxBuilderError::MaximumLengthTooLow => { + write!(f, "Maximum length is below 1") + }, + ComponentTextBoxBuilderError::MinimumGreaterThanMaximum => { + write!(f, "The minimum length is greater than the maximum length") + } + } + } +} \ No newline at end of file diff --git a/src/types/modal.rs b/src/types/modal.rs index 1ff0667..46d8e03 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -11,7 +11,9 @@ use crate::Builder; use log::warn; #[derive(Clone, Debug, Serialize, Deserialize, Default)] - +/// A modal is a popup form formed after an interaction. +/// After sending an [`InteractionResponseType::Modal`], it will send out a form for the user to fill. +/// After filling, Discord will send out an [`InteractionType::ModalSubmit`], where data processing will be done. pub struct Modal { custom_id: String, title: String, @@ -20,12 +22,18 @@ pub struct Modal { #[cfg(feature = "builder")] #[derive(Clone, Debug, Default)] +/// Build a modal pub struct ModalBuilder { obj: Modal, } #[cfg(feature = "builder")] impl ModalBuilder { + /// Sets the custom_id. This is mandatory! + /// The custom id may not be more than 100 characters long + /// + /// ### Errors + /// If the supplied custom id is above 100 characters, the library will print out a warning and ignore the request. pub fn custom_id(mut self, id: String) -> Self { if id.len() > 100 { warn!("Exceeding maximum id char count (100), ignoring"); @@ -34,12 +42,15 @@ impl ModalBuilder { self.obj.custom_id = id; self } - + /// Sets the title. This is mandatory! pub fn title(mut self, title: String) -> Self { self.obj.title = title; self } - + /// Adds a component to the form. You must supply at lease 1 component and no more than 5 components. + /// + /// ### Errors + /// If the component count exceeds 5, this library will print out a warning and ignore the request. pub fn add_component(mut self, component: impl Into) -> Self { if self.obj.components.len() >= 5 { warn!("Exceeding maximum modal component count (5), ignoring"); @@ -52,10 +63,16 @@ impl ModalBuilder { #[cfg(feature = "builder")] #[derive(Clone, Debug)] +/// Errors when building a modal pub enum ModalConversionError { + /// Missing a custom id MissingCustomId, + /// Missing a title MissingTitle, + /// Missing components, modals need atleast **one** component MissingComponents, + + /// Too much components. Modals may only have up to five components. TooMuchComponents, } From 315ddb77d002b8bb4d06d7e00c46e1999f91f71d Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 16:56:44 +0100 Subject: [PATCH 09/15] implement sending modals untested --- src/types/components.rs | 22 ++++++++++++---------- src/types/interaction.rs | 24 ++++++++++++++++++++++++ src/types/modal.rs | 29 +++++++++++++++++++++++------ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/types/components.rs b/src/types/components.rs index 7bc1996..a5b5833 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -519,6 +519,7 @@ impl From for MessageComponent { } #[cfg(feature = "builder")] +#[derive(Clone, Debug, Default)] /// Build a textbox component pub struct ComponentTextBoxBuilder { obj: ComponentTextBox, @@ -527,7 +528,8 @@ pub struct ComponentTextBoxBuilder { #[cfg(feature = "builder")] impl ComponentTextBoxBuilder { /// Sets the placeholder - pub fn placeholder(mut self, placeholder: String) -> Self { + pub fn placeholder(mut self, placeholder: impl Into) -> Self { + let placeholder = placeholder.into(); self.obj.placeholder = Some(placeholder); self } @@ -543,7 +545,7 @@ impl ComponentTextBoxBuilder { } /// Sets the maximum amount of characters that may be inserted - /// + /// /// **The maximum settable length is 4000 characters** pub fn max_length(mut self, maximum_length: u16) -> Self { if maximum_length < 2 || maximum_length > 4000 { @@ -567,19 +569,19 @@ impl Builder for ComponentTextBoxBuilder { type Error = ComponentTextBoxBuilderError; fn build(self) -> Result { - if let Some(ml) = self.obj.min_length.as_ref(){ - if ml > &4000{ + if let Some(ml) = self.obj.min_length.as_ref() { + if ml > &4000 { return Err(ComponentTextBoxBuilderError::MinimumLengthTooHigh); } } - if let Some(ml) = self.obj.max_length.as_ref(){ + if let Some(ml) = self.obj.max_length.as_ref() { if ml < &1 { return Err(ComponentTextBoxBuilderError::MaximumLengthTooLow); } if ml > &4000 { return Err(ComponentTextBoxBuilderError::MaximumLengthTooHigh); } - if let Some(minl) = self.obj.min_length.as_ref(){ + if let Some(minl) = self.obj.min_length.as_ref() { if minl > ml { return Err(ComponentTextBoxBuilderError::MinimumGreaterThanMaximum); } @@ -592,7 +594,7 @@ impl Builder for ComponentTextBoxBuilder { #[cfg(feature = "builder")] #[derive(Clone, Debug)] /// Errors that arise when building the textbox component. -pub enum ComponentTextBoxBuilderError{ +pub enum ComponentTextBoxBuilderError { /// The minimum length is set too high (> 4000) MinimumLengthTooHigh, /// The maximum length is set too high (> 4000) @@ -612,16 +614,16 @@ impl Display for ComponentTextBoxBuilderError { match self { ComponentTextBoxBuilderError::MinimumLengthTooHigh => { write!(f, "Minimum length exceeded 4000 characters") - }, + } ComponentTextBoxBuilderError::MaximumLengthTooHigh => { write!(f, "Maximum length exceeded 4000 characters") } ComponentTextBoxBuilderError::MaximumLengthTooLow => { write!(f, "Maximum length is below 1") - }, + } ComponentTextBoxBuilderError::MinimumGreaterThanMaximum => { write!(f, "The minimum length is greater than the maximum length") } } } -} \ No newline at end of file +} diff --git a/src/types/interaction.rs b/src/types/interaction.rs index b5f2882..c0ccd04 100644 --- a/src/types/interaction.rs +++ b/src/types/interaction.rs @@ -332,6 +332,22 @@ pub enum InteractionResponseType { Modal = 9, } +impl From for InteractionResponse { + fn from(m: super::modal::Modal) -> InteractionResponse { + let r = InteractionResponse { + r#type: InteractionResponseType::Modal, + data: Some(InteractionApplicationCommandCallbackData { + custom_id: Some(m.get_custom_id()), + title: Some(m.get_title()), + components: Some(m.get_components()), + ..Default::default() + }), + }; + + r + } +} + #[serde_as] #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)] @@ -343,6 +359,8 @@ pub struct InteractionApplicationCommandCallbackData { allowed_mentions: Option, flags: Option, components: Option>, + custom_id: Option, + title: Option, } impl InteractionApplicationCommandCallbackData { @@ -654,6 +672,12 @@ impl Context { b } + /// Respond to an [`Interaction`] by sending a [`Modal`] + /// Note that this **does not** return an [`InteractionResponseBuilder`], but an [`InteractionResponse`] + pub fn respond_with_modal(&self, m: super::modal::Modal) -> InteractionResponse { + m.into() + } + /// Edit the original interaction response /// /// This takes an [`WebhookMessage`]. You can convert an [`InteractionResponse`] using [`WebhookMessage::from`]. diff --git a/src/types/modal.rs b/src/types/modal.rs index 46d8e03..e358059 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -11,7 +11,7 @@ use crate::Builder; use log::warn; #[derive(Clone, Debug, Serialize, Deserialize, Default)] -/// A modal is a popup form formed after an interaction. +/// A modal is a popup form formed after an interaction. /// After sending an [`InteractionResponseType::Modal`], it will send out a form for the user to fill. /// After filling, Discord will send out an [`InteractionType::ModalSubmit`], where data processing will be done. pub struct Modal { @@ -20,6 +20,21 @@ pub struct Modal { components: Vec, } +impl Modal { + /// Get custom id + pub fn get_custom_id(&self) -> String { + self.custom_id.clone() + } + /// Get title + pub fn get_title(&self) -> String { + self.title.clone() + } + /// Get components + pub fn get_components(&self) -> Vec { + self.components.clone() + } +} + #[cfg(feature = "builder")] #[derive(Clone, Debug, Default)] /// Build a modal @@ -29,12 +44,13 @@ pub struct ModalBuilder { #[cfg(feature = "builder")] impl ModalBuilder { - /// Sets the custom_id. This is mandatory! + /// Sets the custom_id. This is mandatory! /// The custom id may not be more than 100 characters long - /// + /// /// ### Errors /// If the supplied custom id is above 100 characters, the library will print out a warning and ignore the request. - pub fn custom_id(mut self, id: String) -> Self { + pub fn custom_id(mut self, id: impl Into) -> Self { + let id = id.into(); if id.len() > 100 { warn!("Exceeding maximum id char count (100), ignoring"); return self; @@ -43,12 +59,13 @@ impl ModalBuilder { self } /// Sets the title. This is mandatory! - pub fn title(mut self, title: String) -> Self { + pub fn title(mut self, title: impl Into) -> Self { + let title = title.into(); self.obj.title = title; self } /// Adds a component to the form. You must supply at lease 1 component and no more than 5 components. - /// + /// /// ### Errors /// If the component count exceeds 5, this library will print out a warning and ignore the request. pub fn add_component(mut self, component: impl Into) -> Self { From 4fd9ca543f87f25dab68e834001a4f6dcaa3faf2 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Sun, 13 Feb 2022 16:57:02 +0100 Subject: [PATCH 10/15] Beginning writing example --- examples/Cargo.toml | 3 ++- examples/e10_modals/Cargo.toml | 13 +++++++++ examples/e10_modals/README.md | 20 ++++++++++++++ examples/e10_modals/src/main.rs | 48 +++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 examples/e10_modals/Cargo.toml create mode 100644 examples/e10_modals/README.md create mode 100644 examples/e10_modals/src/main.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4e25d1a..068f496 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,5 +8,6 @@ members = [ "e6_components", "e7_embeds", "e8_managing_commands", - "e9_accessing_other_data" + "e9_accessing_other_data", + "e10_modals" ] \ No newline at end of file diff --git a/examples/e10_modals/Cargo.toml b/examples/e10_modals/Cargo.toml new file mode 100644 index 0000000..c95b59c --- /dev/null +++ b/examples/e10_modals/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "e10_modals" +version = "0.1.0" +authors = ["Hugo Woesthuis "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rusty_interaction = {path = "../../", features=["extended-handler"]} +actix-web = "3" +dotenv = "0.13.0" +env_logger = "0" \ No newline at end of file diff --git a/examples/e10_modals/README.md b/examples/e10_modals/README.md new file mode 100644 index 0000000..4d8022c --- /dev/null +++ b/examples/e10_modals/README.md @@ -0,0 +1,20 @@ +# Example 9: Accessing other data + +From version 0.2.0, the `InteractionHandler` has a field called `data`. This is used to access other data, like database connections for example. + +You can add data to the handler using `InteractionHandler::add_data()`. The backbone is an `AnyMap` and shares the same syntax with accessing data. + + +# Result +![Peek 2021-07-29 21-53](https://user-images.githubusercontent.com/10338882/127557511-724e139a-4a5c-44cf-b403-6d270bbd8953.gif) + + +# Running this example +You can use regular `cargo build` and `cargo run` commands. + +To run this example: + +`cargo run`. Note that you'll need to edit the `PUB_KEY`, `APP_ID` and `TOKEN` constants accordingly (it will panic if you don't give a vaild key). + +# Useful documentation +- [InteractionHandler](https://docs.rs/rusty_interaction/latest/rusty_interaction/handler/struct.InteractionHandler.html) diff --git a/examples/e10_modals/src/main.rs b/examples/e10_modals/src/main.rs new file mode 100644 index 0000000..cc87667 --- /dev/null +++ b/examples/e10_modals/src/main.rs @@ -0,0 +1,48 @@ +#[macro_use] +extern crate rusty_interaction; + +use rusty_interaction::handler::{InteractionHandler, ManipulationScope}; +use rusty_interaction::types::components::*; +use rusty_interaction::types::interaction::*; +// Relevant imports here +use rusty_interaction::types::modal::{Modal, ModalBuilder}; + +use rusty_interaction::Builder; + +const PUB_KEY: &str = "57028473720a7c1d4666132a68007f0902034a13c43cc2c1658b10b5fc754311"; +const APP_ID: u64 = 615112470033596416; + +#[slash_command] +async fn test( + handler: &mut InteractionHandler, + ctx: Context, +) -> Result { + println!("Got trigger"); + let test_modal = ModalBuilder::default() + .custom_id("TEST_MODAL") + .title("My Test Modal") + .add_component( + ComponentTextBoxBuilder::default() + .placeholder("Some placeholder") + .max_length(100) + .required(true) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + Ok(ctx.respond_with_modal(test_modal)) +} + +// The lib uses actix-web +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let mut handle = InteractionHandler::new(APP_ID, PUB_KEY, None); + + handle.add_global_command("summon", test); + + return handle.run(10080).await; +} From 6ab604b6807d863982ab835088335dbcff6c3476 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Thu, 21 Jul 2022 21:30:44 +0200 Subject: [PATCH 11/15] Fix security --- src/security.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/security.rs b/src/security.rs index 11de23b..494b2ba 100644 --- a/src/security.rs +++ b/src/security.rs @@ -1,6 +1,6 @@ use ed25519_dalek::Verifier; use ed25519_dalek::{PublicKey, Signature}; - +use std::convert::TryInto; /// If verification fails, it will return the `ValidationError` enum. pub enum ValidationError { /// For anything related to conversion errors @@ -22,13 +22,14 @@ pub fn verify_discord_message( body: &str, ) -> Result<(), ValidationError> { let signature_bytes = hex::decode(signature) - .map_err(|_| ValidationError::KeyConversionError { name: "Signature" })?; + .map_err(|_| ValidationError::KeyConversionError { name: "Hex conversion" })?; + + let signature_bytes: [u8; 64] = signature_bytes.try_into() + .map_err(|_| ValidationError::KeyConversionError { + name: "Signature Length", + })?; - let signature = Signature::from_bytes(signature_bytes.as_slice()).map_err(|_| { - ValidationError::KeyConversionError { - name: "From bytes conversion error", - } - })?; + let signature = Signature::new(signature_bytes); // Format the data to verify (Timestamp + body) let msg = format!("{}{}", timestamp, body); From 9d19c7d9fc5b3454879bbc6925f55c69147ba27a Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Thu, 21 Jul 2022 21:31:04 +0200 Subject: [PATCH 12/15] Generalize Styles Textboxes use the same field for styling --- src/types/components.rs | 96 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/types/components.rs b/src/types/components.rs index a5b5833..9c72ee5 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -1,3 +1,4 @@ +//use std::default::default; #[cfg(feature = "builder")] use std::error; #[cfg(feature = "builder")] @@ -20,7 +21,7 @@ use serde_with::*; pub struct MessageComponent { /// Type of component r#type: ComponentType, - style: Option, + style: Option, label: Option, emoji: Option, custom_id: Option, @@ -183,9 +184,13 @@ impl Default for ComponentButton { #[cfg(feature = "builder")] impl From for MessageComponent { fn from(t: ComponentButton) -> Self { + let mut a: Option = None; + if let Some(b) = t.style{ + a = Some(b.into()); + } MessageComponent { r#type: ComponentType::Button, - style: t.style, + style: a, label: t.label, emoji: t.emoji, custom_id: t.custom_id, @@ -196,6 +201,8 @@ impl From for MessageComponent { } } +/// Alias for generic styling +pub type ComponentStyle = u8; #[derive(Clone, Serialize_repr, Deserialize_repr, PartialEq, Debug)] #[repr(u8)] #[non_exhaustive] @@ -213,6 +220,12 @@ pub enum ComponentButtonStyle { Link = 5, } +impl From for ComponentStyle{ + fn from(a: ComponentButtonStyle) -> Self { + a as ComponentStyle + } +} + /// Builder for creating a Component Action Row #[cfg(feature = "builder")] @@ -231,6 +244,18 @@ impl Builder for ComponentRowBuilder { #[cfg(feature = "builder")] impl ComponentRowBuilder { + pub fn add_component(mut self, component: impl Into) -> Self{ + match self.obj.components.as_mut() { + None => { + self.obj.components = Some(vec![component.into()]); + } + Some(c) => { + c.push(component.into()); + } + } + self + } + /// Add a button pub fn add_button(mut self, button: ComponentButton) -> Self { match self.obj.components.as_mut() { @@ -256,6 +281,18 @@ impl ComponentRowBuilder { } self } + /// Add a textbox to the row + pub fn add_textbox(mut self, textbox: ComponentTextBox) -> Self{ + match self.obj.components.as_mut(){ + None => { + self.obj.components = Some(vec![textbox.into()]); + } + Some(c) =>{ + c.push(textbox.into()); + } + } + self + } #[deprecated(since = "0.1.9", note = "Use the `build()` function instead")] /// Finish building this row (returns a [`MessageComponent`]) @@ -495,21 +532,40 @@ impl Builder for ComponentSelectMenuBuilder { } } + +#[derive(Clone, Debug, Default)] +pub enum ComponentTextBoxStyle { + #[default] Short = 1, + Paragraph = 2, +} + +impl From for ComponentStyle{ + fn from(a: ComponentTextBoxStyle) -> Self { + a as ComponentStyle + } +} + #[derive(Clone, Debug, Default)] /// A textbox component, where users can put text in pub struct ComponentTextBox { placeholder: Option, - label: Option, + custom_id: String, + label: String, + style: ComponentTextBoxStyle, min_length: Option, max_length: Option, required: Option, } + + impl From for MessageComponent { fn from(t: ComponentTextBox) -> Self { MessageComponent { r#type: ComponentType::TextInput, - label: t.label, + label: Some(t.label), + custom_id: Some(t.custom_id), + style: Some(t.style.into()), min_length: t.min_length, max_length: t.max_length, required: t.required, @@ -527,6 +583,21 @@ pub struct ComponentTextBoxBuilder { #[cfg(feature = "builder")] impl ComponentTextBoxBuilder { + + /// Sets the custom ID of this text thing. **MANDATORY** + pub fn custom_id(mut self, id: impl Into) -> Self{ + let pid = id.into(); + + self.obj.custom_id = pid; + self + } + + // Set the textbox label. **MANDATORY** + pub fn label(mut self, label: impl Into) -> Self{ + self.obj.label = label.into(); + self + } + /// Sets the placeholder pub fn placeholder(mut self, placeholder: impl Into) -> Self { let placeholder = placeholder.into(); @@ -569,6 +640,13 @@ impl Builder for ComponentTextBoxBuilder { type Error = ComponentTextBoxBuilderError; fn build(self) -> Result { + + if self.obj.custom_id.is_empty() { + return Err(ComponentTextBoxBuilderError::MissingCustomId); + } + if self.obj.label.is_empty(){ + return Err(ComponentTextBoxBuilderError::MissingLabel); + } if let Some(ml) = self.obj.min_length.as_ref() { if ml > &4000 { return Err(ComponentTextBoxBuilderError::MinimumLengthTooHigh); @@ -595,6 +673,10 @@ impl Builder for ComponentTextBoxBuilder { #[derive(Clone, Debug)] /// Errors that arise when building the textbox component. pub enum ComponentTextBoxBuilderError { + /// The required custom id isn't set! + MissingCustomId, + /// Missing a label + MissingLabel, /// The minimum length is set too high (> 4000) MinimumLengthTooHigh, /// The maximum length is set too high (> 4000) @@ -624,6 +706,12 @@ impl Display for ComponentTextBoxBuilderError { ComponentTextBoxBuilderError::MinimumGreaterThanMaximum => { write!(f, "The minimum length is greater than the maximum length") } + ComponentTextBoxBuilderError::MissingCustomId => { + write!(f, "The required custom id has not been set for this text box") + } + ComponentTextBoxBuilderError::MissingLabel => { + write!(f, "The required label is not set!") + } } } } From f728dd091cf017a321a8c08376ecadc792e1b683 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Thu, 21 Jul 2022 21:31:23 +0200 Subject: [PATCH 13/15] Fix modal things --- src/types/modal.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/types/modal.rs b/src/types/modal.rs index e358059..9054a66 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -2,7 +2,7 @@ use std::{error::Error, fmt}; use serde::{Deserialize, Serialize}; -use super::components::MessageComponent; +use super::components::{MessageComponent, ComponentRowBuilder}; use serde_with::*; #[cfg(feature = "builder")] @@ -40,6 +40,7 @@ impl Modal { /// Build a modal pub struct ModalBuilder { obj: Modal, + comps : ComponentRowBuilder } #[cfg(feature = "builder")] @@ -69,11 +70,7 @@ impl ModalBuilder { /// ### Errors /// If the component count exceeds 5, this library will print out a warning and ignore the request. pub fn add_component(mut self, component: impl Into) -> Self { - if self.obj.components.len() >= 5 { - warn!("Exceeding maximum modal component count (5), ignoring"); - return self; - } - self.obj.components.push(component.into()); + self.comps = self.comps.add_component(component); self } } @@ -88,7 +85,6 @@ pub enum ModalConversionError { MissingTitle, /// Missing components, modals need atleast **one** component MissingComponents, - /// Too much components. Modals may only have up to five components. TooMuchComponents, } @@ -120,20 +116,27 @@ impl Error for ModalConversionError {} impl Builder for ModalBuilder { type Error = ModalConversionError; - fn build(self) -> Result { + fn build(mut self) -> Result { if self.obj.custom_id.len() < 1 { return Err(ModalConversionError::MissingCustomId); } if self.obj.title.len() < 1 { return Err(ModalConversionError::MissingTitle); } - if self.obj.components.len() < 1 { + /*if self.obj.components.len() < 1 { return Err(ModalConversionError::MissingComponents); - } + }*/ if self.obj.components.len() > 5 { return Err(ModalConversionError::TooMuchComponents); } + if let Ok(v) = self.comps.build(){ + self.obj.components = vec![v]; + } + else{ + return Err(ModalConversionError::MissingComponents); + } + return Ok(self.obj); } } From 11261e3ef6add7d6610534f35b891a926d997196 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Thu, 21 Jul 2022 21:31:32 +0200 Subject: [PATCH 14/15] Update examples --- examples/e10_modals/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/e10_modals/src/main.rs b/examples/e10_modals/src/main.rs index cc87667..8f88472 100644 --- a/examples/e10_modals/src/main.rs +++ b/examples/e10_modals/src/main.rs @@ -25,6 +25,8 @@ async fn test( ComponentTextBoxBuilder::default() .placeholder("Some placeholder") .max_length(100) + .label("My label") + .custom_id("MODAL_TEXT_BOX") .required(true) .build() .unwrap(), From a01adc612f3baf470ff3b91fb0548cda2ad87ed5 Mon Sep 17 00:00:00 2001 From: Hugo Woesthuis Date: Thu, 21 Jul 2022 21:36:07 +0200 Subject: [PATCH 15/15] add style func --- examples/e10_modals/src/main.rs | 5 +++-- src/types/components.rs | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/e10_modals/src/main.rs b/examples/e10_modals/src/main.rs index 8f88472..3d82eac 100644 --- a/examples/e10_modals/src/main.rs +++ b/examples/e10_modals/src/main.rs @@ -23,11 +23,12 @@ async fn test( .title("My Test Modal") .add_component( ComponentTextBoxBuilder::default() - .placeholder("Some placeholder") - .max_length(100) + .placeholder("Some placeholder") + .max_length(100) // Sets a maximum of 100 chars .label("My label") .custom_id("MODAL_TEXT_BOX") .required(true) + .style(ComponentTextBoxStyle::Paragraph) // Sets the textbox to be a paragraph style textbox (multiline) .build() .unwrap(), ) diff --git a/src/types/components.rs b/src/types/components.rs index 9c72ee5..a1fd1cc 100644 --- a/src/types/components.rs +++ b/src/types/components.rs @@ -244,6 +244,7 @@ impl Builder for ComponentRowBuilder { #[cfg(feature = "builder")] impl ComponentRowBuilder { + /// Add a component to this row pub fn add_component(mut self, component: impl Into) -> Self{ match self.obj.components.as_mut() { None => { @@ -592,12 +593,18 @@ impl ComponentTextBoxBuilder { self } - // Set the textbox label. **MANDATORY** + /// Set the textbox label. **MANDATORY** pub fn label(mut self, label: impl Into) -> Self{ self.obj.label = label.into(); self } + /// Sets the style of this textbox. See [`ComponentTextBoxStyle`] for types + pub fn style(mut self, style: ComponentTextBoxStyle) -> Self{ + self.obj.style = style; + self + } + /// Sets the placeholder pub fn placeholder(mut self, placeholder: impl Into) -> Self { let placeholder = placeholder.into();