From 4cfcd3a30527461f2406961cc7b2eb9b310a0afa Mon Sep 17 00:00:00 2001 From: stifskere Date: Mon, 30 Dec 2024 03:24:20 +0100 Subject: [PATCH] feat: crate docs --- Cargo.toml | 6 +- src/bot/commands.rs | 2 + src/bot/commands/crate_docs.rs | 125 +++++++++++++++++++++++++++++++++ src/bot/commands/krate.rs | 2 +- 4 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/bot/commands/crate_docs.rs diff --git a/Cargo.toml b/Cargo.toml index 7d23167..4e46e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,11 +22,7 @@ parking_lot = { version = "0.12", features = ["send_guard"] } poise = "0.6.1" reqwest = { version = "0.11.27", features = ["json"] } # Version used by songbird::input::YoutubeDl regex = "1.10.2" -scraper = { version = "0.19.0", features = [ - "indexmap", - "deterministic", - "atomic", -] } +scraper = { version = "0.19.0", features = ["indexmap", "deterministic", "atomic"] } serde = "1.0.188" serde_json = "1.0.105" songbird = { version = "0.4.1", features = ["serenity"] } diff --git a/src/bot/commands.rs b/src/bot/commands.rs index a1eea7d..ba7f960 100644 --- a/src/bot/commands.rs +++ b/src/bot/commands.rs @@ -4,11 +4,13 @@ mod invite; mod krate; mod ping; mod suggest; +mod crate_docs; use super::{Data, Error}; pub fn commands() -> Vec> { vec![ + crate_docs::crate_docs(), explain::explain(), help::help(), invite::invite(), diff --git a/src/bot/commands/crate_docs.rs b/src/bot/commands/crate_docs.rs new file mode 100644 index 0000000..3645af4 --- /dev/null +++ b/src/bot/commands/crate_docs.rs @@ -0,0 +1,125 @@ +use poise::serenity_prelude::CreateEmbed; +use poise::CreateReply; +use regex::Regex; +use reqwest::get; +use scraper::{Html, Selector}; +use tracing::info; +use crate::bot; +use crate::bot::commands::krate::autocomplete; + +fn strip_html(input: &str) -> String { + Regex::new(r"]+>") + .unwrap() + .replace_all(input, "") + .replace("<", "<") + .replace(">", ">") + .to_string() +} + +#[poise::command(slash_command, prefix_command)] +pub async fn crate_docs( + ctx: bot::Context<'_>, + #[description = "El nombre del crate, crate@version | crate"] + #[rename = "crate"] + #[autocomplete = "autocomplete"] + package: String, + #[description = "Buscar un nombre o expresion minima, como Struct::method."] + search: String +) -> Result<(), bot::Error> { + + let (name, version) = package + .split_once("@") + .unwrap_or((&package, "latest")); + + let response = get(format!("https://docs.rs/{name}/{version}/{name}/all.html")) + .await?; + + if response.status() != 200 { + ctx + .say(format!("The crate `{package}` was not found.")) + .await + .ok(); + + return Ok(()); + } + + let html = response + .text() + .await?; + + let selector = Selector::parse("ul.all-items > li > a").unwrap(); + let elements = Html::parse_document(&html); + let elements = elements.select(&selector); + + let Some((_, url)) = elements + .into_iter() + .map(|elem| (elem.inner_html(), elem.attr("href").unwrap())) + .find(|(name, _)| name.to_lowercase().contains(&search.to_lowercase())) + else { + ctx + .say(format!("The expression `{search}` was not found on `{package}`.")) + .await + .ok(); + + return Ok(()); + }; + + let url = format!("https://docs.rs/{name}/{version}/{name}/{url}"); + let html = get(url.clone()) + .await? + .text() + .await?; + + let html = Html::parse_document(&html); + + let element_type_selector = Selector::parse(".main-heading > h1").unwrap(); + let element_name_selector = Selector::parse(".main-heading > h1 > span").unwrap(); + let element_code_selector = Selector::parse(".item-decl > code").unwrap(); + let element_description_selector = Selector::parse(".docblock > p:last-child").unwrap(); + + let element_type = html.select(&element_type_selector) + .into_iter() + .next() + .unwrap() + .inner_html(); + let element_type = element_type + .split_once(" ") + .unwrap() + .0; + let element_name = html.select(&element_name_selector) + .into_iter() + .next() + .unwrap() + .inner_html(); + let element_name = element_name + .split(" ") + .last() + .unwrap(); + let element_code = strip_html(&html.select(&element_code_selector) + .into_iter() + .next() + .unwrap() + .inner_html()); + let element_description = html.select(&element_description_selector) + .into_iter() + .next() + .unwrap() + .inner_html() + .replace("", "`") + .replace("", "`"); + + ctx + .send( + CreateReply::default() + .embed( + CreateEmbed::new() + .title(format!("{element_type} {element_name}")) + .description(format!( + "```rs\n{element_code}\n```\n{element_description}\n\nSee more: {url}" + )) + ) + ) + .await?; + + Ok(()) +} diff --git a/src/bot/commands/krate.rs b/src/bot/commands/krate.rs index a60702a..dbd300e 100644 --- a/src/bot/commands/krate.rs +++ b/src/bot/commands/krate.rs @@ -23,7 +23,7 @@ pub async fn cargo( Ok(()) } -async fn autocomplete( +pub async fn autocomplete( _: bot::Context<'_>, partial: &str, ) -> impl Iterator {