diff --git a/README.md b/README.md index 7c00ee7..f933e60 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,13 @@ __Other Variables__ * `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran * `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process * `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages + +### Todo List + +* Implement remainder of the `macro` command +* Convert aliases to macros +* Block users from interacting with another users' components +* Split out framework +* Help command +* Change all db keys to be discord IDs +* Test everything diff --git a/src/commands/moderation_cmds.rs b/src/commands/moderation_cmds.rs index e56dcf4..9be74be 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -2,11 +2,14 @@ use chrono::offset::Utc; use chrono_tz::{Tz, TZ_VARIANTS}; use levenshtein::levenshtein; use regex_command_attr::command; -use serenity::{client::Context, model::misc::Mentionable}; +use serenity::{ + client::Context, + model::{id::GuildId, misc::Mentionable}, +}; use crate::{ - component_models::{ComponentDataModel, Restrict}, - consts::THEME_COLOR, + component_models::{pager::Pager, ComponentDataModel, Restrict}, + consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue}, hooks::{CHECK_GUILD_PERMISSIONS_HOOK, CHECK_MANAGED_PERMISSIONS_HOOK}, models::{channel_data::ChannelData, command_macro::CommandMacro, CtxData}, @@ -335,11 +338,11 @@ async fn macro_cmd(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptio &ctx, CreateGenericResponse::new().ephemeral().embed(|e| { e - .title("Macro Recording Started") - .description( + .title("Macro Recording Started") + .description( "Run up to 5 commands, or type `/macro finish` to stop at any point. Any commands ran as part of recording will be inconsequential") - .color(*THEME_COLOR) + .color(*THEME_COLOR) }), ) .await; @@ -396,7 +399,13 @@ Any commands ran as part of recording will be inconsequential") lock.remove(&key); } } - "list" => {} + "list" => { + let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await; + + let resp = show_macro_page(¯os, 0, invoke.guild_id().unwrap()); + + invoke.respond(&ctx, resp).await; + } "run" => { let macro_name = args.get("name").unwrap().to_string(); @@ -435,7 +444,137 @@ Any commands ran as part of recording will be inconsequential") } } } - "delete" => {} + "delete" => { + let macro_name = args.get("name").unwrap().to_string(); + + match sqlx::query!( + "SELECT id FROM macro WHERE guild_id = ? AND name = ?", + invoke.guild_id().unwrap().0, + macro_name + ) + .fetch_one(&pool) + .await + { + Ok(row) => { + sqlx::query!("DELETE FROM macro WHERE id = ?", row.id).execute(&pool).await; + + let _ = invoke + .respond( + &ctx, + CreateGenericResponse::new() + .content(format!("Macro \"{}\" deleted", macro_name)), + ) + .await; + } + + Err(sqlx::Error::RowNotFound) => { + let _ = invoke + .respond( + &ctx, + CreateGenericResponse::new() + .content(format!("Macro \"{}\" not found", macro_name)), + ) + .await; + } + + Err(e) => { + panic!("{}", e); + } + } + } _ => {} } } + +pub fn max_macro_page(macros: &[CommandMacro]) -> usize { + let mut skipped_char_count = 0; + + macros + .iter() + .map(|m| { + if let Some(description) = &m.description { + format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len()) + } else { + format!("**{}**\n- Has {} commands", m.name, m.commands.len()) + } + }) + .fold(1, |mut pages, p| { + skipped_char_count += p.len(); + + if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH { + skipped_char_count = p.len(); + pages += 1; + } + + pages + }) +} + +fn show_macro_page( + macros: &[CommandMacro], + page: usize, + guild_id: GuildId, +) -> CreateGenericResponse { + let pager = Pager::new(page, guild_id); + + if macros.is_empty() { + return CreateGenericResponse::new().embed(|e| { + e.title("Macros") + .description("No Macros Set Up. Use `/macro record` to get started.") + .color(*THEME_COLOR) + }); + } + + let pages = max_macro_page(macros); + + let mut page = page; + if page >= pages { + page = pages - 1; + } + + let mut char_count = 0; + let mut skipped_char_count = 0; + + let mut skipped_pages = 0; + + let display_vec: Vec = macros + .iter() + .map(|m| { + if let Some(description) = &m.description { + format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len()) + } else { + format!("**{}**\n- Has {} commands", m.name, m.commands.len()) + } + }) + .skip_while(|p| { + skipped_char_count += p.len(); + + if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH { + skipped_char_count = p.len(); + skipped_pages += 1; + } + + skipped_pages < page + }) + .take_while(|p| { + char_count += p.len(); + + char_count < EMBED_DESCRIPTION_MAX_LENGTH + }) + .collect::>(); + + let display = display_vec.join("\n"); + + CreateGenericResponse::new() + .embed(|e| { + e.title("Macros") + .description(display) + .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) + .color(*THEME_COLOR) + }) + .components(|comp| { + pager.create_button_row(pages, comp); + + comp + }) +} diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index de1b3f8..f06367a 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -8,7 +8,11 @@ use chrono::NaiveDateTime; use chrono_tz::Tz; use num_integer::Integer; use regex_command_attr::command; -use serenity::{builder::CreateEmbed, client::Context, model::channel::Channel}; +use serenity::{ + builder::CreateEmbed, + client::Context, + model::{channel::Channel, id::UserId}, +}; use crate::{ check_subscription_on_message, @@ -363,7 +367,7 @@ async fn delete(ctx: &Context, invoke: &mut CommandInvoke, _args: CommandOptions let reminders = Reminder::from_guild(ctx, invoke.guild_id(), invoke.author_id()).await; - let resp = show_delete_page(&reminders, 0, timezone); + let resp = show_delete_page(&reminders, 0, timezone, invoke.author_id()); let _ = invoke.respond(&ctx, resp).await; } @@ -394,8 +398,9 @@ pub fn show_delete_page( reminders: &[Reminder], page: usize, timezone: Tz, + author_id: UserId, ) -> CreateGenericResponse { - let pager = Pager::new(page, DelData { timezone }); + let pager = Pager::new(page, DelData { author_id, timezone }); if reminders.is_empty() { return CreateGenericResponse::new() @@ -450,7 +455,7 @@ pub fn show_delete_page( let display = display_vec.join("\n"); - let del_selector = ComponentDataModel::DelSelector(DelSelector { page, timezone }); + let del_selector = ComponentDataModel::DelSelector(DelSelector { page, timezone, author_id }); CreateGenericResponse::new() .embed(|e| { diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index 4ca64ab..b13d95e 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -37,6 +37,7 @@ pub enum ComponentDataModel { LookPager(Pager), DelPager(Pager), TodoPager(Pager), + MacroPager(Pager), } impl ComponentDataModel { @@ -89,7 +90,16 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id .await .unwrap(); } else { - // tell them they cant do this + component + .create_interaction_response(&ctx, |r| { + r.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|response| response + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + .content("Only the original command user can interact with this message") + ) + }) + .await + .unwrap(); } } ComponentDataModel::LookPager(pager) => { @@ -168,16 +178,33 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id .await; } ComponentDataModel::DelPager(pager) => { - let reminders = - Reminder::from_guild(ctx, component.guild_id, component.user.id).await; + if component.user.id == pager.data.author_id { + let reminders = + Reminder::from_guild(ctx, component.guild_id, component.user.id).await; - let max_pages = max_delete_page(&reminders, &pager.data.timezone); + let max_pages = max_delete_page(&reminders, &pager.data.timezone); - let resp = - show_delete_page(&reminders, pager.next_page(max_pages), pager.data.timezone); + let resp = show_delete_page( + &reminders, + pager.next_page(max_pages), + pager.data.timezone, + pager.data.author_id, + ); - let mut invoke = CommandInvoke::component(component); - let _ = invoke.respond(&ctx, resp).await; + let mut invoke = CommandInvoke::component(component); + let _ = invoke.respond(&ctx, resp).await; + } else { + component + .create_interaction_response(&ctx, |r| { + r.kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|response| response + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + .content("Only the original command user can interact with this message") + ) + }) + .await + .unwrap(); + } } ComponentDataModel::DelSelector(selector) => { let pool = ctx.data.read().await.get::().cloned().unwrap(); @@ -191,7 +218,12 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id let reminders = Reminder::from_guild(ctx, component.guild_id, component.user.id).await; - let resp = show_delete_page(&reminders, selector.page, selector.timezone); + let resp = show_delete_page( + &reminders, + selector.page, + selector.timezone, + selector.author_id, + ); let mut invoke = CommandInvoke::component(component); let _ = invoke.respond(&ctx, resp).await; @@ -260,6 +292,7 @@ INSERT IGNORE INTO roles (role, name, guild_id) VALUES (?, \"Role\", (SELECT id let mut invoke = CommandInvoke::component(component); let _ = invoke.respond(&ctx, resp).await; } + ComponentDataModel::MacroPager(pager) => {} } } } @@ -275,6 +308,7 @@ pub struct Restrict { pub struct DelSelector { pub page: usize, pub timezone: Tz, + pub author_id: UserId, } #[derive(Serialize, Deserialize)] diff --git a/src/component_models/pager.rs b/src/component_models/pager.rs index 611452b..af46854 100644 --- a/src/component_models/pager.rs +++ b/src/component_models/pager.rs @@ -2,7 +2,10 @@ use chrono_tz::Tz; use rmp_serde::Serializer; use serde::{Deserialize, Serialize}; use serde_repr::*; -use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle}; +use serenity::{ + builder::CreateComponents, + model::{id::UserId, interactions::message_component::ButtonStyle}, +}; use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags}; @@ -102,6 +105,7 @@ pub struct LookData { #[derive(Deserialize, Serialize, Clone)] pub struct DelData { + pub author_id: UserId, pub timezone: Tz, } diff --git a/src/main.rs b/src/main.rs index d8a5234..e152451 100644 --- a/src/main.rs +++ b/src/main.rs @@ -305,9 +305,6 @@ async fn main() -> Result<(), Box> { .add_command(&reminder_cmds::TIMER_COMMAND) .add_command(&reminder_cmds::REMIND_COMMAND) /* - .add_command("r", &reminder_cmds::REMIND_COMMAND) - .add_command("interval", &reminder_cmds::INTERVAL_COMMAND) - .add_command("i", &reminder_cmds::INTERVAL_COMMAND) .add_command("natural", &reminder_cmds::NATURAL_COMMAND) .add_command("n", &reminder_cmds::NATURAL_COMMAND) .add_command("", &reminder_cmds::NATURAL_COMMAND) @@ -326,10 +323,6 @@ async fn main() -> Result<(), Box> { .add_command(&moderation_cmds::TIMEZONE_COMMAND) .add_command(&moderation_cmds::PREFIX_COMMAND) .add_command(&moderation_cmds::MACRO_CMD_COMMAND) - /* - .add_command("alias", &moderation_cmds::ALIAS_COMMAND) - .add_command("a", &moderation_cmds::ALIAS_COMMAND) - */ .add_hook(&hooks::CHECK_SELF_PERMISSIONS_HOOK) .add_hook(&hooks::MACRO_CHECK_HOOK) .build(); diff --git a/src/models/command_macro.rs b/src/models/command_macro.rs index 4121faa..f9ba066 100644 --- a/src/models/command_macro.rs +++ b/src/models/command_macro.rs @@ -1,6 +1,6 @@ -use serenity::model::id::GuildId; +use serenity::{client::Context, model::id::GuildId}; -use crate::framework::CommandOptions; +use crate::{framework::CommandOptions, SQLPool}; pub struct CommandMacro { pub guild_id: GuildId, @@ -8,3 +8,23 @@ pub struct CommandMacro { pub description: Option, pub commands: Vec, } + +impl CommandMacro { + pub async fn from_guild(ctx: &Context, guild_id: impl Into) -> Vec { + let pool = ctx.data.read().await.get::().cloned().unwrap(); + let guild_id = guild_id.into(); + + sqlx::query!("SELECT * FROM macro WHERE guild_id = ?", guild_id.0) + .fetch_all(&pool) + .await + .unwrap() + .iter() + .map(|row| Self { + guild_id: GuildId(row.guild_id), + name: row.name.clone(), + description: row.description.clone(), + commands: serde_json::from_str(&row.commands).unwrap(), + }) + .collect::>() + } +}