Compare commits
	
		
			8 Commits
		
	
	
		
			jellywx/ma
			...
			jellywx/gu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b0a04bb289 | |||
| eef1f6f3e8 | |||
| 3d08027325 | |||
| 94bfd39085 | |||
| 40cd5f8a36 | |||
| 133b00a2ce | |||
| 57336f5c81 | |||
| b62d24c024 | 
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -2145,7 +2145,7 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "reminder_rs" | name = "reminder_rs" | ||||||
| version = "1.6.5" | version = "1.6.6" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "base64", |  "base64", | ||||||
|  "chrono", |  "chrono", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder_rs" | name = "reminder_rs" | ||||||
| version = "1.6.5" | version = "1.6.6" | ||||||
| authors = ["jellywx <judesouthworth@pm.me>"] | authors = ["jellywx <judesouthworth@pm.me>"] | ||||||
| edition = "2018" | edition = "2018" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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; | ||||||
| @@ -1,6 +1,9 @@ | |||||||
| use chrono_tz::TZ_VARIANTS; | use std::time::{SystemTime, UNIX_EPOCH}; | ||||||
|  |  | ||||||
| use crate::Context; | 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> { | pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> { | ||||||
|     if partial.is_empty() { |     if partial.is_empty() { | ||||||
| @@ -21,7 +24,7 @@ pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Str | |||||||
| SELECT name | SELECT name | ||||||
| FROM macro | FROM macro | ||||||
| WHERE | WHERE | ||||||
|     guild_id = (SELECT id FROM guilds WHERE guild = ?) |     guild_id = ? | ||||||
|     AND name LIKE CONCAT(?, '%')", |     AND name LIKE CONCAT(?, '%')", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         partial, |         partial, | ||||||
| @@ -33,3 +36,96 @@ WHERE | |||||||
|     .map(|s| s.name.clone()) |     .map(|s| s.name.clone()) | ||||||
|     .collect() |     .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(), | ||||||
|  |                 }] | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ pub async fn delete_macro( | |||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     match sqlx::query!( |     match sqlx::query!( | ||||||
|         " |         " | ||||||
| SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?", | SELECT id FROM macro WHERE guild_id = ? AND name = ?", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         name |         name | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -1,38 +0,0 @@ | |||||||
| use poise::serenity_prelude::CommandType; |  | ||||||
|  |  | ||||||
| use crate::{ |  | ||||||
|     commands::autocomplete::macro_name_autocomplete, models::command_macro::guild_command_macro, |  | ||||||
|     Context, Error, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /// Add a macro as a slash-command to this server. Enables controlling permissions per-macro. |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     rename = "install", |  | ||||||
|     guild_only = true, |  | ||||||
|     default_member_permissions = "MANAGE_GUILD", |  | ||||||
|     identifying_name = "install_macro" |  | ||||||
| )] |  | ||||||
| pub async fn install_macro( |  | ||||||
|     ctx: Context<'_>, |  | ||||||
|     #[description = "Name of macro to install"] |  | ||||||
|     #[autocomplete = "macro_name_autocomplete"] |  | ||||||
|     name: String, |  | ||||||
| ) -> Result<(), Error> { |  | ||||||
|     let guild_id = ctx.guild_id().unwrap(); |  | ||||||
|  |  | ||||||
|     if let Some(command_macro) = guild_command_macro(&ctx, &name).await { |  | ||||||
|         guild_id |  | ||||||
|             .create_application_command(&ctx.discord(), |a| { |  | ||||||
|                 a.kind(CommandType::ChatInput) |  | ||||||
|                     .name(command_macro.name) |  | ||||||
|                     .description(command_macro.description.unwrap_or_else(|| "".to_string())) |  | ||||||
|             }) |  | ||||||
|             .await?; |  | ||||||
|         ctx.send(|r| r.ephemeral(true).content("Macro installed. Go to Server Settings 🠚 Integrations 🠚 Reminder Bot to configure permissions.")).await?; |  | ||||||
|     } else { |  | ||||||
|         ctx.send(|r| r.ephemeral(true).content("No macro found with that name")).await?; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
| @@ -2,7 +2,7 @@ use poise::CreateReply; | |||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     component_models::pager::{MacroPager, Pager}, |     component_models::pager::{MacroPager, Pager}, | ||||||
|     consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, |     consts::THEME_COLOR, | ||||||
|     models::{command_macro::CommandMacro, CtxData}, |     models::{command_macro::CommandMacro, CtxData}, | ||||||
|     Context, Error, |     Context, Error, | ||||||
| }; | }; | ||||||
| @@ -30,27 +30,7 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize { | pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize { | ||||||
|     let mut skipped_char_count = 0; |     ((macros.len() as f64) / 25.0).ceil() as usize | ||||||
|  |  | ||||||
|     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 { | pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply { | ||||||
| @@ -75,45 +55,27 @@ pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea | |||||||
|         page = pages - 1; |         page = pages - 1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let mut char_count = 0; |     let lower = (page * 25).min(macros.len()); | ||||||
|     let mut skipped_char_count = 0; |     let upper = ((page + 1) * 25).min(macros.len()); | ||||||
|  |  | ||||||
|     let mut skipped_pages = 0; |     let fields = macros[lower..upper].iter().map(|m| { | ||||||
|  |         if let Some(description) = &m.description { | ||||||
|     let display_vec: Vec<String> = macros |             ( | ||||||
|         .iter() |                 m.name.clone(), | ||||||
|         .map(|m| { |                 format!("*{}*\n- Has {} commands", description, m.commands.len()), | ||||||
|             if let Some(description) = &m.description { |                 true, | ||||||
|                 format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len()) |             ) | ||||||
|             } else { |         } else { | ||||||
|                 format!("**{}**\n- Has {} commands", m.name, m.commands.len()) |             (m.name.clone(), format!("- Has {} commands", m.commands.len()), true) | ||||||
|             } |         } | ||||||
|         }) |     }); | ||||||
|         .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(); |     let mut reply = CreateReply::default(); | ||||||
|  |  | ||||||
|     reply |     reply | ||||||
|         .embed(|e| { |         .embed(|e| { | ||||||
|             e.title("Macros") |             e.title("Macros") | ||||||
|                 .description(display) |                 .fields(fields) | ||||||
|                 .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) |                 .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) | ||||||
|                 .color(*THEME_COLOR) |                 .color(*THEME_COLOR) | ||||||
|         }) |         }) | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|  |  | ||||||
|     let aliases = sqlx::query_as!( |     let aliases = sqlx::query_as!( | ||||||
|         Alias, |         Alias, | ||||||
|         "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |         "SELECT name, command FROM command_aliases WHERE guild_id = ?", | ||||||
|         guild_id.0 |         guild_id.0 | ||||||
|     ) |     ) | ||||||
|     .fetch_all(&mut transaction) |     .fetch_all(&mut transaction) | ||||||
| @@ -36,7 +36,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|         match parse_text_command(guild_id, alias.name, &alias.command) { |         match parse_text_command(guild_id, alias.name, &alias.command) { | ||||||
|             Some(cmd_macro) => { |             Some(cmd_macro) => { | ||||||
|                 sqlx::query!( |                 sqlx::query!( | ||||||
|                     "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)", |                     "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)", | ||||||
|                     cmd_macro.guild_id.0, |                     cmd_macro.guild_id.0, | ||||||
|                     cmd_macro.name, |                     cmd_macro.name, | ||||||
|                     cmd_macro.description, |                     cmd_macro.description, | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| use crate::{Context, Error}; | use crate::{Context, Error}; | ||||||
|  |  | ||||||
| pub mod delete; | pub mod delete; | ||||||
| pub mod install; |  | ||||||
| pub mod list; | pub mod list; | ||||||
| pub mod migrate; | pub mod migrate; | ||||||
| pub mod record; | pub mod record; | ||||||
|   | |||||||
| @@ -15,11 +15,23 @@ pub async fn record_macro( | |||||||
|     #[description = "Name for the new macro"] name: String, |     #[description = "Name for the new macro"] name: String, | ||||||
|     #[description = "Description for the new macro"] description: Option<String>, |     #[description = "Description for the new macro"] description: Option<String>, | ||||||
| ) -> Result<(), Error> { | ) -> 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 guild_id = ctx.guild_id().unwrap(); | ||||||
|  |  | ||||||
|     let row = sqlx::query!( |     let row = sqlx::query!( | ||||||
|         " |         " | ||||||
| SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?", | SELECT 1 as _e FROM macro WHERE guild_id = ? AND name = ?", | ||||||
|         guild_id.0, |         guild_id.0, | ||||||
|         name |         name | ||||||
|     ) |     ) | ||||||
| @@ -109,15 +121,15 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|             let json = serde_json::to_string(&command_macro.commands).unwrap(); |             let json = serde_json::to_string(&command_macro.commands).unwrap(); | ||||||
|  |  | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)", |                 "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)", | ||||||
|                 command_macro.guild_id.0, |                 command_macro.guild_id.0, | ||||||
|                 command_macro.name, |                 command_macro.name, | ||||||
|                 command_macro.description, |                 command_macro.description, | ||||||
|                 json |                 json | ||||||
|             ) |             ) | ||||||
|                 .execute(&ctx.data().database) |             .execute(&ctx.data().database) | ||||||
|                 .await |             .await | ||||||
|                 .unwrap(); |             .unwrap(); | ||||||
|  |  | ||||||
|             ctx.send(|m| { |             ctx.send(|m| { | ||||||
|                 m.embed(|e| { |                 m.embed(|e| { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| pub mod autocomplete; | mod autocomplete; | ||||||
| pub mod command_macro; | pub mod command_macro; | ||||||
| pub mod info_cmds; | pub mod info_cmds; | ||||||
| pub mod moderation_cmds; | pub mod moderation_cmds; | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| 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 log::warn; | ||||||
|  | use poise::serenity_prelude::{ChannelId, Mentionable}; | ||||||
|  |  | ||||||
| use super::autocomplete::timezone_autocomplete; | use super::autocomplete::timezone_autocomplete; | ||||||
| use crate::{consts::THEME_COLOR, models::CtxData, Context, Error}; | use crate::{consts::THEME_COLOR, models::CtxData, Context, Error}; | ||||||
| @@ -147,33 +149,74 @@ pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// View the webhook being used to send reminders to this channel | /// Set defaults for commands | ||||||
| #[poise::command( | #[poise::command( | ||||||
|     slash_command, |     slash_command, | ||||||
|     identifying_name = "webhook_url", |     identifying_name = "default", | ||||||
|     required_permissions = "ADMINISTRATOR" |     default_member_permissions = "MANAGE_GUILD" | ||||||
| )] | )] | ||||||
| pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { | pub async fn default(_ctx: Context<'_>) -> Result<(), Error> { | ||||||
|     match ctx.channel_data().await { |     Ok(()) | ||||||
|         Ok(data) => { | } | ||||||
|             if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { |  | ||||||
|                 let _ = ctx | /// Set a default channel for reminders to be sent to | ||||||
|                     .send(|b| { | #[poise::command( | ||||||
|                         b.ephemeral(true).content(format!( |     slash_command, | ||||||
|                             "**Warning!** |     guild_only = true, | ||||||
| This link can be used by users to anonymously send messages, with or without permissions. |     identifying_name = "default_channel", | ||||||
| Do not share it! |     default_member_permissions = "MANAGE_GUILD" | ||||||
| || https://discord.com/api/webhooks/{}/{} ||", | )] | ||||||
|                             id, token, | pub async fn default_channel( | ||||||
|                         )) |     ctx: Context<'_>, | ||||||
|                     }) |     #[description = "Channel to send reminders to by default"] channel: Option<ChannelId>, | ||||||
|                     .await; | ) -> Result<(), Error> { | ||||||
|             } else { |     if let Some(mut guild_data) = ctx.guild_data().await { | ||||||
|                 let _ = ctx.say("No webhook configured on this channel.").await; |         guild_data.default_channel = channel.map(|c| c.0); | ||||||
|             } |  | ||||||
|         } |         guild_data.commit_changes(&ctx.data().database).await?; | ||||||
|         Err(_) => { |  | ||||||
|             let _ = ctx.say("No webhook configured on this channel.").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 | ||||||
|  | #[poise::command( | ||||||
|  |     slash_command, | ||||||
|  |     identifying_name = "webhook_url", | ||||||
|  |     required_permissions = "ADMINISTRATOR", | ||||||
|  |     default_member_permissions = "ADMINISTRATOR" | ||||||
|  | )] | ||||||
|  | pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { | ||||||
|  |     match ctx.channel_data().await { | ||||||
|  |         Ok(data) => { | ||||||
|  |             if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { | ||||||
|  |                 ctx.send(|b| { | ||||||
|  |                     b.ephemeral(true).content(format!( | ||||||
|  |                         "**Warning!** | ||||||
|  | This link can be used by users to anonymously send messages, with or without permissions. | ||||||
|  | Do not share it! | ||||||
|  | || https://discord.com/api/webhooks/{}/{} ||", | ||||||
|  |                         id, token, | ||||||
|  |                     )) | ||||||
|  |                 }) | ||||||
|  |                 .await?; | ||||||
|  |             } else { | ||||||
|  |                 ctx.say("No webhook configured on this channel.").await?; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Error fetching channel data: {:?}", e); | ||||||
|  |  | ||||||
|  |             ctx.say("No webhook configured on this channel.").await?; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,11 +11,13 @@ use poise::{ | |||||||
|     serenity_prelude::{ |     serenity_prelude::{ | ||||||
|         builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType, |         builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType, | ||||||
|     }, |     }, | ||||||
|     AutocompleteChoice, CreateReply, Modal, |     CreateReply, Modal, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use super::autocomplete::timezone_autocomplete; |  | ||||||
| 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, | ||||||
| @@ -550,20 +552,6 @@ pub async fn delete_timer( | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
| async fn multiline_autocomplete( |  | ||||||
|     _ctx: Context<'_>, |  | ||||||
|     partial: &str, |  | ||||||
| ) -> Vec<AutocompleteChoice<String>> { |  | ||||||
|     if partial.is_empty() { |  | ||||||
|         vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }] |  | ||||||
|     } else { |  | ||||||
|         vec![ |  | ||||||
|             AutocompleteChoice { name: partial.to_string(), value: partial.to_string() }, |  | ||||||
|             AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }, |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(poise::Modal)] | #[derive(poise::Modal)] | ||||||
| #[name = "Reminder"] | #[name = "Reminder"] | ||||||
| struct ContentModal { | struct ContentModal { | ||||||
| @@ -574,7 +562,7 @@ struct ContentModal { | |||||||
|     content: String, |     content: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Create a reminder. Press "+5 more" for other options. A modal will open if "content" is not provided | /// Create a reminder. Press "+4 more" for other options. | ||||||
| #[poise::command( | #[poise::command( | ||||||
|     slash_command, |     slash_command, | ||||||
|     identifying_name = "remind", |     identifying_name = "remind", | ||||||
| @@ -582,7 +570,9 @@ struct ContentModal { | |||||||
| )] | )] | ||||||
| pub async fn remind( | pub async fn remind( | ||||||
|     ctx: ApplicationContext<'_>, |     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"] | ||||||
|  |     #[autocomplete = "time_hint_autocomplete"] | ||||||
|  |     time: String, | ||||||
|     #[description = "The message content to send"] |     #[description = "The message content to send"] | ||||||
|     #[autocomplete = "multiline_autocomplete"] |     #[autocomplete = "multiline_autocomplete"] | ||||||
|     content: String, |     content: String, | ||||||
| @@ -663,7 +653,9 @@ async fn create_reminder( | |||||||
|                 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 | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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_"; | ||||||
|   | |||||||
| @@ -6,7 +6,9 @@ use poise::{ | |||||||
|     serenity_prelude::{model::application::interaction::Interaction, utils::shard_id}, |     serenity_prelude::{model::application::interaction::Interaction, utils::shard_id}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR}; | 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, | ||||||
| @@ -27,7 +29,7 @@ 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 IGNORE INTO guilds (guild) VALUES (?)", guild_id) |                 sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id) | ||||||
|                     .execute(&data.database) |                     .execute(&data.database) | ||||||
|                     .await?; |                     .await?; | ||||||
|  |  | ||||||
| @@ -61,15 +63,27 @@ To stay up to date on the latest features and fixes, join our [Discord](https:// | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         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; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 _ => {} | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         _ => {} |         _ => {} | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ use poise::{ | |||||||
|  |  | ||||||
| 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 recording_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 ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) = |         if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) = | ||||||
|             app_ctx.interaction |             app_ctx.interaction | ||||||
| @@ -95,5 +95,5 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> { | pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> { | ||||||
|     Ok(recording_macro_check(ctx).await && check_self_permissions(ctx).await) |     Ok(macro_check(ctx).await && check_self_permissions(ctx).await) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -117,10 +117,13 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|                     command_macro::record::record_macro(), |                     command_macro::record::record_macro(), | ||||||
|                     command_macro::run::run_macro(), |                     command_macro::run::run_macro(), | ||||||
|                     command_macro::migrate::migrate_macro(), |                     command_macro::migrate::migrate_macro(), | ||||||
|                     command_macro::install::install_macro(), |  | ||||||
|                 ], |                 ], | ||||||
|                 ..command_macro::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(), | ||||||
|             reminder_cmds::nudge(), |             reminder_cmds::nudge(), | ||||||
| @@ -169,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 | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -37,14 +37,13 @@ pub struct RawCommandMacro { | |||||||
|     pub commands: Value, |     pub commands: Value, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get a macro by name form a guild. |  | ||||||
| pub async fn guild_command_macro( | pub async fn guild_command_macro( | ||||||
|     ctx: &Context<'_>, |     ctx: &Context<'_>, | ||||||
|     name: &str, |     name: &str, | ||||||
| ) -> 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_prelude::{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) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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() | ||||||
|                 ) |                 ) | ||||||
|   | |||||||
| @@ -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 | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -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