feat: log analysis

Signed-off-by: seth <getchoo@tuta.io>
This commit is contained in:
TheKodeToad 2023-12-09 16:54:35 +00:00 committed by seth
parent 026d4cb607
commit c6f4295d6a
18 changed files with 487 additions and 20 deletions

View file

@ -9,7 +9,7 @@ const DADJOKE: &str = "https://icanhazdadjoke.com";
pub async fn get_joke() -> Result<String> {
let req = REQWEST_CLIENT.get(DADJOKE).build()?;
info!("Making request to {}", req.url());
debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status();

View file

@ -19,7 +19,7 @@ pub async fn get_sender(message_id: MessageId) -> Result<UserId> {
.get(format!("{PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id}"))
.build()?;
info!("Making request to {}", req.url());
debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status();

View file

@ -22,7 +22,7 @@ pub async fn get_latest_minecraft_version() -> Result<String> {
.get(format!("{PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT}"))
.build()?;
info!("Making request to {}", req.url());
debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status();

View file

@ -16,31 +16,26 @@ const RORY: &str = "https://rory.cat";
const ENDPOINT: &str = "/purr";
pub async fn get_rory(id: Option<u64>) -> Result<RoryResponse> {
let target = {
if let Some(id) = id {
id.to_string()
} else {
"".to_string()
}
};
let target = id.map(|id| id.to_string()).unwrap_or_default();
let req = REQWEST_CLIENT
.get(format!("{RORY}{ENDPOINT}/{target}"))
.build()
.wrap_err_with(|| "Couldn't build reqwest client!")?;
info!("Making request to {}", req.url());
debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT
.execute(req)
.await
.wrap_err_with(|| "Couldn't make request for shiggy!")?;
.wrap_err_with(|| "Couldn't make request for rory!")?;
let status = resp.status();
if let StatusCode::OK = status {
let data = resp
.json::<RoryResponse>()
.await
.wrap_err_with(|| "Couldn't parse the shiggy response!")?;
.wrap_err_with(|| "Couldn't parse the rory response!")?;
Ok(data)
} else {

View file

@ -10,6 +10,7 @@ pub static COLORS: Lazy<HashMap<&str, Color>> = Lazy::new(|| {
("blue", Color::from((96, 165, 250))),
("yellow", Color::from((253, 224, 71))),
("orange", Color::from((251, 146, 60))),
// TODO purple & pink :D
])
});

View file

@ -17,7 +17,7 @@ pub async fn handle(error: FrameworkError<'_, Data, Report>) {
e.title("Something went wrong!")
.description("oopsie")
.timestamp(Timestamp::now())
.color(COLORS["orange"])
.color(COLORS["red"])
})
})
.await

View file

