Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CP leaderboard implementation #18

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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,217 changes: 874 additions & 343 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ hex = "0.4.3"
tower-http = { version = "0.6.1", features = ["cors"] }
tower = "0.5.1"
chrono-tz = "0.10.0"
serde_json = "1.0"
reqwest = { version = "0.11.27", features = ["json"] }
2 changes: 2 additions & 0 deletions migrations/20241115114207_add_leaderboard_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add migration script here
ALTER TABLE Member ADD COLUMN leaderboard_id TEXT;
2 changes: 2 additions & 0 deletions migrations/20241115163130_add_platform.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add migration script here
ALTER TABLE Member ADD COLUMN cp_platform TEXT;
1 change: 1 addition & 0 deletions migrations/20241115165201_add_ratings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE Member ADD COLUMN rating INT DEFAULT NULL;
6 changes: 4 additions & 2 deletions src/db/member.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use sqlx::FromRow;
use async_graphql::SimpleObject;

use sqlx::FromRow;

//Struct for the Member table
#[derive(FromRow, SimpleObject)]
Expand All @@ -14,4 +13,7 @@ pub struct Member {
pub sex: String,
pub year: i32,
pub macaddress: String,
pub leaderboard_id: String,
Copy link
Member

Choose a reason for hiding this comment

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

what is this used for?

Copy link
Author

Choose a reason for hiding this comment

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

this is the username for their respective cp platform

Copy link
Member

Choose a reason for hiding this comment

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

ah gotcha, but they should be free to do both like i said not just 1 platform

Copy link
Author

Choose a reason for hiding this comment

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

alright so to implement this, we might need to create another leaderboard struct as this is one to many relation with foreign key as the member's id (primary key)

Copy link
Member

Choose a reason for hiding this comment

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

Yep that's right

pub cp_platform: String,
Copy link
Member

Choose a reason for hiding this comment

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

aren't members restricted to only one platform if we do it like this?

Copy link
Author

Choose a reason for hiding this comment

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

yes, they will be. I thought we were only allowed to choose one and given the current scenario, no one is trying for both.

pub rating:String,
}
6 changes: 5 additions & 1 deletion src/graphql/mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ impl MutationRoot {
sex: String,
year: i32,
macaddress: String,
leaderboard_id:String,
cp_platform: String,

) -> Result<Member, sqlx::Error> {
let pool = ctx.data::<Arc<PgPool>>().expect("Pool not found in context");



let member = sqlx::query_as::<_, Member>(
"INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *"
"INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, leaderboard_id, cp_platform) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *"
)
.bind(rollno)
.bind(name)
Expand All @@ -45,6 +47,8 @@ impl MutationRoot {
.bind(sex)
.bind(year)
.bind(macaddress)
.bind(leaderboard_id)
.bind(cp_platform)
.fetch_one(pool.as_ref())
.await?;

Expand Down
236 changes: 194 additions & 42 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@

use std::{env, sync::Arc};
use tokio::task;
use tokio::time::{ sleep_until, Instant};
use std::time::Duration;
use crate::graphql::mutations::MutationRoot;
use crate::graphql::query::QueryRoot;
use crate::routes::graphiql;
use async_graphql::{EmptySubscription, Schema};
use async_graphql_axum::GraphQL;
use axum::{routing::get, Router};
use tower_http::cors::{Any, CorsLayer};
use chrono::{ Local, NaiveTime};
use chrono::{Local, NaiveTime};
use chrono_tz::Asia::Kolkata;
use db::member::Member;
use sqlx::PgPool;
use async_graphql::{ Schema, EmptySubscription};
use reqwest;
use serde_json::Value;
use shuttle_runtime::SecretStore;
use crate::graphql::mutations::MutationRoot;
use crate::graphql::query::QueryRoot;
use crate::routes::graphiql;
use sqlx::PgPool;
use std::time::Duration;
use std::{env, sync::Arc};
use tokio::task;
use tokio::time::{sleep_until, Instant};
use tower_http::cors::{Any, CorsLayer};

mod db;
mod graphql;
Expand All @@ -28,10 +29,16 @@ struct MyState {

//Main method
#[shuttle_runtime::main]
async fn main(#[shuttle_shared_db::Postgres] pool: PgPool,#[shuttle_runtime::Secrets] secrets: SecretStore,) -> shuttle_axum::ShuttleAxum {
async fn main(
#[shuttle_shared_db::Postgres] pool: PgPool,
#[shuttle_runtime::Secrets] secrets: SecretStore,
) -> shuttle_axum::ShuttleAxum {
env::set_var("PGOPTIONS", "-c ignore_version=true");

sqlx::migrate!().run(&pool).await.expect("Failed to run migrations");

sqlx::migrate!()
.run(&pool)
.await
.expect("Failed to run migrations");

let pool = Arc::new(pool);
let secret_key = secrets.get("ROOT_SECRET").expect("ROOT_SECRET not found");
Expand All @@ -40,33 +47,36 @@ async fn main(#[shuttle_shared_db::Postgres] pool: PgPool,#[shuttle_runtime::Sec
.data(secret_key.clone()) //
.finish();

let state = MyState { pool: pool.clone() , secret_key: secret_key.clone()};
let state = MyState {
pool: pool.clone(),
secret_key: secret_key.clone(),
};

let cors = CorsLayer::new()
.allow_origin(Any) // Allow any origin
.allow_methods(tower_http::cors::Any) // Allow any HTTP method
.allow_headers(tower_http::cors::Any);
.allow_origin(Any) // Allow any origin
.allow_methods(tower_http::cors::Any) // Allow any HTTP method
.allow_headers(tower_http::cors::Any);

let router = Router::new()
.route("/", get(graphiql).post_service(GraphQL::new(schema.clone())))
.route(
"/",
get(graphiql).post_service(GraphQL::new(schema.clone())),
)
.with_state(state)
.layer(cors);
task::spawn(async move {

schedule_task_at_midnight(pool.clone()).await; // Call the function after 10 seconds
});


Ok(router.into())
}



//Scheduled task for moving all members to Attendance table at midnight.
async fn scheduled_task(pool: Arc<PgPool>) {
let members: Result<Vec<Member>, sqlx::Error> = sqlx::query_as::<_, Member>("SELECT * FROM Member")
.fetch_all(pool.as_ref())
.await;
let members: Result<Vec<Member>, sqlx::Error> =
sqlx::query_as::<_, Member>("SELECT * FROM Member")
.fetch_all(pool.as_ref())
.await;

match members {
Ok(members) => {
Expand All @@ -75,7 +85,7 @@ async fn scheduled_task(pool: Arc<PgPool>) {
for member in members {
let timein = NaiveTime::from_hms_opt(0, 0, 0);
let timeout = NaiveTime::from_hms_opt(0, 0, 0); // Default time, can be modified as needed

let attendance = sqlx::query(
"INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id, date) DO NOTHING RETURNING *"
)
Expand All @@ -86,32 +96,174 @@ async fn scheduled_task(pool: Arc<PgPool>) {
.bind(false)
.execute(pool.as_ref())
.await;

match attendance {
Ok(_) => println!("Attendance record added for member ID: {}", member.id),
Err(e) => eprintln!("Failed to insert attendance for member ID: {}: {:?}", member.id, e),
Err(e) => eprintln!(
"Failed to insert attendance for member ID: {}: {:?}",
member.id, e
),
}
}
},
}
Err(e) => eprintln!("Failed to fetch members: {:?}", e),
}
// Update CP ratings
update_cp_ratings(pool.clone()).await;
println!("Ratings updated successfully.");
}

//Ticker for calling the scheduled task
async fn schedule_task_at_midnight(pool: Arc<PgPool>) {
loop {
let now = Local::now();
let now = Local::now();

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 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 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);

sleep_until(Instant::now() + sleep_duration).await;
scheduled_task(pool.clone()).await;
print!("done");
sleep_until(Instant::now() + sleep_duration).await;
scheduled_task(pool.clone()).await;
print!("done");
}
}
}
// Function to fetch codeforces ranking
async fn fetch_codeforces_rating(
username: &str,
) -> Result<Option<i32>, Box<dyn std::error::Error>> {
let url = format!("https://codeforces.com/api/user.rating?handle={}", username);
let response = reqwest::get(&url).await?.text().await?;
let data: Value = serde_json::from_str(&response)?;

if data["status"] == "OK" {
if let Some(results) = data["result"].as_array() {
if let Some(last_contest) = results.last() {
let new_rating = last_contest["newRating"].as_i64().unwrap_or_default() as i32;
return Ok(Some(new_rating));
}
}
}
Ok(None)
}

// Function to fetch LeetCode ranking
async fn fetch_leetcode_ranking(username: &str) -> Result<Option<i32>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let url = "https://leetcode.com/graphql";
let query = r#"
query userPublicProfile($username: String!) {
matchedUser(username: $username) {
profile {
ranking
}
}
}
"#;

let response = client
.post(url)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"query": query,
"variables": {
"username": username
}
}))
.send()
.await?;

let data: Value = response.json().await?;
let ranking = data["data"]["matchedUser"]["profile"]["ranking"]
.as_i64()
.map(|v| v as i32);

Ok(ranking)
}

