Skip to content

Commit

Permalink
Merge pull request #180 from 2004yash/hustle
Browse files Browse the repository at this point in the history
  • Loading branch information
SkySingh04 authored Jan 19, 2025
2 parents 7f05f33 + b71d667 commit 5d6cbf9
Show file tree
Hide file tree
Showing 5 changed files with 1,210 additions and 68 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/schedule-leaderboard-update.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Schedule Leaderboard Update

on:
schedule:
# Runs at 13:30 UTC (7:00 PM IST) on Friday and Saturday
- cron: '30 13 * * 5,6'

jobs:
update-leaderboard:
runs-on: ubuntu-latest
steps:
- name: Update Leaderboard
run: |
curl -X POST https://pointblank.club/api/hustle
119 changes: 119 additions & 0 deletions app/(default)/api/hustle/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
interface Participant {
[key: string]: [string, string, string]; // [handle, displayName, avatarUrl]
}

interface ContestData {
id: number;
title: string;
begin: number;
length: number;
isReplay: boolean;
participants: {
[key: string]: [string, string, string];
};
submissions: number[][]; // Changed from Submission[][]
}

interface UserProblem {
try: number;
ok: boolean;
tim: number | null;
sub?: { v: number; t: number; }[];
}

interface UserScore {
info: [string, string, string];
prob: Map<number, UserProblem>;
tot: number;
}

interface ProblemDetail {
id: number;
ok: boolean;
try: number;
tim: number;
}

interface BoardEntry {
id: string;
nam: string;
disp: string;
tot: number;
pen: number;
dtl: ProblemDetail[];
}


function sort(subs: number[][], contestLength: number): number[][] {
return subs
.sort((a,b) => a[3] - b[3])
.filter(s => s[3] <= contestLength); // Filter submissions within contest duration
}

export function lead(data: ContestData, contestLength: number): BoardEntry[] {
const pen = 1200;
const scr: {
[key: string]: {
prb: Map<number, { try: number; ok: boolean; tim: number }>;
tot: number;
pen: number;
inf: [string, string, string];
}
} = {};

for(let id in data.participants) {
scr[id] = {
prb: new Map(),
tot: 0,
pen: 0,
inf: data.participants[id]
};
}

const subs = sort(data.submissions, contestLength);
for(let s of subs) {
const usr = scr[s[0]];
if(!usr.prb.has(s[1])) {
usr.prb.set(s[1], {try:0, ok:false, tim:0});
}
const prb = usr.prb.get(s[1]);
if (!prb) continue;

if(!prb.ok) {
if(s[2] === 1) {
prb.ok = true;
prb.tim = s[3];
usr.tot++;
usr.pen += s[3] + (prb.try * pen);
} else {
prb.try++;
}
}
}

const brd: BoardEntry[] = [];
for(let id in scr) {
const d = scr[id];
brd.push({
id,
nam: d.inf[0],
disp: d.inf[1] || d.inf[0],
tot: d.tot,
pen: d.pen,
dtl: Array.from(d.prb.entries()).map(([i,p]) => ({
id: i,
ok: p.ok,
try: p.try,
tim: p.tim
}))
});
}

brd.sort((a,b) => {
if(b.tot !== a.tot) return b.tot - a.tot;
if(a.pen !== b.pen) return a.pen - b.pen;
return a.nam.localeCompare(b.nam);
});

return brd;
}
97 changes: 30 additions & 67 deletions app/(default)/api/hustle/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { NextResponse } from "next/server";
import axios from "axios";
// import { JSDOM } from "jsdom";
import puppeteer from "puppeteer";
import connectMongoDB from "@/lib/dbConnect";
import { LatestModel, LeaderboardModel } from "@/models/PbHustel";
import { lead } from "@/app/(default)/api/hustle/leaderboard"; // Change .js to .ts

