diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
| commit | cd8ed8bc5793985df2c1692072a932ceee517b95 (patch) | |
| tree | 42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /src/commands/web.rs | |
| parent | Merge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff) | |
| download | senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip | |
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'src/commands/web.rs')
| -rw-r--r-- | src/commands/web.rs | 122 |
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 @@ | |||
| 1 | use poise::serenity_prelude as serenity; | ||
| 2 | use scraper::{Html, Selector}; | ||
| 3 | |||
| 4 | use crate::Data; | ||
| 5 | use crate::error::{Error, Result}; | ||
| 6 | use crate::lang; | ||
| 7 | |||
| 8 | type Context<'a> = poise::Context<'a, Data, Error>; | ||
| 9 | |||
| 10 | /// Search the web via DuckDuckGo | ||
| 11 | #[poise::command(slash_command)] | ||
| 12 | pub 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 | |||
| 89 | fn 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 | |||
| 110 | fn 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 | |||
| 120 | pub fn commands() -> Vec<poise::Command<Data, Error>> { | ||
| 121 | vec![web()] | ||
| 122 | } | ||
