cleared up all unwraps from the reminder sender. cleared up clippy lints. added undo button

This commit is contained in:
jude 2022-05-13 23:08:52 +01:00
parent 8bad95510d
commit 7d43aa5918
15 changed files with 318 additions and 158 deletions

View File

@ -58,10 +58,10 @@ fn fmt_displacement(format: &str, seconds: u64) -> String {
pub fn substitute(string: &str) -> String { pub fn substitute(string: &str) -> String {
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| { let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
let final_time = caps.name("time").unwrap().as_str(); let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
let format = caps.name("format").unwrap().as_str(); let format = caps.name("format").map(|m| m.as_str());
if let Ok(final_time) = final_time.parse::<i64>() { if let (Some(final_time), Some(format)) = (final_time, format) {
let dt = NaiveDateTime::from_timestamp(final_time, 0); let dt = NaiveDateTime::from_timestamp(final_time, 0);
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
@ -81,13 +81,11 @@ pub fn substitute(string: &str) -> String {
TIMENOW_REGEX TIMENOW_REGEX
.replace(&new, |caps: &Captures| { .replace(&new, |caps: &Captures| {
let timezone = caps.name("timezone").unwrap().as_str(); let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
let format = caps.name("format").map(|m| m.as_str());
println!("{}", timezone); if let (Some(timezone), Some(format)) = (timezone, format) {
let now = Utc::now().with_timezone(&timezone);
if let Ok(tz) = timezone.parse::<Tz>() {
let format = caps.name("format").unwrap().as_str();
let now = Utc::now().with_timezone(&tz);
now.format(format).to_string() now.format(format).to_string()
} else { } else {
@ -122,7 +120,7 @@ impl Embed {
pool: impl Executor<'_, Database = Database> + Copy, pool: impl Executor<'_, Database = Database> + Copy,
id: u32, id: u32,
) -> Option<Self> { ) -> Option<Self> {
let mut embed = sqlx::query_as!( match sqlx::query_as!(
Self, Self,
r#" r#"
SELECT SELECT
@ -142,21 +140,29 @@ impl Embed {
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
.unwrap(); {
Ok(mut embed) => {
embed.title = substitute(&embed.title);
embed.description = substitute(&embed.description);
embed.footer = substitute(&embed.footer);
embed.title = substitute(&embed.title); embed.fields.iter_mut().for_each(|mut field| {
embed.description = substitute(&embed.description); field.title = substitute(&field.title);
embed.footer = substitute(&embed.footer); field.value = substitute(&field.value);
});
embed.fields.iter_mut().for_each(|mut field| { if embed.has_content() {
field.title = substitute(&field.title); Some(embed)
field.value = substitute(&field.value); } else {
}); None
}
}
if embed.has_content() { Err(e) => {
Some(embed) warn!("Error loading embed from reminder: {:?}", e);
} else {
None None
}
} }
} }
@ -251,9 +257,9 @@ pub struct Reminder {
impl Reminder { impl Reminder {
pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> { pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
sqlx::query_as_unchecked!( match sqlx::query_as!(
Reminder, Reminder,
" r#"
SELECT SELECT
reminders.`id` AS id, reminders.`id` AS id,
@ -261,20 +267,20 @@ SELECT
channels.`webhook_id` AS webhook_id, channels.`webhook_id` AS webhook_id,
channels.`webhook_token` AS webhook_token, channels.`webhook_token` AS webhook_token,
channels.`paused` AS channel_paused, channels.`paused` AS "channel_paused:_",
channels.`paused_until` AS channel_paused_until, channels.`paused_until` AS "channel_paused_until:_",
reminders.`enabled` AS enabled, reminders.`enabled` AS "enabled:_",
reminders.`tts` AS tts, reminders.`tts` AS "tts:_",
reminders.`pin` AS pin, reminders.`pin` AS "pin:_",
reminders.`content` AS content, reminders.`content` AS content,
reminders.`attachment` AS attachment, reminders.`attachment` AS attachment,
reminders.`attachment_name` AS attachment_name, reminders.`attachment_name` AS attachment_name,
reminders.`utc_time` AS 'utc_time', reminders.`utc_time` AS "utc_time:_",
reminders.`timezone` AS timezone, reminders.`timezone` AS timezone,
reminders.`restartable` AS restartable, reminders.`restartable` AS "restartable:_",
reminders.`expires` AS expires, reminders.`expires` AS "expires:_",
reminders.`interval_seconds` AS 'interval_seconds', reminders.`interval_seconds` AS 'interval_seconds',
reminders.`interval_months` AS 'interval_months', reminders.`interval_months` AS 'interval_months',
@ -288,18 +294,26 @@ ON
reminders.channel_id = channels.id reminders.channel_id = channels.id
WHERE WHERE
reminders.`utc_time` < NOW() reminders.`utc_time` < NOW()
", "#,
) )
.fetch_all(pool) .fetch_all(pool)
.await .await
.unwrap() {
.into_iter() Ok(reminders) => reminders
.map(|mut rem| { .into_iter()
rem.content = substitute(&rem.content); .map(|mut rem| {
rem.content = substitute(&rem.content);
rem rem
}) })
.collect::<Vec<Self>>() .collect::<Vec<Self>>(),
Err(e) => {
warn!("Could not fetch reminders: {:?}", e);
vec![]
}
}
} }
async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
@ -319,7 +333,7 @@ UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
let mut updated_reminder_time = self.utc_time; let mut updated_reminder_time = self.utc_time;
if let Some(interval) = self.interval_months { if let Some(interval) = self.interval_months {
let row = sqlx::query!( match sqlx::query!(
// use the second date_add to force return value to datetime // use the second date_add to force return value to datetime
"SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time", "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
updated_reminder_time, updated_reminder_time,
@ -327,9 +341,25 @@ UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
.unwrap(); {
Ok(row) => match row.new_time {
Some(datetime) => {
updated_reminder_time = datetime;
}
None => {
warn!("Could not update interval by months: got NULL");
updated_reminder_time = row.new_time.unwrap(); updated_reminder_time += Duration::days(30);
}
},
Err(e) => {
warn!("Could not update interval by months: {:?}", e);
// naively fallback to adding 30 days
updated_reminder_time += Duration::days(30);
}
}
} }
if let Some(interval) = self.interval_seconds { if let Some(interval) = self.interval_seconds {
@ -538,7 +568,7 @@ UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
error!("Error sending {:?}: {:?}", self, e); error!("Error sending {:?}: {:?}", self, e);
if let Error::Http(error) = e { if let Error::Http(error) = e {
if error.status_code() == Some(StatusCode::from_u16(404).unwrap()) { if error.status_code() == Some(StatusCode::NOT_FOUND) {
error!("Seeing channel is deleted. Removing reminder"); error!("Seeing channel is deleted. Removing reminder");
self.force_delete(pool).await; self.force_delete(pool).await;
} else { } else {

View File

@ -71,7 +71,7 @@ pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
.send(|m| { .send(|m| {
m.ephemeral(true).embed(|e| { m.ephemeral(true).embed(|e| {
e.title("Info") e.title("Info")
.description(format!( .description(
"Help: `/help` "Help: `/help`
**Welcome to Reminder Bot!** **Welcome to Reminder Bot!**
@ -81,7 +81,7 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
Invite the bot: https://invite.reminder-bot.com/ Invite the bot: https://invite.reminder-bot.com/
Use our dashboard: https://reminder-bot.com/", Use our dashboard: https://reminder-bot.com/",
)) )
.footer(footer) .footer(footer)
.color(*THEME_COLOR) .color(*THEME_COLOR)
}) })

View File

@ -1,3 +1,5 @@
use std::collections::hash_map::Entry;
use chrono::offset::Utc; use chrono::offset::Utc;
use chrono_tz::{Tz, TZ_VARIANTS}; use chrono_tz::{Tz, TZ_VARIANTS};
use levenshtein::levenshtein; use levenshtein::levenshtein;
@ -52,7 +54,7 @@ pub async fn timezone(
.description(format!( .description(format!(
"Timezone has been set to **{}**. Your current time should be `{}`", "Timezone has been set to **{}**. Your current time should be `{}`",
timezone, timezone,
now.format("%H:%M").to_string() now.format("%H:%M")
)) ))
.color(*THEME_COLOR) .color(*THEME_COLOR)
}) })
@ -75,10 +77,7 @@ pub async fn timezone(
let fields = filtered_tz.iter().map(|tz| { let fields = filtered_tz.iter().map(|tz| {
( (
tz.to_string(), tz.to_string(),
format!( format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
"🕗 `{}`",
Utc::now().with_timezone(tz).format("%H:%M").to_string()
),
true, true,
) )
}); });
@ -98,11 +97,7 @@ pub async fn timezone(
} }
} else { } else {
let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| { let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
( (t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true)
t.to_string(),
format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M").to_string()),
true,
)
}); });
ctx.send(|m| { ctx.send(|m| {
@ -142,7 +137,7 @@ WHERE
) )
.fetch_all(&ctx.data().database) .fetch_all(&ctx.data().database)
.await .await
.unwrap_or(vec![]) .unwrap_or_default()
.iter() .iter()
.map(|s| s.name.clone()) .map(|s| s.name.clone())
.collect() .collect()
@ -200,14 +195,11 @@ Please select a unique name for your macro.",
let okay = { let okay = {
let mut lock = ctx.data().recording_macros.write().await; let mut lock = ctx.data().recording_macros.write().await;
if lock.contains_key(&(guild_id, ctx.author().id)) { if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
false e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
} else {
lock.insert(
(guild_id, ctx.author().id),
CommandMacro { guild_id, name, description, commands: vec![] },
);
true true
} else {
false
} }
}; };

View File

@ -9,13 +9,14 @@ use chrono_tz::Tz;
use num_integer::Integer; use num_integer::Integer;
use poise::{ use poise::{
serenity::{builder::CreateEmbed, model::channel::Channel}, serenity::{builder::CreateEmbed, model::channel::Channel},
serenity_prelude::{ButtonStyle, ReactionType},
CreateReply, CreateReply,
}; };
use crate::{ use crate::{
component_models::{ component_models::{
pager::{DelPager, LookPager, Pager}, pager::{DelPager, LookPager, Pager},
ComponentDataModel, DelSelector, ComponentDataModel, DelSelector, UndoReminder,
}, },
consts::{ consts::{
EMBED_DESCRIPTION_MAX_LENGTH, HOUR, MINUTE, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES, EMBED_DESCRIPTION_MAX_LENGTH, HOUR, MINUTE, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES,
@ -500,18 +501,16 @@ pub async fn start_timer(
if count >= 25 { if count >= 25 {
ctx.say("You already have 25 timers. Please delete some timers before creating a new one") ctx.say("You already have 25 timers. Please delete some timers before creating a new one")
.await?; .await?;
} else { } else if name.len() <= 32 {
if name.len() <= 32 { Timer::create(&name, owner, &ctx.data().database).await;
Timer::create(&name, owner, &ctx.data().database).await;
ctx.say("Created a new timer").await?; ctx.say("Created a new timer").await?;
} else { } else {
ctx.say(format!( ctx.say(format!(
"Please name your timer something shorted (max. 32 characters, you used {})", "Please name your timer something shorted (max. 32 characters, you used {})",
name.len() name.len()
)) ))
.await?; .await?;
}
} }
Ok(()) Ok(())
@ -589,8 +588,7 @@ pub async fn remind(
}; };
let scopes = { let scopes = {
let list = let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
channels.map(|arg| parse_mention_list(&arg.to_string())).unwrap_or_default();
if list.is_empty() { if list.is_empty() {
if ctx.guild_id().is_some() { if ctx.guild_id().is_some() {
@ -610,7 +608,7 @@ pub async fn remind(
{ {
( (
parse_duration(repeat) parse_duration(repeat)
.or_else(|_| parse_duration(&format!("1 {}", repeat.to_string()))) .or_else(|_| parse_duration(&format!("1 {}", repeat)))
.ok(), .ok(),
{ {
if let Some(arg) = &expires { if let Some(arg) = &expires {
@ -653,15 +651,41 @@ pub async fn remind(
let (errors, successes) = builder.build().await; let (errors, successes) = builder.build().await;
let embed = create_response(successes, errors, time); let embed = create_response(&successes, &errors, time);
ctx.send(|m| { if successes.len() == 1 {
m.embed(|c| { let reminder = successes.iter().next().map(|(r, _)| r.id).unwrap();
*c = embed; let undo_button = ComponentDataModel::UndoReminder(UndoReminder {
c 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())
})
})
})
}) })
}) .await?;
.await?; } else {
ctx.send(|m| {
m.embed(|c| {
*c = embed;
c
})
})
.await?;
}
} }
} }
None => { None => {
@ -673,8 +697,8 @@ pub async fn remind(
} }
fn create_response( fn create_response(
successes: HashSet<ReminderScope>, successes: &HashSet<(Reminder, ReminderScope)>,
errors: HashSet<ReminderError>, errors: &HashSet<ReminderError>,
time: i64, time: i64,
) -> CreateEmbed { ) -> CreateEmbed {
let success_part = match successes.len() { let success_part = match successes.len() {
@ -682,7 +706,8 @@ fn create_response(
n => format!( n => format!(
"Reminder{s} for {locations} set for <t:{offset}:R>", "Reminder{s} for {locations} set for <t:{offset}:R>",
s = if n > 1 { "s" } else { "" }, s = if n > 1 { "s" } else { "" },
locations = successes.iter().map(|l| l.mention()).collect::<Vec<String>>().join(", "), locations =
successes.iter().map(|(_, l)| l.mention()).collect::<Vec<String>>().join(", "),
offset = time offset = time
), ),
}; };

View File

@ -336,7 +336,7 @@ pub fn show_todo_page(
opt.create_option(|o| { opt.create_option(|o| {
o.label(format!("Mark {} complete", count + first_num)) o.label(format!("Mark {} complete", count + first_num))
.value(id) .value(id)
.description(disp.split_once(" ").unwrap_or(("", "")).1) .description(disp.split_once(' ').unwrap_or(("", "")).1)
}); });
} }

View File

@ -3,14 +3,20 @@ pub(crate) mod pager;
use std::io::Cursor; use std::io::Cursor;
use chrono_tz::Tz; use chrono_tz::Tz;
use poise::serenity::{ use log::warn;
builder::CreateEmbed, use poise::{
client::Context, serenity::{
model::{ builder::CreateEmbed,
channel::Channel, client::Context,
interactions::{message_component::MessageComponentInteraction, InteractionResponseType}, model::{
prelude::InteractionApplicationCommandCallbackDataFlags, channel::Channel,
interactions::{
message_component::MessageComponentInteraction, InteractionResponseType,
},
prelude::InteractionApplicationCommandCallbackDataFlags,
},
}, },
serenity_prelude as serenity,
}; };
use rmp_serde::Serializer; use rmp_serde::Serializer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -38,6 +44,7 @@ pub enum ComponentDataModel {
DelSelector(DelSelector), DelSelector(DelSelector),
TodoSelector(TodoSelector), TodoSelector(TodoSelector),
MacroPager(MacroPager), MacroPager(MacroPager),
UndoReminder(UndoReminder),
} }
impl ComponentDataModel { impl ComponentDataModel {
@ -334,6 +341,70 @@ WHERE guilds.guild = ?",
}) })
.await; .await;
} }
ComponentDataModel::UndoReminder(undo_reminder) => {
if component.user.id == undo_reminder.user_id {
let reminder =
Reminder::from_id(&data.database, undo_reminder.reminder_id).await;
if let Some(reminder) = reminder {
match reminder.delete(&data.database).await {
Ok(()) => {
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage)
.interaction_response_data(|d| {
d.embed(|e| {
e.title("Reminder Canceled")
.description(
"This reminder has been canceled.",
)
.color(*THEME_COLOR)
})
.components(|c| c)
})
})
.await;
}
Err(e) => {
warn!("Error canceling reminder: {:?}", e);
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.content(
"The reminder could not be canceled: it may have already been deleted. Check `/del`!")
.ephemeral(true)
})
})
.await;
}
}
} else {
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.content(
"The reminder could not be canceled: it may have already been deleted. Check `/del`!")
.ephemeral(true)
})
})
.await;
}
} else {
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.content(
"Only the user who performed the command can use this button.")
.ephemeral(true)
})
})
.await;
}
}
} }
} }
} }
@ -351,3 +422,9 @@ pub struct TodoSelector {
pub channel_id: Option<u64>, pub channel_id: Option<u64>,
pub guild_id: Option<u64>, pub guild_id: Option<u64>,
} }
#[derive(Serialize, Deserialize)]
pub struct UndoReminder {
pub user_id: serenity::UserId,
pub reminder_id: u32,
}

View File

@ -36,15 +36,11 @@ lazy_static! {
); );
pub static ref CNC_GUILD: Option<u64> = pub static ref CNC_GUILD: Option<u64> =
env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten(); env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
pub static ref MIN_INTERVAL: i64 = env::var("MIN_INTERVAL") pub static ref MIN_INTERVAL: i64 =
.ok() env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600);
.map(|inner| inner.parse::<i64>().ok())
.flatten()
.unwrap_or(600);
pub static ref MAX_TIME: i64 = env::var("MAX_TIME") pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
.ok() .ok()
.map(|inner| inner.parse::<i64>().ok()) .and_then(|inner| inner.parse::<i64>().ok())
.flatten()
.unwrap_or(60 * 60 * 24 * 365 * 50); .unwrap_or(60 * 60 * 24 * 365 * 50);
pub static ref LOCAL_TIMEZONE: String = pub static ref LOCAL_TIMEZONE: String =
env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string()); env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());

View File

@ -42,7 +42,7 @@ pub async fn listener(
}; };
}); });
} else { } else {
warn!("Not running postman") warn!("Not running postman");
} }
if !run_settings.contains("web") { if !run_settings.contains("web") {
@ -50,7 +50,7 @@ pub async fn listener(
reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap(); reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
}); });
} else { } else {
warn!("Not running web") warn!("Not running web");
} }
data.is_loop_running.swap(true, Ordering::Relaxed); data.is_loop_running.swap(true, Ordering::Relaxed);
@ -114,14 +114,13 @@ pub async fn listener(
.execute(&data.database) .execute(&data.database)
.await; .await;
} }
poise::Event::InteractionCreate { interaction } => match interaction { poise::Event::InteractionCreate { interaction } => {
Interaction::MessageComponent(component) => { if let Interaction::MessageComponent(component) = interaction {
let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id); let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
component_model.act(ctx, data, component).await; component_model.act(ctx, data, component).await;
} }
_ => {} }
},
_ => {} _ => {}
} }

