Skip to content

Commit

Permalink
Implement user-facing commands (without permission checking)
Browse files Browse the repository at this point in the history
  • Loading branch information
byte-sized-emi committed Feb 11, 2024
1 parent a15e22b commit 70ccdba
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 30 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ edition = "2021"

[dependencies]
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
futures = "0.3"
itertools = "0.12"
poise = "0.5.7" # https://github.com/serenity-rs/poise
tracing = "0.1.40" # https://github.com/tokio-rs/tracing
tracing-subscriber = "0.3.18"
Expand Down
124 changes: 94 additions & 30 deletions src/bot/commands/subject.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! TODO: Permissions
use std::borrow::BorrowMut;
use poise::serenity_prelude::{GuildId, UserId};
use futures::future::join_all;
use poise::serenity_prelude::{GuildId, RoleId};
use sqlx::{MySql, Pool};
use itertools::Itertools;

use crate::{
bot::{Context, Error},
Expand All @@ -25,16 +26,19 @@ pub async fn subject(_ctx: Context<'_>) -> Result<(), Error> {
#[poise::command(slash_command, prefix_command)]
pub async fn add(
ctx: Context<'_>,
#[description = "list of subject names or id's from \"subject show\""]
names_or_ids: Vec<String>,
#[description = "list of subject names or id's from \"subject show\""] names_or_ids: Vec<
String,
>,
) -> Result<(), Error> {
let author_id = ctx.author().id;
let user_roles = &ctx.author_member().await.unwrap().roles;
let guild_id = ctx.guild_id().unwrap();

let requested_subjects =
parse_subject_names_or_ids(&ctx.data().database_pool, guild_id, author_id, names_or_ids)
get_subjects_from_user_input(&ctx.data().database_pool, guild_id, user_roles, names_or_ids)
.await;

let formatted_subjects = format_subjects(&requested_subjects);

let roles: Vec<_> = requested_subjects
.into_iter()
.map(|subject| subject.role)
Expand All @@ -43,44 +47,40 @@ pub async fn add(
let mut author_member = ctx.author_member().await.unwrap();
author_member.to_mut().add_roles(ctx.http(), &roles).await?;

// TODO: Feedback to user
ctx.reply(format!("Added following subjects to you:\n{formatted_subjects}")).await?;

Ok(())
}

/// Gets all available subjects for this user from the database, sorted alphabetically by the subject name
/// TODO: Filter out only the ones that the user should be able to access
async fn get_available_subjects(
db: &Pool<MySql>,
guild: GuildId,
user: UserId,
) -> Vec<DatabaseSubject> {
mysql_lib::get_subjects(db, guild).await.unwrap()
}

/// Removes a subject from the user that sent this command
#[poise::command(slash_command, prefix_command)]
pub async fn remove(
ctx: Context<'_>,
#[description = "list of subject names or id's from \"subject show\""]
names_or_ids: Vec<String>
#[description = "list of subject names or id's from \"subject show\""] names_or_ids: Vec<
String,
>,
) -> Result<(), Error> {
let author_id = ctx.author().id;
let user_roles = &ctx.author_member().await.unwrap().roles;
let guild_id = ctx.guild_id().unwrap();

let requested_subjects =
parse_subject_names_or_ids(&ctx.data().database_pool, guild_id, author_id, names_or_ids)
get_subjects_from_user_input(&ctx.data().database_pool, guild_id, user_roles, names_or_ids)
.await;

let formatted_subjects = format_subjects(&requested_subjects);

let roles: Vec<_> = requested_subjects
.into_iter()
.map(|subject| subject.role)
.collect();

let mut author_member = ctx.author_member().await.unwrap();
author_member.to_mut().remove_roles(ctx.http(), &roles).await?;
author_member
.to_mut()
.remove_roles(ctx.http(), &roles)
.await?;

// TODO: Feedback to user
ctx.reply(format!("Removed following subjects from you:\n{formatted_subjects}")).await?;

Ok(())
}
Expand All @@ -90,25 +90,90 @@ pub async fn remove(
pub async fn show(ctx: Context<'_>) -> Result<(), Error> {
let db = &ctx.data().database_pool;
let guild = ctx.guild_id().unwrap();
let user = ctx.author().id;
let user_roles = &ctx.author_member().await.unwrap().roles;

let available_subjects = get_available_subjects(db, guild, user_roles).await;

let available_subjects = get_available_subjects(db, guild, user).await;
let formatted_subjects = format_subjects(&available_subjects);

// TODO: Feedback to user
ctx.reply(format!(
"
The following subjects are available (add them using \"subject add/remove\":
{formatted_subjects}
"
))
.await?;

Ok(())
}

fn format_subjects(subjects: &[DatabaseSubject]) -> String {
subjects
.iter()
.map(|subject| format!("{}: {}", subject.id.unwrap_or(-1), subject.name))
.join("\n")
}

/// Gets all available subjects for this user from the database
///
/// This could probably be rewritten to use only a single SQL statement,
/// which would be substantially better
///
/// TODO: Test this
async fn get_available_subjects(
db: &Pool<MySql>,
guild: GuildId,
user_roles: &[RoleId],
) -> Vec<DatabaseSubject> {


// objective: Get all semester study groups below or at the user's

// get all semester study groups for guild (they have an associated discord role)
let guild_semester_study_groups = mysql_lib::get_semester_study_groups_in_guild(db, guild).await.unwrap();

// find out which semester study group user is in using that role
let user_semester_study_group = guild_semester_study_groups
.iter()
.find(|group| user_roles.contains(&group.role))
.unwrap();

// get all related semester study groups
let related_semester_study_groups = mysql_lib::get_semester_study_groups_in_study_group(db, user_semester_study_group.study_group_id).await.unwrap();

// filter out to only those whose semester <= user's semester
let valid_semester_study_groups = related_semester_study_groups.into_iter()
.filter(|sem_study_group| sem_study_group.semester <= user_semester_study_group.semester);

// get all subject's for all those semester study groups
let subjects: Vec<_> = valid_semester_study_groups
.map(|sem_study_group| {
mysql_lib::get_subjects_for_semester_study_group(db, sem_study_group.role)
})
.collect();

let subjects = join_all(subjects).await;

subjects.iter()
.flatten().flatten().cloned()
.sorted_by_key(|subject| subject.id)
.dedup()
.collect()
}

/// Parses subject names/ids from user input, then
/// gets the appropriate database objects for it.
/// Needs the user's roles because those dictate which
/// subjects are available to them.
///
/// TODO: Handle failures correctly
async fn parse_subject_names_or_ids(
async fn get_subjects_from_user_input(
db: &Pool<MySql>,
guild: GuildId,
user: UserId,
user_roles: &[RoleId],
names_or_ids: Vec<String>,
) -> Vec<DatabaseSubject> {
let available_subjects = get_available_subjects(db, guild, user).await;
let available_subjects = get_available_subjects(db, guild, user_roles).await;

names_or_ids
.into_iter()
Expand Down Expand Up @@ -136,7 +201,6 @@ async fn parse_subject_names_or_ids(
.collect()
}


/// Admin commands for creating/deleting subjects
#[poise::command(prefix_command, subcommands("create", "delete"), subcommand_required)]
pub async fn manage(_ctx: Context<'_>) -> Result<(), Error> {
Expand Down
39 changes: 39 additions & 0 deletions src/mysql_lib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ pub async fn get_semester_study_groups_in_study_group(
}
}


/// Inserts a new Subject into the Database. Return if the Subject was inserted into the
/// Database, may be false if the Subject was already in the Database.
#[allow(dead_code)]
Expand Down Expand Up @@ -1188,6 +1189,44 @@ pub async fn get_study_subject_links_for_study_group(
}
}


/// Gets all subjects for this semester study group role
///
/// SQL Query explanation:
///
/// Get all study subject links for a certain semester study group role:
/// "SELECT * FROM Study_subject_link WHERE study_group_role=?"
///
/// Get subject from role:
/// "SELECT * FROM Subject WHERE role=?"
///
/// Combined:
/// "SELECT * FROM Subject
/// LEFT JOIN Study_subject_link
/// ON Subject.role = Study_subject_link.subject_role
/// WHERE study_group_role=?"
pub async fn get_subjects_for_semester_study_group(
pool: &Pool<MySql>,
semester_study_group_role: RoleId,
) -> Option<Vec<DatabaseSubject>> {
match sqlx::query_as::<_, DatabaseSubject>(
"SELECT * FROM Subject
LEFT JOIN Study_subject_link
ON Subject.role = Study_subject_link.subject_role
WHERE study_group_role=?"
)
.bind(semester_study_group_role.0)
.fetch_all(pool)
.await
{
Ok(val) => Some(val),
Err(err) => {
error!(error = err.to_string(), "Problem executing query");
None
}
}
}

/// Checks id there exists a Study-Subject Link saved for the Semester Study Group and Subject in the Database
#[allow(dead_code)]
pub async fn is_study_subject_link_in_database(
Expand Down
3 changes: 3 additions & 0 deletions src/mysql_lib/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ mod tests {
let pool = get_connection_pool().await;
let guild = create_guild_in_database(&pool).await;
let subject = DatabaseSubject {
id: None,
role: RoleId(5),
guild_id: guild.guild_id,
name: "SE1".to_string(),
Expand All @@ -390,6 +391,7 @@ mod tests {
.expect("Query was not successful");
assert!(result, "Subject couldn't be inserted");
let subject2 = DatabaseSubject {
id: None,
role: RoleId(6),
guild_id: guild.guild_id,
name: "SE2".to_string(),
Expand Down Expand Up @@ -427,6 +429,7 @@ mod tests {
let pool = get_connection_pool().await;
let guild = create_guild_in_database(&pool).await;
let subject = DatabaseSubject {
id: None,
role: RoleId(5),
guild_id: guild.guild_id,
name: "SE1".to_string(),
Expand Down

0 comments on commit 70ccdba

Please sign in to comment.