style: use tabs over spaces

This commit is contained in:
seth 2024-01-08 14:56:37 -05:00
parent f2979d4cde
commit f0550dd429
No known key found for this signature in database
GPG key ID: D31BD0D494BBEE86
41 changed files with 1112 additions and 1109 deletions

1
.rustfmt.toml Normal file
View file

@ -0,0 +1 @@
hard_tabs = true

View file

@ -8,15 +8,15 @@ include!("src/tags.rs");
/// generate the ChoiceParameter enum and tag data we will use in the `tag` command /// generate the ChoiceParameter enum and tag data we will use in the `tag` command
#[allow(dead_code)] #[allow(dead_code)]
fn main() { fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
let generated = Path::new(&out_dir).join("generated.rs"); let generated = Path::new(&out_dir).join("generated.rs");
let tag_files: Vec<String> = fs::read_dir(TAG_DIR) let tag_files: Vec<String> = fs::read_dir(TAG_DIR)
.unwrap() .unwrap()
.map(|f| f.unwrap().file_name().to_string_lossy().to_string()) .map(|f| f.unwrap().file_name().to_string_lossy().to_string())
.collect(); .collect();
let tags: Vec<Tag> = tag_files let tags: Vec<Tag> = tag_files
.clone() .clone()
.into_iter() .into_iter()
.map(|name| { .map(|name| {
@ -45,23 +45,23 @@ fn main() {
}) })
.collect(); .collect();
let formatted_names: Vec<String> = tags let formatted_names: Vec<String> = tags
.iter() .iter()
.map(|t| t.file_name.replace(".md", "").replace('-', "_")) .map(|t| t.file_name.replace(".md", "").replace('-', "_"))
.collect(); .collect();
let tag_choice = format!( let tag_choice = format!(
r#" r#"
#[allow(non_camel_case_types, clippy::upper_case_acronyms)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)]
#[derive(Clone, Debug, poise::ChoiceParameter)] #[derive(Clone, Debug, poise::ChoiceParameter)]
pub enum TagChoice {{ pub enum TagChoice {{
{} {}
}}"#, }}"#,
formatted_names.join(",\n") formatted_names.join(",\n")
); );
let to_str = format!( let to_str = format!(
r#" r#"
impl TagChoice {{ impl TagChoice {{
fn as_str(&self) -> &str {{ fn as_str(&self) -> &str {{
match &self {{ match &self {{
@ -70,22 +70,22 @@ fn main() {
}} }}
}} }}
"#, "#,
formatted_names formatted_names
.iter() .iter()
.map(|n| { .map(|n| {
let file_name = n.replace('_', "-") + ".md"; let file_name = n.replace('_', "-") + ".md";
format!("Self::{n} => \"{file_name}\",") format!("Self::{n} => \"{file_name}\",")
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .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(); fs::write(generated, contents).unwrap();
println!( println!(
"cargo:rustc-env=TAGS={}", "cargo:rustc-env=TAGS={}",
// make sure we can deserialize with env! at runtime // make sure we can deserialize with env! at runtime
serde_json::to_string(&tags).unwrap() serde_json::to_string(&tags).unwrap()
); );
} }

View file

@ -7,15 +7,15 @@ use reqwest::StatusCode;
const DADJOKE: &str = "https://icanhazdadjoke.com"; const DADJOKE: &str = "https://icanhazdadjoke.com";
pub async fn get_joke() -> Result<String> { 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()); debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?; let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status(); let status = resp.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
Ok(resp.text().await?) Ok(resp.text().await?)
} else { } else {
Err(eyre!("Couldn't get a joke!")) Err(eyre!("Couldn't get a joke!"))
} }
} }

View file

@ -6,14 +6,14 @@ pub mod prism_meta;
pub mod rory; pub mod rory;
pub static USER_AGENT: Lazy<String> = Lazy::new(|| { 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(|| { pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
reqwest::Client::builder() reqwest::Client::builder()
.user_agent(USER_AGENT.to_string()) .user_agent(USER_AGENT.to_string())
.build() .build()
.unwrap_or_default() .unwrap_or_default()
}); });

View file

@ -8,30 +8,32 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PluralKitMessage { pub struct PluralKitMessage {
pub sender: String, pub sender: String,
} }
const PLURAL_KIT: &str = "https://api.pluralkit.me/v2"; const PLURAL_KIT: &str = "https://api.pluralkit.me/v2";
const MESSAGES_ENDPOINT: &str = "/messages"; const MESSAGES_ENDPOINT: &str = "/messages";
pub async fn get_sender(message_id: MessageId) -> Result<UserId> { pub async fn get_sender(message_id: MessageId) -> Result<UserId> {
let req = REQWEST_CLIENT let req = REQWEST_CLIENT
.get(format!("{PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id}")) .get(format!("{PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id}"))
.build()?; .build()?;
debug!("Making request to {}", req.url()); debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?; let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status(); let status = resp.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
let data = resp.json::<PluralKitMessage>().await?; 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 id: u64 = data.sender.parse().wrap_err_with(|| {
let sender = UserId::from(id); format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{data:#?}")
})?;
let sender = UserId::from(id);
Ok(sender) Ok(sender)
} else { } else {
Err(eyre!( Err(eyre!(
"Failed to get PluralKit message information from {PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id} with {status}", "Failed to get PluralKit message information from {PLURAL_KIT}{MESSAGES_ENDPOINT}/{message_id} with {status}",
)) ))
} }
} }

View file

@ -8,39 +8,39 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MinecraftPackageJson { pub struct MinecraftPackageJson {
pub format_version: u8, pub format_version: u8,
pub name: String, pub name: String,
pub recommended: Vec<String>, pub recommended: Vec<String>,
pub uid: String, pub uid: String,
} }
const PRISM_META: &str = "https://meta.prismlauncher.org/v1"; const PRISM_META: &str = "https://meta.prismlauncher.org/v1";
const MINECRAFT_PACKAGEJSON_ENDPOINT: &str = "/net.minecraft/package.json"; const MINECRAFT_PACKAGEJSON_ENDPOINT: &str = "/net.minecraft/package.json";
pub async fn get_latest_minecraft_version() -> Result<String> { pub async fn get_latest_minecraft_version() -> Result<String> {
let req = REQWEST_CLIENT let req = REQWEST_CLIENT
.get(format!("{PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT}")) .get(format!("{PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT}"))
.build()?; .build()?;
debug!("Making request to {}", req.url()); debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?; let resp = REQWEST_CLIENT.execute(req).await?;
let status = resp.status(); let status = resp.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
let data = resp let data = resp
.json::<MinecraftPackageJson>() .json::<MinecraftPackageJson>()
.await .await
.wrap_err_with(|| "Couldn't parse Minecraft versions!")?; .wrap_err_with(|| "Couldn't parse Minecraft versions!")?;
let version = data let version = data
.recommended .recommended
.first() .first()
.ok_or_else(|| eyre!("Couldn't find latest version of Minecraft!"))?; .ok_or_else(|| eyre!("Couldn't find latest version of Minecraft!"))?;
Ok(version.clone()) Ok(version.clone())
} else { } else {
Err(eyre!( Err(eyre!(
"Failed to get latest Minecraft version from {PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT} with {status}", "Failed to get latest Minecraft version from {PRISM_META}{MINECRAFT_PACKAGEJSON_ENDPOINT} with {status}",
)) ))
} }
} }

View file

@ -7,40 +7,40 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct RoryResponse { pub struct RoryResponse {
pub id: u64, pub id: u64,
pub url: String, pub url: String,
pub error: Option<String>, pub error: Option<String>,
} }
const RORY: &str = "https://rory.cat"; const RORY: &str = "https://rory.cat";
const ENDPOINT: &str = "/purr"; const ENDPOINT: &str = "/purr";
pub async fn get_rory(id: Option<u64>) -> Result<RoryResponse> { 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 let req = REQWEST_CLIENT
.get(format!("{RORY}{ENDPOINT}/{target}")) .get(format!("{RORY}{ENDPOINT}/{target}"))
.build() .build()
.wrap_err_with(|| "Couldn't build reqwest client!")?; .wrap_err_with(|| "Couldn't build reqwest client!")?;
debug!("Making request to {}", req.url()); debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT let resp = REQWEST_CLIENT
.execute(req) .execute(req)
.await .await
.wrap_err_with(|| "Couldn't make request for rory!")?; .wrap_err_with(|| "Couldn't make request for rory!")?;
let status = resp.status(); let status = resp.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
let data = resp let data = resp
.json::<RoryResponse>() .json::<RoryResponse>()
.await .await
.wrap_err_with(|| "Couldn't parse the rory response!")?; .wrap_err_with(|| "Couldn't parse the rory response!")?;
Ok(data) Ok(data)
} else { } else {
Err(eyre!( Err(eyre!(
"Failed to get rory from {RORY}{ENDPOINT}/{target} with {status}", "Failed to get rory from {RORY}{ENDPOINT}/{target} with {status}",
)) ))
} }
} }

View file

@ -6,8 +6,8 @@ use color_eyre::eyre::Result;
/// It's a joke /// It's a joke
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn joke(ctx: Context<'_>) -> Result<()> { pub async fn joke(ctx: Context<'_>) -> Result<()> {
let joke = dadjoke::get_joke().await?; let joke = dadjoke::get_joke().await?;
ctx.reply(joke).await?; ctx.reply(joke).await?;
Ok(()) Ok(())
} }

View file

@ -5,22 +5,22 @@ use color_eyre::eyre::{eyre, Result};
/// Returns the number of members in the server /// Returns the number of members in the server
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn members(ctx: Context<'_>) -> Result<()> { 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 count = guild.member_count;
let online = if let Some(count) = guild.approximate_presence_count { let online = if let Some(count) = guild.approximate_presence_count {
count.to_string() count.to_string()
} else { } else {
"Undefined".to_string() "Undefined".to_string()
}; };
ctx.send(|m| { ctx.send(|m| {
m.embed(|e| { m.embed(|e| {
e.title(format!("{count} total members!")) e.title(format!("{count} total members!"))
.description(format!("{online} online members")) .description(format!("{online} online members"))
.color(consts::COLORS["blue"]) .color(consts::COLORS["blue"])
}) })
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -5,6 +5,6 @@ use color_eyre::eyre::Result;
/// Replies with pong! /// Replies with pong!
#[poise::command(slash_command, prefix_command, ephemeral)] #[poise::command(slash_command, prefix_command, ephemeral)]
pub async fn ping(ctx: Context<'_>) -> Result<()> { pub async fn ping(ctx: Context<'_>) -> Result<()> {
ctx.reply("Pong!").await?; ctx.reply("Pong!").await?;
Ok(()) Ok(())
} }

View file

@ -6,24 +6,24 @@ use color_eyre::eyre::Result;
/// Gets a Rory photo! /// Gets a Rory photo!
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn rory( pub async fn rory(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "specify a Rory ID"] id: Option<u64>, #[description = "specify a Rory ID"] id: Option<u64>,
) -> Result<()> { ) -> Result<()> {
let rory = get_rory(id).await?; let rory = get_rory(id).await?;
ctx.send(|m| { ctx.send(|m| {
m.embed(|e| { m.embed(|e| {
if let Some(error) = rory.error { if let Some(error) = rory.error {
e.title("Error!").description(error) e.title("Error!").description(error)
} else { } else {
e.title("Rory :3") e.title("Rory :3")
.url(&rory.url) .url(&rory.url)
.image(rory.url) .image(rory.url)
.footer(|f| f.text(format!("ID {}", rory.id))) .footer(|f| f.text(format!("ID {}", rory.id)))
} }
}) })
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -4,48 +4,48 @@ use color_eyre::eyre::{eyre, Result};
/// Say something through the bot /// Say something through the bot
#[poise::command( #[poise::command(
slash_command, slash_command,
prefix_command, prefix_command,
ephemeral, ephemeral,
default_member_permissions = "MODERATE_MEMBERS", default_member_permissions = "MODERATE_MEMBERS",
required_permissions = "MODERATE_MEMBERS" required_permissions = "MODERATE_MEMBERS"
)] )]
pub async fn say(ctx: Context<'_>, #[description = "Just content?"] content: String) -> Result<()> { 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 guild = ctx.guild().ok_or_else(|| eyre!("Couldn't get guild!"))?;
let channel = ctx let channel = ctx
.guild_channel() .guild_channel()
.await .await
.ok_or_else(|| eyre!("Couldn't get channel!"))?; .ok_or_else(|| eyre!("Couldn't get channel!"))?;
ctx.defer_ephemeral().await?; ctx.defer_ephemeral().await?;
channel.say(ctx, &content).await?; channel.say(ctx, &content).await?;
ctx.say("I said what you said!").await?; ctx.say("I said what you said!").await?;
if let Some(channel_id) = ctx.data().config.discord.channels.say_log_channel_id { if let Some(channel_id) = ctx.data().config.discord.channels.say_log_channel_id {
let log_channel = guild let log_channel = guild
.channels .channels
.iter() .iter()
.find(|c| c.0 == &channel_id) .find(|c| c.0 == &channel_id)
.ok_or_else(|| eyre!("Couldn't get log channel from guild!"))?; .ok_or_else(|| eyre!("Couldn't get log channel from guild!"))?;
log_channel log_channel
.1 .1
.clone() .clone()
.guild() .guild()
.ok_or_else(|| eyre!("Couldn't cast channel we found from guild as GuildChannel?????"))? .ok_or_else(|| eyre!("Couldn't cast channel we found from guild as GuildChannel?????"))?
.send_message(ctx, |m| { .send_message(ctx, |m| {
m.embed(|e| { m.embed(|e| {
e.title("Say command used!") e.title("Say command used!")
.description(content) .description(content)
.author(|a| { .author(|a| {
a.name(ctx.author().tag()).icon_url( a.name(ctx.author().tag()).icon_url(
ctx.author().avatar_url().unwrap_or("undefined".to_string()), ctx.author().avatar_url().unwrap_or("undefined".to_string()),
) )
}) })
}) })
}) })
.await?; .await?;
} }
Ok(()) Ok(())
} }

View file

@ -5,27 +5,27 @@ use color_eyre::eyre::{Context as _, Result};
/// Returns GitHub stargazer count /// Returns GitHub stargazer count
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn stars(ctx: Context<'_>) -> Result<()> { pub async fn stars(ctx: Context<'_>) -> Result<()> {
let prismlauncher = ctx let prismlauncher = ctx
.data() .data()
.octocrab .octocrab
.repos("PrismLauncher", "PrismLauncher") .repos("PrismLauncher", "PrismLauncher")
.get() .get()
.await .await
.wrap_err_with(|| "Couldn't get PrismLauncher/PrismLauncher from GitHub!")?; .wrap_err_with(|| "Couldn't get PrismLauncher/PrismLauncher from GitHub!")?;
let count = if let Some(count) = prismlauncher.stargazers_count { let count = if let Some(count) = prismlauncher.stargazers_count {
count.to_string() count.to_string()
} else { } else {
"undefined".to_string() "undefined".to_string()
}; };
ctx.send(|m| { ctx.send(|m| {
m.embed(|e| { m.embed(|e| {
e.title(format!("{count} total stars!")) e.title(format!("{count} total stars!"))
.color(COLORS["yellow"]) .color(COLORS["yellow"])
}) })
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -13,48 +13,48 @@ static TAGS: Lazy<Vec<Tag>> = Lazy::new(|| serde_json::from_str(env!("TAGS")).un
/// Send a tag /// Send a tag
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn tag( pub async fn tag(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "the copypasta you want to send"] name: TagChoice, #[description = "the copypasta you want to send"] name: TagChoice,
user: Option<User>, user: Option<User>,
) -> Result<()> { ) -> Result<()> {
let tag_file = name.as_str(); let tag_file = name.as_str();
let tag = TAGS let tag = TAGS
.iter() .iter()
.find(|t| t.file_name == tag_file) .find(|t| t.file_name == tag_file)
.ok_or_else(|| eyre!("Tried to get non-existent tag: {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| { ctx.send(|m| {
if let Some(user) = user { if let Some(user) = user {
m.content(format!("<@{}>", user.id)); m.content(format!("<@{}>", user.id));
} }
m.embed(|e| { m.embed(|e| {
e.title(&frontmatter.title); e.title(&frontmatter.title);
e.description(&tag.content); e.description(&tag.content);
if let Some(color) = &frontmatter.color { if let Some(color) = &frontmatter.color {
let color = *consts::COLORS let color = *consts::COLORS
.get(color.as_str()) .get(color.as_str())
.unwrap_or(&Color::default()); .unwrap_or(&Color::default());
e.color(color); e.color(color);
} }
if let Some(image) = &frontmatter.image { if let Some(image) = &frontmatter.image {
e.image(image); e.image(image);
} }
if let Some(fields) = &frontmatter.fields { if let Some(fields) = &frontmatter.fields {
for field in fields { for field in fields {
e.field(&field.name, &field.value, field.inline); e.field(&field.name, &field.value, field.inline);
} }
} }
e e
}) })
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -6,13 +6,13 @@ use poise::Command;
mod general; mod general;
pub fn to_global_commands() -> Vec<Command<Data, Report>> { pub fn to_global_commands() -> Vec<Command<Data, Report>> {
vec![ vec![
general::joke(), general::joke(),
general::members(), general::members(),
general::ping(), general::ping(),
general::rory(), general::rory(),
general::say(), general::say(),
general::stars(), general::stars(),
general::tag(), general::tag(),
] ]
} }

View file

@ -7,81 +7,81 @@ use url::Url;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RefractionOAuth2 { pub struct RefractionOAuth2 {
pub redirect_uri: Url, pub redirect_uri: Url,
pub scope: String, pub scope: String,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct RefractionChannels { pub struct RefractionChannels {
pub say_log_channel_id: Option<ChannelId>, pub say_log_channel_id: Option<ChannelId>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct DiscordConfig { pub struct DiscordConfig {
pub client_id: ApplicationId, pub client_id: ApplicationId,
pub client_secret: String, pub client_secret: String,
pub bot_token: String, pub bot_token: String,
pub oauth2: RefractionOAuth2, pub oauth2: RefractionOAuth2,
pub channels: RefractionChannels, pub channels: RefractionChannels,
} }
impl Default for RefractionOAuth2 { impl Default for RefractionOAuth2 {
fn default() -> Self { fn default() -> Self {
Self { Self {
scope: "identify connections role_connections.write".to_string(), scope: "identify connections role_connections.write".to_string(),
redirect_uri: Url::parse("https://google.com").unwrap(), redirect_uri: Url::parse("https://google.com").unwrap(),
} }
} }
} }
impl RefractionOAuth2 { impl RefractionOAuth2 {
pub fn new_from_env() -> Result<Self> { pub fn new_from_env() -> Result<Self> {
let unparsed = format!("{}/oauth2/callback", required_var!("PUBLIC_URI")); let unparsed = format!("{}/oauth2/callback", required_var!("PUBLIC_URI"));
let redirect_uri = Url::parse(&unparsed)?; let redirect_uri = Url::parse(&unparsed)?;
debug!("OAuth2 Redirect URI is {redirect_uri}"); debug!("OAuth2 Redirect URI is {redirect_uri}");
Ok(Self { Ok(Self {
redirect_uri, redirect_uri,
..Default::default() ..Default::default()
}) })
} }
} }
impl RefractionChannels { impl RefractionChannels {
pub fn new_from_env() -> Result<Self> { pub fn new_from_env() -> Result<Self> {
let unparsed = std::env::var("DISCORD_SAY_LOG_CHANNELID"); let unparsed = std::env::var("DISCORD_SAY_LOG_CHANNELID");
if let Ok(unparsed) = unparsed { if let Ok(unparsed) = unparsed {
let id = unparsed.parse::<u64>()?; let id = unparsed.parse::<u64>()?;
let channel_id = ChannelId::from(id); let channel_id = ChannelId::from(id);
debug!("Log channel is {id}"); debug!("Log channel is {id}");
Ok(Self { Ok(Self {
say_log_channel_id: Some(channel_id), say_log_channel_id: Some(channel_id),
}) })
} else { } else {
warn!("DISCORD_SAY_LOG_CHANNELID is empty; this will disable logging in your server."); warn!("DISCORD_SAY_LOG_CHANNELID is empty; this will disable logging in your server.");
Ok(Self { Ok(Self {
say_log_channel_id: None, say_log_channel_id: None,
}) })
} }
} }
} }
impl DiscordConfig { impl DiscordConfig {
pub fn new_from_env() -> Result<Self> { pub fn new_from_env() -> Result<Self> {
let unparsed_client = required_var!("DISCORD_CLIENT_ID").parse::<u64>()?; let unparsed_client = required_var!("DISCORD_CLIENT_ID").parse::<u64>()?;
let client_id = ApplicationId::from(unparsed_client); let client_id = ApplicationId::from(unparsed_client);
let client_secret = required_var!("DISCORD_CLIENT_SECRET"); let client_secret = required_var!("DISCORD_CLIENT_SECRET");
let bot_token = required_var!("DISCORD_BOT_TOKEN"); let bot_token = required_var!("DISCORD_BOT_TOKEN");
let oauth2 = RefractionOAuth2::new_from_env()?; let oauth2 = RefractionOAuth2::new_from_env()?;
let channels = RefractionChannels::new_from_env()?; let channels = RefractionChannels::new_from_env()?;
Ok(Self { Ok(Self {
client_id, client_id,
client_secret, client_secret,
bot_token, bot_token,
oauth2, oauth2,
channels, channels,
}) })
} }
} }

View file

@ -4,62 +4,62 @@ use crate::required_var;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RefractionRepo { pub struct RefractionRepo {
pub owner: String, pub owner: String,
pub repo: String, pub repo: String,
pub key: String, pub key: String,
pub name: String, pub name: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GithubConfig { pub struct GithubConfig {
pub token: String, pub token: String,
pub repos: Vec<RefractionRepo>, pub repos: Vec<RefractionRepo>,
pub cache_sec: u16, pub cache_sec: u16,
pub update_cron_job: String, pub update_cron_job: String,
} }
impl Default for GithubConfig { impl Default for GithubConfig {
fn default() -> Self { fn default() -> Self {
let owner = "PrismLauncher".to_string(); let owner = "PrismLauncher".to_string();
let repos = Vec::<RefractionRepo>::from([ let repos = Vec::<RefractionRepo>::from([
RefractionRepo { RefractionRepo {
owner: owner.clone(), owner: owner.clone(),
repo: "PrismLauncher".to_string(), repo: "PrismLauncher".to_string(),
key: "launcher".to_string(), key: "launcher".to_string(),
name: "Launcher contributor".to_string(), name: "Launcher contributor".to_string(),
}, },
RefractionRepo { RefractionRepo {
owner: owner.clone(), owner: owner.clone(),
repo: "prismlauncher.org".to_string(), repo: "prismlauncher.org".to_string(),
key: "website".to_string(), key: "website".to_string(),
name: "Web developer".to_string(), name: "Web developer".to_string(),
}, },
RefractionRepo { RefractionRepo {
owner: owner.clone(), owner: owner.clone(),
repo: "Translations".to_string(), repo: "Translations".to_string(),
key: "translations".to_string(), key: "translations".to_string(),
name: "Translator".to_string(), name: "Translator".to_string(),
}, },
]); ]);
Self { Self {
repos, repos,
cache_sec: 3600, cache_sec: 3600,
update_cron_job: "0 */10 * * * *".to_string(), // every 10 minutes update_cron_job: "0 */10 * * * *".to_string(), // every 10 minutes
token: String::default(), token: String::default(),
} }
} }
} }
impl GithubConfig { impl GithubConfig {
pub fn new_from_env() -> Result<Self> { pub fn new_from_env() -> Result<Self> {
let token = required_var!("GITHUB_TOKEN"); let token = required_var!("GITHUB_TOKEN");
Ok(Self { Ok(Self {
token, token,
..Default::default() ..Default::default()
}) })
} }
} }

View file

@ -8,32 +8,32 @@ pub use github::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub discord: DiscordConfig, pub discord: DiscordConfig,
pub github: GithubConfig, pub github: GithubConfig,
pub http_port: u16, pub http_port: u16,
pub redis_url: String, pub redis_url: String,
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
discord: DiscordConfig::default(), discord: DiscordConfig::default(),
github: GithubConfig::default(), github: GithubConfig::default(),
http_port: 3000, http_port: 3000,
redis_url: "redis://localhost:6379".to_string(), redis_url: "redis://localhost:6379".to_string(),
} }
} }
} }
impl Config { impl Config {
pub fn new_from_env() -> Result<Self> { pub fn new_from_env() -> Result<Self> {
let discord = DiscordConfig::new_from_env()?; let discord = DiscordConfig::new_from_env()?;
let github = GithubConfig::new_from_env()?; let github = GithubConfig::new_from_env()?;
Ok(Self { Ok(Self {
discord, discord,
github, github,
..Default::default() ..Default::default()
}) })
} }
} }

View file

@ -4,31 +4,31 @@ use once_cell::sync::Lazy;
use poise::serenity_prelude::Color; use poise::serenity_prelude::Color;
pub static COLORS: Lazy<HashMap<&str, Color>> = Lazy::new(|| { pub static COLORS: Lazy<HashMap<&str, Color>> = Lazy::new(|| {
HashMap::from([ HashMap::from([
("red", Color::from((239, 68, 68))), ("red", Color::from((239, 68, 68))),
("green", Color::from((34, 197, 94))), ("green", Color::from((34, 197, 94))),
("blue", Color::from((96, 165, 250))), ("blue", Color::from((96, 165, 250))),
("yellow", Color::from((253, 224, 71))), ("yellow", Color::from((253, 224, 71))),
("orange", Color::from((251, 146, 60))), ("orange", Color::from((251, 146, 60))),
// TODO purple & pink :D // TODO purple & pink :D
]) ])
}); });
pub const ETA_MESSAGES: [&str; 16] = [ pub const ETA_MESSAGES: [&str; 16] = [
"Sometime", "Sometime",
"Some day", "Some day",
"Not far", "Not far",
"The future", "The future",
"Never", "Never",
"Perhaps tomorrow?", "Perhaps tomorrow?",
"There are no ETAs", "There are no ETAs",
"No", "No",
"Nah", "Nah",
"Yes", "Yes",
"Yas", "Yas",
"Next month", "Next month",
"Next year", "Next year",
"Next week", "Next week",
"In Prism Launcher 2.0.0", "In Prism Launcher 2.0.0",
"At the appropriate juncture, in due course, in the fullness of time", "At the appropriate juncture, in due course, in the fullness of time",
]; ];

View file

@ -7,43 +7,43 @@ use poise::serenity_prelude::Timestamp;
use poise::FrameworkError; use poise::FrameworkError;
pub async fn handle(error: FrameworkError<'_, Data, Report>) { pub async fn handle(error: FrameworkError<'_, Data, Report>) {
match error { match error {
FrameworkError::Setup { FrameworkError::Setup {
error, framework, .. error, framework, ..
} => { } => {
error!("Error setting up client! Bailing out"); error!("Error setting up client! Bailing out");
framework.shard_manager().lock().await.shutdown_all().await; framework.shard_manager().lock().await.shutdown_all().await;
panic!("{error}") panic!("{error}")
} }
FrameworkError::Command { error, ctx } => { FrameworkError::Command { error, ctx } => {
error!("Error in command {}:\n{error:?}", ctx.command().name); error!("Error in command {}:\n{error:?}", ctx.command().name);
ctx.send(|c| { ctx.send(|c| {
c.embed(|e| { c.embed(|e| {
e.title("Something went wrong!") e.title("Something went wrong!")
.description("oopsie") .description("oopsie")
.timestamp(Timestamp::now()) .timestamp(Timestamp::now())
.color(COLORS["red"]) .color(COLORS["red"])
}) })
}) })
.await .await
.ok(); .ok();
} }
FrameworkError::EventHandler { FrameworkError::EventHandler {
error, error,
ctx: _, ctx: _,
event, event,
framework: _, framework: _,
} => { } => {
error!("Error while handling event {}:\n{error:?}", event.name()); error!("Error while handling event {}:\n{error:?}", event.name());
} }
error => { error => {
if let Err(e) = poise::builtins::on_error(error).await { if let Err(e) = poise::builtins::on_error(error).await {
error!("Unhandled error occured:\n{e:#?}"); error!("Unhandled error occured:\n{e:#?}");
} }
} }
} }
} }

View file

@ -7,164 +7,164 @@ use regex::Regex;
pub type Issue = Option<(String, String)>; pub type Issue = Option<(String, String)>;
pub async fn find_issues(log: &str, data: &Data) -> Result<Vec<(String, String)>> { pub async fn find_issues(log: &str, data: &Data) -> Result<Vec<(String, String)>> {
let issues = [ let issues = [
fabric_internal, fabric_internal,
flatpak_nvidia, flatpak_nvidia,
forge_java, forge_java,
intel_hd, intel_hd,
java_option, java_option,
lwjgl_2_java_9, lwjgl_2_java_9,
macos_ns, macos_ns,
oom, oom,
optinotfine, optinotfine,
pre_1_12_native_transport_java_9, pre_1_12_native_transport_java_9,
wrong_java, 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? { if let Some(issues) = outdated_launcher(log, data).await? {
res.push(issues) res.push(issues)
} }
Ok(res) Ok(res)
} }
fn fabric_internal(log: &str) -> Issue { 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 = ( let issue = (
"Fabric Internal Access".to_string(), "Fabric Internal Access".to_string(),
"The mod you are using is using fabric internals that are not meant \ "The mod you are using is using fabric internals that are not meant \
to be used by anything but the loader itself. to be used by anything but the loader itself.
Those mods break both on Quilt and with fabric updates. Those mods break both on Quilt and with fabric updates.
If you're using fabric, downgrade your fabric loader could work, \ If you're using fabric, downgrade your fabric loader could work, \
on Quilt you can try updating to the latest beta version, \ 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." but there's nothing much to do unless the mod author stops using them."
.to_string(), .to_string(),
); );
let errors = [ let errors = [
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.impl"), &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.impl"),
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.mixin"), &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.impl"),
&format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.mixin"), &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.mixin"),
"org.quiltmc.loader.impl.FormattedException: java.lang.NoSuchMethodError:", "org.quiltmc.loader.impl.FormattedException: java.lang.NoSuchMethodError:",
]; ];
let found = errors.iter().any(|e| log.contains(e)); let found = errors.iter().any(|e| log.contains(e));
found.then_some(issue) found.then_some(issue)
} }
fn flatpak_nvidia(log: &str) -> Issue { fn flatpak_nvidia(log: &str) -> Issue {
let issue = ( let issue = (
"Outdated Nvidia Flatpak Driver".to_string(), "Outdated Nvidia Flatpak Driver".to_string(),
"The Nvidia driver for flatpak is outdated. "The Nvidia driver for flatpak is outdated.
Please run `flatpak update` to fix this issue. \ Please run `flatpak update` to fix this issue. \
If that does not solve it, \ If that does not solve it, \
please wait until the driver is added to Flathub and run it again." 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") let found = log.contains("org.lwjgl.LWJGLException: Could not choose GLX13 config")
|| log.contains("GLFW error 65545: GLX: Failed to find a suitable GLXFBConfig"); || 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 { fn forge_java(log: &str) -> Issue {
let issue = ( let issue = (
"Forge Java Bug".to_string(), "Forge Java Bug".to_string(),
"Old versions of Forge crash with Java 8u321+. "Old versions of Forge crash with Java 8u321+.
To fix this, update forge to the latest version via the Versions tab 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) (right click on Forge, click Change Version, and choose the latest one)
Alternatively, you can download 8u312 or lower. \ Alternatively, you can download 8u312 or lower. \
See [archive](https://github.com/adoptium/temurin8-binaries/releases/tag/jdk8u312-b07)" 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"); let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V");
found.then_some(issue) found.then_some(issue)
} }
fn intel_hd(log: &str) -> Issue { fn intel_hd(log: &str) -> Issue {
let issue = let issue =
( (
"Intel HD Windows 10".to_string(), "Intel HD Windows 10".to_string(),
"Your drivers don't support windows 10 officially "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() 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"); let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V");
found.then_some(issue) found.then_some(issue)
} }
fn java_option(log: &str) -> Issue { fn java_option(log: &str) -> Issue {
static VM_OPTION_REGEX: Lazy<Regex> = static VM_OPTION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Unrecognized VM option '(.+)'[\r\n]").unwrap()); Lazy::new(|| Regex::new(r"Unrecognized VM option '(.+)'[\r\n]").unwrap());
static OPTION_REGEX: Lazy<Regex> = static OPTION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Unrecognized option: (.+)[\r\n]").unwrap()); Lazy::new(|| Regex::new(r"Unrecognized option: (.+)[\r\n]").unwrap());
if let Some(captures) = VM_OPTION_REGEX.captures(log) { if let Some(captures) = VM_OPTION_REGEX.captures(log) {
let title = if &captures[1] == "UseShenandoahGC" { let title = if &captures[1] == "UseShenandoahGC" {
"Wrong Java Arguments" "Wrong Java Arguments"
} else { } else {
"Java 8 and below don't support ShenandoahGC" "Java 8 and below don't support ShenandoahGC"
}; };
return Some(( return Some((
title.to_string(), title.to_string(),
format!("Remove `-XX:{}` from your Java arguments", &captures[1]), format!("Remove `-XX:{}` from your Java arguments", &captures[1]),
)); ));
} }
if let Some(captures) = OPTION_REGEX.captures(log) { if let Some(captures) = OPTION_REGEX.captures(log) {
return Some(( return Some((
"Wrong Java Arguments".to_string(), "Wrong Java Arguments".to_string(),
format!("Remove `{}` from your Java arguments", &captures[1]), format!("Remove `{}` from your Java arguments", &captures[1]),
)); ));
} }
None None
} }
fn lwjgl_2_java_9(log: &str) -> Issue { fn lwjgl_2_java_9(log: &str) -> Issue {
let issue = ( let issue = (
"Linux: crash with pre-1.13 and Java 9+".to_string(), "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. \ "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. Switching to Java 8 or below will fix your issue.
Alternatively, you can use [Temurin](https://adoptium.net/temurin/releases). \ Alternatively, you can use [Temurin](https://adoptium.net/temurin/releases). \
However, multiplayer will not work in versions from 1.8 to 1.11. However, multiplayer will not work in versions from 1.8 to 1.11.
For more information, type `/tag java`." 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!"); let found = log.contains("check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!");
found.then_some(issue) found.then_some(issue)
} }
fn macos_ns(log: &str) -> Issue { fn macos_ns(log: &str) -> Issue {
let issue = ( let issue = (
"MacOS NSInternalInconsistencyException".to_string(), "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() "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 = let found =
log.contains("Terminating app due to uncaught exception 'NSInternalInconsistencyException"); log.contains("Terminating app due to uncaught exception 'NSInternalInconsistencyException");
found.then_some(issue) found.then_some(issue)
} }
fn oom(log: &str) -> Issue { fn oom(log: &str) -> Issue {
let issue = ( let issue = (
"Out of Memory".to_string(), "Out of Memory".to_string(),
"Allocating more RAM to your instance could help prevent this crash.".to_string(), "Allocating more RAM to your instance could help prevent this crash.".to_string(),
); );
let found = log.contains("java.lang.OutOfMemoryError"); let found = log.contains("java.lang.OutOfMemoryError");
found.then_some(issue) found.then_some(issue)
} }
fn optinotfine(log: &str) -> Issue { fn optinotfine(log: &str) -> Issue {
let issue = ( let issue = (
"Potential OptiFine Incompatibilities".to_string(), "Potential OptiFine Incompatibilities".to_string(),
"OptiFine is known to cause problems when paired with other mods. \ "OptiFine is known to cause problems when paired with other mods. \
Try to disable OptiFine and see if the issue persists. Try to disable OptiFine and see if the issue persists.
@ -172,50 +172,50 @@ fn optinotfine(log: &str) -> Issue {
.to_string(), .to_string(),
); );
let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-"); let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-");
found.then_some(issue) found.then_some(issue)
} }
async fn outdated_launcher(log: &str, data: &Data) -> Result<Issue> { async fn outdated_launcher(log: &str, data: &Data) -> Result<Issue> {
static OUTDATED_LAUNCHER_REGEX: Lazy<Regex> = static OUTDATED_LAUNCHER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new("Prism Launcher version: [0-9].[0-9].[0-9]").unwrap()); Lazy::new(|| Regex::new("Prism Launcher version: [0-9].[0-9].[0-9]").unwrap());
let Some(captures) = OUTDATED_LAUNCHER_REGEX.captures(log) else { let Some(captures) = OUTDATED_LAUNCHER_REGEX.captures(log) else {
return Ok(None); 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 storage = &data.storage;
let latest_version = if storage.launcher_version_is_cached().await? { let latest_version = if storage.launcher_version_is_cached().await? {
storage.get_launcher_version().await? storage.get_launcher_version().await?
} else { } else {
let version = data let version = data
.octocrab .octocrab
.repos("PrismLauncher", "PrismLauncher") .repos("PrismLauncher", "PrismLauncher")
.releases() .releases()
.get_latest() .get_latest()
.await? .await?
.tag_name; .tag_name;
storage.cache_launcher_version(&version).await?; storage.cache_launcher_version(&version).await?;
version version
}; };
if version_from_log < latest_version { if version_from_log < latest_version {
let issue = ( let issue = (
"Outdated Prism Launcher".to_string(), "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/") 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)) Ok(Some(issue))
} else { } else {
Ok(None) Ok(None)
} }
} }
fn pre_1_12_native_transport_java_9(log: &str) -> Issue { 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(), "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. "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(), .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" "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 { fn wrong_java(log: &str) -> Issue {
static SWITCH_VERSION_REGEX: Lazy<Regex> = Lazy::new(|| { static SWITCH_VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new( Regex::new(
r"(?m)Please switch to one of the following Java versions for this instance:[\r\n]+(Java version [\d.]+)", r"(?m)Please switch to one of the following Java versions for this instance:[\r\n]+(Java version [\d.]+)",
).unwrap() ).unwrap()
}); });
if let Some(captures) = SWITCH_VERSION_REGEX.captures(log) { if let Some(captures) = SWITCH_VERSION_REGEX.captures(log) {
let versions = captures[1].split('\n').collect::<Vec<&str>>().join(", "); let versions = captures[1].split('\n').collect::<Vec<&str>>().join(", ");
return Some(( return Some((
"Wrong Java Version".to_string(), "Wrong Java Version".to_string(),
format!("Please switch to one of the following: `{versions}`\nFor more information, type `/tag java`"), 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(), "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() "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.") log.contains("Java major version is incompatible. Things might break.")
.then_some(issue) .then_some(issue)
} }

View file

@ -12,57 +12,57 @@ use issues::find_issues;
use providers::find_log; use providers::find_log;
pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> { 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() { if log.is_err() {
channel channel
.send_message(ctx, |m| { .send_message(ctx, |m| {
m.reference_message(message) m.reference_message(message)
.allowed_mentions(|am| am.replied_user(true)) .allowed_mentions(|am| am.replied_user(true))
.embed(|e| { .embed(|e| {
e.title("Analyze failed!") e.title("Analyze failed!")
.description("Couldn't download log") .description("Couldn't download log")
}) })
}) })
.await?; .await?;
return Ok(()); return Ok(());
} }
let Some(log) = log? else { let Some(log) = log? else {
debug!("No log found in message! Skipping analysis"); debug!("No log found in message! Skipping analysis");
return Ok(()); return Ok(());
}; };
let issues = find_issues(&log, data).await?; let issues = find_issues(&log, data).await?;
channel channel
.send_message(ctx, |m| { .send_message(ctx, |m| {
m.reference_message(message) m.reference_message(message)
.allowed_mentions(|am| am.replied_user(true)) .allowed_mentions(|am| am.replied_user(true))
.embed(|e| { .embed(|e| {
e.title("Log analysis"); e.title("Log analysis");
if issues.is_empty() { if issues.is_empty() {
e.color(COLORS["green"]).field( e.color(COLORS["green"]).field(
"Analyze failed!", "Analyze failed!",
"No issues found automatically", "No issues found automatically",
false, false,
); );
} else { } else {
e.color(COLORS["red"]); e.color(COLORS["red"]);
for (title, description) in issues { for (title, description) in issues {
e.field(title, description, false); e.field(title, description, false);
} }
} }
e e
}) })
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -8,17 +8,17 @@ use reqwest::StatusCode;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap()); static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://0x0\.st/\w*\.\w*").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> { pub async fn find(content: &str) -> Result<Option<String>> {
let Some(url) = REGEX.find(content).map(|m| &content[m.range()]) else { let Some(url) = REGEX.find(content).map(|m| &content[m.range()]) else {
return Ok(None); return Ok(None);
}; };
let request = REQWEST_CLIENT.get(url).build()?; let request = REQWEST_CLIENT.get(url).build()?;
let response = REQWEST_CLIENT.execute(request).await?; let response = REQWEST_CLIENT.execute(request).await?;
let status = response.status(); let status = response.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
Ok(Some(response.text().await?)) Ok(Some(response.text().await?))
} else { } else {
Err(eyre!("Failed to fetch paste from {url} with {status}",)) Err(eyre!("Failed to fetch paste from {url} with {status}",))
} }
} }

View file

@ -2,17 +2,17 @@ use color_eyre::eyre::Result;
use poise::serenity_prelude::Message; use poise::serenity_prelude::Message;
pub async fn find(message: &Message) -> Result<Option<String>> { pub async fn find(message: &Message) -> Result<Option<String>> {
// find first uploaded text file // find first uploaded text file
if let Some(attachment) = message.attachments.iter().find(|a| { if let Some(attachment) = message.attachments.iter().find(|a| {
a.content_type a.content_type
.as_ref() .as_ref()
.and_then(|ct| ct.starts_with("text/").then_some(true)) .and_then(|ct| ct.starts_with("text/").then_some(true))
.is_some() .is_some()
}) { }) {
let bytes = attachment.download().await?; let bytes = attachment.download().await?;
let res = String::from_utf8(bytes)?; let res = String::from_utf8(bytes)?;
Ok(Some(res)) Ok(Some(res))
} else { } else {
Ok(None) Ok(None)
} }
} }

View file

@ -6,21 +6,21 @@ use regex::Regex;
use reqwest::StatusCode; use reqwest::StatusCode;
static REGEX: Lazy<Regex> = 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>> { pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else { let Some(captures) = REGEX.captures(content) else {
return Ok(None); return Ok(None);
}; };
let url = format!("https://hst.sh/raw/{}", &captures[1]); let url = format!("https://hst.sh/raw/{}", &captures[1]);
let request = REQWEST_CLIENT.get(&url).build()?; let request = REQWEST_CLIENT.get(&url).build()?;
let response = REQWEST_CLIENT.execute(request).await?; let response = REQWEST_CLIENT.execute(request).await?;
let status = response.status(); let status = response.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
Ok(Some(response.text().await?)) Ok(Some(response.text().await?))
} else { } else {
Err(eyre!("Failed to fetch paste from {url} with {status}")) Err(eyre!("Failed to fetch paste from {url} with {status}"))
} }
} }

View file

@ -8,18 +8,18 @@ use reqwest::StatusCode;
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap()); static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap());
pub async fn find(content: &str) -> Result<Option<String>> { pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else { let Some(captures) = REGEX.captures(content) else {
return Ok(None); return Ok(None);
}; };
let url = format!("https://api.mclo.gs/1/raw/{}", &captures[1]); let url = format!("https://api.mclo.gs/1/raw/{}", &captures[1]);
let request = REQWEST_CLIENT.get(&url).build()?; let request = REQWEST_CLIENT.get(&url).build()?;
let response = REQWEST_CLIENT.execute(request).await?; let response = REQWEST_CLIENT.execute(request).await?;
let status = response.status(); let status = response.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
Ok(Some(response.text().await?)) Ok(Some(response.text().await?))
} else { } else {
Err(eyre!("Failed to fetch log from {url} with {status}")) Err(eyre!("Failed to fetch log from {url} with {status}"))
} }
} }

View file

@ -12,22 +12,22 @@ mod pastebin;
pub type LogProvider = Result<Option<String>>; pub type LogProvider = Result<Option<String>>;
pub async fn find_log(message: &Message) -> LogProvider { pub async fn find_log(message: &Message) -> LogProvider {
macro_rules! provider_impl { macro_rules! provider_impl {
($provider:ident) => { ($provider:ident) => {
if let Some(content) = $provider::find(&message.content).await? { if let Some(content) = $provider::find(&message.content).await? {
return Ok(Some(content)); return Ok(Some(content));
} }
}; };
} }
provider_impl!(_0x0); provider_impl!(_0x0);
provider_impl!(mclogs); provider_impl!(mclogs);
provider_impl!(haste); provider_impl!(haste);
provider_impl!(paste_gg); provider_impl!(paste_gg);
provider_impl!(pastebin); provider_impl!(pastebin);
if let Some(content) = attachment::find(message).await? { if let Some(content) = attachment::find(message).await? {
return Ok(Some(content)); return Ok(Some(content));
} }
Ok(None) Ok(None)
} }

View file

@ -12,59 +12,59 @@ static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://paste.gg/p/\w+/(\w
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
struct PasteResponse { struct PasteResponse {
status: String, status: String,
result: Option<Vec<PasteResult>>, result: Option<Vec<PasteResult>>,
error: Option<String>, error: Option<String>,
message: Option<String>, message: Option<String>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
struct PasteResult { struct PasteResult {
id: String, id: String,
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
visibility: Option<String>, visibility: Option<String>,
} }
pub async fn find(content: &str) -> Result<Option<String>> { pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else { let Some(captures) = REGEX.captures(content) else {
return Ok(None); return Ok(None);
}; };
let paste_id = &captures[1]; let paste_id = &captures[1];
let files_url = format!("{PASTE_GG}{PASTES_ENDPOINT}/{paste_id}/files"); let files_url = format!("{PASTE_GG}{PASTES_ENDPOINT}/{paste_id}/files");
let resp = REQWEST_CLIENT let resp = REQWEST_CLIENT
.execute(REQWEST_CLIENT.get(&files_url).build()?) .execute(REQWEST_CLIENT.get(&files_url).build()?)
.await?; .await?;
let status = resp.status(); let status = resp.status();
if resp.status() != StatusCode::OK { if resp.status() != StatusCode::OK {
return Err(eyre!( return Err(eyre!(
"Couldn't get paste {paste_id} from {PASTE_GG} with status {status}!" "Couldn't get paste {paste_id} from {PASTE_GG} with status {status}!"
)); ));
} }
let paste_files: PasteResponse = resp.json().await?; let paste_files: PasteResponse = resp.json().await?;
let file_id = &paste_files let file_id = &paste_files
.result .result
.ok_or_else(|| eyre!("Couldn't find any files associated with paste {paste_id}!"))?[0] .ok_or_else(|| eyre!("Couldn't find any files associated with paste {paste_id}!"))?[0]
.id; .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 let resp = REQWEST_CLIENT
.execute(REQWEST_CLIENT.get(&raw_url).build()?) .execute(REQWEST_CLIENT.get(&raw_url).build()?)
.await?; .await?;
let status = resp.status(); let status = resp.status();
if status != StatusCode::OK { if status != StatusCode::OK {
return Err(eyre!( return Err(eyre!(
"Couldn't get file {file_id} from paste {paste_id} with status {status}!" "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))
} }

View file

@ -6,21 +6,21 @@ use regex::Regex;
use reqwest::StatusCode; use reqwest::StatusCode;
static REGEX: Lazy<Regex> = 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>> { pub async fn find(content: &str) -> Result<Option<String>> {
let Some(captures) = REGEX.captures(content) else { let Some(captures) = REGEX.captures(content) else {
return Ok(None); return Ok(None);
}; };
let url = format!("https://pastebin.com/raw/{}", &captures[1]); let url = format!("https://pastebin.com/raw/{}", &captures[1]);
let request = REQWEST_CLIENT.get(&url).build()?; let request = REQWEST_CLIENT.get(&url).build()?;
let response = REQWEST_CLIENT.execute(request).await?; let response = REQWEST_CLIENT.execute(request).await?;
let status = response.status(); let status = response.status();
if let StatusCode::OK = status { if let StatusCode::OK = status {
Ok(Some(response.text().await?)) Ok(Some(response.text().await?))
} else { } else {
Err(eyre!("Failed to fetch paste from {url} with {status}")) Err(eyre!("Failed to fetch paste from {url} with {status}"))
} }
} }

View file

@ -2,24 +2,24 @@ use color_eyre::eyre::{Context as _, Result};
use poise::serenity_prelude::{Context, InteractionType, Reaction}; use poise::serenity_prelude::{Context, InteractionType, Reaction};
pub async fn handle(ctx: &Context, reaction: &Reaction) -> Result<()> { pub async fn handle(ctx: &Context, reaction: &Reaction) -> Result<()> {
let user = reaction let user = reaction
.user(ctx) .user(ctx)
.await .await
.wrap_err_with(|| "Couldn't fetch user from reaction!")?; .wrap_err_with(|| "Couldn't fetch user from reaction!")?;
let message = reaction let message = reaction
.message(ctx) .message(ctx)
.await .await
.wrap_err_with(|| "Couldn't fetch message from reaction!")?; .wrap_err_with(|| "Couldn't fetch message from reaction!")?;
if let Some(interaction) = &message.interaction { if let Some(interaction) = &message.interaction {
if interaction.kind == InteractionType::ApplicationCommand if interaction.kind == InteractionType::ApplicationCommand
&& interaction.user == user && interaction.user == user
&& reaction.emoji.unicode_eq("") && reaction.emoji.unicode_eq("")
{ {
message.delete(ctx).await?; message.delete(ctx).await?;
} }
} }
Ok(()) Ok(())
} }

View file

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

View file

@ -4,25 +4,25 @@ use poise::serenity_prelude::{Context, Message};
use crate::utils; use crate::utils;
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> { 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 // TOOD getchoo: actually reply to user
// ...not sure why Message doesn't give me a builder in reply() or equivalents // ...not sure why Message doesn't give me a builder in reply() or equivalents
let our_channel = message let our_channel = message
.channel(ctx) .channel(ctx)
.await .await
.wrap_err_with(|| "Couldn't get channel from message!")? .wrap_err_with(|| "Couldn't get channel from message!")?
.guild() .guild()
.ok_or_else(|| eyre!("Couldn't convert to GuildChannel!"))?; .ok_or_else(|| eyre!("Couldn't convert to GuildChannel!"))?;
if !embeds.is_empty() { if !embeds.is_empty() {
our_channel our_channel
.send_message(ctx, |m| { .send_message(ctx, |m| {
m.set_embeds(embeds) m.set_embeds(embeds)
.allowed_mentions(|am| am.replied_user(false)) .allowed_mentions(|am| am.replied_user(false))
}) })
.await?; .await?;
} }
Ok(()) Ok(())
} }

View file

@ -13,53 +13,53 @@ pub mod pluralkit;
mod support_onboard; mod support_onboard;
pub async fn handle( pub async fn handle(
ctx: &Context, ctx: &Context,
event: &Event<'_>, event: &Event<'_>,
_framework: FrameworkContext<'_, Data, Report>, _framework: FrameworkContext<'_, Data, Report>,
data: &Data, data: &Data,
) -> Result<()> { ) -> Result<()> {
match event { match event {
Event::Ready { data_about_bot } => { Event::Ready { data_about_bot } => {
info!("Logged in as {}!", data_about_bot.user.name); info!("Logged in as {}!", data_about_bot.user.name);
let latest_minecraft_version = api::prism_meta::get_latest_minecraft_version().await?; let latest_minecraft_version = api::prism_meta::get_latest_minecraft_version().await?;
let activity = Activity::playing(format!("Minecraft {}", latest_minecraft_version)); let activity = Activity::playing(format!("Minecraft {}", latest_minecraft_version));
info!("Setting presence to activity {activity:#?}"); info!("Setting presence to activity {activity:#?}");
ctx.set_presence(Some(activity), OnlineStatus::Online).await; ctx.set_presence(Some(activity), OnlineStatus::Online).await;
} }
Event::Message { new_message } => { Event::Message { new_message } => {
// ignore new messages from bots // ignore new messages from bots
// NOTE: the webhook_id check allows us to still respond to PK users // NOTE: the webhook_id check allows us to still respond to PK users
if new_message.author.bot && new_message.webhook_id.is_none() { if new_message.author.bot && new_message.webhook_id.is_none() {
debug!("Ignoring message {} from bot", new_message.id); debug!("Ignoring message {} from bot", new_message.id);
return Ok(()); return Ok(());
} }
// detect PK users first to make sure we don't respond to unproxied messages // detect PK users first to make sure we don't respond to unproxied messages
pluralkit::handle(ctx, new_message, data).await?; pluralkit::handle(ctx, new_message, data).await?;
if data.storage.is_user_plural(new_message.author.id).await? if data.storage.is_user_plural(new_message.author.id).await?
&& pluralkit::is_message_proxied(new_message).await? && pluralkit::is_message_proxied(new_message).await?
{ {
debug!("Not replying to unproxied PluralKit message"); debug!("Not replying to unproxied PluralKit message");
return Ok(()); return Ok(());
} }
eta::handle(ctx, new_message).await?; eta::handle(ctx, new_message).await?;
expand_link::handle(ctx, new_message).await?; expand_link::handle(ctx, new_message).await?;
analyze_logs::handle(ctx, new_message, data).await?; analyze_logs::handle(ctx, new_message, data).await?;
} }
Event::ReactionAdd { add_reaction } => { Event::ReactionAdd { add_reaction } => {
delete_on_reaction::handle(ctx, add_reaction).await? 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(())
} }

View file

@ -9,34 +9,34 @@ use tokio::time::sleep;
const PK_DELAY_SEC: Duration = Duration::from_secs(1000); const PK_DELAY_SEC: Duration = Duration::from_secs(1000);
pub async fn is_message_proxied(message: &Message) -> Result<bool> { pub async fn is_message_proxied(message: &Message) -> Result<bool> {
debug!( debug!(
"Waiting on PluralKit API for {} seconds", "Waiting on PluralKit API for {} seconds",
PK_DELAY_SEC.as_secs() PK_DELAY_SEC.as_secs()
); );
sleep(PK_DELAY_SEC).await; 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<()> { pub async fn handle(_ctx: &Context, msg: &Message, data: &Data) -> Result<()> {
if msg.webhook_id.is_some() { if msg.webhook_id.is_some() {
debug!( debug!(
"Message {} has a webhook ID. Checking if it was sent through PluralKit", "Message {} has a webhook ID. Checking if it was sent through PluralKit",
msg.id msg.id
); );
debug!( debug!(
"Waiting on PluralKit API for {} seconds", "Waiting on PluralKit API for {} seconds",
PK_DELAY_SEC.as_secs() PK_DELAY_SEC.as_secs()
); );
sleep(PK_DELAY_SEC).await; sleep(PK_DELAY_SEC).await;
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await { if let Ok(sender) = api::pluralkit::get_sender(msg.id).await {
data.storage.store_user_plurality(sender).await?; data.storage.store_user_plurality(sender).await?;
} }
} }
Ok(()) Ok(())
} }

View file

@ -3,41 +3,41 @@ use log::*;
use poise::serenity_prelude::{ChannelType, Context, GuildChannel}; use poise::serenity_prelude::{ChannelType, Context, GuildChannel};
pub async fn handle(ctx: &Context, thread: &GuildChannel) -> Result<()> { pub async fn handle(ctx: &Context, thread: &GuildChannel) -> Result<()> {
if thread.kind != ChannelType::PublicThread { if thread.kind != ChannelType::PublicThread {
return Ok(()); return Ok(());
} }
let parent_id = thread let parent_id = thread
.parent_id .parent_id
.ok_or_else(|| eyre!("Couldn't get parent ID from thread {}!", thread.name))?; .ok_or_else(|| eyre!("Couldn't get parent ID from thread {}!", thread.name))?;
let parent_channel = ctx let parent_channel = ctx
.cache .cache
.guild_channel(parent_id) .guild_channel(parent_id)
.ok_or_else(|| eyre!("Couldn't get GuildChannel {}!", parent_id))?; .ok_or_else(|| eyre!("Couldn't get GuildChannel {}!", parent_id))?;
if parent_channel.name != "support" { if parent_channel.name != "support" {
debug!("Not posting onboarding message to threads outside of support"); debug!("Not posting onboarding message to threads outside of support");
return Ok(()); return Ok(());
} }
let owner = thread let owner = thread
.owner_id .owner_id
.ok_or_else(|| eyre!("Couldn't get owner of thread!"))?; .ok_or_else(|| eyre!("Couldn't get owner of thread!"))?;
let msg = format!( let msg = format!(
"<@{}> We've received your support ticket! {} {}", "<@{}> We've received your support ticket! {} {}",
owner, owner,
"Please upload your logs and post the link here if possible (run `tag log` to find out how).", "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." "Please don't ping people for support questions, unless you have their permission."
); );
thread thread
.send_message(ctx, |m| { .send_message(ctx, |m| {
m.content(msg) m.content(msg)
.allowed_mentions(|am| am.replied_user(true).users(Vec::from([owner]))) .allowed_mentions(|am| am.replied_user(true).users(Vec::from([owner])))
}) })
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -7,7 +7,7 @@ use color_eyre::owo_colors::OwoColorize;
use log::*; use log::*;
use poise::{ use poise::{
serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions,
}; };
use serenity::ShardManager; use serenity::ShardManager;
@ -34,108 +34,108 @@ type Context<'a> = poise::Context<'a, Data, Report>;
#[derive(Clone)] #[derive(Clone)]
pub struct Data { pub struct Data {
config: Config, config: Config,
storage: Storage, storage: Storage,
octocrab: Arc<octocrab::Octocrab>, octocrab: Arc<octocrab::Octocrab>,
} }
impl Data { impl Data {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let config = Config::new_from_env()?; let config = Config::new_from_env()?;
let storage = Storage::new(&config.redis_url)?; let storage = Storage::new(&config.redis_url)?;
let octocrab = octocrab::instance(); let octocrab = octocrab::instance();
Ok(Self { Ok(Self {
config, config,
storage, storage,
octocrab, octocrab,
}) })
} }
} }
async fn setup( async fn setup(
ctx: &serenity::Context, ctx: &serenity::Context,
_ready: &serenity::Ready, _ready: &serenity::Ready,
framework: &Framework<Data, Report>, framework: &Framework<Data, Report>,
) -> Result<Data> { ) -> Result<Data> {
let data = Data::new()?; let data = Data::new()?;
// test redis connection // test redis connection
let mut client = data.storage.client.clone(); let mut client = data.storage.client.clone();
if !client.check_connection() { if !client.check_connection() {
return Err(eyre!( return Err(eyre!(
"Couldn't connect to storage! Is your daemon running?" "Couldn't connect to storage! Is your daemon running?"
)); ));
} }
poise::builtins::register_globally(ctx, &framework.options().commands).await?; poise::builtins::register_globally(ctx, &framework.options().commands).await?;
info!("Registered global commands!"); info!("Registered global commands!");
Ok(data) Ok(data)
} }
async fn handle_shutdown(shard_manager: Arc<Mutex<ShardManager>>, reason: &str) { async fn handle_shutdown(shard_manager: Arc<Mutex<ShardManager>>, reason: &str) {
warn!("{reason}! Shutting down bot..."); warn!("{reason}! Shutting down bot...");
shard_manager.lock().await.shutdown_all().await; shard_manager.lock().await.shutdown_all().await;
println!("{}", "Everything is shutdown. Goodbye!".green()) println!("{}", "Everything is shutdown. Goodbye!".green())
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
color_eyre::install()?; color_eyre::install()?;
env_logger::init(); env_logger::init();
let token = std::env::var("DISCORD_BOT_TOKEN") let token = std::env::var("DISCORD_BOT_TOKEN")
.wrap_err_with(|| "Couldn't find bot token in environment!")?; .wrap_err_with(|| "Couldn't find bot token in environment!")?;
let intents = let intents =
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
let options = FrameworkOptions { let options = FrameworkOptions {
commands: commands::to_global_commands(), 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| { command_check: Some(|ctx| {
Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) })
}), }),
event_handler: |ctx, event, framework, data| { event_handler: |ctx, event, framework, data| {
Box::pin(handlers::handle_event(ctx, event, framework, data)) Box::pin(handlers::handle_event(ctx, event, framework, data))
}, },
prefix_options: PrefixFrameworkOptions { prefix_options: PrefixFrameworkOptions {
prefix: Some("r".into()), prefix: Some("r".into()),
edit_tracker: Some(EditTracker::for_timespan(Duration::from_secs(3600))), edit_tracker: Some(EditTracker::for_timespan(Duration::from_secs(3600))),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}; };
let framework = Framework::builder() let framework = Framework::builder()
.token(token) .token(token)
.intents(intents) .intents(intents)
.options(options) .options(options)
.setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework))) .setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework)))
.build() .build()
.await .await
.wrap_err_with(|| "Failed to build framework!")?; .wrap_err_with(|| "Failed to build framework!")?;
let shard_manager = framework.shard_manager().clone(); let shard_manager = framework.shard_manager().clone();
let mut sigterm = signal(SignalKind::terminate())?; let mut sigterm = signal(SignalKind::terminate())?;
tokio::select! { tokio::select! {
result = framework.start() => result.map_err(Report::from), result = framework.start() => result.map_err(Report::from),
_ = sigterm.recv() => { _ = sigterm.recv() => {
handle_shutdown(shard_manager, "Recieved SIGTERM").await; handle_shutdown(shard_manager, "Recieved SIGTERM").await;
std::process::exit(0); std::process::exit(0);
} }
_ = ctrl_c() => { _ = ctrl_c() => {
handle_shutdown(shard_manager, "Interrupted").await; handle_shutdown(shard_manager, "Interrupted").await;
std::process::exit(130); std::process::exit(130);
} }
} }
} }

View file

@ -10,105 +10,105 @@ const LAUNCHER_VERSION_KEY: &str = "launcher-version-v1";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Storage { pub struct Storage {
pub client: Client, pub client: Client,
} }
impl Storage { impl Storage {
pub fn new(redis_url: &str) -> Result<Self> { pub fn new(redis_url: &str) -> Result<Self> {
let client = Client::open(redis_url)?; let client = Client::open(redis_url)?;
Ok(Self { client }) Ok(Self { client })
} }
/* /*
these are mainly light abstractions to avoid the `let mut con` these are mainly light abstractions to avoid the `let mut con`
boilerplate, as well as not require the caller to format the boilerplate, as well as not require the caller to format the
strings for keys strings for keys
*/ */
async fn get_key<T>(&self, key: &str) -> Result<T> async fn get_key<T>(&self, key: &str) -> Result<T>
where where
T: FromRedisValue, T: FromRedisValue,
{ {
debug!("Getting key {key}"); debug!("Getting key {key}");
let mut con = self.client.get_async_connection().await?; let mut con = self.client.get_async_connection().await?;
let res: T = con.get(key).await?; let res: T = con.get(key).await?;
Ok(res) Ok(res)
} }
async fn set_key<'a>( async fn set_key<'a>(
&self, &self,
key: &str, key: &str,
value: impl ToRedisArgs + Debug + Send + Sync + 'a, value: impl ToRedisArgs + Debug + Send + Sync + 'a,
) -> Result<()> { ) -> Result<()> {
debug!("Creating key {key}:\n{value:#?}"); debug!("Creating key {key}:\n{value:#?}");
let mut con = self.client.get_async_connection().await?; let mut con = self.client.get_async_connection().await?;
con.set(key, value).await?; con.set(key, value).await?;
Ok(()) Ok(())
} }
async fn key_exists(&self, key: &str) -> Result<bool> { async fn key_exists(&self, key: &str) -> Result<bool> {
debug!("Checking if key {key} exists"); debug!("Checking if key {key} exists");
let mut con = self.client.get_async_connection().await?; let mut con = self.client.get_async_connection().await?;
let exists: u64 = con.exists(key).await?; let exists: u64 = con.exists(key).await?;
Ok(exists > 0) Ok(exists > 0)
} }
async fn delete_key(&self, key: &str) -> Result<()> { async fn delete_key(&self, key: &str) -> Result<()> {
debug!("Deleting key {key}"); debug!("Deleting key {key}");
let mut con = self.client.get_async_connection().await?; let mut con = self.client.get_async_connection().await?;
con.del(key).await?; con.del(key).await?;
Ok(()) Ok(())
} }
async fn expire_key(&self, key: &str, expire_seconds: usize) -> Result<()> { async fn expire_key(&self, key: &str, expire_seconds: usize) -> Result<()> {
debug!("Expiring key {key} in {expire_seconds}"); debug!("Expiring key {key} in {expire_seconds}");
let mut con = self.client.get_async_connection().await?; let mut con = self.client.get_async_connection().await?;
con.expire(key, expire_seconds).await?; con.expire(key, expire_seconds).await?;
Ok(()) Ok(())
} }
pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> { pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> {
info!("Marking {sender} as a PluralKit user"); info!("Marking {sender} as a PluralKit user");
let key = format!("{PK_KEY}:{sender}"); let key = format!("{PK_KEY}:{sender}");
// Just store some value. We only care about the presence of this key // Just store some value. We only care about the presence of this key
self.set_key(&key, 0).await?; self.set_key(&key, 0).await?;
self.expire_key(&key, 7 * 24 * 60 * 60).await?; self.expire_key(&key, 7 * 24 * 60 * 60).await?;
Ok(()) Ok(())
} }
pub async fn is_user_plural(&self, user_id: UserId) -> Result<bool> { pub async fn is_user_plural(&self, user_id: UserId) -> Result<bool> {
let key = format!("{PK_KEY}:{user_id}"); let key = format!("{PK_KEY}:{user_id}");
self.key_exists(&key).await self.key_exists(&key).await
} }
pub async fn cache_launcher_version(&self, version: &str) -> Result<()> { pub async fn cache_launcher_version(&self, version: &str) -> Result<()> {
self.set_key(LAUNCHER_VERSION_KEY, version).await?; self.set_key(LAUNCHER_VERSION_KEY, version).await?;
Ok(()) Ok(())
} }
pub async fn get_launcher_version(&self) -> Result<String> { pub async fn get_launcher_version(&self) -> Result<String> {
let res = self.get_key(LAUNCHER_VERSION_KEY).await?; let res = self.get_key(LAUNCHER_VERSION_KEY).await?;
Ok(res) Ok(res)
} }
pub async fn launcher_version_is_cached(&self) -> Result<bool> { pub async fn launcher_version_is_cached(&self) -> Result<bool> {
let res = self.key_exists(LAUNCHER_VERSION_KEY).await?; let res = self.key_exists(LAUNCHER_VERSION_KEY).await?;
Ok(res) Ok(res)
} }
} }

View file

@ -6,15 +6,15 @@ pub const TAG_DIR: &str = "tags";
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TagFrontmatter { pub struct TagFrontmatter {
pub title: String, pub title: String,
pub color: Option<String>, pub color: Option<String>,
pub image: Option<String>, pub image: Option<String>,
pub fields: Option<Vec<EmbedField>>, pub fields: Option<Vec<EmbedField>>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Tag { pub struct Tag {
pub content: String, pub content: String,
pub file_name: String, pub file_name: String,
pub frontmatter: TagFrontmatter, pub frontmatter: TagFrontmatter,
} }

View file

@ -1,6 +1,6 @@
#[macro_export] #[macro_export]
macro_rules! required_var { macro_rules! required_var {
($name: expr) => { ($name: expr) => {
std::env::var($name).wrap_err_with(|| format!("Couldn't find {} in environment!", $name))? std::env::var($name).wrap_err_with(|| format!("Couldn't find {} in environment!", $name))?
}; };
} }

View file

@ -11,10 +11,10 @@ pub use resolve_message::resolve as resolve_message;
* chooses a random element from an array * chooses a random element from an array
*/ */
pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String> { pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let resp = arr let resp = arr
.choose(&mut rng) .choose(&mut rng)
.ok_or_else(|| eyre!("Couldn't choose random object from array:\n{arr:#?}!"))?; .ok_or_else(|| eyre!("Couldn't choose random object from array:\n{arr:#?}!"))?;
Ok((*resp).to_string()) Ok((*resp).to_string())
} }

View file

@ -5,120 +5,120 @@ use poise::serenity_prelude::{ChannelType, Colour, Context, CreateEmbed, Message
use regex::Regex; use regex::Regex;
static MESSAGE_PATTERN: Lazy<Regex> = Lazy::new(|| { 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> { pub fn find_first_image(msg: &Message) -> Option<String> {
msg.attachments msg.attachments
.iter() .iter()
.find(|a| { .find(|a| {
a.content_type a.content_type
.as_ref() .as_ref()
.unwrap_or(&"".to_string()) .unwrap_or(&"".to_string())
.starts_with("image/") .starts_with("image/")
}) })
.map(|res| res.url.clone()) .map(|res| res.url.clone())
} }
pub async fn resolve(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> { pub async fn resolve(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> {
let matches = MESSAGE_PATTERN.captures_iter(&msg.content); let matches = MESSAGE_PATTERN.captures_iter(&msg.content);
let mut embeds: Vec<CreateEmbed> = vec![]; let mut embeds: Vec<CreateEmbed> = vec![];
for captured in matches.take(3) { for captured in matches.take(3) {
// don't leak messages from other servers // don't leak messages from other servers
if let Some(server_id) = captured.get(0) { if let Some(server_id) = captured.get(0) {
let other_server: u64 = server_id.as_str().parse().unwrap_or_default(); let other_server: u64 = server_id.as_str().parse().unwrap_or_default();
let current_id = msg.guild_id.unwrap_or_default(); let current_id = msg.guild_id.unwrap_or_default();
if &other_server != current_id.as_u64() { if &other_server != current_id.as_u64() {
debug!("Not resolving message of other guild."); debug!("Not resolving message of other guild.");
continue; continue;
} }
} else { } else {
warn!("Couldn't find server_id from Discord link! Not resolving message to be safe"); warn!("Couldn't find server_id from Discord link! Not resolving message to be safe");
continue; continue;
} }
if let Some(channel_id) = captured.get(1) { if let Some(channel_id) = captured.get(1) {
let parsed: u64 = channel_id.as_str().parse().unwrap_or_default(); let parsed: u64 = channel_id.as_str().parse().unwrap_or_default();
let req_channel = ctx let req_channel = ctx
.cache .cache
.channel(parsed) .channel(parsed)
.ok_or_else(|| eyre!("Couldn't get channel_id from Discord regex!"))? .ok_or_else(|| eyre!("Couldn't get channel_id from Discord regex!"))?
.guild() .guild()
.ok_or_else(|| { .ok_or_else(|| {
eyre!("Couldn't convert to GuildChannel from channel_id {parsed}!") eyre!("Couldn't convert to GuildChannel from channel_id {parsed}!")
})?; })?;
if !req_channel.is_text_based() { if !req_channel.is_text_based() {
debug!("Not resolving message is non-text-based channel."); debug!("Not resolving message is non-text-based channel.");
continue; continue;
} }
if req_channel.kind == ChannelType::PrivateThread { if req_channel.kind == ChannelType::PrivateThread {
if let Some(id) = req_channel.parent_id { if let Some(id) = req_channel.parent_id {
let parent = ctx.cache.guild_channel(id).ok_or_else(|| { let parent = ctx.cache.guild_channel(id).ok_or_else(|| {
eyre!("Couldn't get parent channel {id} for thread {req_channel}!") eyre!("Couldn't get parent channel {id} for thread {req_channel}!")
})?; })?;
let parent_members = parent.members(ctx).await.unwrap_or_default(); let parent_members = parent.members(ctx).await.unwrap_or_default();
if !parent_members.iter().any(|m| m.user.id == msg.author.id) { 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."); debug!("Not resolving message for user not a part of a private thread.");
continue; continue;
} }
} }
} else if req_channel } else if req_channel
.members(ctx) .members(ctx)
.await? .await?
.iter() .iter()
.any(|m| m.user.id == msg.author.id) .any(|m| m.user.id == msg.author.id)
{ {
debug!("Not resolving for message for user not a part of a channel"); debug!("Not resolving for message for user not a part of a channel");
continue; continue;
} }
let message_id: u64 = captured let message_id: u64 = captured
.get(2) .get(2)
.ok_or_else(|| eyre!("Couldn't get message_id from Discord regex!"))? .ok_or_else(|| eyre!("Couldn't get message_id from Discord regex!"))?
.as_str() .as_str()
.parse() .parse()
.wrap_err_with(|| { .wrap_err_with(|| {
eyre!("Couldn't parse message_id from Discord regex as a MessageId!") eyre!("Couldn't parse message_id from Discord regex as a MessageId!")
})?; })?;
let original_message = req_channel.message(ctx, message_id).await?; let original_message = req_channel.message(ctx, message_id).await?;
let mut embed = CreateEmbed::default(); let mut embed = CreateEmbed::default();
embed embed
.author(|a| { .author(|a| {
a.name(original_message.author.tag()) a.name(original_message.author.tag())
.icon_url(original_message.author.default_avatar_url()) .icon_url(original_message.author.default_avatar_url())
}) })
.color(Colour::BLITZ_BLUE) .color(Colour::BLITZ_BLUE)
.timestamp(original_message.timestamp) .timestamp(original_message.timestamp)
.footer(|f| f.text(format!("#{}", req_channel.name))) .footer(|f| f.text(format!("#{}", req_channel.name)))
.description(format!( .description(format!(
"{}\n\n[Jump to original message]({})", "{}\n\n[Jump to original message]({})",
original_message.content, original_message.content,
original_message.link() original_message.link()
)); ));
if !original_message.attachments.is_empty() { if !original_message.attachments.is_empty() {
embed.fields(original_message.attachments.iter().map(|a| { embed.fields(original_message.attachments.iter().map(|a| {
( (
"Attachments".to_string(), "Attachments".to_string(),
format!("[{}]({})", a.filename, a.url), format!("[{}]({})", a.filename, a.url),
false, false,
) )
})); }));
if let Some(image) = find_first_image(msg) { if let Some(image) = find_first_image(msg) {
embed.image(image); embed.image(image);
} }
} }
embeds.push(embed); embeds.push(embed);
} }
} }
Ok(embeds) Ok(embeds)
} }