|
| 1 | +use reqwest; |
| 2 | +use serde_json::Value; |
| 3 | +use sqlx::PgPool; |
| 4 | +use std::sync::Arc; |
| 5 | +use std::collections::HashMap; |
| 6 | +use reqwest::Client; |
| 7 | + |
| 8 | + |
| 9 | +pub async fn fetch_and_update_codeforces_stats( |
| 10 | + pool: Arc<PgPool>, |
| 11 | + member_id: i32, |
| 12 | + username: &str, |
| 13 | +) -> Result<(), Box<dyn std::error::Error>> { |
| 14 | + let url = format!("https://codeforces.com/api/user.rating?handle={}", username); |
| 15 | + let response = reqwest::get(&url).await?.text().await?; |
| 16 | + let data: Value = serde_json::from_str(&response)?; |
| 17 | + |
| 18 | + if data["status"] == "OK" { |
| 19 | + if let Some(results) = data["result"].as_array() { |
| 20 | + let contests_participated = results.len() as i32; |
| 21 | + |
| 22 | + // Calculate the user's current and max ratings |
| 23 | + let mut max_rating = 0; |
| 24 | + let mut codeforces_rating = 0; |
| 25 | + |
| 26 | + for contest in results { |
| 27 | + if let Some(new_rating) = contest["newRating"].as_i64() { |
| 28 | + codeforces_rating = new_rating as i32; |
| 29 | + max_rating = max_rating.max(codeforces_rating); |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + let update_result = sqlx::query( |
| 34 | + r#" |
| 35 | + INSERT INTO codeforces_stats ( |
| 36 | + member_id, codeforces_handle, codeforces_rating, max_rating, contests_participated |
| 37 | + ) |
| 38 | + VALUES ($1, $2, $3, $4, $5) |
| 39 | + ON CONFLICT (member_id) DO UPDATE SET |
| 40 | + codeforces_handle = EXCLUDED.codeforces_handle, |
| 41 | + codeforces_rating = EXCLUDED.codeforces_rating, |
| 42 | + max_rating = EXCLUDED.max_rating, |
| 43 | + contests_participated = EXCLUDED.contests_participated |
| 44 | + "#, |
| 45 | + ) |
| 46 | + .bind(member_id) |
| 47 | + .bind(username) |
| 48 | + .bind(codeforces_rating) |
| 49 | + .bind(max_rating) |
| 50 | + .bind(contests_participated) |
| 51 | + .execute(pool.as_ref()) |
| 52 | + .await; |
| 53 | + |
| 54 | + match update_result { |
| 55 | + Ok(_) => println!("Codeforces stats updated for member ID: {}", member_id), |
| 56 | + Err(e) => eprintln!( |
| 57 | + "Failed to update Codeforces stats for member ID {}: {:?}", |
| 58 | + member_id, e |
| 59 | + ), |
| 60 | + } |
| 61 | + |
| 62 | + return Ok(()); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + Err(format!("Failed to fetch stats for Codeforces handle: {}", username).into()) |
| 67 | +} |
| 68 | + |
| 69 | +pub async fn update_leaderboard_scores(pool: Arc<PgPool>) -> Result<(), sqlx::Error> { |
| 70 | + let leetcode_stats = sqlx::query!( |
| 71 | + "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, |
| 72 | + contests_participated, best_rank |
| 73 | + FROM leetcode_stats" |
| 74 | + ) |
| 75 | + .fetch_all(pool.as_ref()) |
| 76 | + .await?; |
| 77 | + |
| 78 | + let codeforces_stats = sqlx::query!( |
| 79 | + "SELECT member_id, codeforces_rating, max_rating, contests_participated |
| 80 | + FROM codeforces_stats" |
| 81 | + ) |
| 82 | + .fetch_all(pool.as_ref()) |
| 83 | + .await?; |
| 84 | + |
| 85 | + let cf_lookup: HashMap<i32, (i32, i32, i32)> = codeforces_stats |
| 86 | + .iter() |
| 87 | + .map(|row| { |
| 88 | + ( |
| 89 | + row.member_id, |
| 90 | + (row.codeforces_rating, row.max_rating, row.contests_participated), |
| 91 | + ) |
| 92 | + }) |
| 93 | + .collect(); |
| 94 | + |
| 95 | + for row in &leetcode_stats { |
| 96 | + let leetcode_score = (5 * row.easy_solved) |
| 97 | + + (10 * row.medium_solved) |
| 98 | + + (20 * row.hard_solved) |
| 99 | + + (2 * row.contests_participated) |
| 100 | + + (100 - row.best_rank / 10).max(0); |
| 101 | + |
| 102 | + let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) |
| 103 | + .map(|(rating, max_rating, contests)| { |
| 104 | + let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); |
| 105 | + (cf_score, leetcode_score + cf_score) |
| 106 | + }) |
| 107 | + .unwrap_or((0, leetcode_score)); |
| 108 | + |
| 109 | + let result = sqlx::query!( |
| 110 | + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) |
| 111 | + VALUES ($1, $2, $3, $4, NOW()) |
| 112 | + ON CONFLICT (member_id) DO UPDATE SET |
| 113 | + leetcode_score = EXCLUDED.leetcode_score, |
| 114 | + codeforces_score = EXCLUDED.codeforces_score, |
| 115 | + unified_score = EXCLUDED.unified_score, |
| 116 | + last_updated = NOW()", |
| 117 | + row.member_id, |
| 118 | + leetcode_score, |
| 119 | + codeforces_score, |
| 120 | + unified_score |
| 121 | + ) |
| 122 | + .execute(pool.as_ref()) |
| 123 | + .await; |
| 124 | + |
| 125 | + if let Err(e) = result { |
| 126 | + eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + for row in &codeforces_stats { |
| 131 | + if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { |
| 132 | + continue; |
| 133 | + } |
| 134 | + |
| 135 | + let codeforces_score = (row.codeforces_rating / 10) |
| 136 | + + (row.max_rating / 20) |
| 137 | + + (5 * row.contests_participated); |
| 138 | + |
| 139 | + let unified_score = codeforces_score; |
| 140 | + |
| 141 | + let result = sqlx::query!( |
| 142 | + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) |
| 143 | + VALUES ($1, $2, $3, $4, NOW()) |
| 144 | + ON CONFLICT (member_id) DO UPDATE SET |
| 145 | + leetcode_score = EXCLUDED.leetcode_score, |
| 146 | + codeforces_score = EXCLUDED.codeforces_score, |
| 147 | + unified_score = EXCLUDED.unified_score, |
| 148 | + last_updated = NOW()", |
| 149 | + row.member_id, |
| 150 | + 0, |
| 151 | + codeforces_score, |
| 152 | + unified_score |
| 153 | + ) |
| 154 | + .execute(pool.as_ref()) |
| 155 | + .await; |
| 156 | + |
| 157 | + if let Err(e) = result { |
| 158 | + eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + Ok(()) |
| 163 | +} |
| 164 | + |
| 165 | + |
| 166 | + |
| 167 | +pub async fn fetch_and_update_leetcode( |
| 168 | + pool: Arc<PgPool>, |
| 169 | + member_id: i32, |
| 170 | + username: &str, |
| 171 | +) -> Result<(), Box<dyn std::error::Error>> { |
| 172 | + let client = Client::new(); |
| 173 | + let url = "https://leetcode.com/graphql"; |
| 174 | + let query = r#" |
| 175 | + query userProfile($username: String!) { |
| 176 | + userContestRanking(username: $username) { |
| 177 | + attendedContestsCount |
| 178 | + } |
| 179 | + matchedUser(username: $username) { |
| 180 | + profile { |
| 181 | + ranking |
| 182 | + } |
| 183 | + submitStats { |
| 184 | + acSubmissionNum { |
| 185 | + difficulty |
| 186 | + count |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + "#; |
| 192 | + |
| 193 | + let response = client |
| 194 | + .post(url) |
| 195 | + .header("Content-Type", "application/json") |
| 196 | + .json(&serde_json::json!({ |
| 197 | + "query": query, |
| 198 | + "variables": { "username": username } |
| 199 | + })) |
| 200 | + .send() |
| 201 | + .await?; |
| 202 | + |
| 203 | + let data: Value = response.json().await?; |
| 204 | + |
| 205 | + let empty_vec = vec![]; |
| 206 | + let submissions = data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"] |
| 207 | + .as_array() |
| 208 | + .unwrap_or(&empty_vec); |
| 209 | + |
| 210 | + let mut problems_solved = 0; |
| 211 | + let mut easy_solved = 0; |
| 212 | + let mut medium_solved = 0; |
| 213 | + let mut hard_solved = 0; |
| 214 | + |
| 215 | + for stat in submissions { |
| 216 | + let count = stat["count"].as_i64().unwrap_or(0) as i32; |
| 217 | + match stat["difficulty"].as_str().unwrap_or("") { |
| 218 | + "Easy" => easy_solved = count, |
| 219 | + "Medium" => medium_solved = count, |
| 220 | + "Hard" => hard_solved = count, |
| 221 | + "All" => problems_solved = count, |
| 222 | + _ => {} |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + let contests_participated = data["data"]["userContestRanking"]["attendedContestsCount"] |
| 227 | + .as_i64() |
| 228 | + .unwrap_or(0) as i32; |
| 229 | + let rank = data["data"]["matchedUser"]["profile"]["ranking"] |
| 230 | + .as_i64() |
| 231 | + .unwrap_or(0) as i32; |
| 232 | + |
| 233 | + sqlx::query!( |
| 234 | + r#" |
| 235 | + INSERT INTO leetcode_stats ( |
| 236 | + member_id, leetcode_username, problems_solved, easy_solved, medium_solved, |
| 237 | + hard_solved, contests_participated, best_rank, total_contests |
| 238 | + ) |
| 239 | + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) |
| 240 | + ON CONFLICT (member_id) DO UPDATE SET |
| 241 | + leetcode_username = EXCLUDED.leetcode_username, |
| 242 | + problems_solved = EXCLUDED.problems_solved, |
| 243 | + easy_solved = EXCLUDED.easy_solved, |
| 244 | + medium_solved = EXCLUDED.medium_solved, |
| 245 | + hard_solved = EXCLUDED.hard_solved, |
| 246 | + contests_participated = EXCLUDED.contests_participated, |
| 247 | + best_rank = EXCLUDED.best_rank, |
| 248 | + total_contests = EXCLUDED.total_contests |
| 249 | + "#, |
| 250 | + member_id, |
| 251 | + username, |
| 252 | + problems_solved, |
| 253 | + easy_solved, |
| 254 | + medium_solved, |
| 255 | + hard_solved, |
| 256 | + contests_participated, |
| 257 | + rank, |
| 258 | + contests_participated |
| 259 | + ) |
| 260 | + .execute(pool.as_ref()) |
| 261 | + .await?; |
| 262 | + |
| 263 | + Ok(()) |
| 264 | +} |
0 commit comments