Compare commits
	
		
			19 Commits
		
	
	
		
			jellywx/fi
			...
			jellywx/gu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b0a04bb289 | |||
| eef1f6f3e8 | |||
| 3d08027325 | |||
| 94bfd39085 | |||
| 40cd5f8a36 | |||
| 133b00a2ce | |||
| 57336f5c81 | |||
| b62d24c024 | |||
| 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.6"
 | 
				
			||||||
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]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										92
									
								
								migration/05-restructure-guild-table.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								migration/05-restructure-guild-table.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					SET foreign_key_checks = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					START TRANSACTION;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- drop existing constraints
 | 
				
			||||||
 | 
					ALTER TABLE channels DROP FOREIGN KEY `channels_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE command_aliases DROP FOREIGN KEY `command_aliases_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE events DROP FOREIGN KEY `events_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE guild_users DROP FOREIGN KEY `guild_users_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE macro DROP FOREIGN KEY `macro_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE roles DROP FOREIGN KEY `roles_ibfk_1`;
 | 
				
			||||||
 | 
					ALTER TABLE todos DROP FOREIGN KEY `todos_ibfk_2`;
 | 
				
			||||||
 | 
					ALTER TABLE reminder_template DROP FOREIGN KEY `reminder_template_ibfk_1`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- update foreign key types
 | 
				
			||||||
 | 
					ALTER TABLE channels MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE command_aliases MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE events MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE guild_users MODIFY `guild` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE macro MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE roles MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE todos MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE reminder_template MODIFY `guild_id` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- update foreign key values
 | 
				
			||||||
 | 
					UPDATE channels SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE command_aliases SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE events SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE guild_users SET `guild` = (SELECT `guild` FROM guilds WHERE guilds.`id` = guild_users.`guild`);
 | 
				
			||||||
 | 
					UPDATE macro SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE roles SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE todos SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					UPDATE reminder_template SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- update guilds table
 | 
				
			||||||
 | 
					ALTER TABLE guilds MODIFY `id` BIGINT UNSIGNED NOT NULL;
 | 
				
			||||||
 | 
					UPDATE guilds SET `id` = `guild`;
 | 
				
			||||||
 | 
					ALTER TABLE guilds DROP COLUMN `guild`;
 | 
				
			||||||
 | 
					ALTER TABLE guilds ADD COLUMN `default_channel` BIGINT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE guilds ADD CONSTRAINT `default_channel_fk`
 | 
				
			||||||
 | 
					    FOREIGN KEY (`default_channel`)
 | 
				
			||||||
 | 
					        REFERENCES channels(`channel`)
 | 
				
			||||||
 | 
					        ON DELETE SET NULL
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- re-add constraints
 | 
				
			||||||
 | 
					ALTER TABLE channels ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE command_aliases ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE events ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE guild_users ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE macro ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE roles ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE todos ADD CONSTRAINT
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`)
 | 
				
			||||||
 | 
					        REFERENCES guilds(`id`)
 | 
				
			||||||
 | 
					        ON DELETE CASCADE
 | 
				
			||||||
 | 
					        ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMIT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SET foreign_key_checks = 1;
 | 
				
			||||||
