everything except component model actions

This commit is contained in:
jude
2022-02-19 18:21:11 +00:00
parent 84ee7e77c5
commit afc376c44f
21 changed files with 718 additions and 2623 deletions

View File

@ -1,4 +1,4 @@
pub mod info_cmds;
pub mod moderation_cmds;
//pub mod reminder_cmds;
//pub mod todo_cmds;
pub mod reminder_cmds;
pub mod todo_cmds;

View File

@ -4,9 +4,13 @@ use levenshtein::levenshtein;
use poise::CreateReply;
use crate::{
component_models::pager::{MacroPager, Pager},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
hooks::guild_only,
models::{command_macro::CommandMacro, CtxData},
models::{
command_macro::{guild_command_macro, CommandMacro},
CtxData,
},
Context, Data, Error,
};
@ -286,8 +290,7 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
/// List recorded macros
#[poise::command(slash_command, rename = "list", check = "guild_only")]
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
// let macros = CommandMacro::from_guild(&ctx.data().database, ctx.guild_id().unwrap()).await;
let macros: Vec<CommandMacro<Data, Error>> = vec![];
let macros = ctx.command_macros().await?;
let resp = show_macro_page(&macros, 0);
@ -303,32 +306,31 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
/// Run a recorded macro
#[poise::command(slash_command, rename = "run", check = "guild_only")]
pub async fn run_macro(
ctx: Context<'_>,
ctx: poise::ApplicationContext<'_, Data, Error>,
#[description = "Name of macro to run"]
#[autocomplete = "macro_name_autocomplete"]
name: String,
) -> Result<(), Error> {
match sqlx::query!(
"
SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
ctx.guild_id().unwrap().0,
name
)
.fetch_one(&ctx.data().database)
.await
{
Ok(row) => {
ctx.defer().await?;
match guild_command_macro(&Context::Application(ctx), &name).await {
Some(command_macro) => {
ctx.defer_response(false).await?;
// TODO TODO TODO!!!!!!!! RUN COMMAND FROM MACRO
for command in command_macro.commands {
if let Some(action) = command.action {
(action)(poise::ApplicationContext { args: &command.options, ..ctx })
.await
.ok()
.unwrap();
} else {
Context::Application(ctx)
.say(format!("Command \"{}\" failed to execute", command.command_name))
.await?;
}
}
}
Err(sqlx::Error::RowNotFound) => {
ctx.say(format!("Macro \"{}\" not found", name)).await?;
}
Err(e) => {
panic!("{}", e);
None => {
Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
}
}
@ -398,17 +400,6 @@ pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
}
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
let mut reply = CreateReply::default();
reply.embed(|e| {
e.title("Macros")
.description("No Macros Set Up. Use `/macro record` to get started.")
.color(*THEME_COLOR)
});
reply
/*
let pager = MacroPager::new(page);
if macros.is_empty() {
@ -479,5 +470,4 @@ pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea
});
reply
*/
}

View File

@ -7,17 +7,21 @@ use std::{
use chrono::NaiveDateTime;
use chrono_tz::Tz;
use num_integer::Integer;
use regex_command_attr::command;
use serenity::{builder::CreateEmbed, client::Context, model::channel::Channel};
use poise::{
serenity::{builder::CreateEmbed, model::channel::Channel},
serenity_prelude::ActionRole::Create,
CreateReply,
};
use crate::{
component_models::{
pager::{DelPager, LookPager, Pager},
ComponentDataModel, DelSelector,
},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES, THEME_COLOR},
framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue},
hooks::CHECK_GUILD_PERMISSIONS_HOOK,
consts::{
EMBED_DESCRIPTION_MAX_LENGTH, HOUR, MINUTE, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES,
THEME_COLOR,
},
interval_parser::parse_duration,
models::{
reminder::{
@ -33,29 +37,22 @@ use crate::{
},
time_parser::natural_parser,
utils::{check_guild_subscription, check_subscription},
SQLPool,
Context, Error,
};
#[command("pause")]
#[description("Pause all reminders on the current channel until a certain time or indefinitely")]
#[arg(
name = "until",
description = "When to pause until (hint: try 'next Wednesday', or '10 minutes')",
kind = "String",
required = false
)]
#[supports_dm(false)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn pause(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
/// Pause all reminders on the current channel until a certain time or indefinitely
#[poise::command(slash_command)]
pub async fn pause(
ctx: Context<'_>,
#[description = "When to pause until"] until: Option<String>,
) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let timezone = UserData::timezone_of(&invoke.author_id(), &pool).await;
let mut channel = ctx.channel_data().await.unwrap();
let mut channel = ctx.channel_data(invoke.channel_id()).await.unwrap();
match args.get("until") {
Some(OptionValue::String(until)) => {
let parsed = natural_parser(until, &timezone.to_string()).await;
match until {
Some(until) => {
let parsed = natural_parser(&until, &timezone.to_string()).await;
if let Some(timestamp) = parsed {
let dt = NaiveDateTime::from_timestamp(timestamp, 0);
@ -63,92 +60,53 @@ async fn pause(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions)
channel.paused = true;
channel.paused_until = Some(dt);
channel.commit_changes(&pool).await;
channel.commit_changes(&ctx.data().database).await;
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(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?;
} else {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Time could not be processed. Please write the time as clearly as possible"),
)
.await;
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(&pool).await;
channel.commit_changes(&ctx.data().database).await;
if channel.paused {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Reminders in this channel have been silenced indefinitely"),
)
.await;
ctx.say("Reminders in this channel have been silenced indefinitely").await?;
} else {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Reminders in this channel have been unsilenced"),
)
.await;
ctx.say("Reminders in this channel have been unsilenced").await?;
}
}
}
Ok(())
}
#[command("offset")]
#[description("Move all reminders in the current server by a certain amount of time. Times get added together")]
#[arg(
name = "hours",
description = "Number of hours to offset by",
kind = "Integer",
required = false
)]
#[arg(
name = "minutes",
description = "Number of minutes to offset by",
kind = "Integer",
required = false
)]
#[arg(
name = "seconds",
description = "Number of seconds to offset by",
kind = "Integer",
required = false
)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn offset(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let combined_time = args.get("hours").map_or(0, |h| h.as_i64().unwrap() * 3600)
+ args.get("minutes").map_or(0, |m| m.as_i64().unwrap() * 60)
+ args.get("seconds").map_or(0, |s| s.as_i64().unwrap());
/// Move all reminders in the current server by a certain amount of time. Times get added together
#[poise::command(slash_command)]
pub async fn offset(
ctx: Context<'_>,
#[description = "Number of hours to offset by"] hours: Option<isize>,
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
) -> Result<(), Error> {
let combined_time = hours.map_or(0, |h| h * HOUR as isize)
+ minutes.map_or(0, |m| m * MINUTE as isize)
+ seconds.map_or(0, |s| s);
if combined_time == 0 {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Please specify one of `hours`, `minutes` or `seconds`"),
)
.await;
ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
} else {
if let Some(guild) = invoke.guild(ctx.cache.clone()) {
if let Some(guild) = ctx.guild() {
let channels = guild
.channels
.iter()
@ -167,110 +125,67 @@ 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,
combined_time as i64,
channels
)
.execute(&pool)
.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,
invoke.channel_id().0
combined_time as i64,
ctx.channel_id().0
)
.execute(&pool)
.execute(&ctx.data().database)
.await
.unwrap();
}
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content(format!("All reminders offset by {} seconds", combined_time)),
)
.await;
ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?;
}
Ok(())
}
#[command("nudge")]
#[description("Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)")]
#[arg(
name = "minutes",
description = "Number of minutes to nudge new reminders by",
kind = "Integer",
required = false
)]
#[arg(
name = "seconds",
description = "Number of seconds to nudge new reminders by",
kind = "Integer",
required = false
)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn nudge(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)
#[poise::command(slash_command)]
pub async fn nudge(
ctx: Context<'_>,
#[description = "Number of minutes to nudge new reminders by"] minutes: Option<isize>,
#[description = "Number of seconds to nudge new reminders by"] seconds: Option<isize>,
) -> Result<(), Error> {
let combined_time = minutes.map_or(0, |m| m * MINUTE as isize) + seconds.map_or(0, |s| s);
let combined_time = args.get("minutes").map_or(0, |m| m.as_i64().unwrap() * 60)
+ args.get("seconds").map_or(0, |s| s.as_i64().unwrap());
if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("Nudge times must be less than 500 minutes"),
)
.await;
if combined_time < i16::MIN as isize || combined_time > i16::MAX as isize {
ctx.say("Nudge times must be less than 500 minutes").await?;
} else {
let mut channel_data = ctx.channel_data(invoke.channel_id()).await.unwrap();
let mut channel_data = ctx.channel_data().await.unwrap();
channel_data.nudge = combined_time as i16;
channel_data.commit_changes(&pool).await;
channel_data.commit_changes(&ctx.data().database).await;
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(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(())
}
#[command("look")]
#[description("View reminders on a specific channel")]
#[arg(
name = "channel",
description = "The channel to view reminders on",
kind = "Channel",
required = false
)]
#[arg(
name = "disabled",
description = "Whether to show disabled reminders or not",
kind = "Boolean",
required = false
)]
#[arg(
name = "relative",
description = "Whether to display times as relative or exact times",
kind = "Boolean",
required = false
)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let timezone = UserData::timezone_of(&invoke.author_id(), &pool).await;
/// View reminders on a specific channel
#[poise::command(slash_command)]
pub async fn look(
ctx: Context<'_>,
#[description = "Channel to view reminders on"] channel: Option<Channel>,
#[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> {
let timezone = ctx.timezone().await;
let flags = LookFlags {
show_disabled: args.get("disabled").map(|i| i.as_bool()).flatten().unwrap_or(true),
channel_id: args.get("channel").map(|i| i.as_channel_id()).flatten(),
time_display: args.get("relative").map_or(TimeDisplayType::Relative, |b| {
if b.as_bool() == Some(true) {
show_disabled: disabled.unwrap_or(true),
channel_id: channel.map(|c| c.id()),
time_display: relative.map_or(TimeDisplayType::Relative, |b| {
if b {
TimeDisplayType::Relative
} else {
TimeDisplayType::Absolute
@ -278,33 +193,29 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
}),
};
let channel_opt = invoke.channel_id().to_channel_cached(&ctx);
let channel_opt = ctx.channel_id().to_channel_cached(&ctx.discord());
let channel_id = if let Some(Channel::Guild(channel)) = channel_opt {
if Some(channel.guild_id) == invoke.guild_id() {
flags.channel_id.unwrap_or_else(|| invoke.channel_id())
if Some(channel.guild_id) == ctx.guild_id() {
flags.channel_id.unwrap_or_else(|| ctx.channel_id())
} else {
invoke.channel_id()
ctx.channel_id()
}
} else {
invoke.channel_id()
ctx.channel_id()
};
let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
Some(channel.name)
} else {
None
};
let channel_name =
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.discord()) {
Some(channel.name)
} else {
None
};
let reminders = Reminder::from_channel(ctx, channel_id, &flags).await;
let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await;
if reminders.is_empty() {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("No reminders on specified channel"),
)
.await;
let _ = ctx.say("No reminders on specified channel").await;
} else {
let mut char_count = 0;
@ -327,41 +238,45 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
let pager = LookPager::new(flags, timezone);
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.embed(|e| {
e.title(format!(
"Reminders{}",
channel_name.map_or(String::new(), |n| format!(" on #{}", n))
))
.description(display)
.footer(|f| f.text(format!("Page {} of {}", 1, pages)))
.color(*THEME_COLOR)
})
.components(|comp| {
pager.create_button_row(pages, comp);
ctx.send(|r| {
r.ephemeral(true)
.embed(|e| {
e.title(format!(
"Reminders{}",
channel_name.map_or(String::new(), |n| format!(" on #{}", n))
))
.description(display)
.footer(|f| f.text(format!("Page {} of {}", 1, pages)))
.color(*THEME_COLOR)
})
.components(|comp| {
pager.create_button_row(pages, comp);
comp
}),
)
.await
.unwrap();
comp
})
})
.await?;
}
Ok(())
}
#[command("del")]
#[description("Delete reminders")]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn delete(ctx: &Context, invoke: &mut CommandInvoke, _args: CommandOptions) {
let timezone = ctx.timezone(invoke.author_id()).await;
/// Delete reminders
#[poise::command(slash_command, rename = "del")]
pub async fn delete(ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let reminders = Reminder::from_guild(ctx, invoke.guild_id(), invoke.author_id()).await;
let reminders = Reminder::from_guild(&ctx, ctx.guild_id(), ctx.author().id).await;
let resp = show_delete_page(&reminders, 0, timezone);
let _ = invoke.respond(&ctx, resp).await;
ctx.send(|r| {
*r = resp;
r
})
.await?;
Ok(())
}
pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize {
@ -386,20 +301,20 @@ pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize {
})
}
pub fn show_delete_page(
reminders: &[Reminder],
page: usize,
timezone: Tz,
) -> CreateGenericResponse {
pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> CreateReply {
let pager = DelPager::new(page, timezone);
if reminders.is_empty() {
return CreateGenericResponse::new()
let mut reply = CreateReply::default();
reply
.embed(|e| e.title("Delete Reminders").description("No Reminders").color(*THEME_COLOR))
.components(|comp| {
pager.create_button_row(0, comp);
comp
});
return reply;
}
let pages = max_delete_page(reminders, &timezone);
@ -448,7 +363,9 @@ pub fn show_delete_page(
let del_selector = ComponentDataModel::DelSelector(DelSelector { page, timezone });
CreateGenericResponse::new()
let mut reply = CreateReply::default();
reply
.embed(|e| {
e.title("Delete Reminders")
.description(display)
@ -486,290 +403,206 @@ pub fn show_delete_page(
})
})
})
});
reply
}
fn time_difference(start_time: NaiveDateTime) -> String {
let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
let now = NaiveDateTime::from_timestamp(unix_time, 0);
let delta = (now - start_time).num_seconds();
let (minutes, seconds) = delta.div_rem(&60);
let (hours, minutes) = minutes.div_rem(&60);
let (days, hours) = hours.div_rem(&24);
format!("{} days, {:02}:{:02}:{:02}", days, hours, minutes, seconds)
}
/// Manage timers
#[poise::command(slash_command, rename = "timer")]
pub async fn timer_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// List the timers in this server or DM channel
#[poise::command(slash_command, rename = "list")]
pub async fn list_timer(ctx: Context<'_>) -> Result<(), Error> {
let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0);
let timers = Timer::from_owner(owner, &ctx.data().database).await;
if !timers.is_empty() {
ctx.send(|m| {
m.embed(|e| {
e.fields(timers.iter().map(|timer| {
(&timer.name, format!("⌚ `{}`", time_difference(timer.start_time)), false)
}))
.color(*THEME_COLOR)
})
})
.await?;
} else {
ctx.say("No timers currently. Use `/timer start` to create a new timer").await?;
}
Ok(())
}
#[command("timer")]
#[description("Manage timers")]
#[subcommand("list")]
#[description("List the timers in this server or DM channel")]
#[subcommand("start")]
#[description("Start a new timer from now")]
#[arg(name = "name", description = "Name for the new timer", kind = "String", required = true)]
#[subcommand("delete")]
#[description("Delete a timer")]
#[arg(name = "name", description = "Name of the timer to delete", kind = "String", required = true)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn timer(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
fn time_difference(start_time: NaiveDateTime) -> String {
let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
let now = NaiveDateTime::from_timestamp(unix_time, 0);
/// Start a new timer from now
#[poise::command(slash_command, rename = "start")]
pub async fn start_timer(
ctx: Context<'_>,
#[description = "Name for the new timer"] name: String,
) -> Result<(), Error> {
let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0);
let delta = (now - start_time).num_seconds();
let count = Timer::count_from_owner(owner, &ctx.data().database).await;
let (minutes, seconds) = delta.div_rem(&60);
let (hours, minutes) = minutes.div_rem(&60);
let (days, hours) = hours.div_rem(&24);
if count >= 25 {
ctx.say("You already have 25 timers. Please delete some timers before creating a new one")
.await?;
} else {
if name.len() <= 32 {
Timer::create(&name, owner, &ctx.data().database).await;
format!("{} days, {:02}:{:02}:{:02}", days, hours, minutes, seconds)
ctx.say("Created a new timer").await?;
} else {
ctx.say(format!(
"Please name your timer something shorted (max. 32 characters, you used {})",
name.len()
))
.await?;
}
}
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let owner = invoke.guild_id().map(|g| g.0).unwrap_or_else(|| invoke.author_id().0);
match args.subcommand.clone().unwrap().as_str() {
"start" => {
let count = Timer::count_from_owner(owner, &pool).await;
if count >= 25 {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("You already have 25 timers. Please delete some timers before creating a new one"),
)
.await;
} else {
let name = args.get("name").unwrap().to_string();
if name.len() <= 32 {
Timer::create(&name, owner, &pool).await;
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("Created a new timer"),
)
.await;
} else {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content(format!("Please name your timer something shorted (max. 32 characters, you used {})", name.len())),
)
.await;
}
}
}
"delete" => {
let name = args.get("name").unwrap().to_string();
let exists = sqlx::query!(
"
SELECT 1 as _r FROM timers WHERE owner = ? AND name = ?
",
owner,
name
)
.fetch_one(&pool)
.await;
if exists.is_ok() {
sqlx::query!(
"
DELETE FROM timers WHERE owner = ? AND name = ?
",
owner,
name
)
.execute(&pool)
.await
.unwrap();
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("Deleted a timer"),
)
.await;
} else {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("Could not find a timer by that name"),
)
.await;
}
}
"list" => {
let timers = Timer::from_owner(owner, &pool).await;
if !timers.is_empty() {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().embed(|e| {
e.fields(timers.iter().map(|timer| {
(
&timer.name,
format!("⌚ `{}`", time_difference(timer.start_time)),
false,
)
}))
.color(*THEME_COLOR)
}),
)
.await;
} else {
let _ = invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(
"No timers currently. Use `/timer start` to create a new timer",
),
)
.await;
}
}
_ => {}
}
Ok(())
}
#[command("remind")]
#[description("Create a new reminder")]
#[arg(
name = "time",
description = "A description of the time to set the reminder for",
kind = "String",
required = true
)]
#[arg(
name = "content",
description = "The message content to send",
kind = "String",
required = true
)]
#[arg(
name = "channels",
description = "Channel or user mentions to set the reminder for",
kind = "String",
required = false
)]
#[arg(
name = "interval",
description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder",
kind = "String",
required = false
)]
#[arg(
name = "expires",
description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending",
kind = "String",
required = false
)]
#[arg(
name = "tts",
description = "Set the TTS flag on the reminder message (like the /tts command)",
kind = "Boolean",
required = false
)]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn remind(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
if args.get("interval").is_none() && args.get("expires").is_some() {
let _ = invoke
.respond(
&ctx,
CreateGenericResponse::new().content("`expires` can only be used with `interval`"),
)
/// Delete a timer
#[poise::command(slash_command, rename = "delete")]
pub async fn delete_timer(
ctx: Context<'_>,
#[description = "Name of timer to delete"] name: String,
) -> Result<(), Error> {
let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0);
let exists =
sqlx::query!("SELECT 1 as _r FROM timers WHERE owner = ? AND name = ?", owner, name)
.fetch_one(&ctx.data().database)
.await;
return;
if exists.is_ok() {
sqlx::query!("DELETE FROM timers WHERE owner = ? AND name = ?", owner, name)
.execute(&ctx.data().database)
.await
.unwrap();
ctx.say("Deleted a timer").await?;
} else {
ctx.say("Could not find a timer by that name").await?;
}
invoke.defer(&ctx).await;
Ok(())
}
let user_data = ctx.user_data(invoke.author_id()).await.unwrap();
let timezone = user_data.timezone();
/// Create a new reminder
#[poise::command(slash_command)]
pub(crate) async fn remind(
ctx: Context<'_>,
#[description = "A description of the time to set the reminder for"] time: String,
#[description = "The message content to send"] content: String,
#[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
interval: Option<String>,
#[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending"]
expires: Option<String>,
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
tts: Option<bool>,
) -> Result<(), Error> {
if interval.is_none() && expires.is_some() {
ctx.say("`expires` can only be used with `interval`").await?;
let time = {
let time_str = args.get("time").unwrap().to_string();
return Ok(());
}
natural_parser(&time_str, &timezone.to_string()).await
};
ctx.defer().await?;
let user_data = ctx.author_data().await.unwrap();
let timezone = ctx.timezone().await;
let time = natural_parser(&time, &timezone.to_string()).await;
match time {
Some(time) => {
let content = {
let content = args.get("content").unwrap().to_string();
let tts = args.get("tts").map_or(false, |arg| arg.as_bool().unwrap_or(false));
let tts = tts.unwrap_or(false);
Content { content, tts, attachment: None, attachment_name: None }
};
let scopes = {
let list = args
.get("channels")
.map(|arg| parse_mention_list(&arg.to_string()))
.unwrap_or_default();
let list =
channels.map(|arg| parse_mention_list(&arg.to_string())).unwrap_or_default();
if list.is_empty() {
if invoke.guild_id().is_some() {
vec![ReminderScope::Channel(invoke.channel_id().0)]
if ctx.guild_id().is_some() {
vec![ReminderScope::Channel(ctx.channel_id().0)]
} else {
vec![ReminderScope::User(invoke.author_id().0)]
vec![ReminderScope::User(ctx.author().id.0)]
}
} else {
list
}
};
let (interval, expires) = if let Some(repeat) = args.get("interval") {
if check_subscription(&ctx, invoke.author_id()).await
|| (invoke.guild_id().is_some()
&& check_guild_subscription(&ctx, invoke.guild_id().unwrap()).await)
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
if check_subscription(&ctx.discord(), ctx.author().id).await
|| (ctx.guild_id().is_some()
&& check_guild_subscription(&ctx.discord(), ctx.guild_id().unwrap()).await)
{
(
parse_duration(&repeat.to_string())
parse_duration(repeat)
.or_else(|_| parse_duration(&format!("1 {}", repeat.to_string())))
.ok(),
{
if let Some(arg) = args.get("expires") {
natural_parser(&arg.to_string(), &timezone.to_string()).await
if let Some(arg) = &expires {
natural_parser(arg, &timezone.to_string()).await
} else {
None
}
},
)
} else {
let _ = invoke
.respond(&ctx, CreateGenericResponse::new()
.content("`repeat` is only available to Patreon subscribers or self-hosted users")
).await;
ctx.say(
"`repeat` is only available to Patreon subscribers or self-hosted users",
)
.await?;
return;
return Ok(());
}
} else {
(None, None)
};
if interval.is_none() && args.get("interval").is_some() {
let _ = invoke
.respond(
&ctx,
CreateGenericResponse::new().content(
"Repeat interval could not be processed. Try and format the repetition similar to `1 hour` or `4 days`",
),
)
.await;
} else if expires.is_none() && args.get("expires").is_some() {
let _ = invoke
.respond(
&ctx,
CreateGenericResponse::new().content(
"Expiry time failed to process. Please make it as clear as possible",
),
)
.await;
if processed_interval.is_none() && interval.is_some() {
ctx.say(
"Repeat interval could not be processed. Try similar to `1 hour` or `4 days`",
)
.await?;
} else if processed_expires.is_none() && expires.is_some() {
ctx.say("Expiry time failed to process. Please make it as clear as possible")
.await?;
} else {
let mut builder = MultiReminderBuilder::new(ctx, invoke.guild_id())
let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id())
.author(user_data)
.content(content)
.time(time)
.timezone(timezone)
.expires(expires)
.interval(interval);
.expires(processed_expires)
.interval(processed_interval);
builder.set_scopes(scopes);
@ -777,23 +610,21 @@ async fn remind(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions)
let embed = create_response(successes, errors, time);
let _ = invoke
.respond(
&ctx,
CreateGenericResponse::new().embed(|c| {
*c = embed;
c
}),
)
.await;
ctx.send(|m| {
m.embed(|c| {
*c = embed;
c
})
})
.await?;
}
}
None => {
let _ = invoke
.respond(&ctx, CreateGenericResponse::new().content("Time could not be processed"))
.await;
ctx.say("Time could not be processed").await?;
}
}
Ok(())
}
fn create_response(

View File

@ -1,5 +1,4 @@
use regex_command_attr::command;
use serenity::client::Context;
use poise::CreateReply;
use crate::{
component_models::{
@ -7,134 +6,177 @@ use crate::{
ComponentDataModel, TodoSelector,
},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
framework::{CommandInvoke, CommandOptions, CreateGenericResponse},
hooks::CHECK_GUILD_PERMISSIONS_HOOK,
SQLPool,
Context, Error,
};
#[command]
#[description("Manage todo lists")]
#[subcommandgroup("server")]
#[description("Manage the server todo list")]
#[subcommand("add")]
#[description("Add an item to the server todo list")]
#[arg(
name = "task",
description = "The task to add to the todo list",
kind = "String",
required = true
)]
#[subcommand("view")]
#[description("View and remove from the server todo list")]
#[subcommandgroup("channel")]
#[description("Manage the channel todo list")]
#[subcommand("add")]
#[description("Add to the channel todo list")]
#[arg(
name = "task",
description = "The task to add to the todo list",
kind = "String",
required = true
)]
#[subcommand("view")]
#[description("View and remove from the channel todo list")]
#[subcommandgroup("user")]
#[description("Manage your personal todo list")]
#[subcommand("add")]
#[description("Add to your personal todo list")]
#[arg(
name = "task",
description = "The task to add to the todo list",
kind = "String",
required = true
)]
#[subcommand("view")]
#[description("View and remove from your personal todo list")]
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
async fn todo(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
if invoke.guild_id().is_none() && args.subcommand_group != Some("user".to_string()) {
let _ = invoke
.respond(
&ctx,
CreateGenericResponse::new().content("Please use `/todo user` in direct messages"),
)
.await;
} else {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
/// Manage todo lists
#[poise::command(slash_command, rename = "todo")]
pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
let keys = match args.subcommand_group.as_ref().unwrap().as_str() {
"server" => (None, None, invoke.guild_id().map(|g| g.0)),
"channel" => (None, Some(invoke.channel_id().0), invoke.guild_id().map(|g| g.0)),
_ => (Some(invoke.author_id().0), None, None),
};
/// Manage the server todo list
#[poise::command(slash_command, rename = "server")]
pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
match args.get("task") {
Some(task) => {
let task = task.to_string();
/// Add an item to the server todo list
#[poise::command(slash_command, rename = "add")]
pub async fn todo_guild_add(
ctx: Context<'_>,
#[description = "The task to add to the todo list"] task: String,
) -> Result<(), Error> {
sqlx::query!(
"INSERT INTO todos (guild_id, value)
VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
ctx.guild_id().unwrap().0,
task
)
.execute(&ctx.data().database)
.await
.unwrap();
sqlx::query!(
"INSERT INTO todos (user_id, channel_id, guild_id, value) VALUES ((SELECT id FROM users WHERE user = ?), (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?), ?)",
keys.0,
keys.1,
keys.2,
task
)
.execute(&pool)
.await
.unwrap();
ctx.say("Item added to todo list").await?;
let _ = invoke
.respond(&ctx, CreateGenericResponse::new().content("Item added to todo list"))
.await;
}
None => {
let values = if let Some(uid) = keys.0 {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?",
uid,
)
.fetch_all(&pool)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>()
} else if let Some(cid) = keys.1 {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?",
cid,
)
.fetch_all(&pool)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>()
} else {
sqlx::query!(
"SELECT todos.id, value FROM todos
Ok(())
}
/// View and remove from the server todo list
#[poise::command(slash_command, rename = "view")]
pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
let values = sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
keys.2,
)
.fetch_all(&pool)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>()
};
ctx.guild_id().unwrap().0,
)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>();
let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2);
let resp = show_todo_page(&values, 0, None, None, ctx.guild_id().map(|g| g.0));
invoke.respond(&ctx, resp).await.unwrap();
}
}
}
ctx.send(|r| {
*r = resp;
r
})
.await?;
Ok(())
}
/// Manage the channel todo list
#[poise::command(slash_command, rename = "channel")]
pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Add an item to the channel todo list
#[poise::command(slash_command, rename = "add")]
pub async fn todo_channel_add(
ctx: Context<'_>,
#[description = "The task to add to the todo list"] task: String,
) -> Result<(), Error> {
sqlx::query!(
"INSERT INTO todos (guild_id, channel_id, value)
VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
ctx.guild_id().unwrap().0,
ctx.channel_id().0,
task
)
.execute(&ctx.data().database)
.await
.unwrap();
ctx.say("Item added to todo list").await?;
Ok(())
}
/// View and remove from the channel todo list
#[poise::command(slash_command, rename = "view")]
pub async fn todo_channel_view(ctx: Context<'_>) -> Result<(), Error> {
let values = sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?",
ctx.channel_id().0,
)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>();
let resp =
show_todo_page(&values, 0, None, Some(ctx.channel_id().0), ctx.guild_id().map(|g| g.0));
ctx.send(|r| {
*r = resp;
r
})
.await?;
Ok(())
}
/// Manage your personal todo list
#[poise::command(slash_command, rename = "user")]
pub async fn todo_user_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Add an item to your personal todo list
#[poise::command(slash_command, rename = "add")]
pub async fn todo_user_add(
ctx: Context<'_>,
#[description = "The task to add to the todo list"] task: String,
) -> Result<(), Error> {
sqlx::query!(
"INSERT INTO todos (user_id, value)
VALUES ((SELECT id FROM users WHERE user = ?), ?)",
ctx.author().id.0,
task
)
.execute(&ctx.data().database)
.await
.unwrap();
ctx.say("Item added to todo list").await?;
Ok(())
}
/// View and remove from your personal todo list
#[poise::command(slash_command, rename = "view")]
pub async fn todo_user_view(ctx: Context<'_>) -> Result<(), Error> {
let values = sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?",
ctx.author().id.0,
)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
.map(|row| (row.id as usize, row.value.clone()))
.collect::<Vec<(usize, String)>>();
let resp = show_todo_page(&values, 0, Some(ctx.author().id.0), None, None);
ctx.send(|r| {
*r = resp;
r
})
.await?;
Ok(())
}
pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
@ -164,7 +206,7 @@ pub fn show_todo_page(
user_id: Option<u64>,
channel_id: Option<u64>,
guild_id: Option<u64>,
) -> CreateGenericResponse {
) -> CreateReply {
let pager = TodoPager::new(page, user_id, channel_id, guild_id);
let pages = max_todo_page(todo_values);
@ -219,17 +261,23 @@ pub fn show_todo_page(
};
if todo_ids.is_empty() {
CreateGenericResponse::new().embed(|e| {
let mut reply = CreateReply::default();
reply.embed(|e| {
e.title(format!("{} Todo List", title))
.description("Todo List Empty!")
.footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
.color(*THEME_COLOR)
})
});
reply
} else {
let todo_selector =
ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
CreateGenericResponse::new()
let mut reply = CreateReply::default();
reply
.embed(|e| {
e.title(format!("{} Todo List", title))
.description(display)
@ -255,6 +303,8 @@ pub fn show_todo_page(
})
})
})
})
});
reply
}
}

