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"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "extract_macro"
 | 
			
		||||
name = "extract_derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "quote",
 | 
			
		||||
@@ -2235,6 +2235,14 @@ dependencies = [
 | 
			
		||||
 "getrandom",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "recordable_derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.49",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "redox_syscall"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
@@ -2317,7 +2325,7 @@ dependencies = [
 | 
			
		||||
 "chrono-tz",
 | 
			
		||||
 "dotenv",
 | 
			
		||||
 "env_logger",
 | 
			
		||||
 "extract_macro",
 | 
			
		||||
 "extract_derive",
 | 
			
		||||
 "lazy-regex",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "levenshtein",
 | 
			
		||||
@@ -2326,6 +2334,7 @@ dependencies = [
 | 
			
		||||
 "poise",
 | 
			
		||||
 "postman",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "recordable_derive",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "reminder_web",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,11 @@ path = "postman"
 | 
			
		||||
[dependencies.reminder_web]
 | 
			
		||||
path = "web"
 | 
			
		||||
 | 
			
		||||
[dependencies.extract_macro]
 | 
			
		||||
path = "extract_macro"
 | 
			
		||||
[dependencies.extract_derive]
 | 
			
		||||
path = "extract_derive"
 | 
			
		||||
 | 
			
		||||
[dependencies.recordable_derive]
 | 
			
		||||
path = "recordable_derive"
 | 
			
		||||
 | 
			
		||||
[package.metadata.deb]
 | 
			
		||||
depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "extract_macro"
 | 
			
		||||
name = "extract_derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
@@ -12,6 +12,7 @@ fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
 | 
			
		||||
    let name = &ast.ident;
 | 
			
		||||
 | 
			
		||||
    match &ast.data {
 | 
			
		||||
        // Dispatch over struct: extract args directly from context
 | 
			
		||||
        Data::Struct(st) => match &st.fields {
 | 
			
		||||
            Fields::Named(fields) => {
 | 
			
		||||
                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 serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{models::CtxData, utils::Extract, Context, Error};
 | 
			
		||||
use crate::{
 | 
			
		||||
    models::CtxData,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    ctx.defer_ephemeral().await?;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        ctx.defer_ephemeral().await?;
 | 
			
		||||
 | 
			
		||||
    let tz = ctx.timezone().await;
 | 
			
		||||
    let now = Utc::now().with_timezone(&tz);
 | 
			
		||||
        let tz = ctx.timezone().await;
 | 
			
		||||
        let now = Utc::now().with_timezone(&tz);
 | 
			
		||||
 | 
			
		||||
    ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
			
		||||
        "Time in **{}**: `{}`",
 | 
			
		||||
        tz,
 | 
			
		||||
        now.format("%H:%M")
 | 
			
		||||
    )))
 | 
			
		||||
    .await?;
 | 
			
		||||
        ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
			
		||||
            "Time in **{}**: `{}`",
 | 
			
		||||
            tz,
 | 
			
		||||
            now.format("%H:%M")
 | 
			
		||||
        )))
 | 
			
		||||
        .await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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> {
 | 
			
		||||
    clock(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
use poise::{serenity_prelude::CreateEmbed, CreateReply};
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
#[poise::command(
 | 
			
		||||
@@ -32,7 +35,9 @@ pub async fn run_macro(
 | 
			
		||||
                .await?;
 | 
			
		||||
 | 
			
		||||
            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::{
 | 
			
		||||
    consts::THEME_COLOR,
 | 
			
		||||
    utils::{footer, Extract},
 | 
			
		||||
    utils::{footer, Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    let footer = footer(ctx);
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let footer = footer(ctx);
 | 
			
		||||
 | 
			
		||||
    ctx.send(
 | 
			
		||||
        CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
            CreateEmbed::new()
 | 
			
		||||
                .title("Dashboard")
 | 
			
		||||
                .description("**https://beta.reminder-bot.com/dashboard**")
 | 
			
		||||
                .footer(footer)
 | 
			
		||||
                .color(*THEME_COLOR),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    .await?;
 | 
			
		||||
        ctx.send(
 | 
			
		||||
            CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
                CreateEmbed::new()
 | 
			
		||||
                    .title("Dashboard")
 | 
			
		||||
                    .description("**https://beta.reminder-bot.com/dashboard**")
 | 
			
		||||
                    .footer(footer)
 | 
			
		||||
                    .color(*THEME_COLOR),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        .await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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> {
 | 
			
		||||
    dashboard(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ use crate::{
 | 
			
		||||
    },
 | 
			
		||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
			
		||||
    models::{reminder::Reminder, CtxData},
 | 
			
		||||
    utils::Extract,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -140,21 +140,28 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn delete(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    let timezone = ctx.timezone().await;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let timezone = ctx.timezone().await;
 | 
			
		||||
 | 
			
		||||
    let reminders =
 | 
			
		||||
        Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
 | 
			
		||||
        let reminders =
 | 
			
		||||
            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
 | 
			
		||||
#[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> {
 | 
			
		||||
    delete(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,18 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::THEME_COLOR,
 | 
			
		||||
    utils::{footer, Extract},
 | 
			
		||||
    utils::{footer, Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn donate(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    let footer = footer(ctx);
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    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?
 | 
			
		||||
Click below for my Patreon and official bot server :)
 | 
			
		||||
 | 
			
		||||
@@ -36,11 +37,12 @@ Just $2 USD/month!
 | 
			
		||||
    )
 | 
			
		||||
    .await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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> {
 | 
			
		||||
    donate(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,24 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::THEME_COLOR,
 | 
			
		||||
    utils::{footer, Extract},
 | 
			
		||||
    utils::{footer, Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn help(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    let footer = footer(ctx);
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let footer = footer(ctx);
 | 
			
		||||
 | 
			
		||||
    ctx.send(
 | 
			
		||||
        CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
            CreateEmbed::new()
 | 
			
		||||
                .title("Help")
 | 
			
		||||
                .color(*THEME_COLOR)
 | 
			
		||||
                .description(
 | 
			
		||||
                    "__Info Commands__
 | 
			
		||||
        ctx.send(
 | 
			
		||||
            CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
                CreateEmbed::new()
 | 
			
		||||
                    .title("Help")
 | 
			
		||||
                    .color(*THEME_COLOR)
 | 
			
		||||
                    .description(
 | 
			
		||||
                        "__Info Commands__
 | 
			
		||||
`/help` `/info` `/donate` `/dashboard` `/clock`
 | 
			
		||||
*run these commands with no options*
 | 
			
		||||
 | 
			
		||||
@@ -44,17 +45,18 @@ __Setup Commands__
 | 
			
		||||
__Advanced Commands__
 | 
			
		||||
`/macro` - Record and replay command sequences
 | 
			
		||||
                    ",
 | 
			
		||||
                )
 | 
			
		||||
                .footer(footer),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    .await?;
 | 
			
		||||
                    )
 | 
			
		||||
                    .footer(footer),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        .await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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> {
 | 
			
		||||
    help(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,22 +3,23 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::THEME_COLOR,
 | 
			
		||||
    utils::{footer, Extract},
 | 
			
		||||
    utils::{footer, Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn info(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    let footer = footer(ctx);
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let footer = footer(ctx);
 | 
			
		||||
 | 
			
		||||
    ctx.send(
 | 
			
		||||
        CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
            CreateEmbed::new()
 | 
			
		||||
                .title("Info")
 | 
			
		||||
                .description(
 | 
			
		||||
                    "Help: `/help`
 | 
			
		||||
        ctx.send(
 | 
			
		||||
            CreateReply::default().ephemeral(true).embed(
 | 
			
		||||
                CreateEmbed::new()
 | 
			
		||||
                    .title("Info")
 | 
			
		||||
                    .description(
 | 
			
		||||
                        "Help: `/help`
 | 
			
		||||
 | 
			
		||||
**Welcome to Reminder Bot!**
 | 
			
		||||
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/
 | 
			
		||||
Use our dashboard: https://reminder-bot.com/",
 | 
			
		||||
                )
 | 
			
		||||
                .footer(footer)
 | 
			
		||||
                .color(*THEME_COLOR),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    .await?;
 | 
			
		||||
                    )
 | 
			
		||||
                    .footer(footer)
 | 
			
		||||
                    .color(*THEME_COLOR),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        .await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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> {
 | 
			
		||||
    info(ctx, Options {}).await
 | 
			
		||||
    (Options {}).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ use crate::{
 | 
			
		||||
    component_models::pager::{LookPager, Pager},
 | 
			
		||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
			
		||||
    models::{reminder::Reminder, CtxData},
 | 
			
		||||
    utils::Extract,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -40,88 +40,95 @@ pub struct Options {
 | 
			
		||||
    relative: Option<bool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    let timezone = ctx.timezone().await;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let timezone = ctx.timezone().await;
 | 
			
		||||
 | 
			
		||||
    let flags = LookFlags {
 | 
			
		||||
        show_disabled: options.disabled.unwrap_or(true),
 | 
			
		||||
        channel_id: options.channel.map(|c| c.id),
 | 
			
		||||
        time_display: options.relative.map_or(TimeDisplayType::Relative, |b| {
 | 
			
		||||
            if b {
 | 
			
		||||
                TimeDisplayType::Relative
 | 
			
		||||
        let flags = LookFlags {
 | 
			
		||||
            show_disabled: self.disabled.unwrap_or(true),
 | 
			
		||||
            channel_id: self.channel.map(|c| c.id),
 | 
			
		||||
            time_display: self.relative.map_or(TimeDisplayType::Relative, |b| {
 | 
			
		||||
                if b {
 | 
			
		||||
                    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 {
 | 
			
		||||
                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 {
 | 
			
		||||
            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 =
 | 
			
		||||
        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(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "Channel to view reminders on"] channel: Option<PartialChannel>,
 | 
			
		||||
    #[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
 | 
			
		||||
    #[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
 | 
			
		||||
) -> 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::{
 | 
			
		||||
    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
			
		||||
    models::reminder::create_reminder,
 | 
			
		||||
    utils::Extract,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -30,49 +30,58 @@ pub struct Options {
 | 
			
		||||
    timezone: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    match ctx {
 | 
			
		||||
        Context::Application(app_ctx) => {
 | 
			
		||||
            let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
			
		||||
            let data_opt = ContentModal::execute(app_ctx).await?;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        match ctx {
 | 
			
		||||
            Context::Application(app_ctx) => {
 | 
			
		||||
                let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
			
		||||
                let data_opt = ContentModal::execute(app_ctx).await?;
 | 
			
		||||
 | 
			
		||||
            match data_opt {
 | 
			
		||||
                Some(data) => {
 | 
			
		||||
                    create_reminder(
 | 
			
		||||
                        ctx,
 | 
			
		||||
                        options.time,
 | 
			
		||||
                        data.content,
 | 
			
		||||
                        options.channels,
 | 
			
		||||
                        options.interval,
 | 
			
		||||
                        options.expires,
 | 
			
		||||
                        options.tts,
 | 
			
		||||
                        tz,
 | 
			
		||||
                    )
 | 
			
		||||
                    .await
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                None => {
 | 
			
		||||
                    warn!("Unexpected None encountered in /multiline");
 | 
			
		||||
                    Ok(ctx
 | 
			
		||||
                        .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
			
		||||
                match data_opt {
 | 
			
		||||
                    Some(data) => {
 | 
			
		||||
                        create_reminder(
 | 
			
		||||
                            ctx,
 | 
			
		||||
                            self.time,
 | 
			
		||||
                            data.content,
 | 
			
		||||
                            self.channels,
 | 
			
		||||
                            self.interval,
 | 
			
		||||
                            self.expires,
 | 
			
		||||
                            self.tts,
 | 
			
		||||
                            tz,
 | 
			
		||||
                        )
 | 
			
		||||
                        .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");
 | 
			
		||||
            Ok(ctx
 | 
			
		||||
                .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
			
		||||
                .await
 | 
			
		||||
                .map(|_| ())?)
 | 
			
		||||
            _ => {
 | 
			
		||||
                warn!("Shouldn't be here");
 | 
			
		||||
                Ok(ctx
 | 
			
		||||
                    .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
 | 
			
		||||
                    .await
 | 
			
		||||
                    .map(|_| ())?)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "A description of the time to set the reminder for"]
 | 
			
		||||
@@ -89,5 +98,5 @@ pub async fn command(
 | 
			
		||||
    #[autocomplete = "timezone_autocomplete"]
 | 
			
		||||
    timezone: Option<String>,
 | 
			
		||||
) -> 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 crate::{consts::MINUTE, models::CtxData, utils::Extract, Context, Error};
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::MINUTE,
 | 
			
		||||
    models::CtxData,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
pub struct Options {
 | 
			
		||||
@@ -8,30 +13,38 @@ pub struct Options {
 | 
			
		||||
    seconds: Option<i64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    let combined_time =
 | 
			
		||||
        options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s);
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        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 {
 | 
			
		||||
        ctx.say("Nudge times must be less than 500 minutes").await?;
 | 
			
		||||
    } else {
 | 
			
		||||
        let mut channel_data = ctx.channel_data().await.unwrap();
 | 
			
		||||
        if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 {
 | 
			
		||||
            ctx.say("Nudge times must be less than 500 minutes").await?;
 | 
			
		||||
        } else {
 | 
			
		||||
            let mut channel_data = ctx.channel_data().await.unwrap();
 | 
			
		||||
 | 
			
		||||
        channel_data.nudge = combined_time as i16;
 | 
			
		||||
        channel_data.commit_changes(&ctx.data().database).await;
 | 
			
		||||
            channel_data.nudge = combined_time as i16;
 | 
			
		||||
            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`)
 | 
			
		||||
#[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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>,
 | 
			
		||||
    #[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    nudge(ctx, Options { minutes, seconds }).await
 | 
			
		||||
    (Options { minutes, seconds }).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::{HOUR, MINUTE},
 | 
			
		||||
    utils::Extract,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -13,69 +13,76 @@ pub struct Options {
 | 
			
		||||
    seconds: Option<i64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    ctx.defer().await?;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        ctx.defer().await?;
 | 
			
		||||
 | 
			
		||||
    let combined_time = options.hours.map_or(0, |h| h * HOUR as i64)
 | 
			
		||||
        + options.minutes.map_or(0, |m| m * MINUTE as i64)
 | 
			
		||||
        + options.seconds.map_or(0, |s| s);
 | 
			
		||||
        let combined_time = self.hours.map_or(0, |h| h * HOUR as i64)
 | 
			
		||||
            + self.minutes.map_or(0, |m| m * MINUTE as i64)
 | 
			
		||||
            + self.seconds.map_or(0, |s| s);
 | 
			
		||||
 | 
			
		||||
    if combined_time == 0 {
 | 
			
		||||
        ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
 | 
			
		||||
    } else {
 | 
			
		||||
        if let Some(channels) = ctx.guild().map(|guild| {
 | 
			
		||||
            guild
 | 
			
		||||
                .channels
 | 
			
		||||
                .iter()
 | 
			
		||||
                .filter(|(_, channel)| channel.is_text_based())
 | 
			
		||||
                .map(|(id, _)| id.get().to_string())
 | 
			
		||||
                .collect::<Vec<String>>()
 | 
			
		||||
                .join(",")
 | 
			
		||||
        }) {
 | 
			
		||||
            sqlx::query!(
 | 
			
		||||
                "
 | 
			
		||||
        if combined_time == 0 {
 | 
			
		||||
            ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
 | 
			
		||||
        } else {
 | 
			
		||||
            if let Some(channels) = ctx.guild().map(|guild| {
 | 
			
		||||
                guild
 | 
			
		||||
                    .channels
 | 
			
		||||
                    .iter()
 | 
			
		||||
                    .filter(|(_, channel)| channel.is_text_based())
 | 
			
		||||
                    .map(|(id, _)| id.get().to_string())
 | 
			
		||||
                    .collect::<Vec<String>>()
 | 
			
		||||
                    .join(",")
 | 
			
		||||
            }) {
 | 
			
		||||
                sqlx::query!(
 | 
			
		||||
                    "
 | 
			
		||||
                UPDATE reminders
 | 
			
		||||
                INNER JOIN `channels`
 | 
			
		||||
                    ON `channels`.id = reminders.channel_id
 | 
			
		||||
                SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)
 | 
			
		||||
                WHERE FIND_IN_SET(channels.`channel`, ?)
 | 
			
		||||
                ",
 | 
			
		||||
                combined_time as i64,
 | 
			
		||||
                channels
 | 
			
		||||
            )
 | 
			
		||||
            .execute(&ctx.data().database)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        } else {
 | 
			
		||||
            sqlx::query!(
 | 
			
		||||
                "
 | 
			
		||||
                    combined_time as i64,
 | 
			
		||||
                    channels
 | 
			
		||||
                )
 | 
			
		||||
                .execute(&ctx.data().database)
 | 
			
		||||
                .await
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            } else {
 | 
			
		||||
                sqlx::query!(
 | 
			
		||||
                    "
 | 
			
		||||
                UPDATE reminders
 | 
			
		||||
                INNER JOIN `channels`
 | 
			
		||||
                    ON `channels`.id = reminders.channel_id
 | 
			
		||||
                SET reminders.`utc_time` = reminders.`utc_time` + ?
 | 
			
		||||
                WHERE channels.`channel` = ?
 | 
			
		||||
                ",
 | 
			
		||||
                combined_time as i64,
 | 
			
		||||
                ctx.channel_id().get()
 | 
			
		||||
            )
 | 
			
		||||
            .execute(&ctx.data().database)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
                    combined_time as i64,
 | 
			
		||||
                    ctx.channel_id().get()
 | 
			
		||||
                )
 | 
			
		||||
                .execute(&ctx.data().database)
 | 
			
		||||
                .await
 | 
			
		||||
                .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
 | 
			
		||||
#[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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "Number of hours to offset by"] hours: Option<i64>,
 | 
			
		||||
    #[description = "Number of minutes to offset by"] minutes: Option<i64>,
 | 
			
		||||
    #[description = "Number of seconds to offset by"] seconds: Option<i64>,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    offset(ctx, Options { hours, minutes, seconds }).await
 | 
			
		||||
    (Options { hours, minutes, seconds }).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,73 +1,85 @@
 | 
			
		||||
use chrono::NaiveDateTime;
 | 
			
		||||
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)]
 | 
			
		||||
pub struct Options {
 | 
			
		||||
    until: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    let timezone = ctx.timezone().await;
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    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 {
 | 
			
		||||
        Some(until) => {
 | 
			
		||||
            let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
			
		||||
        match self.until {
 | 
			
		||||
            Some(until) => {
 | 
			
		||||
                let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
			
		||||
 | 
			
		||||
            if let Some(timestamp) = parsed {
 | 
			
		||||
                match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
 | 
			
		||||
                    Some(dt) => {
 | 
			
		||||
                        channel.paused = true;
 | 
			
		||||
                        channel.paused_until = Some(dt);
 | 
			
		||||
                if let Some(timestamp) = parsed {
 | 
			
		||||
                    match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
 | 
			
		||||
                        Some(dt) => {
 | 
			
		||||
                            channel.paused = true;
 | 
			
		||||
                            channel.paused_until = Some(dt);
 | 
			
		||||
 | 
			
		||||
                        channel.commit_changes(&ctx.data().database).await;
 | 
			
		||||
                            channel.commit_changes(&ctx.data().database).await;
 | 
			
		||||
 | 
			
		||||
                        ctx.say(format!(
 | 
			
		||||
                            "Reminders in this channel have been silenced until **<t:{}:D>**",
 | 
			
		||||
                            timestamp
 | 
			
		||||
                        ))
 | 
			
		||||
                        .await?;
 | 
			
		||||
                    }
 | 
			
		||||
                            ctx.say(format!(
 | 
			
		||||
                                "Reminders in this channel have been silenced until **<t:{}:D>**",
 | 
			
		||||
                                timestamp
 | 
			
		||||
                            ))
 | 
			
		||||
                            .await?;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    None => {
 | 
			
		||||
                        ctx.say(
 | 
			
		||||
                        None => {
 | 
			
		||||
                            ctx.say(
 | 
			
		||||
                            "Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible",
 | 
			
		||||
                        )
 | 
			
		||||
                        .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;
 | 
			
		||||
 | 
			
		||||
            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(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "When to pause until"] until: Option<String>,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    pause(ctx, Options { until }).await
 | 
			
		||||
    (Options { until }).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
use crate::{
 | 
			
		||||
    commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
 | 
			
		||||
    models::reminder::create_reminder,
 | 
			
		||||
    utils::Extract,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -19,24 +19,31 @@ pub struct Options {
 | 
			
		||||
    timezone: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
			
		||||
 | 
			
		||||
    create_reminder(
 | 
			
		||||
        ctx,
 | 
			
		||||
        options.time,
 | 
			
		||||
        options.content,
 | 
			
		||||
        options.channels,
 | 
			
		||||
        options.interval,
 | 
			
		||||
        options.expires,
 | 
			
		||||
        options.tts,
 | 
			
		||||
        tz,
 | 
			
		||||
    )
 | 
			
		||||
    .await
 | 
			
		||||
        create_reminder(
 | 
			
		||||
            ctx,
 | 
			
		||||
            self.time,
 | 
			
		||||
            self.content,
 | 
			
		||||
            self.channels,
 | 
			
		||||
            self.interval,
 | 
			
		||||
            self.expires,
 | 
			
		||||
            self.tts,
 | 
			
		||||
            tz,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "The time (and optionally date) to set the reminder for"]
 | 
			
		||||
@@ -54,5 +61,5 @@ pub async fn command(
 | 
			
		||||
    #[autocomplete = "timezone_autocomplete"]
 | 
			
		||||
    timezone: Option<String>,
 | 
			
		||||
) -> 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 crate::{
 | 
			
		||||
    commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData,
 | 
			
		||||
    utils::Extract, Context, Error,
 | 
			
		||||
    commands::autocomplete::timezone_autocomplete,
 | 
			
		||||
    consts::THEME_COLOR,
 | 
			
		||||
    models::CtxData,
 | 
			
		||||
    utils::{Extract, Recordable},
 | 
			
		||||
    Context, Error,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Extract)]
 | 
			
		||||
@@ -17,55 +20,56 @@ pub struct Options {
 | 
			
		||||
    pub timezone: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error> {
 | 
			
		||||
    let mut user_data = ctx.author_data().await.unwrap();
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    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 {
 | 
			
		||||
        match timezone.parse::<Tz>() {
 | 
			
		||||
            Ok(tz) => {
 | 
			
		||||
                user_data.timezone = timezone.clone();
 | 
			
		||||
                user_data.commit_changes(&ctx.data().database).await;
 | 
			
		||||
        if let Some(timezone) = self.timezone {
 | 
			
		||||
            match timezone.parse::<Tz>() {
 | 
			
		||||
                Ok(tz) => {
 | 
			
		||||
                    user_data.timezone = timezone.clone();
 | 
			
		||||
                    user_data.commit_changes(&ctx.data().database).await;
 | 
			
		||||
 | 
			
		||||
                let now = Utc::now().with_timezone(&tz);
 | 
			
		||||
                    let now = Utc::now().with_timezone(&tz);
 | 
			
		||||
 | 
			
		||||
                ctx.send(
 | 
			
		||||
                    CreateReply::default().embed(
 | 
			
		||||
                        CreateEmbed::new()
 | 
			
		||||
                            .title("Timezone Set")
 | 
			
		||||
                            .description(format!(
 | 
			
		||||
                    ctx.send(
 | 
			
		||||
                        CreateReply::default().embed(
 | 
			
		||||
                            CreateEmbed::new()
 | 
			
		||||
                                .title("Timezone Set")
 | 
			
		||||
                                .description(format!(
 | 
			
		||||
                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
			
		||||
                                timezone,
 | 
			
		||||
                                now.format("%H:%M")
 | 
			
		||||
                            ))
 | 
			
		||||
                            .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,
 | 
			
		||||
                                .color(*THEME_COLOR),
 | 
			
		||||
                        ),
 | 
			
		||||
                    )
 | 
			
		||||
                });
 | 
			
		||||
                    .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(
 | 
			
		||||
                        CreateEmbed::new()
 | 
			
		||||
                            .title("Timezone Not Recognized")
 | 
			
		||||
@@ -83,14 +87,18 @@ pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                .await?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
			
		||||
            (t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true)
 | 
			
		||||
        });
 | 
			
		||||
        } else {
 | 
			
		||||
            let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
			
		||||
                (
 | 
			
		||||
                    t.to_string(),
 | 
			
		||||
                    format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")),
 | 
			
		||||
                    true,
 | 
			
		||||
                )
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        ctx.send(
 | 
			
		||||
            ctx.send(
 | 
			
		||||
            CreateReply::default().embed(
 | 
			
		||||
                CreateEmbed::new()
 | 
			
		||||
                    .title("Timezone Usage")
 | 
			
		||||
@@ -110,18 +118,19 @@ You may want to use one of the popular timezones below, otherwise click [here](h
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        .await?;
 | 
			
		||||
    }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Select your timezone
 | 
			
		||||
#[poise::command(slash_command, rename = "timezone")]
 | 
			
		||||
#[poise::command(slash_command, rename = "timezone", identifying_name = "timezone")]
 | 
			
		||||
pub async fn command(
 | 
			
		||||
    ctx: Context<'_>,
 | 
			
		||||
    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
			
		||||
    #[autocomplete = "timezone_autocomplete"]
 | 
			
		||||
    timezone: Option<String>,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    timezone_fn(ctx, Options { timezone }).await
 | 
			
		||||
    (Options { timezone }).run(ctx).await
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,39 +2,50 @@ use log::warn;
 | 
			
		||||
use poise::CreateReply;
 | 
			
		||||
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)]
 | 
			
		||||
pub struct Options;
 | 
			
		||||
 | 
			
		||||
pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
 | 
			
		||||
    match ctx.channel_data().await {
 | 
			
		||||
        Ok(data) => {
 | 
			
		||||
            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
			
		||||
                ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
			
		||||
                    "**Warning!**
 | 
			
		||||
impl Recordable for Options {
 | 
			
		||||
    async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
 | 
			
		||||
        match ctx.channel_data().await {
 | 
			
		||||
            Ok(data) => {
 | 
			
		||||
                if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
			
		||||
                    ctx.send(CreateReply::default().ephemeral(true).content(format!(
 | 
			
		||||
                        "**Warning!**
 | 
			
		||||
This link can be used by users to anonymously send messages, with or without permissions.
 | 
			
		||||
Do not share it!
 | 
			
		||||
|| https://discord.com/api/webhooks/{}/{} ||",
 | 
			
		||||
                    id, token,
 | 
			
		||||
                )))
 | 
			
		||||
                .await?;
 | 
			
		||||
            } else {
 | 
			
		||||
                        id, token,
 | 
			
		||||
                    )))
 | 
			
		||||
                    .await?;
 | 
			
		||||
                } else {
 | 
			
		||||
                    ctx.say("No webhook configured on this channel.").await?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                warn!("Error fetching channel data: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                ctx.say("No webhook configured on this channel.").await?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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
 | 
			
		||||
#[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> {
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
                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 {
 | 
			
		||||
                        let _ = ctx
 | 
			
		||||
                            .send(
 | 
			
		||||
@@ -34,16 +22,29 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
			
		||||
                            )
 | 
			
		||||
                            .await;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        let recorded = RecordedCommand::from_context(app_ctx).unwrap();
 | 
			
		||||
                        command_macro.commands.push(recorded);
 | 
			
		||||
                        match RecordedCommand::from_context(app_ctx) {
 | 
			
		||||
                            Some(recorded) => {
 | 
			
		||||
                                command_macro.commands.push(recorded);
 | 
			
		||||
 | 
			
		||||
                        let _ = ctx
 | 
			
		||||
                            .send(
 | 
			
		||||
                                CreateReply::default()
 | 
			
		||||
                                    .ephemeral(true)
 | 
			
		||||
                                    .content("Command recorded to macro"),
 | 
			
		||||
                            )
 | 
			
		||||
                            .await;
 | 
			
		||||
                                let _ = ctx
 | 
			
		||||
                                    .send(
 | 
			
		||||
                                        CreateReply::default()
 | 
			
		||||
                                            .ephemeral(true)
 | 
			
		||||
                                            .content("Command recorded to macro"),
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .await;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            None => {
 | 
			
		||||
                                let _ = ctx
 | 
			
		||||
                                    .send(
 | 
			
		||||
                                        CreateReply::default().ephemeral(true).content(
 | 
			
		||||
                                            "This command is not supported in macros yet.",
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .await;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,29 +2,64 @@ use poise::serenity_prelude::model::id::GuildId;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
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")]
 | 
			
		||||
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 {
 | 
			
		||||
    pub fn from_context(ctx: ApplicationContext) -> Option<Self> {
 | 
			
		||||
        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,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn execute(self, ctx: ApplicationContext<'_>) -> Result<(), Error> {
 | 
			
		||||
        match self {
 | 
			
		||||
            RecordedCommand::Remind(options) => {
 | 
			
		||||
                remind::remind(Context::Application(ctx), options).await
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct CommandMacro {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/utils.rs
									
									
									
									
									
								
							@@ -9,7 +9,7 @@ use poise::{
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    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 {
 | 
			
		||||
@@ -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 {
 | 
			
		||||
    fn extract(ctx: ApplicationContext) -> Self;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub use extract_macro::Extract;
 | 
			
		||||
pub use extract_derive::Extract;
 | 
			
		||||
 | 
			
		||||
macro_rules! extract_arg {
 | 
			
		||||
    ($ctx:ident, $name:ident, String) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user