interface ContestRanking {
rank: number;
Expand All @@ -26,66 +25,43 @@ interface LeaderboardData {

export async function POST() {
try {
console.log("Initializing mongodb connection.");
await connectMongoDB();

const API_URL =
process.env.VJUDGE_CONTEST_API ||
"https://vjudge.net/contest/data?draw=2&start=0&length=20&sortDir=desc&sortCol=0&category=mine&running=3&title=&owner=Pbhustle&_=1733642420751";

console.log(`Fetching contest data from API: ${API_URL}`);
const { data } = await axios.get(API_URL);
console.log("Fetched data from API:", data);

const ccode = data.data[0][0];
console.log(`Extracted contest code: ${ccode}`);

const url = `https://vjudge.net/contest/${ccode}#rank`;
console.log(`Contest URL: ${url}`);
const { data: rankData } = await axios.get(
`https://vjudge.net/contest/rank/single/${ccode}`
);

console.log("Fetching existing leaderboard data from Firestore.");
const leaderboardDoc = await LeaderboardModel.findOne({ name: "leaderboard" });
const leaderboardDoc = await LeaderboardModel.findOne({
name: "leaderboard",
});

const existingData = leaderboardDoc as LeaderboardData | undefined;
console.log("Existing leaderboard data:", existingData);

const lastContestCode = existingData?.lastContestCode;
if (lastContestCode === ccode) {
console.log("This contest has already been processed. Skipping update.");
console.log("Last contest code", lastContestCode);
console.log("Current contest code", ccode);

if (Number(lastContestCode) == Number(ccode)) {
console.log("Leaderboard is already up-to-date.");
return NextResponse.json({
message: "Leaderboard is already up-to-date.",
});
}

console.log("Launching Puppeteer browser.");
const browser = await puppeteer.launch();
const page = await browser.newPage();

console.log("Navigating to contest page.");
await page.goto(url, { waitUntil: "networkidle2" });
const contestLengthInSeconds = rankData.length / 1000;
const boardRankings = lead(rankData, contestLengthInSeconds);

console.log("Waiting for rank table selector.");
await page.waitForSelector("#contest-rank-table tbody");
const latest: ContestRanking[] = boardRankings.map((user, index) => ({
rank: index + 1,
name: `${user.nam} (${user.disp})`,
score: user.tot,
}));

console.log("Extracting contest rankings from page.");
const latest: ContestRanking[] = await page.evaluate(() => {
const rows = Array.from(
document.querySelectorAll("#contest-rank-table tbody tr")
);
return rows.map((row) => {
const cells = row.querySelectorAll("td");
return {
rank: parseInt(cells[0]?.textContent?.trim() || "0"),
name: cells[1]?.textContent?.trim() || "",
score: parseInt(cells[2]?.textContent?.trim() || "0"),
};
});
});

console.log("Extracted rankings:", latest);
await browser.close();

console.log("Updating latest contest results in Firestore.");
await LatestModel.findOneAndUpdate(
{ name: "latest" },
{
Expand All @@ -97,41 +73,35 @@ export async function POST() {
{ upsert: true }
);

let rankings: LeaderboardUser[] = existingData?.rankings || [];
console.log("Existing rankings:", rankings);
let leaderboardRankings: LeaderboardUser[] = existingData?.rankings || [];

console.log("Updating rankings with latest contest data.");
latest.forEach(({ name, score }) => {
const existingUser = rankings.find((user) => user.name === name);
const existingUser = leaderboardRankings.find(
(user) => user.name === name
);
if (existingUser) {
console.log(`Updating existing user: ${name}`);
existingUser.score += score;
existingUser.consistency += 1;
} else {
console.log(`Adding new user: ${name}`);
rankings.push({ name, score, consistency: 1 });
leaderboardRankings.push({ name, score, consistency: 1 });
}
});

console.log("Sorting rankings.");
rankings.sort((a, b) => {
leaderboardRankings.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.consistency !== a.consistency) return b.consistency - a.consistency;
return (a.rank || 0) - (b.rank || 0);
});

console.log("Assigning ranks.");
rankings.forEach((user, index) => {
leaderboardRankings.forEach((user, index) => {
user.rank = index + 1;
});

console.log("Final rankings:", rankings);
console.log("Updating leaderboard in Firestore.");
await LeaderboardModel.findOneAndUpdate(
{ name: "leaderboard" },
{
$set: {
rankings,
rankings: leaderboardRankings,
updatedAt: new Date(),
lastContestCode: ccode,
},
Expand All @@ -141,25 +111,19 @@ export async function POST() {

return NextResponse.json({
message: "Leaderboard updated successfully",
rankings,
rankings: leaderboardRankings,
});
} catch (error) {
console.error("Error updating leaderboard:", error);
return NextResponse.json({ error: "Failed to update leaderboard" });
}
}

export async function GET() {
try {
await connectMongoDB();
console.log("Fetching latest contest results from Firestore.");
const latestDoc = await LatestModel.findOne({ name: "latest" });
console.log("Fetching leaderboard data from Firestore.");
const leaderboardDoc = await LeaderboardModel.findOne({ name: "leaderboard" });

console.log("Fetched data successfully:", {
latest: latestDoc,
leaderboard: leaderboardDoc,
const leaderboardDoc = await LeaderboardModel.findOne({
name: "leaderboard",
});

return NextResponse.json({
Expand All @@ -170,7 +134,6 @@ export async function GET() {
},
});
} catch (error) {
console.error("Error fetching hustle data:", error);
return NextResponse.json({ error: "Failed to fetch hustle data" });
}
}
7 changes: 6 additions & 1 deletion app/(default)/hustle/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export default function ResultsTable() {
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("/api/hustle");
const response = await fetch("/api/hustle", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);
if (response.ok && data?.data) {
Expand Down
Loading

0 comments on commit 5d6cbf9

Please sign in to comment.