style: use tabs over spaces
This commit is contained in:
parent
f2979d4cde
commit
f0550dd429
41 changed files with 1112 additions and 1109 deletions
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
hard_tabs = true
|
66
build.rs
66
build.rs
|
@ -8,15 +8,15 @@ include!("src/tags.rs");
|
|||
/// generate the ChoiceParameter enum and tag data we will use in the `tag` command
|
||||
#[allow(dead_code)]
|
||||
fn main() {
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
let generated = Path::new(&out_dir).join("generated.rs");
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
let generated = Path::new(&out_dir).join("generated.rs");
|
||||
|
||||
let tag_files: Vec<String> = fs::read_dir(TAG_DIR)
|
||||
.unwrap()
|
||||
.map(|f| f.unwrap().file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
let tag_files: Vec<String> = fs::read_dir(TAG_DIR)
|
||||
.unwrap()
|
||||
.map(|f| f.unwrap().file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let tags: Vec<Tag> = tag_files
|
||||
let tags: Vec<Tag> = tag_files
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
|
@ -45,23 +45,23 @@ fn main() {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let formatted_names: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| t.file_name.replace(".md", "").replace('-', "_"))
|
||||
.collect();
|
||||
let formatted_names: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| t.file_name.replace(".md", "").replace('-', "_"))
|
||||
.collect();
|
||||
|
||||
let tag_choice = format!(
|
||||
r#"
|
||||
let tag_choice = format!(
|
||||
r#"
|
||||
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Debug, poise::ChoiceParameter)]
|
||||
pub enum TagChoice {{
|
||||
{}
|
||||
}}"#,
|
||||
formatted_names.join(",\n")
|
||||
);
|
||||
formatted_names.join(",\n")
|
||||
);
|
||||
|
||||
let to_str = format!(
|
||||
r#"
|
||||
let to_str = format!(
|
||||
r#"
|
||||
impl TagChoice {{
|
||||
fn as_str(&self) -> &str {{
|
||||
match &self {{
|
||||
|
@ -70,22 +70,22 @@ fn main() {
|
|||
}}
|
||||
}}
|
||||
"#,
|
||||
formatted_names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let file_name = n.replace('_', "-") + ".md";
|
||||
format!("Self::{n} => \"{file_name}\",")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
);
|
||||
formatted_names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let file_name = n.replace('_', "-") + ".md";
|
||||
format!("Self::{n} => \"{file_name}\",")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
let contents = Vec::from([tag_choice, to_str]).join("\n\n");
|
||||
let contents = Vec::from([tag_choice, to_str]).join("\n\n");
|
||||
|
||||
fs::write(generated, contents).unwrap();
|
||||
println!(
|
||||
"cargo:rustc-env=TAGS={}",
|
||||
// make sure we can deserialize with env! at runtime
|
||||
serde_json::to_string(&tags).unwrap()
|
||||
);
|
||||
fs::write(generated, contents).unwrap();
|
||||
println!(
|
||||
"cargo:rustc-env=TAGS={}",
|
||||
// make sure we can deserialize with env! at runtime
|
||||
serde_json::to_string(&tags).unwrap()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,15 +7,15 @@ use reqwest::StatusCode;
|
|||
const DADJOKE: &str = "https://icanhazdadjoke.com";
|
||||
|
||||
pub async fn get_joke() -> Result<String> {
|
||||
let req = REQWEST_CLIENT.get(DADJOKE).build()?;
|
||||
let req = REQWEST_CLIENT.get(DADJOKE).build()?;
|
||||
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
|
||||
if let StatusCode::OK = status {
|
||||
Ok(resp.text().await?)
|
||||
} else {
|
||||
Err(eyre!("Couldn't get a joke!"))
|
||||
}
|
||||
if let StatusCode::OK = status {
|
||||
Ok(resp.text().await?)
|
||||
} else {
|
||||
Err(eyre!("Couldn't get a joke!"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@ pub mod prism_meta;
|
|||
pub mod rory;
|
||||
|
||||
pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
||||
let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development");
|
||||
let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development");
|
||||
|
||||
format!("refraction/{version}")
|
||||
format!("refraction/{version}")
|
||||
});
|
||||
|
||||
pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(USER_AGENT.to_string())
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
reqwest::Client::builder()
|
||||
.user_agent(USER_AGENT.to_string())
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
|
|
@ -8,30 +8,32 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluralKitMessage {
|
||||
pub sender: String,
|
||||
pub sender: String,
|
||||
}
|
||||
|
||||
const PLURAL_KIT: &str = "https://api.pluralkit.me/v2";
|
||||
const MESSAGES_ENDPOINT: &str = "/messages";
|
||||
|
||||
pub async fn get_sender(message_id: MessageId) -> Result<UserId> {
|
||||
let req = REQWEST_CLIENT
|
||||
.get(format!("{PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id}"))
|
||||
.build()?;
|
||||
let req = REQWEST_CLIENT
|
||||
.get(format!("{PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id}"))
|
||||
.build()?;
|
||||
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp.json::<PluralKitMessage>().await?;
|
||||
let id: u64 = data.sender.parse().wrap_err_with(|| format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{data:#?}"))?;
|
||||
let sender = UserId::from(id);
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp.json::<PluralKitMessage>().await?;
|
||||
let id: u64 = data.sender.parse().wrap_err_with(|| {
|
||||
format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{data:#?}")
|
||||
})?;
|
||||
let sender = UserId::from(id);
|
||||
|
||||
Ok(sender)
|
||||
} else {
|
||||
Err(eyre!(
|
||||
Ok(sender)
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"Failed to get PluralKit message information from {PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id} with {status}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,39 +8,39 @@ use serde::{Deserialize, Serialize};
|
|||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinecraftPackageJson {
|
||||
pub format_version: u8,
|
||||
pub name: String,
|
||||
pub recommended: Vec<String>,
|
||||
pub uid: String,
|
||||
pub format_version: u8,
|
||||
pub name: String,
|
||||
pub recommended: Vec<String>,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
const PRISM_META: &str = "https://meta.prismlauncher.org/v1";
|
||||
const MINECRAFT_PACKAGEJSON_ENDPOINT: &str = "/net.minecraft/package.json";
|
||||
|
||||
pub async fn get_latest_minecraft_version() -> Result<String> {
|
||||
let req = REQWEST_CLIENT
|
||||
.get(format!("{PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT}"))
|
||||
.build()?;
|
||||
let req = REQWEST_CLIENT
|
||||
.get(format!("{PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT}"))
|
||||
.build()?;
|
||||
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT.execute(req).await?;
|
||||
let status = resp.status();
|
||||
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp
|
||||
.json::<MinecraftPackageJson>()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't parse Minecraft versions!")?;
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp
|
||||
.json::<MinecraftPackageJson>()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't parse Minecraft versions!")?;
|
||||
|
||||
let version = data
|
||||
.recommended
|
||||
.first()
|
||||
.ok_or_else(|| eyre!("Couldn't find latest version of Minecraft!"))?;
|
||||
let version = data
|
||||
.recommended
|
||||
.first()
|
||||
.ok_or_else(|| eyre!("Couldn't find latest version of Minecraft!"))?;
|
||||
|
||||
Ok(version.clone())
|
||||
} else {
|
||||
Err(eyre!(
|
||||
Ok(version.clone())
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"Failed to get latest Minecraft version from {PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT} with {status}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,40 +7,40 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RoryResponse {
|
||||
pub id: u64,
|
||||
pub url: String,
|
||||
pub error: Option<String>,
|
||||
pub id: u64,
|
||||
pub url: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
const RORY: &str = "https://rory.cat";
|
||||
const ENDPOINT: &str = "/purr";
|
||||
|
||||
pub async fn get_rory(id: Option<u64>) -> Result<RoryResponse> {
|
||||
let target = id.map(|id| id.to_string()).unwrap_or_default();
|
||||
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!")?;
|
||||
let req = REQWEST_CLIENT
|
||||
.get(format!("{RORY}{ENDPOINT}/{target}"))
|
||||
.build()
|
||||
.wrap_err_with(|| "Couldn't build reqwest client!")?;
|
||||
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT
|
||||
.execute(req)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't make request for rory!")?;
|
||||
debug!("Making request to {}", req.url());
|
||||
let resp = REQWEST_CLIENT
|
||||
.execute(req)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't make request for rory!")?;
|
||||
|
||||
let status = resp.status();
|
||||
let status = resp.status();
|
||||
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp
|
||||
.json::<RoryResponse>()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't parse the rory response!")?;
|
||||
if let StatusCode::OK = status {
|
||||
let data = resp
|
||||
.json::<RoryResponse>()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't parse the rory response!")?;
|
||||
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"Failed to get rory from {RORY}{ENDPOINT}/{target} with {status}",
|
||||
))
|
||||
}
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"Failed to get rory from {RORY}{ENDPOINT}/{target} with {status}",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use color_eyre::eyre::Result;
|
|||
/// It's a joke
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn joke(ctx: Context<'_>) -> Result<()> {
|
||||
let joke = dadjoke::get_joke().await?;
|
||||
let joke = dadjoke::get_joke().await?;
|
||||
|
||||
ctx.reply(joke).await?;
|
||||
Ok(())
|
||||
ctx.reply(joke).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,22 +5,22 @@ use color_eyre::eyre::{eyre, Result};
|
|||
/// Returns the number of members in the server
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn members(ctx: Context<'_>) -> Result<()> {
|
||||
let guild = ctx.guild().ok_or_else(|| eyre!("Couldn't fetch guild!"))?;
|
||||
let guild = ctx.guild().ok_or_else(|| eyre!("Couldn't fetch guild!"))?;
|
||||
|
||||
let count = guild.member_count;
|
||||
let online = if let Some(count) = guild.approximate_presence_count {
|
||||
count.to_string()
|
||||
} else {
|
||||
"Undefined".to_string()
|
||||
};
|
||||
let count = guild.member_count;
|
||||
let online = if let Some(count) = guild.approximate_presence_count {
|
||||
count.to_string()
|
||||
} else {
|
||||
"Undefined".to_string()
|
||||
};
|
||||
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
e.title(format!("{count} total members!"))
|
||||
.description(format!("{online} online members"))
|
||||
.color(consts::COLORS["blue"])
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
e.title(format!("{count} total members!"))
|
||||
.description(format!("{online} online members"))
|
||||
.color(consts::COLORS["blue"])
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,6 +5,6 @@ use color_eyre::eyre::Result;
|
|||
/// Replies with pong!
|
||||
#[poise::command(slash_command, prefix_command, ephemeral)]
|
||||
pub async fn ping(ctx: Context<'_>) -> Result<()> {
|
||||
ctx.reply("Pong!").await?;
|
||||
Ok(())
|
||||
ctx.reply("Pong!").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,24 +6,24 @@ use color_eyre::eyre::Result;
|
|||
/// Gets a Rory photo!
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn rory(
|
||||
ctx: Context<'_>,
|
||||
#[description = "specify a Rory ID"] id: Option<u64>,
|
||||
ctx: Context<'_>,
|
||||
#[description = "specify a Rory ID"] id: Option<u64>,
|
||||
) -> Result<()> {
|
||||
let rory = get_rory(id).await?;
|
||||
let rory = get_rory(id).await?;
|
||||
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
if let Some(error) = rory.error {
|
||||
e.title("Error!").description(error)
|
||||
} else {
|
||||
e.title("Rory :3")
|
||||
.url(&rory.url)
|
||||
.image(rory.url)
|
||||
.footer(|f| f.text(format!("ID {}", rory.id)))
|
||||
}
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
if let Some(error) = rory.error {
|
||||
e.title("Error!").description(error)
|
||||
} else {
|
||||
e.title("Rory :3")
|
||||
.url(&rory.url)
|
||||
.image(rory.url)
|
||||
.footer(|f| f.text(format!("ID {}", rory.id)))
|
||||
}
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,48 +4,48 @@ use color_eyre::eyre::{eyre, Result};
|
|||
|
||||
/// Say something through the bot
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
ephemeral,
|
||||
default_member_permissions = "MODERATE_MEMBERS",
|
||||
required_permissions = "MODERATE_MEMBERS"
|
||||
slash_command,
|
||||
prefix_command,
|
||||
ephemeral,
|
||||
default_member_permissions = "MODERATE_MEMBERS",
|
||||
required_permissions = "MODERATE_MEMBERS"
|
||||
)]
|
||||
pub async fn say(ctx: Context<'_>, #[description = "Just content?"] content: String) -> Result<()> {
|
||||
let guild = ctx.guild().ok_or_else(|| eyre!("Couldn't get guild!"))?;
|
||||
let channel = ctx
|
||||
.guild_channel()
|
||||
.await
|
||||
.ok_or_else(|| eyre!("Couldn't get channel!"))?;
|
||||
let guild = ctx.guild().ok_or_else(|| eyre!("Couldn't get guild!"))?;
|
||||
let channel = ctx
|
||||
.guild_channel()
|
||||
.await
|
||||
.ok_or_else(|| eyre!("Couldn't get channel!"))?;
|
||||
|
||||
ctx.defer_ephemeral().await?;
|
||||
channel.say(ctx, &content).await?;
|
||||
ctx.say("I said what you said!").await?;
|
||||
ctx.defer_ephemeral().await?;
|
||||
channel.say(ctx, &content).await?;
|
||||
ctx.say("I said what you said!").await?;
|
||||
|
||||
if let Some(channel_id) = ctx.data().config.discord.channels.say_log_channel_id {
|
||||
let log_channel = guild
|
||||
.channels
|
||||
.iter()
|
||||
.find(|c| c.0 == &channel_id)
|
||||
.ok_or_else(|| eyre!("Couldn't get log channel from guild!"))?;
|
||||
if let Some(channel_id) = ctx.data().config.discord.channels.say_log_channel_id {
|
||||
let log_channel = guild
|
||||
.channels
|
||||
.iter()
|
||||
.find(|c| c.0 == &channel_id)
|
||||
.ok_or_else(|| eyre!("Couldn't get log channel from guild!"))?;
|
||||
|
||||
log_channel
|
||||
.1
|
||||
.clone()
|
||||
.guild()
|
||||
.ok_or_else(|| eyre!("Couldn't cast channel we found from guild as GuildChannel?????"))?
|
||||
.send_message(ctx, |m| {
|
||||
m.embed(|e| {
|
||||
e.title("Say command used!")
|
||||
.description(content)
|
||||
.author(|a| {
|
||||
a.name(ctx.author().tag()).icon_url(
|
||||
ctx.author().avatar_url().unwrap_or("undefined".to_string()),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
log_channel
|
||||
.1
|
||||
.clone()
|
||||
.guild()
|
||||
.ok_or_else(|| eyre!("Couldn't cast channel we found from guild as GuildChannel?????"))?
|
||||
.send_message(ctx, |m| {
|
||||
m.embed(|e| {
|
||||
e.title("Say command used!")
|
||||
.description(content)
|
||||
.author(|a| {
|
||||
a.name(ctx.author().tag()).icon_url(
|
||||
ctx.author().avatar_url().unwrap_or("undefined".to_string()),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,27 +5,27 @@ use color_eyre::eyre::{Context as _, Result};
|
|||
/// Returns GitHub stargazer count
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn stars(ctx: Context<'_>) -> Result<()> {
|
||||
let prismlauncher = ctx
|
||||
.data()
|
||||
.octocrab
|
||||
.repos("PrismLauncher", "PrismLauncher")
|
||||
.get()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't get PrismLauncher/PrismLauncher from GitHub!")?;
|
||||
let prismlauncher = ctx
|
||||
.data()
|
||||
.octocrab
|
||||
.repos("PrismLauncher", "PrismLauncher")
|
||||
.get()
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't get PrismLauncher/PrismLauncher from GitHub!")?;
|
||||
|
||||
let count = if let Some(count) = prismlauncher.stargazers_count {
|
||||
count.to_string()
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
let count = if let Some(count) = prismlauncher.stargazers_count {
|
||||
count.to_string()
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
e.title(format!("⭐ {count} total stars!"))
|
||||
.color(COLORS["yellow"])
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
e.title(format!("⭐ {count} total stars!"))
|
||||
.color(COLORS["yellow"])
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -13,48 +13,48 @@ static TAGS: Lazy<Vec<Tag>> = Lazy::new(|| serde_json::from_str(env!("TAGS")).un
|
|||
/// Send a tag
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn tag(
|
||||
ctx: Context<'_>,
|
||||
#[description = "the copypasta you want to send"] name: TagChoice,
|
||||
user: Option<User>,
|
||||
ctx: Context<'_>,
|
||||
#[description = "the copypasta you want to send"] name: TagChoice,
|
||||
user: Option<User>,
|
||||
) -> Result<()> {
|
||||
let tag_file = name.as_str();
|
||||
let tag = TAGS
|
||||
.iter()
|
||||
.find(|t| t.file_name == tag_file)
|
||||
.ok_or_else(|| eyre!("Tried to get non-existent tag: {tag_file}"))?;
|
||||
let tag_file = name.as_str();
|
||||
let tag = TAGS
|
||||
.iter()
|
||||
.find(|t| t.file_name == tag_file)
|
||||
.ok_or_else(|| eyre!("Tried to get non-existent tag: {tag_file}"))?;
|
||||
|
||||
let frontmatter = &tag.frontmatter;
|
||||
let frontmatter = &tag.frontmatter;
|
||||
|
||||
ctx.send(|m| {
|
||||
if let Some(user) = user {
|
||||
m.content(format!("<@{}>", user.id));
|
||||
}
|
||||
ctx.send(|m| {
|
||||
if let Some(user) = user {
|
||||
m.content(format!("<@{}>", user.id));
|
||||
}
|
||||
|
||||
m.embed(|e| {
|
||||
e.title(&frontmatter.title);
|
||||
e.description(&tag.content);
|
||||
m.embed(|e| {
|
||||
e.title(&frontmatter.title);
|
||||
e.description(&tag.content);
|
||||
|
||||
if let Some(color) = &frontmatter.color {
|
||||
let color = *consts::COLORS
|
||||
.get(color.as_str())
|
||||
.unwrap_or(&Color::default());
|
||||
e.color(color);
|
||||
}
|
||||
if let Some(color) = &frontmatter.color {
|
||||
let color = *consts::COLORS
|
||||
.get(color.as_str())
|
||||
.unwrap_or(&Color::default());
|
||||
e.color(color);
|
||||
}
|
||||
|
||||
if let Some(image) = &frontmatter.image {
|
||||
e.image(image);
|
||||
}
|
||||
if let Some(image) = &frontmatter.image {
|
||||
e.image(image);
|
||||
}
|
||||
|
||||
if let Some(fields) = &frontmatter.fields {
|
||||
for field in fields {
|
||||
e.field(&field.name, &field.value, field.inline);
|
||||
}
|
||||
}
|
||||
if let Some(fields) = &frontmatter.fields {
|
||||
for field in fields {
|
||||
e.field(&field.name, &field.value, field.inline);
|
||||
}
|
||||
}
|
||||
|
||||
e
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
e
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@ use poise::Command;
|
|||
mod general;
|
||||
|
||||
pub fn to_global_commands() -> Vec<Command<Data, Report>> {
|
||||
vec![
|
||||
general::joke(),
|
||||
general::members(),
|
||||
general::ping(),
|
||||
general::rory(),
|
||||
general::say(),
|
||||
general::stars(),
|
||||
general::tag(),
|
||||
]
|
||||
vec![
|
||||
general::joke(),
|
||||
general::members(),
|
||||
general::ping(),
|
||||
general::rory(),
|
||||
general::say(),
|
||||
general::stars(),
|
||||
general::tag(),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,81 +7,81 @@ use url::Url;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefractionOAuth2 {
|
||||
pub redirect_uri: Url,
|
||||
pub scope: String,
|
||||
pub redirect_uri: Url,
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RefractionChannels {
|
||||
pub say_log_channel_id: Option<ChannelId>,
|
||||
pub say_log_channel_id: Option<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiscordConfig {
|
||||
pub client_id: ApplicationId,
|
||||
pub client_secret: String,
|
||||
pub bot_token: String,
|
||||
pub oauth2: RefractionOAuth2,
|
||||
pub channels: RefractionChannels,
|
||||
pub client_id: ApplicationId,
|
||||
pub client_secret: String,
|
||||
pub bot_token: String,
|
||||
pub oauth2: RefractionOAuth2,
|
||||
pub channels: RefractionChannels,
|
||||
}
|
||||
|
||||
impl Default for RefractionOAuth2 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scope: "identify connections role_connections.write".to_string(),
|
||||
redirect_uri: Url::parse("https://google.com").unwrap(),
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scope: "identify connections role_connections.write".to_string(),
|
||||
redirect_uri: Url::parse("https://google.com").unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RefractionOAuth2 {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed = format!("{}/oauth2/callback", required_var!("PUBLIC_URI"));
|
||||
let redirect_uri = Url::parse(&unparsed)?;
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed = format!("{}/oauth2/callback", required_var!("PUBLIC_URI"));
|
||||
let redirect_uri = Url::parse(&unparsed)?;
|
||||
|
||||
debug!("OAuth2 Redirect URI is {redirect_uri}");
|
||||
Ok(Self {
|
||||
redirect_uri,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
debug!("OAuth2 Redirect URI is {redirect_uri}");
|
||||
Ok(Self {
|
||||
redirect_uri,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RefractionChannels {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed = std::env::var("DISCORD_SAY_LOG_CHANNELID");
|
||||
if let Ok(unparsed) = unparsed {
|
||||
let id = unparsed.parse::<u64>()?;
|
||||
let channel_id = ChannelId::from(id);
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed = std::env::var("DISCORD_SAY_LOG_CHANNELID");
|
||||
if let Ok(unparsed) = unparsed {
|
||||
let id = unparsed.parse::<u64>()?;
|
||||
let channel_id = ChannelId::from(id);
|
||||
|
||||
debug!("Log channel is {id}");
|
||||
Ok(Self {
|
||||
say_log_channel_id: Some(channel_id),
|
||||
})
|
||||
} else {
|
||||
warn!("DISCORD_SAY_LOG_CHANNELID is empty; this will disable logging in your server.");
|
||||
Ok(Self {
|
||||
say_log_channel_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
debug!("Log channel is {id}");
|
||||
Ok(Self {
|
||||
say_log_channel_id: Some(channel_id),
|
||||
})
|
||||
} else {
|
||||
warn!("DISCORD_SAY_LOG_CHANNELID is empty; this will disable logging in your server.");
|
||||
Ok(Self {
|
||||
say_log_channel_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscordConfig {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed_client = required_var!("DISCORD_CLIENT_ID").parse::<u64>()?;
|
||||
let client_id = ApplicationId::from(unparsed_client);
|
||||
let client_secret = required_var!("DISCORD_CLIENT_SECRET");
|
||||
let bot_token = required_var!("DISCORD_BOT_TOKEN");
|
||||
let oauth2 = RefractionOAuth2::new_from_env()?;
|
||||
let channels = RefractionChannels::new_from_env()?;
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let unparsed_client = required_var!("DISCORD_CLIENT_ID").parse::<u64>()?;
|
||||
let client_id = ApplicationId::from(unparsed_client);
|
||||
let client_secret = required_var!("DISCORD_CLIENT_SECRET");
|
||||
let bot_token = required_var!("DISCORD_BOT_TOKEN");
|
||||
let oauth2 = RefractionOAuth2::new_from_env()?;
|
||||
let channels = RefractionChannels::new_from_env()?;
|
||||
|
||||
Ok(Self {
|
||||
client_id,
|
||||
client_secret,
|
||||
bot_token,
|
||||
oauth2,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
Ok(Self {
|
||||
client_id,
|
||||
client_secret,
|
||||
bot_token,
|
||||
oauth2,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,62 +4,62 @@ use crate::required_var;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefractionRepo {
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GithubConfig {
|
||||
pub token: String,
|
||||
pub repos: Vec<RefractionRepo>,
|
||||
pub cache_sec: u16,
|
||||
pub update_cron_job: String,
|
||||
pub token: String,
|
||||
pub repos: Vec<RefractionRepo>,
|
||||
pub cache_sec: u16,
|
||||
pub update_cron_job: String,
|
||||
}
|
||||
|
||||
impl Default for GithubConfig {
|
||||
fn default() -> Self {
|
||||
let owner = "PrismLauncher".to_string();
|
||||
let repos = Vec::<RefractionRepo>::from([
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "PrismLauncher".to_string(),
|
||||
key: "launcher".to_string(),
|
||||
name: "Launcher contributor".to_string(),
|
||||
},
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "prismlauncher.org".to_string(),
|
||||
fn default() -> Self {
|
||||
let owner = "PrismLauncher".to_string();
|
||||
let repos = Vec::<RefractionRepo>::from([
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "PrismLauncher".to_string(),
|
||||
key: "launcher".to_string(),
|
||||
name: "Launcher contributor".to_string(),
|
||||
},
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "prismlauncher.org".to_string(),
|
||||
|
||||
key: "website".to_string(),
|
||||
name: "Web developer".to_string(),
|
||||
},
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "Translations".to_string(),
|
||||
key: "website".to_string(),
|
||||
name: "Web developer".to_string(),
|
||||
},
|
||||
RefractionRepo {
|
||||
owner: owner.clone(),
|
||||
repo: "Translations".to_string(),
|
||||
|
||||
key: "translations".to_string(),
|
||||
name: "Translator".to_string(),
|
||||
},
|
||||
]);
|
||||
key: "translations".to_string(),
|
||||
name: "Translator".to_string(),
|
||||
},
|
||||
]);
|
||||
|
||||
Self {
|
||||
repos,
|
||||
cache_sec: 3600,
|
||||
update_cron_job: "0 */10 * * * *".to_string(), // every 10 minutes
|
||||
token: String::default(),
|
||||
}
|
||||
}
|
||||
Self {
|
||||
repos,
|
||||
cache_sec: 3600,
|
||||
update_cron_job: "0 */10 * * * *".to_string(), // every 10 minutes
|
||||
token: String::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GithubConfig {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let token = required_var!("GITHUB_TOKEN");
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let token = required_var!("GITHUB_TOKEN");
|
||||
|
||||
Ok(Self {
|
||||
token,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
Ok(Self {
|
||||
token,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,32 +8,32 @@ pub use github::*;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub discord: DiscordConfig,
|
||||
pub github: GithubConfig,
|
||||
pub http_port: u16,
|
||||
pub redis_url: String,
|
||||
pub discord: DiscordConfig,
|
||||
pub github: GithubConfig,
|
||||
pub http_port: u16,
|
||||
pub redis_url: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
discord: DiscordConfig::default(),
|
||||
github: GithubConfig::default(),
|
||||
http_port: 3000,
|
||||
redis_url: "redis://localhost:6379".to_string(),
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
discord: DiscordConfig::default(),
|
||||
github: GithubConfig::default(),
|
||||
http_port: 3000,
|
||||
redis_url: "redis://localhost:6379".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let discord = DiscordConfig::new_from_env()?;
|
||||
let github = GithubConfig::new_from_env()?;
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let discord = DiscordConfig::new_from_env()?;
|
||||
let github = GithubConfig::new_from_env()?;
|
||||
|
||||
Ok(Self {
|
||||
discord,
|
||||
github,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
Ok(Self {
|
||||
discord,
|
||||
github,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,31 +4,31 @@ use once_cell::sync::Lazy;
|
|||
use poise::serenity_prelude::Color;
|
||||
|
||||
pub static COLORS: Lazy<HashMap<&str, Color>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
("red", Color::from((239, 68, 68))),
|
||||
("green", Color::from((34, 197, 94))),
|
||||
("blue", Color::from((96, 165, 250))),
|
||||
("yellow", Color::from((253, 224, 71))),
|
||||
("orange", Color::from((251, 146, 60))),
|
||||
// TODO purple & pink :D
|
||||
])
|
||||
HashMap::from([
|
||||
("red", Color::from((239, 68, 68))),
|
||||
("green", Color::from((34, 197, 94))),
|
||||
("blue", Color::from((96, 165, 250))),
|
||||
("yellow", Color::from((253, 224, 71))),
|
||||
("orange", Color::from((251, 146, 60))),
|
||||
// TODO purple & pink :D
|
||||
])
|
||||
});
|
||||
|
||||
pub const ETA_MESSAGES: [&str; 16] = [
|
||||
"Sometime",
|
||||
"Some day",
|
||||
"Not far",
|
||||
"The future",
|
||||
"Never",
|
||||
"Perhaps tomorrow?",
|
||||
"There are no ETAs",
|
||||
"No",
|
||||
"Nah",
|
||||
"Yes",
|
||||
"Yas",
|
||||
"Next month",
|
||||
"Next year",
|
||||
"Next week",
|
||||
"In Prism Launcher 2.0.0",
|
||||
"At the appropriate juncture, in due course, in the fullness of time",
|
||||
"Sometime",
|
||||
"Some day",
|
||||
"Not far",
|
||||
"The future",
|
||||
"Never",
|
||||
"Perhaps tomorrow?",
|
||||
"There are no ETAs",
|
||||
"No",
|
||||
"Nah",
|
||||
"Yes",
|
||||
"Yas",
|
||||
"Next month",
|
||||
"Next year",
|
||||
"Next week",
|
||||
"In Prism Launcher 2.0.0",
|
||||
"At the appropriate juncture, in due course, in the fullness of time",
|
||||
];
|
||||
|
|
|
@ -7,43 +7,43 @@ use poise::serenity_prelude::Timestamp;
|
|||
use poise::FrameworkError;
|
||||
|
||||
pub async fn handle(error: FrameworkError<'_, Data, Report>) {
|
||||
match error {
|
||||
FrameworkError::Setup {
|
||||
error, framework, ..
|
||||
} => {
|
||||
error!("Error setting up client! Bailing out");
|
||||
framework.shard_manager().lock().await.shutdown_all().await;
|
||||
match error {
|
||||
FrameworkError::Setup {
|
||||
error, framework, ..
|
||||
} => {
|
||||
error!("Error setting up client! Bailing out");
|
||||
framework.shard_manager().lock().await.shutdown_all().await;
|
||||
|
||||
panic!("{error}")
|
||||
}
|
||||
panic!("{error}")
|
||||
}
|
||||
|
||||
FrameworkError::Command { error, ctx } => {
|
||||
error!("Error in command {}:\n{error:?}", ctx.command().name);
|
||||
ctx.send(|c| {
|
||||
c.embed(|e| {
|
||||
e.title("Something went wrong!")
|
||||
.description("oopsie")
|
||||
.timestamp(Timestamp::now())
|
||||
.color(COLORS["red"])
|
||||
})
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
FrameworkError::Command { error, ctx } => {
|
||||
error!("Error in command {}:\n{error:?}", ctx.command().name);
|
||||
ctx.send(|c| {
|
||||
c.embed(|e| {
|
||||
e.title("Something went wrong!")
|
||||
.description("oopsie")
|
||||
.timestamp(Timestamp::now())
|
||||
.color(COLORS["red"])
|
||||
})
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
FrameworkError::EventHandler {
|
||||
error,
|
||||
ctx: _,
|
||||
event,
|
||||
framework: _,
|
||||
} => {
|
||||
error!("Error while handling event {}:\n{error:?}", event.name());
|
||||
}
|
||||
FrameworkError::EventHandler {
|
||||
error,
|
||||
ctx: _,
|
||||
event,
|
||||
framework: _,
|
||||
} => {
|
||||
error!("Error while handling event {}:\n{error:?}", event.name());
|
||||
}
|
||||
|
||||
error => {
|
||||
if let Err(e) = poise::builtins::on_error(error).await {
|
||||
error!("Unhandled error occured:\n{e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
error => {
|
||||
if let Err(e) = poise::builtins::on_error(error).await {
|
||||
error!("Unhandled error occured:\n{e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,164 +7,164 @@ use regex::Regex;
|
|||
pub type Issue = Option<(String, String)>;
|
||||
|
||||
pub async fn find_issues(log: &str, data: &Data) -> Result<Vec<(String, String)>> {
|
||||
let issues = [
|
||||
fabric_internal,
|
||||
flatpak_nvidia,
|
||||
forge_java,
|
||||
intel_hd,
|
||||
java_option,
|
||||
lwjgl_2_java_9,
|
||||
macos_ns,
|
||||
oom,
|
||||
optinotfine,
|
||||
pre_1_12_native_transport_java_9,
|
||||
wrong_java,
|
||||
];
|
||||
let issues = [
|
||||
fabric_internal,
|
||||
flatpak_nvidia,
|
||||
forge_java,
|
||||
intel_hd,
|
||||
java_option,
|
||||
lwjgl_2_java_9,
|
||||
macos_ns,
|
||||
oom,
|
||||
optinotfine,
|
||||
pre_1_12_native_transport_java_9,
|
||||
wrong_java,
|
||||
];
|
||||
|
||||
let mut res: Vec<(String, String)> = issues.iter().filter_map(|issue| issue(log)).collect();
|
||||
let mut res: Vec<(String, String)> = issues.iter().filter_map(|issue| issue(log)).collect();
|
||||
|
||||
if let Some(issues) = outdated_launcher(log, data).await? {
|
||||
res.push(issues)
|
||||
}
|
||||
if let Some(issues) = outdated_launcher(log, data).await? {
|
||||
res.push(issues)
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn fabric_internal(log: &str) -> Issue {
|
||||
const CLASS_NOT_FOUND: &str = "Caused by: java.lang.ClassNotFoundException: ";
|
||||
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 \
|
||||
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(),
|
||||
);
|
||||
.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 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)
|
||||
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.
|
||||
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(),
|
||||
);
|
||||
.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");
|
||||
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)
|
||||
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+.
|
||||
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(),
|
||||
);
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V");
|
||||
found.then_some(issue)
|
||||
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 =
|
||||
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)
|
||||
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());
|
||||
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) = 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]),
|
||||
));
|
||||
}
|
||||
if let Some(captures) = OPTION_REGEX.captures(log) {
|
||||
return Some((
|
||||
"Wrong Java Arguments".to_string(),
|
||||
format!("Remove `{}` from your Java arguments", &captures[1]),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
None
|
||||
}
|
||||
|
||||
fn lwjgl_2_java_9(log: &str) -> Issue {
|
||||
let issue = (
|
||||
"Linux: crash with pre-1.13 and Java 9+".to_string(),
|
||||
"Using pre-1.13 (which uses LWJGL 2) with Java 9 or later usually causes a crash. \
|
||||
let issue = (
|
||||
"Linux: crash with pre-1.13 and Java 9+".to_string(),
|
||||
"Using pre-1.13 (which uses LWJGL 2) with Java 9 or later usually causes a crash. \
|
||||
Switching to Java 8 or below will fix your issue.
|
||||
Alternatively, you can use [Temurin](https://adoptium.net/temurin/releases). \
|
||||
However, multiplayer will not work in versions from 1.8 to 1.11.
|
||||
For more information, type `/tag java`."
|
||||
.to_string(),
|
||||
);
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let found = log.contains("check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!");
|
||||
found.then_some(issue)
|
||||
let found = log.contains("check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!");
|
||||
found.then_some(issue)
|
||||
}
|
||||
|
||||
fn macos_ns(log: &str) -> Issue {
|
||||
let 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)
|
||||
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 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)
|
||||
let found = log.contains("java.lang.OutOfMemoryError");
|
||||
found.then_some(issue)
|
||||
}
|
||||
|
||||
fn optinotfine(log: &str) -> Issue {
|
||||
let 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.
|
||||
|
@ -172,50 +172,50 @@ fn optinotfine(log: &str) -> Issue {
|
|||
.to_string(),
|
||||
);
|
||||
|
||||
let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-");
|
||||
found.then_some(issue)
|
||||
let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-");
|
||||
found.then_some(issue)
|
||||
}
|
||||
|
||||
async fn outdated_launcher(log: &str, data: &Data) -> Result<Issue> {
|
||||
static OUTDATED_LAUNCHER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new("Prism Launcher version: [0-9].[0-9].[0-9]").unwrap());
|
||||
static OUTDATED_LAUNCHER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new("Prism Launcher version: [0-9].[0-9].[0-9]").unwrap());
|
||||
|
||||
let Some(captures) = OUTDATED_LAUNCHER_REGEX.captures(log) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(captures) = OUTDATED_LAUNCHER_REGEX.captures(log) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let version_from_log = captures[0].replace("Prism Launcher version: ", "");
|
||||
let version_from_log = captures[0].replace("Prism Launcher version: ", "");
|
||||
|
||||
let storage = &data.storage;
|
||||
let latest_version = if storage.launcher_version_is_cached().await? {
|
||||
storage.get_launcher_version().await?
|
||||
} else {
|
||||
let version = data
|
||||
.octocrab
|
||||
.repos("PrismLauncher", "PrismLauncher")
|
||||
.releases()
|
||||
.get_latest()
|
||||
.await?
|
||||
.tag_name;
|
||||
let storage = &data.storage;
|
||||
let latest_version = if storage.launcher_version_is_cached().await? {
|
||||
storage.get_launcher_version().await?
|
||||
} else {
|
||||
let version = data
|
||||
.octocrab
|
||||
.repos("PrismLauncher", "PrismLauncher")
|
||||
.releases()
|
||||
.get_latest()
|
||||
.await?
|
||||
.tag_name;
|
||||
|
||||
storage.cache_launcher_version(&version).await?;
|
||||
version
|
||||
};
|
||||
storage.cache_launcher_version(&version).await?;
|
||||
version
|
||||
};
|
||||
|
||||
if version_from_log < latest_version {
|
||||
let issue = (
|
||||
if version_from_log < latest_version {
|
||||
let issue = (
|
||||
"Outdated Prism Launcher".to_string(),
|
||||
format!("Your installed version is {version_from_log}, while the newest version is {latest_version}.\nPlease update, for more info see https://prismlauncher.org/download/")
|
||||
);
|
||||
|
||||
Ok(Some(issue))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(Some(issue))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_1_12_native_transport_java_9(log: &str) -> Issue {
|
||||
let issue = (
|
||||
let issue = (
|
||||
"Linux: broken multiplayer with 1.8-1.11 and Java 9+".to_string(),
|
||||
"These versions of Minecraft use an outdated version of Netty which does not properly support Java 9.
|
||||
|
||||
|
@ -229,33 +229,33 @@ which is why the issue was not present."
|
|||
.to_string(),
|
||||
);
|
||||
|
||||
let found = log.contains(
|
||||
let found = log.contains(
|
||||
"java.lang.RuntimeException: Unable to access address of buffer\n\tat io.netty.channel.epoll"
|
||||
);
|
||||
|
||||
found.then_some(issue)
|
||||
found.then_some(issue)
|
||||
}
|
||||
|
||||
fn wrong_java(log: &str) -> Issue {
|
||||
static SWITCH_VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
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((
|
||||
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 = (
|
||||
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)
|
||||
log.contains("Java major version is incompatible. Things might break.")
|
||||
.then_some(issue)
|
||||
}
|
||||
|
|
|
@ -12,57 +12,57 @@ use issues::find_issues;
|
|||
use providers::find_log;
|
||||
|
||||
pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> {
|
||||
let channel = message.channel_id;
|
||||
let channel = message.channel_id;
|
||||
|
||||
let log = find_log(message).await;
|
||||
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?;
|
||||
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(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(log) = log? else {
|
||||
debug!("No log found in message! Skipping analysis");
|
||||
return Ok(());
|
||||
};
|
||||
let Some(log) = log? else {
|
||||
debug!("No log found in message! Skipping analysis");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let issues = find_issues(&log, data).await?;
|
||||
let issues = find_issues(&log, data).await?;
|
||||
|
||||
channel
|
||||
.send_message(ctx, |m| {
|
||||
m.reference_message(message)
|
||||
.allowed_mentions(|am| am.replied_user(true))
|
||||
.embed(|e| {
|
||||
e.title("Log analysis");
|
||||
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"]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (title, description) in issues {
|
||||
e.field(title, description, false);
|
||||
}
|
||||
}
|
||||
|
||||
e
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
e
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,17 +8,17 @@ 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 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();
|
||||
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}",))
|
||||
}
|
||||
if let StatusCode::OK = status {
|
||||
Ok(Some(response.text().await?))
|
||||
} else {
|
||||
Err(eyre!("Failed to fetch paste from {url} with {status}",))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@ 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,21 +6,21 @@ use regex::Regex;
|
|||
use reqwest::StatusCode;
|
||||
|
||||
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>> {
|
||||
let Some(captures) = REGEX.captures(content) else {
|
||||
return Ok(None);
|
||||
};
|
||||
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();
|
||||
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}"))
|
||||
}
|
||||
if let StatusCode::OK = status {
|
||||
Ok(Some(response.text().await?))
|
||||
} else {
|
||||
Err(eyre!("Failed to fetch paste from {url} with {status}"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,18 +8,18 @@ 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 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();
|
||||
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}"))
|
||||
}
|
||||
if let StatusCode::OK = status {
|
||||
Ok(Some(response.text().await?))
|
||||
} else {
|
||||
Err(eyre!("Failed to fetch log from {url} with {status}"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,22 +12,22 @@ 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);
|
||||
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));
|
||||
}
|
||||
if let Some(content) = attachment::find(message).await? {
|
||||
return Ok(Some(content));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(None)
|
||||
}
|
||||
|
|
|
@ -12,59 +12,59 @@ static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct PasteResponse {
|
||||
status: String,
|
||||
result: Option<Vec<PasteResult>>,
|
||||
error: Option<String>,
|
||||
message: Option<String>,
|
||||
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>,
|
||||
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 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 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();
|
||||
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}!"
|
||||
));
|
||||
}
|
||||
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 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 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();
|
||||
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}!"
|
||||
));
|
||||
}
|
||||
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?;
|
||||
let text = resp.text().await?;
|
||||
|
||||
Ok(Some(text))
|
||||
Ok(Some(text))
|
||||
}
|
||||
|
|
|
@ -6,21 +6,21 @@ use regex::Regex;
|
|||
use reqwest::StatusCode;
|
||||
|
||||
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>> {
|
||||
let Some(captures) = REGEX.captures(content) else {
|
||||
return Ok(None);
|
||||
};
|
||||
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();
|
||||
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}"))
|
||||
}
|
||||
if let StatusCode::OK = status {
|
||||
Ok(Some(response.text().await?))
|
||||
} else {
|
||||
Err(eyre!("Failed to fetch paste from {url} with {status}"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,24 @@ use color_eyre::eyre::{Context as _, Result};
|
|||
use poise::serenity_prelude::{Context, InteractionType, Reaction};
|
||||
|
||||
pub async fn handle(ctx: &Context, reaction: &Reaction) -> Result<()> {
|
||||
let user = reaction
|
||||
.user(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't fetch user from reaction!")?;
|
||||
let user = reaction
|
||||
.user(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't fetch user from reaction!")?;
|
||||
|
||||
let message = reaction
|
||||
.message(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't fetch message from reaction!")?;
|
||||
let message = reaction
|
||||
.message(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't fetch message from reaction!")?;
|
||||
|
||||
if let Some(interaction) = &message.interaction {
|
||||
if interaction.kind == InteractionType::ApplicationCommand
|
||||
&& interaction.user == user
|
||||
&& reaction.emoji.unicode_eq("❌")
|
||||
{
|
||||
message.delete(ctx).await?;
|
||||
}
|
||||
}
|
||||
if let Some(interaction) = &message.interaction {
|
||||
if interaction.kind == InteractionType::ApplicationCommand
|
||||
&& interaction.user == user
|
||||
&& reaction.emoji.unicode_eq("❌")
|
||||
{
|
||||
message.delete(ctx).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,15 +8,15 @@ use regex::Regex;
|
|||
static ETA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\beta\b").unwrap());
|
||||
|
||||
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
|
||||
if !ETA_REGEX.is_match(&message.content) {
|
||||
return Ok(());
|
||||
}
|
||||
if !ETA_REGEX.is_match(&message.content) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = format!(
|
||||
"{} <:pofat:1031701005559144458>",
|
||||
utils::random_choice(consts::ETA_MESSAGES)?
|
||||
);
|
||||
let response = format!(
|
||||
"{} <:pofat:1031701005559144458>",
|
||||
utils::random_choice(consts::ETA_MESSAGES)?
|
||||
);
|
||||
|
||||
message.reply(ctx, response).await?;
|
||||
Ok(())
|
||||
message.reply(ctx, response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,25 +4,25 @@ use poise::serenity_prelude::{Context, Message};
|
|||
use crate::utils;
|
||||
|
||||
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
|
||||
let embeds = utils::resolve_message(ctx, message).await?;
|
||||
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 = message
|
||||
.channel(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't get channel from message!")?
|
||||
.guild()
|
||||
.ok_or_else(|| eyre!("Couldn't convert to GuildChannel!"))?;
|
||||
// TOOD getchoo: actually reply to user
|
||||
// ...not sure why Message doesn't give me a builder in reply() or equivalents
|
||||
let our_channel = message
|
||||
.channel(ctx)
|
||||
.await
|
||||
.wrap_err_with(|| "Couldn't get channel from message!")?
|
||||
.guild()
|
||||
.ok_or_else(|| eyre!("Couldn't convert to GuildChannel!"))?;
|
||||
|
||||
if !embeds.is_empty() {
|
||||
our_channel
|
||||
.send_message(ctx, |m| {
|
||||
m.set_embeds(embeds)
|
||||
.allowed_mentions(|am| am.replied_user(false))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
if !embeds.is_empty() {
|
||||
our_channel
|
||||
.send_message(ctx, |m| {
|
||||
m.set_embeds(embeds)
|
||||
.allowed_mentions(|am| am.replied_user(false))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -13,53 +13,53 @@ pub mod pluralkit;
|
|||
mod support_onboard;
|
||||
|
||||
pub async fn handle(
|
||||
ctx: &Context,
|
||||
event: &Event<'_>,
|
||||
_framework: FrameworkContext<'_, Data, Report>,
|
||||
data: &Data,
|
||||
ctx: &Context,
|
||||
event: &Event<'_>,
|
||||
_framework: FrameworkContext<'_, Data, Report>,
|
||||
data: &Data,
|
||||
) -> Result<()> {
|
||||
match event {
|
||||
Event::Ready { data_about_bot } => {
|
||||
info!("Logged in as {}!", data_about_bot.user.name);
|
||||
match event {
|
||||
Event::Ready { data_about_bot } => {
|
||||
info!("Logged in as {}!", data_about_bot.user.name);
|
||||
|
||||
let latest_minecraft_version = api::prism_meta::get_latest_minecraft_version().await?;
|
||||
let activity = Activity::playing(format!("Minecraft {}", latest_minecraft_version));
|
||||
let latest_minecraft_version = api::prism_meta::get_latest_minecraft_version().await?;
|
||||
let activity = Activity::playing(format!("Minecraft {}", latest_minecraft_version));
|
||||
|
||||
info!("Setting presence to activity {activity:#?}");
|
||||
ctx.set_presence(Some(activity), OnlineStatus::Online).await;
|
||||
}
|
||||
info!("Setting presence to activity {activity:#?}");
|
||||
ctx.set_presence(Some(activity), OnlineStatus::Online).await;
|
||||
}
|
||||
|
||||
Event::Message { new_message } => {
|
||||
// ignore new messages from bots
|
||||
// NOTE: the webhook_id check allows us to still respond to PK users
|
||||
if new_message.author.bot && new_message.webhook_id.is_none() {
|
||||
debug!("Ignoring message {} from bot", new_message.id);
|
||||
return Ok(());
|
||||
}
|
||||
Event::Message { new_message } => {
|
||||
// ignore new messages from bots
|
||||
// NOTE: the webhook_id check allows us to still respond to PK users
|
||||
if new_message.author.bot && new_message.webhook_id.is_none() {
|
||||
debug!("Ignoring message {} from bot", new_message.id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// detect PK users first to make sure we don't respond to unproxied messages
|
||||
pluralkit::handle(ctx, new_message, data).await?;
|
||||
// detect PK users first to make sure we don't respond to unproxied messages
|
||||
pluralkit::handle(ctx, new_message, data).await?;
|
||||
|
||||
if data.storage.is_user_plural(new_message.author.id).await?
|
||||
&& pluralkit::is_message_proxied(new_message).await?
|
||||
{
|
||||
debug!("Not replying to unproxied PluralKit message");
|
||||
return Ok(());
|
||||
}
|
||||
if data.storage.is_user_plural(new_message.author.id).await?
|
||||
&& pluralkit::is_message_proxied(new_message).await?
|
||||
{
|
||||
debug!("Not replying to unproxied PluralKit message");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eta::handle(ctx, new_message).await?;
|
||||
expand_link::handle(ctx, new_message).await?;
|
||||
analyze_logs::handle(ctx, new_message, data).await?;
|
||||
}
|
||||
eta::handle(ctx, new_message).await?;
|
||||
expand_link::handle(ctx, new_message).await?;
|
||||
analyze_logs::handle(ctx, new_message, data).await?;
|
||||
}
|
||||
|
||||
Event::ReactionAdd { add_reaction } => {
|
||||
delete_on_reaction::handle(ctx, add_reaction).await?
|
||||
}
|
||||
Event::ReactionAdd { add_reaction } => {
|
||||
delete_on_reaction::handle(ctx, add_reaction).await?
|
||||
}
|
||||
|
||||
Event::ThreadCreate { thread } => support_onboard::handle(ctx, thread).await?,
|
||||
Event::ThreadCreate { thread } => support_onboard::handle(ctx, thread).await?,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,34 +9,34 @@ use tokio::time::sleep;
|
|||
const PK_DELAY_SEC: Duration = Duration::from_secs(1000);
|
||||
|
||||
pub async fn is_message_proxied(message: &Message) -> Result<bool> {
|
||||
debug!(
|
||||
"Waiting on PluralKit API for {} seconds",
|
||||
PK_DELAY_SEC.as_secs()
|
||||
);
|
||||
sleep(PK_DELAY_SEC).await;
|
||||
debug!(
|
||||
"Waiting on PluralKit API for {} seconds",
|
||||
PK_DELAY_SEC.as_secs()
|
||||
);
|
||||
sleep(PK_DELAY_SEC).await;
|
||||
|
||||
let proxied = api::pluralkit::get_sender(message.id).await.is_ok();
|
||||
let proxied = api::pluralkit::get_sender(message.id).await.is_ok();
|
||||
|
||||
Ok(proxied)
|
||||
Ok(proxied)
|
||||
}
|
||||
|
||||
pub async fn handle(_ctx: &Context, msg: &Message, data: &Data) -> Result<()> {
|
||||
if msg.webhook_id.is_some() {
|
||||
debug!(
|
||||
"Message {} has a webhook ID. Checking if it was sent through PluralKit",
|
||||
msg.id
|
||||
);
|
||||
if msg.webhook_id.is_some() {
|
||||
debug!(
|
||||
"Message {} has a webhook ID. Checking if it was sent through PluralKit",
|
||||
msg.id
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Waiting on PluralKit API for {} seconds",
|
||||
PK_DELAY_SEC.as_secs()
|
||||
);
|
||||
sleep(PK_DELAY_SEC).await;
|
||||
debug!(
|
||||
"Waiting on PluralKit API for {} seconds",
|
||||
PK_DELAY_SEC.as_secs()
|
||||
);
|
||||
sleep(PK_DELAY_SEC).await;
|
||||
|
||||
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await {
|
||||
data.storage.store_user_plurality(sender).await?;
|
||||
}
|
||||
}
|
||||
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await {
|
||||
data.storage.store_user_plurality(sender).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -3,41 +3,41 @@ use log::*;
|
|||
use poise::serenity_prelude::{ChannelType, Context, GuildChannel};
|
||||
|
||||
pub async fn handle(ctx: &Context, thread: &GuildChannel) -> Result<()> {
|
||||
if thread.kind != ChannelType::PublicThread {
|
||||
return Ok(());
|
||||
}
|
||||
if thread.kind != ChannelType::PublicThread {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let parent_id = thread
|
||||
.parent_id
|
||||
.ok_or_else(|| eyre!("Couldn't get parent ID from thread {}!", thread.name))?;
|
||||
let parent_id = thread
|
||||
.parent_id
|
||||
.ok_or_else(|| eyre!("Couldn't get parent ID from thread {}!", thread.name))?;
|
||||
|
||||
let parent_channel = ctx
|
||||
.cache
|
||||
.guild_channel(parent_id)
|
||||
.ok_or_else(|| eyre!("Couldn't get GuildChannel {}!", parent_id))?;
|
||||
let parent_channel = ctx
|
||||
.cache
|
||||
.guild_channel(parent_id)
|
||||
.ok_or_else(|| eyre!("Couldn't get GuildChannel {}!", parent_id))?;
|
||||
|
||||
if parent_channel.name != "support" {
|
||||
debug!("Not posting onboarding message to threads outside of support");
|
||||
return Ok(());
|
||||
}
|
||||
if parent_channel.name != "support" {
|
||||
debug!("Not posting onboarding message to threads outside of support");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let owner = thread
|
||||
.owner_id
|
||||
.ok_or_else(|| eyre!("Couldn't get owner of thread!"))?;
|
||||
let owner = thread
|
||||
.owner_id
|
||||
.ok_or_else(|| eyre!("Couldn't get owner of thread!"))?;
|
||||
|
||||
let msg = format!(
|
||||
let msg = format!(
|
||||
"<@{}> We've received your support ticket! {} {}",
|
||||
owner,
|
||||
"Please upload your logs and post the link here if possible (run `tag log` to find out how).",
|
||||
"Please don't ping people for support questions, unless you have their permission."
|
||||
);
|
||||
|
||||
thread
|
||||
.send_message(ctx, |m| {
|
||||
m.content(msg)
|
||||
.allowed_mentions(|am| am.replied_user(true).users(Vec::from([owner])))
|
||||
})
|
||||
.await?;
|
||||
thread
|
||||
.send_message(ctx, |m| {
|
||||
m.content(msg)
|
||||
.allowed_mentions(|am| am.replied_user(true).users(Vec::from([owner])))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
150
src/main.rs
150
src/main.rs
|
@ -7,7 +7,7 @@ use color_eyre::owo_colors::OwoColorize;
|
|||
use log::*;
|
||||
|
||||
use poise::{
|
||||
serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions,
|
||||
serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions,
|
||||
};
|
||||
|
||||
use serenity::ShardManager;
|
||||
|
@ -34,108 +34,108 @@ type Context<'a> = poise::Context<'a, Data, Report>;
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct Data {
|
||||
config: Config,
|
||||
storage: Storage,
|
||||
octocrab: Arc<octocrab::Octocrab>,
|
||||
config: Config,
|
||||
storage: Storage,
|
||||
octocrab: Arc<octocrab::Octocrab>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn new() -> Result<Self> {
|
||||
let config = Config::new_from_env()?;
|
||||
let storage = Storage::new(&config.redis_url)?;
|
||||
let octocrab = octocrab::instance();
|
||||
pub fn new() -> Result<Self> {
|
||||
let config = Config::new_from_env()?;
|
||||
let storage = Storage::new(&config.redis_url)?;
|
||||
let octocrab = octocrab::instance();
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
storage,
|
||||
octocrab,
|
||||
})
|
||||
}
|
||||
Ok(Self {
|
||||
config,
|
||||
storage,
|
||||
octocrab,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup(
|
||||
ctx: &serenity::Context,
|
||||
_ready: &serenity::Ready,
|
||||
framework: &Framework<Data, Report>,
|
||||
ctx: &serenity::Context,
|
||||
_ready: &serenity::Ready,
|
||||
framework: &Framework<Data, Report>,
|
||||
) -> Result<Data> {
|
||||
let data = Data::new()?;
|
||||
let data = Data::new()?;
|
||||
|
||||
// test redis connection
|
||||
let mut client = data.storage.client.clone();
|
||||
// test redis connection
|
||||
let mut client = data.storage.client.clone();
|
||||
|
||||
if !client.check_connection() {
|
||||
return Err(eyre!(
|
||||
"Couldn't connect to storage! Is your daemon running?"
|
||||
));
|
||||
}
|
||||
if !client.check_connection() {
|
||||
return Err(eyre!(
|
||||
"Couldn't connect to storage! Is your daemon running?"
|
||||
));
|
||||
}
|
||||
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
info!("Registered global commands!");
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
info!("Registered global commands!");
|
||||
|
||||
Ok(data)
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn handle_shutdown(shard_manager: Arc<Mutex<ShardManager>>, reason: &str) {
|
||||
warn!("{reason}! Shutting down bot...");
|
||||
shard_manager.lock().await.shutdown_all().await;
|
||||
println!("{}", "Everything is shutdown. Goodbye!".green())
|
||||
warn!("{reason}! Shutting down bot...");
|
||||
shard_manager.lock().await.shutdown_all().await;
|
||||
println!("{}", "Everything is shutdown. Goodbye!".green())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
color_eyre::install()?;
|
||||
env_logger::init();
|
||||
dotenvy::dotenv().ok();
|
||||
color_eyre::install()?;
|
||||
env_logger::init();
|
||||
|
||||
let token = std::env::var("DISCORD_BOT_TOKEN")
|
||||
.wrap_err_with(|| "Couldn't find bot token in environment!")?;
|
||||
let token = std::env::var("DISCORD_BOT_TOKEN")
|
||||
.wrap_err_with(|| "Couldn't find bot token in environment!")?;
|
||||
|
||||
let intents =
|
||||
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
|
||||
let intents =
|
||||
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
|
||||
|
||||
let options = FrameworkOptions {
|
||||
commands: commands::to_global_commands(),
|
||||
let options = FrameworkOptions {
|
||||
commands: commands::to_global_commands(),
|
||||
|
||||
on_error: |error| Box::pin(handlers::handle_error(error)),
|
||||
on_error: |error| Box::pin(handlers::handle_error(error)),
|
||||
|
||||
command_check: Some(|ctx| {
|
||||
Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) })
|
||||
}),
|
||||
command_check: Some(|ctx| {
|
||||
Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) })
|
||||
}),
|
||||
|
||||
event_handler: |ctx, event, framework, data| {
|
||||
Box::pin(handlers::handle_event(ctx, event, framework, data))
|
||||
},
|
||||
event_handler: |ctx, event, framework, data| {
|
||||
Box::pin(handlers::handle_event(ctx, event, framework, data))
|
||||
},
|
||||
|
||||
prefix_options: PrefixFrameworkOptions {
|
||||
prefix: Some("r".into()),
|
||||
edit_tracker: Some(EditTracker::for_timespan(Duration::from_secs(3600))),
|
||||
..Default::default()
|
||||
},
|
||||
prefix_options: PrefixFrameworkOptions {
|
||||
prefix: Some("r".into()),
|
||||
edit_tracker: Some(EditTracker::for_timespan(Duration::from_secs(3600))),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let framework = Framework::builder()
|
||||
.token(token)
|
||||
.intents(intents)
|
||||
.options(options)
|
||||
.setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework)))
|
||||
.build()
|
||||
.await
|
||||
.wrap_err_with(|| "Failed to build framework!")?;
|
||||
let framework = Framework::builder()
|
||||
.token(token)
|
||||
.intents(intents)
|
||||
.options(options)
|
||||
.setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework)))
|
||||
.build()
|
||||
.await
|
||||
.wrap_err_with(|| "Failed to build framework!")?;
|
||||
|
||||
let shard_manager = framework.shard_manager().clone();
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
let shard_manager = framework.shard_manager().clone();
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
|
||||
tokio::select! {
|
||||
result = framework.start() => result.map_err(Report::from),
|
||||
_ = sigterm.recv() => {
|
||||
handle_shutdown(shard_manager, "Recieved SIGTERM").await;
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ = ctrl_c() => {
|
||||
handle_shutdown(shard_manager, "Interrupted").await;
|
||||
std::process::exit(130);
|
||||
}
|
||||
}
|
||||
tokio::select! {
|
||||
result = framework.start() => result.map_err(Report::from),
|
||||
_ = sigterm.recv() => {
|
||||
handle_shutdown(shard_manager, "Recieved SIGTERM").await;
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ = ctrl_c() => {
|
||||
handle_shutdown(shard_manager, "Interrupted").await;
|
||||
std::process::exit(130);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,105 +10,105 @@ const LAUNCHER_VERSION_KEY: &str = "launcher-version-v1";
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Storage {
|
||||
pub client: Client,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new(redis_url: &str) -> Result<Self> {
|
||||
let client = Client::open(redis_url)?;
|
||||
pub fn new(redis_url: &str) -> Result<Self> {
|
||||
let client = Client::open(redis_url)?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/*
|
||||
these are mainly light abstractions to avoid the `let mut con`
|
||||
boilerplate, as well as not require the caller to format the
|
||||
strings for keys
|
||||
*/
|
||||
/*
|
||||
these are mainly light abstractions to avoid the `let mut con`
|
||||
boilerplate, as well as not require the caller to format the
|
||||
strings for keys
|
||||
*/
|
||||
|
||||
async fn get_key<T>(&self, key: &str) -> Result<T>
|
||||
where
|
||||
T: FromRedisValue,
|
||||
{
|
||||
debug!("Getting key {key}");
|
||||
async fn get_key<T>(&self, key: &str) -> Result<T>
|
||||
where
|
||||
T: FromRedisValue,
|
||||
{
|
||||
debug!("Getting key {key}");
|
||||
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
let res: T = con.get(key).await?;
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
let res: T = con.get(key).await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn set_key<'a>(
|
||||
&self,
|
||||
key: &str,
|
||||
value: impl ToRedisArgs + Debug + Send + Sync + 'a,
|
||||
) -> Result<()> {
|
||||
debug!("Creating key {key}:\n{value:#?}");
|
||||
async fn set_key<'a>(
|
||||
&self,
|
||||
key: &str,
|
||||
value: impl ToRedisArgs + Debug + Send + Sync + 'a,
|
||||
) -> Result<()> {
|
||||
debug!("Creating key {key}:\n{value:#?}");
|
||||
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.set(key, value).await?;
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.set(key, value).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn key_exists(&self, key: &str) -> Result<bool> {
|
||||
debug!("Checking if key {key} exists");
|
||||
async fn key_exists(&self, key: &str) -> Result<bool> {
|
||||
debug!("Checking if key {key} exists");
|
||||
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
let exists: u64 = con.exists(key).await?;
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
let exists: u64 = con.exists(key).await?;
|
||||
|
||||
Ok(exists > 0)
|
||||
}
|
||||
Ok(exists > 0)
|
||||
}
|
||||
|
||||
async fn delete_key(&self, key: &str) -> Result<()> {
|
||||
debug!("Deleting key {key}");
|
||||
async fn delete_key(&self, key: &str) -> Result<()> {
|
||||
debug!("Deleting key {key}");
|
||||
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.del(key).await?;
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.del(key).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expire_key(&self, key: &str, expire_seconds: usize) -> Result<()> {
|
||||
debug!("Expiring key {key} in {expire_seconds}");
|
||||
async fn expire_key(&self, key: &str, expire_seconds: usize) -> Result<()> {
|
||||
debug!("Expiring key {key} in {expire_seconds}");
|
||||
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.expire(key, expire_seconds).await?;
|
||||
let mut con = self.client.get_async_connection().await?;
|
||||
con.expire(key, expire_seconds).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> {
|
||||
info!("Marking {sender} as a PluralKit user");
|
||||
let key = format!("{PK_KEY}:{sender}");
|
||||
pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> {
|
||||
info!("Marking {sender} as a PluralKit user");
|
||||
let key = format!("{PK_KEY}:{sender}");
|
||||
|
||||
// Just store some value. We only care about the presence of this key
|
||||
self.set_key(&key, 0).await?;
|
||||
self.expire_key(&key, 7 * 24 * 60 * 60).await?;
|
||||
// Just store some value. We only care about the presence of this key
|
||||
self.set_key(&key, 0).await?;
|
||||
self.expire_key(&key, 7 * 24 * 60 * 60).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_user_plural(&self, user_id: UserId) -> Result<bool> {
|
||||
let key = format!("{PK_KEY}:{user_id}");
|
||||
self.key_exists(&key).await
|
||||
}
|
||||
pub async fn is_user_plural(&self, user_id: UserId) -> Result<bool> {
|
||||
let key = format!("{PK_KEY}:{user_id}");
|
||||
self.key_exists(&key).await
|
||||
}
|
||||
|
||||
pub async fn cache_launcher_version(&self, version: &str) -> Result<()> {
|
||||
self.set_key(LAUNCHER_VERSION_KEY, version).await?;
|
||||
pub async fn cache_launcher_version(&self, version: &str) -> Result<()> {
|
||||
self.set_key(LAUNCHER_VERSION_KEY, version).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_launcher_version(&self) -> Result<String> {
|
||||
let res = self.get_key(LAUNCHER_VERSION_KEY).await?;
|
||||
pub async fn get_launcher_version(&self) -> Result<String> {
|
||||
let res = self.get_key(LAUNCHER_VERSION_KEY).await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn launcher_version_is_cached(&self) -> Result<bool> {
|
||||
let res = self.key_exists(LAUNCHER_VERSION_KEY).await?;
|
||||
pub async fn launcher_version_is_cached(&self) -> Result<bool> {
|
||||
let res = self.key_exists(LAUNCHER_VERSION_KEY).await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
|
14
src/tags.rs
14
src/tags.rs
|
@ -6,15 +6,15 @@ pub const TAG_DIR: &str = "tags";
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct TagFrontmatter {
|
||||
pub title: String,
|
||||
pub color: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub fields: Option<Vec<EmbedField>>,
|
||||
pub title: String,
|
||||
pub color: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub fields: Option<Vec<EmbedField>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Tag {
|
||||
pub content: String,
|
||||
pub file_name: String,
|
||||
pub frontmatter: TagFrontmatter,
|
||||
pub content: String,
|
||||
pub file_name: String,
|
||||
pub frontmatter: TagFrontmatter,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#[macro_export]
|
||||
macro_rules! required_var {
|
||||
($name: expr) => {
|
||||
std::env::var($name).wrap_err_with(|| format!("Couldn't find {} in environment!", $name))?
|
||||
};
|
||||
($name: expr) => {
|
||||
std::env::var($name).wrap_err_with(|| format!("Couldn't find {} in environment!", $name))?
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ pub use resolve_message::resolve as resolve_message;
|
|||
* chooses a random element from an array
|
||||
*/
|
||||
pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let resp = arr
|
||||
.choose(&mut rng)
|
||||
.ok_or_else(|| eyre!("Couldn't choose random object from array:\n{arr:#?}!"))?;
|
||||
let mut rng = rand::thread_rng();
|
||||
let resp = arr
|
||||
.choose(&mut rng)
|
||||
.ok_or_else(|| eyre!("Couldn't choose random object from array:\n{arr:#?}!"))?;
|
||||
|
||||
Ok((*resp).to_string())
|
||||
Ok((*resp).to_string())
|
||||
}
|
||||
|
|
|
@ -5,120 +5,120 @@ use poise::serenity_prelude::{ChannelType, Colour, Context, CreateEmbed, Message
|
|||
use regex::Regex;
|
||||
|
||||
static MESSAGE_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"/(https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?<serverId>\d+)\/(?<channelId>\d+)\/(?<messageId>\d+)/g;").unwrap()
|
||||
Regex::new(r"/(https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?<serverId>\d+)\/(?<channelId>\d+)\/(?<messageId>\d+)/g;").unwrap()
|
||||
});
|
||||
|
||||
pub fn find_first_image(msg: &Message) -> Option<String> {
|
||||
msg.attachments
|
||||
.iter()
|
||||
.find(|a| {
|
||||
a.content_type
|
||||
.as_ref()
|
||||
.unwrap_or(&"".to_string())
|
||||
.starts_with("image/")
|
||||
})
|
||||
.map(|res| res.url.clone())
|
||||
msg.attachments
|
||||
.iter()
|
||||
.find(|a| {
|
||||
a.content_type
|
||||
.as_ref()
|
||||
.unwrap_or(&"".to_string())
|
||||
.starts_with("image/")
|
||||
})
|
||||
.map(|res| res.url.clone())
|
||||
}
|
||||
|
||||
pub async fn resolve(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> {
|
||||
let matches = MESSAGE_PATTERN.captures_iter(&msg.content);
|
||||
let mut embeds: Vec<CreateEmbed> = vec![];
|
||||
let matches = MESSAGE_PATTERN.captures_iter(&msg.content);
|
||||
let mut embeds: Vec<CreateEmbed> = vec![];
|
||||
|
||||
for captured in matches.take(3) {
|
||||
// don't leak messages from other servers
|
||||
if let Some(server_id) = captured.get(0) {
|
||||
let other_server: u64 = server_id.as_str().parse().unwrap_or_default();
|
||||
let current_id = msg.guild_id.unwrap_or_default();
|
||||
for captured in matches.take(3) {
|
||||
// don't leak messages from other servers
|
||||
if let Some(server_id) = captured.get(0) {
|
||||
let other_server: u64 = server_id.as_str().parse().unwrap_or_default();
|
||||
let current_id = msg.guild_id.unwrap_or_default();
|
||||
|
||||
if &other_server != current_id.as_u64() {
|
||||
debug!("Not resolving message of other guild.");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
warn!("Couldn't find server_id from Discord link! Not resolving message to be safe");
|
||||
continue;
|
||||
}
|
||||
if &other_server != current_id.as_u64() {
|
||||
debug!("Not resolving message of other guild.");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
warn!("Couldn't find server_id from Discord link! Not resolving message to be safe");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = captured.get(1) {
|
||||
let parsed: u64 = channel_id.as_str().parse().unwrap_or_default();
|
||||
let req_channel = ctx
|
||||
.cache
|
||||
.channel(parsed)
|
||||
.ok_or_else(|| eyre!("Couldn't get channel_id from Discord regex!"))?
|
||||
.guild()
|
||||
.ok_or_else(|| {
|
||||
eyre!("Couldn't convert to GuildChannel from channel_id {parsed}!")
|
||||
})?;
|
||||
if let Some(channel_id) = captured.get(1) {
|
||||
let parsed: u64 = channel_id.as_str().parse().unwrap_or_default();
|
||||
let req_channel = ctx
|
||||
.cache
|
||||
.channel(parsed)
|
||||
.ok_or_else(|| eyre!("Couldn't get channel_id from Discord regex!"))?
|
||||
.guild()
|
||||
.ok_or_else(|| {
|
||||
eyre!("Couldn't convert to GuildChannel from channel_id {parsed}!")
|
||||
})?;
|
||||
|
||||
if !req_channel.is_text_based() {
|
||||
debug!("Not resolving message is non-text-based channel.");
|
||||
continue;
|
||||
}
|
||||
if !req_channel.is_text_based() {
|
||||
debug!("Not resolving message is non-text-based channel.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if req_channel.kind == ChannelType::PrivateThread {
|
||||
if let Some(id) = req_channel.parent_id {
|
||||
let parent = ctx.cache.guild_channel(id).ok_or_else(|| {
|
||||
eyre!("Couldn't get parent channel {id} for thread {req_channel}!")
|
||||
})?;
|
||||
let parent_members = parent.members(ctx).await.unwrap_or_default();
|
||||
if req_channel.kind == ChannelType::PrivateThread {
|
||||
if let Some(id) = req_channel.parent_id {
|
||||
let parent = ctx.cache.guild_channel(id).ok_or_else(|| {
|
||||
eyre!("Couldn't get parent channel {id} for thread {req_channel}!")
|
||||
})?;
|
||||
let parent_members = parent.members(ctx).await.unwrap_or_default();
|
||||
|
||||
if !parent_members.iter().any(|m| m.user.id == msg.author.id) {
|
||||
debug!("Not resolving message for user not a part of a private thread.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if req_channel
|
||||
.members(ctx)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|m| m.user.id == msg.author.id)
|
||||
{
|
||||
debug!("Not resolving for message for user not a part of a channel");
|
||||
continue;
|
||||
}
|
||||
if !parent_members.iter().any(|m| m.user.id == msg.author.id) {
|
||||
debug!("Not resolving message for user not a part of a private thread.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if req_channel
|
||||
.members(ctx)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|m| m.user.id == msg.author.id)
|
||||
{
|
||||
debug!("Not resolving for message for user not a part of a channel");
|
||||
continue;
|
||||
}
|
||||
|
||||
let message_id: u64 = captured
|
||||
.get(2)
|
||||
.ok_or_else(|| eyre!("Couldn't get message_id from Discord regex!"))?
|
||||
.as_str()
|
||||
.parse()
|
||||
.wrap_err_with(|| {
|
||||
eyre!("Couldn't parse message_id from Discord regex as a MessageId!")
|
||||
})?;
|
||||
let message_id: u64 = captured
|
||||
.get(2)
|
||||
.ok_or_else(|| eyre!("Couldn't get message_id from Discord regex!"))?
|
||||
.as_str()
|
||||
.parse()
|
||||
.wrap_err_with(|| {
|
||||
eyre!("Couldn't parse message_id from Discord regex as a MessageId!")
|
||||
})?;
|
||||
|
||||
let original_message = req_channel.message(ctx, message_id).await?;
|
||||
let mut embed = CreateEmbed::default();
|
||||
embed
|
||||
.author(|a| {
|
||||
a.name(original_message.author.tag())
|
||||
.icon_url(original_message.author.default_avatar_url())
|
||||
})
|
||||
.color(Colour::BLITZ_BLUE)
|
||||
.timestamp(original_message.timestamp)
|
||||
.footer(|f| f.text(format!("#{}", req_channel.name)))
|
||||
.description(format!(
|
||||
"{}\n\n[Jump to original message]({})",
|
||||
original_message.content,
|
||||
original_message.link()
|
||||
));
|
||||
let original_message = req_channel.message(ctx, message_id).await?;
|
||||
let mut embed = CreateEmbed::default();
|
||||
embed
|
||||
.author(|a| {
|
||||
a.name(original_message.author.tag())
|
||||
.icon_url(original_message.author.default_avatar_url())
|
||||
})
|
||||
.color(Colour::BLITZ_BLUE)
|
||||
.timestamp(original_message.timestamp)
|
||||
.footer(|f| f.text(format!("#{}", req_channel.name)))
|
||||
.description(format!(
|
||||
"{}\n\n[Jump to original message]({})",
|
||||
original_message.content,
|
||||
original_message.link()
|
||||
));
|
||||
|
||||
if !original_message.attachments.is_empty() {
|
||||
embed.fields(original_message.attachments.iter().map(|a| {
|
||||
(
|
||||
"Attachments".to_string(),
|
||||
format!("[{}]({})", a.filename, a.url),
|
||||
false,
|
||||
)
|
||||
}));
|
||||
if !original_message.attachments.is_empty() {
|
||||
embed.fields(original_message.attachments.iter().map(|a| {
|
||||
(
|
||||
"Attachments".to_string(),
|
||||
format!("[{}]({})", a.filename, a.url),
|
||||
false,
|
||||
)
|
||||
}));
|
||||
|
||||
if let Some(image) = find_first_image(msg) {
|
||||
embed.image(image);
|
||||
}
|
||||
}
|
||||
if let Some(image) = find_first_image(msg) {
|
||||
embed.image(image);
|
||||
}
|
||||
}
|
||||
|
||||
embeds.push(embed);
|
||||
}
|
||||
}
|
||||
embeds.push(embed);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(embeds)
|
||||
Ok(embeds)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue