Skip to content

Commit eff396d

Browse files
committed
feat(commands): add reminder slash command
1 parent 02eed0e commit eff396d

6 files changed

Lines changed: 291 additions & 5 deletions

File tree

src/commands/add_reminder/common.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use crate::db::reminders::{update_reminder_status, Reminder};
33
use crate::utils::conversion::hex_string_to_int::hex_string_to_int;
44
use crate::utils::message::message_builder::MessageBuilder;
55
use chrono::Local;
6-
use serenity::all::{ChannelId, Context, Message, UserId};
6+
use serenity::all::{ChannelId, CommandInteraction, Context, Message, UserId};
77
use sqlx::SqlitePool;
88
use std::collections::HashMap;
99
use std::time::Duration;
1010
use tokio::time::sleep;
1111

12-
pub async fn send_register_confirmation(
12+
pub async fn send_register_confirmation_from_message(
1313
reminder_id: i64,
1414
reminder_content: &str,
1515
ctx: &Context,
@@ -57,6 +57,58 @@ pub async fn send_register_confirmation(
5757
}
5858
}
5959

60+
pub async fn send_register_confirmation_from_command(
61+
reminder_id: i64,
62+
reminder_content: &str,
63+
ctx: &Context,
64+
command: &CommandInteraction,
65+
config: &Config,
66+
trigger_timestamp: i64,
67+
) {
68+
let mut params = HashMap::new();
69+
params.insert("time".to_string(), format!("<t:{}:F>", trigger_timestamp));
70+
params.insert(
71+
"remaining_time".to_string(),
72+
format!("<t:{}:R>", trigger_timestamp),
73+
);
74+
75+
if !reminder_content.is_empty() {
76+
params.insert("content".to_string(), reminder_content.to_string());
77+
}
78+
79+
if !reminder_content.is_empty() {
80+
let response = MessageBuilder::system_message(&ctx, &config)
81+
.translated_content(
82+
"reminder.registered_with_content",
83+
Some(&params),
84+
None,
85+
None,
86+
)
87+
.await
88+
.to_channel(command.channel_id)
89+
.footer(format!("{}: {}", "ID", reminder_id))
90+
.build_interaction_message_followup()
91+
.await;
92+
93+
let _ = command.create_followup(&ctx.http, response).await;
94+
} else {
95+
let response = MessageBuilder::system_message(&ctx, &config)
96+
.translated_content(
97+
"reminder.registered_without_content",
98+
Some(&params),
99+
None,
100+
None,
101+
)
102+
.await
103+
.to_channel(command.channel_id)
104+
.footer(format!("{}: {}", "ID", reminder_id))
105+
.build_interaction_message_followup()
106+
.await;
107+
108+
let _ = command.create_followup(&ctx.http, response).await;
109+
}
110+
}
111+
60112
pub fn spawn_reminder(reminder: &Reminder, ctx: &Context, config: &Config, pool: &SqlitePool) {
61113
let pool = pool.clone();
62114
let config = config.clone();
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,207 @@
1+
use crate::commands::add_reminder::common::{
2+
send_register_confirmation_from_command, spawn_reminder,
3+
};
4+
use crate::commands::{BoxFuture, RegistrableCommand};
5+
use crate::config::Config;
6+
use crate::db::reminders::{insert_reminder, Reminder};
7+
use crate::db::threads::get_thread_by_user_id;
8+
use crate::errors::{
9+
common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError,
10+
};
11+
use crate::i18n::get_translated_message;
12+
use crate::utils::command::defer_response::defer_response;
13+
use chrono::{Local, NaiveTime};
14+
use regex::Regex;
15+
use serenity::all::{
16+
CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand,
17+
CreateCommandOption, ResolvedOption,
18+
};
119

20+
pub struct AddReminderCommand;
21+
22+
#[async_trait::async_trait]
23+
impl RegistrableCommand for AddReminderCommand {
24+
fn name(&self) -> &'static str {
25+
"add_reminder"
26+
}
27+
28+
fn register(&self, config: &Config) -> BoxFuture<Vec<CreateCommand>> {
29+
let config = config.clone();
30+
let name = self.name();
31+
32+
Box::pin(async move {
33+
let cmd_desc = get_translated_message(
34+
&config,
35+
"slash_command.add_reminder_command_description",
36+
None,
37+
None,
38+
None,
39+
None,
40+
)
41+
.await;
42+
let time_desc = get_translated_message(
43+
&config,
44+
"slash_command.add_reminder_time_argument_description",
45+
None,
46+
None,
47+
None,
48+
None,
49+
)
50+
.await;
51+
let content_desc = get_translated_message(
52+
&config,
53+
"slash_command.add_reminder_content_argument_description",
54+
None,
55+
None,
56+
None,
57+
None,
58+
)
59+
.await;
60+
61+
vec![
62+
CreateCommand::new(name)
63+
.description(cmd_desc)
64+
.add_option(
65+
CreateCommandOption::new(CommandOptionType::String, "time", time_desc)
66+
.required(true),
67+
)
68+
.add_option(
69+
CreateCommandOption::new(
70+
CommandOptionType::String,
71+
"content",
72+
content_desc,
73+
)
74+
.required(true),
75+
),
76+
]
77+
})
78+
}
79+
80+
fn run(
81+
&self,
82+
ctx: &Context,
83+
command: &CommandInteraction,
84+
options: &[ResolvedOption<'_>],
85+
config: &Config,
86+
) -> BoxFuture<ModmailResult<()>> {
87+
let ctx = ctx.clone();
88+
let command = command.clone();
89+
let config = config.clone();
90+
91+
Box::pin(async move {
92+
let pool = config
93+
.db_pool
94+
.as_ref()
95+
.ok_or_else(common::database_connection_failed)?;
96+
97+
let _ = defer_response(&ctx, &command).await;
98+
99+
let mut time: Option<String> = None;
100+
let mut content: Option<String> = None;
101+
102+
for option in &command.data.options {
103+
match option.name.as_str() {
104+
"time" => {
105+
if let CommandDataOptionValue::String(val) = &option.value {
106+
time.replace(val.clone());
107+
}
108+
}
109+
"content" => {
110+
if let CommandDataOptionValue::String(val) = &option.value {
111+
content.replace(val.clone());
112+
}
113+
}
114+
_ => {}
115+
}
116+
}
117+
118+
let time = match time {
119+
Some(t) => t.clone(),
120+
None => {
121+
return Err(ModmailError::Command(CommandError::InvalidArguments(
122+
"Missing required arguments".to_string(),
123+
)));
124+
}
125+
};
126+
127+
let content = match content {
128+
Some(c) => c,
129+
None => {
130+
return Err(ModmailError::Command(CommandError::InvalidArguments(
131+
"Missing required arguments".to_string(),
132+
)));
133+
}
134+
};
135+
136+
let time_str = time.to_string();
137+
let re = Regex::new(r"^(?P<hour>[01]?\d|2[0-3]):(?P<minute>[0-5]\d)$").unwrap();
138+
let captures = re.captures(&time_str).ok_or_else(|| {
139+
return ModmailError::Command(CommandError::InvalidArguments(
140+
"duration".to_string(),
141+
));
142+
})?;
143+
144+
let hours: u32 = captures
145+
.name("hour")
146+
.and_then(|m| m.as_str().parse::<u32>().ok())
147+
.unwrap_or(0);
148+
149+
let minutes: u32 = captures
150+
.name("minute")
151+
.and_then(|m| m.as_str().parse::<u32>().ok())
152+
.unwrap_or(0);
153+
154+
let time = NaiveTime::from_hms_opt(hours, minutes, 0).unwrap();
155+
let now = Local::now();
156+
let mut trigger_dt = now.date_naive().and_time(time);
157+
158+
if trigger_dt < now.date_naive().and_time(time) {
159+
trigger_dt += chrono::Duration::days(1);
160+
}
161+
162+
let trigger_timestamp = trigger_dt.and_local_timezone(Local).unwrap().timestamp();
163+
164+
let thread = match get_thread_by_user_id(command.user.id, pool).await {
165+
Some(t) => t,
166+
None => {
167+
return Err(ModmailError::Thread(ThreadError::ThreadNotFound));
168+
}
169+
};
170+
171+
let reminder: Reminder = Reminder {
172+
thread_id: thread.id,
173+
user_id: command.user.id.get() as i64,
174+
channel_id: command.channel_id.get() as i64,
175+
guild_id: config.bot.get_staff_guild_id() as i64,
176+
reminder_content: content.clone(),
177+
trigger_time: trigger_timestamp,
178+
created_at: now.timestamp(),
179+
completed: false,
180+
};
181+
182+
let reminder_id = match insert_reminder(&reminder, pool).await {
183+
Ok(id) => id,
184+
Err(e) => {
185+
eprintln!("Failed to insert reminder: {}", e);
186+
return Err(ModmailError::Database(DatabaseError::InsertFailed(
187+
e.to_string(),
188+
)));
189+
}
190+
};
191+
192+
send_register_confirmation_from_command(
193+
reminder_id,
194+
&content,
195+
&ctx,
196+
&command,
197+
&config,
198+
trigger_timestamp,
199+
)
200+
.await;
201+
202+
spawn_reminder(&reminder, &ctx, &config, &pool);
203+
204+
Ok(())
205+
})
206+
}
207+
}

src/commands/add_reminder/text_command/add_reminder.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::commands::add_reminder::common::{send_register_confirmation, spawn_reminder};
1+
use crate::commands::add_reminder::common::{
2+
send_register_confirmation_from_message, spawn_reminder,
3+
};
24
use crate::config::Config;
35
use crate::db::reminders::{insert_reminder, Reminder};
46
use crate::db::threads::get_thread_by_user_id;
@@ -86,11 +88,11 @@ pub async fn add_reminder(ctx: &Context, msg: &Message, config: &Config) -> Modm
8688
}
8789
};
8890

