Skip to content

Commit c657d1c

Browse files
committed
simplify codebase structure and add additional documentation
1 parent 4b1211e commit c657d1c

File tree

11 files changed

+159
-47
lines changed

11 files changed

+159
-47
lines changed

docs/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ The event handler takes care of the rest:
122122
```rust
123123
// On the event of a reaction being added
124124
FullEvent::ReactionAdd { add_reaction } => {
125-
let message_id = MessageId::new(ARCHIVE_MESSAGE_ID);
125+
let message_id = MessageId::new(ROLES_MESSAGE_ID);
126126
// Check if the reaction was added to the message we want and if it is reacted with the
127127
// emoji we want
128128
if add_reaction.message_id == message_id && data.reaction_roles.contains_key(&add_reaction.emoji) {

src/commands.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ async fn amdctl(ctx: Context<'_>) -> Result<(), Error> {
2323
Ok(())
2424
}
2525

26+
/// Every function that is defined *should* be added to the
27+
/// returned vector in get_commands to ensure it is registered (available for the user)
28+
/// when the bot goes online.
2629
pub fn get_commands() -> Vec<poise::Command<Data, Error>> {
2730
vec![amdctl()]
2831
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,5 @@ GNU General Public License for more details.
1515
You should have received a copy of the GNU General Public License
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
18-
pub mod scheduler;
19-
pub mod tasks;
20-
21-
pub use self::scheduler::run_scheduler;
18+
pub mod models;
19+
pub mod queries;
Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,29 @@ GNU General Public License for more details.
1515
You should have received a copy of the GNU General Public License
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
18-
pub mod status_update;
19-
pub mod tasks;
18+
use serde::Deserialize;
19+
use std::borrow::Cow;
2020

21-
pub use self::tasks::{get_tasks, Task};
21+
#[derive(Deserialize)]
22+
pub struct Member<'a> {
23+
id: Option<i32>,
24+
roll_num: Option<Cow<'a, str>>,
25+
name: Option<Cow<'a, str>>,
26+
hostel: &'a str,
27+
email: &'a str,
28+
sex: &'a str,
29+
year: i32,
30+
mac_addr: &'a str,
31+
discord_id: &'a str,
32+
group_id: i32,
33+
}
34+
35+
#[derive(Deserialize)]
36+
struct Data<'a> {
37+
getMember: Vec<Member<'a>>,
38+
}
39+
40+
#[derive(Deserialize)]
41+
struct Root<'a> {
42+
data: Data<'a>,
43+
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818
use serde_json::Value;
1919

20+
use super::models::Member;
21+
2022
const REQUEST_URL: &str = "https://root.shuttleapp.rs/";
2123

2224
pub async fn fetch_members() -> Result<Vec<String>, reqwest::Error> {
2325
let client = reqwest::Client::new();
2426
let query = r#"
2527
query {
2628
getMember {
27-
name
29+
name,
30+
groupId,
31+
discordId
2832
}
2933
}"#;
3034

@@ -40,7 +44,7 @@ pub async fn fetch_members() -> Result<Vec<String>, reqwest::Error> {
4044
.as_array()
4145
.unwrap()
4246
.iter()
43-
.map(|member| member["name"].as_str().unwrap().to_string())
47+
.map(Member)
4448
.collect();
4549

4650
Ok(member_names)

src/ids.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ GNU General Public License for more details.
1515
You should have received a copy of the GNU General Public License
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
18-
pub const ARCHIVE_MESSAGE_ID: u64 = 1298636092886749294;
18+
/// Points to the Embed in the #roles channel.
19+
pub const ROLES_MESSAGE_ID: u64 = 1298636092886749294;
20+
21+
// Role IDs
1922
pub const ARCHIVE_ROLE_ID: u64 = 1208457364274028574;
2023
pub const MOBILE_ROLE_ID: u64 = 1298553701094395936;
2124
pub const SYSTEMS_ROLE_ID: u64 = 1298553801191718944;
@@ -24,6 +27,7 @@ pub const RESEARCH_ROLE_ID: u64 = 1298553855474270219;
2427
pub const DEVOPS_ROLE_ID: u64 = 1298553883169132554;
2528
pub const WEB_ROLE_ID: u64 = 1298553910167994428;
2629

30+
// Channel IDs
2731
pub const GROUP_ONE_CHANNEL_ID: u64 = 1225098248293716008;
2832
pub const GROUP_TWO_CHANNEL_ID: u64 = 1225098298935738489;
2933
pub const GROUP_THREE_CHANNEL_ID: u64 = 1225098353378070710;

src/main.rs

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,25 @@ GNU General Public License for more details.
1515
You should have received a copy of the GNU General Public License
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
18+
/// Stores all the commands for the bot.
1819
mod commands;
20+
/// Responsible for queries, models and mutation requests sent to and from
21+
/// [root's](https://www.github.com/amfoss/root) graphql interace.
22+
mod graphql;
23+
/// Stores Discord IDs that are needed across the bot.
1924
mod ids;
25+
/// This module is a simple cron equivalent. It spawns threads for the regular [`Task`]s that need to be completed.
2026
mod scheduler;
27+
/// An interface to define a job that needs to be executed regularly, for example checking for status updates daily.
28+
mod tasks;
29+
/// Misc. helper functions that don't really have a place anywhere else.
2130
mod utils;
2231

23-
use crate::ids::{
24-
AI_ROLE_ID, ARCHIVE_MESSAGE_ID, ARCHIVE_ROLE_ID, DEVOPS_ROLE_ID, MOBILE_ROLE_ID,
25-
RESEARCH_ROLE_ID, SYSTEMS_ROLE_ID, WEB_ROLE_ID,
32+
use ids::{
33+
AI_ROLE_ID, ARCHIVE_ROLE_ID, DEVOPS_ROLE_ID, MOBILE_ROLE_ID, RESEARCH_ROLE_ID,
34+
ROLES_MESSAGE_ID, SYSTEMS_ROLE_ID, WEB_ROLE_ID,
2635
};
36+
2737
use anyhow::Context as _;
2838
use poise::{Context as PoiseContext, Framework, FrameworkOptions, PrefixFrameworkOptions};
2939
use serenity::{
@@ -36,50 +46,90 @@ use std::collections::HashMap;
3646
pub type Error = Box<dyn std::error::Error + Send + Sync>;
3747
pub type Context<'a> = PoiseContext<'a, Data, Error>;
3848

49+
/// Runtime allocated storage for the bot.
3950
pub struct Data {
4051
pub reaction_roles: HashMap<ReactionType, RoleId>,
4152
}
4253

54+
/// This function is responsible for allocating the necessary fields
55+
/// in [`Data`], before it is passed to the bot.
56+
///
57+
/// Currently, it only needs to store the (emoji, [`RoleId`]) pair used
58+
/// for assigning roles to users who react to a particular message.
4359
pub fn initialize_data() -> Data {
4460
let mut data = Data {
4561
reaction_roles: HashMap::new(),
4662
};
4763

48-
let roles = [
49-
(ReactionType::Unicode("📁".to_string()), RoleId::new(ARCHIVE_ROLE_ID)),
50-
(ReactionType::Unicode("📱".to_string()), RoleId::new(MOBILE_ROLE_ID)),
51-
(ReactionType::Unicode("⚙️".to_string()), RoleId::new(SYSTEMS_ROLE_ID)),
52-
(ReactionType::Unicode("🤖".to_string()), RoleId::new(AI_ROLE_ID)),
53-
(ReactionType::Unicode("📜".to_string()), RoleId::new(RESEARCH_ROLE_ID)),
54-
(ReactionType::Unicode("🚀".to_string()), RoleId::new(DEVOPS_ROLE_ID)),
55-
(ReactionType::Unicode("🌐".to_string()), RoleId::new(WEB_ROLE_ID)),
64+
// Define the emoji-role pairs
65+
let roles = [
66+
(
67+
ReactionType::Unicode("📁".to_string()),
68+
RoleId::new(ARCHIVE_ROLE_ID),
69+
),
70+
(
71+
ReactionType::Unicode("📱".to_string()),
72+
RoleId::new(MOBILE_ROLE_ID),
73+
),
74+
(
75+
ReactionType::Unicode("⚙️".to_string()),
76+
RoleId::new(SYSTEMS_ROLE_ID),
77+
),
78+
(
79+
ReactionType::Unicode("🤖".to_string()),
80+
RoleId::new(AI_ROLE_ID),
81+
),
82+
(
83+
ReactionType::Unicode("📜".to_string()),
84+
RoleId::new(RESEARCH_ROLE_ID),
85+
),
86+
(
87+
ReactionType::Unicode("🚀".to_string()),
88+
RoleId::new(DEVOPS_ROLE_ID),
89+
),
90+
(
91+
ReactionType::Unicode("🌐".to_string()),
92+
RoleId::new(WEB_ROLE_ID),
93+
),
5694
];
5795

96+
// Populate reaction_roles map.
5897
data.reaction_roles
5998
.extend::<HashMap<ReactionType, RoleId>>(roles.into());
6099

61100
data
62101
}
102+
103+
/// Sets up the bot using a [`poise::Framework`], which handles most of the
104+
/// configuration including the command prefix, the event handler, the available commands,
105+
/// managing [`Data`] and running the [`scheduler`].
63106
#[shuttle_runtime::main]
64107
async fn main(
65108
#[shuttle_runtime::Secrets] secret_store: shuttle_runtime::SecretStore,
66109
) -> shuttle_serenity::ShuttleSerenity {
110+
// Uses Shuttle's environment variable storage solution SecretStore
111+
// to access the token
67112
let discord_token = secret_store
68113
.get("DISCORD_TOKEN")
69114
.context("'DISCORD_TOKEN' was not found")?;
70115

71116
let framework = Framework::builder()
72117
.options(FrameworkOptions {
118+
// Load bot commands
73119
commands: commands::get_commands(),
120+
// Pass the event handler function
74121
event_handler: |ctx, event, framework, data| {
75122
Box::pin(event_handler(ctx, event, framework, data))
76123
},
124+
// General bot settings, set to default except for prefix
77125
prefix_options: PrefixFrameworkOptions {
78126
prefix: Some(String::from("$")),
79127
..Default::default()
80128
},
81129
..Default::default()
82130
})
131+
// This function that's passed to setup() is called just as
132+
// the bot is ready to start.
83133
.setup(|ctx, _ready, framework| {
84134
Box::pin(async move {
85135
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
@@ -102,21 +152,28 @@ async fn main(
102152

103153
Ok(client.into())
104154
}
105-
155+
/// Handles various events from Discord, such as reactions.
156+
///
157+
/// Current functionality includes:
158+
/// - Adding roles to users based on reactions.
159+
/// - Removing roles from users when their reactions are removed.
160+
///
161+
/// TODO: Refactor for better readability and modularity.
106162
async fn event_handler(
107163
ctx: &SerenityContext,
108164
event: &FullEvent,
109165
_framework: poise::FrameworkContext<'_, Data, Error>,
110166
data: &Data,
111167
) -> Result<(), Error> {
112168
match event {
169+
// Handle reactions being added.
113170
FullEvent::ReactionAdd { add_reaction } => {
114-
let message_id = MessageId::new(ARCHIVE_MESSAGE_ID);
115-
if add_reaction.message_id == message_id
116-
&& data.reaction_roles.contains_key(&add_reaction.emoji)
117-
{
171+
// Check if a role needs to be added i.e check if the reaction was added to [`ROLES_MESSAGE_ID`]
172+
if is_relevant_reaction(add_reaction.message_id, &add_reaction.emoji, data) {
173+
// This check for a guild_id isn't strictly necessary, since we're already checking
174+
// if the reaction was added to the [`ROLES_MESSAGE_ID`] which *should* point to a
175+
// message in the server.
118176
if let Some(guild_id) = add_reaction.guild_id {
119-
// TODO: Use try_join to await concurrently?
120177
if let Ok(member) = guild_id.member(ctx, add_reaction.user_id.unwrap()).await {
121178
if let Err(e) = member
122179
.add_role(
@@ -127,18 +184,21 @@ async fn event_handler(
127184
)
128185
.await
129186
{
130-
eprintln!("Error: {:?}", e);
187+
// TODO: Replace with tracing
188+
eprintln!("Error adding role: {:?}", e);
131189
}
132190
}
133191
}
134192
}
135193
}
136194

195+
// Handle reactions being removed.
137196
FullEvent::ReactionRemove { removed_reaction } => {
138-
let message_id = MessageId::new(ARCHIVE_MESSAGE_ID);
139-
if message_id == removed_reaction.message_id
140-
&& data.reaction_roles.contains_key(&removed_reaction.emoji)
141-
{
197+
// Check if a role needs to be added i.e check if the reaction was added to [`ROLES_MESSAGE_ID`]
198+
if is_relevant_reaction(removed_reaction.message_id, &removed_reaction.emoji, data) {
199+
// This check for a guild_id isn't strictly necessary, since we're already checking
200+
// if the reaction was added to the [`ROLES_MESSAGE_ID`] which *should* point to a
201+
// message in the server.
142202
if let Some(guild_id) = removed_reaction.guild_id {
143203
if let Ok(member) = guild_id
144204
.member(ctx, removed_reaction.user_id.unwrap())
@@ -150,18 +210,26 @@ async fn event_handler(
150210
*data
151211
.reaction_roles
152212
.get(&removed_reaction.emoji)
153-
.expect("Hard coded value verified earlier"),
213+
.expect("Hard coded value verified earlier."),
154214
)
155215
.await
156216
{
157-
eprintln!("Error: {:?}", e);
217+
eprintln!("Error removing role: {:?}", e);
158218
}
159219
}
160220
}
161221
}
162222
}
223+
224+
// Ignore all other events for now.
163225
_ => {}
164226
}
165227

166228
Ok(())
167229
}
230+
231+
/// Helper function to check if a reaction was made to [`ROLES_MESSAGE_ID`] and if
232+
/// [`Data::reaction_roles`] contains a relevant (emoji, role) pair.
233+
fn is_relevant_reaction(message_id: MessageId, emoji: &ReactionType, data: &Data) -> bool {
234+
message_id == MessageId::new(ROLES_MESSAGE_ID) && data.reaction_roles.contains_key(emoji)
235+
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ GNU General Public License for more details.
1515
You should have received a copy of the GNU General Public License
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
18-
use crate::scheduler::tasks::{get_tasks, Task};
18+
use crate::tasks::{get_tasks, Task};
1919
use serenity::client::Context as SerenityContext;
2020

2121
use tokio::spawn;
2222

23+
/// Spawns a thread for each [`Task`].
24+
///
25+
/// [`SerenityContext`] is passed along with it so that they can
26+
/// call any required Serenity functions without creating a new [`serenity::http`]
27+
/// interface with a Discord token.
2328
pub async fn run_scheduler(ctx: SerenityContext) {
2429
let tasks = get_tasks();
2530

@@ -28,6 +33,7 @@ pub async fn run_scheduler(ctx: SerenityContext) {
2833
}
2934
}
3035

36+
/// Runs the function [`Task::run`] and goes back to sleep until it's time to run again.
3137
async fn schedule_task(ctx: SerenityContext, task: Box<dyn Task>) {
3238
loop {
3339
let next_run_in = task.run_in();

0 commit comments

Comments
 (0)