Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Secrets*.toml
backups/
.env
*.log

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

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

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

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

36 changes: 36 additions & 0 deletions migrations/20250312124630_add_leaderboard_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Add migration script here

CREATE TABLE IF NOT EXISTS leaderboard (
id SERIAL PRIMARY KEY,
member_id INT UNIQUE NOT NULL,
leetcode_score INT,
codeforces_score INT,
unified_score INT NOT NULL,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (member_id) REFERENCES member(member_id)
);

CREATE TABLE IF NOT EXISTS leetcode_stats (
id SERIAL PRIMARY KEY,
member_id INT UNIQUE NOT NULL,
leetcode_username VARCHAR(255) NOT NULL,
problems_solved INT NOT NULL,
easy_solved INT NOT NULL,
medium_solved INT NOT NULL,
hard_solved INT NOT NULL,
contests_participated INT NOT NULL,
best_rank INT NOT NULL,
total_contests INT NOT NULL,
FOREIGN KEY (member_id) REFERENCES member(member_id)
);

CREATE TABLE IF NOT EXISTS codeforces_stats (
id SERIAL PRIMARY KEY,
member_id INT UNIQUE NOT NULL,
codeforces_handle VARCHAR(255) NOT NULL,
codeforces_rating INT NOT NULL,
max_rating INT NOT NULL,
contests_participated INT NOT NULL,
FOREIGN KEY (member_id) REFERENCES member(member_id)
);

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": {
"@apollo/client": "^3.13.4",
"graphiql": "^3.8.3",
"graphql": "^16.10.0"
}
}
66 changes: 64 additions & 2 deletions src/daily_task/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use crate::graphql::api::{
fetch_and_update_codeforces_stats, fetch_and_update_leetcode, update_leaderboard_scores,
};
use chrono::NaiveTime;
use chrono_tz::Asia::Kolkata;
use sqlx::PgPool;
use std::sync::Arc;
use tokio::time::sleep_until;
use tracing::{debug, error, info};

use crate::models::member::Member;
use crate::models::{
leaderboard::{CodeforcesStats, LeetCodeStats},
member::Member,
};

pub async fn run_daily_task_at_midnight(pool: Arc<PgPool>) {
loop {
Expand Down Expand Up @@ -47,12 +53,68 @@ async fn execute_daily_task(pool: Arc<PgPool>) {
Ok(members) => {
update_attendance(&members, &pool).await;
update_status_history(&members, &pool).await;
update_leaderboard_task(pool.clone()).await;
}
// TODO: Handle this
Err(e) => error!("Failed to fetch members: {:?}", e),
};
}

pub async fn update_leaderboard_task(pool: Arc<PgPool>) {
#[allow(deprecated)]
let today = chrono::Utc::now()
.with_timezone(&Kolkata)
.date()
.naive_local();
debug!("Updating leaderboard on {}", today);

let members: Result<Vec<Member>, sqlx::Error> =
sqlx::query_as::<_, Member>("SELECT * FROM Member")
.fetch_all(pool.as_ref())
.await;

match members {
Ok(members) => {
for member in &members {
// Update LeetCode stats
if let Ok(Some(leetcode_stats)) = sqlx::query_as::<_, LeetCodeStats>(
"SELECT leetcode_username FROM leetcode_stats WHERE member_id = $1 AND leetcode_username IS NOT NULL AND leetcode_username != ''",
)
.bind(member.member_id)
.fetch_optional(pool.as_ref())
.await {
Comment on lines +80 to +85
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

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

The query returns a single column (leetcode_username) but LeetCodeStats expects all columns defined in the model; this will cause a decode error. Use a lightweight struct with only leetcode_username or select all required columns matching LeetCodeStats.

Copilot uses AI. Check for mistakes.
let username = leetcode_stats.leetcode_username.clone();

match fetch_and_update_leetcode(pool.clone(), member.member_id, &username).await {
Ok(_) => debug!("LeetCode stats updated for member ID: {}", member.member_id),
Err(e) => error!("Failed to update LeetCode stats for member ID {}: {:?}", member.member_id, e),
}
}

if let Ok(Some(codeforces_stats)) = sqlx::query_as::<_, CodeforcesStats>(
"SELECT codeforces_handle FROM codeforces_stats WHERE member_id = $1 AND codeforces_handle IS NOT NULL AND codeforces_handle != ''",
)
.bind(member.member_id)
.fetch_optional(pool.as_ref())
.await {
Comment on lines +94 to +99
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

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

Similar to the LeetCode query, only codeforces_handle is selected while CodeforcesStats expects multiple columns; this will fail row decoding. Either select all columns or map to a minimal handle-only struct.

Copilot uses AI. Check for mistakes.
let username = codeforces_stats.codeforces_handle.clone();

match fetch_and_update_codeforces_stats(pool.clone(), member.member_id, &username).await {
Ok(_) => debug!("Codeforces stats updated for member ID: {}", member.member_id),
Err(e) => error!("Failed to update Codeforces stats for member ID {}: {:?}", member.member_id, e),
}
}
}
Comment on lines +78 to +107
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

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

This performs up to two separate queries per member (N+1 pattern) and sequential external API calls, which will not scale; batch fetch existing usernames/handles first and parallelize external calls with bounded concurrency (e.g. FuturesUnordered + semaphore).

Copilot uses AI. Check for mistakes.

match update_leaderboard_scores(pool.clone()).await {
Ok(_) => debug!("Leaderboard updated successfully."),
Err(e) => error!("Failed to update leaderboard: {e:?}"),
}
}
Err(e) => error!("Failed to fetch members: {e:?}"),
}
}

async fn update_attendance(members: &Vec<Member>, pool: &PgPool) {
#[allow(deprecated)]
let today = chrono::Utc::now()
Expand Down Expand Up @@ -104,7 +166,7 @@ async fn update_status_history(members: &Vec<Member>, pool: &PgPool) {

for member in members {
let status_update = sqlx::query(
"INSERT INTO StatusUpdateHistory (member_id, date, is_updated)
"INSERT INTO StatusUpdateHistory (member_id, date, is_updated)
VALUES ($1, $2, $3)
ON CONFLICT (member_id, date) DO NOTHING",
)
Expand Down
Loading
Loading