Link all top-level commands with macro recording/replaying logic
This commit is contained in:
		
							
								
								
									
										13
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -774,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			|||||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 | 
					checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "extract_macro"
 | 
					name = "extract_derive"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "quote",
 | 
					 "quote",
 | 
				
			||||||
@@ -2235,6 +2235,14 @@ dependencies = [
 | 
				
			|||||||
 "getrandom",
 | 
					 "getrandom",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "recordable_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn 2.0.49",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "redox_syscall"
 | 
					name = "redox_syscall"
 | 
				
			||||||
version = "0.4.1"
 | 
					version = "0.4.1"
 | 
				
			||||||
@@ -2317,7 +2325,7 @@ dependencies = [
 | 
				
			|||||||
 "chrono-tz",
 | 
					 "chrono-tz",
 | 
				
			||||||
 "dotenv",
 | 
					 "dotenv",
 | 
				
			||||||
 "env_logger",
 | 
					 "env_logger",
 | 
				
			||||||
 "extract_macro",
 | 
					 "extract_derive",
 | 
				
			||||||
 "lazy-regex",
 | 
					 "lazy-regex",
 | 
				
			||||||
 "lazy_static",
 | 
					 "lazy_static",
 | 
				
			||||||
 "levenshtein",
 | 
					 "levenshtein",
 | 
				
			||||||
@@ -2326,6 +2334,7 @@ dependencies = [
 | 
				
			|||||||
 "poise",
 | 
					 "poise",
 | 
				
			||||||
 "postman",
 | 
					 "postman",
 | 
				
			||||||
 "rand",
 | 
					 "rand",
 | 
				
			||||||
 | 
					 "recordable_derive",
 | 
				
			||||||
 "regex",
 | 
					 "regex",
 | 
				
			||||||
 "reminder_web",
 | 
					 "reminder_web",
 | 
				
			||||||
 "reqwest",
 | 
					 "reqwest",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,8 +35,11 @@ path = "postman"
 | 
				
			|||||||
[dependencies.reminder_web]
 | 
					[dependencies.reminder_web]
 | 
				
			||||||
path = "web"
 | 
					path = "web"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.extract_macro]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "extract_macro"
 | 
					path = "extract_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies.recordable_derive]
 | 
				
			||||||
 | 
					path = "recordable_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[package.metadata.deb]
 | 
					[package.metadata.deb]
 | 
				
			||||||
depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
					depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "extract_macro"
 | 
					name = "extract_derive"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,6 +12,7 @@ fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			|||||||
    let name = &ast.ident;
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match &ast.data {
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        // Dispatch over struct: extract args directly from context
 | 
				
			||||||
        Data::Struct(st) => match &st.fields {
 | 
					        Data::Struct(st) => match &st.fields {
 | 
				
			||||||
            Fields::Named(fields) => {
 | 
					            Fields::Named(fields) => {
 | 
				
			||||||
                let extracted = fields.named.iter().map(|field| {
 | 
					                let extracted = fields.named.iter().map(|field| {
 | 
				
			||||||
							
								
								
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "recordable_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
 | 
				
			||||||
 | 
					#[proc_macro_derive(Recordable)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_recordable(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        Data::Enum(en) => {
 | 
				
			||||||
 | 
					            let extracted = en.variants.iter().map(|var| {
 | 
				
			||||||
 | 
					                let ident = &var.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                quote::quote_spanned! {var.span()=>
 | 
				
			||||||
 | 
					                    Self::#ident (opt) => opt.run(ctx).await?
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Recordable for #name {
 | 
				
			||||||
 | 
					                    async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
 | 
				
			||||||
 | 
					                        match self {
 | 
				
			||||||
 | 
					                            #(#extracted,)*
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Ok(())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only enums can derive Recordable");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,29 +2,35 @@ use chrono::Utc;
 | 
				
			|||||||
use poise::CreateReply;
 | 
					use poise::CreateReply;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{models::CtxData, utils::Extract, Context, Error};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::CtxData,
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    ctx.defer_ephemeral().await?;
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        ctx.defer_ephemeral().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let tz = ctx.timezone().await;
 | 
					        let tz = ctx.timezone().await;
 | 
				
			||||||
    let now = Utc::now().with_timezone(&tz);
 | 
					        let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
					        ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
				
			||||||
        "Time in **{}**: `{}`",
 | 
					            "Time in **{}**: `{}`",
 | 
				
			||||||
        tz,
 | 
					            tz,
 | 
				
			||||||
        now.format("%H:%M")
 | 
					            now.format("%H:%M")
 | 
				
			||||||
    )))
 | 
					        )))
 | 
				
			||||||
    .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// View the current time in your selected timezone
 | 
					/// View the current time in your selected timezone
 | 
				
			||||||
#[poise::command(slash_command, rename = "clock")]
 | 
					#[poise::command(slash_command, rename = "clock", identifying_name = "clock")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    clock(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
use poise::{serenity_prelude::CreateEmbed, CreateReply};
 | 
					use poise::{serenity_prelude::CreateEmbed, CreateReply};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::super::autocomplete::macro_name_autocomplete;
 | 
					use super::super::autocomplete::macro_name_autocomplete;
 | 
				
			||||||
use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::command_macro::guild_command_macro, utils::Recordable, Context, Data, Error,
 | 
				
			||||||
 | 
					    THEME_COLOR,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Run a recorded macro
 | 
					/// Run a recorded macro
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
@@ -32,7 +35,9 @@ pub async fn run_macro(
 | 
				
			|||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for command in command_macro.commands {
 | 
					            for command in command_macro.commands {
 | 
				
			||||||
                command.execute(poise::ApplicationContext { ..ctx }).await?;
 | 
					                command
 | 
				
			||||||
 | 
					                    .run(poise::Context::Application(poise::ApplicationContext { ..ctx }))
 | 
				
			||||||
 | 
					                    .await?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,32 +3,34 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    utils::{footer, Extract},
 | 
					    utils::{footer, Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(
 | 
					        ctx.send(
 | 
				
			||||||
        CreateReply::default().ephemeral(true).embed(
 | 
					            CreateReply::default().ephemeral(true).embed(
 | 
				
			||||||
            CreateEmbed::new()
 | 
					                CreateEmbed::new()
 | 
				
			||||||
                .title("Dashboard")
 | 
					                    .title("Dashboard")
 | 
				
			||||||
                .description("**https://beta.reminder-bot.com/dashboard**")
 | 
					                    .description("**https://beta.reminder-bot.com/dashboard**")
 | 
				
			||||||
                .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                .color(*THEME_COLOR),
 | 
					                    .color(*THEME_COLOR),
 | 
				
			||||||
        ),
 | 
					            ),
 | 
				
			||||||
    )
 | 
					        )
 | 
				
			||||||
    .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get the link to the web dashboard
 | 
					/// Get the link to the web dashboard
 | 
				
			||||||
#[poise::command(slash_command, rename = "dashboard")]
 | 
					#[poise::command(slash_command, rename = "dashboard", identifying_name = "dashboard")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    dashboard(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ use crate::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
				
			||||||
    models::{reminder::Reminder, CtxData},
 | 
					    models::{reminder::Reminder, CtxData},
 | 
				
			||||||
    utils::Extract,
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,21 +140,28 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr
 | 
				
			|||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn delete(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let timezone = ctx.timezone().await;
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let timezone = ctx.timezone().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let reminders =
 | 
					        let reminders =
 | 
				
			||||||
        Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
 | 
					            Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let resp = show_delete_page(&reminders, 0, timezone);
 | 
					        let resp = show_delete_page(&reminders, 0, timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(resp).await?;
 | 
					        ctx.send(resp).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Delete reminders
 | 
					/// Delete reminders
 | 
				
			||||||
#[poise::command(slash_command, rename = "del", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "delete",
 | 
				
			||||||
 | 
					    identifying_name = "delete",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    delete(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,17 +3,18 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    utils::{footer, Extract},
 | 
					    utils::{footer, Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn donate(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
 | 
					        ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
 | 
				
			||||||
            .description("Thinking of adding a monthly contribution?
 | 
					            .description("Thinking of adding a monthly contribution?
 | 
				
			||||||
Click below for my Patreon and official bot server :)
 | 
					Click below for my Patreon and official bot server :)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,11 +37,12 @@ Just $2 USD/month!
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    .await?;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Details on supporting the bot and Patreon benefits
 | 
					/// Details on supporting the bot and Patreon benefits
 | 
				
			||||||
#[poise::command(slash_command, rename = "patreon")]
 | 
					#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    donate(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,23 +3,24 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    utils::{footer, Extract},
 | 
					    utils::{footer, Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn help(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(
 | 
					        ctx.send(
 | 
				
			||||||
        CreateReply::default().ephemeral(true).embed(
 | 
					            CreateReply::default().ephemeral(true).embed(
 | 
				
			||||||
            CreateEmbed::new()
 | 
					                CreateEmbed::new()
 | 
				
			||||||
                .title("Help")
 | 
					                    .title("Help")
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
                .description(
 | 
					                    .description(
 | 
				
			||||||
                    "__Info Commands__
 | 
					                        "__Info Commands__
 | 
				
			||||||
`/help` `/info` `/donate` `/dashboard` `/clock`
 | 
					`/help` `/info` `/donate` `/dashboard` `/clock`
 | 
				
			||||||
*run these commands with no options*
 | 
					*run these commands with no options*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,17 +45,18 @@ __Setup Commands__
 | 
				
			|||||||
__Advanced Commands__
 | 
					__Advanced Commands__
 | 
				
			||||||
`/macro` - Record and replay command sequences
 | 
					`/macro` - Record and replay command sequences
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                )
 | 
					                    )
 | 
				
			||||||
                .footer(footer),
 | 
					                    .footer(footer),
 | 
				
			||||||
        ),
 | 
					            ),
 | 
				
			||||||
    )
 | 
					        )
 | 
				
			||||||
    .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get an overview of bot commands
 | 
					/// Get an overview of bot commands
 | 
				
			||||||
#[poise::command(slash_command, rename = "help")]
 | 
					#[poise::command(slash_command, rename = "help", identifying_name = "help")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    help(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,22 +3,23 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    utils::{footer, Extract},
 | 
					    utils::{footer, Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn info(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(
 | 
					        ctx.send(
 | 
				
			||||||
        CreateReply::default().ephemeral(true).embed(
 | 
					            CreateReply::default().ephemeral(true).embed(
 | 
				
			||||||
            CreateEmbed::new()
 | 
					                CreateEmbed::new()
 | 
				
			||||||
                .title("Info")
 | 
					                    .title("Info")
 | 
				
			||||||
                .description(
 | 
					                    .description(
 | 
				
			||||||
                    "Help: `/help`
 | 
					                        "Help: `/help`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Welcome to Reminder Bot!**
 | 
					**Welcome to Reminder Bot!**
 | 
				
			||||||
Developer: <@203532103185465344>
 | 
					Developer: <@203532103185465344>
 | 
				
			||||||
@@ -27,18 +28,19 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Invite the bot: https://invite.reminder-bot.com/
 | 
					Invite the bot: https://invite.reminder-bot.com/
 | 
				
			||||||
Use our dashboard: https://reminder-bot.com/",
 | 
					Use our dashboard: https://reminder-bot.com/",
 | 
				
			||||||
                )
 | 
					                    )
 | 
				
			||||||
                .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                .color(*THEME_COLOR),
 | 
					                    .color(*THEME_COLOR),
 | 
				
			||||||
        ),
 | 
					            ),
 | 
				
			||||||
    )
 | 
					        )
 | 
				
			||||||
    .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get information about the bot
 | 
					/// Get information about the bot
 | 
				
			||||||
#[poise::command(slash_command, rename = "info")]
 | 
					#[poise::command(slash_command, rename = "info", identifying_name = "info")]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    info(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ use crate::{
 | 
				
			|||||||
    component_models::pager::{LookPager, Pager},
 | 
					    component_models::pager::{LookPager, Pager},
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
    models::{reminder::Reminder, CtxData},
 | 
					    models::{reminder::Reminder, CtxData},
 | 
				
			||||||
    utils::Extract,
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,88 +40,95 @@ pub struct Options {
 | 
				
			|||||||
    relative: Option<bool>,
 | 
					    relative: Option<bool>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let timezone = ctx.timezone().await;
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let timezone = ctx.timezone().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let flags = LookFlags {
 | 
					        let flags = LookFlags {
 | 
				
			||||||
        show_disabled: options.disabled.unwrap_or(true),
 | 
					            show_disabled: self.disabled.unwrap_or(true),
 | 
				
			||||||
        channel_id: options.channel.map(|c| c.id),
 | 
					            channel_id: self.channel.map(|c| c.id),
 | 
				
			||||||
        time_display: options.relative.map_or(TimeDisplayType::Relative, |b| {
 | 
					            time_display: self.relative.map_or(TimeDisplayType::Relative, |b| {
 | 
				
			||||||
            if b {
 | 
					                if b {
 | 
				
			||||||
                TimeDisplayType::Relative
 | 
					                    TimeDisplayType::Relative
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    TimeDisplayType::Absolute
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
 | 
				
			||||||
 | 
					            if Some(channel.guild_id) == ctx.guild_id() {
 | 
				
			||||||
 | 
					                flags.channel_id.unwrap_or_else(|| ctx.channel_id())
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                TimeDisplayType::Absolute
 | 
					                ctx.channel_id()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
 | 
					 | 
				
			||||||
        if Some(channel.guild_id) == ctx.guild_id() {
 | 
					 | 
				
			||||||
            flags.channel_id.unwrap_or_else(|| ctx.channel_id())
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            ctx.channel_id()
 | 
					            ctx.channel_id()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let channel_name =
 | 
				
			||||||
 | 
					            channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reminders.is_empty() {
 | 
				
			||||||
 | 
					            let _ = ctx.say("No reminders on specified channel").await;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let mut char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let display = reminders
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|reminder| reminder.display(&flags, &timezone))
 | 
				
			||||||
 | 
					                .take_while(|p| {
 | 
				
			||||||
 | 
					                    char_count += p.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect::<Vec<String>>()
 | 
				
			||||||
 | 
					                .join("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let pages = reminders
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|reminder| reminder.display(&flags, &timezone))
 | 
				
			||||||
 | 
					                .fold(0, |t, r| t + r.len())
 | 
				
			||||||
 | 
					                .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let pager = LookPager::new(flags, timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.send(
 | 
				
			||||||
 | 
					                CreateReply::default()
 | 
				
			||||||
 | 
					                    .ephemeral(true)
 | 
				
			||||||
 | 
					                    .embed(
 | 
				
			||||||
 | 
					                        CreateEmbed::new()
 | 
				
			||||||
 | 
					                            .title(format!(
 | 
				
			||||||
 | 
					                                "Reminders{}",
 | 
				
			||||||
 | 
					                                channel_name.map_or(String::new(), |n| format!(" on #{}", n))
 | 
				
			||||||
 | 
					                            ))
 | 
				
			||||||
 | 
					                            .description(display)
 | 
				
			||||||
 | 
					                            .footer(CreateEmbedFooter::new(format!("Page {} of {}", 1, pages)))
 | 
				
			||||||
 | 
					                            .color(*THEME_COLOR),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .components(vec![pager.create_button_row(pages)]),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        ctx.channel_id()
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let channel_name =
 | 
					        Ok(())
 | 
				
			||||||
        channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if reminders.is_empty() {
 | 
					 | 
				
			||||||
        let _ = ctx.say("No reminders on specified channel").await;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let mut char_count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let display = reminders
 | 
					 | 
				
			||||||
            .iter()
 | 
					 | 
				
			||||||
            .map(|reminder| reminder.display(&flags, &timezone))
 | 
					 | 
				
			||||||
            .take_while(|p| {
 | 
					 | 
				
			||||||
                char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
            .join("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let pages = reminders
 | 
					 | 
				
			||||||
            .iter()
 | 
					 | 
				
			||||||
            .map(|reminder| reminder.display(&flags, &timezone))
 | 
					 | 
				
			||||||
            .fold(0, |t, r| t + r.len())
 | 
					 | 
				
			||||||
            .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let pager = LookPager::new(flags, timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ctx.send(
 | 
					 | 
				
			||||||
            CreateReply::default()
 | 
					 | 
				
			||||||
                .ephemeral(true)
 | 
					 | 
				
			||||||
                .embed(
 | 
					 | 
				
			||||||
                    CreateEmbed::new()
 | 
					 | 
				
			||||||
                        .title(format!(
 | 
					 | 
				
			||||||
                            "Reminders{}",
 | 
					 | 
				
			||||||
                            channel_name.map_or(String::new(), |n| format!(" on #{}", n))
 | 
					 | 
				
			||||||
                        ))
 | 
					 | 
				
			||||||
                        .description(display)
 | 
					 | 
				
			||||||
                        .footer(CreateEmbedFooter::new(format!("Page {} of {}", 1, pages)))
 | 
					 | 
				
			||||||
                        .color(*THEME_COLOR),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .components(vec![pager.create_button_row(pages)]),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .await?;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// View reminders on a specific channel
 | 
					/// View reminders on a specific channel
 | 
				
			||||||
#[poise::command(slash_command, rename = "look", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "look",
 | 
				
			||||||
 | 
					    identifying_name = "look",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Channel to view reminders on"] channel: Option<PartialChannel>,
 | 
					    #[description = "Channel to view reminders on"] channel: Option<PartialChannel>,
 | 
				
			||||||
    #[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
 | 
					    #[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
 | 
				
			||||||
    #[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
 | 
					    #[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    look(ctx, Options { channel, disabled, relative }).await
 | 
					    (Options { channel, disabled, relative }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
					    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
				
			||||||
    models::reminder::create_reminder,
 | 
					    models::reminder::create_reminder,
 | 
				
			||||||
    utils::Extract,
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,49 +30,58 @@ pub struct Options {
 | 
				
			|||||||
    timezone: Option<String>,
 | 
					    timezone: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    match ctx {
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
        Context::Application(app_ctx) => {
 | 
					        match ctx {
 | 
				
			||||||
            let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
					            Context::Application(app_ctx) => {
 | 
				
			||||||
            let data_opt = ContentModal::execute(app_ctx).await?;
 | 
					                let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
				
			||||||
 | 
					                let data_opt = ContentModal::execute(app_ctx).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            match data_opt {
 | 
					                match data_opt {
 | 
				
			||||||
                Some(data) => {
 | 
					                    Some(data) => {
 | 
				
			||||||
                    create_reminder(
 | 
					                        create_reminder(
 | 
				
			||||||
                        ctx,
 | 
					                            ctx,
 | 
				
			||||||
                        options.time,
 | 
					                            self.time,
 | 
				
			||||||
                        data.content,
 | 
					                            data.content,
 | 
				
			||||||
                        options.channels,
 | 
					                            self.channels,
 | 
				
			||||||
                        options.interval,
 | 
					                            self.interval,
 | 
				
			||||||
                        options.expires,
 | 
					                            self.expires,
 | 
				
			||||||
                        options.tts,
 | 
					                            self.tts,
 | 
				
			||||||
                        tz,
 | 
					                            tz,
 | 
				
			||||||
                    )
 | 
					                        )
 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                None => {
 | 
					 | 
				
			||||||
                    warn!("Unexpected None encountered in /multiline");
 | 
					 | 
				
			||||||
                    Ok(ctx
 | 
					 | 
				
			||||||
                        .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
					 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .map(|_| ())?)
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    None => {
 | 
				
			||||||
 | 
					                        warn!("Unexpected None encountered in /multiline");
 | 
				
			||||||
 | 
					                        Ok(ctx
 | 
				
			||||||
 | 
					                            .send(
 | 
				
			||||||
 | 
					                                CreateReply::default().content("Unexpected error.").ephemeral(true),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .map(|_| ())?)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _ => {
 | 
					            _ => {
 | 
				
			||||||
            warn!("Shouldn't be here");
 | 
					                warn!("Shouldn't be here");
 | 
				
			||||||
            Ok(ctx
 | 
					                Ok(ctx
 | 
				
			||||||
                .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
					                    .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
				
			||||||
                .await
 | 
					                    .await
 | 
				
			||||||
                .map(|_| ())?)
 | 
					                    .map(|_| ())?)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a reminder with multi-line content. Press "+4 more" for other options.
 | 
					/// Create a reminder with multi-line content. Press "+4 more" for other options.
 | 
				
			||||||
#[poise::command(slash_command, rename = "multiline", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "multiline",
 | 
				
			||||||
 | 
					    identifying_name = "multiline",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "A description of the time to set the reminder for"]
 | 
					    #[description = "A description of the time to set the reminder for"]
 | 
				
			||||||
@@ -89,5 +98,5 @@ pub async fn command(
 | 
				
			|||||||
    #[autocomplete = "timezone_autocomplete"]
 | 
					    #[autocomplete = "timezone_autocomplete"]
 | 
				
			||||||
    timezone: Option<String>,
 | 
					    timezone: Option<String>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    multiline(ctx, Options { time, channels, interval, expires, tts, timezone }).await
 | 
					    (Options { time, channels, interval, expires, tts, timezone }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,11 @@
 | 
				
			|||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::MINUTE, models::CtxData, utils::Extract, Context, Error};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    consts::MINUTE,
 | 
				
			||||||
 | 
					    models::CtxData,
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options {
 | 
					pub struct Options {
 | 
				
			||||||
@@ -8,30 +13,38 @@ pub struct Options {
 | 
				
			|||||||
    seconds: Option<i64>,
 | 
					    seconds: Option<i64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let combined_time =
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
        options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s);
 | 
					        let combined_time =
 | 
				
			||||||
 | 
					            self.minutes.map_or(0, |m| m * MINUTE as i64) + self.seconds.map_or(0, |s| s);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 {
 | 
					        if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 {
 | 
				
			||||||
        ctx.say("Nudge times must be less than 500 minutes").await?;
 | 
					            ctx.say("Nudge times must be less than 500 minutes").await?;
 | 
				
			||||||
    } else {
 | 
					        } else {
 | 
				
			||||||
        let mut channel_data = ctx.channel_data().await.unwrap();
 | 
					            let mut channel_data = ctx.channel_data().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        channel_data.nudge = combined_time as i16;
 | 
					            channel_data.nudge = combined_time as i16;
 | 
				
			||||||
        channel_data.commit_changes(&ctx.data().database).await;
 | 
					            channel_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.say(format!("Future reminders will be nudged by {} seconds", combined_time)).await?;
 | 
					            ctx.say(format!("Future reminders will be nudged by {} seconds", combined_time))
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)
 | 
					/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)
 | 
				
			||||||
#[poise::command(slash_command, rename = "nudge", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "nudge",
 | 
				
			||||||
 | 
					    identifying_name = "nudge",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>,
 | 
					    #[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>,
 | 
				
			||||||
    #[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>,
 | 
					    #[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    nudge(ctx, Options { minutes, seconds }).await
 | 
					    (Options { minutes, seconds }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::{HOUR, MINUTE},
 | 
					    consts::{HOUR, MINUTE},
 | 
				
			||||||
    utils::Extract,
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,69 +13,76 @@ pub struct Options {
 | 
				
			|||||||
    seconds: Option<i64>,
 | 
					    seconds: Option<i64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    ctx.defer().await?;
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        ctx.defer().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let combined_time = options.hours.map_or(0, |h| h * HOUR as i64)
 | 
					        let combined_time = self.hours.map_or(0, |h| h * HOUR as i64)
 | 
				
			||||||
        + options.minutes.map_or(0, |m| m * MINUTE as i64)
 | 
					            + self.minutes.map_or(0, |m| m * MINUTE as i64)
 | 
				
			||||||
        + options.seconds.map_or(0, |s| s);
 | 
					            + self.seconds.map_or(0, |s| s);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if combined_time == 0 {
 | 
					        if combined_time == 0 {
 | 
				
			||||||
        ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
 | 
					            ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
 | 
				
			||||||
    } else {
 | 
					        } else {
 | 
				
			||||||
        if let Some(channels) = ctx.guild().map(|guild| {
 | 
					            if let Some(channels) = ctx.guild().map(|guild| {
 | 
				
			||||||
            guild
 | 
					                guild
 | 
				
			||||||
                .channels
 | 
					                    .channels
 | 
				
			||||||
                .iter()
 | 
					                    .iter()
 | 
				
			||||||
                .filter(|(_, channel)| channel.is_text_based())
 | 
					                    .filter(|(_, channel)| channel.is_text_based())
 | 
				
			||||||
                .map(|(id, _)| id.get().to_string())
 | 
					                    .map(|(id, _)| id.get().to_string())
 | 
				
			||||||
                .collect::<Vec<String>>()
 | 
					                    .collect::<Vec<String>>()
 | 
				
			||||||
                .join(",")
 | 
					                    .join(",")
 | 
				
			||||||
        }) {
 | 
					            }) {
 | 
				
			||||||
            sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                "
 | 
					                    "
 | 
				
			||||||
                UPDATE reminders
 | 
					                UPDATE reminders
 | 
				
			||||||
                INNER JOIN `channels`
 | 
					                INNER JOIN `channels`
 | 
				
			||||||
                    ON `channels`.id = reminders.channel_id
 | 
					                    ON `channels`.id = reminders.channel_id
 | 
				
			||||||
                SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)
 | 
					                SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)
 | 
				
			||||||
                WHERE FIND_IN_SET(channels.`channel`, ?)
 | 
					                WHERE FIND_IN_SET(channels.`channel`, ?)
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                combined_time as i64,
 | 
					                    combined_time as i64,
 | 
				
			||||||
                channels
 | 
					                    channels
 | 
				
			||||||
            )
 | 
					                )
 | 
				
			||||||
            .execute(&ctx.data().database)
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
            .await
 | 
					                .await
 | 
				
			||||||
            .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
        } else {
 | 
					            } else {
 | 
				
			||||||
            sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                "
 | 
					                    "
 | 
				
			||||||
                UPDATE reminders
 | 
					                UPDATE reminders
 | 
				
			||||||
                INNER JOIN `channels`
 | 
					                INNER JOIN `channels`
 | 
				
			||||||
                    ON `channels`.id = reminders.channel_id
 | 
					                    ON `channels`.id = reminders.channel_id
 | 
				
			||||||
                SET reminders.`utc_time` = reminders.`utc_time` + ?
 | 
					                SET reminders.`utc_time` = reminders.`utc_time` + ?
 | 
				
			||||||
                WHERE channels.`channel` = ?
 | 
					                WHERE channels.`channel` = ?
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                combined_time as i64,
 | 
					                    combined_time as i64,
 | 
				
			||||||
                ctx.channel_id().get()
 | 
					                    ctx.channel_id().get()
 | 
				
			||||||
            )
 | 
					                )
 | 
				
			||||||
            .execute(&ctx.data().database)
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
            .await
 | 
					                .await
 | 
				
			||||||
            .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?;
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Move all reminders in the current server by a certain amount of time. Times get added together
 | 
					/// Move all reminders in the current server by a certain amount of time. Times get added together
 | 
				
			||||||
#[poise::command(slash_command, rename = "offset", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "offset",
 | 
				
			||||||
 | 
					    identifying_name = "offset",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Number of hours to offset by"] hours: Option<i64>,
 | 
					    #[description = "Number of hours to offset by"] hours: Option<i64>,
 | 
				
			||||||
    #[description = "Number of minutes to offset by"] minutes: Option<i64>,
 | 
					    #[description = "Number of minutes to offset by"] minutes: Option<i64>,
 | 
				
			||||||
    #[description = "Number of seconds to offset by"] seconds: Option<i64>,
 | 
					    #[description = "Number of seconds to offset by"] seconds: Option<i64>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    offset(ctx, Options { hours, minutes, seconds }).await
 | 
					    (Options { hours, minutes, seconds }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,73 +1,85 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{models::CtxData, time_parser::natural_parser, utils::Extract, Context, Error};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::CtxData,
 | 
				
			||||||
 | 
					    time_parser::natural_parser,
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options {
 | 
					pub struct Options {
 | 
				
			||||||
    until: Option<String>,
 | 
					    until: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let timezone = ctx.timezone().await;
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let timezone = ctx.timezone().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut channel = ctx.channel_data().await.unwrap();
 | 
					        let mut channel = ctx.channel_data().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match options.until {
 | 
					        match self.until {
 | 
				
			||||||
        Some(until) => {
 | 
					            Some(until) => {
 | 
				
			||||||
            let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
					                let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Some(timestamp) = parsed {
 | 
					                if let Some(timestamp) = parsed {
 | 
				
			||||||
                match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
 | 
					                    match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
 | 
				
			||||||
                    Some(dt) => {
 | 
					                        Some(dt) => {
 | 
				
			||||||
                        channel.paused = true;
 | 
					                            channel.paused = true;
 | 
				
			||||||
                        channel.paused_until = Some(dt);
 | 
					                            channel.paused_until = Some(dt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        channel.commit_changes(&ctx.data().database).await;
 | 
					                            channel.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        ctx.say(format!(
 | 
					                            ctx.say(format!(
 | 
				
			||||||
                            "Reminders in this channel have been silenced until **<t:{}:D>**",
 | 
					                                "Reminders in this channel have been silenced until **<t:{}:D>**",
 | 
				
			||||||
                            timestamp
 | 
					                                timestamp
 | 
				
			||||||
                        ))
 | 
					                            ))
 | 
				
			||||||
                        .await?;
 | 
					                            .await?;
 | 
				
			||||||
                    }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    None => {
 | 
					                        None => {
 | 
				
			||||||
                        ctx.say(
 | 
					                            ctx.say(
 | 
				
			||||||
                            "Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible",
 | 
					                            "Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible",
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .await?;
 | 
					                        .await?;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    ctx.say(
 | 
				
			||||||
 | 
					                        "Time could not be processed. Please write the time as clearly as possible",
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                channel.paused = !channel.paused;
 | 
				
			||||||
 | 
					                channel.paused_until = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                channel.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if channel.paused {
 | 
				
			||||||
 | 
					                    ctx.say("Reminders in this channel have been silenced indefinitely").await?;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    ctx.say("Reminders in this channel have been unsilenced").await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                ctx.say(
 | 
					 | 
				
			||||||
                    "Time could not be processed. Please write the time as clearly as possible",
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await?;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        _ => {
 | 
					 | 
				
			||||||
            channel.paused = !channel.paused;
 | 
					 | 
				
			||||||
            channel.paused_until = None;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            channel.commit_changes(&ctx.data().database).await;
 | 
					        Ok(())
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if channel.paused {
 | 
					 | 
				
			||||||
                ctx.say("Reminders in this channel have been silenced indefinitely").await?;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                ctx.say("Reminders in this channel have been unsilenced").await?;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// 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
 | 
				
			||||||
#[poise::command(slash_command, rename = "pause", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "pause",
 | 
				
			||||||
 | 
					    identifying_name = "pause",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "When to pause until"] until: Option<String>,
 | 
					    #[description = "When to pause until"] until: Option<String>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    pause(ctx, Options { until }).await
 | 
					    (Options { until }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
					    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
				
			||||||
    models::reminder::create_reminder,
 | 
					    models::reminder::create_reminder,
 | 
				
			||||||
    utils::Extract,
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,24 +19,31 @@ pub struct Options {
 | 
				
			|||||||
    timezone: Option<String>,
 | 
					    timezone: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    create_reminder(
 | 
					        create_reminder(
 | 
				
			||||||
        ctx,
 | 
					            ctx,
 | 
				
			||||||
        options.time,
 | 
					            self.time,
 | 
				
			||||||
        options.content,
 | 
					            self.content,
 | 
				
			||||||
        options.channels,
 | 
					            self.channels,
 | 
				
			||||||
        options.interval,
 | 
					            self.interval,
 | 
				
			||||||
        options.expires,
 | 
					            self.expires,
 | 
				
			||||||
        options.tts,
 | 
					            self.tts,
 | 
				
			||||||
        tz,
 | 
					            tz,
 | 
				
			||||||
    )
 | 
					        )
 | 
				
			||||||
    .await
 | 
					        .await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content.
 | 
					/// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content.
 | 
				
			||||||
#[poise::command(slash_command, rename = "remind", default_member_permissions = "MANAGE_GUILD")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "remind",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "remind"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "The time (and optionally date) to set the reminder for"]
 | 
					    #[description = "The time (and optionally date) to set the reminder for"]
 | 
				
			||||||
@@ -54,5 +61,5 @@ pub async fn command(
 | 
				
			|||||||
    #[autocomplete = "timezone_autocomplete"]
 | 
					    #[autocomplete = "timezone_autocomplete"]
 | 
				
			||||||
    timezone: Option<String>,
 | 
					    timezone: Option<String>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    remind(ctx, Options { time, content, channels, interval, expires, tts, timezone }).await
 | 
					    (Options { time, content, channels, interval, expires, tts, timezone }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,11 @@ use poise::{
 | 
				
			|||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData,
 | 
					    commands::autocomplete::timezone_autocomplete,
 | 
				
			||||||
    utils::Extract, Context, Error,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
 | 
					    models::CtxData,
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
@@ -17,55 +20,56 @@ pub struct Options {
 | 
				
			|||||||
    pub timezone: Option<String>,
 | 
					    pub timezone: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    let mut user_data = ctx.author_data().await.unwrap();
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let mut user_data = ctx.author_data().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let footer_text = format!("Current timezone: {}", user_data.timezone);
 | 
					        let footer_text = format!("Current timezone: {}", user_data.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Some(timezone) = options.timezone {
 | 
					        if let Some(timezone) = self.timezone {
 | 
				
			||||||
        match timezone.parse::<Tz>() {
 | 
					            match timezone.parse::<Tz>() {
 | 
				
			||||||
            Ok(tz) => {
 | 
					                Ok(tz) => {
 | 
				
			||||||
                user_data.timezone = timezone.clone();
 | 
					                    user_data.timezone = timezone.clone();
 | 
				
			||||||
                user_data.commit_changes(&ctx.data().database).await;
 | 
					                    user_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let now = Utc::now().with_timezone(&tz);
 | 
					                    let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                ctx.send(
 | 
					                    ctx.send(
 | 
				
			||||||
                    CreateReply::default().embed(
 | 
					                        CreateReply::default().embed(
 | 
				
			||||||
                        CreateEmbed::new()
 | 
					                            CreateEmbed::new()
 | 
				
			||||||
                            .title("Timezone Set")
 | 
					                                .title("Timezone Set")
 | 
				
			||||||
                            .description(format!(
 | 
					                                .description(format!(
 | 
				
			||||||
                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
					                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
				
			||||||
                                timezone,
 | 
					                                timezone,
 | 
				
			||||||
                                now.format("%H:%M")
 | 
					                                now.format("%H:%M")
 | 
				
			||||||
                            ))
 | 
					                            ))
 | 
				
			||||||
                            .color(*THEME_COLOR),
 | 
					                                .color(*THEME_COLOR),
 | 
				
			||||||
                    ),
 | 
					                        ),
 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await?;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(_) => {
 | 
					 | 
				
			||||||
                let filtered_tz = TZ_VARIANTS
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .filter(|tz| {
 | 
					 | 
				
			||||||
                        timezone.contains(&tz.to_string())
 | 
					 | 
				
			||||||
                            || tz.to_string().contains(&timezone)
 | 
					 | 
				
			||||||
                            || levenshtein(&tz.to_string(), &timezone) < 4
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .take(25)
 | 
					 | 
				
			||||||
                    .map(|t| t.to_owned())
 | 
					 | 
				
			||||||
                    .collect::<Vec<Tz>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let fields = filtered_tz.iter().map(|tz| {
 | 
					 | 
				
			||||||
                    (
 | 
					 | 
				
			||||||
                        tz.to_string(),
 | 
					 | 
				
			||||||
                        format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
 | 
					 | 
				
			||||||
                        true,
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                });
 | 
					                    .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                ctx.send(
 | 
					                Err(_) => {
 | 
				
			||||||
 | 
					                    let filtered_tz = TZ_VARIANTS
 | 
				
			||||||
 | 
					                        .iter()
 | 
				
			||||||
 | 
					                        .filter(|tz| {
 | 
				
			||||||
 | 
					                            timezone.contains(&tz.to_string())
 | 
				
			||||||
 | 
					                                || tz.to_string().contains(&timezone)
 | 
				
			||||||
 | 
					                                || levenshtein(&tz.to_string(), &timezone) < 4
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .take(25)
 | 
				
			||||||
 | 
					                        .map(|t| t.to_owned())
 | 
				
			||||||
 | 
					                        .collect::<Vec<Tz>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let fields = filtered_tz.iter().map(|tz| {
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            tz.to_string(),
 | 
				
			||||||
 | 
					                            format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ctx.send(
 | 
				
			||||||
                    CreateReply::default().embed(
 | 
					                    CreateReply::default().embed(
 | 
				
			||||||
                        CreateEmbed::new()
 | 
					                        CreateEmbed::new()
 | 
				
			||||||
                            .title("Timezone Not Recognized")
 | 
					                            .title("Timezone Not Recognized")
 | 
				
			||||||
@@ -83,14 +87,18 @@ pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        } else {
 | 
				
			||||||
    } else {
 | 
					            let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
				
			||||||
        let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
					                (
 | 
				
			||||||
            (t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true)
 | 
					                    t.to_string(),
 | 
				
			||||||
        });
 | 
					                    format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")),
 | 
				
			||||||
 | 
					                    true,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.send(
 | 
					            ctx.send(
 | 
				
			||||||
            CreateReply::default().embed(
 | 
					            CreateReply::default().embed(
 | 
				
			||||||
                CreateEmbed::new()
 | 
					                CreateEmbed::new()
 | 
				
			||||||
                    .title("Timezone Usage")
 | 
					                    .title("Timezone Usage")
 | 
				
			||||||
@@ -110,18 +118,19 @@ You may want to use one of the popular timezones below, otherwise click [here](h
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
    }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Select your timezone
 | 
					/// Select your timezone
 | 
				
			||||||
#[poise::command(slash_command, rename = "timezone")]
 | 
					#[poise::command(slash_command, rename = "timezone", identifying_name = "timezone")]
 | 
				
			||||||
pub async fn command(
 | 
					pub async fn command(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
					    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
				
			||||||
    #[autocomplete = "timezone_autocomplete"]
 | 
					    #[autocomplete = "timezone_autocomplete"]
 | 
				
			||||||
    timezone: Option<String>,
 | 
					    timezone: Option<String>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    timezone_fn(ctx, Options { timezone }).await
 | 
					    (Options { timezone }).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,39 +2,50 @@ use log::warn;
 | 
				
			|||||||
use poise::CreateReply;
 | 
					use poise::CreateReply;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{models::CtxData, utils::Extract, Context, Error};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::CtxData,
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Extract)]
 | 
					#[derive(Serialize, Deserialize, Extract)]
 | 
				
			||||||
pub struct Options;
 | 
					pub struct Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
					impl Recordable for Options {
 | 
				
			||||||
    match ctx.channel_data().await {
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
        Ok(data) => {
 | 
					        match ctx.channel_data().await {
 | 
				
			||||||
            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
					            Ok(data) => {
 | 
				
			||||||
                ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
					                if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
				
			||||||
                    "**Warning!**
 | 
					                    ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
				
			||||||
 | 
					                        "**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 {
 | 
				
			||||||
 | 
					                    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?;
 | 
					                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?;
 | 
					        Ok(())
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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(slash_command, rename = "webhook", required_permissions = "ADMINISTRATOR")]
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "webhook",
 | 
				
			||||||
 | 
					    identifying_name = "webhook",
 | 
				
			||||||
 | 
					    required_permissions = "ADMINISTRATOR"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    webhook(ctx, Options {}).await
 | 
					    (Options {}).run(ctx).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -13,18 +13,6 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                let mut lock = ctx.data().recording_macros.write().await;
 | 
					                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 ctx.command().identifying_name != "remind" {
 | 
					 | 
				
			||||||
                        let _ = ctx
 | 
					 | 
				
			||||||
                            .send(
 | 
					 | 
				
			||||||
                                CreateReply::default()
 | 
					 | 
				
			||||||
                                    .ephemeral(true)
 | 
					 | 
				
			||||||
                                    .content("Macro recording only supports `/remind`. Please stop recording with `/macro finish` before using other commands.")
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        return false;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
					                    if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
				
			||||||
                        let _ = ctx
 | 
					                        let _ = ctx
 | 
				
			||||||
                            .send(
 | 
					                            .send(
 | 
				
			||||||
@@ -34,16 +22,29 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
                            )
 | 
					                            )
 | 
				
			||||||
                            .await;
 | 
					                            .await;
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        let recorded = RecordedCommand::from_context(app_ctx).unwrap();
 | 
					                        match RecordedCommand::from_context(app_ctx) {
 | 
				
			||||||
                        command_macro.commands.push(recorded);
 | 
					                            Some(recorded) => {
 | 
				
			||||||
 | 
					                                command_macro.commands.push(recorded);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        let _ = ctx
 | 
					                                let _ = ctx
 | 
				
			||||||
                            .send(
 | 
					                                    .send(
 | 
				
			||||||
                                CreateReply::default()
 | 
					                                        CreateReply::default()
 | 
				
			||||||
                                    .ephemeral(true)
 | 
					                                            .ephemeral(true)
 | 
				
			||||||
                                    .content("Command recorded to macro"),
 | 
					                                            .content("Command recorded to macro"),
 | 
				
			||||||
                            )
 | 
					                                    )
 | 
				
			||||||
                            .await;
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            None => {
 | 
				
			||||||
 | 
					                                let _ = ctx
 | 
				
			||||||
 | 
					                                    .send(
 | 
				
			||||||
 | 
					                                        CreateReply::default().ephemeral(true).content(
 | 
				
			||||||
 | 
					                                            "This command is not supported in macros yet.",
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return false;
 | 
					                    return false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,29 +2,64 @@ use poise::serenity_prelude::model::id::GuildId;
 | 
				
			|||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_json::Value;
 | 
					use serde_json::Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{commands::remind, utils::Extract, ApplicationContext, Context, Error};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    utils::{Extract, Recordable},
 | 
				
			||||||
 | 
					    ApplicationContext, Context,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					#[derive(Serialize, Deserialize, Recordable)]
 | 
				
			||||||
#[serde(tag = "command_name")]
 | 
					#[serde(tag = "command_name")]
 | 
				
			||||||
pub enum RecordedCommand {
 | 
					pub enum RecordedCommand {
 | 
				
			||||||
    Remind(remind::Options),
 | 
					    #[serde(rename = "clock")]
 | 
				
			||||||
 | 
					    Clock(crate::commands::clock::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "dashboard")]
 | 
				
			||||||
 | 
					    Dashboard(crate::commands::dashboard::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "delete")]
 | 
				
			||||||
 | 
					    Delete(crate::commands::delete::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "donate")]
 | 
				
			||||||
 | 
					    Donate(crate::commands::donate::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "help")]
 | 
				
			||||||
 | 
					    Help(crate::commands::help::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "info")]
 | 
				
			||||||
 | 
					    Info(crate::commands::info::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "look")]
 | 
				
			||||||
 | 
					    Look(crate::commands::look::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "multiline")]
 | 
				
			||||||
 | 
					    Multiline(crate::commands::multiline::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "nudge")]
 | 
				
			||||||
 | 
					    Nudge(crate::commands::nudge::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "offset")]
 | 
				
			||||||
 | 
					    Offset(crate::commands::offset::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "pause")]
 | 
				
			||||||
 | 
					    Pause(crate::commands::pause::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "remind")]
 | 
				
			||||||
 | 
					    Remind(crate::commands::remind::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "timezone")]
 | 
				
			||||||
 | 
					    Timezone(crate::commands::timezone::Options),
 | 
				
			||||||
 | 
					    #[serde(rename = "webhook")]
 | 
				
			||||||
 | 
					    Webhook(crate::commands::webhook::Options),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl RecordedCommand {
 | 
					impl RecordedCommand {
 | 
				
			||||||
    pub fn from_context(ctx: ApplicationContext) -> Option<Self> {
 | 
					    pub fn from_context(ctx: ApplicationContext) -> Option<Self> {
 | 
				
			||||||
        match ctx.command().identifying_name.as_str() {
 | 
					        match ctx.command().identifying_name.as_str() {
 | 
				
			||||||
            "remind" => Some(Self::Remind(remind::Options::extract(ctx))),
 | 
					            "clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "multiline" => Some(Self::Multiline(crate::commands::multiline::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "nudge" => Some(Self::Nudge(crate::commands::nudge::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "offset" => Some(Self::Offset(crate::commands::offset::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "pause" => Some(Self::Pause(crate::commands::pause::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "remind" => Some(Self::Remind(crate::commands::remind::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "timezone" => Some(Self::Timezone(crate::commands::timezone::Options::extract(ctx))),
 | 
				
			||||||
 | 
					            "webhook" => Some(Self::Webhook(crate::commands::webhook::Options::extract(ctx))),
 | 
				
			||||||
            _ => None,
 | 
					            _ => None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn execute(self, ctx: ApplicationContext<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            RecordedCommand::Remind(options) => {
 | 
					 | 
				
			||||||
                remind::remind(Context::Application(ctx), options).await
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct CommandMacro {
 | 
					pub struct CommandMacro {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							@@ -9,7 +9,7 @@ use poise::{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
 | 
					    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
 | 
				
			||||||
    ApplicationContext, Context,
 | 
					    ApplicationContext, Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
					pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
				
			||||||
@@ -65,11 +65,17 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter {
 | 
				
			|||||||
    ))
 | 
					    ))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait Recordable {
 | 
				
			||||||
 | 
					    async fn run(self, ctx: Context<'_>) -> Result<(), Error>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use recordable_derive::Recordable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub trait Extract {
 | 
					pub trait Extract {
 | 
				
			||||||
    fn extract(ctx: ApplicationContext) -> Self;
 | 
					    fn extract(ctx: ApplicationContext) -> Self;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use extract_macro::Extract;
 | 
					pub use extract_derive::Extract;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
macro_rules! extract_arg {
 | 
					macro_rules! extract_arg {
 | 
				
			||||||
    ($ctx:ident, $name:ident, String) => {
 | 
					    ($ctx:ident, $name:ident, String) => {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user