@@ -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"] }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										131
									
								
								src/commands/autocomplete.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/commands/autocomplete.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono_tz::TZ_VARIANTS;
 | 
				
			||||||
 | 
					use poise::AutocompleteChoice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{models::CtxData, time_parser::natural_parser, 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 = ?
 | 
				
			||||||
 | 
					    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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub 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() },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn time_hint_autocomplete(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    partial: &str,
 | 
				
			||||||
 | 
					) -> Vec<AutocompleteChoice<String>> {
 | 
				
			||||||
 | 
					    if partial.is_empty() {
 | 
				
			||||||
 | 
					        vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					            name: "Start typing a time...".to_string(),
 | 
				
			||||||
 | 
					            value: "now".to_string(),
 | 
				
			||||||
 | 
					        }]
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        match natural_parser(partial, &ctx.timezone().await.to_string()).await {
 | 
				
			||||||
 | 
					            Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
 | 
				
			||||||
 | 
					                Ok(now) => {
 | 
				
			||||||
 | 
					                    let diff = timestamp - now.as_secs() as i64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if diff < 0 {
 | 
				
			||||||
 | 
					                        vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                            name: "Time is in the past".to_string(),
 | 
				
			||||||
 | 
					                            value: "now".to_string(),
 | 
				
			||||||
 | 
					                        }]
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        if diff > 86400 {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!(
 | 
				
			||||||
 | 
					                                        "In approximately {} days, {} hours",
 | 
				
			||||||
 | 
					                                        diff / 86400,
 | 
				
			||||||
 | 
					                                        (diff % 86400) / 3600
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        } else if diff > 3600 {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!("In approximately {} hours", diff / 3600),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!("In approximately {} minutes", diff / 60),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(_) => {
 | 
				
			||||||
 | 
					                    vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                        name: partial.to_string(),
 | 
				
			||||||
 | 
					                        value: partial.to_string(),
 | 
				
			||||||
 | 
					                    }]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                    name: "Time not recognised".to_string(),
 | 
				
			||||||
 | 
					                    value: "now".to_string(),
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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 = ? 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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								src/commands/command_macro/list.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/commands/command_macro/list.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    component_models::pager::{MacroPager, Pager},
 | 
				
			||||||
 | 
					    consts::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 {
 | 
				
			||||||
 | 
					    ((macros.len() as f64) / 25.0).ceil() as usize
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 lower = (page * 25).min(macros.len());
 | 
				
			||||||
 | 
					    let upper = ((page + 1) * 25).min(macros.len());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let fields = macros[lower..upper].iter().map(|m| {
 | 
				
			||||||
 | 
					        if let Some(description) = &m.description {
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                m.name.clone(),
 | 
				
			||||||
 | 
					                format!("*{}*\n- Has {} commands", description, m.commands.len()),
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            (m.name.clone(), format!("- Has {} commands", m.commands.len()), true)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					        .embed(|e| {
 | 
				
			||||||
 | 
					            e.title("Macros")
 | 
				
			||||||
 | 
					                .fields(fields)
 | 
				
			||||||
 | 
					                .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 = ?",
 | 
				
			||||||
 | 
					        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 (?, ?, ?, ?)",
 | 
				
			||||||
 | 
					                    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,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/commands/command_macro/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/commands/command_macro/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					use crate::{Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod delete;
 | 
				
			||||||
 | 
					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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										151
									
								
								src/commands/command_macro/record.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/commands/command_macro/record.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					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> {
 | 
				
			||||||
 | 
					    if name.len() > 100 {
 | 
				
			||||||
 | 
					        ctx.say("Name must be less than 100 characters").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if description.as_ref().map_or(0, |d| d.len()) > 100 {
 | 
				
			||||||
 | 
					        ctx.say("Description must be less than 100 characters").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let guild_id = ctx.guild_id().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT 1 as _e FROM macro WHERE guild_id = ? 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 (?, ?, ?, ?)",
 | 
				
			||||||
 | 
					                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 @@
 | 
				
			|||||||
 | 
					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,11 @@
 | 
				
			|||||||
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 log::warn;
 | 
				
			||||||
 | 
					use poise::serenity_prelude::{ChannelId, Mentionable};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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")]
 | 
				
			||||||
@@ -170,409 +149,76 @@ pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Set defaults for commands
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    identifying_name = "default",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn default(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Set a default channel for reminders to be sent to
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "default_channel",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn default_channel(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "Channel to send reminders to by default"] channel: Option<ChannelId>,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    if let Some(mut guild_data) = ctx.guild_data().await {
 | 
				
			||||||
 | 
					        guild_data.default_channel = channel.map(|c| c.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        guild_data.commit_changes(&ctx.data().database).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(channel) = channel {
 | 
				
			||||||
 | 
					            ctx.send(|r| {
 | 
				
			||||||
 | 
					                r.ephemeral(true).content(format!("Default channel set to {}", channel.mention()))
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            ctx.send(|r| r.ephemeral(true).content("Default channel unset.")).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// View the webhook being used to send reminders to this channel
 | 
					/// View the webhook being used to send reminders to this channel
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
    slash_command,
 | 
					    slash_command,
 | 
				
			||||||
    identifying_name = "webhook_url",
 | 
					    identifying_name = "webhook_url",
 | 
				
			||||||
    required_permissions = "ADMINISTRATOR"
 | 
					    required_permissions = "ADMINISTRATOR",
 | 
				
			||||||
 | 
					    default_member_permissions = "ADMINISTRATOR"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    match ctx.channel_data().await {
 | 
					    match ctx.channel_data().await {
 | 
				
			||||||
        Ok(data) => {
 | 
					        Ok(data) => {
 | 
				
			||||||
            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
					            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
				
			||||||
                let _ = ctx
 | 
					                ctx.send(|b| {
 | 
				
			||||||
                    .send(|b| {
 | 
					                    b.ephemeral(true).content(format!(
 | 
				
			||||||
                        b.ephemeral(true).content(format!(
 | 
					                        "**Warning!**
 | 
				
			||||||
                            "**Warning!**
 | 
					 | 
				
			||||||
This link can be used by users to anonymously send messages, with or without permissions.
 | 
					This link can be used by users to anonymously send messages, with or without permissions.
 | 
				
			||||||
Do not share it!
 | 
					Do not share it!
 | 
				
			||||||
|| https://discord.com/api/webhooks/{}/{} ||",
 | 
					|| https://discord.com/api/webhooks/{}/{} ||",
 | 
				
			||||||
                            id, token,
 | 
					                        id, token,
 | 
				
			||||||
                        ))
 | 
					                    ))
 | 
				
			||||||
                    })
 | 
					                })
 | 
				
			||||||
                    .await;
 | 
					                .await?;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                let _ = ctx.say("No webhook configured on this channel.").await;
 | 
					                ctx.say("No webhook configured on this channel.").await?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(_) => {
 | 
					 | 
				
			||||||
            let _ = ctx.say("No webhook configured on this channel.").await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            panic!("{}", e);
 | 
					            warn!("Error fetching channel data: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.say("No webhook configured on this channel.").await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    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,12 +8,16 @@ 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,
 | 
					    },
 | 
				
			||||||
 | 
					    CreateReply, Modal,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
 | 
					    commands::autocomplete::{
 | 
				
			||||||
 | 
					        multiline_autocomplete, time_hint_autocomplete, timezone_autocomplete,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    component_models::{
 | 
					    component_models::{
 | 
				
			||||||
        pager::{DelPager, LookPager, Pager},
 | 
					        pager::{DelPager, LookPager, Pager},
 | 
				
			||||||
        ComponentDataModel, DelSelector, UndoReminder,
 | 
					        ComponentDataModel, DelSelector, UndoReminder,
 | 
				
			||||||
@@ -36,7 +40,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 +552,81 @@ pub async fn delete_timer(
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a new reminder
 | 
					#[derive(poise::Modal)]
 | 
				
			||||||
 | 
					#[name = "Reminder"]
 | 
				
			||||||
 | 
					struct ContentModal {
 | 
				
			||||||
 | 
					    #[name = "Content"]
 | 
				
			||||||
 | 
					    #[placeholder = "Message..."]
 | 
				
			||||||
 | 
					    #[paragraph]
 | 
				
			||||||
 | 
					    #[max_length = 2000]
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a reminder. Press "+4 more" for other options.
 | 
				
			||||||
#[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"]
 | 
				
			||||||
    #[description = "The message content to send"] content: String,
 | 
					    #[autocomplete = "time_hint_autocomplete"]
 | 
				
			||||||
 | 
					    time: 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 +637,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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -591,7 +653,9 @@ pub async fn remind(
 | 
				
			|||||||
                let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
 | 
					                let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if list.is_empty() {
 | 
					                if list.is_empty() {
 | 
				
			||||||
                    if ctx.guild_id().is_some() {
 | 
					                    if let Some(channel_id) = ctx.default_channel().await {
 | 
				
			||||||
 | 
					                        vec![ReminderScope::Channel(channel_id.0)]
 | 
				
			||||||
 | 
					                    } else if ctx.guild_id().is_some() {
 | 
				
			||||||
                        vec![ReminderScope::Channel(ctx.channel_id().0)]
 | 
					                        vec![ReminderScope::Channel(ctx.channel_id().0)]
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        vec![ReminderScope::User(ctx.author().id.0)]
 | 
					                        vec![ReminderScope::User(ctx.author().id.0)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ pub async fn todo_guild_add(
 | 
				
			|||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, value)
 | 
					        "INSERT INTO todos (guild_id, value)
 | 
				
			||||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
					VALUES (?, ?)",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        task
 | 
					        task
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -70,9 +70,7 @@ VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
				
			|||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let values = sqlx::query!(
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
        "SELECT todos.id, value FROM todos
 | 
					        "SELECT todos.id, value FROM todos WHERE guild_id = ?",
 | 
				
			||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
@@ -122,7 +120,7 @@ pub async fn todo_channel_add(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, channel_id, value)
 | 
					        "INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
					VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        ctx.channel_id().0,
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
        task
 | 
					        task
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -222,9 +222,7 @@ WHERE channels.channel = ?",
 | 
				
			|||||||
                        .collect::<Vec<(usize, String)>>()
 | 
					                        .collect::<Vec<(usize, String)>>()
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					                            "SELECT todos.id, value FROM todos WHERE guild_id = ?",
 | 
				
			||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
                            pager.guild_id,
 | 
					                            pager.guild_id,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&data.database)
 | 
					                        .fetch_all(&data.database)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ pub const DAY: u64 = 86_400;
 | 
				
			|||||||
pub const HOUR: u64 = 3_600;
 | 
					pub const HOUR: u64 = 3_600;
 | 
				
			||||||
pub const MINUTE: u64 = 60;
 | 
					pub const MINUTE: u64 = 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
					pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4096;
 | 
				
			||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
					pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
@@ -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,14 @@
 | 
				
			|||||||
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, models::guild_data::GuildData, Data, Error, THEME_COLOR,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn listener(
 | 
					pub async fn listener(
 | 
				
			||||||
    ctx: &serenity::Context,
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
@@ -17,45 +19,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,59 +29,61 @@ 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 (id) VALUES (?)", guild_id)
 | 
				
			||||||
                    .execute(&data.database)
 | 
					                    .execute(&data.database)
 | 
				
			||||||
                    .await
 | 
					                    .await?;
 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
					                if let Err(e) = post_guild_count(ctx, &data.http, guild_id).await {
 | 
				
			||||||
                    let shard_count = ctx.cache.shard_count();
 | 
					                    error!("DiscordBotList: {:?}", e);
 | 
				
			||||||
                    let current_shard_id = shard_id(guild_id, shard_count);
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let guild_count = ctx
 | 
					                let default_channel = guild.default_channel_guaranteed();
 | 
				
			||||||
                        .cache
 | 
					
 | 
				
			||||||
                        .guilds()
 | 
					                if let Some(default_channel) = default_channel {
 | 
				
			||||||
                        .iter()
 | 
					                    default_channel
 | 
				
			||||||
                        .filter(|g| {
 | 
					                        .send_message(&ctx, |m| {
 | 
				
			||||||
                            shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
 | 
					                            m.embed(|e| {
 | 
				
			||||||
 | 
					                                e.title("Thank you for adding Reminder Bot!").description(
 | 
				
			||||||
 | 
					                                    "To get started:
 | 
				
			||||||
 | 
					• Set your timezone with `/timezone`
 | 
				
			||||||
 | 
					• Set up permissions in Server Settings 🠚 Integrations 🠚 Reminder Bot (desktop only)
 | 
				
			||||||
 | 
					• Create your first reminder with `/remind`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__Support__
 | 
				
			||||||
 | 
					If you need any support, please come and ask us! Join our [Discord](https://discord.jellywx.com).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__Updates__
 | 
				
			||||||
 | 
					To stay up to date on the latest features and fixes, join our [Discord](https://discord.jellywx.com).
 | 
				
			||||||
 | 
					",
 | 
				
			||||||
 | 
					                                ).color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
                        })
 | 
					                        })
 | 
				
			||||||
                        .count() as u64;
 | 
					                        .await?;
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let mut hm = HashMap::new();
 | 
					 | 
				
			||||||
                    hm.insert("server_count", guild_count);
 | 
					 | 
				
			||||||
                    hm.insert("shard_id", current_shard_id);
 | 
					 | 
				
			||||||
                    hm.insert("shard_count", shard_count);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let response = data
 | 
					 | 
				
			||||||
                        .http
 | 
					 | 
				
			||||||
                        .post(
 | 
					 | 
				
			||||||
                            format!(
 | 
					 | 
				
			||||||
                                "https://top.gg/api/bots/{}/stats",
 | 
					 | 
				
			||||||
                                ctx.cache.current_user_id().as_u64()
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            .as_str(),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .header("Authorization", token)
 | 
					 | 
				
			||||||
                        .json(&hm)
 | 
					 | 
				
			||||||
                        .send()
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Err(res) = response {
 | 
					 | 
				
			||||||
                        println!("DiscordBots Response: {:?}", res);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        poise::Event::GuildDelete { incomplete, .. } => {
 | 
					        poise::Event::GuildDelete { incomplete, .. } => {
 | 
				
			||||||
            let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
 | 
					            let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.0)
 | 
				
			||||||
                .execute(&data.database)
 | 
					                .execute(&data.database)
 | 
				
			||||||
                .await;
 | 
					                .await;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        poise::Event::InteractionCreate { interaction } => {
 | 
					        poise::Event::InteractionCreate { interaction } => {
 | 
				
			||||||
            if let Interaction::MessageComponent(component) = interaction {
 | 
					            match interaction {
 | 
				
			||||||
                let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
					                Interaction::ApplicationCommand(app_command) => {
 | 
				
			||||||
 | 
					                    if let Some(guild_id) = app_command.guild_id {
 | 
				
			||||||
 | 
					                        // check database guild exists
 | 
				
			||||||
 | 
					                        GuildData::from_guild(guild_id, &data.database).await?;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                component_model.act(ctx, data, component).await;
 | 
					                Interaction::MessageComponent(component) => {
 | 
				
			||||||
 | 
					                    let component_model =
 | 
				
			||||||
 | 
					                        ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    component_model.act(ctx, data, component).await;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _ => {}
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        _ => {}
 | 
					        _ => {}
 | 
				
			||||||
@@ -126,3 +91,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(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										44
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -1,36 +1,42 @@
 | 
				
			|||||||
use poise::serenity::model::channel::Channel;
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity_prelude::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
					use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn macro_check(ctx: Context<'_>) -> bool {
 | 
					async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			||||||
    if let Context::Application(app_ctx) = ctx {
 | 
					    if let Context::Application(app_ctx) = ctx {
 | 
				
			||||||
        if let Some(guild_id) = ctx.guild_id() {
 | 
					        if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) =
 | 
				
			||||||
            if ctx.command().identifying_name != "finish_macro" {
 | 
					            app_ctx.interaction
 | 
				
			||||||
                let mut lock = ctx.data().recording_macros.write().await;
 | 
					        {
 | 
				
			||||||
 | 
					            if let Some(guild_id) = ctx.guild_id() {
 | 
				
			||||||
 | 
					                if ctx.command().identifying_name != "finish_macro" {
 | 
				
			||||||
 | 
					                    let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
					                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
				
			||||||
                    if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
					                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
				
			||||||
                        let _ = ctx.send(|m| {
 | 
					                            let _ = ctx.send(|m| {
 | 
				
			||||||
                            m.ephemeral(true).content(
 | 
					                            m.ephemeral(true).content(
 | 
				
			||||||
                                format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
					                                format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                        })
 | 
					                        })
 | 
				
			||||||
                            .await;
 | 
					                            .await;
 | 
				
			||||||
                    } else {
 | 
					                        } else {
 | 
				
			||||||
                        let recorded = RecordedCommand {
 | 
					                            let recorded = RecordedCommand {
 | 
				
			||||||
                            action: None,
 | 
					                                action: None,
 | 
				
			||||||
                            command_name: ctx.command().identifying_name.clone(),
 | 
					                                command_name: ctx.command().identifying_name.clone(),
 | 
				
			||||||
                            options: Vec::from(app_ctx.args),
 | 
					                                options: Vec::from(app_ctx.args),
 | 
				
			||||||
                        };
 | 
					                            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        command_macro.commands.push(recorded);
 | 
					                            command_macro.commands.push(recorded);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        let _ = ctx
 | 
					                            let _ = ctx
 | 
				
			||||||
                            .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
					                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
				
			||||||
                            .await;
 | 
					                                .await;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										81
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								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,18 @@ 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(),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
                ..moderation_cmds::macro_base()
 | 
					                ..command_macro::macro_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![moderation_cmds::default_channel()],
 | 
				
			||||||
 | 
					                ..moderation_cmds::default()
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            reminder_cmds::pause(),
 | 
					            reminder_cmds::pause(),
 | 
				
			||||||
            reminder_cmds::offset(),
 | 
					            reminder_cmds::offset(),
 | 
				
			||||||
@@ -167,7 +172,12 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
					        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let popular_timezones = sqlx::query!(
 | 
					    let popular_timezones = sqlx::query!(
 | 
				
			||||||
        "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
					        "SELECT IFNULL(timezone, 'UTC') AS timezone
 | 
				
			||||||
 | 
					        FROM users
 | 
				
			||||||
 | 
					        WHERE timezone IS NOT NULL
 | 
				
			||||||
 | 
					        GROUP BY timezone
 | 
				
			||||||
 | 
					        ORDER BY COUNT(timezone) DESC
 | 
				
			||||||
 | 
					        LIMIT 21"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&database)
 | 
					    .fetch_all(&database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
@@ -176,27 +186,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 {
 | 
				
			||||||
@@ -38,7 +38,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            sqlx::query!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
					INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, ?)
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                channel_id,
 | 
					                channel_id,
 | 
				
			||||||
                channel_name,
 | 
					                channel_name,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,13 +30,20 @@ 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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn guild_command_macro(
 | 
					pub async fn guild_command_macro(
 | 
				
			||||||
    ctx: &Context<'_>,
 | 
					    ctx: &Context<'_>,
 | 
				
			||||||
    name: &str,
 | 
					    name: &str,
 | 
				
			||||||
) -> Option<CommandMacro<Data, Error>> {
 | 
					) -> Option<CommandMacro<Data, Error>> {
 | 
				
			||||||
    let row = sqlx::query!(
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
 | 
					SELECT * FROM macro WHERE guild_id = ? AND name = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        name
 | 
					        name
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										52
									
								
								src/models/guild_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/models/guild_data.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::GuildId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct GuildData {
 | 
				
			||||||
 | 
					    pub id: u64,
 | 
				
			||||||
 | 
					    pub default_channel: Option<u64>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GuildData {
 | 
				
			||||||
 | 
					    pub async fn from_guild(guild: GuildId, pool: &MySqlPool) -> Result<Self, sqlx::Error> {
 | 
				
			||||||
 | 
					        let guild_id = guild.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(row) = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT id, default_channel FROM guilds WHERE id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            guild_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(row)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					INSERT IGNORE INTO guilds (id) VALUES (?)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                guild_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(&pool.clone())
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(Self { id: guild_id, default_channel: None })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) -> Result<(), sqlx::Error> {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					UPDATE guilds SET default_channel = ? WHERE id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.default_channel,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,28 +1,28 @@
 | 
				
			|||||||
pub mod channel_data;
 | 
					pub mod channel_data;
 | 
				
			||||||
pub mod command_macro;
 | 
					pub mod command_macro;
 | 
				
			||||||
 | 
					pub mod guild_data;
 | 
				
			||||||
pub mod reminder;
 | 
					pub mod reminder;
 | 
				
			||||||
pub mod timer;
 | 
					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 log::warn;
 | 
				
			||||||
 | 
					use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelId};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::{channel_data::ChannelData, user_data::UserData},
 | 
					    models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData},
 | 
				
			||||||
    CommandMacro, Context, Data, Error, GuildId,
 | 
					    CommandMacro, Context, Data, Error, GuildId,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
pub trait CtxData {
 | 
					pub trait CtxData {
 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
					    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn author_data(&self) -> Result<UserData, Error>;
 | 
					    async fn author_data(&self) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn timezone(&self) -> Tz;
 | 
					    async fn timezone(&self) -> Tz;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
					    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
				
			||||||
 | 
					    async fn guild_data(&self) -> Option<GuildData>;
 | 
				
			||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
				
			||||||
 | 
					    async fn default_channel(&self) -> Option<ChannelId>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
@@ -51,24 +51,55 @@ impl CtxData for Context<'_> {
 | 
				
			|||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
        self.data().command_macros(self.guild_id().unwrap()).await
 | 
					        self.data().command_macros(self.guild_id().unwrap()).await
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn default_channel(&self) -> Option<ChannelId> {
 | 
				
			||||||
 | 
					        match self.guild_id() {
 | 
				
			||||||
 | 
					            Some(guild_id) => {
 | 
				
			||||||
 | 
					                let guild_data = GuildData::from_guild(guild_id, &self.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match guild_data {
 | 
				
			||||||
 | 
					                    Ok(data) => data.default_channel.map(|c| ChannelId(c)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!("SQL error: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        None
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn guild_data(&self) -> Option<GuildData> {
 | 
				
			||||||
 | 
					        match self.guild_id() {
 | 
				
			||||||
 | 
					            Some(guild_id) => GuildData::from_guild(guild_id, &self.data().database).await.ok(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Data {
 | 
					impl Data {
 | 
				
			||||||
    pub(crate) async fn command_macros(
 | 
					    pub async fn command_macros(
 | 
				
			||||||
        &self,
 | 
					        &self,
 | 
				
			||||||
        guild_id: GuildId,
 | 
					        guild_id: GuildId,
 | 
				
			||||||
    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
        let rows = sqlx::query!(
 | 
					        let rows = sqlx::query!(
 | 
				
			||||||
            "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					            "SELECT name, description, commands FROM macro WHERE guild_id = ?",
 | 
				
			||||||
            guild_id.0
 | 
					            guild_id.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(&self.database)
 | 
					        .fetch_all(&self.database)
 | 
				
			||||||
        .await?.iter().map(|row| CommandMacro {
 | 
					        .await?
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|row| CommandMacro {
 | 
				
			||||||
            guild_id,
 | 
					            guild_id,
 | 
				
			||||||
            name: row.name.clone(),
 | 
					            name: row.name.clone(),
 | 
				
			||||||
            description: row.description.clone(),
 | 
					            description: row.description.clone(),
 | 
				
			||||||
            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
					            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
				
			||||||
        }).collect();
 | 
					        })
 | 
				
			||||||
 | 
					        .collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(rows)
 | 
					        Ok(rows)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -245,7 +245,7 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
    channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
					    channels.guild_id = ?
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    guild_id.as_u64()
 | 
					                    guild_id.as_u64()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
@@ -22,7 +22,7 @@ impl UserData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        match sqlx::query!(
 | 
					        match sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT timezone FROM users WHERE user = ?
 | 
					SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            user_id
 | 
					            user_id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,10 +61,13 @@ pub async fn get_user_info(
 | 
				
			|||||||
            .member(&ctx.inner(), user_id)
 | 
					            .member(&ctx.inner(), user_id)
 | 
				
			||||||
            .await;
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
 | 
					        let timezone = sqlx::query!(
 | 
				
			||||||
            .fetch_one(pool.inner())
 | 
					            "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
 | 
				
			||||||
            .await
 | 
					            user_id
 | 
				
			||||||
            .map_or(None, |q| Some(q.timezone));
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_or(None, |q| Some(q.timezone));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let user_info = UserInfo {
 | 
					        let user_info = UserInfo {
 | 
				
			||||||
            name: cookies
 | 
					            name: cookies
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user