From 827b5a4bd78b915ce6f700194252f04397c144ca Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 23 Mar 2024 15:56:54 -0400 Subject: [PATCH] analyze_logs: introduce LogProvider trait --- Cargo.lock | 14 +++- Cargo.toml | 2 +- src/api/mod.rs | 1 + src/api/paste_gg.rs | 55 +++++++++++++ .../event/analyze_logs/providers/0x0.rs | 32 ++++---- .../analyze_logs/providers/attachment.rs | 36 +++++---- .../event/analyze_logs/providers/haste.rs | 36 ++++----- .../event/analyze_logs/providers/mclogs.rs | 34 ++++---- .../event/analyze_logs/providers/mod.rs | 79 +++++++++++++----- .../event/analyze_logs/providers/paste_gg.rs | 80 +++++-------------- .../event/analyze_logs/providers/pastebin.rs | 37 ++++----- src/utils/mod.rs | 28 +++++++ 12 files changed, 270 insertions(+), 164 deletions(-) create mode 100644 src/api/paste_gg.rs diff --git a/Cargo.lock b/Cargo.lock index 7014914..c74d784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,6 +465,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "env_filter" version = "0.1.0" @@ -1390,9 +1402,9 @@ dependencies = [ name = "refraction" version = "2.0.0" dependencies = [ - "async-trait", "color-eyre", "dotenvy", + "enum_dispatch", "env_logger", "eyre", "gray_matter", diff --git a/Cargo.toml b/Cargo.toml index ea5bc06..abcd751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,9 @@ serde = "1.0.196" serde_json = "1.0.112" [dependencies] -async-trait = "0.1.77" color-eyre = "0.6.2" dotenvy = "0.15.7" +enum_dispatch = "0.3.12" env_logger = "0.11.1" eyre = "0.6.11" log = "0.4.20" diff --git a/src/api/mod.rs b/src/api/mod.rs index 5198953..a32d14b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,7 @@ use once_cell::sync::Lazy; pub mod dadjoke; +pub mod paste_gg; pub mod pluralkit; pub mod prism_meta; pub mod rory; diff --git a/src/api/paste_gg.rs b/src/api/paste_gg.rs new file mode 100644 index 0000000..c649429 --- /dev/null +++ b/src/api/paste_gg.rs @@ -0,0 +1,55 @@ +use crate::{api::REQWEST_CLIENT, utils}; + +use eyre::{eyre, OptionExt, Result}; +use log::debug; +use serde::{Deserialize, Serialize}; + +const PASTE_GG: &str = "https://api.paste.gg/v1"; +const PASTES: &str = "/pastes"; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] +enum Status { + #[serde(rename = "success")] + Success, + #[serde(rename = "error")] + Error, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Response { + status: Status, + pub result: Option>, + error: Option, + message: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Files { + pub id: String, + pub name: Option, +} + +pub async fn get_files(id: &str) -> Result> { + let url = format!("{PASTE_GG}{PASTES}/{id}/files"); + debug!("Making request to {url}"); + let resp = REQWEST_CLIENT.get(url).send().await?; + resp.error_for_status_ref()?; + let resp: Response = resp.json().await?; + + if resp.status == Status::Error { + let message = resp + .error + .ok_or_eyre("Paste.gg gave us an error but with no message!")?; + + Err(eyre!(message)) + } else { + Ok(resp) + } +} + +pub async fn get_raw_file(paste_id: &str, file_id: &str) -> eyre::Result { + let url = format!("{PASTE_GG}{PASTES}/{paste_id}/files/{file_id}/raw"); + let text = utils::text_from_url(&url).await?; + + Ok(text) +} diff --git a/src/handlers/event/analyze_logs/providers/0x0.rs b/src/handlers/event/analyze_logs/providers/0x0.rs index cfa553a..9c76a40 100644 --- a/src/handlers/event/analyze_logs/providers/0x0.rs +++ b/src/handlers/event/analyze_logs/providers/0x0.rs @@ -1,27 +1,27 @@ -use crate::api::REQWEST_CLIENT; +use crate::utils; -use eyre::{eyre, Result}; +use eyre::Result; use log::trace; use once_cell::sync::Lazy; +use poise::serenity_prelude::Message; use regex::Regex; -use reqwest::StatusCode; -static REGEX: Lazy = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap()); +pub struct _0x0; -pub async fn find(content: &str) -> Result> { - trace!("Checking if {content} is a 0x0 paste"); +impl super::LogProvider for _0x0 { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: Lazy = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap()); - let Some(url) = REGEX.find(content).map(|m| &content[m.range()]) else { - return Ok(None); - }; + trace!("Checking if message {} is a 0x0 paste", message.id); + REGEX + .find_iter(&message.content) + .map(|m| m.as_str().to_string()) + .nth(0) + } - let request = REQWEST_CLIENT.get(url).build()?; - let response = REQWEST_CLIENT.execute(request).await?; - let status = response.status(); + async fn fetch(&self, content: &str) -> Result { + let log = utils::text_from_url(content).await?; - if let StatusCode::OK = status { - Ok(Some(response.text().await?)) - } else { - Err(eyre!("Failed to fetch paste from {url} with {status}",)) + Ok(log) } } diff --git a/src/handlers/event/analyze_logs/providers/attachment.rs b/src/handlers/event/analyze_logs/providers/attachment.rs index 97e8c3e..103c4c2 100644 --- a/src/handlers/event/analyze_logs/providers/attachment.rs +++ b/src/handlers/event/analyze_logs/providers/attachment.rs @@ -2,20 +2,28 @@ use eyre::Result; use log::trace; use poise::serenity_prelude::Message; -pub async fn find(message: &Message) -> Result> { - trace!("Checking for text attachments in message {}", message.id); +use crate::utils; - // find first uploaded text file - if let Some(attachment) = message.attachments.iter().find(|a| { - a.content_type - .as_ref() - .and_then(|ct| ct.starts_with("text/").then_some(true)) - .is_some() - }) { - let bytes = attachment.download().await?; - let res = String::from_utf8(bytes)?; - Ok(Some(res)) - } else { - Ok(None) +pub struct Attachment; + +impl super::LogProvider for Attachment { + async fn find_match(&self, message: &Message) -> Option { + trace!("Checking if message {} has text attachments", message.id); + + message + .attachments + .iter() + .filter_map(|a| { + a.content_type + .as_ref() + .and_then(|ct| ct.starts_with("text/").then_some(a.url.clone())) + }) + .nth(0) + } + + async fn fetch(&self, content: &str) -> Result { + let attachment = utils::bytes_from_url(content).await?; + let log = String::from_utf8(attachment)?; + Ok(log) } } diff --git a/src/handlers/event/analyze_logs/providers/haste.rs b/src/handlers/event/analyze_logs/providers/haste.rs index 194ae79..9909176 100644 --- a/src/handlers/event/analyze_logs/providers/haste.rs +++ b/src/handlers/event/analyze_logs/providers/haste.rs @@ -1,29 +1,29 @@ -use crate::api::REQWEST_CLIENT; +use crate::utils; -use eyre::{eyre, Result}; +use eyre::Result; use log::trace; use once_cell::sync::Lazy; +use poise::serenity_prelude::Message; use regex::Regex; -use reqwest::StatusCode; -static REGEX: Lazy = - Lazy::new(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap()); +const HASTE: &str = "https://hst.sh"; +const RAW: &str = "/raw"; -pub async fn find(content: &str) -> Result> { - trace!("Checking if {content} is a haste log"); +pub struct Haste; - let Some(captures) = REGEX.captures(content) else { - return Ok(None); - }; +impl super::LogProvider for Haste { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: Lazy = + Lazy::new(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap()); - let url = format!("https://hst.sh/raw/{}", &captures[1]); - let request = REQWEST_CLIENT.get(&url).build()?; - let response = REQWEST_CLIENT.execute(request).await?; - let status = response.status(); + trace!("Checking if message {} is a hst.sh paste", message.id); + super::get_first_capture(®EX, &message.content) + } - if let StatusCode::OK = status { - Ok(Some(response.text().await?)) - } else { - Err(eyre!("Failed to fetch paste from {url} with {status}")) + async fn fetch(&self, content: &str) -> Result { + let url = format!("{HASTE}{RAW}/{content}"); + let log = utils::text_from_url(&url).await?; + + Ok(log) } } diff --git a/src/handlers/event/analyze_logs/providers/mclogs.rs b/src/handlers/event/analyze_logs/providers/mclogs.rs index 34933de..7e9265b 100644 --- a/src/handlers/event/analyze_logs/providers/mclogs.rs +++ b/src/handlers/event/analyze_logs/providers/mclogs.rs @@ -1,28 +1,28 @@ -use crate::api::REQWEST_CLIENT; +use crate::utils; -use eyre::{eyre, Result}; +use eyre::Result; use log::trace; use once_cell::sync::Lazy; +use poise::serenity_prelude::Message; use regex::Regex; -use reqwest::StatusCode; -static REGEX: Lazy = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap()); +const MCLOGS: &str = "https://api.mclo.gs/1"; +const RAW: &str = "/raw"; -pub async fn find(content: &str) -> Result> { - trace!("Checking if {content} is an mclo.gs paste"); +pub struct MCLogs; - let Some(captures) = REGEX.captures(content) else { - return Ok(None); - }; +impl super::LogProvider for MCLogs { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: Lazy = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap()); - let url = format!("https://api.mclo.gs/1/raw/{}", &captures[1]); - let request = REQWEST_CLIENT.get(&url).build()?; - let response = REQWEST_CLIENT.execute(request).await?; - let status = response.status(); + trace!("Checking if message {} is an mclo.gs paste", message.id); + super::get_first_capture(®EX, &message.content) + } - if let StatusCode::OK = status { - Ok(Some(response.text().await?)) - } else { - Err(eyre!("Failed to fetch log from {url} with {status}")) + async fn fetch(&self, content: &str) -> Result { + let url = format!("{MCLOGS}{RAW}/{content}"); + let log = utils::text_from_url(&url).await?; + + Ok(log) } } diff --git a/src/handlers/event/analyze_logs/providers/mod.rs b/src/handlers/event/analyze_logs/providers/mod.rs index 2d13d6d..f8f096c 100644 --- a/src/handlers/event/analyze_logs/providers/mod.rs +++ b/src/handlers/event/analyze_logs/providers/mod.rs @@ -1,5 +1,15 @@ +use std::slice::Iter; + +use enum_dispatch::enum_dispatch; use eyre::Result; +use once_cell::sync::Lazy; use poise::serenity_prelude::Message; +use regex::Regex; + +use self::{ + _0x0::_0x0 as _0x0st, attachment::Attachment, haste::Haste, mclogs::MCLogs, paste_gg::PasteGG, + pastebin::PasteBin, +}; #[path = "0x0.rs"] mod _0x0; @@ -9,25 +19,52 @@ mod mclogs; mod paste_gg; mod pastebin; -pub type LogProvider = Result>; - -pub async fn find_log(message: &Message) -> LogProvider { - macro_rules! provider_impl { - ($provider:ident) => { - if let Some(content) = $provider::find(&message.content).await? { - return Ok(Some(content)); - } - }; - } - provider_impl!(_0x0); - provider_impl!(mclogs); - provider_impl!(haste); - provider_impl!(paste_gg); - provider_impl!(pastebin); - - if let Some(content) = attachment::find(message).await? { - return Ok(Some(content)); - } - - Ok(None) +#[enum_dispatch] +pub trait LogProvider { + async fn find_match(&self, message: &Message) -> Option; + async fn fetch(&self, content: &str) -> Result; +} + +fn get_first_capture(regex: &Lazy, string: &str) -> Option { + regex + .captures_iter(string) + .filter_map(|c| c.get(1).map(|c| c.as_str().to_string())) + .nth(1) +} + +#[enum_dispatch(LogProvider)] +enum Provider { + _0x0st, + Attachment, + Haste, + MCLogs, + PasteGG, + PasteBin, +} + +impl Provider { + pub fn interator() -> Iter<'static, Provider> { + static PROVIDERS: [Provider; 6] = [ + Provider::_0x0st(_0x0st), + Provider::Attachment(Attachment), + Provider::Haste(Haste), + Provider::MCLogs(MCLogs), + Provider::PasteBin(PasteBin), + Provider::PasteGG(PasteGG), + ]; + PROVIDERS.iter() + } +} + +pub async fn find_log(message: &Message) -> Result> { + let providers = Provider::interator(); + + for provider in providers { + if let Some(found) = provider.find_match(message).await { + let log = provider.fetch(&found).await?; + return Ok(Some(log)); + } + } + + todo!() } diff --git a/src/handlers/event/analyze_logs/providers/paste_gg.rs b/src/handlers/event/analyze_logs/providers/paste_gg.rs index a45ea81..5800a68 100644 --- a/src/handlers/event/analyze_logs/providers/paste_gg.rs +++ b/src/handlers/event/analyze_logs/providers/paste_gg.rs @@ -1,72 +1,36 @@ -use crate::api::REQWEST_CLIENT; +use crate::api::paste_gg; -use eyre::{eyre, OptionExt, Result}; +use eyre::{OptionExt, Result}; use log::trace; use once_cell::sync::Lazy; +use poise::serenity_prelude::Message; use regex::Regex; -use reqwest::StatusCode; -use serde::{Deserialize, Serialize}; -const PASTE_GG: &str = "https://api.paste.gg/v1"; -const PASTES_ENDPOINT: &str = "/pastes"; -static REGEX: Lazy = Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap()); +pub struct PasteGG; -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PasteResponse { - status: String, - result: Option>, - error: Option, - message: Option, -} +impl super::LogProvider for PasteGG { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: Lazy = + Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap()); -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PasteResult { - id: String, - name: Option, - description: Option, - visibility: Option, -} - -pub async fn find(content: &str) -> Result> { - trace!("Checking if {content} is a paste.gg log"); - let Some(captures) = REGEX.captures(content) else { - return Ok(None); - }; - - let paste_id = &captures[1]; - let files_url = format!("{PASTE_GG}{PASTES_ENDPOINT}/{paste_id}/files"); - - let resp = REQWEST_CLIENT - .execute(REQWEST_CLIENT.get(&files_url).build()?) - .await?; - let status = resp.status(); - - if resp.status() != StatusCode::OK { - return Err(eyre!( - "Couldn't get paste {paste_id} from {PASTE_GG} with status {status}!" - )); + trace!("Checking if message {} is a paste.gg paste", message.id); + super::get_first_capture(®EX, &message.content) } - let paste_files: PasteResponse = resp.json().await?; - let file_id = &paste_files - .result - .ok_or_eyre("Couldn't find any files associated with paste {paste_id}!")?[0] - .id; + async fn fetch(&self, content: &str) -> Result { + let files = paste_gg::get_files(content).await?; + let result = files + .result + .ok_or_eyre("Got an empty result from paste.gg!")?; - let raw_url = format!("{PASTE_GG}{PASTES_ENDPOINT}/{paste_id}/files/{file_id}/raw"); + let file_id = result + .iter() + .map(|f| f.id.as_str()) + .nth(0) + .ok_or_eyre("Couldn't get file id from empty paste.gg response!")?; - let resp = REQWEST_CLIENT - .execute(REQWEST_CLIENT.get(&raw_url).build()?) - .await?; - let status = resp.status(); + let log = paste_gg::get_raw_file(content, file_id).await?; - if status != StatusCode::OK { - return Err(eyre!( - "Couldn't get file {file_id} from paste {paste_id} with status {status}!" - )); + Ok(log) } - - let text = resp.text().await?; - - Ok(Some(text)) } diff --git a/src/handlers/event/analyze_logs/providers/pastebin.rs b/src/handlers/event/analyze_logs/providers/pastebin.rs index c42b3de..f124018 100644 --- a/src/handlers/event/analyze_logs/providers/pastebin.rs +++ b/src/handlers/event/analyze_logs/providers/pastebin.rs @@ -1,28 +1,29 @@ -use crate::api::REQWEST_CLIENT; +use crate::utils; -use eyre::{eyre, Result}; +use eyre::Result; use log::trace; use once_cell::sync::Lazy; +use poise::serenity_prelude::Message; use regex::Regex; -use reqwest::StatusCode; -static REGEX: Lazy = - Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap()); +const PASTEBIN: &str = "https://pastebin.com"; +const RAW: &str = "/raw"; -pub async fn find(content: &str) -> Result> { - trace!("Checking if {content} is a pastebin log"); - let Some(captures) = REGEX.captures(content) else { - return Ok(None); - }; +pub struct PasteBin; - let url = format!("https://pastebin.com/raw/{}", &captures[1]); - let request = REQWEST_CLIENT.get(&url).build()?; - let response = REQWEST_CLIENT.execute(request).await?; - let status = response.status(); +impl super::LogProvider for PasteBin { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: Lazy = + Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap()); - if let StatusCode::OK = status { - Ok(Some(response.text().await?)) - } else { - Err(eyre!("Failed to fetch paste from {url} with {status}")) + trace!("Checking if message {} is a pastebin paste", message.id); + super::get_first_capture(®EX, &message.content) + } + + async fn fetch(&self, content: &str) -> Result { + let url = format!("{PASTEBIN}{RAW}/{content}"); + let log = utils::text_from_url(&url).await?; + + Ok(log) } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3df2c9a..64172ca 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,29 @@ +use crate::api::REQWEST_CLIENT; + +use eyre::Result; +use log::debug; +use reqwest::Response; + pub mod resolve_message; + +pub async fn get_url(url: &str) -> Result { + debug!("Making request to {url}"); + let resp = REQWEST_CLIENT.get(url).send().await?; + resp.error_for_status_ref()?; + + Ok(resp) +} + +pub async fn text_from_url(url: &str) -> Result { + let resp = get_url(url).await?; + + let text = resp.text().await?; + Ok(text) +} + +pub async fn bytes_from_url(url: &str) -> Result> { + let resp = get_url(url).await?; + + let bytes = resp.bytes().await?; + Ok(bytes.to_vec()) +}