summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorseth <[email protected]>2023-07-10 00:18:36 -0400
committerseth <[email protected]>2023-11-16 00:35:07 +0000
commita4a9353e1c8f902b7d7b3cf74e3e5b129c214330 (patch)
treeb58da1d30af52e97c0251e0d6882cd0ccdfeb20a /src
parent5e9ec7f008e01d25c0b7f782c5ae043bc9ca0933 (diff)
start using poise
Diffstat (limited to 'src')
-rw-r--r--src/api/guzzle.rs28
-rw-r--r--src/api/shiggy.rs27
-rw-r--r--src/commands/ask.rs40
-rw-r--r--src/commands/bing.rs8
-rw-r--r--src/commands/bottom.rs98
-rw-r--r--src/commands/convert.rs88
-rw-r--r--src/commands/copypasta.rs115
-rw-r--r--src/commands/mod.rs2
-rw-r--r--src/commands/random_lore.rs25
-rw-r--r--src/commands/random_shiggy.rs24
-rw-r--r--src/commands/random_teawie.rs24
-rw-r--r--src/commands/teawiespam.rs17
-rw-r--r--src/consts.rs5
-rw-r--r--src/handler/events.rs24
-rw-r--r--src/handler/mod.rs34
-rw-r--r--src/main.rs285
-rw-r--r--src/pinboard.rs10
-rw-r--r--src/utils.rs113
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)
}