@ -0,0 +1,181 @@
use once_cell::sync::Lazy;
use regex::Regex;
pub type Issue = Option<(String, String)>;
pub fn find_issues(log: &str) -> Vec<(String, String)> {
let issues = [
fabric_internal,
flatpak_nvidia,
forge_java,
intel_hd,
java_option,
macos_ns,
oom,
optinofine,
outdated_launcher,
wrong_java,
];
issues.iter().filter_map(|issue| issue(log)).collect()
}
fn fabric_internal(log: &str) -> Issue {
const CLASS_NOT_FOUND: &str = "Caused by: java.lang.ClassNotFoundException: ";
let issue = (
"Fabric Internal Access".to_string(),
"The mod you are using is using fabric internals that are not meant \
to be used by anything but the loader itself.
Those mods break both on Quilt and with fabric updates.
If you're using fabric, downgrade your fabric loader could work, \
on Quilt you can try updating to the latest beta version, \
but there's nothing much to do unless the mod author stops using them."
.to_string(),
);
let errors = [
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.impl"),
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.mixin"),
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.impl"),
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.mixin"),
"org.quiltmc.loader.impl.FormattedException: java.lang.NoSuchMethodError:",
];
let found = errors.iter().any(|e| log.contains(e));
found.then_some(issue)
}
fn flatpak_nvidia(log: &str) -> Issue {
let issue = (
"Outdated Nvidia Flatpak Driver".to_string(),
"The Nvidia driver for flatpak is outdated.
Please run `flatpak update` to fix this issue. \
If that does not solve it, \
please wait until the driver is added to Flathub and run it again."
.to_string(),
);
let found = log.contains("org.lwjgl.LWJGLException: Could not choose GLX13 config")
|| log.contains("GLFW error 65545: GLX: Failed to find a suitable GLXFBConfig");
found.then_some(issue)
}
fn forge_java(log: &str) -> Issue {
let issue = (
"Forge Java Bug".to_string(),
"Old versions of Forge crash with Java 8u321+.
To fix this, update forge to the latest version via the Versions tab
(right click on Forge, click Change Version, and choose the latest one)
Alternatively, you can download 8u312 or lower. \
See [archive](https://github.com/adoptium/temurin8-binaries/releases/tag/jdk8u312-b07)"
.to_string(),
);
let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V");
found.then_some(issue)
}
fn intel_hd(log: &str) -> Issue {
let issue =
(
"Intel HD Windows 10".to_string(),
"Your drivers don't support windows 10 officially
See https://prismlauncher.org/wiki/getting-started/installing-java/#a-note-about-intel-hd-20003000-on-windows-10 for more info".to_string()
);
let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V");
found.then_some(issue)
}
fn java_option(log: &str) -> Issue {
static VM_OPTION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Unrecognized VM option '(.+)'[\r\n]").unwrap());
static OPTION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Unrecognized option: (.+)[\r\n]").unwrap());
if let Some(captures) = VM_OPTION_REGEX.captures(log) {
let title = if &captures[1] == "UseShenandoahGC" {
"Wrong Java Arguments"
} else {
"Java 8 and below don't support ShenandoahGC"
};
return Some((
title.to_string(),
format!("Remove `-XX:{}` from your Java arguments", &captures[1]),
));
}
if let Some(captures) = OPTION_REGEX.captures(log) {
return Some((
"Wrong Java Arguments".to_string(),
format!("Remove `{}` from your Java arguments", &captures[1]),
));
}
None
}
fn macos_ns(log: &str) -> Issue {
let issue = (
"MacOS NSInternalInconsistencyException".to_string(),
"You need to downgrade your Java 8 version. See https://prismlauncher.org/wiki/getting-started/installing-java/#older-minecraft-on-macos".to_string()
);
let found =
log.contains("Terminating app due to uncaught exception 'NSInternalInconsistencyException");
found.then_some(issue)
}
fn oom(log: &str) -> Issue {
let issue = (
"Out of Memory".to_string(),
"Allocating more RAM to your instance could help prevent this crash.".to_string(),
);
let found = log.contains("java.lang.OutOfMemoryError");
found.then_some(issue)
}
fn optinofine(log: &str) -> Issue {
let issue = (
"Potential OptiFine Incompatibilities".to_string(),
"OptiFine is known to cause problems when paired with other mods. \
Try to disable OptiFine and see if the issue persists.
Check `/tag optifine` for more info & some typically more compatible alternatives you can use."
.to_string(),
);
let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-");
found.then_some(issue)
}
// TODO: @TheKodeToad
fn outdated_launcher(_log: &str) -> Issue {
todo!()
}
fn wrong_java(log: &str) -> Issue {
static SWITCH_VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?m)Please switch to one of the following Java versions for this instance:[\r\n]+(Java version [\d.]+)",
).unwrap()
});
if let Some(captures) = SWITCH_VERSION_REGEX.captures(log) {
let versions = captures[1].split('\n').collect::<Vec<&str>>().join(", ");
return Some((
"Wrong Java Version".to_string(),
format!("Please switch to one of the following: `{versions}`\nFor more information, type `/tag java`"),
));
}
let issue = (
"Java compatibility check skipped".to_string(),
"The Java major version may not work with your Minecraft instance. Please switch to a compatible version".to_string()
);
log.contains("Java major version is incompatible. Things might break.")
.then_some(issue)
}

View file

@ -0,0 +1,66 @@
use crate::consts::COLORS;
use color_eyre::eyre::Result;
use log::*;
use poise::serenity_prelude::{Context, Message};
mod issues;
mod providers;
use issues::find_issues;
use providers::find_log;
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
let channel = message.channel_id;
let log = find_log(message).await;
if log.is_err() {
channel
.send_message(ctx, |m| {
m.reference_message(message)
.allowed_mentions(|am| am.replied_user(true))
.embed(|e| {
e.title("Analyze failed!")
.description("Couldn't download log")
})
})
.await?;
return Ok(());
}
let Some(log) = log? else {
debug!("No log found in message! Skipping analysis");
return Ok(());
};
let issues = find_issues(&log);
channel
.send_message(ctx, |m| {
m.reference_message(message)
.allowed_mentions(|am| am.replied_user(true))
.embed(|e| {
e.title("Log analysis");
if issues.is_empty() {
e.color(COLORS["green"]).field(
"Analyze failed!",
"No issues found automatically",
false,
);
} else {
e.color(COLORS["red"]);
for (title, description) in issues {
e.field(title, description, false);
}
}
e
})
})
.await?;
Ok(())
}

