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}