View File

@ -11,10 +11,10 @@ async fn macro_check(ctx: Context<'_>) -> bool {
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) { if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
if command_macro.commands.len() >= MACRO_MAX_COMMANDS { if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
let _ = ctx.send(|m| { let _ = ctx.send(|m| {
m.ephemeral(true).content( m.ephemeral(true).content(
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS), format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
) )
}) })
.await; .await;
} else { } else {
let recorded = RecordedCommand { let recorded = RecordedCommand {
@ -30,19 +30,13 @@ async fn macro_check(ctx: Context<'_>) -> bool {
.await; .await;
} }
false return false;
} else {
true
} }
} else {
true
} }
} else {
true
} }
} else {
true
} }
true
} }
async fn check_self_permissions(ctx: Context<'_>) -> bool { async fn check_self_permissions(ctx: Context<'_>) -> bool {
@ -56,14 +50,13 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
let (view_channel, send_messages, embed_links) = ctx let (view_channel, send_messages, embed_links) = ctx
.channel_id() .channel_id()
.to_channel_cached(&ctx.discord()) .to_channel_cached(&ctx.discord())
.map(|c| { .and_then(|c| {
if let Channel::Guild(channel) = c { if let Channel::Guild(channel) = c {
channel.permissions_for_user(&ctx.discord(), user_id).ok() channel.permissions_for_user(&ctx.discord(), user_id).ok()
} else { } else {
None None
} }
}) })
.flatten()
.map_or((false, false, false), |p| { .map_or((false, false, false), |p| {
(p.view_channel(), p.send_messages(), p.embed_links()) (p.view_channel(), p.send_messages(), p.embed_links())
}); });

View File

@ -75,7 +75,7 @@ impl fmt::Display for Error {
match self { match self {
Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset), Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset),
Error::NumberExpected(offset) => write!(f, "expected number at {}", offset), Error::NumberExpected(offset) => write!(f, "expected number at {}", offset),
Error::UnknownUnit { unit, value, .. } if &unit == &"" => { Error::UnknownUnit { unit, value, .. } if unit.is_empty() => {
write!(f, "time unit needed, for example {0}sec or {0}ms", value,) write!(f, "time unit needed, for example {0}sec or {0}ms", value,)
} }
Error::UnknownUnit { unit, .. } => { Error::UnknownUnit { unit, .. } => {
@ -162,11 +162,11 @@ impl<'a> Parser<'a> {
}; };
let mut nsec = self.current.2 + nsec; let mut nsec = self.current.2 + nsec;
if nsec > 1_000_000_000 { if nsec > 1_000_000_000 {
sec = sec + nsec / 1_000_000_000; sec += nsec / 1_000_000_000;
nsec %= 1_000_000_000; nsec %= 1_000_000_000;
} }
sec = self.current.1 + sec; sec += self.current.1;
month = self.current.0 + month; month += self.current.0;
self.current = (month, sec, nsec); self.current = (month, sec, nsec);

View File

@ -1,4 +1,5 @@
#![feature(int_roundings)] #![feature(int_roundings)]
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
@ -23,7 +24,7 @@ use std::{
use chrono_tz::Tz; use chrono_tz::Tz;
use dotenv::dotenv; use dotenv::dotenv;
use poise::serenity::model::{ use poise::serenity::model::{
gateway::{Activity, GatewayIntents}, gateway::GatewayIntents,
id::{GuildId, UserId}, id::{GuildId, UserId},
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
@ -52,7 +53,7 @@ pub struct Data {
broadcast: Sender<()>, broadcast: Sender<()>,
} }
impl std::fmt::Debug for Data { impl Debug for Data {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Data {{ .. }}") write!(f, "Data {{ .. }}")
} }

View File

@ -5,11 +5,11 @@ use serde::{Deserialize, Serialize};
use crate::{Context, Data, Error}; use crate::{Context, Data, Error};
fn default_none<U, E>() -> Option< type Func<U, E> = for<'a> fn(
for<'a> fn( poise::ApplicationContext<'a, U, E>,
poise::ApplicationContext<'a, U, E>, ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>;
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
> { fn default_none<U, E>() -> Option<Func<U, E>> {
None None
} }
@ -17,11 +17,7 @@ fn default_none<U, E>() -> Option<
pub struct RecordedCommand<U, E> { pub struct RecordedCommand<U, E> {
#[serde(skip)] #[serde(skip)]
#[serde(default = "default_none::<U, E>")] #[serde(default = "default_none::<U, E>")]
pub action: Option< pub action: Option<Func<U, E>>,
for<'a> fn(
poise::ApplicationContext<'a, U, E>,
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
>,
pub command_name: String, pub command_name: String,
pub options: Vec<ApplicationCommandInteractionDataOption>, pub options: Vec<ApplicationCommandInteractionDataOption>,
} }
@ -59,7 +55,7 @@ SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND
.iter() .iter()
.find(|c| c.identifying_name == recorded_command.command_name); .find(|c| c.identifying_name == recorded_command.command_name);
recorded_command.action = command.map(|c| c.slash_action).flatten().clone(); recorded_command.action = command.map(|c| c.slash_action).flatten();
} }
let command_macro = CommandMacro { let command_macro = CommandMacro {

View File

@ -126,7 +126,7 @@ INSERT INTO reminders (
.await .await
.unwrap(); .unwrap();
Ok(Reminder::from_uid(&self.pool, self.uid).await.unwrap()) Ok(Reminder::from_uid(&self.pool, &self.uid).await.unwrap())
} }
} }
@ -207,7 +207,7 @@ impl<'a> MultiReminderBuilder<'a> {
self.scopes = scopes; self.scopes = scopes;
} }
pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) { pub async fn build(self) -> (HashSet<ReminderError>, HashSet<(Reminder, ReminderScope)>) {
let mut errors = HashSet::new(); let mut errors = HashSet::new();
let mut ok_locs = HashSet::new(); let mut ok_locs = HashSet::new();
@ -309,8 +309,8 @@ impl<'a> MultiReminderBuilder<'a> {
}; };
match builder.build().await { match builder.build().await {
Ok(_) => { Ok(r) => {
ok_locs.insert(scope); ok_locs.insert((r, scope));
} }
Err(e) => { Err(e) => {
errors.insert(e); errors.insert(e);

View File

@ -4,6 +4,8 @@ pub mod errors;
mod helper; mod helper;
pub mod look_flags; pub mod look_flags;
use std::hash::{Hash, Hasher};
use chrono::{NaiveDateTime, TimeZone}; use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Tz; use chrono_tz::Tz;
use poise::{ use poise::{
@ -32,11 +34,22 @@ pub struct Reminder {
pub set_by: Option<u64>, pub set_by: Option<u64>,
} }
impl Hash for Reminder {
fn hash<H: Hasher>(&self, state: &mut H) {
self.uid.hash(state);
}
}
impl PartialEq<Self> for Reminder {
fn eq(&self, other: &Self) -> bool {
self.uid == other.uid
}
}
impl Eq for Reminder {}
impl Reminder { impl Reminder {
pub async fn from_uid( pub async fn from_uid(pool: impl Executor<'_, Database = Database>, uid: &str) -> Option<Self> {
pool: impl Executor<'_, Database = Database>,
uid: String,
) -> Option<Self> {
sqlx::query_as_unchecked!( sqlx::query_as_unchecked!(
Self, Self,
" "
@ -72,6 +85,42 @@ WHERE
.ok() .ok()
} }
pub async fn from_id(pool: impl Executor<'_, Database = Database>, id: u32) -> Option<Self> {
sqlx::query_as_unchecked!(
Self,
"
SELECT
reminders.id,
reminders.uid,
channels.channel,
reminders.utc_time,
reminders.interval_seconds,
reminders.interval_months,
reminders.expires,
reminders.enabled,
reminders.content,
reminders.embed_description,
users.user AS set_by
FROM
reminders
INNER JOIN
channels
ON
reminders.channel_id = channels.id
LEFT JOIN
users
ON
reminders.set_by = users.id
WHERE
reminders.id = ?
",
id
)
.fetch_one(pool)
.await
.ok()
}
pub async fn from_channel<C: Into<ChannelId>>( pub async fn from_channel<C: Into<ChannelId>>(
pool: impl Executor<'_, Database = Database>, pool: impl Executor<'_, Database = Database>,
channel_id: C, channel_id: C,
@ -240,6 +289,13 @@ WHERE
.unwrap() .unwrap()
} }
pub async fn delete(
&self,
db: impl Executor<'_, Database = Database>,
) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reminders WHERE uid = ?", self.uid).execute(db).await.map(|_| ())
}
pub fn display_content(&self) -> &str { pub fn display_content(&self) -> &str {
if self.content.is_empty() { if self.content.is_empty() {
&self.embed_description &self.embed_description
@ -254,10 +310,7 @@ WHERE
count + 1, count + 1,
self.display_content(), self.display_content(),
self.channel, self.channel,
timezone timezone.timestamp(self.utc_time.timestamp(), 0).format("%Y-%m-%d %H:%M:%S")
.timestamp(self.utc_time.timestamp(), 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
) )
} }

View File

@ -211,14 +211,12 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
.output() .output()
.await .await
.ok() .ok()
.map(|inner| { .and_then(|inner| {
if inner.status.success() { if inner.status.success() {
Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap()) Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
} else { } else {
None None
} }
}) })
.flatten() .and_then(|inner| if inner < 0 { None } else { Some(inner) })
.map(|inner| if inner < 0 { None } else { Some(inner) })
.flatten()
} }