Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ poise = "0.6.1"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
imap = "3.0.0-alpha.12"
mailparse = "0.15"
html2text = "0.12"
html2text = "0.12"
rand = "0.8.5"
4 changes: 3 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod random;
mod set_log_level;

use crate::commands::random::random;
use crate::commands::set_log_level::set_log_level;
use serenity::all::RoleId;
use tracing::{debug, instrument};
Expand Down Expand Up @@ -31,7 +33,7 @@ async fn amdctl(ctx: Context<'_>) -> Result<(), Error> {

/// Returns a vector containg [Poise Commands][`poise::Command`]
pub fn get_commands() -> Vec<poise::Command<Data, Error>> {
let commands = vec![amdctl(), set_log_level()];
let commands = vec![amdctl(), set_log_level(), random()];
debug!(commands = ?commands.iter().map(|c| &c.name).collect::<Vec<_>>());
commands
}
93 changes: 93 additions & 0 deletions src/commands/random.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use crate::{
ids::{FIRST_YEAR_ROLE_ID, SECOND_YEAR_ROLE_ID},
Context, Error,
};
Comment thread
hrideshmg marked this conversation as resolved.
use rand::seq::IteratorRandom;
use serenity::all::{Mentionable as _, RoleId, UserId};
use std::collections::HashSet;

#[poise::command(slash_command)]
pub async fn random(ctx: Context<'_>) -> Result<(), Error> {
Comment thread
hrideshmg marked this conversation as resolved.
Outdated
let guild = ctx.guild_id().ok_or("No guild id")?;
let members = guild.members(ctx.http(), None, None).await?;
Comment thread
hrideshmg marked this conversation as resolved.

Comment thread
AnandajithS marked this conversation as resolved.
let first_year_role = RoleId::new(FIRST_YEAR_ROLE_ID);
let second_year_role = RoleId::new(SECOND_YEAR_ROLE_ID);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are only first and second years included as eligible members? Ideally one should be able to specify which all years they want to target with it falling back to all 3 as a default

let eligible_members: Vec<_> = members // Filtering out bots and other ineligible members
.into_iter()
.filter(|m| {
!m.user.bot
&& (m.roles.contains(&first_year_role) || m.roles.contains(&second_year_role))
})
.collect();

if eligible_members.is_empty() {
ctx.say("No eligible members found.").await?;
return Ok(());
}

let recent_picks = {
// Accessing recently picked members to avoid repetition
let data = ctx.data();

@hrideshmg hrideshmg Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is ctx.data() persisted across bot restarts? If not, it can't be relied upon as the bot service may restart due to various factors.

let recent_random_picks = data.recent_random_picks.lock().unwrap();
recent_random_picks.clone()
Comment on lines +47 to +49
};

let available_members: Vec<_> = eligible_members // Members who haven't been picked recently
.iter()
.filter(|member| !recent_picks.contains(&member.user.id))
.collect();

/* Since ThreadRng is not Send, keeping it alive across an .await causes command future to become non-Send.
So, we are enclosing the selection in it's own scope so the ThreadRng is dropped before we hit any .await,
Comment thread
hrideshmg marked this conversation as resolved.
allowing the command future to remain Send
*/

let selected = {
let mut rng = rand::thread_rng();

let mut selected: Vec<_> = available_members.into_iter().choose_multiple(&mut rng, 5);

if selected.len() < 5 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

number should be configurable via the slash syntax rather than hardcoded to 5

// If not enough members are available, fetch recently picked members
let remaining_needed = 5 - selected.len();

let selected_ids: HashSet<UserId> = selected.iter().map(|m| m.user.id).collect();

let additional: Vec<_> = eligible_members
.iter()
.filter(|m| !selected_ids.contains(&m.user.id))
.choose_multiple(&mut rng, remaining_needed);

selected.extend(additional);
}
selected
};
Comment thread
hrideshmg marked this conversation as resolved.

let ping_message = selected
.iter()
.map(|m| m.user.mention().to_string())
.collect::<Vec<_>>()
.join("\n");

{
let data = ctx.data();
let mut recent_random_picks = data.recent_random_picks.lock().unwrap();
recent_random_picks.extend(selected.iter().map(|m| m.user.id)); // Adding selected members to recently picked set
Comment on lines +92 to +94

let eligible_ids: HashSet<UserId> = eligible_members.iter().map(|m| m.user.id).collect();
recent_random_picks.retain(|user_id| eligible_ids.contains(user_id));
if recent_random_picks.len() >= eligible_ids.len() {
// If all eligible members have been picked atleast once, clear the recently picked set
recent_random_picks.clear();
Comment thread
hrideshmg marked this conversation as resolved.
}
}
ctx.say(format!(
"Pinging {} members: {}",
selected.len(),
ping_message
))
.await?;

Ok(())
}
3 changes: 3 additions & 0 deletions src/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ pub const GROUP_ONE_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098248293716008;
pub const GROUP_TWO_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098298935738489;
pub const GROUP_THREE_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098353378070710;
pub const GROUP_FOUR_STATUS_UPDATE_CHANNEL_ID: u64 = 1225098407216156712;

pub const FIRST_YEAR_ROLE_ID: u64 = 1283689015450669108;
pub const SECOND_YEAR_ROLE_ID: u64 = 1288903184990994522;
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ use serenity::{
use trace::{setup_tracing, ReloadHandle};
use tracing::{debug, info, instrument};

use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};

type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = PoiseContext<'a, Data, Error>;
Expand All @@ -49,6 +52,7 @@ type Context<'a> = PoiseContext<'a, Data, Error>;
#[derive(Clone)]
struct Data {
reaction_roles: HashMap<ReactionType, RoleId>,
recent_random_picks: Arc<Mutex<HashSet<UserId>>>,
log_reload_handle: ReloadHandle,
Comment on lines 43 to 58
graphql_client: GraphQLClient,
}
Expand All @@ -58,6 +62,7 @@ impl Data {
fn new(reload_handle: ReloadHandle, root_url: String, api_key: String) -> Self {
Data {
reaction_roles: HashMap::new(),
recent_random_picks: Arc::new(Mutex::new(HashSet::new())),
log_reload_handle: reload_handle,
graphql_client: GraphQLClient::new(root_url, api_key),
}
Expand Down
Loading