1use chrono::prelude::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use anyhow::format_err;
4use sha1::{Sha1, Digest};
5
6#[cfg(feature = "reqwest")]
7use reqwest as reqwest;
8
9#[cfg(all(feature = "rquest", not(feature = "reqwest")))]
10use rquest as reqwest;
11
12#[cfg(feature = "queue")]
13pub mod queue {
14 use std::collections::VecDeque;
15 use std::sync::Arc;
16 use std::time::Duration;
17 use tokio::sync::Mutex;
18 use tokio::task::JoinHandle;
19 use tokio::time::Instant;
20 use crate::Webhook;
21
22 pub struct WebhookQueue {
23 pub webhooks: Arc<Mutex<VecDeque<Webhook>>>,
24 }
25
26 impl WebhookQueue {
27 pub fn new() -> Self {
28 Self {
29 webhooks: Arc::new(Mutex::new(VecDeque::new())),
30 }
31 }
32
33 pub async fn enqueue(queue: Arc<Mutex<VecDeque<Webhook>>>, webhook: Webhook) {
34 let mut q = queue.lock().await;
35 q.push_front(webhook);
36 }
37
38 pub async fn enqueue_multi(queue: Arc<Mutex<VecDeque<Webhook>>>, webhooks: Vec<Webhook>) {
39 let mut q = queue.lock().await;
40 for webhook in webhooks {
41 q.push_front(webhook);
42 }
43 }
44
45 pub fn start(self) -> JoinHandle<Self> {
46 tokio::task::spawn(async move {
47 loop {
48 let (one, two) = {
49 let mut whs = self.webhooks.as_ref().lock().await;
50 let one = whs.pop_back();
51 let two = whs.pop_back();
52 (one, two)
53 };
54
55 if cfg!(test) {
56 if one.is_none() && two.is_none() {
57 return self;
58 }
59 }
60
61 let _ = match one {
63 Some(w) => w.send().await,
64 None => Ok(()),
65 };
66
67 let _ = match two {
68 Some(w) => w.send().await,
69 None => Ok(()),
70 };
71
72 tokio::time::sleep_until(Instant::now() + Duration::from_millis(2000)).await;
74 }
75 })
76 }
77 }
78}
79
80
81#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
82pub struct Webhook {
83 #[serde(skip)]
84 webhook_url: String,
85 content: Option<String>,
86 username: Option<String>,
87 avatar_url: Option<String>,
88 embeds: Vec<Embed>,
89 components: Vec<Component>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
93struct Component {}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
96pub struct Embed {
97 title: Option<String>,
98 #[serde(rename = "type")]
99 _type: String,
100 description: Option<String>,
101 url: Option<String>,
102 timestamp: Option<String>,
103 color: Option<usize>,
104 footer: Option<Footer>,
105 image: Option<Image>,
106 thumbnail: Option<Thumbnail>,
107 video: Option<Video>,
108 provider: Option<Provider>,
109 author: Option<Author>,
110 fields: Vec<Field>,
111}
112#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
113struct Footer {
114 text: String,
115 icon_url: Option<String>,
116 proxy_icon_url: Option<String>,
117}
118#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
119struct Image {
120 url: String,
121 proxy_url: Option<String>,
122 height: Option<usize>,
123 width: Option<usize>,
124}
125#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
126struct Thumbnail {
127 url: String,
128 proxy_url: Option<String>,
129 height: Option<usize>,
130 width: Option<usize>,
131}
132#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
133struct Video {
134 url: String,
135 proxy_url: Option<String>,
136 height: Option<usize>,
137 width: Option<usize>,
138}
139#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
140struct Provider {
141 name: Option<String>,
142 url: Option<String>,
143}
144#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
145struct Author {
146 name: String,
147 url: Option<String>,
148 icon_url: Option<String>,
149 proxy_icon_url: Option<String>,
150}
151#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
152pub struct Field {
153 pub name: String,
154 pub value: String,
155 pub inline: bool,
156}
157pub enum ColourType<S: AsRef<str>> {
158 Hex(S),
159 Integer(usize),
160 FromSeed(S)
161}
162
163impl Webhook {
164 pub fn new<S: AsRef<str>>(webhook_url: S) -> Webhook {
165 Webhook {
166 webhook_url: webhook_url.as_ref().to_string(),
167 content: None,
168 username: None,
169 avatar_url: None,
170 embeds: Vec::new(),
171 components: Vec::new(),
172 }
173 }
174 pub fn set_content<S: AsRef<str>>(mut self, content: S) -> Self {
175 self.content = Some(content.as_ref().to_string());
176 self
177 }
178 pub fn set_username<S: AsRef<str>>(mut self, username: S) -> Self {
179 self.username = Some(username.as_ref().to_string());
180 self
181 }
182 pub fn set_avatar_url<S: AsRef<str>>(mut self, url: S) -> Self {
183 self.avatar_url = Some(url.as_ref().to_string());
184 self
185 }
186 pub fn add_embed(mut self, embed: Embed) -> Self {
187 self.embeds.push(embed);
188 self
189 }
190
191 pub fn add_embeds(mut self, embeds: &mut Vec<Embed>) -> Self {
192 self.embeds.append(embeds);
193 self
194 }
195
196 #[cfg(not(feature = "retry"))]
197 pub async fn send(&self) -> anyhow::Result<()> {
198 let client = reqwest::Client::new();
199
200 let body = serde_json::to_string(self).unwrap();
201
202 let resp = client
203 .post(format!("{}?wait=true", &self.webhook_url))
204 .header("Content-Type", "application/json")
205 .body(body)
206 .send()
207 .await?;
208
209 match resp.status() {
210 reqwest::StatusCode::NO_CONTENT | reqwest::StatusCode::OK => {
211 Ok(())
212 },
213 _ => {
214 let body = resp.text().await.unwrap_or(String::from(""));
215 Err(format_err!("Failed to send request, {}", body))
216 }
217 }
218 }
219
220
221 #[cfg(feature = "retry")]
222 #[async_recursion::async_recursion]
223 pub async fn send(&self) -> anyhow::Result<()> {
224 let client = reqwest::Client::new();
225
226 let body = serde_json::to_string(self).unwrap();
227
228 let resp = client
229 .post(format!("{}?wait=true", &self.webhook_url))
230 .header("Content-Type", "application/json")
231 .body(body)
232 .send()
233 .await?;
234
235 match resp.status() {
236 reqwest::StatusCode::NO_CONTENT | reqwest::StatusCode::OK => {
237 Ok(())
238 },
239 reqwest::StatusCode::TOO_MANY_REQUESTS => {
240 use std::time::Duration;
241 use tokio::time::{Instant, sleep_until};
242 let retry_after = match resp.headers().get("Retry-After") {
243 Some(header) => {
244 let str = header.to_str().unwrap_or("5");
245 str.parse::<u64>().unwrap_or(5)
246 },
247 None => return Err(format_err!("Missing \"Retry After\" header"))
248 };
249 log::warn!("Webhook rate limited, retrying in {} seconds", retry_after);
250 sleep_until(Instant::now() + Duration::from_secs(retry_after)).await;
251 self.send().await
252 },
253 _ => {
254 let body = resp.text().await.unwrap_or(String::from(""));
255 Err(format_err!("Failed to send request, {}", body))
256 }
257 }
258 }
259}
260
261impl Embed {
262 pub fn new() -> Embed {
263 Embed {
264 title: None,
265 _type: "rich".to_string(),
266 description: None,
267 url: None,
268 timestamp: None,
269 color: None,
270 footer: None,
271 image: None,
272 thumbnail: None,
273 video: None,
274 provider: None,
275 author: None,
276 fields: Vec::new(),
277 }
278 }
279 pub fn set_title<S: AsRef<str>>(mut self, title: S) -> Self {
280 self.title = Some(title.as_ref().to_string());
281 self
282 }
283 pub fn set_description<S: AsRef<str>>(mut self, description: S) -> Self {
284 self.description = Some(description.as_ref().to_string());
285 self
286 }
287 pub fn set_url<S: AsRef<str>>(mut self, url: S) -> Self {
288 self.url = Some(url.as_ref().to_string());
289 self
290 }
291 pub fn set_timestamp(mut self, timestamp: Option<&std::time::SystemTime>) -> Self {
292 let timestamp: DateTime<Utc> = match timestamp {
293 Some(ts) => (*ts).into(),
294 None => Utc::now(),
295 };
296 let timestamp = timestamp.format("%+").to_string();
297 self.timestamp = Some(timestamp);
298 self
299 }
300 pub fn set_colour<S: AsRef<str>>(mut self, colour: ColourType<S>) -> Self {
301 let colour: usize = match colour {
302 ColourType::Hex(hex) => usize::from_str_radix(
303 hex.as_ref()
304 .trim_start_matches('#')
305 .trim_start_matches("0x"),
306 16,
307 )
308 .unwrap_or(10066329),
309 ColourType::Integer(int) => int,
310 ColourType::FromSeed(seed) => {
311 let mut hasher = Sha1::new();
312 hasher.update(seed.as_ref().as_bytes());
313 let result = hasher.finalize();
314 let encoded = format!("#{}", &hex::encode(result)[0..6]);
315 return self.set_colour(ColourType::Hex(encoded))
316 }
317 };
318
319 self.color = Some(colour);
320 self
321 }
322 pub fn set_color<S: AsRef<str>>(self, color: ColourType<S>) -> Self {
323 self.set_colour(color)
324 }
325 pub fn set_footer<A: AsRef<str>, B: AsRef<str>, C: AsRef<str>>(
326 mut self,
327 text: A,
328 icon_url: Option<B>,
329 proxy_icon_url: Option<C>,
330 ) -> Self {
331 let icon_url = icon_url.map(|n| n.as_ref().to_string());
332 let proxy_icon_url = proxy_icon_url.map(|n| n.as_ref().to_string());
333
334 self.footer = Some(Footer {
335 text: text.as_ref().to_string(),
336 icon_url,
337 proxy_icon_url,
338 });
339
340 self
341 }
342 pub fn set_image<A: AsRef<str>, B: AsRef<str>>(
343 mut self,
344 url: A,
345 proxy_url: Option<B>,
346 height: Option<usize>,
347 width: Option<usize>,
348 ) -> Self {
349 let proxy_url = proxy_url.map(|p| p.as_ref().to_string());
350
351 self.image = Some(Image {
352 url: url.as_ref().to_string(),
353 proxy_url,
354 height,
355 width,
356 });
357
358 self
359 }
360 pub fn set_thumbnail<A: AsRef<str>, B: AsRef<str>>(
361 mut self,
362 url: A,
363 proxy_url: Option<B>,
364 height: Option<usize>,
365 width: Option<usize>,
366 ) -> Self {
367 let proxy_url = proxy_url.map(|p| p.as_ref().to_string());
368
369 self.thumbnail = Some(Thumbnail {
370 url: url.as_ref().to_string(),
371 proxy_url,
372 height,
373 width,
374 });
375
376 self
377 }
378 pub fn set_video<A: AsRef<str>, B: AsRef<str>>(
379 mut self,
380 url: A,
381 proxy_url: Option<B>,
382 height: Option<usize>,
383 width: Option<usize>,
384 ) -> Self {
385 let proxy_url = proxy_url.map(|p| p.as_ref().to_string());
386
387 self.video = Some(Video {
388 url: url.as_ref().to_string(),
389 proxy_url,
390 height,
391 width,
392 });
393
394 self
395 }
396 pub fn set_provider<A: AsRef<str>, B: AsRef<str>>(mut self, name: Option<A>, url: Option<B>) -> Self {
397 let name = name.map(|n| n.as_ref().to_string());
398 let url = url.map(|n| n.as_ref().to_string());
399 self.provider = Some(Provider { name, url });
400 self
401 }
402 pub fn set_author<A: AsRef<str>, B: AsRef<str>, C: AsRef<str>, D: AsRef<str>>(
403 mut self,
404 name: A,
405 url: Option<B>,
406 icon_url: Option<C>,
407 proxy_icon_url: Option<D>,
408 ) -> Self {
409 let url = url.map(|n| n.as_ref().to_string());
410 let icon_url = icon_url.map(|n| n.as_ref().to_string());
411 let proxy_icon_url = proxy_icon_url.map(|n| n.as_ref().to_string());
412 self.author = Some(Author {
413 name: name.as_ref().to_string(),
414 url,
415 icon_url,
416 proxy_icon_url,
417 });
418
419 self
420 }
421 pub fn add_field<A: AsRef<str>, B: AsRef<str>>(mut self, name: A, value: B, inline: bool) -> Self {
422 let field = Field {
423 name: name.as_ref().to_string(),
424 value: value.as_ref().to_string(),
425 inline,
426 };
427
428 self.fields.push(field);
429
430 self
431 }
432
433 pub fn add_fields(mut self, fields: &mut Vec<Field>) -> Self {
434 self.fields.append(fields);
435 self
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use std::env;
442 use crate::{Author, ColourType, Embed, Field, Footer, Thumbnail, Webhook};
443
444 #[test]
445 fn create_embed() {
446 let embed = Embed::new()
447 .set_colour(ColourType::Hex("#FFFFFF"))
448 .set_author("Author Name", Some("https://example.com/"), None::<String>, None::<String>)
449 .set_thumbnail("https://example.com/", None::<String>, None, None)
450 .set_title("Example")
451 .set_url("https://example.com/")
452 .set_footer("Footer Text", None::<String>, None::<String>)
453 .set_description("Description Text")
454 .add_field("Example 1", "Value 1", true)
455 .add_fields(&mut vec![
456 Field {
457 name: "Example 2".to_string(),
458 value: "Value 2".to_string(),
459 inline: false,
460 },
461 Field {
462 name: "Example 3".to_string(),
463 value: "Value 3".to_string(),
464 inline: false,
465 }
466 ]);
467
468 let expected = Embed {
469 title: Some("Example".to_string()),
470 _type: "rich".to_string(),
471 description: Some("Description Text".to_string()),
472 url: Some("https://example.com/".to_string()),
473 timestamp: None,
474 color: Some(16777215),
475 footer: Some(Footer {
476 text: "Footer Text".to_string(),
477 icon_url: None,
478 proxy_icon_url: None,
479 }),
480 image: None,
481 thumbnail: Some(Thumbnail {
482 url: "https://example.com/".to_string(),
483 proxy_url: None,
484 height: None,
485 width: None,
486 }),
487 video: None,
488 provider: None,
489 author: Some(Author {
490 name: "Author Name".to_string(),
491 url: Some("https://example.com/".to_string()),
492 icon_url: None,
493 proxy_icon_url: None,
494 }),
495 fields: vec![
496 Field {
497 name: "Example 1".to_string(),
498 value: "Value 1".to_string(),
499 inline: true,
500 },
501 Field {
502 name: "Example 2".to_string(),
503 value: "Value 2".to_string(),
504 inline: false,
505 },
506 Field {
507 name: "Example 3".to_string(),
508 value: "Value 3".to_string(),
509 inline: false,
510 }
511 ],
512 };
513 assert_eq!(embed, expected);
514 }
515
516 #[test]
517 fn embed_adds_to_webhook() {
518 let webhook = Webhook::new("https://discord.com/webhook")
519 .set_content("Content Text")
520 .set_username("Test Username");
521
522 let embed = Embed::new()
523 .set_colour(ColourType::Hex("#FFFFFF"))
524 .set_author("Author Name", Some("https://example.com/"), None::<String>, None::<String>)
525 .set_thumbnail("https://example.com/", None::<String>, None, None)
526 .set_title("Example")
527 .set_url("https://example.com/")
528 .set_footer("Footer Text", None::<String>, None::<String>)
529 .set_description("Description Text")
530 .add_field("Example 1", "Value 1", true)
531 .add_fields(&mut vec![
532 Field {
533 name: "Example 2".to_string(),
534 value: "Value 2".to_string(),
535 inline: false,
536 },
537 Field {
538 name: "Example 3".to_string(),
539 value: "Value 3".to_string(),
540 inline: false,
541 }
542 ]);
543
544 let webhook = webhook.add_embed(embed);
545
546 let expected = Webhook {
547 webhook_url: "https://discord.com/webhook".to_string(),
548 content: Some("Content Text".to_string()),
549 username: Some("Test Username".to_string()),
550 avatar_url: None,
551 embeds: vec![
552 Embed {
553 title: Some("Example".to_string()),
554 _type: "rich".to_string(),
555 description: Some("Description Text".to_string()),
556 url: Some("https://example.com/".to_string()),
557 timestamp: None,
558 color: Some(16777215),
559 footer: Some(Footer {
560 text: "Footer Text".to_string(),
561 icon_url: None,
562 proxy_icon_url: None,
563 }),
564 image: None,
565 thumbnail: Some(Thumbnail {
566 url: "https://example.com/".to_string(),
567 proxy_url: None,
568 height: None,
569 width: None,
570 }),
571 video: None,
572 provider: None,
573 author: Some(Author {
574 name: "Author Name".to_string(),
575 url: Some("https://example.com/".to_string()),
576 icon_url: None,
577 proxy_icon_url: None,
578 }),
579 fields: vec![
580 Field {
581 name: "Example 1".to_string(),
582 value: "Value 1".to_string(),
583 inline: true,
584 },
585 Field {
586 name: "Example 2".to_string(),
587 value: "Value 2".to_string(),
588 inline: false,
589 },
590 Field {
591 name: "Example 3".to_string(),
592 value: "Value 3".to_string(),
593 inline: false,
594 }
595 ],
596 }
597 ],
598 components: vec![],
599 };
600
601 assert_eq!(webhook, expected);
602 }
603
604 #[tokio::test]
605 async fn submit_webhook() {
606
607 let webhook_url = env::var("WEBHOOK").unwrap();
608 let webhook = Webhook::new(&webhook_url);
609
610 let embed = Embed::new()
611 .set_title("Blurple Test")
612 .set_description("Testing Blurple")
613 .set_url("https://example.com/")
614 .set_timestamp(None)
615 .add_field("Example 1", "Value 1", true)
616 .add_fields(&mut vec![
617 Field {
618 name: "Example 2".to_string(),
619 value: "Value 2".to_string(),
620 inline: true,
621 },
622 Field {
623 name: "Example 3".to_string(),
624 value: "Value 3".to_string(),
625 inline: false,
626 },
627 Field {
628 name: "Example 4".to_string(),
629 value: "Example 4".to_string(),
630 inline: false,
631 }
632 ]);
633
634 let webhook = webhook.add_embed(embed);
635 let result = webhook.send().await;
636
637 assert!(result.is_ok());
638 }
639 #[cfg(feature = "queue")]
640 #[tokio::test]
641 async fn test_queue() {
642 use crate::queue::WebhookQueue;
643 use std::sync::Arc;
644
645 let queue = WebhookQueue::new();
646
647 let webhook_url = env::var("WEBHOOK").unwrap();
648
649 let webhooks = Arc::clone(&queue.webhooks);
650 for i in 0..5 {
651 let webhooks = Arc::clone(&webhooks);
652 let embed = Embed::new().set_title("Example");
653 let webhook = Webhook::new(&webhook_url).add_embed(embed);
654
655 WebhookQueue::enqueue(webhooks, webhook).await;
656 }
657
658 {
659 let webhooks = Arc::clone(&webhooks);
660 let webhooks = webhooks.lock().await;
661 assert_eq!(webhooks.len(), 5, "Len is not 5, {}", webhooks.len());
662 }
663
664 let _ = queue.start().await;
665
666 let webhooks = Arc::clone(&webhooks);
667 let webhooks = webhooks.lock().await;
668 assert!(webhooks.is_empty(), "Webhooks not empty, {}", webhooks.len())
669 }
670}