858 lines
27 KiB
Rust
858 lines
27 KiB
Rust
use std::{collections::HashSet, string::ToString};
|
|
|
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
|
use chrono_tz::Tz;
|
|
use log::warn;
|
|
use num_integer::Integer;
|
|
use poise::{
|
|
serenity_prelude::{
|
|
builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType,
|
|
},
|
|
CreateReply, Modal,
|
|
};
|
|
|
|
use crate::{
|
|
commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
|
|
component_models::{
|
|
pager::{DelPager, LookPager, Pager},
|
|
ComponentDataModel, DelSelector, UndoReminder,
|
|
},
|
|
consts::{
|
|
EMBED_DESCRIPTION_MAX_LENGTH, HOUR, MINUTE, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES,
|
|
THEME_COLOR,
|
|
},
|
|
interval_parser::parse_duration,
|
|
models::{
|
|
reminder::{
|
|
builder::{MultiReminderBuilder, ReminderScope},
|
|
content::Content,
|
|
errors::ReminderError,
|
|
look_flags::{LookFlags, TimeDisplayType},
|
|
Reminder,
|
|
},
|
|
timer::Timer,
|
|
CtxData,
|
|
},
|
|
time_parser::natural_parser,
|
|
utils::{check_guild_subscription, check_subscription},
|
|
ApplicationContext, Context, Error,
|
|
};
|
|
|
|
/// Pause all reminders on the current channel until a certain time or indefinitely
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "pause",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
pub async fn pause(
|
|
ctx: Context<'_>,
|
|
#[description = "When to pause until"] until: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let timezone = ctx.timezone().await;
|
|
|
|
let mut channel = ctx.channel_data().await.unwrap();
|
|
|
|
match 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);
|
|
|
|
channel.commit_changes(&ctx.data().database).await;
|
|
|
|
ctx.say(format!(
|
|
"Reminders in this channel have been silenced until **<t:{}:D>**",
|
|
timestamp
|
|
))
|
|
.await?;
|
|
}
|
|
|
|
None => {
|
|
ctx.say(format!(
|
|
"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?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Move all reminders in the current server by a certain amount of time. Times get added together
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "offset",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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> {
|
|
ctx.defer().await?;
|
|
|
|
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 {
|
|
ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
|
|
} else {
|
|
if let Some(guild) = ctx.guild() {
|
|
let channels = guild
|
|
.channels
|
|
.iter()
|
|
.filter(|(_, channel)| match channel {
|
|
Channel::Guild(guild_channel) => guild_channel.is_text_based(),
|
|
_ => false,
|
|
})
|
|
.map(|(id, _)| id.0.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!(
|
|
"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().0
|
|
)
|
|
.execute(&ctx.data().database)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "nudge",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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);
|
|
|
|
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().await.unwrap();
|
|
|
|
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?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// View reminders on a specific channel
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "look",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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: 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
|
|
}
|
|
}),
|
|
};
|
|
|
|
let channel_opt = ctx.channel_id().to_channel_cached(&ctx);
|
|
|
|
let channel_id = if let Some(Channel::Guild(channel)) = channel_opt {
|
|
if Some(channel.guild_id) == ctx.guild_id() {
|
|
flags.channel_id.unwrap_or_else(|| ctx.channel_id())
|
|
} else {
|
|
ctx.channel_id()
|
|
}
|
|
} else {
|
|
ctx.channel_id()
|
|
};
|
|
|
|
let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
|
Some(channel.name)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
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(|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?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete reminders
|
|
#[poise::command(
|
|
slash_command,
|
|
rename = "del",
|
|
identifying_name = "delete",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
pub async fn delete(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 resp = show_delete_page(&reminders, 0, timezone);
|
|
|
|
ctx.send(|r| {
|
|
*r = resp;
|
|
r
|
|
})
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize {
|
|
let mut rows = 0;
|
|
let mut char_count = 0;
|
|
|
|
reminders
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(count, reminder)| reminder.display_del(count, timezone))
|
|
.fold(1, |mut pages, reminder| {
|
|
rows += 1;
|
|
char_count += reminder.len();
|
|
|
|
if char_count > EMBED_DESCRIPTION_MAX_LENGTH || rows > SELECT_MAX_ENTRIES {
|
|
rows = 1;
|
|
char_count = reminder.len();
|
|
pages += 1;
|
|
}
|
|
|
|
pages
|
|
})
|
|
}
|
|
|
|
pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> CreateReply {
|
|
let pager = DelPager::new(page, timezone);
|
|
|
|
if reminders.is_empty() {
|
|
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);
|
|
|
|
let mut page = page;
|
|
if page >= pages {
|
|
page = pages - 1;
|
|
}
|
|
|
|
let mut char_count = 0;
|
|
let mut rows = 0;
|
|
let mut skipped_rows = 0;
|
|
let mut skipped_char_count = 0;
|
|
let mut first_num = 0;
|
|
|
|
let mut skipped_pages = 0;
|
|
|
|
let (shown_reminders, display_vec): (Vec<&Reminder>, Vec<String>) = reminders
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(count, reminder)| (reminder, reminder.display_del(count, &timezone)))
|
|
.skip_while(|(_, p)| {
|
|
first_num += 1;
|
|
skipped_rows += 1;
|
|
skipped_char_count += p.len();
|
|
|
|
if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH
|
|
|| skipped_rows > SELECT_MAX_ENTRIES
|
|
{
|
|
skipped_rows = 1;
|
|
skipped_char_count = p.len();
|
|
skipped_pages += 1;
|
|
}
|
|
|
|
skipped_pages < page
|
|
})
|
|
.take_while(|(_, p)| {
|
|
rows += 1;
|
|
char_count += p.len();
|
|
|
|
char_count < EMBED_DESCRIPTION_MAX_LENGTH && rows <= SELECT_MAX_ENTRIES
|
|
})
|
|
.unzip();
|
|
|
|
let display = display_vec.join("\n");
|
|
|
|
let del_selector = ComponentDataModel::DelSelector(DelSelector { page, timezone });
|
|
|
|
let mut reply = CreateReply::default();
|
|
|
|
reply
|
|
.embed(|e| {
|
|
e.title("Delete Reminders")
|
|
.description(display)
|
|
.footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
|
|
.color(*THEME_COLOR)
|
|
})
|
|
.components(|comp| {
|
|
pager.create_button_row(pages, comp);
|
|
|
|
comp.create_action_row(|row| {
|
|
row.create_select_menu(|menu| {
|
|
menu.custom_id(del_selector.to_custom_id()).options(|opt| {
|
|
for (count, reminder) in shown_reminders.iter().enumerate() {
|
|
opt.create_option(|o| {
|
|
o.label(count + first_num).value(reminder.id).description({
|
|
let c = reminder.display_content();
|
|
|
|
if c.len() > 100 {
|
|
format!(
|
|
"{}...",
|
|
reminder
|
|
.display_content()
|
|
.chars()
|
|
.take(97)
|
|
.collect::<String>()
|
|
)
|
|
} else {
|
|
c.to_string()
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
opt
|
|
})
|
|
})
|
|
})
|
|
});
|
|
|
|
reply
|
|
}
|
|
|
|
fn time_difference(start_time: DateTime<Utc>) -> String {
|
|
let delta = (Utc::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",
|
|
identifying_name = "timer_base",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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",
|
|
identifying_name = "list_timer",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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(())
|
|
}
|
|
|
|
/// Start a new timer from now
|
|
#[poise::command(
|
|
slash_command,
|
|
rename = "start",
|
|
identifying_name = "start_timer",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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 count = Timer::count_from_owner(owner, &ctx.data().database).await;
|
|
|
|
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;
|
|
|
|
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?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete a timer
|
|
#[poise::command(
|
|
slash_command,
|
|
rename = "delete",
|
|
identifying_name = "delete_timer",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
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;
|
|
|
|
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?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(poise::Modal)]
|
|
#[name = "Reminder"]
|
|
struct ContentModal {
|
|
#[name = "Content"]
|
|
#[placeholder = "Message..."]
|
|
#[paragraph]
|
|
#[max_length = 2000]
|
|
content: String,
|
|
}
|
|
|
|
/// Create a reminder with multi-line content. Press "+4 more" for other options.
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "multiline",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
pub async fn multiline(
|
|
ctx: ApplicationContext<'_>,
|
|
#[description = "A description of the time to set the reminder for"]
|
|
#[autocomplete = "time_hint_autocomplete"]
|
|
time: 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 repeating"]
|
|
expires: Option<String>,
|
|
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
|
tts: Option<bool>,
|
|
#[description = "Set a timezone override for this reminder only"]
|
|
#[autocomplete = "timezone_autocomplete"]
|
|
timezone: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
|
|
let data_opt = ContentModal::execute(ctx).await?;
|
|
|
|
match data_opt {
|
|
Some(data) => {
|
|
create_reminder(
|
|
Context::Application(ctx),
|
|
time,
|
|
data.content,
|
|
channels,
|
|
interval,
|
|
expires,
|
|
tts,
|
|
tz,
|
|
)
|
|
.await
|
|
}
|
|
|
|
None => {
|
|
warn!("Unexpected None encountered in /multiline");
|
|
Ok(Context::Application(ctx)
|
|
.send(|m| m.content("Unexpected error.").ephemeral(true))
|
|
.await
|
|
.map(|_| ())?)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content.
|
|
#[poise::command(
|
|
slash_command,
|
|
identifying_name = "remind",
|
|
default_member_permissions = "MANAGE_GUILD"
|
|
)]
|
|
pub async fn remind(
|
|
ctx: ApplicationContext<'_>,
|
|
#[description = "The time (and optionally date) to set the reminder for"]
|
|
#[autocomplete = "time_hint_autocomplete"]
|
|
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 repeating"]
|
|
expires: Option<String>,
|
|
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
|
tts: Option<bool>,
|
|
#[description = "Set a timezone override for this reminder only"]
|
|
#[autocomplete = "timezone_autocomplete"]
|
|
timezone: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
|
|
|
|
create_reminder(Context::Application(ctx), time, content, channels, interval, expires, tts, tz)
|
|
.await
|
|
}
|
|
|
|
async fn create_reminder(
|
|
ctx: Context<'_>,
|
|
time: String,
|
|
content: String,
|
|
channels: Option<String>,
|
|
interval: Option<String>,
|
|
expires: Option<String>,
|
|
tts: Option<bool>,
|
|
timezone: Option<Tz>,
|
|
) -> Result<(), Error> {
|
|
if interval.is_none() && expires.is_some() {
|
|
ctx.say("`expires` can only be used with `interval`").await?;
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let ephemeral =
|
|
ctx.guild_data().await.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations));
|
|
if ephemeral {
|
|
ctx.defer_ephemeral().await?;
|
|
} else {
|
|
ctx.defer().await?;
|
|
}
|
|
|
|
let user_data = ctx.author_data().await.unwrap();
|
|
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
|
|
|
let time = natural_parser(&time, &timezone.to_string()).await;
|
|
|
|
match time {
|
|
Some(time) => {
|
|
let content = {
|
|
let tts = tts.unwrap_or(false);
|
|
|
|
Content { content, tts, attachment: None, attachment_name: None }
|
|
};
|
|
|
|
let scopes = {
|
|
let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
|
|
|
|
if list.is_empty() {
|
|
if ctx.guild_id().is_some() {
|
|
vec![ReminderScope::Channel(ctx.channel_id().0)]
|
|
} else {
|
|
vec![ReminderScope::User(ctx.author().id.0)]
|
|
}
|
|
} else {
|
|
list
|
|
}
|
|
};
|
|
|
|
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
|
|
if check_subscription(&ctx, ctx.author().id).await
|
|
|| (ctx.guild_id().is_some()
|
|
&& check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await)
|
|
{
|
|
(
|
|
parse_duration(repeat)
|
|
.or_else(|_| parse_duration(&format!("1 {}", repeat)))
|
|
.ok(),
|
|
{
|
|
if let Some(arg) = &expires {
|
|
natural_parser(arg, &timezone.to_string()).await
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
)
|
|
} else {
|
|
ctx.send(|b| {
|
|
b.content(
|
|
"`repeat` is only available to Patreon subscribers or self-hosted users")
|
|
})
|
|
.await?;
|
|
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
if processed_interval.is_none() && interval.is_some() {
|
|
ctx.send(|b| {
|
|
b.content(
|
|
"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.send(|b| {
|
|
b.ephemeral(true).content(
|
|
"Expiry time failed to process. Please make it as clear as possible",
|
|
)
|
|
})
|
|
.await?;
|
|
} else {
|
|
let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id())
|
|
.author(user_data)
|
|
.content(content)
|
|
.time(time)
|
|
.timezone(timezone)
|
|
.expires(processed_expires)
|
|
.interval(processed_interval);
|
|
|
|
builder.set_scopes(scopes);
|
|
|
|
let (errors, successes) = builder.build().await;
|
|
|
|
let embed = create_response(&successes, &errors, time);
|
|
|
|
if successes.len() == 1 {
|
|
let reminder = successes.iter().next().map(|(r, _)| r.id).unwrap();
|
|
let undo_button = ComponentDataModel::UndoReminder(UndoReminder {
|
|
user_id: ctx.author().id,
|
|
reminder_id: reminder,
|
|
});
|
|
|
|
ctx.send(|m| {
|
|
m.embed(|c| {
|
|
*c = embed;
|
|
c
|
|
})
|
|
.components(|c| {
|
|
c.create_action_row(|r| {
|
|
r.create_button(|b| {
|
|
b.emoji(ReactionType::Unicode("🔕".to_string()))
|
|
.label("Cancel")
|
|
.style(ButtonStyle::Danger)
|
|
.custom_id(undo_button.to_custom_id())
|
|
})
|
|
.create_button(|b| {
|
|
b.emoji(ReactionType::Unicode("📝".to_string()))
|
|
.label("Edit")
|
|
.style(ButtonStyle::Link)
|
|
.url("https://beta.reminder-bot.com/dashboard")
|
|
})
|
|
})
|
|
})
|
|
})
|
|
.await?;
|
|
} else {
|
|
ctx.send(|m| {
|
|
m.embed(|c| {
|
|
*c = embed;
|
|
c
|
|
})
|
|
})
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
None => {
|
|
ctx.say("Time could not be processed").await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_response(
|
|
successes: &HashSet<(Reminder, ReminderScope)>,
|
|
errors: &HashSet<ReminderError>,
|
|
time: i64,
|
|
) -> CreateEmbed {
|
|
let success_part = match successes.len() {
|
|
0 => "".to_string(),
|
|
n => format!(
|
|
"Reminder{s} for {locations} set for <t:{offset}:R>",
|
|
s = if n > 1 { "s" } else { "" },
|
|
locations =
|
|
successes.iter().map(|(_, l)| l.mention()).collect::<Vec<String>>().join(", "),
|
|
offset = time
|
|
),
|
|
};
|
|
|
|
let error_part = match errors.len() {
|
|
0 => "".to_string(),
|
|
n => format!(
|
|
"{n} reminder{s} failed to set:\n{errors}",
|
|
s = if n > 1 { "s" } else { "" },
|
|
n = n,
|
|
errors = errors.iter().map(|e| e.to_string()).collect::<Vec<String>>().join("\n")
|
|
),
|
|
};
|
|
|
|
let mut embed = CreateEmbed::default();
|
|
embed
|
|
.title(format!(
|
|
"{n} Reminder{s} Set",
|
|
n = successes.len(),
|
|
s = if successes.len() > 1 { "s" } else { "" }
|
|
))
|
|
.description(format!("{}\n\n{}", success_part, error_part))
|
|
.color(*THEME_COLOR);
|
|
|
|
embed
|
|
}
|
|
|
|
fn parse_mention_list(mentions: &str) -> Vec<ReminderScope> {
|
|
REGEX_CHANNEL_USER
|
|
.captures_iter(mentions)
|
|
.map(|i| {
|
|
let pref = i.get(1).unwrap().as_str();
|
|
let id = i.get(2).unwrap().as_str().parse::<u64>().unwrap();
|
|
|
|
if pref == "#" {
|
|
ReminderScope::Channel(id)
|
|
} else {
|
|
ReminderScope::User(id)
|
|
}
|
|
})
|
|
.collect::<Vec<ReminderScope>>()
|
|
}
|