reminder-bot/src/commands/moderation_cmds.rs

474 lines
14 KiB
Rust
Raw Normal View History

use chrono::offset::Utc;
use chrono_tz::{Tz, TZ_VARIANTS};
use levenshtein::levenshtein;
2022-02-19 14:32:03 +00:00
use poise::CreateReply;
2020-08-18 19:09:21 +00:00
2020-08-22 00:24:12 +00:00
use crate::{
component_models::pager::{MacroPager, Pager},
2021-10-16 18:18:16 +00:00
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
2022-02-19 14:32:03 +00:00
hooks::guild_only,
models::{
command_macro::{guild_command_macro, CommandMacro},
CtxData,
},
2022-02-19 14:32:03 +00:00
Context, Data, Error,
2020-08-22 00:24:12 +00:00
};
2022-02-19 14:32:03 +00:00
async fn timezone_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
if partial.is_empty() {
ctx.data().popular_timezones.iter().map(|t| t.to_string()).collect::<Vec<String>>()
} else {
TZ_VARIANTS
.iter()
.filter(|tz| tz.to_string().contains(&partial))
.take(25)
.map(|t| t.to_string())
.collect::<Vec<String>>()
}
}
/// Select your timezone
#[poise::command(slash_command)]
pub async fn timezone(
ctx: Context<'_>,
#[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
#[autocomplete = "timezone_autocomplete"]
timezone: Option<String>,
) -> Result<(), Error> {
let mut user_data = ctx.author_data().await.unwrap();
let footer_text = format!("Current timezone: {}", user_data.timezone);
2022-02-19 14:32:03 +00:00
if let Some(timezone) = timezone {
match timezone.parse::<Tz>() {
Ok(tz) => {
user_data.timezone = timezone.clone();
2022-02-19 14:32:03 +00:00
user_data.commit_changes(&ctx.data().database).await;
2020-08-27 20:37:44 +00:00
let now = Utc::now().with_timezone(&tz);
2022-02-19 14:32:03 +00:00
ctx.send(|m| {
m.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()
.filter(|tz| {
timezone.contains(&tz.to_string())
2022-02-19 14:32:03 +00:00
|| tz.to_string().contains(&timezone)
|| levenshtein(&tz.to_string(), &timezone) < 4
})
2020-11-22 23:58:46 +00:00
.take(25)
.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()
),
true,
)
});
2020-11-22 23:58:46 +00:00
2022-02-19 14:32:03 +00:00
ctx.send(|m| {
m.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):")
.color(*THEME_COLOR)
.fields(fields)
.footer(|f| f.text(footer_text))
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
})
})
.await?;
2020-08-29 17:07:15 +00:00
}
2020-08-27 20:37:44 +00:00
}
} else {
2022-02-19 14:32:03 +00:00
let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
2020-11-22 23:58:46 +00:00
(
t.to_string(),
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
)
});
2022-02-19 14:32:03 +00:00
ctx.send(|m| {
m.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):",
2022-02-19 14:32:03 +00:00
)
.color(*THEME_COLOR)
.fields(popular_timezones_iter)
.footer(|f| f.text(footer_text))
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
})
})
.await?;
2020-08-29 17:07:15 +00:00
}
2022-02-19 14:32:03 +00:00
Ok(())
2020-08-27 11:15:20 +00:00
}
2020-08-29 19:57:11 +00:00
2022-02-19 14:32:03 +00:00
async fn macro_name_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
sqlx::query!(
"
SELECT name
FROM macro
WHERE
guild_id = (SELECT id FROM guilds WHERE guild = ?)
AND name LIKE CONCAT(?, '%')",
ctx.guild_id().unwrap().0,
partial,
)
.fetch_all(&ctx.data().database)
.await
.unwrap_or(vec![])
.iter()
.map(|s| s.name.clone())
.collect()
}
/// Record and replay command sequences
#[poise::command(slash_command, rename = "macro", check = "guild_only")]
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Start recording up to 5 commands to replay
#[poise::command(slash_command, rename = "record", check = "guild_only")]
pub async fn record_macro(
ctx: Context<'_>,
#[description = "Name for the new macro"] name: String,
#[description = "Description for the new macro"] description: Option<String>,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
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(&ctx.data().database)
.await;
if row.is_ok() {
ctx.send(|m| {
m.ephemeral(true).embed(|e| {
e.title("Unique Name Required")
.description(
"A macro already exists under this name.
Please select a unique name for your macro.",
)
2022-02-19 14:32:03 +00:00
.color(*THEME_COLOR)
})
})
.await?;
} else {
let okay = {
let mut lock = ctx.data().recording_macros.write().await;
if lock.contains_key(&(guild_id, ctx.author().id)) {
false
} else {
2022-02-19 14:32:03 +00:00
lock.insert(
(guild_id, ctx.author().id),
CommandMacro { guild_id, name, description, commands: vec![] },
);
true
}
};
if okay {
ctx.send(|m| {
m.ephemeral(true).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",
)
2022-02-19 14:32:03 +00:00
.color(*THEME_COLOR)
})
})
.await?;
} else {
ctx.send(|m| {
m.ephemeral(true).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.",
)
2022-02-19 14:32:03 +00:00
.color(*THEME_COLOR)
})
})
.await?;
}
2022-02-19 14:32:03 +00:00
}
2022-02-19 14:32:03 +00:00
Ok(())
}
/// Finish current macro recording
#[poise::command(
slash_command,
rename = "finish",
check = "guild_only",
identifying_name = "macro_finish"
)]
pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
let key = (ctx.guild_id().unwrap(), ctx.author().id);
{
let lock = ctx.data().recording_macros.read().await;
let contained = lock.get(&key);
if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
ctx.send(|m| {
m.embed(|e| {
e.title("No Macro Recorded")
.description("Use `/macro record` to start recording a macro")
.color(*THEME_COLOR)
})
})
.await?;
} else {
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(&ctx.data().database)
.await
.unwrap();
ctx.send(|m| {
m.embed(|e| {
e.title("Macro Recorded")
.description("Use `/macro run` to execute the macro")
.color(*THEME_COLOR)
})
})
.await?;
}
2022-02-19 14:32:03 +00:00
}
{
let mut lock = ctx.data().recording_macros.write().await;
lock.remove(&key);
}
2021-10-16 18:18:16 +00:00
2022-02-19 14:32:03 +00:00
Ok(())
}
/// List recorded macros
#[poise::command(slash_command, rename = "list", check = "guild_only")]
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
let macros = ctx.command_macros().await?;
2022-02-19 14:32:03 +00:00
let resp = show_macro_page(&macros, 0);
ctx.send(|m| {
*m = resp;
m
})
.await?;
Ok(())
}
2021-10-16 18:18:16 +00:00
2022-02-19 14:32:03 +00:00
/// Run a recorded macro
#[poise::command(slash_command, rename = "run", check = "guild_only")]
pub async fn run_macro(
ctx: poise::ApplicationContext<'_, Data, Error>,
2022-02-19 14:32:03 +00:00
#[description = "Name of macro to run"]
#[autocomplete = "macro_name_autocomplete"]
name: String,
) -> Result<(), Error> {
match guild_command_macro(&Context::Application(ctx), &name).await {
Some(command_macro) => {
ctx.defer_response(false).await?;
for command in command_macro.commands {
if let Some(action) = command.action {
(action)(poise::ApplicationContext { args: &command.options, ..ctx })
.await
.ok()
.unwrap();
} else {
Context::Application(ctx)
.say(format!("Command \"{}\" failed to execute", command.command_name))
.await?;
}
}
2022-02-19 14:32:03 +00:00
}
2021-10-12 20:52:43 +00:00
None => {
Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
2021-10-12 20:52:43 +00:00
}
2022-02-19 14:32:03 +00:00
}
2021-10-16 18:18:16 +00:00
2022-02-19 14:32:03 +00:00
Ok(())
}
2021-10-16 18:18:16 +00:00
2022-02-19 14:32:03 +00:00
/// Delete a recorded macro
#[poise::command(slash_command, rename = "delete", check = "guild_only")]
pub async fn delete_macro(
ctx: Context<'_>,
#[description = "Name of macro to delete"]
#[autocomplete = "macro_name_autocomplete"]
name: String,
) -> Result<(), Error> {
match sqlx::query!(
"
SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
ctx.guild_id().unwrap().0,
name
)
.fetch_one(&ctx.data().database)
.await
{
Ok(row) => {
sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
.execute(&ctx.data().database)
.await
.unwrap();
ctx.say(format!("Macro \"{}\" deleted", name)).await?;
}
Err(sqlx::Error::RowNotFound) => {
ctx.say(format!("Macro \"{}\" not found", name)).await?;
}
Err(e) => {
panic!("{}", e);
2021-10-16 18:18:16 +00:00
}
2021-10-12 20:52:43 +00:00
}
2022-02-19 14:32:03 +00:00
Ok(())
2021-10-12 20:52:43 +00:00
}
2021-10-16 18:18:16 +00:00
2022-02-19 14:32:03 +00:00
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
2021-10-16 18:18:16 +00:00
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
})
}
2022-02-19 14:32:03 +00:00
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
2021-10-26 19:54:22 +00:00
let pager = MacroPager::new(page);
2021-10-16 18:18:16 +00:00
if macros.is_empty() {
2022-02-19 14:32:03 +00:00
let mut reply = CreateReply::default();
reply.embed(|e| {
2021-10-16 18:18:16 +00:00
e.title("Macros")
.description("No Macros Set Up. Use `/macro record` to get started.")
.color(*THEME_COLOR)
});
2022-02-19 14:32:03 +00:00
return reply;
2021-10-16 18:18:16 +00:00
}
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");
2022-02-19 14:32:03 +00:00
let mut reply = CreateReply::default();
reply
2021-10-16 18:18:16 +00:00
.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
2022-02-19 14:32:03 +00:00
});
reply
2021-10-16 18:18:16 +00:00
}