Skip to main content

stoat/commands/
converter.rs

1use std::{fmt::Debug, sync::LazyLock};
2
3use async_trait::async_trait;
4use regex::Regex;
5use stoat_models::v0::{Channel, Emoji, Member, Role, User};
6
7use crate::{Error, commands::Context, ulid::Ulid};
8
9static ID_REGEX: LazyLock<Regex> =
10    LazyLock::new(|| Regex::new("^([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})$").unwrap());
11static USER_REGEX: LazyLock<Regex> =
12    LazyLock::new(|| Regex::new("^<@([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})>$").unwrap());
13static CHANNEL_REGEX: LazyLock<Regex> =
14    LazyLock::new(|| Regex::new("^<#([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})>$").unwrap());
15static ROLE_REGEX: LazyLock<Regex> =
16    LazyLock::new(|| Regex::new("^<%([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})>$").unwrap());
17static EMOJI_REGEX: LazyLock<Regex> =
18    LazyLock::new(|| Regex::new("^:([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}):$").unwrap());
19
20#[async_trait]
21pub trait Converter<
22    E: From<Error> + Clone + Debug + Send + Sync + 'static,
23    S: Debug + Clone + Send + Sync + 'static,
24>: Sized
25{
26    async fn from_context(context: &Context<E, S>) -> Result<Self, E> {
27        let input = context.words.next().ok_or(Error::MissingParameter)?;
28
29        Self::convert(context, input).await
30    }
31
32    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E>;
33}
34
35macro_rules! impl_parse_converter {
36    ($ty:ty) => {
37        #[async_trait]
38        impl<
39            E: From<Error> + Clone + Debug + Send + Sync + 'static,
40            S: Debug + Clone + Send + Sync + 'static,
41        > Converter<E, S> for $ty
42        {
43            async fn convert(_context: &Context<E, S>, input: String) -> Result<Self, E> {
44                input
45                    .parse::<$ty>()
46                    .map_err(|e| Error::ConverterError(e.to_string()).into())
47            }
48        }
49    };
50}
51
52impl_parse_converter!(u8);
53impl_parse_converter!(u16);
54impl_parse_converter!(u32);
55impl_parse_converter!(u64);
56impl_parse_converter!(u128);
57impl_parse_converter!(i8);
58impl_parse_converter!(i16);
59impl_parse_converter!(i32);
60impl_parse_converter!(i64);
61impl_parse_converter!(i128);
62impl_parse_converter!(f32);
63impl_parse_converter!(f64);
64
65#[async_trait]
66impl<
67    E: From<Error> + Clone + Debug + Send + Sync + 'static,
68    S: Debug + Clone + Send + Sync + 'static,
69> Converter<E, S> for String
70{
71    async fn convert(_context: &Context<E, S>, input: String) -> Result<Self, E> {
72        Ok(input)
73    }
74}
75
76#[async_trait]
77impl<
78    E: From<Error> + Clone + Debug + Send + Sync + 'static,
79    S: Debug + Clone + Send + Sync + 'static,
80> Converter<E, S> for bool
81{
82    async fn convert(_context: &Context<E, S>, input: String) -> Result<Self, E> {
83        match input.to_lowercase().as_str() {
84            "yes" | "y" | "true" | "t" | "1" | "enable" | "enabled" | "on" => Ok(true),
85            "no" | "n" | "false" | "f" | "0" | "disable" | "disabled" | "off" => Ok(false),
86            _ => Err(Error::ConverterError("Bad boolean value".to_string()).into()),
87        }
88    }
89}
90
91#[async_trait]
92impl<
93    E: From<Error> + Clone + Debug + Send + Sync + 'static,
94    S: Debug + Clone + Send + Sync + 'static,
95> Converter<E, S> for Ulid
96{
97    async fn convert(_context: &Context<E, S>, input: String) -> Result<Self, E> {
98        Ulid::from_string(input)
99            .map_err(|_| Error::ConverterError("Bad id value".to_string()).into())
100    }
101}
102
103#[async_trait]
104impl<
105    E: From<Error> + Clone + Debug + Send + Sync + 'static,
106    S: Debug + Clone + Send + Sync + 'static,
107> Converter<E, S> for User
108{
109    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
110        if let Some(captures) = USER_REGEX
111            .captures(&input)
112            .or_else(|| ID_REGEX.captures(&input))
113        {
114            let id = captures.get(1).unwrap().as_str();
115
116            let user = context.cache.get_user(id);
117
118            if let Some(user) = user {
119                return Ok(user.clone());
120            } else if let Ok(user) = context.http.fetch_user(id).await {
121                return Ok(user);
122            };
123        };
124
125        Err(Error::ConverterError("User not found".to_string()).into())
126    }
127}
128
129#[async_trait]
130impl<
131    E: From<Error> + Clone + Debug + Send + Sync + 'static,
132    S: Debug + Clone + Send + Sync + 'static,
133> Converter<E, S> for Channel
134{
135    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
136        if let Some(captures) = CHANNEL_REGEX
137            .captures(&input)
138            .or_else(|| ID_REGEX.captures(&input))
139        {
140            let id = captures.get(1).unwrap().as_str();
141
142            if let Some(channel) = context.cache.get_channel(id) {
143                return Ok(channel);
144            }
145        } else if let Some(entry) = context
146            .cache
147            .channels
148            .any_async(|_, channel| match channel {
149                Channel::TextChannel { name, .. } => name == &input,
150                _ => false,
151            })
152            .await
153        {
154            return Ok(entry.get().clone());
155        }
156
157        Err(Error::ConverterError("Channel not found".to_string()).into())
158    }
159}
160
161#[async_trait]
162impl<
163    E: From<Error> + Clone + Debug + Send + Sync + 'static,
164    S: Debug + Clone + Send + Sync + 'static,
165> Converter<E, S> for Role
166{
167    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
168        let Ok(server) = context.get_current_server() else {
169            return Err(Error::ConverterError("Role not found".to_string()).into());
170        };
171
172        if let Some(captures) = ROLE_REGEX
173            .captures(&input)
174            .or_else(|| ID_REGEX.captures(&input))
175        {
176            let id = captures.get(1).unwrap().as_str();
177
178            if let Some(role) = server.roles.get(id) {
179                return Ok(role.clone());
180            }
181        };
182
183        Err(Error::ConverterError("Role not found".to_string()).into())
184    }
185}
186
187#[async_trait]
188impl<
189    E: From<Error> + Clone + Debug + Send + Sync + 'static,
190    S: Debug + Clone + Send + Sync + 'static,
191> Converter<E, S> for Member
192{
193    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
194        if let Ok(server) = context.get_current_server() {
195            let user = <User as Converter<E, S>>::convert(context, input).await?;
196
197            if let Some(member) = context.cache.get_member(&server.id, &user.id) {
198                return Ok(member);
199            } else if let Ok(member) = context.http.fetch_member(&server.id, &user.id).await {
200                return Ok(member);
201            };
202        };
203
204        Err(Error::ConverterError("Member not found".to_string()).into())
205    }
206}
207
208#[async_trait]
209impl<
210    E: From<Error> + Clone + Debug + Send + Sync + 'static,
211    S: Debug + Clone + Send + Sync + 'static,
212> Converter<E, S> for Emoji
213{
214    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
215        if let Some(captures) = EMOJI_REGEX
216            .captures(&input)
217            .or_else(|| ID_REGEX.captures(&input))
218        {
219            let id = captures.get(1).unwrap().as_str();
220
221            if let Some(emoji) = context.cache.get_emoji(id) {
222                return Ok(emoji);
223            }
224        } else {
225            if let Some(emoji) = context
226                .cache
227                .emojis
228                .any_sync(|_, emoji| &emoji.name == &input)
229            {
230                return Ok(emoji.get().clone());
231            }
232        };
233
234        Err(Error::ConverterError("Emoji not found".to_string()).into())
235    }
236}
237
238pub struct ConsumeRest<T = String>(pub T);
239
240#[async_trait]
241impl<
242    T: Converter<E, S>,
243    E: From<Error> + Clone + Debug + Send + Sync + 'static,
244    S: Debug + Clone + Send + Sync + 'static,
245> Converter<E, S> for ConsumeRest<T>
246{
247    async fn from_context(context: &Context<E, S>) -> Result<Self, E> {
248        let words = context.words.rest().join(" ");
249
250        Self::convert(context, words).await
251    }
252
253    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
254        T::convert(context, input).await.map(Self)
255    }
256}
257
258#[cfg(feature = "either")]
259#[async_trait]
260impl<
261    E: From<Error> + Clone + Debug + Send + Sync + 'static,
262    S: Debug + Clone + Send + Sync + 'static,
263    L: Converter<E, S>,
264    R: Converter<E, S>,
265> Converter<E, S> for either::Either<L, R>
266{
267    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
268        if let Ok(left) = L::convert(context, input.clone()).await {
269            Ok(either::Either::Left(left))
270        } else {
271            R::convert(context, input).await.map(either::Either::Right)
272        }
273    }
274}
275
276#[async_trait]
277impl<
278    E: From<Error> + Clone + Debug + Send + Sync + 'static,
279    S: Debug + Clone + Send + Sync + 'static,
280    T: Converter<E, S>,
281> Converter<E, S> for Option<T>
282{
283    async fn from_context(context: &Context<E, S>) -> Result<Self, E> {
284        let Some(input) = context.words.next() else {
285            return Ok(None);
286        };
287
288        Self::convert(context, input).await
289    }
290
291    async fn convert(context: &Context<E, S>, input: String) -> Result<Self, E> {
292        Ok(T::convert(context, input).await.ok())
293    }
294}
295
296pub struct Greedy<T>(pub Vec<T>);
297
298#[async_trait]
299impl<
300    E: From<Error> + Clone + Debug + Send + Sync + 'static,
301    S: Debug + Clone + Send + Sync + 'static,
302    T: Converter<E, S> + Send + Sync,
303> Converter<E, S> for Greedy<T>
304{
305    async fn from_context(context: &Context<E, S>) -> Result<Self, E> {
306        let mut converted = Vec::new();
307
308        while let Some(arg) = context.words.next() {
309            if let Ok(value) = T::convert(context, arg).await {
310                converted.push(value);
311            } else {
312                context.words.undo();
313                break;
314            }
315        }
316
317        Ok(Self(converted))
318    }
319
320    async fn convert(_context: &Context<E, S>, _input: String) -> Result<Self, E> {
321        unreachable!("Cannot use Greedy inside another converter")
322    }
323}
324
325#[async_trait]
326impl<
327    E: From<Error> + Clone + Debug + Send + Sync + 'static,
328    S: Debug + Clone + Send + Sync + 'static,
329    T: Converter<E, S> + Send + Sync,
330> Converter<E, S> for Vec<T>
331{
332    async fn from_context(context: &Context<E, S>) -> Result<Self, E> {
333        let mut converted = Vec::new();
334
335        while let Some(arg) = context.words.next() {
336            converted.push(T::convert(context, arg).await?);
337        }
338
339        Ok(converted)
340    }
341
342    async fn convert(_context: &Context<E, S>, _input: String) -> Result<Self, E> {
343        unreachable!("Cannot use Vec inside another converter")
344    }
345}