analyze_logs: introduce LogProvider trait

This commit is contained in:
seth 2024-03-23 15:56:54 -04:00
parent b63ecde6b4
commit 827b5a4bd7
12 changed files with 270 additions and 164 deletions

14
Cargo.lock generated
View file

@ -465,6 +465,18 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.0" version = "0.1.0"
@ -1390,9 +1402,9 @@ dependencies = [
name = "refraction" name = "refraction"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"async-trait",
"color-eyre", "color-eyre",
"dotenvy", "dotenvy",
"enum_dispatch",
"env_logger", "env_logger",
"eyre", "eyre",
"gray_matter", "gray_matter",

View file

@ -15,9 +15,9 @@ serde = "1.0.196"
serde_json = "1.0.112" serde_json = "1.0.112"
[dependencies] [dependencies]
async-trait = "0.1.77"
color-eyre = "0.6.2" color-eyre = "0.6.2"
dotenvy = "0.15.7" dotenvy = "0.15.7"
enum_dispatch = "0.3.12"
env_logger = "0.11.1" env_logger = "0.11.1"
eyre = "0.6.11" eyre = "0.6.11"
log = "0.4.20" log = "0.4.20"

View file

@ -1,6 +1,7 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
pub mod dadjoke; pub mod dadjoke;
pub mod paste_gg;
pub mod pluralkit; pub mod pluralkit;
pub mod prism_meta; pub mod prism_meta;
pub mod rory; pub mod rory;

55
src/api/paste_gg.rs Normal file
View file

@ -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<T> {
status: Status,
pub result: Option<Vec<T>>,
error: Option<String>,
message: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Files {
pub id: String,
pub name: Option<String>,
}
pub async fn get_files(id: &str) -> Result<Response<Files>> {
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<Files> = 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<String> {
let url = format!("{PASTE_GG}{PASTES}/{paste_id}/files/{file_id}/raw");
let text = utils::text_from_url(&url).await?;
Ok(text)
}

View file

@ -1,27 +1,27 @@
use crate::api::REQWEST_CLIENT; use crate::utils;
use eyre::{eyre, Result}; use eyre::Result;
use log::trace; use log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
use regex::Regex; use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap()); pub struct _0x0;
pub async fn find(content: &str) -> Result<Option<String>> { impl super::LogProvider for _0x0 {
trace!("Checking if {content} is a 0x0 paste"); async fn find_match(&self, message: &Message) -> Option<String> {
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap());
let Some(url) = REGEX.find(content).map(|m| &content[m.range()]) else { trace!("Checking if message {} is a 0x0 paste", message.id);
return Ok(None); REGEX
}; .find_iter(&message.content)
.map(|m| m.as_str().to_string())
.nth(0)
}
let request = REQWEST_CLIENT.get(url).build()?; async fn fetch(&self, content: &str) -> Result<String> {
let response = REQWEST_CLIENT.execute(request).await?; let log = utils::text_from_url(content).await?;
let status = response.status();
if let StatusCode::OK = status { Ok(log)
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}",))
} }
} }

View file

@ -2,20 +2,28 @@ use eyre::Result;
use log::trace; use log::trace;
use poise::serenity_prelude::Message; use poise::serenity_prelude::Message;
pub async fn find(message: &Message) -> Result<Option<String>> { use crate::utils;
trace!("Checking for text attachments in message {}", message.id);
// find first uploaded text file pub struct Attachment;
if let Some(attachment) = message.attachments.iter().find(|a| {
impl super::LogProvider for Attachment {
async fn find_match(&self, message: &Message) -> Option<String> {
trace!("Checking if message {} has text attachments", message.id);
message
.attachments
.iter()
.filter_map(|a| {
a.content_type a.content_type
.as_ref() .as_ref()
.and_then(|ct| ct.starts_with("text/").then_some(true)) .and_then(|ct| ct.starts_with("text/").then_some(a.url.clone()))
.is_some() })
}) { .nth(0)
let bytes = attachment.download().await?; }
let res = String::from_utf8(bytes)?;
Ok(Some(res)) async fn fetch(&self, content: &str) -> Result<String> {
} else { let attachment = utils::bytes_from_url(content).await?;
Ok(None) let log = String::from_utf8(attachment)?;
Ok(log)
} }
} }

View file

@ -1,29 +1,29 @@
use crate::api::REQWEST_CLIENT; use crate::utils;
use eyre::{eyre, Result}; use eyre::Result;
use log::trace; use log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
use regex::Regex; use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = const HASTE: &str = "https://hst.sh";
const RAW: &str = "/raw";
pub struct Haste;
impl super::LogProvider for Haste {
async fn find_match(&self, message: &Message) -> Option<String> {
static REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap()); Lazy::new(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> { trace!("Checking if message {} is a hst.sh paste", message.id);
trace!("Checking if {content} is a haste log"); super::get_first_capture(&REGEX, &message.content)
}
let Some(captures) = REGEX.captures(content) else { async fn fetch(&self, content: &str) -> Result<String> {
return Ok(None); let url = format!("{HASTE}{RAW}/{content}");
}; let log = utils::text_from_url(&url).await?;
let url = format!("https://hst.sh/raw/{}", &captures[1]); Ok(log)
let request = REQWEST_CLIENT.get(&url).build()?;
let response = REQWEST_CLIENT.execute(request).await?;
let status = response.status();
if let StatusCode::OK = status {
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}"))
} }
} }

View file

