diff --git a/README.md b/README.md index f933e60..0847c08 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,8 @@ __Other Variables__ ### 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 9be74be..6b36896 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -2,13 +2,13 @@ use chrono::offset::Utc; use chrono_tz::{Tz, TZ_VARIANTS}; use levenshtein::levenshtein; use regex_command_attr::command; -use serenity::{ - client::Context, - model::{id::GuildId, misc::Mentionable}, -}; +use serenity::{client::Context, model::misc::Mentionable}; use crate::{ - component_models::{pager::Pager, ComponentDataModel, Restrict}, + component_models::{ + pager::{MacroPager, Pager}, + ComponentDataModel, Restrict, + }, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue}, hooks::{CHECK_GUILD_PERMISSIONS_HOOK, CHECK_MANAGED_PERMISSIONS_HOOK}, @@ -16,52 +16,6 @@ use crate::{ PopularTimezones, RecordingMacros, RegexFramework, SQLPool, }; -#[command("blacklist")] -#[description("Block channels from using bot commands")] -#[arg( - name = "channel", - description = "The channel to blacklist", - kind = "Channel", - required = false -)] -#[supports_dm(false)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -#[can_blacklist(false)] -async fn blacklist(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - let channel = match args.get("channel") { - Some(OptionValue::Channel(channel_id)) => *channel_id, - - _ => invoke.channel_id(), - } - .to_channel_cached(&ctx) - .unwrap(); - - let mut channel_data = ChannelData::from_channel(&channel, &pool).await.unwrap(); - - channel_data.blacklisted = !channel_data.blacklisted; - channel_data.commit_changes(&pool).await; - - if channel_data.blacklisted { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("{} has been blacklisted", channel.mention())), - ) - .await; - } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("{} has been removed from the blacklist", channel.mention())), - ) - .await; - } -} - #[command("timezone")] #[description("Select your timezone")] #[arg( @@ -173,44 +127,6 @@ You may want to use one of the popular timezones below, otherwise click [here](h } } -#[command("prefix")] -#[description("Configure a prefix for text-based commands (deprecated)")] -#[supports_dm(false)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn prefix(ctx: &Context, invoke: &mut CommandInvoke, args: String) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap(); - - if args.len() > 5 { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Please select a prefix under 5 characters"), - ) - .await; - } else if args.is_empty() { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Please use this command as `@reminder-bot prefix `"), - ) - .await; - } else { - guild_data.write().await.prefix = args; - guild_data.read().await.commit_changes(&pool).await; - - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("Prefix changed to {}", guild_data.read().await.prefix)), - ) - .await; - } -} - #[command("restrict")] #[description("Configure which roles can use commands on the bot")] #[arg( @@ -402,9 +318,9 @@ Any commands ran as part of recording will be inconsequential") "list" => { let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await; - let resp = show_macro_page(¯os, 0, invoke.guild_id().unwrap()); + let resp = show_macro_page(¯os, 0); - invoke.respond(&ctx, resp).await; + invoke.respond(&ctx, resp).await.unwrap(); } "run" => { let macro_name = args.get("name").unwrap().to_string(); @@ -456,7 +372,10 @@ Any commands ran as part of recording will be inconsequential") .await { Ok(row) => { - sqlx::query!("DELETE FROM macro WHERE id = ?", row.id).execute(&pool).await; + sqlx::query!("DELETE FROM macro WHERE id = ?", row.id) + .execute(&pool) + .await + .unwrap(); let _ = invoke .respond( @@ -510,12 +429,8 @@ pub fn max_macro_page(macros: &[CommandMacro]) -> usize { }) } -fn show_macro_page( - macros: &[CommandMacro], - page: usize, - guild_id: GuildId, -) -> CreateGenericResponse { - let pager = Pager::new(page, guild_id); +pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateGenericResponse { + let pager = MacroPager::new(page); if macros.is_empty() { return CreateGenericResponse::new().embed(|e| { diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index b046011..a94baa9 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -16,15 +16,10 @@ use crate::{ pager::{DelPager, LookPager, Pager}, ComponentDataModel, DelSelector, }, - consts::{ - EMBED_DESCRIPTION_MAX_LENGTH, REGEX_CHANNEL_USER, REGEX_NATURAL_COMMAND_1, - REGEX_NATURAL_COMMAND_2, SELECT_MAX_ENTRIES, THEME_COLOR, - }, + consts::{EMBED_DESCRIPTION_MAX_LENGTH, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES, THEME_COLOR}, framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue}, hooks::{CHECK_GUILD_PERMISSIONS_HOOK, CHECK_MANAGED_PERMISSIONS_HOOK}, models::{ - channel_data::ChannelData, - guild_data::GuildData, reminder::{ builder::{MultiReminderBuilder, ReminderScope}, content::Content, @@ -704,6 +699,7 @@ async fn remind(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) } }; + // todo gate this on patreon subscription let interval = args .get("repeat") .map(|arg| { @@ -805,178 +801,3 @@ fn parse_mention_list(mentions: &str) -> Vec { }) .collect::>() } - -/* -#[command("natural")] -#[permission_level(Managed)] -async fn natural(ctx: &Context, msg: &Message, args: String) { - let (pool, lm) = get_ctx_data(&ctx).await; - - let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); - - match REGEX_NATURAL_COMMAND_1.captures(&args) { - Some(captures) => { - let (expires, interval, string_content) = - if check_subscription_on_message(&ctx, msg).await { - let rest_content = captures.name("msg").unwrap().as_str(); - - match REGEX_NATURAL_COMMAND_2.captures(rest_content) { - Some(secondary_captures) => { - let expires = - if let Some(expires_crop) = secondary_captures.name("expires") { - natural_parser(expires_crop.as_str(), &user_data.timezone).await - } else { - None - }; - - let interval = - if let Some(interval_crop) = secondary_captures.name("interval") { - humantime::parse_duration(interval_crop.as_str()) - .or_else(|_| { - humantime::parse_duration(&format!( - "1 {}", - interval_crop.as_str() - )) - }) - .map(|duration| duration.as_secs() as i64) - .ok() - } else { - None - }; - - ( - expires, - interval, - if interval.is_some() { - secondary_captures.name("msg").unwrap().as_str() - } else { - rest_content - }, - ) - } - - None => (None, None, rest_content), - } - } else { - (None, None, captures.name("msg").unwrap().as_str()) - }; - - let location_ids = if let Some(mentions) = captures.name("mentions").map(|m| m.as_str()) - { - parse_mention_list(mentions) - } else { - vec![ReminderScope::Channel(msg.channel_id.into())] - }; - - if let Some(timestamp) = - natural_parser(captures.name("time").unwrap().as_str(), &user_data.timezone).await - { - let content_res = Content::build(string_content, msg).await; - - match content_res { - Ok(mut content) => { - if let Some(guild) = msg.guild(&ctx) { - content.substitute(guild); - } - - let user_data = ctx.user_data(&msg.author).await.unwrap(); - - let mut builder = MultiReminderBuilder::new(ctx, msg.guild_id) - .author(user_data) - .content(content) - .interval(interval) - .expires(expires) - .time(timestamp); - - builder.set_scopes(location_ids); - - let (errors, successes) = builder.build().await; - - let success_part = match successes.len() { - 0 => "".to_string(), - n => format!( - "Reminder{s} for {locations} set for ", - s = if n > 1 { "s" } else { "" }, - locations = successes - .iter() - .map(|l| l.mention()) - .collect::>() - .join(", "), - offset = timestamp - ), - }; - - let error_part = match errors.len() { - 0 => "".to_string(), - n => format!( - "{n} reminder{s} failed to set:\n{errors}", - s = if n > 1 { "s" } else { "" }, - n = n, - errors = errors - .iter() - .map(|e| e.display(true)) - .collect::>() - .join("\n") - ), - }; - - let _ = msg - .channel_id - .send_message(&ctx, |m| { - m.embed(|e| { - e.title(format!( - "{n} Reminder{s} Set", - n = successes.len(), - s = if successes.len() > 1 { "s" } else { "" } - )) - .description(format!("{}\n\n{}", success_part, error_part)) - .color(*THEME_COLOR) - }) - }) - .await; - } - - Err(content_error) => { - let _ = msg - .channel_id - .send_message(ctx, |m| { - m.embed(move |e| { - e.title("0 Reminders Set") - .description(content_error.to_string()) - .color(*THEME_COLOR) - }) - }) - .await; - } - } - } else { - let _ = msg - .channel_id - .send_message(ctx, |m| { - m.embed(move |e| { - e.title( - lm.get(&user_data.language, "remind/title") - .replace("{number}", "0"), - ) - .description(lm.get(&user_data.language, "natural/invalid_time")) - .color(*THEME_COLOR) - }) - }) - .await; - } - } - - None => { - command_help( - ctx, - msg, - lm, - &ctx.prefix(msg.guild_id).await, - &user_data.language, - "natural", - ) - .await; - } - } -} -*/ diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index d0ba9d9..29188d0 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -18,13 +18,14 @@ use serenity::{ use crate::{ commands::{ + moderation_cmds::{max_macro_page, show_macro_page}, reminder_cmds::{max_delete_page, show_delete_page}, todo_cmds::{max_todo_page, show_todo_page}, }, - component_models::pager::{DelPager, LookPager, Pager, TodoPager}, + component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, framework::CommandInvoke, - models::reminder::Reminder, + models::{command_macro::CommandMacro, reminder::Reminder}, SQLPool, }; @@ -38,6 +39,7 @@ pub enum ComponentDataModel { TodoPager(TodoPager), DelSelector(DelSelector), TodoSelector(TodoSelector), + MacroPager(MacroPager), } impl ComponentDataModel { @@ -260,6 +262,17 @@ 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) => { + let mut invoke = CommandInvoke::component(component); + + let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await; + + let max_page = max_macro_page(¯os); + let page = pager.next_page(max_page); + + let resp = show_macro_page(¯os, page); + let _ = invoke.respond(&ctx, resp).await; + } } } } diff --git a/src/component_models/pager.rs b/src/component_models/pager.rs index 0d41ab0..8ab83e2 100644 --- a/src/component_models/pager.rs +++ b/src/component_models/pager.rs @@ -330,3 +330,82 @@ impl TodoPager { ) } } + +#[derive(Serialize, Deserialize)] +pub struct MacroPager { + pub page: usize, + action: PageAction, +} + +impl Pager for MacroPager { + fn next_page(&self, max_pages: usize) -> usize { + match self.action { + PageAction::First => 0, + PageAction::Previous => 0.max(self.page - 1), + PageAction::Refresh => self.page, + PageAction::Next => (max_pages - 1).min(self.page + 1), + PageAction::Last => max_pages - 1, + } + } + + fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) { + let next_page = self.next_page(max_pages); + + let (page_first, page_prev, page_refresh, page_next, page_last) = + MacroPager::buttons(next_page); + + comp.create_action_row(|row| { + row.create_button(|b| { + b.label("⏮️") + .style(ButtonStyle::Primary) + .custom_id(page_first.to_custom_id()) + .disabled(next_page == 0) + }) + .create_button(|b| { + b.label("◀️") + .style(ButtonStyle::Secondary) + .custom_id(page_prev.to_custom_id()) + .disabled(next_page == 0) + }) + .create_button(|b| { + b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id()) + }) + .create_button(|b| { + b.label("▶️") + .style(ButtonStyle::Secondary) + .custom_id(page_next.to_custom_id()) + .disabled(next_page + 1 == max_pages) + }) + .create_button(|b| { + b.label("⏭️") + .style(ButtonStyle::Primary) + .custom_id(page_last.to_custom_id()) + .disabled(next_page + 1 == max_pages) + }) + }); + } +} + +impl MacroPager { + pub fn new(page: usize) -> Self { + Self { page, action: PageAction::Refresh } + } + + pub fn buttons( + page: usize, + ) -> ( + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ComponentDataModel, + ) { + ( + ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::First }), + ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Previous }), + ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Refresh }), + ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Next }), + ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Last }), + ) + } +} diff --git a/src/framework.rs b/src/framework.rs index 47aa1fb..1efb652 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -243,9 +243,9 @@ impl CommandInvoke { .map(|_| ()) } } - InvokeModel::Component(i) => { - if self.already_responded { - i.create_followup_message(http, |d| { + InvokeModel::Component(i) => i + .create_interaction_response(http, |r| { + r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(|d| { d.content(generic_response.content); if let Some(embed) = generic_response.embed { @@ -261,51 +261,9 @@ impl CommandInvoke { d }) - .await - .map(|_| ()) - } else if self.deferred { - i.edit_original_interaction_response(http, |d| { - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - .await - .map(|_| ()) - } else { - i.create_interaction_response(http, |r| { - r.kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|d| { - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - }) - .await - .map(|_| ()) - } - } + }) + .await + .map(|_| ()), InvokeModel::Text(m) => m .channel_id .send_message(http, |m| { @@ -457,7 +415,12 @@ impl CommandOptions { cmd_opts.options.insert( option.name, OptionValue::User(UserId( - option.value.map(|m| m.as_u64()).flatten().unwrap(), + option + .value + .map(|m| m.as_str().map(|s| s.parse::().ok())) + .flatten() + .flatten() + .unwrap(), )), ); } @@ -465,7 +428,12 @@ impl CommandOptions { cmd_opts.options.insert( option.name, OptionValue::Channel(ChannelId( - option.value.map(|m| m.as_u64()).flatten().unwrap(), + option + .value + .map(|m| m.as_str().map(|s| s.parse::().ok())) + .flatten() + .flatten() + .unwrap(), )), ); } diff --git a/src/main.rs b/src/main.rs index e152451..a00b1a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -304,11 +304,6 @@ async fn main() -> Result<(), Box> { // reminder commands .add_command(&reminder_cmds::TIMER_COMMAND) .add_command(&reminder_cmds::REMIND_COMMAND) - /* - .add_command("natural", &reminder_cmds::NATURAL_COMMAND) - .add_command("n", &reminder_cmds::NATURAL_COMMAND) - .add_command("", &reminder_cmds::NATURAL_COMMAND) - */ // management commands .add_command(&reminder_cmds::DELETE_COMMAND) .add_command(&reminder_cmds::LOOK_COMMAND) @@ -318,10 +313,8 @@ async fn main() -> Result<(), Box> { // to-do commands .add_command(&todo_cmds::TODO_COMMAND) // moderation commands - .add_command(&moderation_cmds::BLACKLIST_COMMAND) .add_command(&moderation_cmds::RESTRICT_COMMAND) .add_command(&moderation_cmds::TIMEZONE_COMMAND) - .add_command(&moderation_cmds::PREFIX_COMMAND) .add_command(&moderation_cmds::MACRO_CMD_COMMAND) .add_hook(&hooks::CHECK_SELF_PERMISSIONS_HOOK) .add_hook(&hooks::MACRO_CHECK_HOOK) diff --git a/src/models/reminder/content.rs b/src/models/reminder/content.rs index 0c9e6f2..cb9d0c4 100644 --- a/src/models/reminder/content.rs +++ b/src/models/reminder/content.rs @@ -1,8 +1,3 @@ -use regex::Captures; -use serenity::model::{channel::Message, guild::Guild, misc::Mentionable}; - -use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError}; - pub struct Content { pub content: String, pub tts: bool, @@ -14,55 +9,4 @@ impl Content { pub fn new() -> Self { Self { content: "".to_string(), tts: false, attachment: None, attachment_name: None } } - - pub async fn build(content: S, message: &Message) -> Result { - if message.attachments.len() > 1 { - Err(ContentError::TooManyAttachments) - } else if let Some(attachment) = message.attachments.get(0) { - if attachment.size > 8_000_000 { - Err(ContentError::AttachmentTooLarge) - } else if let Ok(attachment_bytes) = attachment.download().await { - Ok(Self { - content: content.to_string(), - tts: false, - attachment: Some(attachment_bytes), - attachment_name: Some(attachment.filename.clone()), - }) - } else { - Err(ContentError::AttachmentDownloadFailed) - } - } else { - Ok(Self { - content: content.to_string(), - tts: false, - attachment: None, - attachment_name: None, - }) - } - } - - pub fn substitute(&mut self, guild: Guild) { - if self.content.starts_with("/tts ") { - self.tts = true; - self.content = self.content.split_off(5); - } - - self.content = REGEX_CONTENT_SUBSTITUTION - .replace(&self.content, |caps: &Captures| { - if let Some(user) = caps.name("user") { - format!("<@{}>", user.as_str()) - } else if let Some(role_name) = caps.name("role") { - if let Some(role) = guild.role_by_name(role_name.as_str()) { - role.mention().to_string() - } else { - format!("<<{}>>", role_name.as_str().to_string()) - } - } else { - String::new() - } - }) - .to_string() - .replace("<>", "@everyone") - .replace("<>", "@here"); - } } diff --git a/src/models/reminder/errors.rs b/src/models/reminder/errors.rs index 141feb8..fbd54fd 100644 --- a/src/models/reminder/errors.rs +++ b/src/models/reminder/errors.rs @@ -7,8 +7,6 @@ pub enum ReminderError { PastTime, ShortInterval, InvalidTag, - InvalidTime, - InvalidExpiration, DiscordError(String), } @@ -32,31 +30,7 @@ impl ToString for ReminderError { ReminderError::InvalidTag => { "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string() } - ReminderError::InvalidTime => { - "Your time failed to process. Please make it as clear as possible, for example `\"16th of july\"` or `\"in 20 minutes\"`".to_string() - } - ReminderError::InvalidExpiration => { - "Your expiration time failed to process. Please make it as clear as possible, for example `\"16th of july\"` or `\"in 20 minutes\"`".to_string() - } ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s), } } } - -#[derive(Debug)] -pub enum ContentError { - TooManyAttachments, - AttachmentTooLarge, - AttachmentDownloadFailed, -} - -impl ToString for ContentError { - fn to_string(&self) -> String { - match self { - ContentError::TooManyAttachments => "remind/too_many_attachments", - ContentError::AttachmentTooLarge => "remind/attachment_too_large", - ContentError::AttachmentDownloadFailed => "remind/attachment_download_failed", - } - .to_string() - } -}