View File

@ -3,17 +3,16 @@ pub(crate) mod pager;
use std::io::Cursor;
use chrono_tz::Tz;
use rmp_serde::Serializer;
use serde::{Deserialize, Serialize};
use serenity::{
use poise::serenity::{
builder::CreateEmbed,
client::Context,
model::{
channel::Channel,
interactions::{message_component::MessageComponentInteraction, InteractionResponseType},
prelude::InteractionApplicationCommandCallbackDataFlags,
},
};
use rmp_serde::Serializer;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
@ -23,9 +22,8 @@ use crate::{
},
component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
framework::CommandInvoke,
models::{command_macro::CommandMacro, reminder::Reminder},
SQLPool,
models::{reminder::Reminder, CtxData},
Context, Data,
};
#[derive(Deserialize, Serialize)]
@ -55,12 +53,12 @@ impl ComponentDataModel {
rmp_serde::from_read(cur).unwrap()
}
pub async fn act(&self, ctx: &Context, component: MessageComponentInteraction) {
pub async fn act(&self, ctx: Context<'_>, component: &MessageComponentInteraction) {
match self {
ComponentDataModel::LookPager(pager) => {
let flags = pager.flags;
let channel_opt = component.channel_id.to_channel_cached(&ctx);
let channel_opt = component.channel_id.to_channel_cached(&ctx.discord());
let channel_id = if let Some(Channel::Guild(channel)) = channel_opt {
if Some(channel.guild_id) == component.guild_id {
@ -72,7 +70,7 @@ impl ComponentDataModel {
component.channel_id
};
let reminders = Reminder::from_channel(ctx, channel_id, &flags).await;
let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await;
let pages = reminders
.iter()
@ -80,12 +78,13 @@ impl ComponentDataModel {
.fold(0, |t, r| t + r.len())
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let channel_name =
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
Some(channel.name)
} else {
None
};
let channel_name = if let Some(Channel::Guild(channel)) =
channel_id.to_channel_cached(&ctx.discord())
{
Some(channel.name)
} else {
None
};
let next_page = pager.next_page(pages);
@ -119,7 +118,7 @@ impl ComponentDataModel {
.color(*THEME_COLOR);
let _ = component
.create_interaction_response(&ctx, |r| {
.create_interaction_response(&ctx.discord(), |r| {
r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
|response| {
response.embeds(vec![embed]).components(|comp| {
@ -134,44 +133,49 @@ impl ComponentDataModel {
}
ComponentDataModel::DelPager(pager) => {
let reminders =
Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
Reminder::from_guild(&ctx, component.guild_id, component.user.id).await;
let max_pages = max_delete_page(&reminders, &pager.timezone);
let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
let mut invoke = CommandInvoke::component(component);
let _ = invoke.respond(&ctx, resp).await;
let _ = ctx
.send(|r| {
*r = resp;
r
})
.await;
}
ComponentDataModel::DelSelector(selector) => {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let selected_id = component.data.values.join(",");
sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
.execute(&pool)
.execute(&ctx.data().database)
.await
.unwrap();
let reminders =
Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
Reminder::from_guild(&ctx, component.guild_id, component.user.id).await;
let resp = show_delete_page(&reminders, selector.page, selector.timezone);
let mut invoke = CommandInvoke::component(component);
let _ = invoke.respond(&ctx, resp).await;
let _ = ctx
.send(|r| {
*r = resp;
r
})
.await;
}
ComponentDataModel::TodoPager(pager) => {
if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let values = if let Some(uid) = pager.user_id {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?",
INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?",
uid,
)
.fetch_all(&pool)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
@ -180,11 +184,11 @@ impl ComponentDataModel {
} else if let Some(cid) = pager.channel_id {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?",
INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?",
cid,
)
.fetch_all(&pool)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
@ -193,11 +197,11 @@ impl ComponentDataModel {
} else {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
pager.guild_id,
)
.fetch_all(&pool)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
@ -215,11 +219,15 @@ impl ComponentDataModel {
pager.guild_id,
);
let mut invoke = CommandInvoke::component(component);
let _ = invoke.respond(&ctx, resp).await;
let _ = ctx
.send(|r| {
*r = resp;
r
})
.await;
} else {
let _ = component
.create_interaction_response(&ctx, |r| {
.create_interaction_response(&ctx.discord(), |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.flags(
@ -233,11 +241,10 @@ impl ComponentDataModel {
}
ComponentDataModel::TodoSelector(selector) => {
if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let selected_id = component.data.values.join(",");
sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
.execute(&pool)
.execute(&ctx.data().database)
.await
.unwrap();
@ -248,7 +255,7 @@ impl ComponentDataModel {
selector.channel_id,
selector.guild_id,
)
.fetch_all(&pool)
.fetch_all(&ctx.data().database)
.await
.unwrap()
.iter()
@ -263,11 +270,15 @@ impl ComponentDataModel {
selector.guild_id,
);
let mut invoke = CommandInvoke::component(component);
let _ = invoke.respond(&ctx, resp).await;
let _ = ctx
.send(|r| {
*r = resp;
r
})
.await;
} else {
let _ = component
.create_interaction_response(&ctx, |r| {
.create_interaction_response(&ctx.discord(), |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.flags(
@ -280,15 +291,19 @@ impl ComponentDataModel {
}
}
ComponentDataModel::MacroPager(pager) => {
let mut invoke = CommandInvoke::component(component);
let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
let macros = ctx.command_macros().await.unwrap();
let max_page = max_macro_page(&macros);
let page = pager.next_page(max_page);
let resp = show_macro_page(&macros, page);
let _ = invoke.respond(&ctx, resp).await;
let _ = ctx
.send(|r| {
*r = resp;
r
})
.await;
}
}
}

View File

@ -1,8 +1,10 @@
// todo split pager out into a single struct
use chrono_tz::Tz;
use poise::serenity::{
builder::CreateComponents, model::interactions::message_component::ButtonStyle,
};
use serde::{Deserialize, Serialize};
use serde_repr::*;
use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle};
use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};

View File

@ -1,4 +1,6 @@
pub const DAY: u64 = 86_400;
pub const HOUR: u64 = 3_600;
pub const MINUTE: u64 = 60;
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
pub const SELECT_MAX_ENTRIES: usize = 25;

View File

@ -1,11 +1,27 @@
use std::{collections::HashMap, env, sync::atomic::Ordering};
use std::{
collections::HashMap,
env,
sync::atomic::{AtomicBool, Ordering},
};
use log::{info, warn};
use poise::serenity::{client::Context, model::interactions::Interaction, utils::shard_id};
use poise::{
serenity::{model::interactions::Interaction, utils::shard_id},
serenity_prelude as serenity,
serenity_prelude::{
ApplicationCommandInteraction, ApplicationCommandInteractionData, ApplicationCommandType,
InteractionType,
},
ApplicationCommandOrAutocompleteInteraction, ApplicationContext, Command,
};
use crate::{Data, Error};
use crate::{component_models::ComponentDataModel, Context, Data, Error};
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
pub async fn listener(
ctx: &serenity::Context,
event: &poise::Event<'_>,
data: &Data,
) -> Result<(), Error> {
match event {
poise::Event::CacheReady { .. } => {
info!("Cache Ready!");
@ -97,15 +113,16 @@ DELETE FROM channels WHERE channel = ?
}
}
}
poise::Event::GuildDelete { incomplete, full } => {
poise::Event::GuildDelete { incomplete, .. } => {
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
.execute(&data.database)
.await;
}
poise::Event::InteractionCreate { interaction } => match interaction {
Interaction::MessageComponent(component) => {
//let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
//component_model.act(&ctx, component).await;
let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
// component_model.act(ctx, component).await;
}
_ => {}
},

View File

@ -1,692 +0,0 @@
// todo move framework to its own module, split out permission checks
use std::{
collections::{HashMap, HashSet},
hash::{Hash, Hasher},
sync::Arc,
};
use log::info;
use serde::{Deserialize, Serialize};
use serenity::{
builder::{CreateApplicationCommands, CreateComponents, CreateEmbed},
cache::Cache,
client::Context,
futures::prelude::future::BoxFuture,
http::Http,
model::{
guild::Guild,
id::{ChannelId, GuildId, RoleId, UserId},
interactions::{
application_command::{
ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
},
message_component::MessageComponentInteraction,
InteractionApplicationCommandCallbackDataFlags, InteractionResponseType,
},
prelude::application_command::ApplicationCommandInteractionDataOption,
},
prelude::TypeMapKey,
Result as SerenityResult,
};
use crate::SQLPool;
pub struct CreateGenericResponse {
content: String,
embed: Option<CreateEmbed>,
components: Option<CreateComponents>,
flags: InteractionApplicationCommandCallbackDataFlags,
}
impl CreateGenericResponse {
pub fn new() -> Self {
Self {
content: "".to_string(),
embed: None,
components: None,
flags: InteractionApplicationCommandCallbackDataFlags::empty(),
}
}
pub fn ephemeral(mut self) -> Self {
self.flags.insert(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
self
}
pub fn content<D: ToString>(mut self, content: D) -> Self {
self.content = content.to_string();
self
}
pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self {
let mut embed = CreateEmbed::default();
f(&mut embed);
self.embed = Some(embed);
self
}
pub fn components<F: FnOnce(&mut CreateComponents) -> &mut CreateComponents>(
mut self,
f: F,
) -> Self {
let mut components = CreateComponents::default();
f(&mut components);
self.components = Some(components);
self
}
}
#[derive(Clone)]
enum InvokeModel {
Slash(ApplicationCommandInteraction),
Component(MessageComponentInteraction),
}
#[derive(Clone)]
pub struct CommandInvoke {
model: InvokeModel,
already_responded: bool,
deferred: bool,
}
impl CommandInvoke {
pub fn component(component: MessageComponentInteraction) -> Self {
Self { model: InvokeModel::Component(component), already_responded: false, deferred: false }
}
fn slash(interaction: ApplicationCommandInteraction) -> Self {
Self { model: InvokeModel::Slash(interaction), already_responded: false, deferred: false }
}
pub async fn defer(&mut self, http: impl AsRef<Http>) {
if !self.deferred {
match &self.model {
InvokeModel::Slash(i) => {
i.create_interaction_response(http, |r| {
r.kind(InteractionResponseType::DeferredChannelMessageWithSource)
})
.await
.unwrap();
self.deferred = true;
}
InvokeModel::Component(i) => {
i.create_interaction_response(http, |r| {
r.kind(InteractionResponseType::DeferredChannelMessageWithSource)
})
.await
.unwrap();
self.deferred = true;
}
}
}
}
pub fn channel_id(&self) -> ChannelId {
match &self.model {
InvokeModel::Slash(i) => i.channel_id,
InvokeModel::Component(i) => i.channel_id,
}
}
pub fn guild_id(&self) -> Option<GuildId> {
match &self.model {
InvokeModel::Slash(i) => i.guild_id,
InvokeModel::Component(i) => i.guild_id,
}
}
pub fn guild(&self, cache: impl AsRef<Cache>) -> Option<Guild> {
self.guild_id().map(|id| id.to_guild_cached(cache)).flatten()
}
pub fn author_id(&self) -> UserId {
match &self.model {
InvokeModel::Slash(i) => i.user.id,
InvokeModel::Component(i) => i.user.id,
}
}
pub async fn respond(
&mut self,
http: impl AsRef<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()> {
match &self.model {
InvokeModel::Slash(i) => {
if self.already_responded {
i.create_followup_message(http, |d| {
d.allowed_mentions(|m| m.empty_parse());
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed);
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
.await
.map(|_| ())
} else if self.deferred {
i.edit_original_interaction_response(http, |d| {
d.allowed_mentions(|m| m.empty_parse());
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed);
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
.await
.map(|_| ())
} else {
i.create_interaction_response(http, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.allowed_mentions(|m| m.empty_parse());
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed);
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
})
.await
.map(|_| ())
}
}
InvokeModel::Component(i) => i
.create_interaction_response(http, |r| {
r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(|d| {
d.allowed_mentions(|m| m.empty_parse());
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed);
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
})
.await
.map(|_| ()),
}?;
self.already_responded = true;
Ok(())
}
}
#[derive(Debug)]
pub struct Arg {
pub name: &'static str,
pub description: &'static str,
pub kind: ApplicationCommandOptionType,
pub required: bool,
pub options: &'static [&'static Self],
}
#[derive(Serialize, Deserialize, Clone)]
pub enum OptionValue {
String(String),
Integer(i64),
Boolean(bool),
User(UserId),
Channel(ChannelId),
Role(RoleId),
Mentionable(u64),
Number(f64),
}
impl OptionValue {
pub fn as_i64(&self) -> Option<i64> {
match self {
OptionValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
OptionValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_channel_id(&self) -> Option<ChannelId> {
match self {
OptionValue::Channel(c) => Some(*c),
_ => None,
}
}
pub fn to_string(&self) -> String {
match self {
OptionValue::String(s) => s.to_string(),
OptionValue::Integer(i) => i.to_string(),
OptionValue::Boolean(b) => b.to_string(),
OptionValue::User(u) => u.to_string(),
OptionValue::Channel(c) => c.to_string(),
OptionValue::Role(r) => r.to_string(),
OptionValue::Mentionable(m) => m.to_string(),
OptionValue::Number(n) => n.to_string(),
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CommandOptions {
pub command: String,
pub subcommand: Option<String>,
pub subcommand_group: Option<String>,
pub options: HashMap<String, OptionValue>,
}
impl CommandOptions {
pub fn get(&self, key: &str) -> Option<&OptionValue> {
self.options.get(key)
}
}
impl CommandOptions {
fn new(command: &'static Command) -> Self {
Self {
command: command.names[0].to_string(),
subcommand: None,
subcommand_group: None,
options: Default::default(),
}
}
fn populate(mut self, interaction: &ApplicationCommandInteraction) -> Self {
fn match_option(
option: ApplicationCommandInteractionDataOption,
cmd_opts: &mut CommandOptions,
) {
match option.kind {
ApplicationCommandOptionType::SubCommand => {
cmd_opts.subcommand = Some(option.name);
for opt in option.options {
match_option(opt, cmd_opts);
}
}
ApplicationCommandOptionType::SubCommandGroup => {
cmd_opts.subcommand_group = Some(option.name);
for opt in option.options {
match_option(opt, cmd_opts);
}
}
ApplicationCommandOptionType::String => {
cmd_opts.options.insert(
option.name,
OptionValue::String(option.value.unwrap().as_str().unwrap().to_string()),
);
}
ApplicationCommandOptionType::Integer => {
cmd_opts.options.insert(
option.name,
OptionValue::Integer(option.value.map(|m| m.as_i64()).flatten().unwrap()),
);
}
ApplicationCommandOptionType::Boolean => {
cmd_opts.options.insert(
option.name,
OptionValue::Boolean(option.value.map(|m| m.as_bool()).flatten().unwrap()),
);
}
ApplicationCommandOptionType::User => {
cmd_opts.options.insert(
option.name,
OptionValue::User(UserId(
option
.value
.map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
.flatten()
.flatten()
.unwrap(),
)),
);
}
ApplicationCommandOptionType::Channel => {
cmd_opts.options.insert(
option.name,
OptionValue::Channel(ChannelId(
option
.value
.map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
.flatten()
.flatten()
.unwrap(),
)),
);
}
ApplicationCommandOptionType::Role => {
cmd_opts.options.insert(
option.name,
OptionValue::Role(RoleId(
option
.value
.map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
.flatten()
.flatten()
.unwrap(),
)),
);
}
ApplicationCommandOptionType::Mentionable => {
cmd_opts.options.insert(
option.name,
OptionValue::Mentionable(
option.value.map(|m| m.as_u64()).flatten().unwrap(),
),
);
}
ApplicationCommandOptionType::Number => {
cmd_opts.options.insert(
option.name,
OptionValue::Number(option.value.map(|m| m.as_f64()).flatten().unwrap()),
);
}
_ => {}
}
}
for option in &interaction.data.options {
match_option(option.clone(), &mut self)
}
self
}
}
pub enum HookResult {
Continue,
Halt,
}
type SlashCommandFn =
for<'fut> fn(&'fut Context, &'fut mut CommandInvoke, CommandOptions) -> BoxFuture<'fut, ()>;
type MultiCommandFn = for<'fut> fn(&'fut Context, &'fut mut CommandInvoke) -> BoxFuture<'fut, ()>;
pub type HookFn = for<'fut> fn(
&'fut Context,
&'fut mut CommandInvoke,
&'fut CommandOptions,
) -> BoxFuture<'fut, HookResult>;
pub enum CommandFnType {
Slash(SlashCommandFn),
Multi(MultiCommandFn),
}
pub struct Hook {
pub fun: HookFn,
pub uuid: u128,
}
impl PartialEq for Hook {
fn eq(&self, other: &Self) -> bool {
self.uuid == other.uuid
}
}
pub struct Command {
pub fun: CommandFnType,
pub names: &'static [&'static str],
pub desc: &'static str,
pub examples: &'static [&'static str],
pub group: &'static str,
pub args: &'static [&'static Arg],
pub can_blacklist: bool,
pub supports_dm: bool,
pub hooks: &'static [&'static Hook],
}
impl Hash for Command {
fn hash<H: Hasher>(&self, state: &mut H) {
self.names[0].hash(state)
}
}
impl PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.names[0] == other.names[0]
}
}
impl Eq for Command {}
pub struct RegexFramework {
pub commands_map: HashMap<String, &'static Command>,
pub commands: HashSet<&'static Command>,
ignore_bots: bool,
dm_enabled: bool,
debug_guild: Option<GuildId>,
hooks: Vec<&'static Hook>,
}
impl TypeMapKey for RegexFramework {
type Value = Arc<RegexFramework>;
}
impl RegexFramework {
pub fn new() -> Self {
Self {
commands_map: HashMap::new(),
commands: HashSet::new(),
ignore_bots: true,
dm_enabled: true,
debug_guild: None,
hooks: vec![],
}
}
pub fn ignore_bots(mut self, ignore_bots: bool) -> Self {
self.ignore_bots = ignore_bots;
self
}
pub fn dm_enabled(mut self, dm_enabled: bool) -> Self {
self.dm_enabled = dm_enabled;
self
}
pub fn add_hook(mut self, fun: &'static Hook) -> Self {
self.hooks.push(fun);
self
}
pub fn add_command(mut self, command: &'static Command) -> Self {
self.commands.insert(command);
for name in command.names {
self.commands_map.insert(name.to_string(), command);
}
self
}
pub fn debug_guild(mut self, guild_id: Option<GuildId>) -> Self {
self.debug_guild = guild_id;
self
}
fn _populate_commands<'a>(
&self,
commands: &'a mut CreateApplicationCommands,
) -> &'a mut CreateApplicationCommands {
for command in &self.commands {
commands.create_application_command(|c| {
c.name(command.names[0]).description(command.desc);
for arg in command.args {
c.create_option(|o| {
o.name(arg.name)
.description(arg.description)
.kind(arg.kind)
.required(arg.required);
for option in arg.options {
o.create_sub_option(|s| {
s.name(option.name)
.description(option.description)
.kind(option.kind)
.required(option.required);
for sub_option in option.options {
s.create_sub_option(|ss| {
ss.name(sub_option.name)
.description(sub_option.description)
.kind(sub_option.kind)
.required(sub_option.required)
});
}
s
});
}
o
});
}
c
});
}
commands
}
pub async fn build_slash(&self, http: impl AsRef<Http>) {
info!("Building slash commands...");
match self.debug_guild {
None => {
ApplicationCommand::set_global_application_commands(&http, |c| {
self._populate_commands(c)
})
.await
.unwrap();
}
Some(debug_guild) => {
debug_guild
.set_application_commands(&http, |c| self._populate_commands(c))
.await
.unwrap();
}
}
info!("Slash commands built!");
}
pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) {
{
if let Some(guild_id) = interaction.guild_id {
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
let _ = sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0)
.execute(&pool)
.await;
}
}
let command = {
self.commands_map
.get(&interaction.data.name)
.expect(&format!("Received invalid command: {}", interaction.data.name))
};
let args = CommandOptions::new(command).populate(&interaction);
let mut command_invoke = CommandInvoke::slash(interaction);
for hook in command.hooks {
match (hook.fun)(&ctx, &mut command_invoke, &args).await {
HookResult::Continue => {}
HookResult::Halt => {
return;
}
}
}
for hook in &self.hooks {
match (hook.fun)(&ctx, &mut command_invoke, &args).await {
HookResult::Continue => {}
HookResult::Halt => {
return;
}
}
}
match command.fun {
CommandFnType::Slash(t) => t(&ctx, &mut command_invoke, args).await,
CommandFnType::Multi(m) => m(&ctx, &mut command_invoke).await,
}
}
pub async fn run_command_from_options(
&self,
ctx: &Context,
command_invoke: &mut CommandInvoke,
command_options: CommandOptions,
) {
let command = {
self.commands_map
.get(&command_options.command)
.expect(&format!("Received invalid command: {}", command_options.command))
};
match command.fun {
CommandFnType::Slash(t) => t(&ctx, command_invoke, command_options).await,
CommandFnType::Multi(m) => m(&ctx, command_invoke).await,
}
}
}

View File

@ -1,6 +1,6 @@
use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction};
use crate::{consts::MACRO_MAX_COMMANDS, Context, Error};
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
pub async fn guild_only(ctx: Context<'_>) -> Result<bool, Error> {
if ctx.guild_id().is_some() {
@ -25,12 +25,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
let _ = ctx.send(|m| {
m.ephemeral(true).content(
"5 commands already recorded. Please use `/macro finish` to end recording.",
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
)
})
.await;
} else {
// TODO TODO TODO write command to macro
let recorded = RecordedCommand {
action: None,
command_name: ctx.command().identifying_name.clone(),
options: Vec::from(app_ctx.args),
};
command_macro.commands.push(recorded);
let _ = ctx
.send(|m| m.ephemeral(true).content("Command recorded to macro"))

View File

@ -3,7 +3,7 @@
extern crate lazy_static;
mod commands;
// mod component_models;
mod component_models;
mod consts;
mod event_handlers;
mod hooks;
@ -24,7 +24,7 @@ use sqlx::{MySql, Pool};
use tokio::sync::RwLock;
use crate::{
commands::{info_cmds, moderation_cmds},
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
consts::THEME_COLOR,
event_handlers::listener,
hooks::all_checks,
@ -71,6 +71,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
],
..moderation_cmds::macro_base()
},
reminder_cmds::pause(),
reminder_cmds::offset(),
reminder_cmds::nudge(),
reminder_cmds::look(),
reminder_cmds::delete(),
poise::Command {
subcommands: vec![
reminder_cmds::list_timer(),
reminder_cmds::start_timer(),
reminder_cmds::delete_timer(),
],
..reminder_cmds::timer_base()
},
reminder_cmds::remind(),
poise::Command {
subcommands: vec![
poise::Command {
subcommands: vec![
todo_cmds::todo_guild_add(),
todo_cmds::todo_guild_view(),
],
..todo_cmds::todo_guild_base()
},
poise::Command {
subcommands: vec![
todo_cmds::todo_channel_add(),
todo_cmds::todo_channel_view(),
],
..todo_cmds::todo_channel_base()
},
poise::Command {
subcommands: vec![todo_cmds::todo_user_add(), todo_cmds::todo_user_view()],
..todo_cmds::todo_user_base()
},
],
..todo_cmds::todo_base()
},
],
allowed_mentions: None,
command_check: Some(|ctx| Box::pin(all_checks(ctx))),

View File

@ -1,20 +1,29 @@
use poise::serenity::{
client::Context,
model::{
id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption,
},
use poise::serenity::model::{
id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
use crate::{Context, Data, Error};
fn default_none<U, E>() -> Option<
for<'a> fn(
poise::ApplicationContext<'a, U, E>,
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
> {
None
}
#[derive(Serialize, Deserialize)]
pub struct RecordedCommand<U, E> {
#[serde(skip)]
action: for<'a> fn(
poise::ApplicationContext<'a, U, E>,
&'a [ApplicationCommandInteractionDataOption],
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
command_name: String,
options: Vec<ApplicationCommandInteractionDataOption>,
#[serde(default = "default_none::<U, E>")]
pub action: Option<
for<'a> fn(
poise::ApplicationContext<'a, U, E>,
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
>,
pub command_name: String,
pub options: Vec<ApplicationCommandInteractionDataOption>,
}
pub struct CommandMacro<U, E> {
@ -23,3 +32,42 @@ pub struct CommandMacro<U, E> {
pub description: Option<String>,
pub commands: Vec<RecordedCommand<U, E>>,
}
pub async fn guild_command_macro(
ctx: &Context<'_>,
name: &str,
) -> Option<CommandMacro<Data, Error>> {
let row = sqlx::query!(
"
SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
",
ctx.guild_id().unwrap().0,
name
)
.fetch_one(&ctx.data().database)
.await
.ok()?;
let mut commands: Vec<RecordedCommand<Data, Error>> =
serde_json::from_str(&row.commands).unwrap();
for recorded_command in &mut commands {
let command = &ctx
.framework()
.options()
.commands
.iter()
.find(|c| c.identifying_name == recorded_command.command_name);
recorded_command.action = command.map(|c| c.slash_action).flatten().clone();
}
let command_macro = CommandMacro {
guild_id: ctx.guild_id().unwrap(),
name: row.name,
description: row.description,
commands,
};
Some(command_macro)
}

View File

@ -9,21 +9,20 @@ use poise::serenity::{async_trait, model::id::UserId};
use crate::{
models::{channel_data::ChannelData, user_data::UserData},
Context,
CommandMacro, Context, Data, Error,
};
#[async_trait]
pub trait CtxData {
async fn user_data<U: Into<UserId> + Send>(
&self,
user_id: U,
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
async fn author_data(&self) -> Result<UserData, Error>;
async fn timezone(&self) -> Tz;
async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
async fn channel_data(&self) -> Result<ChannelData, Error>;
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
}
#[async_trait]
@ -48,4 +47,22 @@ impl CtxData for Context<'_> {
ChannelData::from_channel(&channel, &self.data().database).await
}
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
let guild_id = self.guild_id().unwrap();
let rows = sqlx::query!(
"SELECT name, description FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
guild_id.0
)
.fetch_all(&self.data().database)
.await?.iter().map(|row| CommandMacro {
guild_id,
name: row.name.clone(),
description: row.description.clone(),
commands: vec![]
}).collect();
Ok(rows)
}
}