This commit is contained in:
seth 2024-03-31 17:26:16 -04:00
parent 9dfc3b21ff
commit 0b0779f8b7
No known key found for this signature in database
GPG key ID: D31BD0D494BBEE86
32 changed files with 137 additions and 181 deletions

12
Cargo.lock generated
View file

@ -263,7 +263,7 @@ dependencies = [
"eyre", "eyre",
"indenter", "indenter",
"once_cell", "once_cell",
"owo-colors 3.5.0", "owo-colors",
"tracing-error", "tracing-error",
] ]
@ -274,7 +274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"owo-colors 3.5.0", "owo-colors",
"tracing-core", "tracing-core",
"tracing-error", "tracing-error",
] ]
@ -1235,12 +1235,6 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "owo-colors"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1459,7 +1453,6 @@ dependencies = [
"gray_matter", "gray_matter",
"log", "log",
"octocrab", "octocrab",
"owo-colors 4.0.0",
"poise", "poise",
"redis", "redis",
"regex", "regex",
@ -1467,7 +1460,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"url",
] ]
[[package]] [[package]]

View file

@ -23,7 +23,6 @@ eyre = "0.6.11"
log = "0.4.20" log = "0.4.20"
poise = "0.6.1" poise = "0.6.1"
octocrab = "0.37.0" octocrab = "0.37.0"
owo-colors = "4.0.0"
redis = { version = "0.25.2", features = ["tokio-comp", "tokio-rustls-comp"] } redis = { version = "0.25.2", features = ["tokio-comp", "tokio-rustls-comp"] }
regex = "1.10.3" regex = "1.10.3"
reqwest = { version = "0.12.2", default-features = false, features = [ reqwest = { version = "0.12.2", default-features = false, features = [
@ -37,7 +36,17 @@ tokio = { version = "1.35.1", features = [
"rt-multi-thread", "rt-multi-thread",
"signal", "signal",
] } ] }
url = { version = "2.5.0", features = ["serde"] }
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
complexity = "warn"
correctness = "deny"
pedantic = "warn"
perf = "warn"
style = "warn"
suspicious = "deny"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

View file

@ -6,7 +6,7 @@ use gray_matter::{engine, Matter};
include!("src/tags.rs"); 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();

View file

@ -1,12 +1,12 @@
use std::sync::{Arc, OnceLock}; use std::sync::OnceLock;
use eyre::{Context, OptionExt, Result}; use eyre::{Context, OptionExt, Result};
use log::debug; use log::debug;
use octocrab::Octocrab; use octocrab::Octocrab;
fn octocrab() -> &'static Arc<Octocrab> { fn octocrab() -> &'static Octocrab {
static OCTOCRAB: OnceLock<Arc<Octocrab>> = OnceLock::new(); static OCTOCRAB: OnceLock<Octocrab> = OnceLock::new();
OCTOCRAB.get_or_init(octocrab::instance) OCTOCRAB.get_or_init(Octocrab::default)
} }
pub async fn get_latest_prism_version() -> Result<String> { pub async fn get_latest_prism_version() -> Result<String> {

View file

@ -3,6 +3,7 @@ use std::sync::OnceLock;
use eyre::Result; use eyre::Result;
use log::debug; use log::debug;
use reqwest::{Client, Response}; use reqwest::{Client, Response};
use serde::de::DeserializeOwned;
pub mod dadjoke; pub mod dadjoke;
pub mod github; pub mod github;
@ -44,3 +45,10 @@ pub async fn bytes_from_url(url: &str) -> Result<Vec<u8>> {
let bytes = resp.bytes().await?; let bytes = resp.bytes().await?;
Ok(bytes.to_vec()) Ok(bytes.to_vec())
} }
pub async fn json_from_url<T: DeserializeOwned>(url: &str) -> Result<T> {
let resp = get_url(url).await?;
let json = resp.json().await?;
Ok(json)
}

View file

@ -6,7 +6,7 @@ const PASTE_GG: &str = "https://api.paste.gg/v1";
const PASTES: &str = "/pastes"; const PASTES: &str = "/pastes";
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
enum Status { pub enum Status {
#[serde(rename = "success")] #[serde(rename = "success")]
Success, Success,
#[serde(rename = "error")] #[serde(rename = "error")]
@ -15,10 +15,10 @@ enum Status {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Response<T> { pub struct Response<T> {
status: Status, pub status: Status,
pub result: Option<Vec<T>>, pub result: Option<Vec<T>>,
error: Option<String>, pub error: Option<String>,
message: Option<String>, pub message: Option<String>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -27,12 +27,11 @@ pub struct Files {
pub name: Option<String>, pub name: Option<String>,
} }
pub async fn get_files(id: &str) -> Result<Response<Files>> { pub async fn files_from(id: &str) -> Result<Response<Files>> {
let url = format!("{PASTE_GG}{PASTES}/{id}/files"); let url = format!("{PASTE_GG}{PASTES}/{id}/files");
debug!("Making request to {url}"); debug!("Making request to {url}");
let resp = super::client().get(url).send().await?;
resp.error_for_status_ref()?; let resp: Response<Files> = super::json_from_url(&url).await?;
let resp: Response<Files> = resp.json().await?;
if resp.status == Status::Error { if resp.status == Status::Error {
let message = resp let message = resp

View file

@ -11,17 +11,15 @@ pub struct Message {
const PLURAL_KIT: &str = "https://api.pluralkit.me/v2"; const PLURAL_KIT: &str = "https://api.pluralkit.me/v2";
const MESSAGES: &str = "/messages"; const MESSAGES: &str = "/messages";
pub async fn get_sender(message_id: MessageId) -> Result<UserId> { pub async fn sender_from(message_id: MessageId) -> Result<UserId> {
let url = format!("{PLURAL_KIT}{MESSAGES}/{message_id}"); let url = format!("{PLURAL_KIT}{MESSAGES}/{message_id}");
debug!("Making request to {url}"); debug!("Making request to {url}");
let resp = super::client().get(url).send().await?;
resp.error_for_status_ref()?;
let data: Message = resp.json().await?; let resp: Message = super::json_from_url(&url).await?;
let id: u64 = let id: u64 =
data.sender.parse().wrap_err_with(|| { resp.sender.parse().wrap_err_with(|| {
format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{data:#?}") format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{resp:#?}")
})?; })?;
let sender = UserId::from(id); let sender = UserId::from(id);

View file

@ -14,7 +14,7 @@ pub struct MinecraftPackageJson {
const META: &str = "https://meta.prismlauncher.org/v1"; const META: &str = "https://meta.prismlauncher.org/v1";
const MINECRAFT_PACKAGEJSON: &str = "/net.minecraft/package.json"; const MINECRAFT_PACKAGEJSON: &str = "/net.minecraft/package.json";
pub async fn get_latest_minecraft_version() -> Result<String> { pub async fn latest_minecraft_version() -> Result<String> {
let url = format!("{META}{MINECRAFT_PACKAGEJSON}"); let url = format!("{META}{MINECRAFT_PACKAGEJSON}");
debug!("Making request to {url}"); debug!("Making request to {url}");

View file

@ -1,6 +1,5 @@
use crate::Context; use crate::{Context, Error};
use eyre::Result;
use log::trace; use log::trace;
use poise::samples::HelpConfiguration; use poise::samples::HelpConfiguration;
@ -9,7 +8,7 @@ use poise::samples::HelpConfiguration;
pub async fn help( pub async fn help(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Provide information about a specific command"] command: Option<String>, #[description = "Provide information about a specific command"] command: Option<String>,
) -> Result<()> { ) -> Result<(), Error> {
trace!("Running help command"); trace!("Running help command");
let configuration = HelpConfiguration { let configuration = HelpConfiguration {

View file

@ -1,12 +1,11 @@
use crate::api::dadjoke; use crate::{api::dadjoke, Context, Error};
use crate::Context;
use eyre::Result; use eyre::Result;
use log::trace; use log::trace;
/// It's a joke /// It's a joke
#[poise::command(slash_command, prefix_command, track_edits = true)] #[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn joke(ctx: Context<'_>) -> Result<()> { pub async fn joke(ctx: Context<'_>) -> Result<(), Error> {
trace!("Running joke command"); trace!("Running joke command");
ctx.defer().await?; ctx.defer().await?;

View file

@ -1,13 +1,13 @@
use crate::{consts::Colors, Context}; use crate::{consts::Colors, Context, Error};
use eyre::{eyre, Context as _, OptionExt, Result}; use eyre::{eyre, Context as _, OptionExt};
use log::trace; use log::trace;
use poise::serenity_prelude::CreateEmbed; use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply; use poise::CreateReply;
/// Returns the number of members in the server /// Returns the number of members in the server
#[poise::command(slash_command, prefix_command, guild_only = true, track_edits = true)] #[poise::command(slash_command, prefix_command, guild_only = true, track_edits = true)]
pub async fn members(ctx: Context<'_>) -> Result<()> { pub async fn members(ctx: Context<'_>) -> Result<(), Error> {
trace!("Running members command"); trace!("Running members command");
ctx.defer().await?; ctx.defer().await?;
@ -29,7 +29,7 @@ pub async fn members(ctx: Context<'_>) -> Result<()> {
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title(format!("{member_count} total members!",)) .title(format!("{member_count} total members!",))
.description(format!("{online_count} online members",)) .description(format!("{online_count} online members",))
.color(Colors::BLUE); .color(Colors::Blue);
let reply = CreateReply::default().embed(embed); let reply = CreateReply::default().embed(embed);
ctx.send(reply).await?; ctx.send(reply).await?;

View file

@ -1,11 +1,10 @@
use crate::Context; use crate::{Context, Error};
use eyre::Result;
use log::trace; use log::trace;
/// Replies with pong! /// Replies with pong!
#[poise::command(slash_command, prefix_command, track_edits = true, ephemeral)] #[poise::command(slash_command, prefix_command, track_edits = true, ephemeral)]
pub async fn ping(ctx: Context<'_>) -> Result<()> { pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
trace!("Running ping command!"); trace!("Running ping command!");
ctx.say("Pong!").await?; ctx.say("Pong!").await?;
Ok(()) Ok(())

View file

@ -1,7 +1,5 @@
use crate::api::rory; use crate::{api::rory, Context, Error};
use crate::Context;
use eyre::Result;
use log::trace; use log::trace;
use poise::serenity_prelude::{CreateEmbed, CreateEmbedFooter}; use poise::serenity_prelude::{CreateEmbed, CreateEmbedFooter};
use poise::CreateReply; use poise::CreateReply;
@ -11,7 +9,7 @@ use poise::CreateReply;
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<(), Error> {
trace!("Running rory command"); trace!("Running rory command");
ctx.defer().await?; ctx.defer().await?;

View file

@ -1,6 +1,5 @@
use crate::{utils, Context}; use crate::{utils, Context, Error};
use eyre::Result;
use log::trace; use log::trace;
use poise::serenity_prelude::{CreateEmbed, CreateMessage}; use poise::serenity_prelude::{CreateEmbed, CreateMessage};
@ -15,18 +14,12 @@ use poise::serenity_prelude::{CreateEmbed, CreateMessage};
pub async fn say( pub async fn say(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "the message content"] content: String, #[description = "the message content"] content: String,
) -> Result<()> { ) -> Result<(), Error> {
let channel = ctx.channel_id(); let channel = ctx.channel_id();
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 if let Some(channel_id) = ctx.data().config.discord.channels.log_channel_id {
.data()
.config
.discord_config()
.channels()
.log_channel_id()
{
let author = utils::embed_author_from_user(ctx.author()); let author = utils::embed_author_from_user(ctx.author());
let embed = CreateEmbed::default() let embed = CreateEmbed::default()

View file

@ -1,13 +1,12 @@
use crate::{api, consts::Colors, Context}; use crate::{api, consts::Colors, Context, Error};
use eyre::Result;
use log::trace; use log::trace;
use poise::serenity_prelude::CreateEmbed; use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply; use poise::CreateReply;
/// Returns GitHub stargazer count /// Returns GitHub stargazer count
#[poise::command(slash_command, prefix_command, track_edits = true)] #[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn stars(ctx: Context<'_>) -> Result<()> { pub async fn stars(ctx: Context<'_>) -> Result<(), Error> {
trace!("Running stars command"); trace!("Running stars command");
ctx.defer().await?; ctx.defer().await?;
@ -27,7 +26,7 @@ pub async fn stars(ctx: Context<'_>) -> Result<()> {
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title(format!("{count} total stars!")) .title(format!("{count} total stars!"))
.color(Colors::YELLOW); .color(Colors::Yellow);
let reply = CreateReply::default().embed(embed); let reply = CreateReply::default().embed(embed);
ctx.send(reply).await?; ctx.send(reply).await?;

View file

@ -1,10 +1,10 @@
#![allow(non_camel_case_types, clippy::upper_case_acronyms)] #![allow(non_camel_case_types, clippy::upper_case_acronyms)]
use crate::{consts::Colors, tags::Tag, Context}; use crate::{consts::Colors, tags::Tag, Context, Error};
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use eyre::{eyre, Result}; use eyre::eyre;
use log::trace; use log::trace;
use poise::serenity_prelude::{Color, CreateEmbed, User}; use poise::serenity_prelude::{Color, CreateEmbed, User};
use poise::CreateReply; use poise::CreateReply;
@ -26,7 +26,7 @@ pub async fn tag(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "the tag to send"] name: Choice, #[description = "the tag to send"] name: Choice,
#[description = "a user to mention"] user: Option<User>, #[description = "a user to mention"] user: Option<User>,
) -> Result<()> { ) -> Result<(), Error> {
trace!("Running tag command"); trace!("Running tag command");
let tag_id = name.as_str(); let tag_id = name.as_str();

View file

@ -1,6 +1,4 @@
use crate::Data; use crate::{Data, Error};
use eyre::Report;
mod general; mod general;
mod moderation; mod moderation;
@ -32,7 +30,7 @@ macro_rules! module_macro {
module_macro!(general); module_macro!(general);
module_macro!(moderation); module_macro!(moderation);
pub type Command = poise::Command<Data, Report>; pub type Command = poise::Command<Data, Error>;
pub fn get() -> Vec<Command> { pub fn get() -> Vec<Command> {
vec![ vec![

View file

@ -1,24 +1,23 @@
use std::{fmt::Write, str::FromStr}; use std::{fmt::Write, str::FromStr};
use crate::{api, utils, Context}; use crate::{api, utils, Context, Error};
use eyre::{bail, Result}; use eyre::Result;
use log::trace; use log::trace;
use poise::serenity_prelude::{ use poise::serenity_prelude::{
futures::TryStreamExt, Attachment, CreateActionRow, CreateButton, CreateEmbed, CreateMessage, futures::TryStreamExt, Attachment, CreateActionRow, CreateButton, CreateEmbed, CreateMessage,
Mentionable, Message, ReactionType, Mentionable, Message, ReactionType,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
struct WelcomeEmbed { struct WelcomeEmbed {
title: String, title: String,
description: Option<String>, description: Option<String>,
url: Option<Url>, url: Option<String>,
hex_color: Option<String>, hex_color: Option<String>,
image: Option<Url>, image: Option<String>,
} }
impl From<WelcomeEmbed> for CreateMessage { impl From<WelcomeEmbed> for CreateMessage {
@ -112,11 +111,11 @@ pub async fn set_welcome(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "A file to use"] file: Option<Attachment>, #[description = "A file to use"] file: Option<Attachment>,
#[description = "A URL for a file to use"] url: Option<String>, #[description = "A URL for a file to use"] url: Option<String>,
) -> Result<()> { ) -> Result<(), Error> {
trace!("Running set_welcome command!"); trace!("Running set_welcome command!");
let configured_channels = ctx.data().config.discord_config().channels(); let configured_channels = ctx.data().config.discord.channels;
let Some(channel_id) = configured_channels.welcome_channel_id() else { let Some(channel_id) = configured_channels.welcome_channel_id else {
ctx.say("You don't have a welcome channel ID set, so I can't do anything :(") ctx.say("You don't have a welcome channel ID set, so I can't do anything :(")
.await?; .await?;
return Ok(()); return Ok(());
@ -127,7 +126,7 @@ pub async fn set_welcome(
// download attachment from discord or URL // download attachment from discord or URL
let file = if let Some(attachment) = file { let file = if let Some(attachment) = file {
let Some(content_type) = &attachment.content_type else { let Some(content_type) = &attachment.content_type else {
bail!("Welcome channel attachment was sent without a content type!"); return Err("Welcome channel attachment was sent without a content type!".into());
}; };
if !content_type.starts_with("application/json;") { if !content_type.starts_with("application/json;") {
@ -139,11 +138,6 @@ pub async fn set_welcome(
let downloaded = attachment.download().await?; let downloaded = attachment.download().await?;
String::from_utf8(downloaded)? String::from_utf8(downloaded)?
} else if let Some(url) = url { } else if let Some(url) = url {
if Url::parse(&url).is_err() {
ctx.say("Invalid url!").await?;
return Ok(());
}
api::text_from_url(&url).await? api::text_from_url(&url).await?
} else { } else {
ctx.say("A text file or URL must be provided!").await?; ctx.say("A text file or URL must be provided!").await?;
@ -180,7 +174,7 @@ pub async fn set_welcome(
channel_id.say(ctx, message).await?; channel_id.say(ctx, message).await?;
} }
if let Some(log_channel) = configured_channels.log_channel_id() { if let Some(log_channel) = configured_channels.log_channel_id {
let author = utils::embed_author_from_user(ctx.author()); let author = utils::embed_author_from_user(ctx.author());
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title("set_welcome command used!") .title("set_welcome command used!")

View file

@ -2,7 +2,7 @@ use log::{info, warn};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Config { pub struct Config {
redis_url: Option<String>, pub redis_url: Option<String>,
} }
impl Config { impl Config {
@ -21,8 +21,4 @@ impl Config {
Self::new(redis_url) Self::new(redis_url)
} }
pub fn redis_url(&self) -> Option<&str> {
self.redis_url.as_deref()
}
} }

View file

@ -5,13 +5,13 @@ use poise::serenity_prelude::ChannelId;
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct RefractionChannels { pub struct RefractionChannels {
log_channel_id: Option<ChannelId>, pub log_channel_id: Option<ChannelId>,
welcome_channel_id: Option<ChannelId>, pub welcome_channel_id: Option<ChannelId>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Config { pub struct Config {
channels: RefractionChannels, pub channels: RefractionChannels,
} }
impl RefractionChannels { impl RefractionChannels {
@ -45,14 +45,6 @@ impl RefractionChannels {
.ok() .ok()
.and_then(|env_var| ChannelId::from_str(&env_var).ok()) .and_then(|env_var| ChannelId::from_str(&env_var).ok())
} }
pub fn log_channel_id(&self) -> Option<&ChannelId> {
self.log_channel_id.as_ref()
}
pub fn welcome_channel_id(&self) -> Option<&ChannelId> {
self.welcome_channel_id.as_ref()
}
} }
impl Config { impl Config {
@ -65,8 +57,4 @@ impl Config {
Self::new(channels) Self::new(channels)
} }
pub fn channels(&self) -> &RefractionChannels {
&self.channels
}
} }

View file

@ -3,8 +3,8 @@ mod discord;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Config { pub struct Config {
bot: bot::Config, pub bot: bot::Config,
discord: discord::Config, pub discord: discord::Config,
} }
impl Config { impl Config {
@ -21,12 +21,4 @@ impl Config {
Self::new(bot, discord) Self::new(bot, discord)
} }
pub fn bot_config(&self) -> &bot::Config {
&self.bot
}
pub fn discord_config(&self) -> &discord::Config {
&self.discord
}
} }

View file

@ -3,18 +3,31 @@ use std::str::FromStr;
use poise::serenity_prelude::Colour; use poise::serenity_prelude::Colour;
const BLUE: u32 = 0x60A5FA;
const GREEN: u32 = 0x22C55E;
const ORANGE: u32 = 0xFB923C;
const RED: u32 = 0xEF4444;
const YELLOW: u32 = 0xFDE047;
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct Colors(i32); pub enum Colors {
Blue,
#[default]
Green,
Orange,
Red,
Yellow,
}
impl Colors { impl From<Colors> for Colour {
pub const RED: i32 = 0xEF4444; fn from(value: Colors) -> Self {
pub const GREEN: i32 = 0x22C55E; Self::from(match &value {
pub const BLUE: i32 = 0x60A5FA; Colors::Blue => BLUE,
pub const YELLOW: i32 = 0xFDE047; Colors::Green => GREEN,
pub const ORANGE: i32 = 0xFB923C; Colors::Orange => ORANGE,
Colors::Red => RED,
pub fn as_i32(self) -> i32 { Colors::Yellow => YELLOW,
self.0 })
} }
} }
@ -22,18 +35,12 @@ impl FromStr for Colors {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"red" => Ok(Colors(Self::RED)), "blue" => Ok(Self::Blue),
"green" => Ok(Colors(Self::GREEN)), "green" => Ok(Self::Green),
"blue" => Ok(Colors(Self::BLUE)), "orange" => Ok(Self::Orange),
"yellow" => Ok(Colors(Self::YELLOW)), "red" => Ok(Self::Red),
"orange" => Ok(Colors(Self::ORANGE)), "yellow" => Ok(Self::Yellow),
_ => Err(()), _ => Err(()),
} }
} }
} }
impl From<Colors> for Colour {
fn from(value: Colors) -> Self {
Self::from(value.as_i32())
}
}

View file

@ -1,8 +1,7 @@
use crate::{consts::Colors, Data}; use crate::{consts::Colors, Data, Error};
use std::fmt::Write; use std::fmt::Write;
use eyre::Report;
use log::error; use log::error;
use poise::serenity_prelude::{CreateEmbed, Timestamp}; use poise::serenity_prelude::{CreateEmbed, Timestamp};
use poise::{CreateReply, FrameworkError}; use poise::{CreateReply, FrameworkError};
@ -16,7 +15,7 @@ macro_rules! writelne {
} }
} }
pub async fn handle(error: FrameworkError<'_, Data, Report>) { pub async fn handle(error: FrameworkError<'_, Data, Error>) {
match error { match error {
FrameworkError::Setup { FrameworkError::Setup {
error, framework, .. error, framework, ..
@ -34,7 +33,7 @@ pub async fn handle(error: FrameworkError<'_, Data, Report>) {
.title("Something went wrong!") .title("Something went wrong!")
.description("oopsie") .description("oopsie")
.timestamp(Timestamp::now()) .timestamp(Timestamp::now())
.color(Colors::RED); .color(Colors::Red);
let reply = CreateReply::default().embed(embed); let reply = CreateReply::default().embed(embed);

View file

@ -198,7 +198,9 @@ async fn outdated_launcher(log: &str, data: &Data) -> Result<Issue> {
if let Ok(version) = storage.launcher_version().await { if let Ok(version) = storage.launcher_version().await {
version version
} else { } else {
api::github::get_latest_prism_version().await? let version = api::github::get_latest_prism_version().await?;
storage.cache_launcher_version(&version).await?;
version
} }
} else { } else {
trace!("Not caching launcher version, as we're running without a storage backend"); trace!("Not caching launcher version, as we're running without a storage backend");

View file

@ -48,10 +48,10 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
if issues.is_empty() { if issues.is_empty() {
e = e e = e
.color(Colors::GREEN) .color(Colors::Green)
.description("No issues found automatically"); .description("No issues found automatically");
} else { } else {
e = e.color(Colors::RED); e = e.color(Colors::Red);
for (title, description) in issues { for (title, description) in issues {
e = e.field(title, description, false); e = e.field(title, description, false);

View file

@ -19,7 +19,7 @@ impl super::LogProvider for PasteGG {
} }
async fn fetch(&self, content: &str) -> Result<String> { async fn fetch(&self, content: &str) -> Result<String> {
let files = paste_gg::get_files(content).await?; let files = paste_gg::files_from(content).await?;
let result = files let result = files
.result .result
.ok_or_eyre("Got an empty result from paste.gg!")?; .ok_or_eyre("Got an empty result from paste.gg!")?;

View file

@ -4,7 +4,7 @@ use poise::serenity_prelude::{Context, CreateAllowedMentions, CreateMessage, Mes
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::from_message(ctx, message).await?; let embeds = utils::messages::from_message(ctx, message).await?;
if !embeds.is_empty() { if !embeds.is_empty() {
let allowed_mentions = CreateAllowedMentions::new().replied_user(false); let allowed_mentions = CreateAllowedMentions::new().replied_user(false);

View file

@ -1,6 +1,5 @@
use crate::{api, Data}; use crate::{api, Data, Error};
use eyre::{Report, Result};
use log::{debug, info, trace}; use log::{debug, info, trace};
use poise::serenity_prelude::{ActivityData, Context, FullEvent, OnlineStatus}; use poise::serenity_prelude::{ActivityData, Context, FullEvent, OnlineStatus};
use poise::FrameworkContext; use poise::FrameworkContext;
@ -10,20 +9,20 @@ mod delete_on_reaction;
mod eta; mod eta;
mod expand_link; mod expand_link;
mod give_role; mod give_role;
pub mod pluralkit; mod pluralkit;
mod support_onboard; mod support_onboard;
pub async fn handle( pub async fn handle(
ctx: &Context, ctx: &Context,
event: &FullEvent, event: &FullEvent,
_: FrameworkContext<'_, Data, Report>, _: FrameworkContext<'_, Data, Error>,
data: &Data, data: &Data,
) -> Result<()> { ) -> Result<(), Error> {
match event { match event {
FullEvent::Ready { data_about_bot } => { FullEvent::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::latest_minecraft_version().await?;
let activity = ActivityData::playing(format!("Minecraft {latest_minecraft_version}")); let activity = ActivityData::playing(format!("Minecraft {latest_minecraft_version}"));
info!("Setting presence to activity {activity:#?}"); info!("Setting presence to activity {activity:#?}");

View file

@ -15,7 +15,7 @@ pub async fn is_message_proxied(message: &Message) -> Result<bool> {
); );
sleep(PK_DELAY).await; sleep(PK_DELAY).await;
let proxied = api::pluralkit::get_sender(message.id).await.is_ok(); let proxied = api::pluralkit::sender_from(message.id).await.is_ok();
Ok(proxied) Ok(proxied)
} }
@ -36,7 +36,7 @@ pub async fn handle(_: &Context, msg: &Message, storage: &Storage) -> Result<()>
); );
sleep(PK_DELAY).await; sleep(PK_DELAY).await;
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await { if let Ok(sender) = api::pluralkit::sender_from(msg.id).await {
storage.store_user_plurality(sender).await?; storage.store_user_plurality(sender).await?;
} }

View file

@ -1,18 +1,10 @@
#![warn(clippy::all, clippy::pedantic, clippy::perf)]
#![allow(clippy::missing_errors_doc)]
#![forbid(unsafe_code)]
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use eyre::{bail, Context as _, Report, Result}; use eyre::Context as _;
use log::{info, trace, warn}; use log::{info, trace, warn};
use poise::{ use poise::{
serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions,
}; };
use owo_colors::OwoColorize;
use tokio::signal::ctrl_c; use tokio::signal::ctrl_c;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
@ -31,7 +23,8 @@ mod utils;
use config::Config; use config::Config;
use storage::Storage; use storage::Storage;
type Context<'a> = poise::Context<'a, Data, Report>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Data { pub struct Data {
@ -39,21 +32,14 @@ pub struct Data {
storage: Option<Storage>, storage: Option<Storage>,
} }
impl Data {
#[must_use]
pub fn new(config: Config, storage: Option<Storage>) -> Self {
Self { config, storage }
}
}
async fn setup( async fn setup(
ctx: &serenity::Context, ctx: &serenity::Context,
_: &serenity::Ready, _: &serenity::Ready,
framework: &Framework<Data, Report>, framework: &Framework<Data, Error>,
) -> Result<Data> { ) -> Result<Data, Error> {
let config = Config::new_from_env(); let config = Config::new_from_env();
let storage = if let Some(url) = config.bot_config().redis_url() { let storage = if let Some(url) = &config.bot.redis_url {
Some(Storage::from_url(url)?) Some(Storage::from_url(url)?)
} else { } else {
None None
@ -61,13 +47,15 @@ async fn setup(
if let Some(storage) = storage.as_ref() { if let Some(storage) = storage.as_ref() {
if !storage.clone().has_connection() { if !storage.clone().has_connection() {
bail!("You specified a storage backend but there's no connection! Is it running?") return Err(
"You specified a storage backend but there's no connection! Is it running?".into(),
);
} }
trace!("Redis connection looks good!"); trace!("Redis connection looks good!");
} }
let data = Data::new(config, storage); let data = Data { config, storage };
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!");
@ -78,11 +66,11 @@ async fn setup(
async fn handle_shutdown(shard_manager: Arc<serenity::ShardManager>, reason: &str) { async fn handle_shutdown(shard_manager: Arc<serenity::ShardManager>, reason: &str) {
warn!("{reason}! Shutting down bot..."); warn!("{reason}! Shutting down bot...");
shard_manager.shutdown_all().await; shard_manager.shutdown_all().await;
println!("{}", "Everything is shutdown. Goodbye!".green()); println!("Everything is shutdown. Goodbye!");
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> eyre::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
color_eyre::install()?; color_eyre::install()?;
env_logger::init(); env_logger::init();
@ -134,7 +122,7 @@ async fn main() -> Result<()> {
let mut sigterm = ctrl_close()?; let mut sigterm = ctrl_close()?;
tokio::select! { tokio::select! {
result = client.start() => result.map_err(Report::from), result = client.start() => result.map_err(eyre::Report::from),
_ = sigterm.recv() => { _ = sigterm.recv() => {
handle_shutdown(shard_manager, "Received SIGTERM").await; handle_shutdown(shard_manager, "Received SIGTERM").await;
std::process::exit(0); std::process::exit(0);

View file

@ -24,7 +24,7 @@ fn find_first_image(message: &Message) -> Option<String> {
} }
async fn find_real_author_id(message: &Message) -> UserId { async fn find_real_author_id(message: &Message) -> UserId {
if let Ok(sender) = pluralkit::get_sender(message.id).await { if let Ok(sender) = pluralkit::sender_from(message.id).await {
sender sender
} else { } else {
message.author.id message.author.id

View file

@ -1,6 +1,6 @@
use poise::serenity_prelude::{CreateEmbedAuthor, User}; use poise::serenity_prelude::{CreateEmbedAuthor, User};
pub mod resolve_message; pub mod messages;
pub fn embed_author_from_user(user: &User) -> CreateEmbedAuthor { pub fn embed_author_from_user(user: &User) -> CreateEmbedAuthor {
CreateEmbedAuthor::new(user.tag()).icon_url( CreateEmbedAuthor::new(user.tag()).icon_url(