// Fetch and update CP ratings for all members
async fn update_cp_ratings(pool: Arc<PgPool>) {
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 {
let rating = match member.cp_platform.as_str() {
"Codeforces" => fetch_codeforces_rating(&member.leaderboard_id)
.await
.ok()
.flatten(),
"LeetCode" => fetch_leetcode_ranking(&member.leaderboard_id)
.await
.ok()
.flatten(),
_ => None,
};

if let Some(rating) = rating {
let update_result = sqlx::query("UPDATE Member SET rating = $1 WHERE id = $2")
.bind(rating)
.bind(member.id)
.execute(pool.as_ref())
.await;

match update_result {
Ok(_) => println!("Updated rating for {}: {}", member.name, rating),
Err(e) => eprintln!("Failed to update rating for {}: {:?}", member.name, e),
}
}
}
}
Err(e) => eprintln!("Failed to fetch members: {:?}", e),
}
}
Comment on lines +135 to +225
Copy link
Member

Choose a reason for hiding this comment

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

Single Leaderboard with Weighted Scores:
Combines scores from LeetCode and Codeforces into a unified ranking.

  • Pros: Simple, gives a balanced view, and avoids splitting attention.
    
  • Cons: Needs agreement on weightage and a formula to merge scores fairly.
    

Separate Leaderboards:
Maintains distinct rankings for each platform.

  • Pros: Highlights specific performance on each platform.
    
  • Cons: Can feel redundant or less competitive if interest skews heavily toward one platform
    

