aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands/web.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/commands/web.rs122
1 files changed, 122 insertions, 0 deletions
diff --git a/src/commands/web.rs b/src/commands/web.rs
new file mode 100644
index 0000000..d68f663
--- /dev/null
+++ b/src/commands/web.rs
@@ -0,0 +1,122 @@
1use poise::serenity_prelude as serenity;
2use scraper::{Html, Selector};
3
4use crate::Data;
5use crate::error::{Error, Result};
6use crate::lang;
7
8type Context<'a> = poise::Context<'a, Data, Error>;
9
10/// Search the web via DuckDuckGo
11#[poise::command(slash_command)]
12pub async fn web(ctx: Context<'_>, #[description = "Search query"] query: String) -> Result<()> {
13 ctx.defer().await?;
14
15 let url = format!(
16 "https://html.duckduckgo.com/html/?q={}",
17 urlencoding(&query)
18 );
19 let html = ctx
20 .data()
21 .http_client
22 .get(&url)
23 .send()
24 .await?
25 .text()
26 .await?;
27
28 let results = {
29 let document = Html::parse_document(&html);
30
31 let result_selector = Selector::parse(".result").unwrap();
32 let title_selector = Selector::parse(".result__title a").unwrap();
33 let snippet_selector = Selector::parse(".result__snippet").unwrap();
34
35 let mut results = Vec::new();
36 for result in document.select(&result_selector).take(5) {
37 let title = result
38 .select(&title_selector)
39 .next()
40 .map(|a| a.text().collect::<String>())
41 .unwrap_or_default();
42
43 let url = result
44 .select(&title_selector)
45 .next()
46 .and_then(|a| a.attr("href"))
47 .unwrap_or("")
48 .to_string();
49
50 let snippet = result
51 .select(&snippet_selector)
52 .next()
53 .map(|s| s.text().collect::<String>())
54 .unwrap_or_default();
55
56 let clean_url = if let Some(encoded) = url.split("uddg=").nth(1) {
57 let encoded = encoded.split('&').next().unwrap_or(encoded);
58 url_decode(encoded)
59 } else {
60 url.to_string()
61 };
62
63 results.push((title, clean_url, snippet));
64 }
65 results
66 };
67
68 if results.is_empty() {
69 ctx.say(lang::L_WEB_NO_RESULTS).await?;
70 return Ok(());
71 }
72
73 let mut embed = serenity::CreateEmbed::default()
74 .title(lang::fmt_web_title(&query))
75 .color(0x00aaff);
76
77 for (i, (title, url, snippet)) in results.iter().enumerate() {
78 embed = embed.field(
79 format!("{}. {title}", i + 1),
80 format!("{url}\n{snippet}"),
81 false,
82 );
83 }
84
85 ctx.send(poise::CreateReply::default().embed(embed)).await?;
86 Ok(())
87}
88
89fn url_decode(s: &str) -> String {
90 let mut result = String::new();
91 let mut chars = s.chars();
92 while let Some(c) = chars.next() {
93 match c {
94 '+' => result.push(' '),
95 '%' => {
96 let hi = chars.next().and_then(|c| c.to_digit(16));
97 let lo = chars.next().and_then(|c| c.to_digit(16));
98 if let (Some(hi), Some(lo)) = (hi, lo) {
99 result.push(char::from_u32((hi << 4) | lo).unwrap_or('�'));
100 } else {
101 result.push('%');
102 }
103 }
104 _ => result.push(c),
105 }
106 }
107 result
108}
109
110fn urlencoding(s: &str) -> String {
111 s.chars()
112 .map(|c| match c {
113 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '.' | '~' => c.to_string(),
114 ' ' => "+".to_string(),
115 _ => format!("%{:02X}", c as u8),
116 })
117 .collect()
118}
119
120pub fn commands() -> Vec<poise::Command<Data, Error>> {
121 vec![web()]
122}