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
 | 
					 | 
				
			||||||
                        .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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    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);
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let default_channel = guild.default_channel_guaranteed();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(default_channel) = default_channel {
 | 
				
			||||||
 | 
					                    default_channel
 | 
				
			||||||
 | 
					                        .send_message(&ctx, |m| {
 | 
				
			||||||
 | 
					                            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)
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -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(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -1,9 +1,14 @@
 | 
				
			|||||||
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 ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) =
 | 
				
			||||||
 | 
					            app_ctx.interaction
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            if let Some(guild_id) = ctx.guild_id() {
 | 
					            if let Some(guild_id) = ctx.guild_id() {
 | 
				
			||||||
                if ctx.command().identifying_name != "finish_macro" {
 | 
					                if ctx.command().identifying_name != "finish_macro" {
 | 
				
			||||||
                    let mut lock = ctx.data().recording_macros.write().await;
 | 
					                    let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
@@ -35,6 +40,7 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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