can you ask in discord which is preferred? make sure to highlite these pros and cons

Copy link
Author

Choose a reason for hiding this comment

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

yes done : )


#[cfg(test)]
mod tests {
use super::*;

// Mocking the PgPool for testing purposes (if necessary)
use sqlx::PgPool;

#[tokio::test]
// Update these variables with the actual values before running the test

async fn test_fetch_codeforces_rating() {
let codeforces_username = ""; // Add your Codeforces username here
let result = fetch_codeforces_rating(codeforces_username).await;
assert!(result.is_ok());
let rating = result.unwrap();
assert!(rating.is_some());
}

#[tokio::test]
async fn test_fetch_leetcode_ranking() {
let leetcode_username = ""; // Add your LeetCode username here
let result = fetch_leetcode_ranking(leetcode_username).await;
assert!(result.is_ok());
let ranking = result.unwrap();
assert!(ranking.is_some());
}

#[tokio::test]
async fn test_scheduled_task() {
let database_url = ""; // Add your database URL here
let pool = Arc::new(PgPool::connect_lazy(database_url).unwrap());

scheduled_task(pool).await;
}

// Test for update_cp_ratings
#[tokio::test]
async fn test_update_cp_ratings() {
let database_url = ""; // Add your database URL here
let pool = Arc::new(PgPool::connect(database_url).await.unwrap());
update_cp_ratings(pool).await;
}
}
Comment on lines +227 to +269
Copy link
Member

Choose a reason for hiding this comment

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

good to add tests but should be in a separate directory not in main.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah sure will do it. I also thought about creating more tests for seeing members for dev purpose so this function can lie there.