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",
]
[[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",

View file

@ -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"

View file

@ -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;

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 once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
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>> {
trace!("Checking if {content} is a 0x0 paste");
impl super::LogProvider for _0x0 {
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 {
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<String> {
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)
}
}

View file

@ -2,20 +2,28 @@ use eyre::Result;
use log::trace;
use poise::serenity_prelude::Message;
pub async fn find(message: &Message) -> Result<Option<String>> {
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| {
pub struct Attachment;
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
.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)
.and_then(|ct| ct.starts_with("text/").then_some(a.url.clone()))
})
.nth(0)
}
async fn fetch(&self, content: &str) -> Result<String> {
let attachment = utils::bytes_from_url(content).await?;
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 once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
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());
pub async fn find(content: &str) -> Result<Option<String>> {
trace!("Checking if {content} is a haste log");
trace!("Checking if message {} is a hst.sh paste", message.id);
super::get_first_capture(&REGEX, &message.content)
}
let Some(captures) = REGEX.captures(content) else {
return Ok(None);
};
async fn fetch(&self, content: &str) -> Result<String> {
let url = format!("{HASTE}{RAW}/{content}");
let log = utils::text_from_url(&url).await?;
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();
if let StatusCode::OK = status {
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}"))
Ok(log)
}
}

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 once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
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>> {
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<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]);
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(&REGEX, &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<String> {
let url = format!("{MCLOGS}{RAW}/{content}");
let log = utils::text_from_url(&url).await?;
Ok(log)
}
}

View file

@ -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<Option<String>>;
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<String>;
async fn fetch(&self, content: &str) -> Result<String>;
}
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()))
.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<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 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<Regex> = 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<Vec<PasteResult>>,
error: Option<String>,
message: Option<String>,
}
impl super::LogProvider for PasteGG {
async fn find_match(&self, message: &Message) -> Option<String> {
static REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap());
#[derive(Clone, Debug, Deserialize, Serialize)]
struct PasteResult {
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}!"
));
trace!("Checking if message {} is a paste.gg paste", message.id);
super::get_first_capture(&REGEX, &message.content)
}
let paste_files: PasteResponse = resp.json().await?;
let file_id = &paste_files
async fn fetch(&self, content: &str) -> Result<String> {
let files = paste_gg::get_files(content).await?;
let result = files
.result
.ok_or_eyre("Couldn't find any files associated with paste {paste_id}!")?[0]
.id;
.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))
}

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 once_cell::sync::Lazy;
use poise::serenity_prelude::Message;
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());
pub async fn find(content: &str) -> Result<Option<String>> {
trace!("Checking if {content} is a pastebin log");
let Some(captures) = REGEX.captures(content) else {
return Ok(None);
};
trace!("Checking if message {} is a pastebin paste", message.id);
super::get_first_capture(&REGEX, &message.content)
}
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();
async fn fetch(&self, content: &str) -> Result<String> {
let url = format!("{PASTEBIN}{RAW}/{content}");
let log = utils::text_from_url(&url).await?;
if let StatusCode::OK = status {
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}"))
Ok(log)
}
}

View file

@ -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<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())
}