diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/guzzle.rs | 28 | ||||
| -rw-r--r-- | src/api/shiggy.rs | 27 | ||||
| -rw-r--r-- | src/commands/ask.rs | 40 | ||||
| -rw-r--r-- | src/commands/bing.rs | 8 | ||||
| -rw-r--r-- | src/commands/bottom.rs | 98 | ||||
| -rw-r--r-- | src/commands/convert.rs | 88 | ||||
| -rw-r--r-- | src/commands/copypasta.rs | 115 | ||||
| -rw-r--r-- | src/commands/mod.rs | 2 | ||||
| -rw-r--r-- | src/commands/random_lore.rs | 25 | ||||
| -rw-r--r-- | src/commands/random_shiggy.rs | 24 | ||||
| -rw-r--r-- | src/commands/random_teawie.rs | 24 | ||||
| -rw-r--r-- | src/commands/teawiespam.rs | 17 | ||||
| -rw-r--r-- | src/consts.rs | 5 | ||||
| -rw-r--r-- | src/handler/events.rs | 24 | ||||
| -rw-r--r-- | src/handler/mod.rs | 34 | ||||
| -rw-r--r-- | src/main.rs | 285 | ||||
| -rw-r--r-- | src/pinboard.rs | 10 | ||||
| -rw-r--r-- | src/utils.rs | 113 |
18 files changed, 407 insertions, 560 deletions
diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs index a13243d..6d1e41b 100644 --- a/src/api/guzzle.rs +++ b/src/api/guzzle.rs @@ -1,4 +1,6 @@ use crate::api::REQWEST_CLIENT; +use crate::Error; + use reqwest::StatusCode; use serde::{Deserialize, Serialize}; @@ -9,22 +11,22 @@ struct GuzzleResponse { const GUZZLE: &str = "https://api.mydadleft.me"; -pub async fn get_random_teawie() -> String { - let endpoint = "get_random_teawie"; +pub async fn get_random_teawie() -> Result<String, Error> { + let endpoint = "/get_random_teawie"; + let req = REQWEST_CLIENT - .get(format!("{GUZZLE}/{endpoint}")) + .get(format!("{GUZZLE}{endpoint}")) .build() .unwrap(); - let resp = REQWEST_CLIENT.execute(req).await.unwrap(); // why did i have to own - // this constant? i have - // no idea! - let err_msg = "couldn't get a teawie"; - match resp.status() { - StatusCode::OK => match resp.json::<GuzzleResponse>().await { - Ok(data) => data.url, - Err(why) => format!("{} ({:?})", err_msg, why), - }, - other => format!("{} ({:?})", err_msg, other), + let resp = REQWEST_CLIENT.execute(req).await.unwrap(); + + if let StatusCode::OK = resp.status() { + match resp.json::<GuzzleResponse>().await { + Ok(data) => Ok(data.url), + Err(why) => Err(Box::new(why)), + } + } else { + Err(resp.status().to_string().into()) } } diff --git a/src/api/shiggy.rs b/src/api/shiggy.rs index 0e9fd19..97895d9 100644 --- a/src/api/shiggy.rs +++ b/src/api/shiggy.rs @@ -1,29 +1,26 @@ use crate::api::REQWEST_CLIENT; +use crate::Error; use reqwest::StatusCode; use serde::Deserialize; const URL: &str = "https://safebooru.donmai.us/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; -const ERROR_MSG: &str = "couldn't get a shiggy"; #[derive(Deserialize)] struct SafebooruResponse { file_url: String, } -pub async fn get_random_shiggy() -> String { - let resp = match REQWEST_CLIENT - .execute(REQWEST_CLIENT.get(URL).build().unwrap()) - .await - { - Ok(r) => r, - Err(e) => return format!("{} ({:?})", ERROR_MSG, e), - }; +pub async fn get_random_shiggy() -> Result<String, Error> { + let req = REQWEST_CLIENT.get(URL).build().unwrap(); - match resp.status() { - StatusCode::OK => match resp.json::<SafebooruResponse>().await { - Ok(sr) => sr.file_url, - Err(e) => format!("{} ({:?})", ERROR_MSG, e), - }, - other => format!("{} ({:?})", ERROR_MSG, other), + let resp = REQWEST_CLIENT.execute(req).await.unwrap(); + + if let StatusCode::OK = resp.status() { + match resp.json::<SafebooruResponse>().await { + Ok(data) => Ok(data.file_url), + Err(why) => Err(Box::new(why)), + } + } else { + Err(resp.status().to_string().into()) } } diff --git a/src/commands/ask.rs b/src/commands/ask.rs index b0b24f3..5075b9d 100644 --- a/src/commands/ask.rs +++ b/src/commands/ask.rs @@ -1,21 +1,23 @@ -use crate::utils; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::command::CommandOptionType; -use serenity::model::prelude::interaction::application_command::CommandDataOption; +use crate::consts; +use crate::utils::random_choice; +use crate::{Context, Error}; -pub fn run(_: &[CommandDataOption]) -> String { - utils::get_random_response() -} - -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("ask") - .description("ask lord teawie a question and they shall respond") - .create_option(|option| { - option - .name("question") - .description("the question you want to ask teawie") - .kind(CommandOptionType::String) - .required(true) - }) +/// ask teawie a question! +#[poise::command(prefix_command, slash_command)] +pub async fn ask( + ctx: Context<'_>, + #[description = "the question you want to ask teawie"] + #[rename = "question"] + _question: String, +) -> Result<(), Error> { + match random_choice(consts::RESPONSES) { + Ok(resp) => { + ctx.say(resp).await?; + Ok(()) + } + Err(why) => { + ctx.say("idk").await?; + Err(why) + } + } } diff --git a/src/commands/bing.rs b/src/commands/bing.rs new file mode 100644 index 0000000..ed91bb3 --- /dev/null +++ b/src/commands/bing.rs @@ -0,0 +1,8 @@ +use crate::{Context, Error}; + +/// make sure the wie is alive +#[poise::command(prefix_command)] +pub async fn bing(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("bong!").await?; + Ok(()) +} diff --git a/src/commands/bottom.rs b/src/commands/bottom.rs index dbe74b9..d38c4b8 100644 --- a/src/commands/bottom.rs +++ b/src/commands/bottom.rs @@ -1,70 +1,42 @@ -use crate::utils::{bottom_decode, bottom_encode}; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::command::CommandOptionType; -use serenity::model::prelude::interaction::application_command::{ - CommandDataOption, CommandDataOptionValue, -}; +use crate::{Context, Error}; +use bottomify::bottom::{decode_string, encode_string}; -pub fn run(options: &[CommandDataOption]) -> String { - let err = "failed to get nested option in"; - - let data = options - .get(0) - .unwrap_or_else(|| panic!("{} {:?}", err, options)); +fn decode_sync(s: &str) -> Result<String, bottomify::bottom::TranslationError> { + decode_string(&s) +} - // get subcommand to decide whether to encode/decode - let subcommand = data.name.as_str(); +#[poise::command(slash_command, subcommands("encode", "decode"))] +pub async fn bottom(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} - // TODO: this is horrendous - // get message content - let option = data - .options - .get(0) - .unwrap_or_else(|| panic!("{} {:?}", err, data)) - .resolved - .as_ref() - .expect("failed to resolve string!"); // this is annoying +/// teawie will translate to bottom 🥺 +#[poise::command(slash_command)] +pub async fn encode( + ctx: Context<'_>, + #[description = "what teawie will translate into bottom"] message: String, +) -> Result<(), Error> { + let encoded = encode_string(&message); + ctx.say(encoded).await?; + Ok(()) +} - if let CommandDataOptionValue::String(msg) = option { - match subcommand { - "encode" => bottom_encode(msg), - "decode" => bottom_decode(msg), - _ => "something went wrong :(".to_owned(), +/// teawie will translate from bottom 🥸 +#[poise::command(slash_command)] +pub async fn decode( + ctx: Context<'_>, + #[description = "what teawie will translate from bottom"] message: String, +) -> Result<(), Error> { + let d = decode_sync(&message); + match d { + Ok(decoded) => { + ctx.say(decoded).await?; + Ok(()) + } + Err(why) => { + ctx.say("couldn't decode that for you, i'm sowwy!! :((".to_string()) + .await?; + Err(Box::new(why)) } - } else { - "did you forget to enter a message?".to_owned() } } - -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("bottom") - .description("teawie will translate something to/from bottom for you 🥺") - // nesting...so much nesting - .create_option(|option| { - option - .name("encode") - .description("teawie will encode a message in bottom for you 🥺") - .kind(CommandOptionType::SubCommand) - .create_sub_option(|suboption| { - suboption - .name("content") - .description("what teawie will translate into bottom") - .kind(CommandOptionType::String) - .required(true) - }) - }) - .create_option(|option| { - option - .name("decode") - .description("teawie will decode a message in bottom for you 🥸") - .kind(CommandOptionType::SubCommand) - .create_sub_option(|suboption| { - suboption - .name("content") - .description("what teawie will translate from bottom") - .kind(CommandOptionType::String) - .required(true) - }) - }) -} diff --git a/src/commands/convert.rs b/src/commands/convert.rs index 8f8c424..c7e09c9 100644 --- a/src/commands/convert.rs +++ b/src/commands/convert.rs @@ -1,70 +1,28 @@ -use crate::utils; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::command::CommandOptionType; -use serenity::model::prelude::interaction::application_command::{ - CommandDataOption, CommandDataOptionValue, -}; +use crate::{Context, Error}; -pub fn run(options: &[CommandDataOption]) -> String { - let err = "couldn't get convert subcommand!"; - let data = options - .get(0) - .unwrap_or_else(|| panic!("{} {:?}", err, options)); - let subcommand = data.name.as_str(); - // get message content - let option = data - .options - .get(0) - .unwrap_or_else(|| panic!("{} {:?}", err, data)) - .resolved - .as_ref() - .expect("failed to resolve string!"); - - let temp = if let &CommandDataOptionValue::Number(number) = option { - match subcommand { - "fahrenheit" => Some(utils::celsius_to_fahrenheit(number)), - "celsius" => Some(utils::fahrenheit_to_celsius(number)), - _ => None, - } - } else { - None - }; +#[poise::command(slash_command, subcommands("to_fahrenheit", "to_celsius"))] +pub async fn convert(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} - if let Some(temp) = temp { - format!("{temp:.2}") - } else { - "couldn't figure it out oops".to_owned() - } +/// ask teawie to convert °F to °C +#[poise::command(slash_command)] +pub async fn to_celsius( + ctx: Context<'_>, + #[description = "what teawie will convert"] degrees_fahrenheit: f32, +) -> Result<(), Error> { + let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); + ctx.say(temp.to_string()).await?; + Ok(()) } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("convertto") - .description("ask teawie to convert something for you") - .create_option(|option| { - option - .name("fahrenheit") - .description("ask teawie to convert celsius to fahrenheit") - .kind(CommandOptionType::SubCommand) - .create_sub_option(|suboption| { - suboption - .name("degrees_celsius") - .description("what teawie will convert") - .kind(CommandOptionType::Number) - .required(true) - }) - }) - .create_option(|option| { - option - .name("celsius") - .description("ask teawie to convert fahrenheit to celsius") - .kind(CommandOptionType::SubCommand) - .create_sub_option(|suboption| { - suboption - .name("degrees_fahrenheit") - .description("what teawie will convert") - .kind(CommandOptionType::Number) - .required(true) - }) - }) +/// ask teawie to convert °C to °F +#[poise::command(slash_command)] +pub async fn to_fahrenheit( + ctx: Context<'_>, + #[description = "what teawie will convert"] degrees_celsius: f32, +) -> Result<(), Error> { + let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; + ctx.say(temp.to_string()).await?; + Ok(()) } diff --git a/src/commands/copypasta.rs b/src/commands/copypasta.rs index 670a5df..dcff558 100644 --- a/src/commands/copypasta.rs +++ b/src/commands/copypasta.rs @@ -1,64 +1,71 @@ use crate::utils; -use serenity::builder::CreateApplicationCommand; -use serenity::http::client::Http; -use serenity::model::id::ChannelId; -use serenity::model::prelude::command::CommandOptionType; -use serenity::model::prelude::interaction::application_command::{ - CommandDataOption, CommandDataOptionValue, -}; -use std::sync::Arc; +use crate::{Context, Error}; +use include_dir::{include_dir, Dir}; +use log::*; +use std::collections::HashMap; -pub async fn run(options: &[CommandDataOption], channel_id: ChannelId, http: &Arc<Http>) -> String { - let err_msg = "expected a copypasta"; - let option = options - .get(0) - .expect(err_msg) - .resolved - .as_ref() - .expect(err_msg); +const FILES: Dir = include_dir!("src/copypastas"); - if let CommandDataOptionValue::String(copypasta) = option { - let replies = utils::get_copypasta(copypasta); - - if replies.len() > 1 { - for reply in replies { - let resp = channel_id.send_message(&http, |m| m.content(reply)).await; +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, poise::ChoiceParameter)] +pub enum Copypastas { + Astral, + DVD, + Egrill, + HappyMeal, + //Ismah, + Sus, + TickTock, + Twitter, +} - match resp { - Ok(_) => continue, - Err(why) => { - println!("couldn't send message: {:?}", why); - return "something went wrong!".to_string(); - } - } - } - return "here's your copypasta:".to_string(); // yes this causes the - // application to not respond. - // no i don't care. +impl Copypastas { + fn as_str(&self) -> &str { + match self { + Copypastas::Astral => "astral", + Copypastas::DVD => "dvd", + Copypastas::Egrill => "egrill", + Copypastas::HappyMeal => "happymeal", + //Copypastas::Ismah => "ismah", + Copypastas::Sus => "sus", + Copypastas::TickTock => "ticktock", + Copypastas::Twitter => "twitter", } - return replies[0].to_string(); + } +} + +fn get_copypasta(name: Copypastas) -> String { + let mut files: HashMap<&str, &str> = HashMap::new(); + + for file in FILES.files() { + let name = file.path().file_stem().unwrap().to_str().unwrap(); + + let contents = file.contents_utf8().unwrap(); + + // refer to files by their name w/o extension + files.insert(name, contents); } - "couldn't find a copypasta".to_string() + if files.contains_key(name.as_str()) { + files[name.as_str()].to_string() + } else { + format!("i don't have a copypasta named {name} :(") + } } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("copypasta") - .description("send funni copypasta") - .create_option(|option| { - option - .name("copypasta") - .description("the copypasta you want to send") - .kind(CommandOptionType::String) - .required(true) - .add_string_choice("astral", "astral") - .add_string_choice("dvd", "dvd") - .add_string_choice("egrill", "egrill") - .add_string_choice("happymeal", "happymeal") - .add_string_choice("ismah", "ismah") - .add_string_choice("sus", "sus") - .add_string_choice("ticktock", "ticktock") - .add_string_choice("twitter", "twitter") - }) +/// ask teawie to send funni copypasta +#[poise::command(slash_command)] +pub async fn copypasta( + ctx: Context<'_>, + #[description = "the copypasta you want to send"] copypasta: Copypastas, +) -> Result<(), Error> { + let gid = ctx.guild_id().unwrap_or_default(); + if !utils::is_guild_allowed(gid) { + info!("not running copypasta command in {gid}"); + return Ok(()); + } + + ctx.say(get_copypasta(copypasta)).await?; + + Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1640707..fe536c1 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,7 +1,9 @@ pub mod ask; +pub mod bing; pub mod bottom; pub mod convert; pub mod copypasta; pub mod random_lore; pub mod random_shiggy; pub mod random_teawie; +pub mod teawiespam; diff --git a/src/commands/random_lore.rs b/src/commands/random_lore.rs index b07660e..875a35e 100644 --- a/src/commands/random_lore.rs +++ b/src/commands/random_lore.rs @@ -1,13 +1,16 @@ -use crate::utils::get_random_lore; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::interaction::application_command::CommandDataOption; +use crate::{consts, utils, Context, Error}; -pub fn run(_: &[CommandDataOption]) -> String { - get_random_lore() -} - -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("random_lore") - .description("get a random piece of teawie lore!") +/// get a random piece of teawie lore! +#[poise::command(prefix_command, slash_command)] +pub async fn random_lore(ctx: Context<'_>) -> Result<(), Error> { + match utils::random_choice(consts::LORE) { + Ok(resp) => { + ctx.say(resp).await?; + Ok(()) + } + Err(why) => { + ctx.say("i can't think of any right now :(").await?; + Err(why) + } + } } diff --git a/src/commands/random_shiggy.rs b/src/commands/random_shiggy.rs index c6aa6de..e509a71 100644 --- a/src/commands/random_shiggy.rs +++ b/src/commands/random_shiggy.rs @@ -1,13 +1,17 @@ use crate::api::shiggy::get_random_shiggy; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::application_command::CommandDataOption; +use crate::{Context, Error}; -pub async fn run(_: &[CommandDataOption]) -> String { - get_random_shiggy().await -} - -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("random_shiggy") - .description("get a random shiggy!") +/// get a random shiggy +#[poise::command(prefix_command, slash_command)] +pub async fn random_shiggy(ctx: Context<'_>) -> Result<(), Error> { + match get_random_shiggy().await { + Ok(resp) => { + ctx.say(resp).await?; + Ok(()) + } + Err(why) => { + ctx.say("i can't get a shiggy right now :(").await?; + Err(why) + } + } } diff --git a/src/commands/random_teawie.rs b/src/commands/random_teawie.rs index b3c433d..8dcc76b 100644 --- a/src/commands/random_teawie.rs +++ b/src/commands/random_teawie.rs @@ -1,13 +1,17 @@ use crate::api::guzzle::get_random_teawie; -use serenity::builder::CreateApplicationCommand; -use serenity::model::prelude::interaction::application_command::CommandDataOption; +use crate::{Context, Error}; -pub async fn run(_: &[CommandDataOption]) -> String { - get_random_teawie().await -} - -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("random_teawie") - .description("get a random teawie!") +/// get a random teawie +#[poise::command(prefix_command, slash_command)] +pub async fn random_teawie(ctx: Context<'_>) -> Result<(), Error> { + match get_random_teawie().await { + Ok(resp) => { + ctx.say(resp).await?; + Ok(()) + } + Err(why) => { + ctx.say("i'm too lazy to send a selfie").await?; + Err(why) + } + } } diff --git a/src/commands/teawiespam.rs b/src/commands/teawiespam.rs new file mode 100644 index 0000000..4964e90 --- /dev/null +++ b/src/commands/teawiespam.rs @@ -0,0 +1,17 @@ +use crate::utils; +use crate::{Context, Error}; +use log::*; + +/// teawie will spam you. +#[poise::command(slash_command, prefix_command)] +pub async fn teawiespam(ctx: Context<'_>) -> Result<(), Error> { + let gid = ctx.guild_id().unwrap_or_default(); + if !utils::is_guild_allowed(gid) { + info!("not running copypasta command in {gid}"); + return Ok(()); + } + + let wies = "<:teawiesmile:1056438046440042546>".repeat(50); + ctx.say(wies).await?; + Ok(()) +} diff --git a/src/consts.rs b/src/consts.rs index b108f34..27a70bc 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,8 @@ +use poise::serenity_prelude::{GuildId, UserId}; + +pub const TEAWIE_GUILD: GuildId = GuildId(1055663552679137310); +pub const BOT: UserId = UserId(1056467120986271764); + pub const TEAMOJIS: [&str; 15] = [ "<:teawiecry:1056438041872433303>", "<:teawiederp:1056438043109757018>", diff --git a/src/handler/events.rs b/src/handler/events.rs new file mode 100644 index 0000000..d971b25 --- /dev/null +++ b/src/handler/events.rs @@ -0,0 +1,24 @@ +use crate::handler::Handler; +use log::*; +use poise::async_trait; +use poise::serenity_prelude::{ChannelPinsUpdateEvent, Context, EventHandler, Message}; + +#[async_trait] +impl EventHandler for Handler { + async fn message(&self, ctx: Context, msg: Message) { + if self.should_echo(&msg) { + let send = msg.reply(&ctx, &msg.content); + if let Err(why) = send.await { + error!("error when replying to {:?}: {:?}", msg.content, why); + } + } + } + + async fn channel_pins_update(&self, ctx: Context, pin: ChannelPinsUpdateEvent) { + let Some(pin_board) = &self.data.pin_board else { + return; + }; + + pin_board.handle_pin(&ctx, &pin).await; + } +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs new file mode 100644 index 0000000..7f7c881 --- /dev/null +++ b/src/handler/mod.rs @@ -0,0 +1,34 @@ +use crate::utils; +use crate::{consts, Data}; +use log::*; + +use poise::serenity_prelude::Message; + +mod events; + +pub struct Handler { + data: Data, +} + +impl Handler { + pub fn new(data: Data) -> Self { + Self { data } + } + + fn should_echo(&self, msg: &Message) -> bool { + let gid = msg.guild_id.unwrap_or_default(); + if msg.author.id == self.data.bot || !utils::is_guild_allowed(gid) { + info!("not running copypasta command in {gid}"); + return false; + } + + let content = &msg.content; + + content == "🗿" + || consts::TEAMOJIS.contains(&content.as_str()) + || content.to_ascii_lowercase() == "moyai" + || content + .to_ascii_lowercase() + .contains("twitter's recommendation algorithm") + } +} diff --git a/src/main.rs b/src/main.rs index 51f0f3c..d4914e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,232 +1,115 @@ -use once_cell::sync::Lazy; -use serenity::async_trait; -use serenity::framework::standard::macros::{command, group}; -use serenity::framework::standard::{CommandResult, StandardFramework}; -use serenity::model::application::command::Command; -use serenity::model::prelude::*; -use serenity::prelude::*; -use utils::parse_snowflake_from_env; +use std::time::Duration; +use std::{env, error}; +use crate::commands::*; +use crate::consts::*; use crate::pinboard::PinBoard; -use crate::utils::parse_snowflakes_from_env; +use log::*; +use poise::serenity_prelude as serentiy; +use poise::serenity_prelude::*; mod api; mod commands; mod consts; +mod handler; mod pinboard; mod utils; -const TEAWIE_GUILD: GuildId = GuildId(1055663552679137310); -const BOT: UserId = UserId(1056467120986271764); +type Error = Box<dyn error::Error + Send + Sync>; +type Context<'a> = poise::Context<'a, Data, Error>; -fn is_guild_allowed(gid: GuildId) -> bool { - // Had to be global state because Serenity doesn't allow you to store - // extra state in frameworks - static ALLOWED_GUILDS: Lazy<Vec<GuildId>> = Lazy::new(|| { - parse_snowflakes_from_env("ALLOWED_GUILDS", GuildId) - .unwrap_or_else(|| vec![TEAWIE_GUILD, GuildId(1091969030694375444)]) - }); - - ALLOWED_GUILDS.contains(&gid) +#[derive(Clone)] +pub struct Data { + bot: serentiy::UserId, + pin_board: Option<PinBoard>, } -#[group] -#[commands(bing, ask, random_lore, random_teawie, teawiespam)] -struct General; - -struct Handler { - bot: UserId, - pin_board: Option<PinBoard>, +impl Default for Data { + fn default() -> Self { + Self::new() + } } -impl Handler { +impl Data { pub fn new() -> Self { - let bot = parse_snowflake_from_env("BOT", UserId).unwrap_or(BOT); + let bot = utils::parse_snowflake_from_env("BOT", UserId).unwrap_or(consts::BOT); let pin_board = PinBoard::new(); Self { bot, pin_board } } - fn should_echo(&self, msg: &Message) -> bool { - // Don't echo to anything we posted ourselves, and don't echo at all unless on certain - // servers - if msg.author.id == self.bot || !is_guild_allowed(msg.guild_id.unwrap_or_default()) { - return false; - } - - let content = &msg.content; - - content == "🗿" - || consts::TEAMOJIS.contains(&content.as_str()) - || content.to_ascii_lowercase() == "moyai" - || content - .to_ascii_lowercase() - .contains("twitter's recommendation algorithm") - } } -#[async_trait] -impl EventHandler for Handler { - /* - * echo some messages when they're sent - */ - async fn message(&self, ctx: Context, msg: Message) { - if self.should_echo(&msg) { - let send = msg.reply(&ctx, &msg.content); - if let Err(why) = send.await { - println!("error when replying to {:?}: {:?}", msg.content, why); - } +async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { + match error { + poise::FrameworkError::Setup { error, .. } => panic!("failed to start bot: {error:?}"), + poise::FrameworkError::Command { error, ctx } => { + error!("error in command {}: {:?}", ctx.command().name, error); } - } - - async fn channel_pins_update(&self, ctx: Context, pin: ChannelPinsUpdateEvent) { - let Some(pin_board) = &self.pin_board else { - return; - }; - - println!( - "audit log: {:#?}", - pin.guild_id - .unwrap() - .audit_logs( - &ctx.http, - Some(Action::Message(MessageAction::Pin).num()), - None, - None, - Some(1), - ) - .await - ); - pin_board.handle_pin(&ctx, &pin).await; - } - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(command) = interaction { - println!("Received command interaction: {command:#?}"); - let content = match command.data.name.as_str() { - "ask" => commands::ask::run(&command.data.options), - "bottom" => commands::bottom::run(&command.data.options), - "convertto" => commands::convert::run(&command.data.options), - "copypasta" => { - commands::copypasta::run(&command.data.options, command.channel_id, &ctx.http) - .await - } - "random_lore" => commands::random_lore::run(&command.data.options), - "random_shiggy" => commands::random_shiggy::run(&command.data.options).await, - "random_teawie" => commands::random_teawie::run(&command.data.options).await, - _ => "not implemented :(".to_string(), - }; - - if let Err(why) = command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content(content)) - }) - .await - { - println!("cannot respond to slash command: {why}"); + error => { + if let Err(e) = poise::builtins::on_error(error).await { + error!("error while handling an error: {}", e); } } } - - async fn ready(&self, ctx: Context, ready: Ready) { - println!("connected as {:?}", ready.user.name); - - let guild_commands = - GuildId::set_application_commands(&TEAWIE_GUILD, &ctx.http, |commands| { - commands.create_application_command(commands::copypasta::register) - }) - .await; - - println!("registered guild commands: {guild_commands:#?}"); - - let commands = Command::set_global_application_commands(&ctx.http, |commands| { - commands - .create_application_command(commands::ask::register) - .create_application_command(commands::bottom::register) - .create_application_command(commands::convert::register) - .create_application_command(commands::random_lore::register) - .create_application_command(commands::random_shiggy::register) - .create_application_command(commands::random_teawie::register) - }) - .await; - - println!("registered global commands: {commands:#?}"); - } } #[tokio::main] async fn main() { - let framework = StandardFramework::new() - .configure(|c| c.prefix("!")) - .group(&GENERAL_GROUP); - - let token = std::env::var("TOKEN").expect("couldn't find token in environment."); - - let intents = GatewayIntents::all(); - let handler = Handler::new(); - - let mut client = Client::builder(token, intents) - .event_handler(handler) - .framework(framework) - .await - .expect("error creating client"); - - if let Err(why) = client.start().await { - println!("an error occurred: {:?}", why); - } -} - -#[command] -async fn bing(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id - .send_message(&ctx.http, |m| m.content("bong")) - .await?; - - Ok(()) -} - -#[command] -async fn ask(ctx: &Context, msg: &Message) -> CommandResult { - let resp = utils::get_random_response(); - msg.channel_id - .send_message(&ctx.http, |m| m.content(resp)) - .await?; - - Ok(()) -} - -#[command] -async fn random_lore(ctx: &Context, msg: &Message) -> CommandResult { - let resp = utils::get_random_lore(); - msg.channel_id - .send_message(&ctx.http, |m| m.content(resp)) - .await?; - - Ok(()) -} - -#[command] -async fn random_teawie(ctx: &Context, msg: &Message) -> CommandResult { - let resp = api::guzzle::get_random_teawie().await; - msg.channel_id - .send_message(&ctx.http, |m| m.content(resp)) - .await?; - - Ok(()) -} - -#[command] -async fn teawiespam(ctx: &Context, msg: &Message) -> CommandResult { - if !is_guild_allowed(msg.guild_id.unwrap_or_default()) { - return Ok(()); - } - - let resp = "<:teawiesmile:1056438046440042546>".repeat(50); - - msg.channel_id - .send_message(&ctx.http, |m| m.content(resp)) - .await?; + env_logger::init(); + dotenvy::dotenv().unwrap(); + + let guild_commands = vec![copypasta::copypasta(), teawiespam::teawiespam()]; + + let options = poise::FrameworkOptions { + commands: vec![ + ask::ask(), + bing::bing(), + bottom::bottom(), + convert::convert(), + random_lore::random_lore(), + random_shiggy::random_shiggy(), + random_teawie::random_teawie(), + copypasta::copypasta(), + teawiespam::teawiespam(), + ], + event_handler: |ctx, event, _, data| { + Box::pin(async move { + // yes this is dumb. no i don't care. + let handler = handler::Handler::new(data.clone()); + event.clone().dispatch(ctx.clone(), &handler).await; + Ok(()) + }) + }, + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some("!".into()), + edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))), + ..Default::default() + }, + on_error: |error| Box::pin(on_error(error)), + command_check: Some(|ctx| { + Box::pin(async move { + Ok(ctx.author().id != ctx.framework().bot_id && ctx.author().id != consts::BOT) + }) + }), + ..Default::default() + }; + + let framework = poise::Framework::builder() + .options(options) + .token(env::var("TOKEN").expect("couldn't find token in environment.")) + .intents(serentiy::GatewayIntents::all()) + .setup(|ctx, _ready, framework| { + Box::pin(async move { + info!("logged in as {}", _ready.user.name); + + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + info!("registered global commands!"); + poise::builtins::register_in_guild(ctx, &guild_commands, TEAWIE_GUILD).await?; + info!("registered guild commands!"); + + Ok(Data::new()) + }) + }); - Ok(()) + framework.run().await.unwrap() } diff --git a/src/pinboard.rs b/src/pinboard.rs index 8f01bff..56f53b4 100644 --- a/src/pinboard.rs +++ b/src/pinboard.rs @@ -1,7 +1,9 @@ use crate::utils::{floor_char_boundary, parse_snowflake_from_env, parse_snowflakes_from_env}; -use serenity::model::prelude::*; -use serenity::prelude::Context; +use log::*; +use poise::serenity_prelude::model::prelude::*; +use poise::serenity_prelude::Context; +#[derive(Clone)] pub struct PinBoard { sources: Option<Vec<ChannelId>>, target: ChannelId, @@ -19,7 +21,8 @@ impl PinBoard { pub async fn handle_pin(&self, ctx: &Context, pin: &ChannelPinsUpdateEvent) { if let Some(sources) = &self.sources { if !sources.contains(&pin.channel_id) { - return; // Not on the list of permitted sources + warn!("can't access source of pin!"); + return; } } @@ -104,6 +107,7 @@ async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option<Use .map(|first| first.user_id) } else { // TODO: mayyyyybe we can guess who pinned something in a DM...? + warn!("couldn't figure out who pinned in {}!", pin.channel_id); None } } diff --git a/src/utils.rs b/src/utils.rs index c2a2031..8773b14 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,9 @@ -use crate::consts::{LORE, RESPONSES}; -use bottomify::bottom::{decode_string, encode_string}; -use include_dir::{include_dir, Dir}; -use rand::seq::SliceRandom; -use std::collections::HashMap; -use std::vec; +use crate::consts::*; +use crate::Error; -const FILES: Dir = include_dir!("src/copypastas"); +use once_cell::sync::Lazy; +use poise::serenity_prelude::GuildId; +use rand::seq::SliceRandom; pub fn parse_snowflake_from_env<T, F: Fn(u64) -> T>(key: &str, f: F) -> Option<T> { std::env::var(key).ok().and_then(|v| v.parse().map(&f).ok()) @@ -21,23 +19,13 @@ pub fn parse_snowflakes_from_env<T, F: Fn(u64) -> T>(key: &str, f: F) -> Option< /* * chooses a random element from an array */ -fn random_choice<const N: usize>(arr: [&str; N]) -> String { +pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String, Error> { let mut rng = rand::thread_rng(); - let resp = arr.choose(&mut rng).expect("couldn't choose random value!"); - (*resp).to_string() -} - -/* - * pub functions to get random elements - * from our consts - */ - -pub fn get_random_response() -> String { - random_choice(RESPONSES) -} - -pub fn get_random_lore() -> String { - random_choice(LORE) + if let Some(resp) = arr.choose(&mut rng) { + Ok((*resp).to_string()) + } else { + Err(Into::into("couldn't choose from arr!")) + } } // waiting for `round_char_boundary` to stabilize @@ -54,79 +42,12 @@ pub fn floor_char_boundary(s: &str, index: usize) -> usize { lower_bound + new_index.unwrap() } } -// waiting for `int_roundings` to stabilize -fn div_ceil(a: usize, b: usize) -> usize { - (a + b - 1) / b -} - -/* - * splits a message into multiple parts so that - * it can fit discord's character limit - */ -fn split_msg(mut msg: String) -> Vec<String> { - const CHAR_LIMIT: usize = 2000; - let mut msgs = Vec::with_capacity(div_ceil(msg.len(), CHAR_LIMIT)); - - while msg.len() > CHAR_LIMIT { - msgs.push(msg.split_off(floor_char_boundary(&msg, CHAR_LIMIT))); - } - msgs -} - -/* - * gets a random copypasta from include/ - */ -pub fn get_copypasta(name: &str) -> Vec<String> { - let mut files: HashMap<&str, &str> = HashMap::new(); - - for file in FILES.files() { - let name = file.path().file_stem().unwrap().to_str().unwrap(); - let contents = file.contents_utf8().unwrap(); +pub fn is_guild_allowed(gid: GuildId) -> bool { + static ALLOWED_GUILDS: Lazy<Vec<GuildId>> = Lazy::new(|| { + parse_snowflakes_from_env("ALLOWED_GUILDS", GuildId) + .unwrap_or_else(|| vec![TEAWIE_GUILD, GuildId(1091969030694375444)]) + }); - // refer to files by their name w/o extension - files.insert(name, contents); - } - - if files.contains_key(&name) { - let reply = files[name].to_string(); - split_msg(reply) - } else { - vec![format!("couldn't find {name:?} in files")] - } -} - -/* - * encodes a message into bottom - */ -pub fn bottom_encode(msg: &str) -> String { - encode_string(&msg) -} - -/* - * decodes a bottom string into english - */ -pub fn bottom_decode(msg: &str) -> String { - let decoded = decode_string(&msg); - match decoded { - Ok(ret) => ret, - Err(why) => { - println!("couldn't decode {msg:?}! ({why:?})"); - "couldn't decode that! sowwy 🥺".to_string() - } - } -} - -/* - * converts celsius to fahrenheit - */ -pub fn celsius_to_fahrenheit(c: f64) -> f64 { - (c * (9.0 / 5.0)) + 32.0 -} - -/* - * converts fahrenheit to celsius - */ -pub fn fahrenheit_to_celsius(f: f64) -> f64 { - (f - 32.0) * (5.0 / 9.0) + ALLOWED_GUILDS.contains(&gid) } |
