diff --git a/package.json b/package.json index 1b8c425..d2cf6a5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "node-schedule": "^2.1.1", "@grammyjs/parse-mode": "^1.10.0", "fastify": "^4.27.0", - "leetcode-query": "^1.2.3" + "leetcode-query": "^1.2.3", + "codeforces-api-ts": "^3.0.1" }, "devDependencies": { "@grammyjs/types": "^3.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1195d6..4c46649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@grammyjs/parse-mode': specifier: ^1.10.0 version: 1.10.0(grammy@1.24.1) + codeforces-api-ts: + specifier: ^3.0.1 + version: 3.0.1 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -493,6 +496,17 @@ packages: ieee754: 1.2.1 dev: false + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -506,6 +520,13 @@ packages: supports-color: 7.2.0 dev: true + /codeforces-api-ts@3.0.1: + resolution: {integrity: sha512-VkHDKFD8EA94OqJD9GGehJeNP5GtACNrhEzZBejroTl6k8GlTH/X/s7yQUnzkqfek8gWe2dvuhvvE07DkZ25CQ==} + dependencies: + lodash: 4.17.21 + qs: 6.12.1 + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -572,6 +593,15 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -589,6 +619,18 @@ packages: engines: {node: '>=12'} dev: false + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -842,6 +884,21 @@ packages: engines: {node: '>= 0.6'} dev: false + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -873,6 +930,12 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: false + /grammy@1.24.1: resolution: {integrity: sha512-0ijKmqL1wlxIk+Zml4qZDXTbacWFf9IEmTPAjvNpl5jpaarkE7SOxst1gDb0ZmySAPimzvP2vH4rc0IhwexSOA==} engines: {node: ^12.20.0 || >=14.13.1} @@ -895,6 +958,29 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -1021,6 +1107,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /long-timeout@0.1.1: resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} dev: false @@ -1096,6 +1186,10 @@ packages: sorted-array-functions: 1.3.0 dev: false + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + /on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1208,6 +1302,13 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -1289,6 +1390,18 @@ packages: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1301,6 +1414,16 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} diff --git a/src/api/cfFetch.ts b/src/api/cfFetch.ts new file mode 100644 index 0000000..e3115c6 --- /dev/null +++ b/src/api/cfFetch.ts @@ -0,0 +1,17 @@ +import { CodeforcesAPI } from "codeforces-api-ts"; + +export async function cfFetchContests(): Promise<{ name: string; startTime: number; id: number }[]> { + const res = await CodeforcesAPI.call("contest.list", {}); + if (res.status == "OK") { + let futureContests = res.result.filter(o => o.phase == "BEFORE"); + futureContests.filter(o => o.name.includes("Codeforces") && o.name.includes("Div. ") && o.startTimeSeconds !== undefined); + return futureContests.map((o) => ({ + name: o.name, + id: o.id, + startTime: o.startTimeSeconds as number + })); + } + else { + throw new Error("Unable to reach Codeforces API :("); + } +} diff --git a/src/bot/commands/contestRem.ts b/src/bot/commands/contestRem.ts index 1403de4..34ae496 100644 --- a/src/bot/commands/contestRem.ts +++ b/src/bot/commands/contestRem.ts @@ -1,4 +1,5 @@ import { bot } from "../bot"; +import { CodeforcesAPI } from "codeforces-api-ts"; export async function reminderContestLC(contestName: string, contestKey: string): Promise { await bot.api.sendMessage(parseInt(process.env.CHAT_ID as string), @@ -10,4 +11,14 @@ export async function reminderContestCodeChef(): Promise { await bot.api.sendMessage(parseInt(process.env.CHAT_ID as string), `CodeChef contest is in ~10 minutes.\n Attend the contest here: https://www.codechef.com/contests/`); +} + +export async function reminderContestCF(contestName: string, contestID: number, contestTime: Date) { + let options: {timeZone: string, timeStyle: "short"} = { + timeZone: 'Asia/Calcutta', + timeStyle: "short" + }; + await bot.api.sendMessage(parseInt(process.env.CHAT_ID as string), + `${contestName} at ${new Date(contestTime).toLocaleTimeString('en-US', options)} IST starts in ~10 minutes. +Attend the contest here: https://codeforces.com/contests/${contestID}`); } \ No newline at end of file diff --git a/src/helpers/cfSchedule.ts b/src/helpers/cfSchedule.ts new file mode 100644 index 0000000..2e0645f --- /dev/null +++ b/src/helpers/cfSchedule.ts @@ -0,0 +1,18 @@ +import { cfFetchContests } from "../api/cfFetch"; +import { scheduleJob } from "node-schedule"; +import { reminderContestCF } from "../bot/commands/contestRem"; + + +export async function cfSchedule() { + const contests = await cfFetchContests(); + if (contests.length === 0) { + console.log("No codeforces contests found!"); + return; + } + for (let contest of contests) { + const contestRemTime = new Date((contest.startTime - 600) * 1000); + const contestTime = new Date(contest.startTime * 1000); + scheduleJob(`${contest.name}`, contestRemTime, reminderContestCF.bind(null, contest.name, contest.id, contestTime)); + } + console.log("Codeforces contests scheduled!"); +} \ No newline at end of file diff --git a/src/helpers/lcSchedule.ts b/src/helpers/lcSchedule.ts index e60c0d8..4eee581 100644 --- a/src/helpers/lcSchedule.ts +++ b/src/helpers/lcSchedule.ts @@ -20,7 +20,6 @@ function getNextBiweeklyDate(): Date { export async function lcSchedule() { - await gracefulShutdown(); const contestNames = await fetchLCContests(); const biweekly = contestNames.filter(o => o.key.match(/^biweekly-contest-(\d+)$/) diff --git a/src/helpers/scheduler.ts b/src/helpers/scheduler.ts index 9c9c879..0e05d73 100644 --- a/src/helpers/scheduler.ts +++ b/src/helpers/scheduler.ts @@ -1,7 +1,11 @@ import { lcSchedule } from "./lcSchedule"; import { ccSchedule } from "./ccSchedule"; +import { cfSchedule } from "./cfSchedule"; +import { gracefulShutdown } from "node-schedule"; export async function contestScheduler() { + await gracefulShutdown(); await lcSchedule(); ccSchedule(); + await cfSchedule(); } \ No newline at end of file