diff options
| author | seth <[email protected]> | 2024-08-09 23:35:41 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-08-09 23:35:41 -0400 |
| commit | b643a6a235b0c1c9902b97421f24eff2b0d0a5ac (patch) | |
| tree | 350794c0e9330fb77367838313bc6bb97278a0aa /src | |
| parent | 372780546b508684839916e5ad54c9e90456a94f (diff) | |
tree-wide: end of summer cleanup (#214)
* api: refactor & rename module to http
* client: split from main.rs
* tree-wide: use eyre::Report as error
* nix: alejandra -> nixfmt
* nix: start using treefmt-nix
* nix: simplify flake
* nix: refactor derivation & docker image
* nix: remove overlay
* ci: update & cleanup workflows
* commands: assign all commands automatically
* commands/copypasta: remove
* http/teawie: update response struct for upstream rust rewrite
* handlers: rename modules to events; flatten
* crates: rename self to teawie-bot
* nix: fenix -> rust-overlay
i want a specific rust version grrrrrrr
* ci: pin rust to 1.79
this is what our nix dev shell uses and what we can compile on. it seems
the time crate doesn't like v1.80 of the compiler :(
* ci: always run release gates
* nix: fix static toolchain
* nix: rust-overlay -> nixpkgs
* ci: adopt actions-rust-lang actions
* nix: use docker arch names for containers
* crates/time: 0.3.30 -> 0.3.36
fixes building on rust 1.80.0
Diffstat (limited to 'src')
37 files changed, 271 insertions, 369 deletions
diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs deleted file mode 100644 index 9ad6ad6..0000000 --- a/src/api/guzzle.rs +++ /dev/null @@ -1,20 +0,0 @@ -use eyre::Result; -use log::debug; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -struct RandomTeawieResponse { - url: String, -} - -// TODO: read this from an env var -const GUZZLE: &str = "https://api.getchoo.com"; -const RANDOM_TEAWIE: &str = "/random_teawie"; - -pub async fn random_teawie() -> Result<String> { - let url = format!("{GUZZLE}{RANDOM_TEAWIE}"); - debug!("Making request to {url}"); - let json: RandomTeawieResponse = super::get_json(&url).await?; - - Ok(json.url) -} diff --git a/src/api/mod.rs b/src/api/mod.rs deleted file mode 100644 index dac9209..0000000 --- a/src/api/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::sync::OnceLock; - -use eyre::Result; -use reqwest::Client; -use serde::de::DeserializeOwned; - -pub mod guzzle; -pub mod shiggy; - -pub fn client() -> &'static Client { - static USER_AGENT: OnceLock<String> = OnceLock::new(); - static CLIENT: OnceLock<Client> = OnceLock::new(); - - let user_agent = USER_AGENT.get_or_init(|| { - let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development"); - - format!("teawieBot/{version}") - }); - - CLIENT.get_or_init(|| Client::builder().user_agent(user_agent).build().unwrap()) -} - -async fn get_json<T: DeserializeOwned>(url: &str) -> Result<T> { - let resp = client().get(url).send().await?; - resp.error_for_status_ref()?; - let json = resp.json().await?; - - Ok(json) -} diff --git a/src/api/shiggy.rs b/src/api/shiggy.rs deleted file mode 100644 index d6a6238..0000000 --- a/src/api/shiggy.rs +++ /dev/null @@ -1,20 +0,0 @@ -use eyre::Result; -use log::debug; -use serde::Deserialize; - -const SHIGGY: &str = "https://safebooru.donmai.us"; -const RANDOM_SHIGGY: &str = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; - -#[derive(Deserialize)] -struct SafebooruResponse { - file_url: String, -} - -#[allow(clippy::module_name_repetitions)] -pub async fn random_shiggy() -> Result<String> { - let url = format!("{SHIGGY}{RANDOM_SHIGGY}"); - debug!("Making request to {url}"); - - let resp: SafebooruResponse = super::get_json(&url).await?; - Ok(resp.file_url) -} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..65c221b --- /dev/null +++ b/src/client.rs @@ -0,0 +1,99 @@ +use crate::{commands, events, http, storage::Storage}; + +use std::{sync::Arc, time::Duration}; + +use eyre::{bail, Context as _, Result}; +use log::{info, trace, warn}; +use poise::{ + serenity_prelude::{self as serenity}, + EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, +}; + +pub type Error = eyre::Report; +pub type Context<'a> = poise::Context<'a, Data, Error>; + +#[derive(Clone, Debug, Default)] +pub struct Data { + pub http_client: http::Client, + pub storage: Option<Storage>, +} + +async fn setup(ctx: &serenity::Context) -> Result<Data> { + let storage = Storage::from_env().ok(); + + if let Some(storage) = storage.as_ref() { + if !storage.clone().is_connected() { + bail!("You specified a storage backend but there's no connection! Is it running?"); + } + trace!("Storage backend connected!"); + + poise::builtins::register_globally(ctx, &commands::global()).await?; + info!("Registered global commands!"); + + // register "extra" commands in guilds that allow it + let guilds = storage.get_opted_guilds().await?; + + for guild in guilds { + poise::builtins::register_in_guild(ctx, &commands::optional(), guild).await?; + + info!("Registered guild commands to {}", guild); + } + } else { + warn!("No storage backend was specified. Features requiring storage cannot be used"); + warn!("Registering optional commands globally since there's no storage backend"); + poise::builtins::register_globally(ctx, &commands::all()).await?; + } + + let http_client = <http::Client as http::Ext>::default(); + let data = Data { + http_client, + storage, + }; + + Ok(data) +} + +pub async fn handle_shutdown(shard_manager: Arc<serenity::ShardManager>, reason: &str) { + warn!("{reason}! Shutting down bot..."); + shard_manager.shutdown_all().await; + println!("Everything is shutdown. Goodbye!"); +} + +pub async fn get() -> Result<serenity::Client> { + let token = std::env::var("TOKEN").wrap_err("Couldn't find bot token in environment!")?; + + let intents = + serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; + + let options = FrameworkOptions { + commands: commands::all(), + on_error: |error| Box::pin(events::error::handle(error)), + + command_check: Some(|ctx| { + Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) + }), + + event_handler: |ctx, event, _framework, data| Box::pin(events::handle(ctx, event, data)), + + prefix_options: PrefixFrameworkOptions { + prefix: Some("!".into()), + edit_tracker: Some(Arc::new(EditTracker::for_timespan(Duration::from_secs( + 3600, + )))), + ..Default::default() + }, + + ..Default::default() + }; + + let framework = Framework::builder() + .options(options) + .setup(|ctx, _ready, _framework| Box::pin(setup(ctx))) + .build(); + + let client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await?; + + Ok(client) +} diff --git a/src/commands/general/ask.rs b/src/commands/general/ask.rs index c715e3a..1300e97 100644 --- a/src/commands/general/ask.rs +++ b/src/commands/general/ask.rs @@ -1,6 +1,6 @@ -use crate::{consts, utils, Context, Error}; +use crate::{client::Context, consts, utils}; -use eyre::Context as _; +use eyre::{Context as _, Result}; /// Ask teawie a question! #[poise::command(prefix_command, slash_command)] @@ -10,7 +10,7 @@ pub async fn ask( #[rename = "question"] #[description = "The question you want to ask teawie"] _question: String, -) -> Result<(), Error> { +) -> Result<()> { let resp = utils::random_choice(consts::RESPONSES) .wrap_err("Couldn't choose from random responses!")?; diff --git a/src/commands/general/bing.rs b/src/commands/general/bing.rs index 54ee0dc..28fdf0d 100644 --- a/src/commands/general/bing.rs +++ b/src/commands/general/bing.rs @@ -1,8 +1,10 @@ -use crate::{Context, Error}; +use crate::client::Context; + +use eyre::Result; /// Make sure the wie is alive #[poise::command(prefix_command)] -pub async fn bing(ctx: Context<'_>) -> Result<(), Error> { +pub async fn bing(ctx: Context<'_>) -> Result<()> { ctx.say("bong!").await?; Ok(()) } diff --git a/src/commands/general/config.rs b/src/commands/general/config.rs index 456e791..6adb78b 100644 --- a/src/commands/general/config.rs +++ b/src/commands/general/config.rs @@ -1,5 +1,5 @@ +use crate::client::Context; use crate::storage::settings::{Properties, Settings}; -use crate::{Context, Error}; use std::str::FromStr; @@ -41,7 +41,7 @@ fn prop_to_val(setting: &Properties, settings: &Settings) -> String { required_permissions = "MANAGE_GUILD", default_member_permissions = "MANAGE_GUILD" )] -pub async fn config(_: Context<'_>) -> Result<(), Error> { +pub async fn config(_: Context<'_>) -> Result<()> { Ok(()) } @@ -72,7 +72,7 @@ pub async fn set( #[description = "Toggle ReactBoard"] reactboard_enabled: Option<bool>, #[description = "Enables 'extra' commands like teawiespam and copypasta. Defaults to false."] optional_commands_enabled: Option<bool>, -) -> Result<(), Error> { +) -> Result<()> { if let Some(storage) = &ctx.data().storage { let gid = ctx.guild_id().unwrap_or_default(); let mut settings = storage.get_guild_settings(&gid).await?; @@ -149,7 +149,7 @@ pub async fn set( pub async fn get( ctx: Context<'_>, #[description = "The setting you want to get"] setting: Properties, -) -> Result<(), Error> { +) -> Result<()> { let gid = &ctx .guild_id() .ok_or_eyre("Failed to get GuildId from context!")?; diff --git a/src/commands/general/convert.rs b/src/commands/general/convert.rs index 4d38eb2..b5e7018 100644 --- a/src/commands/general/convert.rs +++ b/src/commands/general/convert.rs @@ -1,4 +1,4 @@ -use crate::{Context, Error}; +use crate::client::Context; use bottomify::bottom; use eyre::Result; @@ -9,7 +9,7 @@ use poise::serenity_prelude::constants::MESSAGE_CODE_LIMIT; slash_command, subcommands("to_fahrenheit", "to_celsius", "to_bottom", "from_bottom") )] -pub async fn convert(_: Context<'_>) -> Result<(), Error> { +pub async fn convert(_: Context<'_>) -> Result<()> { Ok(()) } @@ -18,7 +18,7 @@ pub async fn convert(_: Context<'_>) -> Result<(), Error> { pub async fn to_celsius( ctx: Context<'_>, #[description = "What teawie will convert"] degrees_fahrenheit: f32, -) -> Result<(), Error> { +) -> Result<()> { let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); ctx.say(temp.to_string()).await?; Ok(()) @@ -29,7 +29,7 @@ pub async fn to_celsius( pub async fn to_fahrenheit( ctx: Context<'_>, #[description = "What teawie will convert"] degrees_celsius: f32, -) -> Result<(), Error> { +) -> Result<()> { let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; ctx.say(temp.to_string()).await?; Ok(()) @@ -40,7 +40,7 @@ pub async fn to_fahrenheit( pub async fn to_bottom( ctx: Context<'_>, #[description = "What teawie will translate into bottom"] message: String, -) -> Result<(), Error> { +) -> Result<()> { let encoded = bottom::encode_string(&message); ctx.say(encoded).await?; Ok(()) @@ -51,7 +51,7 @@ pub async fn to_bottom( pub async fn from_bottom( ctx: Context<'_>, #[description = "What teawie will translate from bottom"] message: String, -) -> Result<(), Error> { +) -> Result<()> { let resp: String; if let Ok(decoded) = bottom::decode_string(&message.clone()) { diff --git a/src/commands/general/emoji.rs b/src/commands/general/emoji.rs index 81cd9a3..bbae0b5 100644 --- a/src/commands/general/emoji.rs +++ b/src/commands/general/emoji.rs @@ -1,5 +1,6 @@ -use crate::{consts::Colors, Context, Error}; +use crate::{client::Context, consts::Colors}; +use eyre::Result; use poise::{ serenity_prelude::{CreateEmbed, Emoji}, CreateReply, @@ -7,7 +8,7 @@ use poise::{ /// Get the URL for an emoji #[poise::command(slash_command)] -pub async fn emoji(ctx: Context<'_>, emoji: Emoji) -> Result<(), Error> { +pub async fn emoji(ctx: Context<'_>, emoji: Emoji) -> Result<()> { let url = emoji.url(); let embed = CreateEmbed::new() .title(emoji.name) diff --git a/src/commands/general/pfp.rs b/src/commands/general/pfp.rs index 2ad062b..34ae795 100644 --- a/src/commands/general/pfp.rs +++ b/src/commands/general/pfp.rs @@ -1,13 +1,14 @@ +use crate::{client::Context, consts::Colors}; + +use eyre::Result; use poise::{ serenity_prelude::{CreateEmbed, User}, CreateReply, }; -use crate::{consts::Colors, Context, Error}; - /// Get someone's profile pic #[poise::command(context_menu_command = "Get profile picture", slash_command)] -pub async fn pfp(ctx: Context<'_>, user: User) -> Result<(), Error> { +pub async fn pfp(ctx: Context<'_>, user: User) -> Result<()> { let url = user .avatar_url() .unwrap_or_else(|| user.default_avatar_url()); diff --git a/src/commands/general/random.rs b/src/commands/general/random.rs index 92e9188..094123b 100644 --- a/src/commands/general/random.rs +++ b/src/commands/general/random.rs @@ -1,14 +1,16 @@ -use crate::{api, consts, utils, Context, Error}; +use crate::{client::Context, consts, http, utils}; + +use eyre::Result; #[poise::command(slash_command, subcommands("lore", "teawie", "shiggy"))] #[allow(clippy::unused_async)] -pub async fn random(_: Context<'_>) -> Result<(), Error> { +pub async fn random(_: Context<'_>) -> Result<()> { Ok(()) } /// Get a random piece of teawie lore! #[poise::command(prefix_command, slash_command)] -pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { +pub async fn lore(ctx: Context<'_>) -> Result<()> { let resp = utils::random_choice(consts::LORE)?; ctx.say(resp).await?; @@ -17,8 +19,8 @@ pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { /// Get a random teawie #[poise::command(prefix_command, slash_command)] -pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { - let url = api::guzzle::random_teawie().await?; +pub async fn teawie(ctx: Context<'_>) -> Result<()> { + let url = http::teawie::random(&ctx.data().http_client).await?; utils::send_url_as_embed(ctx, url).await?; Ok(()) @@ -26,8 +28,8 @@ pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { /// Get a random shiggy #[poise::command(prefix_command, slash_command)] -pub async fn shiggy(ctx: Context<'_>) -> Result<(), Error> { - let url = api::shiggy::random_shiggy().await?; +pub async fn shiggy(ctx: Context<'_>) -> Result<()> { + let url = http::shiggy::random(&ctx.data().http_client).await?; utils::send_url_as_embed(ctx, url).await?; Ok(()) diff --git a/src/commands/general/version.rs b/src/commands/general/version.rs index 5f8eac9..bdf6805 100644 --- a/src/commands/general/version.rs +++ b/src/commands/general/version.rs @@ -1,12 +1,13 @@ -use crate::{consts::Colors, Context, Error}; +use crate::{client::Context, consts::Colors}; use std::env::consts::{ARCH, OS}; +use eyre::Result; use poise::{serenity_prelude::CreateEmbed, CreateReply}; /// Get version info #[poise::command(slash_command)] -pub async fn version(ctx: Context<'_>) -> Result<(), Error> { +pub async fn version(ctx: Context<'_>) -> Result<()> { let sha = option_env!("GIT_SHA").unwrap_or("main"); let revision_url = format!( "[{}]({}/tree/{})", diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e8cac33..b8d0381 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ -use crate::{Data, Error}; +use crate::client::{Data, Error}; mod general; mod moderation; @@ -17,24 +17,13 @@ macro_rules! cmd { }; } -pub fn to_vec() -> Vec<Command> { - vec![ - cmd!(general, ask), - cmd!(general, bing), - cmd!(general, config), - cmd!(general, convert), - cmd!(general, emoji), - cmd!(general, pfp), - cmd!(general, random), - cmd!(general, version), - cmd!(moderation, clear_messages), - cmd!(optional, copypasta), - cmd!(optional, teawiespam), - cmd!(optional, uwurandom), - ] +pub fn all() -> Vec<Command> { + let mut all_commands = global(); + all_commands.append(&mut optional()); + all_commands } -pub fn to_vec_global() -> Vec<Command> { +pub fn global() -> Vec<Command> { vec![ cmd!(general, ask), cmd!(general, bing), @@ -48,10 +37,6 @@ pub fn to_vec_global() -> Vec<Command> { ] } -pub fn to_vec_optional() -> Vec<Command> { - vec![ - cmd!(optional, copypasta), - cmd!(optional, teawiespam), - cmd!(optional, uwurandom), - ] +pub fn optional() -> Vec<Command> { + vec![cmd!(optional, teawiespam), cmd!(optional, uwurandom)] } diff --git a/src/commands/moderation/clear_messages.rs b/src/commands/moderation/clear_messages.rs index 8761bcb..65a30be 100644 --- a/src/commands/moderation/clear_messages.rs +++ b/src/commands/moderation/clear_messages.rs @@ -1,5 +1,6 @@ -use crate::{Context, Error}; +use crate::client::Context; +use eyre::Result; use log::debug; use poise::serenity_prelude::GetMessages; @@ -13,7 +14,7 @@ use poise::serenity_prelude::GetMessages; pub async fn clear_messages( ctx: Context<'_>, #[description = "How many messages to delete"] num_messages: u8, -) -> Result<(), Error> { +) -> Result<()> { ctx.defer_ephemeral().await?; let channel = ctx.channel_id(); diff --git a/src/commands/optional/copypasta.rs b/src/commands/optional/copypasta.rs deleted file mode 100644 index 06440b1..0000000 --- a/src/commands/optional/copypasta.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{Context, Error}; - -use include_dir::{include_dir, Dir}; -use log::debug; - -const COPYPASTAS: Dir = include_dir!("src/copypastas"); - -#[derive(Debug, poise::ChoiceParameter)] -pub enum Copypasta { - Astral, - Dvd, - Egrill, - HappyMeal, - Sus, - TickTock, - Twitter, -} - -impl ToString for Copypasta { - fn to_string(&self) -> String { - let str = match self { - Self::Astral => "astral", - Self::Dvd => "dvd", - Self::Egrill => "egrill", - Self::HappyMeal => "happymeal", - Self::Sus => "sus", - Self::TickTock => "ticktock", - Self::Twitter => "twitter", - }; - str.to_string() - } -} - -impl Copypasta { - fn contents(&self) -> Option<&str> { - let file_name = format!("{}.txt", self.to_string()); - COPYPASTAS - .get_file(file_name) - .and_then(|file| file.contents_utf8()) - } -} - -/// ask teawie to send funni copypasta -#[poise::command(slash_command)] -pub async fn copypasta( - ctx: Context<'_>, - #[description = "the copypasta you want to send"] copypasta: Copypasta, -) -> Result<(), Error> { - if let Some(guild_id) = ctx.guild_id() { - if let Some(storage) = &ctx.data().storage { - let settings = storage.get_guild_settings(&guild_id).await?; - - if !settings.optional_commands_enabled { - debug!("Not running command in {guild_id} since it's disabled"); - ctx.reply("I'm not allowed to do that here").await?; - - return Ok(()); - } - } else { - debug!("Ignoring restrictions on command; no storage backend is attached!"); - } - } else { - debug!("Ignoring restrictions on command; we're not in a guild"); - } - - if let Some(contents) = copypasta.contents() { - ctx.say(contents).await?; - } else { - ctx.reply("I couldn't find that copypasta :(").await?; - } - - Ok(()) -} diff --git a/src/commands/optional/mod.rs b/src/commands/optional/mod.rs index 95c39bd..a3d1bd2 100644 --- a/src/commands/optional/mod.rs +++ b/src/commands/optional/mod.rs @@ -1,3 +1,2 @@ -pub mod copypasta; pub mod teawiespam; pub mod uwurandom; diff --git a/src/commands/optional/teawiespam.rs b/src/commands/optional/teawiespam.rs index 3a9a387..bfac852 100644 --- a/src/commands/optional/teawiespam.rs +++ b/src/commands/optional/teawiespam.rs @@ -1,10 +1,11 @@ -use crate::{Context, Error}; +use crate::client::Context; +use eyre::Result; use log::debug; /// teawie will spam you. #[poise::command(slash_command)] -pub async fn teawiespam(ctx: Context<'_>) -> Result<(), Error> { +pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { if let Some(guild_id) = ctx.guild_id() { if let Some(storage) = &ctx.data().storage { let settings = storage.get_guild_settings(&guild_id).await?; diff --git a/src/commands/optional/uwurandom.rs b/src/commands/optional/uwurandom.rs index e717d5e..c952dee 100644 --- a/src/commands/optional/uwurandom.rs +++ b/src/commands/optional/uwurandom.rs @@ -1,4 +1,4 @@ -use crate::{Context, Error}; +use crate::client::Context; use eyre::Result; use log::debug; @@ -12,7 +12,7 @@ pub async fn uwurandom( #[min = 1] #[max = 2000] length: Option<u16>, -) -> Result<(), Error> { +) -> Result<()> { if let Some(guild_id) = ctx.guild_id() { if let Some(storage) = &ctx.data().storage { let settings = storage.get_guild_settings(&guild_id).await?; diff --git a/src/copypastas/astral.txt b/src/copypastas/astral.txt deleted file mode 100644 index 9984f31..0000000 --- a/src/copypastas/astral.txt +++ /dev/null @@ -1,7 +0,0 @@ -Today while astral projecting I summoned allah to try and weaken him so our hexing spells would work better. - -He is so fucking powerful. I’m not at a power level to do this alone. I barely escaped with my life and I’m spiritually injured to a great amount, but I think I’ll make it. - -I can’t imagine what he would do to a new, unsuspecting witch. I’m scared that I will have to face him again soon if I ever want to continue astral projecting. I’m currently burning healing incense and drawing spiritual energy from my crystals to try and heal as quickly as possible. - -Please be safe everyone. Allah is much stronger than I first imagined and we will have to do this together if we want to slay a god. diff --git a/src/copypastas/dvd.txt b/src/copypastas/dvd.txt deleted file mode 100644 index 16d396b..0000000 --- a/src/copypastas/dvd.txt +++ /dev/null @@ -1 +0,0 @@ -𝙏𝙝𝙞𝙨 𝘿𝙞𝙨𝙣𝙚𝙮 𝘿𝙑𝘿 𝙞𝙨 𝙚𝙣𝙝𝙖𝙣𝙘𝙚𝙙 𝙬𝙞𝙩𝙝 𝘿𝙞𝙨𝙣𝙚𝙮’𝙨 𝙁𝙖𝙨𝙩𝙋𝙡𝙖𝙮. 𝙔𝙤𝙪𝙧 𝙢𝙤𝙫𝙞𝙚 𝙖𝙣𝙙 𝙖 𝙨𝙚𝙡𝙚𝙘𝙩𝙞𝙤𝙣 𝙤𝙛 𝙗𝙤𝙣𝙪𝙨 𝙛𝙚𝙖𝙩𝙪𝙧𝙚𝙨 𝙬𝙞𝙡𝙡 𝙗𝙚𝙜𝙞𝙣 𝙖𝙪𝙩𝙤𝙢𝙖𝙩𝙞𝙘𝙖𝙡𝙡𝙮. 𝙏𝙤 𝙗𝙮𝙥𝙖𝙨𝙨 𝙁𝙖𝙨𝙩 𝙋𝙡𝙖𝙮, 𝙨𝙚𝙡𝙚𝙘𝙩 𝙩𝙝𝙚 𝙈𝙖𝙞𝙣 𝙈𝙚𝙣𝙪 𝙗𝙪𝙩𝙩𝙤𝙣 𝙖𝙩 𝙖𝙣𝙮 𝙩𝙞𝙢𝙚. 𝙁𝙖𝙨𝙩 𝙋𝙡𝙖𝙮 𝙬𝙞𝙡𝙡 𝙗𝙚𝙜𝙞𝙣 𝙞𝙣 𝙖 𝙢𝙤𝙢𝙚𝙣𝙩… diff --git a/src/copypastas/egrill.txt b/src/copypastas/egrill.txt deleted file mode 100644 index 02919b8..0000000 --- a/src/copypastas/egrill.txt +++ /dev/null @@ -1 +0,0 @@ -Everyone jokes about grilling, but they all forget about people like me. I have existed in a constant state of grilling since a BBQ in 1997. I have been attending this charcoal grill for twenty-two years. Do you genuinely think I can make time to argue about politics? Don't make me laugh. I have been flipping this exact burger for a third of my life. It's been decades why is it still raw. It's literally not cooking and I'm not allowed to leave. It's been over a fucking a flame for 22 years and it's still fucking frozen. What fucking vengeful god cursed me with this. help diff --git a/src/copypastas/happymeal.txt b/src/copypastas/happymeal.txt deleted file mode 100644 index bf52100..0000000 --- a/src/copypastas/happymeal.txt +++ /dev/null @@ -1 +0,0 @@ -OH MY GOD ITS 3 IN THE MORNING AND IM IN MCDONALDS AND WE JUST FOUND OUT THAT WHEN U PULL UP IN MCDONALDS AT 3 AM YOU CAN BUY THE AMONG US HAPPY MEAL WITH A TOY IN IT WHICH IS EITHER THE IMPOSTOR OR THE CREWMATE AND IF YOU DONT KNOW WHAT AMONG US IS YOU MUST BE MUST REALLY BE LIVING UNDER A ROCK ITS AN AWESOME GAME WITH IMPOSTORS AND CREWMATES AND BASICALLY THE IMPOSTOR TRIES TO SABOTAGE THE WHOLE GAME AND THE CREWMATES NEED TO STOP HIM BUT APPARENTLY WHEN YOU PURCHASE THE AMONG US HAPPY MEAL SOMETHING SCARY HAPPENS diff --git a/src/copypastas/sus.txt b/src/copypastas/sus.txt deleted file mode 100644 index 83deb72..0000000 --- a/src/copypastas/sus.txt +++ /dev/null @@ -1 +0,0 @@ -HOLY SHIT DID YOU JUST SAY THE WORD SUS???😳1?/1😱//1😳/1111!!!! Wait, you don't know what it is from?😳😳😳Let 👆give you a brief r/history. 📚📚📚👨🚀If you didn't r/knowyourshit, the r/term sus(suspicious) is a saying from the r/popular r/game r/AmongUs. Among us is so fun😔 👉👈, don't insult it, every youtuber and streamer says so!!!!!!!11 Corpses voice is so deep am i right or am i right😳😳????? I mean Mr beast and Dream play and pull big 🧠 1000000000000 iq moves in their videos..... YOU WERE THE IMPOSTER.... ඞ ඞ ඞ Get it because you don't know what sus means? r/stupidquestions r/youranidot r/stupidcuck. I CAnT BELEeVE YOUU dont KNoW WHT SUS MeaNS?/??!??!?!!🖕🖕🖕🖕🖕 Man why do i have to r/explain this to a r/idiot🤪🤪🤪📚📚📚... Sus is a GREAT WORD from a GREAT VIDEO GAME. in class, YOU CAN PLAY IT ON YOUR PHONE😜😜😜😜😜😜??!?!? such a masterpiece... FOR THE GREAT PRICE OF FREE!!!11!💰💰🤑🤑🤑🤑😜😜😜💰💰 It can also mean gay 😳😳😳😳 diff --git a/src/copypastas/ticktock.txt b/src/copypastas/ticktock.txt deleted file mode 100644 index bd4f36e..0000000 --- a/src/copypastas/ticktock.txt +++ /dev/null @@ -1,8 +0,0 @@ -Tick-tock -Heavy like a Brinks truck -Looking like I'm tip-top -Shining like a wristwatch -Time will grab your wrist -Lock it down 'til the thing pop -Can you stick around for a minute 'til the ring stop? -Please, God diff --git a/src/copypastas/twitter.txt b/src/copypastas/twitter.txt deleted file mode 100644 index 883cd1b..0000000 --- a/src/copypastas/twitter.txt +++ /dev/null @@ -1,35 +0,0 @@ -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm - diff --git a/src/handlers/error.rs b/src/events/error.rs index e706fec..d98bb5c 100644 --- a/src/handlers/error.rs +++ b/src/events/error.rs @@ -1,4 +1,7 @@ -use crate::{consts::Colors, Data, Error}; +use crate::{ + client::{Data, Error}, + consts::Colors, +}; use log::error; use poise::serenity_prelude::{CreateEmbed, Timestamp}; diff --git a/src/handlers/event/guild.rs b/src/events/guild.rs index 774179c..06af978 100644 --- a/src/handlers/event/guild.rs +++ b/src/events/guild.rs @@ -1,10 +1,10 @@ +use crate::{client::Data, storage}; +use storage::settings::Settings; + use eyre::Result; use log::{debug, warn}; use poise::serenity_prelude::{Guild, UnavailableGuild}; -use crate::{storage, Data}; -use storage::settings::Settings; - pub async fn handle_create(guild: &Guild, data: &Data) -> Result<()> { if let Some(storage) = &data.storage { if storage.guild_settings_exist(&guild.id).await? { diff --git a/src/handlers/event/message.rs b/src/events/message.rs index 67dbb21..e115eb9 100644 --- a/src/handlers/event/message.rs +++ b/src/events/message.rs @@ -1,4 +1,4 @@ -use crate::{consts, Data}; +use crate::{client::Data, consts}; use eyre::{eyre, Result}; use log::{debug, warn}; diff --git a/src/handlers/event/mod.rs b/src/events/mod.rs index cc7d727..390c3a8 100644 --- a/src/handlers/event/mod.rs +++ b/src/events/mod.rs @@ -1,16 +1,17 @@ -use crate::{consts, Data, Error}; +use crate::{client::Data, consts}; use eyre::Result; use log::{debug, info}; use poise::serenity_prelude::{self as serenity, CreateBotAuthParameters}; use serenity::FullEvent; +pub mod error; mod guild; mod message; mod pinboard; mod reactboard; -pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<(), Error> { +pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<()> { match event { FullEvent::Ready { data_about_bot } => { info!("Logged in as {}!", data_about_bot.user.name); diff --git a/src/handlers/event/pinboard.rs b/src/events/pinboard.rs index 5b7d454..bb0dfe0 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/events/pinboard.rs @@ -1,4 +1,4 @@ -use crate::{utils, Data}; +use crate::{client::Data, utils}; use eyre::{eyre, Context as _, OptionExt as _, Result}; use log::{debug, warn}; diff --git a/src/handlers/event/reactboard.rs b/src/events/reactboard.rs index 75fc858..c27bd80 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/events/reactboard.rs @@ -1,4 +1,4 @@ -use crate::{storage, utils, Data}; +use crate::{client::Data, storage, utils}; use storage::reactboard::ReactBoardEntry; use eyre::{eyre, Context as _, Result}; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index 1610d23..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod event; diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..0f16852 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,43 @@ +use eyre::Result; +use log::trace; +use serde::de::DeserializeOwned; + +pub mod shiggy; +pub mod teawie; + +pub type Client = reqwest::Client; +pub type Response = reqwest::Response; + +/// Primary extensions for HTTP Client +pub trait Ext { + async fn get_request(&self, url: &str) -> Result<Response>; + async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T>; + fn default() -> Self; +} + +impl Ext for Client { + fn default() -> Self { + reqwest::ClientBuilder::new() + .user_agent(format!( + "teawie-bot/{}", + option_env!("CARGO_PKG_VERSION").unwrap_or("development") + )) + .build() + .unwrap() + } + + async fn get_request(&self, url: &str) -> Result<Response> { + trace!("Making request to {url}"); + let resp = self.get(url).send().await?; + resp.error_for_status_ref()?; + + Ok(resp) + } + + async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> { + let resp = self.get_request(url).await?; + let json = resp.json().await?; + + Ok(json) + } +} diff --git a/src/http/shiggy.rs b/src/http/shiggy.rs new file mode 100644 index 0000000..397d397 --- /dev/null +++ b/src/http/shiggy.rs @@ -0,0 +1,20 @@ +use eyre::Result; +use serde::Deserialize; + +const SHIGGY: &str = "https://safebooru.donmai.us"; +const RANDOM: &str = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; + +#[derive(Deserialize)] +struct SafebooruResponse { + file_url: String, +} + +pub async fn random<T>(http: &T) -> Result<String> +where + T: super::Ext, +{ + let url = format!("{SHIGGY}{RANDOM}"); + let resp: SafebooruResponse = http.get_json(&url).await?; + + Ok(resp.file_url) +} diff --git a/src/http/teawie.rs b/src/http/teawie.rs new file mode 100644 index 0000000..368fad5 --- /dev/null +++ b/src/http/teawie.rs @@ -0,0 +1,28 @@ +use eyre::{bail, OptionExt, Result}; +use serde::{Deserialize, Serialize}; + +// https://github.com/getchoo/teawieAPI +#[derive(Deserialize, Serialize)] +struct RandomTeawieResponse { + url: Option<String>, + error: Option<String>, +} + +// TODO: read this from an env var +const TEAWIE: &str = "https://api.getchoo.com"; +const RANDOM: &str = "/random_teawie"; + +pub async fn random<T>(http: &T) -> Result<String> +where + T: super::Ext, +{ + let url = format!("{TEAWIE}{RANDOM}"); + let json: RandomTeawieResponse = http.get_json(&url).await?; + + if let Some(error) = json.error { + bail!("TeawieAPI reported error: {error}"); + }; + + json.url + .ok_or_eyre("TeawieAPI didn't return an error or URL???") +} diff --git a/src/main.rs b/src/main.rs index 7f19b9e..e91c8e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,72 +1,18 @@ -use std::{sync::Arc, time::Duration}; - -use eyre::{Context as _, Report, Result}; -use log::{info, trace, warn}; -use poise::{ - serenity_prelude::{self as serenity}, - EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, -}; -use tokio::signal::ctrl_c; -#[cfg(target_family = "unix")] -use tokio::signal::unix::{signal, SignalKind}; -#[cfg(target_family = "windows")] -use tokio::signal::windows::ctrl_close; - -mod api; +mod client; mod commands; mod consts; -mod handlers; +mod events; +mod http; mod storage; mod utils; -use storage::Storage; - -type Error = Box<dyn std::error::Error + Send + Sync>; -type Context<'a> = poise::Context<'a, Data, Error>; - -#[derive(Clone, Debug, Default)] -pub struct Data { - storage: Option<Storage>, -} - -async fn setup(ctx: &serenity::Context) -> Result<Data, Error> { - let storage = Storage::from_env().ok(); - - if let Some(storage) = storage.as_ref() { - if !storage.clone().is_connected() { - return Err( - "You specified a storage backend but there's no connection! Is it running?".into(), - ); - } - trace!("Storage backend connected!"); - - poise::builtins::register_globally(ctx, &commands::to_vec_global()).await?; - info!("Registered global commands!"); - - // register "extra" commands in guilds that allow it - let guilds = storage.get_opted_guilds().await?; - - for guild in guilds { - poise::builtins::register_in_guild(ctx, &commands::to_vec_optional(), guild).await?; - - info!("Registered guild commands to {}", guild); - } - } else { - warn!("No storage backend was specified. Features requiring storage will be disabled"); - warn!("Registering optional commands globally since there's no storage backend"); - poise::builtins::register_globally(ctx, &commands::to_vec()).await?; - } - - let data = Data { storage }; - - Ok(data) -} +use eyre::{Report, Result}; -async fn handle_shutdown(shard_manager: Arc<serenity::ShardManager>, reason: &str) { - warn!("{reason}! Shutting down bot..."); - shard_manager.shutdown_all().await; - println!("Everything is shutdown. Goodbye!"); -} +use tokio::signal::ctrl_c; +#[cfg(target_family = "unix")] +use tokio::signal::unix::{signal, SignalKind}; +#[cfg(target_family = "windows")] +use tokio::signal::windows::ctrl_close; #[tokio::main] async fn main() -> Result<()> { @@ -74,44 +20,9 @@ async fn main() -> Result<()> { color_eyre::install()?; env_logger::init(); - let token = std::env::var("TOKEN").wrap_err("Couldn't find bot token in environment!")?; - - let intents = - serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; - - let options = FrameworkOptions { - commands: commands::to_vec(), - on_error: |error| Box::pin(handlers::error::handle(error)), - - 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::event::handle(ctx, event, data)) - }, - - prefix_options: PrefixFrameworkOptions { - prefix: Some("!".into()), - edit_tracker: Some(Arc::new(EditTracker::for_timespan(Duration::from_secs( - 3600, - )))), - ..Default::default() - }, - - ..Default::default() - }; - - let framework = Framework::builder() - .options(options) - .setup(|ctx, _ready, _framework| Box::pin(setup(ctx))) - .build(); - - let mut client = serenity::ClientBuilder::new(token, intents) - .framework(framework) - .await?; + let mut client = client::get().await?; - let shard_manager = client.shard_manager.clone(); + let shard_manager = client.shard_manager.clone(); // We need this to shut down the bot #[cfg(target_family = "unix")] let mut sigterm = signal(SignalKind::terminate())?; #[cfg(target_family = "windows")] @@ -120,11 +31,13 @@ async fn main() -> Result<()> { tokio::select! { result = client.start() => result.map_err(Report::from), _ = sigterm.recv() => { - handle_shutdown(shard_manager, "Received SIGTERM").await; + client::handle_shutdown(shard_manager, "Received SIGTERM").await; + println!("Everything is shutdown. Goodbye!"); std::process::exit(0); }, _ = ctrl_c() => { - handle_shutdown(shard_manager, "Interrupted").await; + client::handle_shutdown(shard_manager, "Interrupted").await; + println!("Everything is shutdown. Goodbye!"); std::process::exit(130); } } diff --git a/src/utils.rs b/src/utils.rs index 9b642a7..3cab8c3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{consts::Colors, Context}; +use crate::{client::Context, consts::Colors}; use color_eyre::eyre::{eyre, Result}; use poise::serenity_prelude::{self as serenity, CreateEmbedAuthor, CreateEmbedFooter}; |
