From cd8ed8bc5793985df2c1692072a932ceee517b95 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 8 Jul 2026 02:13:35 +0200 Subject: OPEN THE RUST GATES Signed-off-by: salmonik --- src/commands/web.rs | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/commands/web.rs (limited to 'src/commands/web.rs') 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 @@ +use poise::serenity_prelude as serenity; +use scraper::{Html, Selector}; + +use crate::Data; +use crate::error::{Error, Result}; +use crate::lang; + +type Context<'a> = poise::Context<'a, Data, Error>; + +/// Search the web via DuckDuckGo +#[poise::command(slash_command)] +pub async fn web(ctx: Context<'_>, #[description = "Search query"] query: String) -> Result<()> { + ctx.defer().await?; + + let url = format!( + "https://html.duckduckgo.com/html/?q={}", + urlencoding(&query) + ); + let html = ctx + .data() + .http_client + .get(&url) + .send() + .await? + .text() + .await?; + + let results = { + let document = Html::parse_document(&html); + + let result_selector = Selector::parse(".result").unwrap(); + let title_selector = Selector::parse(".result__title a").unwrap(); + let snippet_selector = Selector::parse(".result__snippet").unwrap(); + + let mut results = Vec::new(); + for result in document.select(&result_selector).take(5) { + let title = result + .select(&title_selector) + .next() + .map(|a| a.text().collect::()) + .unwrap_or_default(); + + let url = result + .select(&title_selector) + .next() + .and_then(|a| a.attr("href")) + .unwrap_or("") + .to_string(); + + let snippet = result + .select(&snippet_selector) + .next() + .map(|s| s.text().collect::()) + .unwrap_or_default(); + + let clean_url = if let Some(encoded) = url.split("uddg=").nth(1) { + let encoded = encoded.split('&').next().unwrap_or(encoded); + url_decode(encoded) + } else { + url.to_string() + }; + + results.push((title, clean_url, snippet)); + } + results + }; + + if results.is_empty() { + ctx.say(lang::L_WEB_NO_RESULTS).await?; + return Ok(()); + } + + let mut embed = serenity::CreateEmbed::default() + .title(lang::fmt_web_title(&query)) + .color(0x00aaff); + + for (i, (title, url, snippet)) in results.iter().enumerate() { + embed = embed.field( + format!("{}. {title}", i + 1), + format!("{url}\n{snippet}"), + false, + ); + } + + ctx.send(poise::CreateReply::default().embed(embed)).await?; + Ok(()) +} + +fn url_decode(s: &str) -> String { + let mut result = String::new(); + let mut chars = s.chars(); + while let Some(c) = chars.next() { + match c { + '+' => result.push(' '), + '%' => { + let hi = chars.next().and_then(|c| c.to_digit(16)); + let lo = chars.next().and_then(|c| c.to_digit(16)); + if let (Some(hi), Some(lo)) = (hi, lo) { + result.push(char::from_u32((hi << 4) | lo).unwrap_or('�')); + } else { + result.push('%'); + } + } + _ => result.push(c), + } + } + result +} + +fn urlencoding(s: &str) -> String { + s.chars() + .map(|c| match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '.' | '~' => c.to_string(), + ' ' => "+".to_string(), + _ => format!("%{:02X}", c as u8), + }) + .collect() +} + +pub fn commands() -> Vec> { + vec![web()] +} -- cgit v1.2.3