Skip to content

Commit f021627

Browse files
committed
refactor(reminder): allow staff to ping role from reminder command
1 parent 396305f commit f021627

3 files changed

Lines changed: 431 additions & 10 deletions

File tree

crates/rustmail/src/commands/add_reminder/common.rs

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use crate::prelude::config::*;
22
use crate::prelude::db::*;
33
use crate::prelude::utils::*;
44
use chrono::Local;
5-
use serenity::all::{ChannelId, CommandInteraction, Context, Message, UserId};
5+
use serenity::all::{ChannelId, CommandInteraction, Context, GuildId, Message, RoleId, UserId};
66
use sqlx::SqlitePool;
7-
use std::collections::HashMap;
7+
use std::collections::{HashMap, HashSet};
88
use std::sync::Arc;
99
use std::time::Duration;
1010
use tokio::select;
@@ -162,12 +162,46 @@ pub fn spawn_reminder(
162162
params.insert("user".to_string(), reminder.user_id.to_string());
163163
params.insert("content".to_string(), reminder.reminder_content.to_string());
164164

165-
let mut mentions = Vec::<UserId>::new();
166-
mentions.push(UserId::new(reminder.user_id as u64));
165+
let (mentions, is_role_targeted) = if let Some(ref target_roles_str) = reminder.target_roles
166+
{
167+
let role_mentions: String = target_roles_str
168+
.split(',')
169+
.filter_map(|s| s.trim().parse::<u64>().ok())
170+
.map(|id| format!("<@&{}>", id))
171+
.collect::<Vec<_>>()
172+
.join(", ");
173+
params.insert("roles".to_string(), role_mentions);
174+
175+
let members =
176+
get_targeted_mentions(&ctx, &pool, reminder.guild_id as u64, target_roles_str)
177+
.await;
178+
(members, true)
179+
} else {
180+
(vec![UserId::new(reminder.user_id as u64)], false)
181+
};
182+
183+
if mentions.is_empty() {
184+
if let Err(e) = update_reminder_status(&reminder, true, &pool).await {
185+
eprintln!("Failed to update reminder status: {}", e);
186+
}
187+
return;
188+
}
189+
190+
let (key_with_content, key_without_content) = if is_role_targeted {
191+
(
192+
"reminder.show_with_content_roles",
193+
"reminder.show_without_content_roles",
194+
)
195+
} else {
196+
(
197+
"reminder.show_with_content",
198+
"reminder.show_without_content",
199+
)
200+
};
167201

168202
if !reminder.reminder_content.is_empty() {
169203
let _ = MessageBuilder::system_message(&ctx, &config)
170-
.translated_content("reminder.show_with_content", Some(&params), None, None)
204+
.translated_content(key_with_content, Some(&params), None, None)
171205
.await
172206
.to_channel(ChannelId::new(reminder.channel_id as u64))
173207
.color(hex_string_to_int(&config.reminders.embed_color) as u32)
@@ -176,7 +210,7 @@ pub fn spawn_reminder(
176210
.await;
177211
} else {
178212
let _ = MessageBuilder::system_message(&ctx, &config)
179-
.translated_content("reminder.show_without_content", Some(&params), None, None)
213+
.translated_content(key_without_content, Some(&params), None, None)
180214
.await
181215
.to_channel(ChannelId::new(reminder.channel_id as u64))
182216
.color(hex_string_to_int(&config.reminders.embed_color) as u32)
@@ -190,3 +224,60 @@ pub fn spawn_reminder(
190224
}
191225
});
192226
}
227+
228+
async fn get_targeted_mentions(
229+
ctx: &Context,
230+
pool: &SqlitePool,
231+
guild_id: u64,
232+
target_roles_str: &str,
233+
) -> Vec<UserId> {
234+
let guild_id_obj = GuildId::new(guild_id);
235+
236+
let role_ids: Vec<u64> = target_roles_str
237+
.split(',')
238+
.filter_map(|s| s.trim().parse::<u64>().ok())
239+
.collect();
240+
241+
if role_ids.is_empty() {
242+
return vec![];
243+
}
244+
245+
let members = match guild_id_obj.members(&ctx.http, None, None).await {
246+
Ok(m) => m,
247+
Err(e) => {
248+
eprintln!("Failed to fetch guild members: {}", e);
249+
return vec![];
250+
}
251+
};
252+
253+
let mut user_ids_with_roles: HashSet<UserId> = HashSet::new();
254+
255+
for role_id in &role_ids {
256+
let role_id_obj = RoleId::new(*role_id);
257+
for member in &members {
258+
if member.roles.contains(&role_id_obj) {
259+
user_ids_with_roles.insert(member.user.id);
260+
}
261+
}
262+
}
263+
264+
let mut opted_out_users: HashSet<u64> = HashSet::new();
265+
266+
for role_id in &role_ids {
267+
match get_optouts_for_role(guild_id as i64, *role_id as i64, pool).await {
268+
Ok(optouts) => {
269+
for user_id in optouts {
270+
opted_out_users.insert(user_id as u64);
271+
}
272+
}
273+
Err(e) => {
274+
eprintln!("Failed to get optouts for role {}: {}", role_id, e);
275+
}
276+
}
277+
}
278+
279+
user_ids_with_roles
280+
.into_iter()
281+
.filter(|user_id| !opted_out_users.contains(&user_id.get()))
282+
.collect()
283+
}