View file

@ -0,0 +1,24 @@
use crate::api::REQWEST_CLIENT;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> {
let Some(url) = REGEX.find(content).map(|m| &content[m.range()]) else {
return Ok(None);
};
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

@ -0,0 +1,18 @@
use color_eyre::eyre::Result;
use poise::serenity_prelude::Message;
pub async fn find(message: &Message) -> Result<Option<String>> {
// 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)
}
}

View file

@ -0,0 +1,26 @@
use crate::api::REQWEST_CLIENT;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::StatusCode;
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>> {
let Some(captures) = REGEX.captures(content) else {
return Ok(None);
};
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}"))
}
}

View file

@ -0,0 +1,25 @@
use crate::api::REQWEST_CLIENT;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else {
return Ok(None);
};
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();
if let StatusCode::OK = status {
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch log from {url} with {status}"))
}
}

View file

@ -0,0 +1,33 @@
use color_eyre::eyre::Result;
use poise::serenity_prelude::Message;
#[path = "0x0.rs"]
mod _0x0;
mod attachment;
mod haste;
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)
}

View file

@ -0,0 +1,70 @@
use crate::api::REQWEST_CLIENT;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::Lazy;
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());
#[derive(Clone, Debug, Deserialize, Serialize)]
struct PasteResponse {
status: String,
result: Option<Vec<PasteResult>>,
error: Option<String>,
message: Option<String>,
}
#[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>> {
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?;
let file_id = &paste_files
.result
.ok_or_else(|| eyre!("Couldn't find any files associated with paste {paste_id}!"))?[0]
.id;
let raw_url = format!("{PASTE_GG}{PASTES_ENDPOINT}/{paste_id}/files/{file_id}/raw");
let resp = REQWEST_CLIENT
.execute(REQWEST_CLIENT.get(&raw_url).build()?)
.await?;
let status = resp.status();
if status != StatusCode::OK {
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

@ -0,0 +1,26 @@
use crate::api::REQWEST_CLIENT;
use color_eyre::eyre::{eyre, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::StatusCode;
static REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else {
return Ok(None);
};
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();
if let StatusCode::OK = status {
Ok(Some(response.text().await?))
} else {
Err(eyre!("Failed to fetch paste from {url} with {status}"))
}
}

View file

@ -7,8 +7,8 @@ use regex::Regex;
static ETA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\beta\b").unwrap());
pub async fn handle(ctx: &Context, msg: &Message) -> Result<()> {
if !ETA_REGEX.is_match(&msg.content) {
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
if !ETA_REGEX.is_match(&message.content) {
return Ok(());
}
@ -17,6 +17,6 @@ pub async fn handle(ctx: &Context, msg: &Message) -> Result<()> {
utils::random_choice(consts::ETA_MESSAGES)?
);
msg.reply(ctx, response).await?;
message.reply(ctx, response).await?;
Ok(())
}

View file

@ -3,12 +3,12 @@ use poise::serenity_prelude::{Context, Message};
use crate::utils;
pub async fn handle(ctx: &Context, msg: &Message) -> Result<()> {
let embeds = utils::resolve_message(ctx, msg).await?;
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
let embeds = utils::resolve_message(ctx, message).await?;
// TOOD getchoo: actually reply to user
// ...not sure why Message doesn't give me a builder in reply() or equivalents
let our_channel = msg
let our_channel = message
.channel(ctx)
.await
.wrap_err_with(|| "Couldn't get channel from message!")?

View file

@ -5,6 +5,7 @@ use log::*;
use poise::serenity_prelude::{Activity, Context, OnlineStatus};
use poise::{Event, FrameworkContext};
mod analyze_logs;
mod delete_on_reaction;
mod eta;
mod expand_link;
@ -52,6 +53,7 @@ pub async fn handle(
eta::handle(ctx, new_message).await?;
expand_link::handle(ctx, new_message).await?;
analyze_logs::handle(ctx, new_message).await?;
}
Event::MessageDelete {