Add mclo.gs upload prompt
This commit is contained in:
parent
c4da3b5f00
commit
dcc08ee4ad
9 changed files with 343 additions and 80 deletions
35
src/api/mclogs.rs
Normal file
35
src/api/mclogs.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use eyre::{eyre, OptionExt, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{HttpClient, HttpClientExt};
|
||||
|
||||
const MCLOGS: &str = "https://api.mclo.gs/1";
|
||||
const UPLOAD: &str = "/log";
|
||||
const RAW: &str = "/raw";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PostLogResponse {
|
||||
pub success: bool,
|
||||
pub id: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub raw: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn upload_log(http: &HttpClient, content: &str) -> Result<PostLogResponse> {
|
||||
let url = format!("{MCLOGS}{UPLOAD}");
|
||||
let request = http
|
||||
.post(url)
|
||||
.form(&HashMap::from([("content", content)]))
|
||||
.build()?;
|
||||
|
||||
Ok(http.execute(request).await?.json().await?)
|
||||
}
|
||||
|
||||
pub async fn raw_log(http: &HttpClient, id: &str) -> Result<String> {
|
||||
let url = format!("{MCLOGS}{RAW}/{id}");
|
||||
|
||||
Ok(http.get_request(&url).await?.text().await?)
|
||||
}
|
|
@ -3,6 +3,7 @@ use reqwest::Response;
|
|||
|
||||
pub mod dadjoke;
|
||||
pub mod github;
|
||||
pub mod mclogs;
|
||||
pub mod paste_gg;
|
||||
pub mod pluralkit;
|
||||
pub mod prism_meta;
|
||||
|
|
65
src/handlers/event/analyze_logs/heuristics.rs
Normal file
65
src/handlers/event/analyze_logs/heuristics.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use log::trace;
|
||||
use regex::Regex;
|
||||
|
||||
pub fn looks_like_launcher_log(log: &str) -> bool {
|
||||
static QT_LOG_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
trace!("Guessing whether log is launcher log");
|
||||
|
||||
let qt_log = QT_LOG_REGEX.get_or_init(|| Regex::new(r"\d\.\d{3} [CDFIW] \|").unwrap());
|
||||
qt_log.is_match(log)
|
||||
}
|
||||
|
||||
pub fn looks_like_mc_log(log: &str) -> bool {
|
||||
static LOG4J_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
trace!("Guessing whether log is Minecraft log");
|
||||
|
||||
if log.contains("Minecraft process ID: ") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// present in almost every Minecraft version
|
||||
if log.contains("Setting user: ") || log.contains("Minecraft Version: ") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if log.contains("Exception in thread ")
|
||||
|| log.contains("Exception: ")
|
||||
|| log.contains("Error: ")
|
||||
|| log.contains("Throwable: ")
|
||||
|| log.contains("Caused by: ")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if log.contains("org.prismlauncher.EntryPoint.main(EntryPoint.java")
|
||||
|| log.contains("java.lang.Thread.run(Thread.java")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let log4j = LOG4J_REGEX
|
||||
.get_or_init(|| Regex::new(r"\[\d{2}:\d{2}:\d{2}\] \[.+?/(FATAL|ERROR|WARN|INFO|DEBUG|TRACE)\] ").unwrap());
|
||||
|
||||
if log4j.is_match(&log) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if log.contains("[INFO]")
|
||||
|| log.contains("[CONFIG]")
|
||||
|| log.contains("[FINE]")
|
||||
|| log.contains("[FINER]")
|
||||
|| log.contains("[FINEST]")
|
||||
|| log.contains("[SEVERE]")
|
||||
|| log.contains("[STDERR]")
|
||||
|| log.contains("[WARNING]")
|
||||
|| log.contains("[DEBUG]")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
|
@ -1,17 +1,34 @@
|
|||
use crate::{consts::Colors, Data};
|
||||
|
||||
use eyre::Result;
|
||||
use log::{debug, trace};
|
||||
use poise::serenity_prelude::{
|
||||
Context, CreateAllowedMentions, CreateEmbed, CreateMessage, Message,
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::{mclogs, HttpClientExt},
|
||||
consts::Colors,
|
||||
utils::first_text_attachment,
|
||||
Data,
|
||||
};
|
||||
|
||||
use color_eyre::owo_colors::OwoColorize;
|
||||
use eyre::{eyre, OptionExt, Result};
|
||||
use log::{debug, trace};
|
||||
use poise::serenity_prelude::{
|
||||
ButtonStyle, ComponentInteraction, Context, CreateAllowedMentions, CreateButton, CreateEmbed,
|
||||
CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, EditMessage,
|
||||
Message, MessageId, MessageType,
|
||||
};
|
||||
|
||||
mod heuristics;
|
||||
mod issues;
|
||||
mod providers;
|
||||
|
||||
use providers::find_log;
|
||||
|
||||
pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> {
|
||||
const BUTTON_UPLOAD_YES: &str = "log-upload-yes";
|
||||
const BUTTON_UPLOAD_NO: &str = "log-upload-no";
|
||||
|
||||
pub async fn handle_message(ctx: &Context, message: &Message, data: &Data) -> Result<()> {
|
||||
trace!(
|
||||
"Checking message {} from {} for logs",
|
||||
message.id,
|
||||
|
@ -19,9 +36,7 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
|
|||
);
|
||||
let channel = message.channel_id;
|
||||
|
||||
let log = find_log(&data.http_client, message).await;
|
||||
|
||||
if log.is_err() {
|
||||
let Ok(log) = find_log(&data.http_client, message).await else {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Analysis failed!")
|
||||
.description("Couldn't download log");
|
||||
|
@ -33,43 +48,216 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
|
|||
|
||||
channel.send_message(ctx, our_message).await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(log) = log? else {
|
||||
debug!("No log found in message! Skipping analysis");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let log = log.replace("\r\n", "\n");
|
||||
let attachment = first_text_attachment(message);
|
||||
|
||||
let log = match log {
|
||||
Some(log) => log,
|
||||
None => match attachment {
|
||||
Some(attachment) => {
|
||||
data.http_client
|
||||
.get_request(&attachment.url)
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
debug!("No log found in message! Skipping analysis");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let issues = issues::find(&log, data).await?;
|
||||
let launcher_log = heuristics::looks_like_launcher_log(&log);
|
||||
let mc_log = !launcher_log && heuristics::looks_like_mc_log(&log);
|
||||
|
||||
let embed = {
|
||||
let mut e = CreateEmbed::new().title("Log analysis");
|
||||
debug!("Heuristics: mc_log = {mc_log}, launcher_log = {launcher_log}");
|
||||
|
||||
if issues.is_empty() {
|
||||
e = e
|
||||
.color(Colors::Green)
|
||||
.description("The automatic check didn't reveal any issues, but it's possible that some issues went undetected. Please wait for a volunteer to assist you.");
|
||||
} else {
|
||||
e = e.color(Colors::Red);
|
||||
let show_analysis = !issues.is_empty() || mc_log;
|
||||
let show_upload_prompt = attachment.is_some() && (mc_log || launcher_log);
|
||||
|
||||
for (title, description) in issues {
|
||||
e = e.field(title, description, false);
|
||||
}
|
||||
}
|
||||
|
||||
e
|
||||
};
|
||||
if !show_analysis && !show_upload_prompt {
|
||||
debug!("Found log but there is nothing to respond with");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let allowed_mentions = CreateAllowedMentions::new().replied_user(true);
|
||||
let message = CreateMessage::new()
|
||||
|
||||
let mut message = CreateMessage::new()
|
||||
.reference_message(message)
|
||||
.allowed_mentions(allowed_mentions)
|
||||
.embed(embed);
|
||||
.allowed_mentions(allowed_mentions);
|
||||
|
||||
if show_analysis {
|
||||
message = message.add_embed({
|
||||
let mut e = CreateEmbed::new().title("Log analysis");
|
||||
|
||||
if issues.is_empty() {
|
||||
e = e
|
||||
.color(Colors::Green)
|
||||
.description("The automatic check didn't reveal any issues, but it's possible that some issues went undetected. Please wait for a volunteer to assist you.");
|
||||
} else {
|
||||
e = e.color(Colors::Red);
|
||||
|
||||
for (title, description) in issues {
|
||||
e = e.field(title, description, false);
|
||||
}
|
||||
}
|
||||
|
||||
e
|
||||
});
|
||||
}
|
||||
|
||||
if show_upload_prompt {
|
||||
message = message.add_embed(
|
||||
CreateEmbed::new()
|
||||
.title("Upload log?")
|
||||
.color(Colors::Blue)
|
||||
.description("Discord attachments make it difficult for volunteers to view logs. Would you like me to upload your log to [mclo.gs](https://mclo.gs/)?"),
|
||||
);
|
||||
message = message
|
||||
.button(
|
||||
CreateButton::new(BUTTON_UPLOAD_NO)
|
||||
.style(ButtonStyle::Secondary)
|
||||
.label("No"),
|
||||
)
|
||||
.button(
|
||||
CreateButton::new(BUTTON_UPLOAD_YES)
|
||||
.style(ButtonStyle::Primary)
|
||||
.label("Yes"),
|
||||
);
|
||||
}
|
||||
|
||||
channel.send_message(ctx, message).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_component_interaction(
|
||||
ctx: &Context,
|
||||
interaction: &ComponentInteraction,
|
||||
data: &Data,
|
||||
) -> Result<()> {
|
||||
if interaction.message.kind != MessageType::InlineReply {
|
||||
debug!("Ignoring component interaction on message which is not a reply");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let yes = interaction.data.custom_id == BUTTON_UPLOAD_YES;
|
||||
|
||||
if !yes && interaction.data.custom_id != BUTTON_UPLOAD_NO {
|
||||
debug!(
|
||||
"Ignoring component interaction without ID {BUTTON_UPLOAD_YES} or {BUTTON_UPLOAD_NO}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO: what is this called? need it to automatically be removed when going out of scope
|
||||
// static ACTIVE_MUTEX_LOCK: OnceLock<Mutex<HashSet<MessageId>>> = OnceLock::new();
|
||||
// let active_mutex = ACTIVE_MUTEX_LOCK.get_or_init(|| Mutex::new(HashSet::new()));
|
||||
//
|
||||
// if !active_mutex.lock().unwrap().insert(interaction.message.id) {
|
||||
// debug!(
|
||||
// "Already handling upload button {}; returning",
|
||||
// interaction.message.id
|
||||
// );
|
||||
// return Ok(());
|
||||
// }
|
||||
|
||||
let mut embeds: Vec<CreateEmbed> = interaction
|
||||
.message
|
||||
.embeds
|
||||
.iter()
|
||||
.map(|embed| CreateEmbed::from(embed.to_owned()))
|
||||
.collect();
|
||||
|
||||
if yes {
|
||||
// for some reason Discord never sends us the referenced message, only its id
|
||||
let message_reference = interaction
|
||||
.message
|
||||
.message_reference
|
||||
.as_ref()
|
||||
.ok_or_eyre("Missing message reference")?;
|
||||
let referenced_message = ctx
|
||||
.http
|
||||
.get_message(
|
||||
message_reference.channel_id,
|
||||
message_reference
|
||||
.message_id
|
||||
.ok_or_eyre("Reference missing message ID")?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let first_attachment = first_text_attachment(&referenced_message)
|
||||
.ok_or_eyre("Log attachment disappeared (should not be possible)")?;
|
||||
let body = data
|
||||
.http_client
|
||||
.get_request(&first_attachment.url)
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let response = mclogs::upload_log(&data.http_client, &body).await?;
|
||||
|
||||
if !response.success {
|
||||
let error = response
|
||||
.error
|
||||
.ok_or_else(|| eyre!("mclo.gs gave us an error but with no message!"))?;
|
||||
|
||||
interaction
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.ephemeral(true)
|
||||
.embed(
|
||||
CreateEmbed::new()
|
||||
.title("Upload failed")
|
||||
.color(Colors::Red)
|
||||
.description(&error),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// active_mutex.lock().unwrap().remove(&interaction.message.id);
|
||||
return Err(eyre!("Failed to upload log: {}", &error));
|
||||
}
|
||||
|
||||
let url = &response
|
||||
.url
|
||||
.ok_or_eyre("Missing URL in mclo.gs response!")?;
|
||||
|
||||
let length = embeds.len();
|
||||
|
||||
embeds[length - 1] = CreateEmbed::new()
|
||||
.title("Uploaded log")
|
||||
.color(Colors::Blue)
|
||||
.description(url);
|
||||
} else {
|
||||
embeds.pop();
|
||||
}
|
||||
|
||||
interaction
|
||||
.create_response(ctx, CreateInteractionResponse::Acknowledge)
|
||||
.await?;
|
||||
|
||||
if embeds.len() == 0 {
|
||||
interaction.message.delete(ctx).await?;
|
||||
} else {
|
||||
ctx.http
|
||||
.edit_message(
|
||||
interaction.channel_id,
|
||||
interaction.message.id,
|
||||
&EditMessage::new().embeds(embeds).components(vec![]),
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// active_mutex.lock().unwrap().remove(&interaction.message.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
use crate::api::{HttpClient, HttpClientExt};
|
||||
|
||||
use eyre::Result;
|
||||
use log::trace;
|
||||
use poise::serenity_prelude::Message;
|
||||
|
||||
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(a.url.clone()))
|
||||
})
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
async fn fetch(&self, http: &HttpClient, content: &str) -> Result<String> {
|
||||
let attachment = http.get_request(content).await?.bytes().await?.to_vec();
|
||||
let log = String::from_utf8(attachment)?;
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::api::{HttpClient, HttpClientExt};
|
||||
use crate::api::{mclogs::raw_log, HttpClient, HttpClientExt};
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
@ -7,9 +7,6 @@ use log::trace;
|
|||
use poise::serenity_prelude::Message;
|
||||
use regex::Regex;
|
||||
|
||||
const MCLOGS: &str = "https://api.mclo.gs/1";
|
||||
const RAW: &str = "/raw";
|
||||
|
||||
pub struct MCLogs;
|
||||
|
||||
impl super::LogProvider for MCLogs {
|
||||
|
@ -22,9 +19,6 @@ impl super::LogProvider for MCLogs {
|
|||
}
|
||||
|
||||
async fn fetch(&self, http: &HttpClient, content: &str) -> Result<String> {
|
||||
let url = format!("{MCLOGS}{RAW}/{content}");
|
||||
let log = http.get_request(&url).await?.text().await?;
|
||||
|
||||
Ok(log)
|
||||
Ok(raw_log(&http, content).await?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,13 +8,11 @@ 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,
|
||||
_0x0::_0x0 as _0x0st, haste::Haste, mclogs::MCLogs, paste_gg::PasteGG, pastebin::PasteBin,
|
||||
};
|
||||
|
||||
#[path = "0x0.rs"]
|
||||
mod _0x0;
|
||||
mod attachment;
|
||||
mod haste;
|
||||
mod mclogs;
|
||||
mod paste_gg;
|
||||
|
@ -35,7 +33,6 @@ fn get_first_capture(regex: &Regex, string: &str) -> Option<String> {
|
|||
#[enum_dispatch(LogProvider)]
|
||||
enum Provider {
|
||||
_0x0st,
|
||||
Attachment,
|
||||
Haste,
|
||||
MCLogs,
|
||||
PasteGG,
|
||||
|
@ -44,9 +41,8 @@ enum Provider {
|
|||
|
||||
impl Provider {
|
||||
pub fn iterator() -> Iter<'static, Provider> {
|
||||
static PROVIDERS: [Provider; 6] = [
|
||||
static PROVIDERS: [Provider; 5] = [
|
||||
Provider::_0x0st(_0x0st),
|
||||
Provider::Attachment(Attachment),
|
||||
Provider::Haste(Haste),
|
||||
Provider::MCLogs(MCLogs),
|
||||
Provider::PasteBin(PasteBin),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{api, Data, Error};
|
||||
|
||||
use analyze_logs::handle_component_interaction;
|
||||
use log::{debug, info, trace};
|
||||
use poise::serenity_prelude::{ActivityData, Context, FullEvent, OnlineStatus};
|
||||
use poise::FrameworkContext;
|
||||
|
@ -33,6 +34,7 @@ pub async fn handle(
|
|||
FullEvent::InteractionCreate { interaction } => {
|
||||
if let Some(component_interaction) = interaction.as_message_component() {
|
||||
give_role::handle(ctx, component_interaction).await?;
|
||||
handle_component_interaction(ctx, component_interaction, data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +65,7 @@ pub async fn handle(
|
|||
|
||||
eta::handle(ctx, new_message).await?;
|
||||
expand_link::handle(ctx, &data.http_client, new_message).await?;
|
||||
analyze_logs::handle(ctx, new_message, data).await?;
|
||||
analyze_logs::handle_message(ctx, new_message, data).await?;
|
||||
}
|
||||
|
||||
FullEvent::ReactionAdd { add_reaction } => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use poise::serenity_prelude::{CreateEmbedAuthor, User};
|
||||
use poise::serenity_prelude::{Attachment, CreateEmbedAuthor, Message, User};
|
||||
|
||||
pub mod messages;
|
||||
|
||||
|
@ -15,3 +15,15 @@ pub fn semver_split(version: &str) -> Vec<u32> {
|
|||
.filter_map(|s| s.parse().ok())
|
||||
.collect::<Vec<u32>>()
|
||||
}
|
||||
|
||||
pub fn first_text_attachment(message: &Message) -> Option<&Attachment> {
|
||||
message
|
||||
.attachments
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
a.content_type
|
||||
.as_ref()
|
||||
.is_some_and(|content_type| content_type.starts_with("text/"))
|
||||
})
|
||||
.nth(0)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue