serenity_slash_decode/
lib.rs

1//! Simple and easy to use slash command abstraction for [Serenity]
2//!
3//! Abstractions:
4//! - Puts all arguments of a slash command into a map with helper functions for easy argument handling
5//! - Returns full path of subcommand for easy routing
6//!
7//! For an example, check the `examples` directory
8//!
9//! [Serenity]: https://docs.rs/serenity/latest/serenity/
10
11mod errors;
12
13pub use crate::errors::{Error, Result};
14use serenity::model::channel::PartialChannel;
15use serenity::model::guild::{PartialMember, Role};
16use serenity::model::interactions::application_command::{
17    ApplicationCommandInteractionData, ApplicationCommandInteractionDataOptionValue,
18    ApplicationCommandOptionType,
19};
20use serenity::model::misc::{Mention, Mentionable as SerenityMentionable};
21use serenity::model::user::User;
22use std::collections::HashMap;
23
24/// Contains the values of the slash command
25#[derive(Debug)]
26pub struct SlashValue {
27    /// The actual value
28    inner: Option<ApplicationCommandInteractionDataOptionValue>,
29    /// The name of the parameter; Included for error messages
30    name: String,
31}
32
33/// Optionally contains a `PartialMember` so you don't need to do a cache lookup
34pub enum UserOrMember {
35    User(User),
36    Member(User, PartialMember),
37}
38
39impl UserOrMember {
40    fn from_pair(user: User, member: Option<PartialMember>) -> Self {
41        match member {
42            Some(m) => Self::Member(user, m),
43            None => Self::User(user),
44        }
45    }
46
47    /// Gets the inner user
48    pub fn get_user(&self) -> &User {
49        match self {
50            UserOrMember::User(s) => s,
51            UserOrMember::Member(u, _) => u,
52        }
53    }
54
55    /// Gets the inner member, if it exists
56    pub fn get_member(&self) -> Option<&PartialMember> {
57        match self {
58            UserOrMember::User(_) => None,
59            UserOrMember::Member(_, m) => Some(m),
60        }
61    }
62}
63
64/// Mentionables
65pub enum Mentionable {
66    UserOrMember(UserOrMember),
67    Role(Role),
68}
69
70impl SerenityMentionable for Mentionable {
71    fn mention(&self) -> Mention {
72        match self {
73            Mentionable::UserOrMember(u) => u.get_user().mention(),
74            Mentionable::Role(r) => r.mention(),
75        }
76    }
77}
78
79impl SlashValue {
80    fn get_type_name(&self) -> String {
81        match self.inner.as_ref().unwrap() {
82            ApplicationCommandInteractionDataOptionValue::String(_) => "String".to_string(),
83            ApplicationCommandInteractionDataOptionValue::Integer(_) => "Integer".to_string(),
84            ApplicationCommandInteractionDataOptionValue::Boolean(_) => "Boolean".to_string(),
85            ApplicationCommandInteractionDataOptionValue::User(_, _) => "User".to_string(),
86            ApplicationCommandInteractionDataOptionValue::Channel(_) => "Channel".to_string(),
87            ApplicationCommandInteractionDataOptionValue::Role(_) => "Role".to_string(),
88            _ => "Unknown".to_string(),
89        }
90    }
91
92    /// Returns the inner value if it is `Some`
93    pub fn expect_some(&self) -> Result<ApplicationCommandInteractionDataOptionValue> {
94        match self.inner.clone() {
95            Some(s) => Ok(s),
96            None => Err(Error::MissingValue {
97                name: self.name.clone(),
98            }),
99        }
100    }
101
102    /// Returns the inner value if it is a `String`
103    pub fn get_string(&self) -> Result<String> {
104        match self.expect_some()? {
105            ApplicationCommandInteractionDataOptionValue::String(s) => Ok(s),
106            _ => Err(Error::WrongType {
107                expected: "String".to_string(),
108                found: self.get_type_name(),
109                name: self.name.clone(),
110            }),
111        }
112    }
113
114    /// Returns the inner value if it is an `Integer`
115    pub fn get_integer(&self) -> Result<i64> {
116        match self.expect_some()? {
117            ApplicationCommandInteractionDataOptionValue::Integer(s) => Ok(s),
118            _ => Err(Error::WrongType {
119                expected: "Integer".to_string(),
120                found: self.get_type_name(),
121                name: self.name.clone(),
122            }),
123        }
124    }
125
126    /// Returns the inner value if it is a `Boolean`
127    pub fn get_boolean(&self) -> Result<bool> {
128        match self.expect_some()? {
129            ApplicationCommandInteractionDataOptionValue::Boolean(s) => Ok(s),
130            _ => Err(Error::WrongType {
131                expected: "Boolean".to_string(),
132                found: self.get_type_name(),
133                name: self.name.clone(),
134            }),
135        }
136    }
137
138    /// Returns the inner value if it is a `UserOrMember`
139    pub fn get_user(&self) -> Result<UserOrMember> {
140        match self.expect_some()? {
141            ApplicationCommandInteractionDataOptionValue::User(u, m) => {
142                Ok(UserOrMember::from_pair(u, m))
143            }
144            _ => Err(Error::WrongType {
145                expected: "User".to_string(),
146                found: self.get_type_name(),
147                name: self.name.clone(),
148            }),
149        }
150    }
151
152    /// Returns the inner value if it is a `PartialChannel`
153    pub fn get_channel(&self) -> Result<PartialChannel> {
154        match self.expect_some()? {
155            ApplicationCommandInteractionDataOptionValue::Channel(s) => Ok(s),
156            _ => Err(Error::WrongType {
157                expected: "Channel".to_string(),
158                found: self.get_type_name(),
159                name: self.name.clone(),
160            }),
161        }
162    }
163
164    /// Returns the inner value if it is a `Role`
165    pub fn get_role(&self) -> Result<Role> {
166        match self.expect_some()? {
167            ApplicationCommandInteractionDataOptionValue::Role(s) => Ok(s),
168            _ => Err(Error::WrongType {
169                expected: "Role".to_string(),
170                found: self.get_type_name(),
171                name: self.name.clone(),
172            }),
173        }
174    }
175
176    /// Returns the inner value if it is a `Mentionable`
177    pub fn get_mentionable(&self) -> Result<Mentionable> {
178        match self.expect_some()? {
179            ApplicationCommandInteractionDataOptionValue::User(u, m) => {
180                Ok(Mentionable::UserOrMember(UserOrMember::from_pair(u, m)))
181            }
182            ApplicationCommandInteractionDataOptionValue::Role(r) => Ok(Mentionable::Role(r)),
183            _ => Err(Error::WrongType {
184                expected: "Mentionable".to_string(),
185                found: self.get_type_name(),
186                name: self.name.clone(),
187            }),
188        }
189    }
190}
191
192/// Wrapper around `HashMap<String, SlashValue>`
193pub struct SlashMap(HashMap<String, SlashValue>);
194
195impl SlashMap {
196    fn new() -> Self {
197        Self(HashMap::new())
198    }
199
200    /// If `SlashMap` has value, call `SlashValue::get_string()` on it
201    pub fn get_string(&self, name: &str) -> Result<String> {
202        match self.0.get(name) {
203            Some(s) => s.get_string(),
204            None => Err(Error::MissingValue {
205                name: name.to_string(),
206            }),
207        }
208    }
209
210    /// If `SlashMap` has value, call `SlashValue::get_integer()` on it
211    pub fn get_integer(&self, name: &str) -> Result<i64> {
212        match self.0.get(name) {
213            Some(s) => s.get_integer(),
214            None => Err(Error::MissingValue {
215                name: name.to_string(),
216            }),
217        }
218    }
219
220    /// If `SlashMap` has value, call `SlashValue::get_boolean()` on it
221    pub fn get_boolean(&self, name: &str) -> Result<bool> {
222        match self.0.get(name) {
223            Some(s) => s.get_boolean(),
224            None => Err(Error::MissingValue {
225                name: name.to_string(),
226            }),
227        }
228    }
229
230    /// If `SlashMap` has value, call `SlashValue::get_user()` on it
231    pub fn get_user(&self, name: &str) -> Result<UserOrMember> {
232        match self.0.get(name) {
233            Some(s) => s.get_user(),
234            None => Err(Error::MissingValue {
235                name: name.to_string(),
236            }),
237        }
238    }
239
240    /// If `SlashMap` has value, call `SlashValue::get_channel()` on it
241    pub fn get_channel(&self, name: &str) -> Result<PartialChannel> {
242        match self.0.get(name) {
243            Some(s) => s.get_channel(),
244            None => Err(Error::MissingValue {
245                name: name.to_string(),
246            }),
247        }
248    }
249
250    /// If `SlashMap` has value, call `SlashValue::get_role()` on it
251    pub fn get_role(&self, name: &str) -> Result<Role> {
252        match self.0.get(name) {
253            Some(s) => s.get_role(),
254            None => Err(Error::MissingValue {
255                name: name.to_string(),
256            }),
257        }
258    }
259
260    /// If `SlashMap` has value, call `SlashValue::get_mentionable()` on it
261    pub fn get_mentionable(&self, name: &str) -> Result<Mentionable> {
262        match self.0.get(name) {
263            Some(s) => s.get_mentionable(),
264            None => Err(Error::MissingValue {
265                name: name.to_string(),
266            }),
267        }
268    }
269}
270
271/// For derive macros
272pub trait FromSlashMap {
273    fn from_slash_map(_: SlashMap) -> Result<Self>
274    where
275        Self: Sized;
276}
277
278/// Processes a `ApplicationCommandInteractionData` and returns the path and arguments
279pub fn process(interaction: &ApplicationCommandInteractionData) -> (String, SlashMap) {
280    // traverse
281    let mut options = &interaction.options;
282    let mut path = Vec::new();
283    path.push(interaction.name.clone());
284
285    loop {
286        match options.get(0) {
287            None => break,
288            Some(option) => {
289                if matches!(
290                    option.kind,
291                    ApplicationCommandOptionType::SubCommand
292                        | ApplicationCommandOptionType::SubCommandGroup
293                ) {
294                    path.push(option.name.clone());
295                    options = &option.options;
296                } else {
297                    break;
298                }
299            }
300        }
301    }
302
303    // map data
304    let mut map = SlashMap::new();
305    for option in options {
306        map.0.insert(
307            option.name.clone(),
308            SlashValue {
309                inner: option.resolved.clone(),
310                name: option.name.clone(),
311            },
312        );
313    }
314
315    (path.join(" "), map)
316}