Compare commits
12 Commits
jellywx/fi
...
jellywx/ma
Author | SHA1 | Date | |
---|---|---|---|
e2bf23f194 | |||
8f8235a86e | |||
c8f646a8fa | |||
ecaa382a1e | |||
8991198fd3 | |||
f20b95a482 | |||
8dd7dc6409 | |||
c799d10727 | |||
ceb6fb7b12 | |||
6708abdb0f | |||
a38f6024c1 | |||
bb3386c4e8 |
589
Cargo.lock
generated
589
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@ -1,28 +1,29 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder_rs"
|
name = "reminder_rs"
|
||||||
version = "1.6.3"
|
version = "1.6.5"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
poise = "0.2"
|
poise = "0.3"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
regex = "1.4"
|
lazy-regex = "2.3.0"
|
||||||
|
regex = "1.6"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.8"
|
env_logger = "0.9"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = { version = "0.5", features = ["serde"] }
|
chrono-tz = { version = "0.6", features = ["serde"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
rmp-serde = "0.15"
|
rmp-serde = "1.1"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
levenshtein = "1.0"
|
levenshtein = "1.0"
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
|
|
||||||
[dependencies.postman]
|
[dependencies.postman]
|
||||||
|
@ -12,5 +12,5 @@ chrono-tz = { version = "0.5", features = ["serde"] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
|
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
|
||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
||||||
|
35
src/commands/autocomplete.rs
Normal file
35
src/commands/autocomplete.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use chrono_tz::TZ_VARIANTS;
|
||||||
|
|
||||||
|
use crate::Context;
|
||||||
|
|
||||||
|
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> 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>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> 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_default()
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
46
src/commands/command_macro/delete.rs
Normal file
46
src/commands/command_macro/delete.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use super::super::autocomplete::macro_name_autocomplete;
|
||||||
|
use crate::{Context, Error};
|
||||||
|
|
||||||
|
/// Delete a recorded macro
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "delete",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "delete_macro"
|
||||||
|
)]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
38
src/commands/command_macro/install.rs
Normal file
38
src/commands/command_macro/install.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use poise::serenity_prelude::CommandType;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands::autocomplete::macro_name_autocomplete, models::command_macro::guild_command_macro,
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Add a macro as a slash-command to this server. Enables controlling permissions per-macro.
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "install",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "install_macro"
|
||||||
|
)]
|
||||||
|
pub async fn install_macro(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Name of macro to install"]
|
||||||
|
#[autocomplete = "macro_name_autocomplete"]
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let guild_id = ctx.guild_id().unwrap();
|
||||||
|
|
||||||
|
if let Some(command_macro) = guild_command_macro(&ctx, &name).await {
|
||||||
|
guild_id
|
||||||
|
.create_application_command(&ctx.discord(), |a| {
|
||||||
|
a.kind(CommandType::ChatInput)
|
||||||
|
.name(command_macro.name)
|
||||||
|
.description(command_macro.description.unwrap_or_else(|| "".to_string()))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
ctx.send(|r| r.ephemeral(true).content("Macro installed. Go to Server Settings 🠚 Integrations 🠚 Reminder Bot to configure permissions.")).await?;
|
||||||
|
} else {
|
||||||
|
ctx.send(|r| r.ephemeral(true).content("No macro found with that name")).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
127
src/commands/command_macro/list.rs
Normal file
127
src/commands/command_macro/list.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
component_models::pager::{MacroPager, Pager},
|
||||||
|
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
||||||
|
models::{command_macro::CommandMacro, CtxData},
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// List recorded macros
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "list",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "list_macro"
|
||||||
|
)]
|
||||||
|
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let macros = ctx.command_macros().await?;
|
||||||
|
|
||||||
|
let resp = show_macro_page(¯os, 0);
|
||||||
|
|
||||||
|
ctx.send(|m| {
|
||||||
|
*m = resp;
|
||||||
|
m
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
|
||||||
|
let pager = MacroPager::new(page);
|
||||||
|
|
||||||
|
if macros.is_empty() {
|
||||||
|
let mut reply = CreateReply::default();
|
||||||
|
|
||||||
|
reply.embed(|e| {
|
||||||
|
e.title("Macros")
|
||||||
|
.description("No Macros Set Up. Use `/macro record` to get started.")
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let mut reply = CreateReply::default();
|
||||||
|
|
||||||
|
reply
|
||||||
|
.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
|
||||||
|
});
|
||||||
|
|
||||||
|
reply
|
||||||
|
}
|
229
src/commands/command_macro/migrate.rs
Normal file
229
src/commands/command_macro/migrate.rs
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
use lazy_regex::regex;
|
||||||
|
use poise::serenity_prelude::command::CommandOptionType;
|
||||||
|
use regex::Captures;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::{models::command_macro::RawCommandMacro, Context, Error, GuildId};
|
||||||
|
|
||||||
|
struct Alias {
|
||||||
|
name: String,
|
||||||
|
command: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Migrate old $alias reminder commands to macros. Only macro names that are not taken will be used.
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "migrate",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "migrate_macro"
|
||||||
|
)]
|
||||||
|
pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let guild_id = ctx.guild_id().unwrap();
|
||||||
|
let mut transaction = ctx.data().database.begin().await?;
|
||||||
|
|
||||||
|
let aliases = sqlx::query_as!(
|
||||||
|
Alias,
|
||||||
|
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||||
|
guild_id.0
|
||||||
|
)
|
||||||
|
.fetch_all(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut added_aliases = 0;
|
||||||
|
|
||||||
|
for alias in aliases {
|
||||||
|
match parse_text_command(guild_id, alias.name, &alias.command) {
|
||||||
|
Some(cmd_macro) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
|
||||||
|
cmd_macro.guild_id.0,
|
||||||
|
cmd_macro.name,
|
||||||
|
cmd_macro.description,
|
||||||
|
cmd_macro.commands
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
added_aliases += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
ctx.send(|b| b.content(format!("Added {} macros.", added_aliases))).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_text_command(
|
||||||
|
guild_id: GuildId,
|
||||||
|
alias_name: String,
|
||||||
|
command: &str,
|
||||||
|
) -> Option<RawCommandMacro> {
|
||||||
|
match command.split_once(" ") {
|
||||||
|
Some((command_word, args)) => {
|
||||||
|
let command_word = command_word.to_lowercase();
|
||||||
|
|
||||||
|
if command_word == "r"
|
||||||
|
|| command_word == "i"
|
||||||
|
|| command_word == "remind"
|
||||||
|
|| command_word == "interval"
|
||||||
|
{
|
||||||
|
let matcher = regex!(
|
||||||
|
r#"(?P<mentions>(?:<@\d+>\s+|<@!\d+>\s+|<#\d+>\s+)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#s
|
||||||
|
);
|
||||||
|
|
||||||
|
match matcher.captures(&args) {
|
||||||
|
Some(captures) => {
|
||||||
|
let mut args: Vec<Value> = vec![];
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("time") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "time",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("content") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "content",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("interval") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "interval",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("expires") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "expires",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("mentions") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "channels",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(RawCommandMacro {
|
||||||
|
guild_id,
|
||||||
|
name: alias_name,
|
||||||
|
description: None,
|
||||||
|
commands: json!([
|
||||||
|
{
|
||||||
|
"command_name": "remind",
|
||||||
|
"options": args,
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else if command_word == "n" || command_word == "natural" {
|
||||||
|
let matcher_primary = regex!(
|
||||||
|
r#"(?P<time>.*?)(?:\s+)(?:send|say)(?:\s+)(?P<content>.*?)(?:(?:\s+)to(?:\s+)(?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+))?$"#s
|
||||||
|
);
|
||||||
|
let matcher_secondary = regex!(
|
||||||
|
r#"(?P<msg>.*)(?:\s+)every(?:\s+)(?P<interval>.*?)(?:(?:\s+)(?:until|for)(?:\s+)(?P<expires>.*?))?$"#s
|
||||||
|
);
|
||||||
|
|
||||||
|
match matcher_primary.captures(&args) {
|
||||||
|
Some(captures) => {
|
||||||
|
let captures_secondary = matcher_secondary.captures(&args);
|
||||||
|
|
||||||
|
let mut args: Vec<Value> = vec![];
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("time") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "time",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("content") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "content",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) =
|
||||||
|
captures_secondary.as_ref().and_then(|c: &Captures| c.name("interval"))
|
||||||
|
{
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "interval",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) =
|
||||||
|
captures_secondary.and_then(|c: Captures| c.name("expires"))
|
||||||
|
{
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "expires",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(group) = captures.name("mentions") {
|
||||||
|
let content = group.as_str();
|
||||||
|
args.push(json!({
|
||||||
|
"name": "channels",
|
||||||
|
"value": content,
|
||||||
|
"type": CommandOptionType::String,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(RawCommandMacro {
|
||||||
|
guild_id,
|
||||||
|
name: alias_name,
|
||||||
|
description: None,
|
||||||
|
commands: json!([
|
||||||
|
{
|
||||||
|
"command_name": "remind",
|
||||||
|
"options": args,
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
20
src/commands/command_macro/mod.rs
Normal file
20
src/commands/command_macro/mod.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use crate::{Context, Error};
|
||||||
|
|
||||||
|
pub mod delete;
|
||||||
|
pub mod install;
|
||||||
|
pub mod list;
|
||||||
|
pub mod migrate;
|
||||||
|
pub mod record;
|
||||||
|
pub mod run;
|
||||||
|
|
||||||
|
/// Record and replay command sequences
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "macro",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "macro_base"
|
||||||
|
)]
|
||||||
|
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
139
src/commands/command_macro/record.rs
Normal file
139
src/commands/command_macro/record.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
|
use crate::{consts::THEME_COLOR, models::command_macro::CommandMacro, Context, Error};
|
||||||
|
|
||||||
|
/// Start recording up to 5 commands to replay
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "record",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "record_macro"
|
||||||
|
)]
|
||||||
|
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.",
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let okay = {
|
||||||
|
let mut lock = ctx.data().recording_macros.write().await;
|
||||||
|
|
||||||
|
if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
|
||||||
|
e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
.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.",
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish current macro recording
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "finish",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "finish_macro"
|
||||||
|
)]
|
||||||
|
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?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut lock = ctx.data().recording_macros.write().await;
|
||||||
|
lock.remove(&key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
46
src/commands/command_macro/run.rs
Normal file
46
src/commands/command_macro/run.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use super::super::autocomplete::macro_name_autocomplete;
|
||||||
|
use crate::{models::command_macro::guild_command_macro, Context, Data, Error};
|
||||||
|
|
||||||
|
/// Run a recorded macro
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "run",
|
||||||
|
guild_only = true,
|
||||||
|
default_member_permissions = "MANAGE_GUILD",
|
||||||
|
identifying_name = "run_macro"
|
||||||
|
)]
|
||||||
|
pub async fn run_macro(
|
||||||
|
ctx: poise::ApplicationContext<'_, Data, Error>,
|
||||||
|
#[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 {
|
||||||
|
match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Context::Application(ctx)
|
||||||
|
.say(format!("Command \"{}\" not found", command.command_name))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
pub mod autocomplete;
|
||||||
|
pub mod command_macro;
|
||||||
pub mod info_cmds;
|
pub mod info_cmds;
|
||||||
pub mod moderation_cmds;
|
pub mod moderation_cmds;
|
||||||
pub mod reminder_cmds;
|
pub mod reminder_cmds;
|
||||||
|
@ -1,32 +1,9 @@
|
|||||||
use std::collections::hash_map::Entry;
|
|
||||||
|
|
||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use chrono_tz::{Tz, TZ_VARIANTS};
|
use chrono_tz::{Tz, TZ_VARIANTS};
|
||||||
use levenshtein::levenshtein;
|
use levenshtein::levenshtein;
|
||||||
use poise::CreateReply;
|
|
||||||
|
|
||||||
use crate::{
|
use super::autocomplete::timezone_autocomplete;
|
||||||
component_models::pager::{MacroPager, Pager},
|
use crate::{consts::THEME_COLOR, models::CtxData, Context, Error};
|
||||||
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
|
||||||
models::{
|
|
||||||
command_macro::{guild_command_macro, CommandMacro},
|
|
||||||
CtxData,
|
|
||||||
},
|
|
||||||
Context, Data, Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
/// Select your timezone
|
||||||
#[poise::command(slash_command, identifying_name = "timezone")]
|
#[poise::command(slash_command, identifying_name = "timezone")]
|
||||||
@ -202,377 +179,3 @@ Do not share it!
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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_default()
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.name.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record and replay command sequences
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "macro",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "macro_base"
|
|
||||||
)]
|
|
||||||
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start recording up to 5 commands to replay
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "record",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "record_macro"
|
|
||||||
)]
|
|
||||||
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.",
|
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
let okay = {
|
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
|
||||||
|
|
||||||
if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
|
|
||||||
e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
.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.",
|
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finish current macro recording
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "finish",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "finish_macro"
|
|
||||||
)]
|
|
||||||
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?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
|
||||||
lock.remove(&key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List recorded macros
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "list",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "list_macro"
|
|
||||||
)]
|
|
||||||
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
let macros = ctx.command_macros().await?;
|
|
||||||
|
|
||||||
let resp = show_macro_page(¯os, 0);
|
|
||||||
|
|
||||||
ctx.send(|m| {
|
|
||||||
*m = resp;
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a recorded macro
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "run",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "run_macro"
|
|
||||||
)]
|
|
||||||
pub async fn run_macro(
|
|
||||||
ctx: poise::ApplicationContext<'_, Data, Error>,
|
|
||||||
#[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 {
|
|
||||||
match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(e) => {
|
|
||||||
println!("{:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Context::Application(ctx)
|
|
||||||
.say(format!("Command \"{}\" not found", command.command_name))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
|
||||||
Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a recorded macro
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "delete",
|
|
||||||
guild_only = true,
|
|
||||||
default_member_permissions = "MANAGE_GUILD",
|
|
||||||
identifying_name = "delete_macro"
|
|
||||||
)]
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
|
|
||||||
let pager = MacroPager::new(page);
|
|
||||||
|
|
||||||
if macros.is_empty() {
|
|
||||||
let mut reply = CreateReply::default();
|
|
||||||
|
|
||||||
reply.embed(|e| {
|
|
||||||
e.title("Macros")
|
|
||||||
.description("No Macros Set Up. Use `/macro record` to get started.")
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
});
|
|
||||||
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
let mut reply = CreateReply::default();
|
|
||||||
|
|
||||||
reply
|
|
||||||
.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
|
|
||||||
});
|
|
||||||
|
|
||||||
reply
|
|
||||||
}
|
|
||||||
|
@ -8,11 +8,13 @@ use chrono::NaiveDateTime;
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use num_integer::Integer;
|
use num_integer::Integer;
|
||||||
use poise::{
|
use poise::{
|
||||||
serenity::{builder::CreateEmbed, model::channel::Channel},
|
serenity_prelude::{
|
||||||
serenity_prelude::{component::ButtonStyle, ReactionType},
|
builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType,
|
||||||
CreateReply,
|
},
|
||||||
|
AutocompleteChoice, CreateReply, Modal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::autocomplete::timezone_autocomplete;
|
||||||
use crate::{
|
use crate::{
|
||||||
component_models::{
|
component_models::{
|
||||||
pager::{DelPager, LookPager, Pager},
|
pager::{DelPager, LookPager, Pager},
|
||||||
@ -36,7 +38,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
time_parser::natural_parser,
|
time_parser::natural_parser,
|
||||||
utils::{check_guild_subscription, check_subscription},
|
utils::{check_guild_subscription, check_subscription},
|
||||||
Context, Error,
|
ApplicationContext, Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Pause all reminders on the current channel until a certain time or indefinitely
|
/// Pause all reminders on the current channel until a certain time or indefinitely
|
||||||
@ -548,23 +550,93 @@ pub async fn delete_timer(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new reminder
|
async fn multiline_autocomplete(
|
||||||
|
_ctx: Context<'_>,
|
||||||
|
partial: &str,
|
||||||
|
) -> Vec<AutocompleteChoice<String>> {
|
||||||
|
if partial.is_empty() {
|
||||||
|
vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
AutocompleteChoice { name: partial.to_string(), value: partial.to_string() },
|
||||||
|
AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(poise::Modal)]
|
||||||
|
#[name = "Reminder"]
|
||||||
|
struct ContentModal {
|
||||||
|
#[name = "Content"]
|
||||||
|
#[placeholder = "Message..."]
|
||||||
|
#[paragraph]
|
||||||
|
#[max_length = 2000]
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a reminder. Press "+5 more" for other options. A modal will open if "content" is not provided
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
slash_command,
|
slash_command,
|
||||||
identifying_name = "remind",
|
identifying_name = "remind",
|
||||||
default_member_permissions = "MANAGE_GUILD"
|
default_member_permissions = "MANAGE_GUILD"
|
||||||
)]
|
)]
|
||||||
pub async fn remind(
|
pub async fn remind(
|
||||||
ctx: Context<'_>,
|
ctx: ApplicationContext<'_>,
|
||||||
#[description = "A description of the time to set the reminder for"] time: String,
|
#[description = "A description of the time to set the reminder for"] time: String,
|
||||||
#[description = "The message content to send"] content: String,
|
#[description = "The message content to send"]
|
||||||
|
#[autocomplete = "multiline_autocomplete"]
|
||||||
|
content: String,
|
||||||
#[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
|
#[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
|
||||||
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
|
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
|
||||||
interval: Option<String>,
|
interval: Option<String>,
|
||||||
#[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending"]
|
#[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop repeating"]
|
||||||
expires: Option<String>,
|
expires: Option<String>,
|
||||||
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
||||||
tts: Option<bool>,
|
tts: Option<bool>,
|
||||||
|
#[description = "Set a timezone override for this reminder only"]
|
||||||
|
#[autocomplete = "timezone_autocomplete"]
|
||||||
|
timezone: Option<String>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
|
||||||
|
|
||||||
|
if content.is_empty() {
|
||||||
|
let data = ContentModal::execute(ctx).await?;
|
||||||
|
|
||||||
|
create_reminder(
|
||||||
|
Context::Application(ctx),
|
||||||
|
time,
|
||||||
|
data.content,
|
||||||
|
channels,
|
||||||
|
interval,
|
||||||
|
expires,
|
||||||
|
tts,
|
||||||
|
tz,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
create_reminder(
|
||||||
|
Context::Application(ctx),
|
||||||
|
time,
|
||||||
|
content,
|
||||||
|
channels,
|
||||||
|
interval,
|
||||||
|
expires,
|
||||||
|
tts,
|
||||||
|
tz,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_reminder(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
time: String,
|
||||||
|
content: String,
|
||||||
|
channels: Option<String>,
|
||||||
|
interval: Option<String>,
|
||||||
|
expires: Option<String>,
|
||||||
|
tts: Option<bool>,
|
||||||
|
timezone: Option<Tz>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if interval.is_none() && expires.is_some() {
|
if interval.is_none() && expires.is_some() {
|
||||||
ctx.say("`expires` can only be used with `interval`").await?;
|
ctx.say("`expires` can only be used with `interval`").await?;
|
||||||
@ -575,7 +647,7 @@ pub async fn remind(
|
|||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
let user_data = ctx.author_data().await.unwrap();
|
let user_data = ctx.author_data().await.unwrap();
|
||||||
let timezone = ctx.timezone().await;
|
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
||||||
|
|
||||||
let time = natural_parser(&time, &timezone.to_string()).await;
|
let time = natural_parser(&time, &timezone.to_string()).await;
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@ use std::io::Cursor;
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use poise::{
|
use poise::{
|
||||||
serenity::{
|
serenity_prelude as serenity,
|
||||||
|
serenity_prelude::{
|
||||||
builder::CreateEmbed,
|
builder::CreateEmbed,
|
||||||
client::Context,
|
|
||||||
model::{
|
model::{
|
||||||
application::interaction::{
|
application::interaction::{
|
||||||
message_component::MessageComponentInteraction, InteractionResponseType,
|
message_component::MessageComponentInteraction, InteractionResponseType,
|
||||||
@ -15,15 +15,15 @@ use poise::{
|
|||||||
},
|
},
|
||||||
channel::Channel,
|
channel::Channel,
|
||||||
},
|
},
|
||||||
|
Context,
|
||||||
},
|
},
|
||||||
serenity_prelude as serenity,
|
|
||||||
};
|
};
|
||||||
use rmp_serde::Serializer;
|
use rmp_serde::Serializer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
moderation_cmds::{max_macro_page, show_macro_page},
|
command_macro::list::{max_macro_page, show_macro_page},
|
||||||
reminder_cmds::{max_delete_page, show_delete_page},
|
reminder_cmds::{max_delete_page, show_delete_page},
|
||||||
todo_cmds::{max_todo_page, show_todo_page},
|
todo_cmds::{max_todo_page, show_todo_page},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// todo split pager out into a single struct
|
// todo split pager out into a single struct
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{builder::CreateComponents, model::application::component::ButtonStyle};
|
use poise::serenity_prelude::{
|
||||||
|
builder::CreateComponents, model::application::component::ButtonStyle,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::*;
|
use serde_repr::*;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ pub const MACRO_MAX_COMMANDS: usize = 5;
|
|||||||
|
|
||||||
use std::{collections::HashSet, env, iter::FromIterator};
|
use std::{collections::HashSet, env, iter::FromIterator};
|
||||||
|
|
||||||
use poise::serenity::model::prelude::AttachmentType;
|
use poise::serenity_prelude::model::prelude::AttachmentType;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use std::{collections::HashMap, env, sync::atomic::Ordering};
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
use log::{error, info, warn};
|
use log::error;
|
||||||
use poise::{
|
use poise::{
|
||||||
serenity::{model::application::interaction::Interaction, utils::shard_id},
|
|
||||||
serenity_prelude as serenity,
|
serenity_prelude as serenity,
|
||||||
|
serenity_prelude::{model::application::interaction::Interaction, utils::shard_id},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{component_models::ComponentDataModel, Data, Error};
|
use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR};
|
||||||
|
|
||||||
pub async fn listener(
|
pub async fn listener(
|
||||||
ctx: &serenity::Context,
|
ctx: &serenity::Context,
|
||||||
@ -17,45 +17,6 @@ pub async fn listener(
|
|||||||
poise::Event::Ready { .. } => {
|
poise::Event::Ready { .. } => {
|
||||||
ctx.set_activity(serenity::Activity::watching("for /remind")).await;
|
ctx.set_activity(serenity::Activity::watching("for /remind")).await;
|
||||||
}
|
}
|
||||||
poise::Event::CacheReady { .. } => {
|
|
||||||
info!("Cache Ready! Preparing extra processes");
|
|
||||||
|
|
||||||
if !data.is_loop_running.load(Ordering::Relaxed) {
|
|
||||||
let kill_tx = data.broadcast.clone();
|
|
||||||
let kill_recv = data.broadcast.subscribe();
|
|
||||||
|
|
||||||
let ctx1 = ctx.clone();
|
|
||||||
let ctx2 = ctx.clone();
|
|
||||||
|
|
||||||
let pool1 = data.database.clone();
|
|
||||||
let pool2 = data.database.clone();
|
|
||||||
|
|
||||||
let run_settings = env::var("DONTRUN").unwrap_or_else(|_| "".to_string());
|
|
||||||
|
|
||||||
if !run_settings.contains("postman") {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
match postman::initialize(kill_recv, ctx1, &pool1).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
error!("postman exiting: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
warn!("Not running postman");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !run_settings.contains("web") {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
warn!("Not running web");
|
|
||||||
}
|
|
||||||
|
|
||||||
data.is_loop_running.swap(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
poise::Event::ChannelDelete { channel } => {
|
poise::Event::ChannelDelete { channel } => {
|
||||||
sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.as_u64())
|
sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.as_u64())
|
||||||
.execute(&data.database)
|
.execute(&data.database)
|
||||||
@ -66,46 +27,36 @@ pub async fn listener(
|
|||||||
if *is_new {
|
if *is_new {
|
||||||
let guild_id = guild.id.as_u64().to_owned();
|
let guild_id = guild.id.as_u64().to_owned();
|
||||||
|
|
||||||
sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id)
|
sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id)
|
||||||
.execute(&data.database)
|
.execute(&data.database)
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
if let Err(e) = post_guild_count(ctx, &data.http, guild_id).await {
|
||||||
let shard_count = ctx.cache.shard_count();
|
error!("DiscordBotList: {:?}", e);
|
||||||
let current_shard_id = shard_id(guild_id, shard_count);
|
}
|
||||||
|
|
||||||
let guild_count = ctx
|
let default_channel = guild.default_channel_guaranteed();
|
||||||
.cache
|
|
||||||
.guilds()
|
if let Some(default_channel) = default_channel {
|
||||||
.iter()
|
default_channel
|
||||||
.filter(|g| {
|
.send_message(&ctx, |m| {
|
||||||
shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
|
m.embed(|e| {
|
||||||
|
e.title("Thank you for adding Reminder Bot!").description(
|
||||||
|
"To get started:
|
||||||
|
• Set your timezone with `/timezone`
|
||||||
|
• Set up permissions in Server Settings 🠚 Integrations 🠚 Reminder Bot (desktop only)
|
||||||
|
• Create your first reminder with `/remind`
|
||||||
|
|
||||||
|
__Support__
|
||||||
|
If you need any support, please come and ask us! Join our [Discord](https://discord.jellywx.com).
|
||||||
|
|
||||||
|
__Updates__
|
||||||
|
To stay up to date on the latest features and fixes, join our [Discord](https://discord.jellywx.com).
|
||||||
|
",
|
||||||
|
).color(*THEME_COLOR)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.count() as u64;
|
.await?;
|
||||||
|
|
||||||
let mut hm = HashMap::new();
|
|
||||||
hm.insert("server_count", guild_count);
|
|
||||||
hm.insert("shard_id", current_shard_id);
|
|
||||||
hm.insert("shard_count", shard_count);
|
|
||||||
|
|
||||||
let response = data
|
|
||||||
.http
|
|
||||||
.post(
|
|
||||||
format!(
|
|
||||||
"https://top.gg/api/bots/{}/stats",
|
|
||||||
ctx.cache.current_user_id().as_u64()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.header("Authorization", token)
|
|
||||||
.json(&hm)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(res) = response {
|
|
||||||
println!("DiscordBots Response: {:?}", res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,3 +77,38 @@ pub async fn listener(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn post_guild_count(
|
||||||
|
ctx: &serenity::Context,
|
||||||
|
http: &reqwest::Client,
|
||||||
|
guild_id: u64,
|
||||||
|
) -> Result<(), reqwest::Error> {
|
||||||
|
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
||||||
|
let shard_count = ctx.cache.shard_count();
|
||||||
|
let current_shard_id = shard_id(guild_id, shard_count);
|
||||||
|
|
||||||
|
let guild_count = ctx
|
||||||
|
.cache
|
||||||
|
.guilds()
|
||||||
|
.iter()
|
||||||
|
.filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id)
|
||||||
|
.count() as u64;
|
||||||
|
|
||||||
|
let mut hm = HashMap::new();
|
||||||
|
hm.insert("server_count", guild_count);
|
||||||
|
hm.insert("shard_id", current_shard_id);
|
||||||
|
hm.insert("shard_count", shard_count);
|
||||||
|
|
||||||
|
http.post(
|
||||||
|
format!("https://top.gg/api/bots/{}/stats", ctx.cache.current_user_id().as_u64())
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.header("Authorization", token)
|
||||||
|
.json(&hm)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
48
src/hooks.rs
48
src/hooks.rs
@ -1,36 +1,42 @@
|
|||||||
use poise::serenity::model::channel::Channel;
|
use poise::{
|
||||||
|
serenity_prelude::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
||||||
|
|
||||||
async fn macro_check(ctx: Context<'_>) -> bool {
|
async fn recording_macro_check(ctx: Context<'_>) -> bool {
|
||||||
if let Context::Application(app_ctx) = ctx {
|
if let Context::Application(app_ctx) = ctx {
|
||||||
if let Some(guild_id) = ctx.guild_id() {
|
if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) =
|
||||||
if ctx.command().identifying_name != "finish_macro" {
|
app_ctx.interaction
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
{
|
||||||
|
if let Some(guild_id) = ctx.guild_id() {
|
||||||
|
if ctx.command().identifying_name != "finish_macro" {
|
||||||
|
let mut lock = ctx.data().recording_macros.write().await;
|
||||||
|
|
||||||
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
|
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
|
||||||
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
|
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
|
||||||
let _ = ctx.send(|m| {
|
let _ = ctx.send(|m| {
|
||||||
m.ephemeral(true).content(
|
m.ephemeral(true).content(
|
||||||
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
|
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
let recorded = RecordedCommand {
|
let recorded = RecordedCommand {
|
||||||
action: None,
|
action: None,
|
||||||
command_name: ctx.command().identifying_name.clone(),
|
command_name: ctx.command().identifying_name.clone(),
|
||||||
options: Vec::from(app_ctx.args),
|
options: Vec::from(app_ctx.args),
|
||||||
};
|
};
|
||||||
|
|
||||||
command_macro.commands.push(recorded);
|
command_macro.commands.push(recorded);
|
||||||
|
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.send(|m| m.ephemeral(true).content("Command recorded to macro"))
|
.send(|m| m.ephemeral(true).content("Command recorded to macro"))
|
||||||
.await;
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,5 +95,5 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> {
|
pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
Ok(macro_check(ctx).await && check_self_permissions(ctx).await)
|
Ok(recording_macro_check(ctx).await && check_self_permissions(ctx).await)
|
||||||
}
|
}
|
||||||
|
71
src/main.rs
71
src/main.rs
@ -18,12 +18,12 @@ use std::{
|
|||||||
env,
|
env,
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt::{Debug, Display, Formatter},
|
fmt::{Debug, Display, Formatter},
|
||||||
sync::atomic::AtomicBool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use poise::serenity::model::{
|
use log::{error, warn};
|
||||||
|
use poise::serenity_prelude::model::{
|
||||||
gateway::GatewayIntents,
|
gateway::GatewayIntents,
|
||||||
id::{GuildId, UserId},
|
id::{GuildId, UserId},
|
||||||
};
|
};
|
||||||
@ -31,7 +31,7 @@ use sqlx::{MySql, Pool};
|
|||||||
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
|
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
|
commands::{command_macro, info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
|
||||||
consts::THEME_COLOR,
|
consts::THEME_COLOR,
|
||||||
event_handlers::listener,
|
event_handlers::listener,
|
||||||
hooks::all_checks,
|
hooks::all_checks,
|
||||||
@ -43,14 +43,14 @@ type Database = MySql;
|
|||||||
|
|
||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
|
||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
database: Pool<Database>,
|
database: Pool<Database>,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
|
recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
|
||||||
popular_timezones: Vec<Tz>,
|
popular_timezones: Vec<Tz>,
|
||||||
is_loop_running: AtomicBool,
|
_broadcast: Sender<()>,
|
||||||
broadcast: Sender<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Data {
|
impl Debug for Data {
|
||||||
@ -111,13 +111,15 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
|||||||
moderation_cmds::webhook(),
|
moderation_cmds::webhook(),
|
||||||
poise::Command {
|
poise::Command {
|
||||||
subcommands: vec![
|
subcommands: vec![
|
||||||
moderation_cmds::delete_macro(),
|
command_macro::delete::delete_macro(),
|
||||||
moderation_cmds::finish_macro(),
|
command_macro::record::finish_macro(),
|
||||||
moderation_cmds::list_macro(),
|
command_macro::list::list_macro(),
|
||||||
moderation_cmds::record_macro(),
|
command_macro::record::record_macro(),
|
||||||
moderation_cmds::run_macro(),
|
command_macro::run::run_macro(),
|
||||||
|
command_macro::migrate::migrate_macro(),
|
||||||
|
command_macro::install::install_macro(),
|
||||||
],
|
],
|
||||||
..moderation_cmds::macro_base()
|
..command_macro::macro_base()
|
||||||
},
|
},
|
||||||
reminder_cmds::pause(),
|
reminder_cmds::pause(),
|
||||||
reminder_cmds::offset(),
|
reminder_cmds::offset(),
|
||||||
@ -176,27 +178,50 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
|||||||
.map(|t| t.timezone.parse::<Tz>().unwrap())
|
.map(|t| t.timezone.parse::<Tz>().unwrap())
|
||||||
.collect::<Vec<Tz>>();
|
.collect::<Vec<Tz>>();
|
||||||
|
|
||||||
poise::Framework::build()
|
poise::Framework::builder()
|
||||||
.token(discord_token)
|
.token(discord_token)
|
||||||
.user_data_setup(move |ctx, _bot, framework| {
|
.user_data_setup(move |ctx, _bot, framework| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
register_application_commands(
|
register_application_commands(ctx, framework, None).await.unwrap();
|
||||||
ctx,
|
|
||||||
framework,
|
let kill_tx = tx.clone();
|
||||||
env::var("DEBUG_GUILD")
|
let kill_recv = tx.subscribe();
|
||||||
.map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
|
|
||||||
.ok(),
|
let ctx1 = ctx.clone();
|
||||||
)
|
let ctx2 = ctx.clone();
|
||||||
.await
|
|
||||||
.unwrap();
|
let pool1 = database.clone();
|
||||||
|
let pool2 = database.clone();
|
||||||
|
|
||||||
|
let run_settings = env::var("DONTRUN").unwrap_or_else(|_| "".to_string());
|
||||||
|
|
||||||
|
if !run_settings.contains("postman") {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match postman::initialize(kill_recv, ctx1, &pool1).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("postman exiting: {}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
warn!("Not running postman");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !run_settings.contains("web") {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
warn!("Not running web");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Data {
|
Ok(Data {
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::new(),
|
||||||
database,
|
database,
|
||||||
popular_timezones,
|
popular_timezones,
|
||||||
recording_macros: Default::default(),
|
recording_macros: Default::default(),
|
||||||
is_loop_running: AtomicBool::new(false),
|
_broadcast: tx,
|
||||||
broadcast: tx,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use poise::serenity::model::channel::Channel;
|
use poise::serenity_prelude::model::channel::Channel;
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
pub struct ChannelData {
|
pub struct ChannelData {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use poise::serenity::model::{
|
use poise::serenity_prelude::model::{
|
||||||
application::interaction::application_command::CommandDataOption, id::GuildId,
|
application::interaction::application_command::CommandDataOption, id::GuildId,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{Context, Data, Error};
|
use crate::{Context, Data, Error};
|
||||||
|
|
||||||
@ -29,6 +30,14 @@ pub struct CommandMacro<U, E> {
|
|||||||
pub commands: Vec<RecordedCommand<U, E>>,
|
pub commands: Vec<RecordedCommand<U, E>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RawCommandMacro {
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub commands: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a macro by name form a guild.
|
||||||
pub async fn guild_command_macro(
|
pub async fn guild_command_macro(
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
@ -5,7 +5,7 @@ pub mod timer;
|
|||||||
pub mod user_data;
|
pub mod user_data;
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{async_trait, model::id::UserId};
|
use poise::serenity_prelude::{async_trait, model::id::UserId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{channel_data::ChannelData, user_data::UserData},
|
models::{channel_data::ChannelData, user_data::UserData},
|
||||||
|
@ -2,7 +2,7 @@ use std::{collections::HashSet, fmt::Display};
|
|||||||
|
|
||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{
|
use poise::serenity_prelude::{
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
model::{
|
model::{
|
||||||
channel::GuildChannel,
|
channel::GuildChannel,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use poise::serenity::model::id::ChannelId;
|
use poise::serenity_prelude::model::id::ChannelId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::*;
|
use serde_repr::*;
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ use std::hash::{Hash, Hasher};
|
|||||||
|
|
||||||
use chrono::{NaiveDateTime, TimeZone};
|
use chrono::{NaiveDateTime, TimeZone};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::{
|
use poise::serenity_prelude::{
|
||||||
serenity::model::id::{ChannelId, GuildId, UserId},
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
serenity_prelude::Cache,
|
Cache,
|
||||||
};
|
};
|
||||||
use sqlx::Executor;
|
use sqlx::Executor;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use log::error;
|
use log::error;
|
||||||
use poise::serenity::{http::CacheHttp, model::id::UserId};
|
use poise::serenity_prelude::{http::CacheHttp, model::id::UserId};
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::consts::LOCAL_TIMEZONE;
|
use crate::consts::LOCAL_TIMEZONE;
|
||||||
|
12
src/utils.rs
12
src/utils.rs
@ -1,11 +1,11 @@
|
|||||||
use poise::{
|
use poise::{
|
||||||
serenity::{
|
serenity_prelude as serenity,
|
||||||
|
serenity_prelude::{
|
||||||
builder::CreateApplicationCommands,
|
builder::CreateApplicationCommands,
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
|
interaction::MessageFlags,
|
||||||
model::id::{GuildId, UserId},
|
model::id::{GuildId, UserId},
|
||||||
},
|
},
|
||||||
serenity_prelude as serenity,
|
|
||||||
serenity_prelude::interaction::MessageFlags,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -14,10 +14,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub async fn register_application_commands(
|
pub async fn register_application_commands(
|
||||||
ctx: &poise::serenity::client::Context,
|
ctx: &serenity::Context,
|
||||||
framework: &poise::Framework<Data, Error>,
|
framework: &poise::Framework<Data, Error>,
|
||||||
guild_id: Option<GuildId>,
|
guild_id: Option<GuildId>,
|
||||||
) -> Result<(), poise::serenity::Error> {
|
) -> Result<(), serenity::Error> {
|
||||||
let mut commands_builder = CreateApplicationCommands::default();
|
let mut commands_builder = CreateApplicationCommands::default();
|
||||||
let commands = &framework.options().commands;
|
let commands = &framework.options().commands;
|
||||||
for command in commands {
|
for command in commands {
|
||||||
@ -28,7 +28,7 @@ pub async fn register_application_commands(
|
|||||||
commands_builder.add_application_command(context_menu_command);
|
commands_builder.add_application_command(context_menu_command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let commands_builder = poise::serenity::json::Value::Array(commands_builder.0);
|
let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
|
||||||
|
|
||||||
if let Some(guild_id) = guild_id {
|
if let Some(guild_id) = guild_id {
|
||||||
ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
|
ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
|
||||||
|
@ -12,7 +12,7 @@ oauth2 = "4"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
|
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = "0.5"
|
chrono-tz = "0.5"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
Reference in New Issue
Block a user