From c6f4295d6a6c74cc75c1a76660660f3d6fbb99fe Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 9 Dec 2023 16:54:35 +0000 Subject: [PATCH] feat: log analysis Signed-off-by: seth --- src/api/dadjoke.rs | 2 +- src/api/pluralkit.rs | 2 +- src/api/prism_meta.rs | 2 +- src/api/rory.rs | 15 +- src/consts.rs | 1 + src/handlers/error.rs | 2 +- src/handlers/event/analyze_logs/issues.rs | 181 ++++++++++++++++++ src/handlers/event/analyze_logs/mod.rs | 66 +++++++ .../event/analyze_logs/providers/0x0.rs | 24 +++ .../analyze_logs/providers/attachment.rs | 18 ++ .../event/analyze_logs/providers/haste.rs | 26 +++ .../event/analyze_logs/providers/mclogs.rs | 25 +++ .../event/analyze_logs/providers/mod.rs | 33 ++++ .../event/analyze_logs/providers/paste_gg.rs | 70 +++++++ .../event/analyze_logs/providers/pastebin.rs | 26 +++ src/handlers/event/eta.rs | 6 +- src/handlers/event/expand_link.rs | 6 +- src/handlers/event/mod.rs | 2 + 18 files changed, 487 insertions(+), 20 deletions(-) create mode 100644 src/handlers/event/analyze_logs/issues.rs create mode 100644 src/handlers/event/analyze_logs/mod.rs create mode 100644 src/handlers/event/analyze_logs/providers/0x0.rs create mode 100644 src/handlers/event/analyze_logs/providers/attachment.rs create mode 100644 src/handlers/event/analyze_logs/providers/haste.rs create mode 100644 src/handlers/event/analyze_logs/providers/mclogs.rs create mode 100644 src/handlers/event/analyze_logs/providers/mod.rs create mode 100644 src/handlers/event/analyze_logs/providers/paste_gg.rs create mode 100644 src/handlers/event/analyze_logs/providers/pastebin.rs diff --git a/src/api/dadjoke.rs b/src/api/dadjoke.rs index b29e2bf..8daef2c 100644 --- a/src/api/dadjoke.rs +++ b/src/api/dadjoke.rs @@ -9,7 +9,7 @@ const DADJOKE: &str = "https://icanhazdadjoke.com"; pub async fn get_joke() -> Result { 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(); diff --git a/src/api/pluralkit.rs b/src/api/pluralkit.rs index fd4dad5..89cfb88 100644 --- a/src/api/pluralkit.rs +++ b/src/api/pluralkit.rs @@ -19,7 +19,7 @@ pub async fn get_sender(message_id: MessageId) -> Result { .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(); diff --git a/src/api/prism_meta.rs b/src/api/prism_meta.rs index ce6adf1..3a99e40 100644 --- a/src/api/prism_meta.rs +++ b/src/api/prism_meta.rs @@ -22,7 +22,7 @@ pub async fn get_latest_minecraft_version() -> Result { .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(); diff --git a/src/api/rory.rs b/src/api/rory.rs index 63be450..399500c 100644 --- a/src/api/rory.rs +++ b/src/api/rory.rs @@ -16,31 +16,26 @@ const RORY: &str = "https://rory.cat"; const ENDPOINT: &str = "/purr"; pub async fn get_rory(id: Option) -> Result { - 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::() .await - .wrap_err_with(|| "Couldn't parse the shiggy response!")?; + .wrap_err_with(|| "Couldn't parse the rory response!")?; Ok(data) } else { diff --git a/src/consts.rs b/src/consts.rs index 1c305fe..35fdb61 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -10,6 +10,7 @@ pub static COLORS: Lazy> = Lazy::new(|| { ("blue", Color::from((96, 165, 250))), ("yellow", Color::from((253, 224, 71))), ("orange", Color::from((251, 146, 60))), + // TODO purple & pink :D ]) }); diff --git a/src/handlers/error.rs b/src/handlers/error.rs index 5842b49..de2121e 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -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 diff --git a/src/handlers/event/analyze_logs/issues.rs b/src/handlers/event/analyze_logs/issues.rs new file mode 100644 index 0000000..f75415f --- /dev/null +++ b/src/handlers/event/analyze_logs/issues.rs @@ -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.(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.(Ljava/util/jar/Manifest;)V"); + found.then_some(issue) +} + +fn java_option(log: &str) -> Issue { + static VM_OPTION_REGEX: Lazy = + Lazy::new(|| Regex::new(r"Unrecognized VM option '(.+)'[\r\n]").unwrap()); + static OPTION_REGEX: Lazy = + 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 = 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::>().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) +} diff --git a/src/handlers/event/analyze_logs/mod.rs b/src/handlers/event/analyze_logs/mod.rs new file mode 100644 index 0000000..74b0c85 --- /dev/null +++ b/src/handlers/event/analyze_logs/mod.rs @@ -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(()) +} diff --git a/src/handlers/event/analyze_logs/providers/0x0.rs b/src/handlers/event/analyze_logs/providers/0x0.rs new file mode 100644 index 0000000..c958389 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/0x0.rs @@ -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 = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap()); + +pub async fn find(content: &str) -> Result> { + 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}",)) + } +} diff --git a/src/handlers/event/analyze_logs/providers/attachment.rs b/src/handlers/event/analyze_logs/providers/attachment.rs new file mode 100644 index 0000000..71d8d65 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/attachment.rs @@ -0,0 +1,18 @@ +use color_eyre::eyre::Result; +use poise::serenity_prelude::Message; + +pub async fn find(message: &Message) -> Result> { + // 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) + } +} diff --git a/src/handlers/event/analyze_logs/providers/haste.rs b/src/handlers/event/analyze_logs/providers/haste.rs new file mode 100644 index 0000000..525da1b --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/haste.rs @@ -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 = + Lazy::new(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap()); + +pub async fn find(content: &str) -> Result> { + 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}")) + } +} diff --git a/src/handlers/event/analyze_logs/providers/mclogs.rs b/src/handlers/event/analyze_logs/providers/mclogs.rs new file mode 100644 index 0000000..c22f0c6 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/mclogs.rs @@ -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 = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap()); + +pub async fn find(content: &str) -> Result> { + 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}")) + } +} diff --git a/src/handlers/event/analyze_logs/providers/mod.rs b/src/handlers/event/analyze_logs/providers/mod.rs new file mode 100644 index 0000000..3ebeb9a --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/mod.rs @@ -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>; + +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) +} diff --git a/src/handlers/event/analyze_logs/providers/paste_gg.rs b/src/handlers/event/analyze_logs/providers/paste_gg.rs new file mode 100644 index 0000000..da52453 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/paste_gg.rs @@ -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 = Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap()); + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct PasteResponse { + status: String, + result: Option>, + error: Option, + message: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct PasteResult { + id: String, + name: Option, + description: Option, + visibility: Option, +} + +pub async fn find(content: &str) -> Result> { + 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)) +} diff --git a/src/handlers/event/analyze_logs/providers/pastebin.rs b/src/handlers/event/analyze_logs/providers/pastebin.rs new file mode 100644 index 0000000..b8dc0bb --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/pastebin.rs @@ -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 = + Lazy::new(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap()); + +pub async fn find(content: &str) -> Result> { + 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}")) + } +} diff --git a/src/handlers/event/eta.rs b/src/handlers/event/eta.rs index 2eb8ae2..e62d22a 100644 --- a/src/handlers/event/eta.rs +++ b/src/handlers/event/eta.rs @@ -7,8 +7,8 @@ use regex::Regex; static ETA_REGEX: Lazy = 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(()) } diff --git a/src/handlers/event/expand_link.rs b/src/handlers/event/expand_link.rs index 95065e1..0863115 100644 --- a/src/handlers/event/expand_link.rs +++ b/src/handlers/event/expand_link.rs @@ -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!")? diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index bf0367e..660b1da 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -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 {