2021-09-06 12:46:16 +00:00
use chrono ::offset ::Utc ;
use chrono_tz ::{ Tz , TZ_VARIANTS } ;
use levenshtein ::levenshtein ;
use regex_command_attr ::command ;
2021-09-22 20:12:29 +00:00
use serenity ::{
client ::Context ,
model ::{
interactions ::InteractionResponseType , misc ::Mentionable ,
prelude ::InteractionApplicationCommandCallbackDataFlags ,
} ,
} ;
2020-08-18 19:09:21 +00:00
2020-08-22 00:24:12 +00:00
use crate ::{
2021-09-11 19:40:58 +00:00
component_models ::{ ComponentDataModel , Restrict } ,
2021-09-16 17:30:16 +00:00
consts ::THEME_COLOR ,
2021-09-22 20:12:29 +00:00
framework ::{ CommandInvoke , CommandOptions , CreateGenericResponse , OptionValue } ,
hooks ::{ CHECK_GUILD_PERMISSIONS_HOOK , CHECK_MANAGED_PERMISSIONS_HOOK } ,
models ::{ channel_data ::ChannelData , command_macro ::CommandMacro , CtxData } ,
PopularTimezones , RecordingMacros , RegexFramework , SQLPool ,
2020-08-22 00:24:12 +00:00
} ;
2020-09-28 15:11:27 +00:00
2021-09-10 23:14:23 +00:00
#[ command( " blacklist " ) ]
#[ description( " Block channels from using bot commands " ) ]
#[ arg(
name = " channel " ,
description = " The channel to blacklist " ,
kind = " Channel " ,
required = false
) ]
2020-08-18 19:09:21 +00:00
#[ supports_dm(false) ]
2021-09-22 20:12:29 +00:00
#[ hook(CHECK_GUILD_PERMISSIONS_HOOK) ]
2020-08-25 16:19:08 +00:00
#[ can_blacklist(false) ]
2021-09-22 20:12:29 +00:00
async fn blacklist ( ctx : & Context , invoke : CommandInvoke , args : CommandOptions ) {
2021-09-10 23:14:23 +00:00
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
2020-08-26 17:26:28 +00:00
2021-09-10 23:14:23 +00:00
let channel = match args . get ( " channel " ) {
2021-09-12 15:59:19 +00:00
Some ( OptionValue ::Channel ( channel_id ) ) = > * channel_id ,
2020-08-26 17:26:28 +00:00
2021-09-12 15:59:19 +00:00
_ = > invoke . channel_id ( ) ,
2021-09-10 23:14:23 +00:00
}
. to_channel_cached ( & ctx )
. unwrap ( ) ;
2020-08-22 00:24:12 +00:00
2021-09-10 23:14:23 +00:00
let mut channel_data = ChannelData ::from_channel ( & channel , & pool ) . await . unwrap ( ) ;
2020-11-19 21:31:31 +00:00
channel_data . blacklisted = ! channel_data . blacklisted ;
channel_data . commit_changes ( & pool ) . await ;
if channel_data . blacklisted {
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( )
. content ( format! ( " {} has been blacklisted " , channel . mention ( ) ) ) ,
)
2021-07-17 16:00:47 +00:00
. await ;
2020-10-12 20:01:27 +00:00
} else {
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( )
. content ( format! ( " {} has been removed from the blacklist " , channel . mention ( ) ) ) ,
)
2021-07-17 16:00:47 +00:00
. await ;
2020-08-22 00:24:12 +00:00
}
2020-08-18 19:09:21 +00:00
}
2020-08-27 11:15:20 +00:00
2021-09-10 23:14:23 +00:00
#[ command( " timezone " ) ]
#[ description( " Select your timezone " ) ]
#[ arg(
name = " timezone " ,
description = " Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee " ,
kind = " String " ,
required = false
) ]
2021-09-22 20:12:29 +00:00
async fn timezone ( ctx : & Context , invoke : CommandInvoke , args : CommandOptions ) {
2021-09-10 23:14:23 +00:00
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
let mut user_data = ctx . user_data ( invoke . author_id ( ) ) . await . unwrap ( ) ;
let footer_text = format! ( " Current timezone: {} " , user_data . timezone ) ;
2021-09-12 15:59:19 +00:00
if let Some ( OptionValue ::String ( timezone ) ) = args . get ( " timezone " ) {
2021-09-10 23:14:23 +00:00
match timezone . parse ::< Tz > ( ) {
Ok ( tz ) = > {
user_data . timezone = timezone . clone ( ) ;
2020-09-01 16:07:51 +00:00
user_data . commit_changes ( & pool ) . await ;
2020-08-27 20:37:44 +00:00
2021-09-10 23:14:23 +00:00
let now = Utc ::now ( ) . with_timezone ( & tz ) ;
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( ) . embed ( | e | {
e . title ( " Timezone Set " )
. description ( format! (
" Timezone has been set to **{}**. Your current time should be `{}` " ,
timezone ,
now . format ( " %H:%M " ) . to_string ( )
) )
. color ( * THEME_COLOR )
} ) ,
)
. await ;
2020-08-29 17:07:15 +00:00
}
2020-08-27 20:37:44 +00:00
2020-08-29 17:07:15 +00:00
Err ( _ ) = > {
2020-11-22 23:58:46 +00:00
let filtered_tz = TZ_VARIANTS
. iter ( )
2021-06-27 15:31:54 +00:00
. filter ( | tz | {
2021-09-10 23:14:23 +00:00
timezone . contains ( & tz . to_string ( ) )
| | tz . to_string ( ) . contains ( timezone )
| | levenshtein ( & tz . to_string ( ) , timezone ) < 4
2021-06-27 15:31:54 +00:00
} )
2020-11-22 23:58:46 +00:00
. take ( 25 )
2021-06-27 15:31:54 +00:00
. map ( | t | t . to_owned ( ) )
. collect ::< Vec < Tz > > ( ) ;
let fields = filtered_tz . iter ( ) . map ( | tz | {
(
tz . to_string ( ) ,
format! (
" 🕗 `{}` " ,
2021-07-16 20:28:51 +00:00
Utc ::now ( ) . with_timezone ( tz ) . format ( " %H:%M " ) . to_string ( )
2021-06-27 15:31:54 +00:00
) ,
true ,
)
} ) ;
2020-11-22 23:58:46 +00:00
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( ) . embed ( | e | {
e . title ( " Timezone Not Recognized " )
. description ( " Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee): " )
2020-11-22 23:58:46 +00:00
. color ( * THEME_COLOR )
2021-06-27 15:31:54 +00:00
. fields ( fields )
2020-11-22 23:58:46 +00:00
. footer ( | f | f . text ( footer_text ) )
. url ( " https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee " )
2021-09-10 23:14:23 +00:00
} ) ,
)
2020-10-12 20:01:27 +00:00
. await ;
2020-08-29 17:07:15 +00:00
}
2020-08-27 20:37:44 +00:00
}
2020-10-12 20:01:27 +00:00
} else {
2021-09-10 23:14:23 +00:00
let popular_timezones = ctx . data . read ( ) . await . get ::< PopularTimezones > ( ) . cloned ( ) . unwrap ( ) ;
2020-10-11 00:42:19 +00:00
2020-11-22 23:58:46 +00:00
let popular_timezones_iter = popular_timezones . iter ( ) . map ( | t | {
(
2021-01-19 12:19:20 +00:00
t . to_string ( ) ,
2021-09-10 23:14:23 +00:00
format! ( " 🕗 ` {} ` " , Utc ::now ( ) . with_timezone ( t ) . format ( " %H:%M " ) . to_string ( ) ) ,
2020-11-23 14:11:57 +00:00
true ,
2020-11-22 23:58:46 +00:00
)
} ) ;
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( ) . embed ( | e | {
e . title ( " Timezone Usage " )
. description (
" **Usage:**
` / timezone Name `
* * Example :* *
` / timezone Europe / London `
You may want to use one of the popular timezones below , otherwise click [ here ] ( https ://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):",
)
2020-11-22 23:58:46 +00:00
. color ( * THEME_COLOR )
. fields ( popular_timezones_iter )
. footer ( | f | f . text ( footer_text ) )
. url ( " https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee " )
2021-09-10 23:14:23 +00:00
} ) ,
2020-11-23 14:11:57 +00:00
)
. await ;
2020-08-29 17:07:15 +00:00
}
2020-08-27 11:15:20 +00:00
}
2020-08-29 19:57:11 +00:00
2021-09-10 23:14:23 +00:00
#[ command( " prefix " ) ]
#[ description( " Configure a prefix for text-based commands (deprecated) " ) ]
2020-09-01 17:48:40 +00:00
#[ supports_dm(false) ]
2021-09-22 20:12:29 +00:00
#[ hook(CHECK_GUILD_PERMISSIONS_HOOK) ]
async fn prefix ( ctx : & Context , invoke : CommandInvoke , args : String ) {
2021-09-10 23:14:23 +00:00
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
2020-10-12 20:01:27 +00:00
2021-09-10 23:14:23 +00:00
let guild_data = ctx . guild_data ( invoke . guild_id ( ) . unwrap ( ) ) . await . unwrap ( ) ;
2020-08-29 19:57:11 +00:00
if args . len ( ) > 5 {
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( ) . content ( " Please select a prefix under 5 characters " ) ,
)
2020-10-12 20:01:27 +00:00
. await ;
} else if args . is_empty ( ) {
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( )
. content ( " Please use this command as `@reminder-bot prefix <prefix>` " ) ,
)
2020-10-12 20:01:27 +00:00
. await ;
} else {
2021-05-23 09:59:06 +00:00
guild_data . write ( ) . await . prefix = args ;
guild_data . read ( ) . await . commit_changes ( & pool ) . await ;
2020-08-29 19:57:11 +00:00
2021-09-10 23:14:23 +00:00
let _ = invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( )
. content ( format! ( " Prefix changed to {} " , guild_data . read ( ) . await . prefix ) ) ,
)
. await ;
2020-08-29 19:57:11 +00:00
}
}
2020-09-01 18:00:56 +00:00
2021-09-10 23:14:23 +00:00
#[ command( " restrict " ) ]
#[ description( " Configure which roles can use commands on the bot " ) ]
#[ arg(
name = " role " ,
description = " The role to configure command permissions for " ,
kind = " Role " ,
required = true
) ]
2020-09-01 18:00:56 +00:00
#[ supports_dm(false) ]
2021-09-22 20:12:29 +00:00
#[ hook(CHECK_GUILD_PERMISSIONS_HOOK) ]
async fn restrict ( ctx : & Context , invoke : CommandInvoke , args : CommandOptions ) {
2021-09-10 23:14:23 +00:00
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
let framework = ctx . data . read ( ) . await . get ::< RegexFramework > ( ) . cloned ( ) . unwrap ( ) ;
2021-09-12 15:59:19 +00:00
if let Some ( OptionValue ::Role ( role ) ) = args . get ( " role " ) {
let restricted_commands =
2021-09-16 14:42:50 +00:00
sqlx ::query! ( " SELECT command FROM command_restrictions WHERE role_id = (SELECT id FROM roles WHERE role = ?) " , role . 0 )
2021-09-12 15:59:19 +00:00
. fetch_all ( & pool )
. await
. unwrap ( )
. iter ( )
. map ( | row | row . command . clone ( ) )
. collect ::< Vec < String > > ( ) ;
let restrictable_commands = framework
. commands
2021-09-10 23:14:23 +00:00
. iter ( )
2021-09-22 20:12:29 +00:00
. filter ( | c | c . hooks . contains ( & & CHECK_MANAGED_PERMISSIONS_HOOK ) )
2021-09-12 15:59:19 +00:00
. map ( | c | c . names [ 0 ] . to_string ( ) )
2020-10-12 20:01:27 +00:00
. collect ::< Vec < String > > ( ) ;
2020-09-02 16:13:17 +00:00
2021-09-12 15:59:19 +00:00
let len = restrictable_commands . len ( ) ;
2021-09-16 14:42:50 +00:00
let restrict_pl = ComponentDataModel ::Restrict ( Restrict {
role_id : * role ,
author_id : invoke . author_id ( ) ,
guild_id : invoke . guild_id ( ) . unwrap ( ) ,
} ) ;
2021-09-12 15:59:19 +00:00
invoke
. respond (
ctx . http . clone ( ) ,
CreateGenericResponse ::new ( )
. content ( format! (
" Select the commands to allow to {} from below: " ,
role . mention ( )
) )
. components ( | c | {
c . create_action_row ( | row | {
row . create_select_menu ( | select | {
select
. custom_id ( restrict_pl . to_custom_id ( ) )
. options ( | options | {
for command in restrictable_commands {
options . create_option ( | opt | {
opt . label ( & command )
. value ( & command )
. default_selection (
restricted_commands . contains ( & command ) ,
)
} ) ;
}
options
} )
. min_values ( 0 )
. max_values ( len as u64 )
} )
2021-09-10 23:14:23 +00:00
} )
2021-09-12 15:59:19 +00:00
} ) ,
)
. await
. unwrap ( ) ;
}
2020-09-01 18:00:56 +00:00
}
2020-09-03 23:29:19 +00:00
2021-09-22 20:12:29 +00:00
#[ command( " macro " ) ]
#[ description( " Record and replay command sequences " ) ]
#[ subcommand( " record " ) ]
#[ description( " Start recording up to 5 commands to replay " ) ]
#[ arg(name = " name " , description = " Name for the new macro " , kind = " String " , required = true) ]
#[ arg(
name = " description " ,
description = " Description for the new macro " ,
kind = " String " ,
required = false
) ]
#[ subcommand( " finish " ) ]
#[ description( " Finish current recording " ) ]
#[ subcommand( " list " ) ]
#[ description( " List recorded macros " ) ]
#[ subcommand( " run " ) ]
#[ description( " Run a recorded macro " ) ]
#[ arg(name = " name " , description = " Name of the macro to run " , kind = " String " , required = true) ]
#[ supports_dm(false) ]
#[ hook(CHECK_MANAGED_PERMISSIONS_HOOK) ]
async fn macro_cmd ( ctx : & Context , invoke : CommandInvoke , args : CommandOptions ) {
let interaction = invoke . interaction ( ) . unwrap ( ) ;
match args . subcommand . clone ( ) . unwrap ( ) . as_str ( ) {
" record " = > {
let macro_buffer = ctx . data . read ( ) . await . get ::< RecordingMacros > ( ) . cloned ( ) . unwrap ( ) ;
{
let mut lock = macro_buffer . write ( ) . await ;
let guild_id = interaction . guild_id . unwrap ( ) ;
lock . insert (
( guild_id , interaction . user . id ) ,
CommandMacro {
guild_id ,
name : args . get ( " name " ) . unwrap ( ) . to_string ( ) ,
description : args . get ( " description " ) . map ( | d | d . to_string ( ) ) ,
commands : vec ! [ ] ,
} ,
) ;
}
let _ = interaction
. create_interaction_response ( & ctx , | r | {
r . kind ( InteractionResponseType ::ChannelMessageWithSource )
. interaction_response_data ( | d | {
d . flags ( InteractionApplicationCommandCallbackDataFlags ::EPHEMERAL )
. create_embed ( | e | {
e
. 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 )
} )
} )
} )
. await ;
}
" finish " = > {
let key = ( interaction . guild_id . unwrap ( ) , interaction . user . id ) ;
let macro_buffer = ctx . data . read ( ) . await . get ::< RecordingMacros > ( ) . cloned ( ) . unwrap ( ) ;
{
let lock = macro_buffer . read ( ) . await ;
let contained = lock . get ( & key ) ;
if contained . map_or ( true , | cmacro | cmacro . commands . is_empty ( ) ) {
let _ = interaction
. create_interaction_response ( & ctx , | r | {
r . kind ( InteractionResponseType ::ChannelMessageWithSource )
. interaction_response_data ( | d | {
d . create_embed ( | e | {
e . title ( " No Macro Recorded " )
. description (
" Use `/macro record` to start recording a macro " ,
)
. color ( * THEME_COLOR )
} )
} )
} )
. await ;
} else {
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
let command_macro = contained . unwrap ( ) ;
let json = serde_json ::to_string ( & command_macro . commands ) . unwrap ( ) ;
sqlx ::query! (
" INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?) " ,
command_macro . guild_id . 0 ,
command_macro . name ,
command_macro . description ,
json
)
. execute ( & pool )
. await
. unwrap ( ) ;
let _ = interaction
. create_interaction_response ( & ctx , | r | {
r . kind ( InteractionResponseType ::ChannelMessageWithSource )
. interaction_response_data ( | d | {
d . create_embed ( | e | {
e . title ( " Macro Recorded " )
. description ( " Use `/macro run` to execute the macro " )
. color ( * THEME_COLOR )
} )
} )
} )
. await ;
}
}
{
let mut lock = macro_buffer . write ( ) . await ;
lock . remove ( & key ) ;
}
}
" list " = > { }
" run " = > { }
_ = > { }
}
}
2021-09-27 16:34:13 +00:00
#[ command( " webhook " ) ]
#[ description( " Modify this channel's webhooks " ) ]
#[ subcommand( " username " ) ]
#[ description( " Change the webhook username " ) ]
#[ arg(name = " username " , description = " The username to use " , kind = " String " , required = true) ]
#[ subcommand( " avatar " ) ]
#[ description( " Change the webhook avatar " ) ]
#[ arg(name = " url " , description = " The URL of the image to use " , kind = " String " , required = true) ]
async fn configure_webhook ( ctx : & Context , invoke : CommandInvoke , args : CommandOptions ) { }