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-11-02 20:10:10 +00:00
use serenity ::client ::Context ;
2020-08-18 19:09:21 +00:00
2020-08-22 00:24:12 +00:00
use crate ::{
2021-11-02 20:10:10 +00:00
component_models ::pager ::{ MacroPager , Pager } ,
2021-10-16 18:18:16 +00:00
consts ::{ EMBED_DESCRIPTION_MAX_LENGTH , THEME_COLOR } ,
2021-09-22 20:12:29 +00:00
framework ::{ CommandInvoke , CommandOptions , CreateGenericResponse , OptionValue } ,
2021-11-02 20:10:10 +00:00
hooks ::CHECK_GUILD_PERMISSIONS_HOOK ,
2021-10-26 20:10:14 +00:00
models ::{ command_macro ::CommandMacro , CtxData } ,
2021-09-22 20:12:29 +00:00
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( " 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-10-13 15:37:15 +00:00
async fn timezone ( ctx : & Context , invoke : & mut 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-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) ]
2021-10-13 15:37:15 +00:00
#[ subcommand( " delete " ) ]
#[ description( " Delete a recorded macro " ) ]
#[ arg(name = " name " , description = " Name of the macro to delete " , kind = " String " , required = true) ]
2021-09-22 20:12:29 +00:00
#[ supports_dm(false) ]
2021-11-02 20:10:10 +00:00
#[ hook(CHECK_GUILD_PERMISSIONS_HOOK) ]
2021-10-13 15:37:15 +00:00
async fn macro_cmd ( ctx : & Context , invoke : & mut CommandInvoke , args : CommandOptions ) {
let pool = ctx . data . read ( ) . await . get ::< SQLPool > ( ) . cloned ( ) . unwrap ( ) ;
2021-09-22 20:12:29 +00:00
match args . subcommand . clone ( ) . unwrap ( ) . as_str ( ) {
" record " = > {
2021-10-30 19:57:33 +00:00
let guild_id = invoke . guild_id ( ) . unwrap ( ) ;
2021-09-22 20:12:29 +00:00
2021-10-30 19:57:33 +00:00
let name = args . get ( " name " ) . unwrap ( ) . to_string ( ) ;
2021-09-22 20:12:29 +00:00
2021-10-30 19:57:33 +00:00
let row = sqlx ::query! (
" SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ? " ,
guild_id . 0 ,
name
)
. fetch_one ( & pool )
. await ;
if row . is_ok ( ) {
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( ) . ephemeral ( ) . embed ( | e | {
e
. title ( " Unique Name Required " )
. description ( " A macro already exists under this name. Please select a unique name for your macro. " )
. color ( * THEME_COLOR )
} ) ,
)
. await ;
} else {
let macro_buffer = ctx . data . read ( ) . await . get ::< RecordingMacros > ( ) . cloned ( ) . unwrap ( ) ;
let okay = {
let mut lock = macro_buffer . write ( ) . await ;
if lock . contains_key ( & ( guild_id , invoke . author_id ( ) ) ) {
false
} else {
lock . insert (
( guild_id , invoke . author_id ( ) ) ,
CommandMacro {
guild_id ,
name ,
description : args . get ( " description " ) . map ( | d | d . to_string ( ) ) ,
commands : vec ! [ ] ,
} ,
) ;
true
}
} ;
2021-09-22 20:12:29 +00:00
2021-10-30 19:57:33 +00:00
if okay {
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( ) . ephemeral ( ) . embed ( | e | {
e
. title ( " Macro Recording Started " )
. description (
2021-09-22 20:12:29 +00:00
" Run up to 5 commands, or type `/macro finish` to stop at any point.
Any commands ran as part of recording will be inconsequential " )
2021-10-30 19:57:33 +00:00
. color ( * THEME_COLOR )
} ) ,
)
. await ;
} else {
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( ) . ephemeral ( ) . embed ( | e | {
e . title ( " Macro Already Recording " )
. description (
" You are already recording a macro in this server.
Please use ` / macro finish ` to end this recording before starting another . " ,
)
. color ( * THEME_COLOR )
} ) ,
)
. await ;
}
}
2021-09-22 20:12:29 +00:00
}
" finish " = > {
2021-10-13 15:37:15 +00:00
let key = ( invoke . guild_id ( ) . unwrap ( ) , invoke . author_id ( ) ) ;
2021-09-22 20:12:29 +00:00
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 ( ) ) {
2021-10-13 15:37:15 +00:00
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( ) . embed ( | e | {
e . title ( " No Macro Recorded " )
. description ( " Use `/macro record` to start recording a macro " )
. color ( * THEME_COLOR )
} ) ,
)
2021-09-22 20:12:29 +00:00
. await ;
} else {
let command_macro = contained . unwrap ( ) ;
let json = serde_json ::to_string ( & command_macro . commands ) . unwrap ( ) ;
sqlx ::query! (
2021-10-30 19:57:33 +00:00
" INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?) " ,
2021-09-22 20:12:29 +00:00
command_macro . guild_id . 0 ,
command_macro . name ,
command_macro . description ,
json
)
. execute ( & pool )
. await
. unwrap ( ) ;
2021-10-13 15:37:15 +00:00
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( ) . embed ( | e | {
e . title ( " Macro Recorded " )
. description ( " Use `/macro run` to execute the macro " )
. color ( * THEME_COLOR )
} ) ,
)
2021-09-22 20:12:29 +00:00
. await ;
}
}
{
let mut lock = macro_buffer . write ( ) . await ;
lock . remove ( & key ) ;
}
}
2021-10-16 18:18:16 +00:00
" list " = > {
let macros = CommandMacro ::from_guild ( ctx , invoke . guild_id ( ) . unwrap ( ) ) . await ;
2021-10-26 19:54:22 +00:00
let resp = show_macro_page ( & macros , 0 ) ;
2021-10-16 18:18:16 +00:00
2021-10-26 19:54:22 +00:00
invoke . respond ( & ctx , resp ) . await . unwrap ( ) ;
2021-10-16 18:18:16 +00:00
}
2021-10-13 15:37:15 +00:00
" run " = > {
let macro_name = args . get ( " name " ) . unwrap ( ) . to_string ( ) ;
2021-09-22 20:12:29 +00:00
2021-10-13 15:37:15 +00:00
match sqlx ::query! (
2021-10-30 19:57:33 +00:00
" SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ? " ,
2021-10-13 15:37:15 +00:00
invoke . guild_id ( ) . unwrap ( ) . 0 ,
macro_name
)
. fetch_one ( & pool )
. await
{
Ok ( row ) = > {
invoke . defer ( & ctx ) . await ;
2021-10-12 20:52:43 +00:00
2021-10-13 15:37:15 +00:00
let commands : Vec < CommandOptions > =
serde_json ::from_str ( & row . commands ) . unwrap ( ) ;
let framework = ctx . data . read ( ) . await . get ::< RegexFramework > ( ) . cloned ( ) . unwrap ( ) ;
2021-10-12 20:52:43 +00:00
2021-10-13 15:37:15 +00:00
for command in commands {
framework . run_command_from_options ( ctx , invoke , command ) . await ;
}
}
2021-10-12 20:52:43 +00:00
2021-10-13 15:37:15 +00:00
Err ( sqlx ::Error ::RowNotFound ) = > {
let _ = invoke
. respond (
& ctx ,
CreateGenericResponse ::new ( )
. content ( format! ( " Macro \" {} \" not found " , macro_name ) ) ,
)
. await ;
}
2021-10-12 20:52:43 +00:00
2021-10-13 15:37:15 +00:00
Err ( e ) = > {
panic! ( " {} " , e ) ;
}
2021-10-12 20:52:43 +00:00
}
}
2021-10-16 18:18:16 +00:00
" delete " = > {
let macro_name = args . get ( " name " ) . unwrap ( ) . to_string ( ) ;
match sqlx ::query! (
2021-10-30 19:57:33 +00:00
" SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ? " ,
2021-10-16 18:18:16 +00:00
invoke . guild_id ( ) . unwrap ( ) . 0 ,
macro_name
)
. fetch_one ( & pool )
. await
{
Ok ( row ) = > {
2021-10-26 19:54:22 +00:00
sqlx ::query! ( " DELETE FROM macro WHERE id = ? " , row . id )
. execute ( & pool )
. await
. unwrap ( ) ;
2021-10-16 18:18:16 +00:00
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 ) ;
}
}
}
2021-10-13 15:37:15 +00:00
_ = > { }
2021-10-12 20:52:43 +00:00
}
}
2021-10-16 18:18:16 +00:00
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
} )
}
2021-10-26 19:54:22 +00:00
pub fn show_macro_page ( macros : & [ CommandMacro ] , page : usize ) -> CreateGenericResponse {
let pager = MacroPager ::new ( page ) ;
2021-10-16 18:18:16 +00:00
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 < String > = 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 ::< Vec < String > > ( ) ;
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
} )
}