yadwh/
embed.rs

1//! Embed Object that is optionally sent in messages.
2//!
3//! `embed` contains the Embed struct used to be sent with messages to the Discord API. Up to 10
4//! embeds can be sent per message.
5
6use crate::client::{Limit, WebhookError};
7use serde::{Deserialize, Serialize};
8
9/// Author information for the embed.
10///
11/// ## References / Documentation
12///
13/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure>
14#[derive(Serialize, Deserialize, Debug, Clone)]
15pub struct EmbedAuthor {
16    /// Name of the author.
17    pub name: String,
18    /// URL of the author (only supports http(s)).
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub url: Option<String>,
21    /// URL to the icon for the author (only supports http(s) and attachments).
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub icon_url: Option<String>,
24    /// Proxy URL to the icon for the author.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub proxy_icon_url: Option<String>,
27}
28
29/// Fields information for the embed.
30///
31/// ## References / Documentation
32///
33/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure>
34#[derive(Serialize, Deserialize, Debug, Clone)]
35pub struct EmbedField {
36    /// Name of the field.
37    pub name: String,
38    /// Value of the field.
39    pub value: String,
40    /// Whether or not this field should display inline.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub inline: Option<bool>,
43}
44
45/// Footer information for the embed.
46///
47/// ## References / Documentation
48///
49/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure>
50#[derive(Serialize, Deserialize, Debug, Clone)]
51pub struct EmbedFooter {
52    /// Footer text.
53    pub text: String,
54    /// URL of the footer icon (only supports http(s) and attachments)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub icon_url: Option<String>,
57    /// A proxied URL of the footer icon.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub proxy_icon_url: Option<String>,
60}
61
62/// Image, Video, or Thumbnail information for the embed.
63///
64/// ## References / Documentation
65///
66/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure>
67/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure>
68/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure>
69#[derive(Serialize, Deserialize, Debug, Clone)]
70pub struct EmbedMedia {
71    /// Source URL of thumbnail (only supports http(s) and attachments)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub url: Option<String>,
74    /// A proxied URL of the media.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub proxy_url: Option<String>,
77    /// Height of the media.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub height: Option<u32>,
80    /// Width of the media.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub width: Option<u32>,
83}
84
85/// Provider information for the embed.
86///
87/// ## References / Documentation
88///
89/// <https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure>
90#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct EmbedProvider {
92    /// Name of the provider.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub name: Option<String>,
95    /// URL of the provider.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub url: Option<String>,
98}
99
100/// Embed is an optional object that can be sent with a message to Discord. Up to 10 embeds can
101/// exist for any single message.
102///
103/// ## References / Documentation
104///
105/// <https://discord.com/developers/docs/resources/channel#embed-object>
106#[derive(Serialize, Deserialize, Debug, Default, Clone)]
107pub struct Embed {
108    /// Author information.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub author: Option<EmbedAuthor>,
111    /// Title of the embed.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub title: Option<String>,
114    /// Description of the embed.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub description: Option<String>,
117    /// URL of the embed.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub url: Option<String>,
120    /// Timestamp of the embed content.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub timestamp: Option<String>,
123    /// color code of the embed.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub color: Option<u32>,
126    /// Fields information.
127    pub fields: Vec<EmbedField>,
128    /// Footer information.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub footer: Option<EmbedFooter>,
131    /// Image information.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub image: Option<EmbedMedia>,
134    /// Thumbnail information.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub thumbnail: Option<EmbedMedia>,
137    /// Video information.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub video: Option<EmbedMedia>,
140    /// Provider information.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub provider: Option<EmbedProvider>,
143}
144
145impl Embed {
146    /// Creates a new instance of an Embed.
147    pub fn new() -> Self {
148        Self {
149            fields: vec![],
150            ..Default::default()
151        }
152    }
153
154    /// Validates the Embed does not exceed the maxmium lengths. Returns to the total amount of
155    /// characters within the embed.
156    pub fn validate(&self) -> Result<usize, WebhookError> {
157        let too_big = |name: &str, size: usize, max: usize| -> WebhookError {
158            WebhookError::TooBig(name.to_string(), size, max)
159        };
160
161        let mut total: usize = 0;
162
163        // Check if the author is too large.
164        let author = match &self.author {
165            Some(value) => value.name.len(),
166            None => 0,
167        };
168        total += match author {
169            0..=Limit::AUTHOR_NAME => author,
170            _ => return Err(too_big("author", author, Limit::AUTHOR_NAME)),
171        };
172
173        // Check if the title is too large.
174        let title = match &self.title {
175            Some(value) => value.len(),
176            None => 0,
177        };
178        total += match title {
179            0..=Limit::TITLE => title,
180            _ => return Err(too_big("title", title, Limit::TITLE)),
181        };
182
183        // Check if the description is too large.
184        let desc = match &self.description {
185            Some(value) => value.len(),
186            None => 0,
187        };
188        total += match desc {
189            0..=Limit::DESCRIPTION => desc,
190            _ => return Err(too_big("description", desc, Limit::DESCRIPTION)),
191        };
192
193        // Check if the footer is too large.
194        let footer = match &self.footer {
195            Some(value) => value.text.len(),
196            None => 0,
197        };
198        total += match footer {
199            0..=Limit::FOOTER_TEXT => footer,
200            _ => return Err(too_big("footer", footer, Limit::FOOTER_TEXT)),
201        };
202
203        // Check all of the fields.
204        for field in self.fields.iter() {
205            // Check if the name is too large.
206            let name = field.name.len();
207            total += match name {
208                0..=Limit::FIELD_NAME => name,
209                _ => return Err(too_big("field name", name, Limit::FIELD_NAME)),
210            };
211
212            // Check if the value is too large.
213            let value = field.value.len();
214            total += match value {
215                0..=Limit::FIELD_VALUE => value,
216                _ => return Err(too_big("field value", value, Limit::FIELD_VALUE)),
217            };
218        }
219
220        // Verify the total is less than embed max.
221        match total {
222            0..=Limit::EMBED_TOTAL => Ok(total),
223            _ => Err(too_big("embed", total, Limit::EMBED_TOTAL)),
224        }
225    }
226
227    /// Sets the title for the Embed.
228    ///
229    /// # Arguments
230    ///
231    /// * `title` - Title of the embed.
232    pub fn title(&mut self, title: &str) -> &mut Self {
233        self.title = Some(title.to_string());
234        self
235    }
236
237    /// Sets the description for the Embed.
238    ///
239    /// # Arguments
240    ///
241    /// * `description` - Description of the embed.
242    pub fn description(&mut self, description: &str) -> &mut Self {
243        self.description = Some(description.to_string());
244        self
245    }
246
247    /// Sets the url for the Embed.
248    ///
249    /// # Arguments
250    ///
251    /// * `url` - URL to assign to the embed.
252    pub fn url(&mut self, url: &str) -> &mut Self {
253        self.url = Some(url.to_string());
254        self
255    }
256
257    /// Sets the timestamp for the Embed.
258    ///
259    /// # Arguments
260    ///
261    /// * `timestamp` - Timestamp to assign to the embed.
262    pub fn timestamp(&mut self, timestamp: &str) -> &mut Self {
263        self.timestamp = Some(timestamp.to_string());
264        self
265    }
266
267    /// Sets the color (in hex, such as AA11BB or #AA11BB) for the Embed.
268    ///
269    /// # Arguments
270    ///
271    /// * `color` - Color to assign to the embed.
272    pub fn color(&mut self, color: &str) -> &mut Self {
273        // Remove the '#' prefix if it exists.
274        let color_hex = match color.is_empty() {
275            true => return self,
276            false => match color.strip_prefix('#') {
277                Some(value) => value,
278                None => color,
279            },
280        };
281
282        // Convert the HEX color to u32.
283        let color_u32: u32 = match u32::from_str_radix(color_hex, 16) {
284            Ok(value) => value,
285            Err(_) => return self,
286        };
287        self.color = Some(color_u32);
288
289        self
290    }
291
292    /// Sets the footer for the Embed.
293    ///
294    /// # Arguments
295    ///
296    /// * `text` - Text for the footer.
297    /// * `icon_url` - URL for the icon.
298    /// * `proxy_icon_url` - Proxy URL for the icon to assign to the embed.
299    pub fn footer(
300        &mut self,
301        text: &str,
302        icon_url: Option<String>,
303        proxy_icon_url: Option<String>,
304    ) -> &mut Self {
305        self.footer = Some(EmbedFooter {
306            text: text.to_string(),
307            icon_url,
308            proxy_icon_url,
309        });
310
311        self
312    }
313
314    /// Sets the image information for the Embed.
315    ///
316    /// # Arguments
317    ///
318    /// * `url` - URL for the image.
319    /// * `proxy_url` - Proxy URL for the image.
320    /// * `height` - Height of the image.
321    /// * `width` - Width of the image.
322    pub fn image(
323        &mut self,
324        url: Option<String>,
325        proxy_url: Option<String>,
326        height: Option<u32>,
327        width: Option<u32>,
328    ) -> &mut Self {
329        self.image = Some(EmbedMedia {
330            url,
331            proxy_url,
332            height,
333            width,
334        });
335
336        self
337    }
338
339    /// Sets the thumbnail information for the Embed.
340    ///
341    /// # Arguments
342    ///
343    /// * `url` - URL for the thumbnail.
344    /// * `proxy_url` - Proxy URL for the thumbnail.
345    /// * `height` - Height of the thumbnail.
346    /// * `width` - Width of the thumbnail.
347    pub fn thumbnail(
348        &mut self,
349        url: Option<String>,
350        proxy_url: Option<String>,
351        height: Option<u32>,
352        width: Option<u32>,
353    ) -> &mut Self {
354        self.thumbnail = Some(EmbedMedia {
355            url,
356            proxy_url,
357            height,
358            width,
359        });
360
361        self
362    }
363
364    /// Sets the video information for the Embed.
365    ///
366    /// # Arguments
367    ///
368    /// * `url` - URL for the video.
369    /// * `proxy_url` - Proxy URL for the video.
370    /// * `height` - Height of the video.
371    /// * `width` - Width of the video.
372    pub fn video(
373        &mut self,
374        url: Option<String>,
375        proxy_url: Option<String>,
376        height: Option<u32>,
377        width: Option<u32>,
378    ) -> &mut Self {
379        self.video = Some(EmbedMedia {
380            url,
381            proxy_url,
382            height,
383            width,
384        });
385
386        self
387    }
388
389    /// Sets the provider information for the embed.
390    ///
391    /// # Arguments
392    ///
393    /// * `name` - Name of the provider.
394    /// * `url` - URL for the provider.
395    pub fn provider(&mut self, name: Option<String>, url: Option<String>) -> &mut Self {
396        self.provider = Some(EmbedProvider { name, url });
397
398        self
399    }
400
401    /// Sets the author information for the embed.
402    ///
403    /// # Arguments
404    ///
405    /// * `name` - Name of the author.
406    /// * `url` - URL of the author.
407    /// * `icon_url` - URL for the icon of the author.
408    /// * `proxy_icon_url` - Proxy URL for the icon of the author.
409    pub fn author(
410        &mut self,
411        name: &str,
412        url: Option<String>,
413        icon_url: Option<String>,
414        proxy_icon_url: Option<String>,
415    ) -> &mut Self {
416        self.author = Some(EmbedAuthor {
417            name: name.to_string(),
418            url,
419            icon_url,
420            proxy_icon_url,
421        });
422
423        self
424    }
425
426    /// Creates a field for the embed.
427    ///
428    /// # Arguments
429    ///
430    /// * `name` - Name of the field.
431    /// * `value` - Value of the field.
432    /// * `inline` - Whether or not the field is an inline field.
433    pub fn field(&mut self, name: &str, value: &str, inline: Option<bool>) -> &mut Self {
434        let field = EmbedField {
435            name: name.to_string(),
436            value: value.to_string(),
437            inline,
438        };
439
440        self.fields.push(field);
441
442        self
443    }
444}