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..3d82eac --- /dev/null +++ b/examples/e10_modals/src/main.rs @@ -0,0 +1,51 @@ +#[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) // 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(), + ) + .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; +} diff --git a/src/handler.rs b/src/handler.rs index 0498679..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 @@ -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)); @@ -546,6 +547,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/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); diff --git a/src/types/components.rs b/src/types/components.rs index 5e51aa5..a1fd1cc 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, @@ -32,6 +33,7 @@ pub struct MessageComponent { max_values: Option, components: Option>, + // Text input specific min_length: Option, max_length: Option, required: Option, @@ -127,7 +129,6 @@ pub struct ComponentSelectMenu { max_values: u8, } -#[cfg(feature = "builder")] impl Default for ComponentSelectMenu { fn default() -> Self { Self { @@ -142,7 +143,6 @@ impl Default for ComponentSelectMenu { } } -#[cfg(feature = "builder")] impl From for MessageComponent { fn from(t: ComponentSelectMenu) -> Self { MessageComponent { @@ -184,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, @@ -197,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] @@ -214,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")] @@ -232,6 +244,19 @@ 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 => { + 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() { @@ -257,6 +282,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,3 +532,193 @@ impl Builder for ComponentSelectMenuBuilder { Ok(self.obj) } } + + +#[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, + 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: 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, + ..Default::default() + } + } +} + +#[cfg(feature = "builder")] +#[derive(Clone, Debug, Default)] +/// Build a textbox component +pub struct ComponentTextBoxBuilder { + obj: ComponentTextBox, +} + +#[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 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(); + 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"); + return self; + } + + self.obj.min_length = Some(minimum_length); + 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"); + return self; + } + + self.obj.max_length = Some(maximum_length); + 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 + } +} + +#[cfg(feature = "builder")] +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); + } + } + 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 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) + 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") + } + 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!") + } + } + } +} diff --git a/src/types/interaction.rs b/src/types/interaction.rs index 50c6948..c0ccd04 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,28 @@ 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, +} + +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] @@ -331,6 +359,8 @@ pub struct InteractionApplicationCommandCallbackData { allowed_mentions: Option, flags: Option, components: Option>, + custom_id: Option, + title: Option, } impl InteractionApplicationCommandCallbackData { @@ -642,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 439b46b..9054a66 100644 --- a/src/types/modal.rs +++ b/src/types/modal.rs @@ -1,11 +1,142 @@ +use std::{error::Error, fmt}; + use serde::{Deserialize, Serialize}; -use super::components::MessageComponent; +use super::components::{MessageComponent, ComponentRowBuilder}; use serde_with::*; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg(feature = "builder")] +use crate::Builder; +#[cfg(feature = "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, 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 +pub struct ModalBuilder { + obj: Modal, + comps : ComponentRowBuilder +} + +#[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: impl Into) -> Self { + let id = id.into(); + if id.len() > 100 { + warn!("Exceeding maximum id char count (100), ignoring"); + return self; + } + self.obj.custom_id = id; + self + } + /// Sets the title. This is mandatory! + 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 { + self.comps = self.comps.add_component(component); + self + } +} + +#[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, +} + +#[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(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 { + 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); + } +}