crates/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use regex::Regex;
1010
use serenity::FutureExt;
1111
use serenity::all::{
1212
CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand,
13-
CreateCommandOption, ResolvedOption,
13+
CreateCommandOption, GuildId, ResolvedOption, RoleId,
1414
};
1515
use std::sync::Arc;
1616

@@ -60,6 +60,15 @@ impl RegistrableCommand for AddReminderCommand {
6060
None,
6161
)
6262
.await;
63+
let roles_desc = get_translated_message(
64+
&config,
65+
"slash_command.add_reminder_roles_argument_description",
66+
None,
67+
None,
68+
None,
69+
None,
70+
)
71+
.await;
6372

6473
vec![
6574
CreateCommand::new(name)
@@ -75,6 +84,10 @@ impl RegistrableCommand for AddReminderCommand {
7584
content_desc,
7685
)
7786
.required(true),
87+
)
88+
.add_option(
89+
CreateCommandOption::new(CommandOptionType::String, "roles", roles_desc)
90+
.required(false),
7891
),
7992
]
8093
})
@@ -102,6 +115,7 @@ impl RegistrableCommand for AddReminderCommand {
102115

103116
let mut time: Option<String> = None;
104117
let mut content: Option<String> = None;
118+
let mut roles: Option<String> = None;
105119

106120
for option in &command.data.options {
107121
match option.name.as_str() {
@@ -115,6 +129,11 @@ impl RegistrableCommand for AddReminderCommand {
115129
content.replace(val.clone());
116130
}
117131
}
132+
"roles" => {
133+
if let CommandDataOptionValue::String(val) = &option.value {
134+
roles.replace(val.clone());
135+
}
136+
}
118137
_ => {}
119138
}
120139
}
@@ -171,6 +190,12 @@ impl RegistrableCommand for AddReminderCommand {
171190
}
172191
};
173192

193+
let target_roles = if let Some(roles_str) = roles {
194+
resolve_role_names_to_ids(&ctx, config.bot.get_staff_guild_id(), &roles_str).await
195+
} else {
196+
None
197+
};
198+
174199
let reminder: Reminder = Reminder {
175200
thread_id: thread.id,
176201
user_id: command.user.id.get() as i64,
@@ -180,6 +205,7 @@ impl RegistrableCommand for AddReminderCommand {
180205
trigger_time: trigger_timestamp,
181206
created_at: now.timestamp(),
182207
completed: false,
208+
target_roles,
183209
};
184210

185211
let reminder_id = match insert_reminder(&reminder, pool).await {
@@ -213,3 +239,61 @@ impl RegistrableCommand for AddReminderCommand {
213239
})
214240
}
215241
}
242+
243+
async fn resolve_role_names_to_ids(
244+
ctx: &Context,
245+
guild_id: u64,
246+
roles_str: &str,
247+
) -> Option<String> {
248+
if roles_str.is_empty() {
249+
return None;
250+
}
251+
252+
let guild_id_obj = GuildId::new(guild_id);
253+
let guild = match guild_id_obj.to_partial_guild(&ctx.http).await {
254+
Ok(g) => g,
255+
Err(_) => return None,
256+
};
257+
258+
let mention_regex = Regex::new(r"<@&(\d+)>").unwrap();
259+
260+
let role_parts: Vec<&str> = roles_str.split(',').map(|s| s.trim()).collect();
261+
let mut role_ids: Vec<u64> = Vec::new();
262+
263+
for role_part in role_parts {
264+
if role_part.is_empty() {
265+
continue;
266+
}
267+
268+
if let Some(caps) = mention_regex.captures(role_part) {
269+
if let Some(id_match) = caps.get(1) {
270+
if let Ok(id) = id_match.as_str().parse::<u64>() {
271+
if guild.roles.contains_key(&RoleId::new(id)) {
272+
role_ids.push(id);
273+
}
274+
}
275+
}
276+
} else {
277+
let role_name_lower = role_part.to_lowercase();
278+
if let Some(role) = guild
279+
.roles
280+
.values()
281+
.find(|r| r.name.to_lowercase() == role_name_lower)
282+
{
283+
role_ids.push(role.id.get());
284+
}
285+
}
286+
}
287+
288+
if role_ids.is_empty() {
289+
None
290+
} else {
291+
Some(
292+
role_ids
293+
.iter()
294+
.map(|id| id.to_string())
295+
.collect::<Vec<_>>()
296+
.join(","),
297+
)
298+
}
299+
}

0 commit comments

Comments
 (0)