webhook/
models.rs

1use serde::{Deserialize, Serialize, Serializer};
2use std::collections::HashSet;
3
4type Snowflake = String;
5
6#[derive(Deserialize, Debug)]
7pub struct Webhook {
8    pub id: Snowflake,
9    #[serde(rename = "type")]
10    pub webhook_type: i8,
11    pub guild_id: Snowflake,
12    pub channel_id: Snowflake,
13    pub name: Option<String>,
14    pub avatar: Option<String>,
15    pub token: String,
16    pub application_id: Option<Snowflake>,
17}
18
19#[derive(Debug)]
20pub(crate) struct MessageContext {
21    custom_ids: HashSet<String>,
22}
23
24impl MessageContext {
25    /// Tries to register a custom id.
26    ///
27    /// # Arguments
28    ///
29    /// * `id`: the custom id to be registered
30    ///
31    ///
32    /// # Return value
33    /// Returns true if the custom id is unique.
34    ///
35    /// Returns false if the supplied custom id is duplicate of an already registered custom id.
36    pub fn register_custom_id(&mut self, id: &str) -> bool {
37        self.custom_ids.insert(id.to_string())
38    }
39
40    pub fn new() -> MessageContext {
41        MessageContext {
42            custom_ids: HashSet::new(),
43        }
44    }
45}
46
47#[derive(Serialize, Debug)]
48pub struct Message {
49    pub content: Option<String>,
50    pub username: Option<String>,
51    pub avatar_url: Option<String>,
52    pub tts: bool,
53    pub embeds: Vec<Embed>,
54    pub allow_mentions: Option<AllowedMentions>,
55    #[serde(rename = "components")]
56    pub action_rows: Vec<ActionRow>,
57}
58
59impl Message {
60    pub fn new() -> Self {
61        Self {
62            content: None,
63            username: None,
64            avatar_url: None,
65            tts: false,
66            embeds: vec![],
67            allow_mentions: None,
68            action_rows: vec![],
69        }
70    }
71
72    pub fn content(&mut self, content: &str) -> &mut Self {
73        self.content = Some(content.to_owned());
74        self
75    }
76
77    pub fn username(&mut self, username: &str) -> &mut Self {
78        self.username = Some(username.to_owned());
79        self
80    }
81
82    pub fn avatar_url(&mut self, avatar_url: &str) -> &mut Self {
83        self.avatar_url = Some(avatar_url.to_owned());
84        self
85    }
86
87    pub fn tts(&mut self, tts: bool) -> &mut Self {
88        self.tts = tts;
89        self
90    }
91
92    pub fn embed<Func>(&mut self, func: Func) -> &mut Self
93    where
94        Func: Fn(&mut Embed) -> &mut Embed,
95    {
96        let mut embed = Embed::new();
97        func(&mut embed);
98        self.embeds.push(embed);
99
100        self
101    }
102
103    pub fn action_row<Func>(&mut self, func: Func) -> &mut Self
104    where
105        Func: Fn(&mut ActionRow) -> &mut ActionRow,
106    {
107        let mut row = ActionRow::new();
108        func(&mut row);
109        self.action_rows.push(row);
110
111        self
112    }
113
114    pub fn max_action_row_count() -> usize {
115        5
116    }
117
118    pub fn label_max_len() -> usize {
119        80
120    }
121
122    pub fn custom_id_max_len() -> usize {
123        100
124    }
125
126    pub fn allow_mentions(
127        &mut self,
128        parse: Option<Vec<AllowedMention>>,
129        roles: Option<Vec<Snowflake>>,
130        users: Option<Vec<Snowflake>>,
131        replied_user: bool,
132    ) -> &mut Self {
133        self.allow_mentions = Some(AllowedMentions::new(parse, roles, users, replied_user));
134        self
135    }
136}
137
138#[derive(Serialize, Debug)]
139pub struct Embed {
140    pub title: Option<String>,
141    #[serde(rename = "type")]
142    embed_type: String,
143    pub description: Option<String>,
144    pub url: Option<String>,
145    // ISO8601,
146    pub timestamp: Option<String>,
147    pub color: Option<String>,
148    pub footer: Option<EmbedFooter>,
149    pub image: Option<EmbedImage>,
150    pub video: Option<EmbedVideo>,
151    pub thumbnail: Option<EmbedThumbnail>,
152    pub provider: Option<EmbedProvider>,
153    pub author: Option<EmbedAuthor>,
154    pub fields: Vec<EmbedField>,
155}
156
157impl Embed {
158    pub fn new() -> Self {
159        Self {
160            title: None,
161            embed_type: String::from("rich"),
162            description: None,
163            url: None,
164            timestamp: None,
165            color: None,
166            footer: None,
167            image: None,
168            video: None,
169            thumbnail: None,
170            provider: None,
171            author: None,
172            fields: vec![],
173        }
174    }
175
176    pub fn title(&mut self, title: &str) -> &mut Self {
177        self.title = Some(title.to_owned());
178        self
179    }
180
181    pub fn description(&mut self, description: &str) -> &mut Self {
182        self.description = Some(description.to_owned());
183        self
184    }
185
186    pub fn url(&mut self, url: &str) -> &mut Self {
187        self.url = Some(url.to_owned());
188        self
189    }
190
191    pub fn timestamp(&mut self, timestamp: &str) -> &mut Self {
192        self.timestamp = Some(timestamp.to_owned());
193        self
194    }
195
196    pub fn color(&mut self, color: &str) -> &mut Self {
197        self.color = Some(color.to_owned());
198        self
199    }
200
201    pub fn footer(&mut self, text: &str, icon_url: Option<String>) -> &mut Self {
202        self.footer = Some(EmbedFooter::new(text, icon_url));
203        self
204    }
205
206    pub fn image(&mut self, url: &str) -> &mut Self {
207        self.image = Some(EmbedImage::new(url));
208        self
209    }
210
211    pub fn video(&mut self, url: &str) -> &mut Self {
212        self.video = Some(EmbedVideo::new(url));
213        self
214    }
215
216    pub fn thumbnail(&mut self, url: &str) -> &mut Self {
217        self.thumbnail = Some(EmbedThumbnail::new(url));
218        self
219    }
220
221    pub fn provider(&mut self, name: &str, url: &str) -> &mut Self {
222        self.provider = Some(EmbedProvider::new(name, url));
223        self
224    }
225
226    pub fn author(
227        &mut self,
228        name: &str,
229        url: Option<String>,
230        icon_url: Option<String>,
231    ) -> &mut Self {
232        self.author = Some(EmbedAuthor::new(name, url, icon_url));
233        self
234    }
235
236    pub fn field(&mut self, name: &str, value: &str, inline: bool) -> &mut Self {
237        if self.fields.len() == 25 {
238            panic!("You can't have more than 25 fields in an embed!")
239        }
240
241        self.fields.push(EmbedField::new(name, value, inline));
242        self
243    }
244}
245
246#[derive(Serialize, Debug)]
247pub struct EmbedField {
248    pub name: String,
249    pub value: String,
250    pub inline: bool,
251}
252
253impl EmbedField {
254    pub fn new(name: &str, value: &str, inline: bool) -> Self {
255        Self {
256            name: name.to_owned(),
257            value: value.to_owned(),
258            inline,
259        }
260    }
261}
262
263#[derive(Serialize, Debug)]
264pub struct EmbedFooter {
265    pub text: String,
266    pub icon_url: Option<String>,
267}
268
269impl EmbedFooter {
270    pub fn new(text: &str, icon_url: Option<String>) -> Self {
271        Self {
272            text: text.to_owned(),
273            icon_url,
274        }
275    }
276}
277
278pub type EmbedImage = EmbedUrlSource;
279pub type EmbedThumbnail = EmbedUrlSource;
280pub type EmbedVideo = EmbedUrlSource;
281
282#[derive(Serialize, Debug)]
283pub struct EmbedUrlSource {
284    pub url: String,
285}
286
287impl EmbedUrlSource {
288    pub fn new(url: &str) -> Self {
289        Self {
290            url: url.to_owned(),
291        }
292    }
293}
294
295#[derive(Serialize, Debug)]
296pub struct EmbedProvider {
297    pub name: String,
298    pub url: String,
299}
300
301impl EmbedProvider {
302    pub fn new(name: &str, url: &str) -> Self {
303        Self {
304            name: name.to_owned(),
305            url: url.to_owned(),
306        }
307    }
308}
309
310#[derive(Serialize, Debug)]
311pub struct EmbedAuthor {
312    pub name: String,
313    pub url: Option<String>,
314    pub icon_url: Option<String>,
315}
316
317impl EmbedAuthor {
318    pub fn new(name: &str, url: Option<String>, icon_url: Option<String>) -> Self {
319        Self {
320            name: name.to_owned(),
321            url,
322            icon_url,
323        }
324    }
325}
326
327pub enum AllowedMention {
328    RoleMention,
329    UserMention,
330    EveryoneMention,
331}
332
333fn resolve_allowed_mention_name(allowed_mention: AllowedMention) -> String {
334    match allowed_mention {
335        AllowedMention::RoleMention => "roles".to_string(),
336        AllowedMention::UserMention => "users".to_string(),
337        AllowedMention::EveryoneMention => "everyone".to_string(),
338    }
339}
340
341#[derive(Serialize, Debug)]
342pub struct AllowedMentions {
343    pub parse: Option<Vec<String>>,
344    pub roles: Option<Vec<Snowflake>>,
345    pub users: Option<Vec<Snowflake>>,
346    pub replied_user: bool,
347}
348
349impl AllowedMentions {
350    pub fn new(
351        parse: Option<Vec<AllowedMention>>,
352        roles: Option<Vec<Snowflake>>,
353        users: Option<Vec<Snowflake>>,
354        replied_user: bool,
355    ) -> Self {
356        let mut parse_strings: Vec<String> = vec![];
357        if parse.is_some() {
358            parse
359                .unwrap()
360                .into_iter()
361                .for_each(|x| parse_strings.push(resolve_allowed_mention_name(x)))
362        }
363
364        Self {
365            parse: Some(parse_strings),
366            roles,
367            users,
368            replied_user,
369        }
370    }
371}
372
373// ready to be extended with other components
374// non-composite here specifically means *not an action row*
375#[derive(Debug)]
376enum NonCompositeComponent {
377    Button(Button),
378}
379
380impl Serialize for NonCompositeComponent {
381    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
382    where
383        S: Serializer,
384    {
385        match self {
386            NonCompositeComponent::Button(button) => button.serialize(serializer),
387        }
388    }
389}
390
391#[derive(Serialize, Debug)]
392pub struct ActionRow {
393    #[serde(rename = "type")]
394    pub component_type: u8,
395    components: Vec<NonCompositeComponent>,
396}
397
398impl ActionRow {
399    fn new() -> ActionRow {
400        ActionRow {
401            component_type: 1,
402            components: vec![],
403        }
404    }
405
406    pub fn link_button<Func>(&mut self, button_mutator: Func) -> &mut Self
407    where
408        Func: Fn(&mut LinkButton) -> &mut LinkButton,
409    {
410        let mut button = LinkButton::new();
411        button_mutator(&mut button);
412        self.components.push(NonCompositeComponent::Button(
413            button.to_serializable_button()
414        ));
415        self
416    }
417
418    pub fn regular_button<Func>(&mut self, button_mutator: Func) -> &mut Self
419    where
420        Func: Fn(&mut RegularButton) -> &mut RegularButton,
421    {
422        let mut button = RegularButton::new();
423        button_mutator(&mut button);
424        self.components.push(NonCompositeComponent::Button(
425            button.to_serializable_button()
426        ));
427        self
428    }
429}
430
431#[derive(Debug, Clone)]
432pub enum NonLinkButtonStyle {
433    Primary,
434    Secondary,
435    Success,
436    Danger,
437}
438
439impl NonLinkButtonStyle {
440    fn get_button_style(&self) -> ButtonStyles {
441        match *self {
442            NonLinkButtonStyle::Primary => ButtonStyles::Primary,
443            NonLinkButtonStyle::Secondary => ButtonStyles::Secondary,
444            NonLinkButtonStyle::Success => ButtonStyles::Success,
445            NonLinkButtonStyle::Danger => ButtonStyles::Danger,
446        }
447    }
448}
449
450// since link button has an explicit way of creation via the action row
451// this enum is kept hidden from the user ans the NonLinkButtonStyle is created to avoid
452// user confusion
453#[derive(Debug)]
454enum ButtonStyles {
455    Primary,
456    Secondary,
457    Success,
458    Danger,
459    Link,
460}
461
462impl ButtonStyles {
463    /// value for serialization purposes
464    fn value(&self) -> i32 {
465        match *self {
466            ButtonStyles::Primary => 1,
467            ButtonStyles::Secondary => 2,
468            ButtonStyles::Success => 3,
469            ButtonStyles::Danger => 4,
470            ButtonStyles::Link => 5,
471        }
472    }
473}
474
475impl Serialize for ButtonStyles {
476    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
477    where
478        S: Serializer,
479    {
480        serializer.serialize_i32(self.value())
481    }
482}
483
484#[derive(Serialize, Debug, Clone)]
485pub struct PartialEmoji {
486    pub id: Snowflake,
487    pub name: String,
488    pub animated: Option<bool>,
489}
490
491/// the button struct intended for serialized
492#[derive(Serialize, Debug)]
493struct Button {
494    #[serde(rename = "type")]
495    pub component_type: i8,
496    pub style: Option<ButtonStyles>,
497    pub label: Option<String>,
498    pub emoji: Option<PartialEmoji>,
499    pub custom_id: Option<String>,
500    pub url: Option<String>,
501    pub disabled: Option<bool>,
502}
503
504impl Button {
505    fn new(
506        style: Option<ButtonStyles>,
507        label: Option<String>,
508        emoji: Option<PartialEmoji>,
509        url: Option<String>,
510        custom_id: Option<String>,
511        disabled: Option<bool>,
512    ) -> Self {
513        Self {
514            component_type: 2,
515            style,
516            label,
517            emoji,
518            url,
519            custom_id,
520            disabled,
521        }
522    }
523}
524
525/// Data holder for shared fields of link and regular buttons
526#[derive(Debug)]
527struct ButtonCommonBase {
528    pub label: Option<String>,
529    pub emoji: Option<PartialEmoji>,
530    pub disabled: Option<bool>,
531}
532
533impl ButtonCommonBase {
534    fn new(label: Option<String>, emoji: Option<PartialEmoji>, disabled: Option<bool>) -> Self {
535        ButtonCommonBase {
536            label,
537            emoji,
538            disabled,
539        }
540    }
541    fn label(&mut self, label: &str) -> &mut Self {
542        self.label = Some(label.to_string());
543        self
544    }
545
546    fn emoji(&mut self, emoji_id: Snowflake, name: &str, animated: bool) -> &mut Self {
547        self.emoji = Some(PartialEmoji {
548            id: emoji_id,
549            name: name.to_string(),
550            animated: Some(animated),
551        });
552        self
553    }
554
555    fn disabled(&mut self, disabled: bool) -> &mut Self {
556        self.disabled = Some(disabled);
557        self
558    }
559}
560
561/// a macro which takes an identifier (`base`) of the ButtonCommonBase (relative to `self`)
562/// and generates setter functions that delegate their inputs to the `self.base`
563macro_rules! button_base_delegation {
564    ($base:ident) => {
565        pub fn emoji(&mut self, emoji_id: &str, name: &str, animated: bool) -> &mut Self {
566            self.$base.emoji(emoji_id.to_string(), name, animated);
567            self
568        }
569
570        pub fn disabled(&mut self, disabled: bool) -> &mut Self {
571            self.$base.disabled(disabled);
572            self
573        }
574
575        pub fn label(&mut self, label: &str) -> &mut Self {
576            self.$base.label(label);
577            self
578        }
579    };
580}
581
582#[derive(Debug)]
583pub struct LinkButton {
584    button_base: ButtonCommonBase,
585    url: Option<String>,
586}
587
588impl LinkButton {
589    fn new() -> Self {
590        LinkButton {
591            button_base: ButtonCommonBase::new(None, None, None),
592            url: None,
593        }
594    }
595
596    pub fn url(&mut self, url: &str) -> &mut Self {
597        self.url = Some(url.to_string());
598        self
599    }
600
601    button_base_delegation!(button_base);
602}
603
604pub struct RegularButton {
605    button_base: ButtonCommonBase,
606    custom_id: Option<String>,
607    style: Option<NonLinkButtonStyle>,
608}
609
610impl RegularButton {
611    fn new() -> Self {
612        RegularButton {
613            button_base: ButtonCommonBase::new(None, None, None),
614            custom_id: None,
615            style: None,
616        }
617    }
618
619    pub fn custom_id(&mut self, custom_id: &str) -> &mut Self {
620        self.custom_id = Some(custom_id.to_string());
621        self
622    }
623
624    pub fn style(&mut self, style: NonLinkButtonStyle) -> &mut Self {
625        self.style = Some(style);
626        self
627    }
628
629    button_base_delegation!(button_base);
630}
631
632trait ToSerializableButton {
633    fn to_serializable_button(&self) -> Button;
634}
635
636impl ToSerializableButton for LinkButton {
637    fn to_serializable_button(&self) -> Button {
638        Button::new(
639            Some(ButtonStyles::Link),
640            self.button_base.label.clone(),
641            self.button_base.emoji.clone(),
642            self.url.clone(),
643            None,
644            self.button_base.disabled,
645        )
646    }
647}
648
649impl ToSerializableButton for RegularButton {
650    fn to_serializable_button(&self) -> Button {
651        Button::new(
652            self.style.clone().map(|s| s.get_button_style()),
653            self.button_base.label.clone(),
654            self.button_base.emoji.clone(),
655            None,
656            self.custom_id.clone(),
657            self.button_base.disabled,
658        )
659    }
660}
661
662/// A trait for checking that an API message component is compatible with the official Discord API constraints
663///
664/// This trait should be implemented for any components for which the Discord API documentation states
665/// limitations (maximum count, maximum length, uniqueness with respect to other components, restrictions
666/// on children components, ...)
667pub(crate) trait DiscordApiCompatible {
668    fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String>;
669}
670
671impl DiscordApiCompatible for NonCompositeComponent {
672    fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> {
673        match self {
674            NonCompositeComponent::Button(b) => b.check_compatibility(context),
675        }
676    }
677}
678
679fn bool_to_result<E>(b: bool, err: E) -> Result<(), E> {
680    if b {
681        Ok(())
682    } else {
683        Err(err)
684    }
685}
686
687impl DiscordApiCompatible for Button {
688    fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> {
689        if self.label.is_some() && self.label.as_ref().unwrap().len() > Message::label_max_len() {
690            return Err(format!(
691                "Label length exceeds {} characters",
692                Message::label_max_len()
693            ));
694        }
695
696        return match self.style {
697            None => Err("Button style must be set!".to_string()),
698            Some(ButtonStyles::Link) => {
699                if self.url.is_none() {
700                    Err("Url of a Link button must be set!".to_string())
701                } else {
702                    Ok(())
703                }
704            }
705            // list all remaining in case a style with different requirements is added
706            Some(ButtonStyles::Danger)
707            | Some(ButtonStyles::Primary)
708            | Some(ButtonStyles::Success)
709            | Some(ButtonStyles::Secondary) => {
710                return if let Some(id) = self.custom_id.as_ref() {
711                    bool_to_result(
712                        id.len() <= Message::custom_id_max_len(),
713                        format!(
714                            "Custom ID length exceeds {} characters",
715                            Message::custom_id_max_len()
716                        ),
717                    )
718                    .and(bool_to_result(
719                        context.register_custom_id(id),
720                        format!(
721                            "Attempt to use the same custom ID ({}) twice! (buttonLabel: {:?})",
722                            id, self.label
723                        ),
724                    ))
725                } else {
726                    Err("Custom ID of a NonLink button must be set!".to_string())
727                };
728            }
729        };
730    }
731}
732
733impl DiscordApiCompatible for ActionRow {
734    fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> {
735        self.components.iter().fold(Ok(()), |acc, component| {
736            acc.and(component.check_compatibility(context))
737        })
738    }
739}
740
741impl DiscordApiCompatible for Message {
742    fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> {
743        if self.action_rows.len() > Self::max_action_row_count() {
744            return Err(format!(
745                "Action row count exceeded {} (maximum)",
746                Message::max_action_row_count()
747            ));
748        }
749
750        self.action_rows
751            .iter()
752            .fold(Ok(()), |acc, row| acc.and(row.check_compatibility(context)))
753    }
754}