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
#[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()
);
}

View file

@ -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!"))
}
}

View file

@ -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()
});

View file

@ -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}",
))
}
}
}

View file

@ -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}",
))
}
}
}

View file

@ -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}",
))
}
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(),
]
}

View file

@ -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,
})
}
}

View file

@ -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()
})
}
}

View file

@ -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()
})
}
}

View file

@ -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",
];

View file

@ -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:#?}");
}
}
}
}

View file

@ -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)
}

View file

@ -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(())
}

View file

@ -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}",))
}
}

View file

@ -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)
}
}

View file

@ -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}"))
}
}

View file

@ -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}"))
}
}

View file

@ -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)
}

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)]
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))
}

View file

@ -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}"))
}
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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);
}
}
}

View file

@ -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)
}
}

View file

@ -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,
}

View file

@ -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))?
};
}

View file

@ -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())
}

View file

@ -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)
}