89-
send_register_confirmation(
91+
send_register_confirmation_from_message(
9092
reminder_id,
9193
reminder_content,
9294
ctx,
93-
msg,
95+
&msg,
9496
config,
9597
trigger_timestamp,
9698
)

src/i18n/language/en.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,4 +731,16 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) {
731731
"reminder.show_without_content".to_string(),
732732
DictionaryMessage::new("⏰ Reminder <@{user}>!"),
733733
);
734+
dict.messages.insert(
735+
"slash_command.add_reminder_command_description".to_string(),
736+
DictionaryMessage::new("Add a reminder for yourself"),
737+
);
738+
dict.messages.insert(
739+
"slash_command.add_reminder_time_argument_description".to_string(),
740+
DictionaryMessage::new("The time when the reminder should trigger (format: HH:MM)"),
741+
);
742+
dict.messages.insert(
743+
"slash_command.add_reminder_content_argument_description".to_string(),
744+
DictionaryMessage::new("Optional content for the reminder"),
745+
);
734746
}

src/i18n/language/fr.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,16 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) {
748748
"reminder.show_without_content".to_string(),
749749
DictionaryMessage::new("⏰ Rappel <@{user}> !"),
750750
);
751+
dict.messages.insert(
752+
"slash_command.add_reminder_command_description".to_string(),
753+
DictionaryMessage::new("Ajouter un rappel pour vous-même"),
754+
);
755+
dict.messages.insert(
756+
"slash_command.add_reminder_time_argument_description".to_string(),
757+
DictionaryMessage::new("L'heure à laquelle vous souhaitez être rappelé (format HH:MM)"),
758+
);
759+
dict.messages.insert(
760+
"slash_command.add_reminder_content_argument_description".to_string(),
761+
DictionaryMessage::new("Le contenu du rappel (optionnel)"),
762+
);
751763
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::commands::CommandRegistry;
2+
use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand;
23
use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand;
34
use crate::commands::alert::slash_command::alert::AlertCommand;
45
use crate::commands::close::slash_command::close::CloseCommand;
@@ -80,6 +81,7 @@ async fn main() {
8081
registry.register_command(RecoverCommand);
8182
registry.register_command(RemoveStaffCommand);
8283
registry.register_command(ReplyCommand);
84+
registry.register_command(AddReminderCommand);
8385

8486
let registry = Arc::new(registry);
8587

0 commit comments

Comments
 (0)