diff --git a/src/attendance/scheduled_task.rs b/src/attendance/scheduled_task.rs index a49c8a4..efceb51 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/scheduled_task.rs @@ -57,7 +57,7 @@ pub async fn scheduled_task(pool: Arc) { if let Ok(Some(leetcode_stats)) = leetcode_username { let username = leetcode_stats.leetcode_username.clone(); - + // Fetch and update LeetCode stats match fetch_leetcode_stats(pool.clone(), member.id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), @@ -104,8 +104,13 @@ pub async fn scheduled_task(pool: Arc) { // Function to update attendance streak async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); - let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); + let today = chrono::Local::now() + .with_timezone(&chrono_tz::Asia::Kolkata) + .naive_local(); + let yesterday = today + .checked_sub_signed(chrono::Duration::hours(12)) + .unwrap() + .date(); if today.day() == 1 { let _ = sqlx::query( @@ -176,6 +181,9 @@ async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { println!("Sreak not incremented for member ID: {}", member_id); } Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), + Err(e) => eprintln!( + "Error checking attendance for member ID {}: {:?}", + member_id, e + ), } } diff --git a/src/db/attendance.rs b/src/db/attendance.rs index f03da67..4aa7f77 100644 --- a/src/db/attendance.rs +++ b/src/db/attendance.rs @@ -1,6 +1,6 @@ +use async_graphql::SimpleObject; use chrono::{NaiveDate, NaiveTime}; use sqlx::FromRow; -use async_graphql::SimpleObject; //Struct for the Attendance table #[derive(FromRow, SimpleObject)] @@ -22,19 +22,19 @@ pub struct AttendanceStreak { #[derive(FromRow, SimpleObject)] pub struct AttendanceSummary { - pub max_days:i64, + pub max_days: i64, pub member_attendance: Vec, pub daily_count: Vec, } #[derive(FromRow, SimpleObject)] pub struct MemberAttendance { - pub id:i32, - pub present_days:i64, + pub id: i32, + pub present_days: i64, } #[derive(FromRow, SimpleObject)] pub struct DailyCount { pub date: NaiveDate, pub count: i64, -} \ No newline at end of file +} diff --git a/src/db/projects.rs b/src/db/projects.rs index d0f930f..448c5e5 100644 --- a/src/db/projects.rs +++ b/src/db/projects.rs @@ -1,9 +1,9 @@ -use sqlx::FromRow; use async_graphql::SimpleObject; +use sqlx::FromRow; #[derive(FromRow, SimpleObject)] pub struct ActiveProjects { id: i32, member_id: i32, project_title: Option, -} \ No newline at end of file +} diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index 74a0d55..7a13211 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -1,41 +1,44 @@ -use async_graphql::{Context, Object}; use ::chrono::Local; +use async_graphql::{Context, Object}; use chrono::{NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; -use sqlx::PgPool; +use hmac::{Hmac, Mac}; +use sha2::Sha256; use sqlx::types::chrono; +use sqlx::PgPool; use std::sync::Arc; -use hmac::{Hmac,Mac}; -use sha2::Sha256; - type HmacSha256 = Hmac; -use crate::db::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; +use crate::db::{ + attendance::Attendance, + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + member::StreakUpdate, + projects::ActiveProjects, +}; pub struct MutationRoot; #[Object] impl MutationRoot { - //Mutation for adding members to the Member table async fn add_member( - &self, - ctx: &Context<'_>, - rollno: String, - name: String, - hostel: String, - email: String, - sex: String, + &self, + ctx: &Context<'_>, + rollno: String, + name: String, + hostel: String, + email: String, + sex: String, year: i32, macaddress: String, discord_id: String, group_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let member = sqlx::query_as::<_, Member>( "INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, discord_id, group_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *" @@ -55,7 +58,6 @@ impl MutationRoot { Ok(member) } - async fn edit_member( &self, ctx: &Context<'_>, @@ -66,25 +68,31 @@ impl MutationRoot { discord_id: String, group_id: i32, hmac_signature: String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); - let message = format!("{}{}{}{}{}{}", id, hostel, year, macaddress, discord_id, group_id); + let message = format!( + "{}{}{}{}{}{}", + id, hostel, year, macaddress, discord_id, group_id + ); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -99,9 +107,8 @@ impl MutationRoot { group_id = CASE WHEN $5 = 0 THEN group_id ELSE $5 END WHERE id = $6 RETURNING * - " + ", ) - .bind(hostel) .bind(year) .bind(macaddress) @@ -113,27 +120,25 @@ impl MutationRoot { Ok(member) } - + //Mutation for adding attendance to the Attendance table async fn add_attendance( - &self, - + ctx: &Context<'_>, id: i32, date: NaiveDate, timein: NaiveTime, timeout: NaiveTime, is_present: bool, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance = sqlx::query_as::<_, Attendance>( "INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) RETURNING *" ) - .bind(id) .bind(date) .bind(timein) @@ -144,34 +149,36 @@ impl MutationRoot { Ok(attendance) } - + async fn mark_attendance( &self, ctx: &Context<'_>, id: i32, date: NaiveDate, is_present: bool, - hmac_signature: String, - ) -> Result { - - let pool = ctx.data::>().expect("Pool not found in context"); + hmac_signature: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); let message = format!("{}{}{}", id, date, is_present); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -186,7 +193,7 @@ impl MutationRoot { is_present = $2 WHERE id = $3 AND date = $4 RETURNING * - " + ", ) .bind(current_time) .bind(is_present) @@ -199,14 +206,16 @@ impl MutationRoot { } //here when user changes the handle, it just updates the handle in the database without updating the other values till midnight - + async fn add_or_update_leetcode_username( &self, ctx: &Context<'_>, member_id: i32, username: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, LeetCodeStats>( " @@ -231,7 +240,9 @@ impl MutationRoot { member_id: i32, handle: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, CodeforcesStats>( " @@ -255,20 +266,22 @@ impl MutationRoot { id: i32, has_sent_update: bool, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak_info = sqlx::query_as::<_, StreakUpdate>( " SELECT id, streak, max_streak FROM StreakUpdate WHERE id = $1 - " + ", ) .bind(id) .fetch_optional(pool.as_ref()) .await?; - match streak_info{ + match streak_info { Some(mut member) => { let current_streak = member.streak.unwrap_or(0); let max_streak = member.max_streak.unwrap_or(0); @@ -285,7 +298,7 @@ impl MutationRoot { SET streak = $1, max_streak = $2 WHERE id = $3 RETURNING * - " + ", ) .bind(new_streak) .bind(new_max_streak) @@ -294,14 +307,14 @@ impl MutationRoot { .await?; Ok(updated_member) - }, + } None => { let new_member = sqlx::query_as::<_, StreakUpdate>( " INSERT INTO StreakUpdate (id, streak, max_streak) VALUES ($1, $2, $3) RETURNING * - " + ", ) .bind(id) .bind(0) @@ -318,16 +331,18 @@ impl MutationRoot { &self, ctx: &Context<'_>, id: i32, - project_name:String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + project_name: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " INSERT INTO ActiveProjects (member_id,project_title) VALUES ($1,$2) RETURNING * - " + ", ) .bind(id) .bind(project_name) @@ -341,15 +356,17 @@ impl MutationRoot { &self, ctx: &Context<'_>, project_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " DELETE FROM ActiveProjects WHERE id = $1 RETURNING * - " + ", ) .bind(project_id) .fetch_one(pool.as_ref()) @@ -357,5 +374,4 @@ impl MutationRoot { Ok(active_project) } - -} \ No newline at end of file +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 65c5d76..0d1a896 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,11 +1,16 @@ +use crate::db::{ + attendance::Attendance, + leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, + member::{Member, StreakUpdate}, +}; use async_graphql::{Context, Object}; use chrono::NaiveDate; -use root::db::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; +use root::db::{ + attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, + projects::ActiveProjects, +}; use sqlx::PgPool; use std::sync::Arc; -use crate::db::{ - attendance::Attendance, leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, member::{Member, StreakUpdate} -}; pub struct QueryRoot; @@ -97,43 +102,42 @@ impl QueryRoot { .await?; Ok(attendance_list) } - async fn get_streak( - &self, - ctx: &Context<'_>, - id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_streak(&self, ctx: &Context<'_>, id: i32) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate WHERE id = $1") - .bind(id) - .fetch_one(pool.as_ref()) - .await?; + .bind(id) + .fetch_one(pool.as_ref()) + .await?; Ok(streak) } - async fn get_update_streak( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_update_streak(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate") - .fetch_all(pool.as_ref()) - .await?; + .fetch_all(pool.as_ref()) + .await?; Ok(streak) } - + async fn get_attendance_streak( &self, ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - let attendance_streak = sqlx::query_as::<_,AttendanceStreak>( + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let attendance_streak = sqlx::query_as::<_, AttendanceStreak>( "SELECT * from AttendanceStreak WHERE month >= $1 AND month < $2 - " + ", ) .bind(start_date) .bind(end_date) @@ -148,13 +152,15 @@ impl QueryRoot { ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance_days = sqlx::query_as::<_, (NaiveDate, i64)>( "SELECT date, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY date ORDER BY date" + GROUP BY date ORDER BY date", ) .bind(start_date) .bind(end_date) @@ -165,7 +171,7 @@ impl QueryRoot { "SELECT id, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY id" + GROUP BY id", ) .bind(start_date) .bind(end_date) @@ -175,7 +181,7 @@ impl QueryRoot { let max_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 - AND is_present = true" + AND is_present = true", ) .bind(start_date) .bind(end_date) @@ -183,18 +189,16 @@ impl QueryRoot { .await?; let daily_count = attendance_days - .into_iter().map(|(date, count)| DailyCount{ - date, count - }) + .into_iter() + .map(|(date, count)| DailyCount { date, count }) .collect(); let member_attendance = member_attendance - .into_iter().map(|(id, present_days)| MemberAttendance{ - id, present_days - }) + .into_iter() + .map(|(id, present_days)| MemberAttendance { id, present_days }) .collect(); - let summaries: AttendanceSummary = AttendanceSummary{ + let summaries: AttendanceSummary = AttendanceSummary { max_days: max_count[0], member_attendance, daily_count, @@ -207,33 +211,35 @@ impl QueryRoot { &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let dates = sqlx::query_scalar::<_, NaiveDate>( "SELECT date FROM Attendance GROUP BY date HAVING BOOL_AND(NOT is_present) - ORDER BY date" + ORDER BY date", ) .fetch_all(pool.as_ref()) .await?; - + Ok(dates) } - + pub async fn get_projects( &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - - let active_projects = sqlx::query_as::<_, ActiveProjects>( - "SELECT * FROM ActiveProjects" - ) - .fetch_all(pool.as_ref()) - .await?; - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let active_projects = sqlx::query_as::<_, ActiveProjects>("SELECT * FROM ActiveProjects") + .fetch_all(pool.as_ref()) + .await?; + Ok(active_projects) } } diff --git a/src/leaderboard/mod.rs b/src/leaderboard/mod.rs index 7d7eadf..5dbae36 100644 --- a/src/leaderboard/mod.rs +++ b/src/leaderboard/mod.rs @@ -1,4 +1,2 @@ pub mod fetch_stats; pub mod update_leaderboard; - - diff --git a/src/leaderboard/update_leaderboard.rs b/src/leaderboard/update_leaderboard.rs index e8bf348..f575957 100644 --- a/src/leaderboard/update_leaderboard.rs +++ b/src/leaderboard/update_leaderboard.rs @@ -1,8 +1,7 @@ -use std::sync::Arc; use sqlx::PgPool; +use std::sync::Arc; pub async fn update_leaderboard(pool: Arc) -> Result<(), Box> { - let leetcode_stats: Result, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32, i32, i32, i32)>( "SELECT id, member_id, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank @@ -19,7 +18,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32)>( "SELECT id, member_id, codeforces_rating, max_rating, contests_participated @@ -49,7 +47,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box shuttle_axum::ShuttleAxum { + // TODO: Explain? env::set_var("PGOPTIONS", "-c ignore_version=true"); sqlx::migrate!() .run(&pool) .await - .expect("Failed to run migrations"); + .expect("Failed to run migrations."); let pool = Arc::new(pool); let secret_key = secrets.get("ROOT_SECRET").expect("ROOT_SECRET not found"); @@ -50,6 +49,7 @@ async fn main( secret_key: secret_key.clone(), }; + // TODO: Restrict to amD and Home let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(tower_http::cors::Any) @@ -62,27 +62,29 @@ async fn main( ) .with_state(state) .layer(cors); + task::spawn(async move { - schedule_task_at_midnight(pool.clone()).await; // Call the function after 10 seconds + schedule_task_at_midnight(pool.clone()).await; }); + Ok(router.into()) } -//Ticker for calling the scheduled task +// Sleep till midnight, then execute the task, repeat. async fn schedule_task_at_midnight(pool: Arc) { loop { let now = Local::now(); + let next_midnight = (now + chrono::Duration::days(1)) + .date_naive() + .and_hms_opt(0, 0, 0) + .unwrap(); - let tomorrow = now.date_naive().succ_opt().unwrap(); - let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); - let next_midnight = tomorrow.and_time(midnight); - - let now_naive = now.naive_local(); - let duration_until_midnight = next_midnight.signed_duration_since(now_naive); - let sleep_duration = Duration::from_secs(duration_until_midnight.num_seconds() as u64 + 60); + let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); + let sleep_duration = tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); sleep_until(Instant::now() + sleep_duration).await; scheduled_task(pool.clone()).await; + // TODO: Use tracing print!("done"); } } diff --git a/src/routes.rs b/src/routes.rs index 1dfdeac..f94d6b3 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,6 +1,5 @@ - -use axum::response::{Html, IntoResponse}; use async_graphql::http::GraphiQLSource; +use axum::response::{Html, IntoResponse}; pub async fn graphiql() -> impl IntoResponse { Html(