@ -1,28 +1,28 @@
use crate::api::REQWEST_CLIENT; use crate::utils;
use eyre::{eyre, Result}; use eyre::Result;
use log::trace; use log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
use regex::Regex; use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = 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<Option<String>> { pub struct MCLogs;
trace!("Checking if {content} is an mclo.gs paste");
let Some(captures) = REGEX.captures(content) else { impl super::LogProvider for MCLogs {
return Ok(None); async fn find_match(&self, message: &Message) -> Option<String> {
}; static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap());
let url = format!("https://api.mclo.gs/1/raw/{}", &captures[1]); trace!("Checking if message {} is an mclo.gs paste", message.id);
let request = REQWEST_CLIENT.get(&url).build()?; super::get_first_capture(&REGEX, &message.content)
let response = REQWEST_CLIENT.execute(request).await?; }
let status = response.status();
if let StatusCode::OK = status { async fn fetch(&self, content: &str) -> Result<String> {
Ok(Some(response.text().await?)) let url = format!("{MCLOGS}{RAW}/{content}");
} else { let log = utils::text_from_url(&url).await?;
Err(eyre!("Failed to fetch log from {url} with {status}"))
Ok(log)
} }
} }

View file

@ -1,5 +1,15 @@
use std::slice::Iter;
use enum_dispatch::enum_dispatch;
use eyre::Result; use eyre::Result;
use once_cell::sync::Lazy;
use poise::serenity_prelude::Message; 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"] #[path = "0x0.rs"]
mod _0x0; mod _0x0;
@ -9,25 +19,52 @@ mod mclogs;
mod paste_gg; mod paste_gg;
mod pastebin; mod pastebin;
pub type LogProvider = Result<Option<String>>; #[enum_dispatch]
pub trait LogProvider {
pub async fn find_log(message: &Message) -> LogProvider { async fn find_match(&self, message: &Message) -> Option<String>;
macro_rules! provider_impl { async fn fetch(&self, content: &str) -> Result<String>;
($provider:ident) => { }
if let Some(content) = $provider::find(&message.content).await? {
return Ok(Some(content)); fn get_first_capture(regex: &Lazy<Regex>, string: &str) -> Option<String> {
} regex
}; .captures_iter(string)
} .filter_map(|c| c.get(1).map(|c| c.as_str().to_string()))
provider_impl!(_0x0); .nth(1)
provider_impl!(mclogs); }
provider_impl!(haste);
provider_impl!(paste_gg); #[enum_dispatch(LogProvider)]
provider_impl!(pastebin); enum Provider {
_0x0st,
if let Some(content) = attachment::find(message).await? { Attachment,
return Ok(Some(content)); Haste,
} MCLogs,
PasteGG,
Ok(None) 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<Option<String>> {
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!()
} }

View file

@ -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 log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
use regex::Regex; use regex::Regex;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
const PASTE_GG: &str = "https://api.paste.gg/v1"; pub struct PasteGG;
const PASTES_ENDPOINT: &str = "/pastes";
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap());
#[derive(Clone, Debug, Deserialize, Serialize)] impl super::LogProvider for PasteGG {
struct PasteResponse { async fn find_match(&self, message: &Message) -> Option<String> {
status: String, static REGEX: Lazy<Regex> =
result: Option<Vec<PasteResult>>, Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap());
error: Option<String>,
message: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)] trace!("Checking if message {} is a paste.gg paste", message.id);
struct PasteResult { super::get_first_capture(&REGEX, &message.content)
id: String,
name: Option<String>,
description: Option<String>,
visibility: Option<String>,
}
pub async fn find(content: &str) -> Result<Option<String>> {
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}!"
));
} }
let paste_files: PasteResponse = resp.json().await?; async fn fetch(&self, content: &str) -> Result<String> {
let file_id = &paste_files let files = paste_gg::get_files(content).await?;
let result = files
.result .result
.ok_or_eyre("Couldn't find any files associated with paste {paste_id}!")?[0] .ok_or_eyre("Got an empty result from paste.gg!")?;
.id;
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 let log = paste_gg::get_raw_file(content, file_id).await?;
.execute(REQWEST_CLIENT.get(&raw_url).build()?)
.await?;
let status = resp.status();
if status != StatusCode::OK { Ok(log)
return Err(eyre!(
"Couldn't get file {file_id} from paste {paste_id} with status {status}!"
));
} }
let text = resp.text().await?;
Ok(Some(text))
} }

View file

@ -1,28 +1,29 @@
use crate::api::REQWEST_CLIENT; use crate::utils;
use eyre::{eyre, Result}; use eyre::Result;
use log::trace; use log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
use regex::Regex; use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = const PASTEBIN: &str = "https://pastebin.com";
const RAW: &str = "/raw";
pub struct PasteBin;
impl super::LogProvider for PasteBin {
async fn find_match(&self, message: &Message) -> Option<String> {
static REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap()); Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> { trace!("Checking if message {} is a pastebin paste", message.id);
trace!("Checking if {content} is a pastebin log"); super::get_first_capture(&REGEX, &message.content)
let Some(captures) = REGEX.captures(content) else { }
return Ok(None);
};
let url = format!("https://pastebin.com/raw/{}", &captures[1]); async fn fetch(&self, content: &str) -> Result<String> {
let request = REQWEST_CLIENT.get(&url).build()?; let url = format!("{PASTEBIN}{RAW}/{content}");
let response = REQWEST_CLIENT.execute(request).await?; let log = utils::text_from_url(&url).await?;
let status = response.status();
if let StatusCode::OK = status { Ok(log)
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}"))
} }
} }

View file

@ -1 +1,29 @@
use crate::api::REQWEST_CLIENT;
use eyre::Result;
use log::debug;
use reqwest::Response;
pub mod resolve_message; pub mod resolve_message;
pub async fn get_url(url: &str) -> Result<Response> {
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<String> {
let resp = get_url(url).await?;
let text = resp.text().await?;
Ok(text)
}
pub async fn bytes_from_url(url: &str) -> Result<Vec<u8>> {
let resp = get_url(url).await?;
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}