├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── package.json ├── secrets.json.example └── src ├── index.js ├── parsers ├── atcoder.js ├── codechef.js ├── codeforces.js ├── coj.js ├── csacademy.js ├── hackerearth.js ├── hackerrank.js ├── kaggle.js ├── leetcode.js └── topcoder.js └── utils.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .DS_Store 4 | dist/ 5 | secrets.json -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion":6 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nishanth Vijayan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoderCalendar-API 2 | An API listing programming contests across multiple platforms 3 | 4 | 5 | ## Building locally 6 | 1. To install the dependencies, run from inside this directory. 7 | ``` 8 | npm install 9 | ``` 10 | 11 | 12 | 2. To use the Kaggle API, sign up for a Kaggle account at https://www.kaggle.com. Then go to the 'Account' tab of your user profile (https://www.kaggle.com/account) and select 'Create API Token'. This will trigger the download of kaggle.json, a file containing your API credentials. 13 | Create a file named `secrets.json` inside the cloned repo. Add your kaggle username and APIKEY to in the format prescribed in `secrets.json.example` file. 14 | 15 | 16 | 3. To start the server, run. 17 | ``` 18 | npm start 19 | ``` 20 | 21 | 22 | 23 | 24 | ## Platforms supported 25 | 1. Hackerrank 26 | 2. Codechef 27 | 3. Codeforces 28 | 4. HackerEarth 29 | 5. Topcoder 30 | 6. Leetcode 31 | 7. Atcoder 32 | 8. CSAcademy 33 | 9. Kaggle 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "programming-contests-api", 3 | "version": "1.0.0", 4 | "description": "An API listing programming contests across multiple platforms", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/nishanthvijayan/programming-contests-api.git" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "bugs": { 14 | "url": "https://github.com/nishanthvijayan/programming-contests-api/issues" 15 | }, 16 | "homepage": "https://github.com/nishanthvijayan/programming-contests-api#readme", 17 | "dependencies": { 18 | "aws-sdk": "^2.809.0", 19 | "axios": "^0.19.0", 20 | "cheerio": "^1.0.0-rc.2" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.3.0", 24 | "eslint-config-airbnb-base": "^13.1.0", 25 | "eslint-plugin-import": "^2.14.0", 26 | "mocha": "^3.4.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /secrets.json.example: -------------------------------------------------------------------------------- 1 | {"kaggleUsername":"nishanthvijayan","kaggleApiKey":"0b1c3cc38366e2269c3ba6e527f19e01"} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const axios = require('axios'); 3 | 4 | const { flat, getCurrentTimeInSeconds } = require('./utils'); 5 | 6 | const codeforces = require('./parsers/codeforces'); 7 | const hackerearth = require('./parsers/hackerearth'); 8 | const hackerrank = require('./parsers/hackerrank'); 9 | const topcoder = require('./parsers/topcoder'); 10 | const leetcode = require('./parsers/leetcode'); 11 | const codechef = require('./parsers/codechef'); 12 | const atcoder = require('./parsers/atcoder'); 13 | const csacademy = require('./parsers/csacademy'); 14 | const coj = require('./parsers/coj'); 15 | const kaggle = require('./parsers/kaggle'); 16 | 17 | const s3bucket = new AWS.S3({}); 18 | 19 | 20 | exports.handler = async (event) => { 21 | return axios.all([ 22 | codeforces(), 23 | hackerearth(), 24 | hackerrank(), 25 | topcoder(), 26 | leetcode(), 27 | codechef(), 28 | atcoder(), 29 | csacademy(), 30 | coj(), 31 | kaggle(), 32 | ]) 33 | .then((contestsByPlatform) => { 34 | const contests = flat(contestsByPlatform.filter(it => Array.isArray(it))); 35 | 36 | const curTime = getCurrentTimeInSeconds(); 37 | 38 | const sortByStartTime = (a, b) => a.startTime - b.startTime; 39 | const sortByEndTime = (a, b) => a.endTime - b.endTime; 40 | 41 | const isOngoing = contest => contest.startTime < curTime && contest.endTime > curTime; 42 | const isUpcoming = contest => contest.startTime > curTime && contest.endTime > curTime; 43 | 44 | const ongoingContests = contests.filter(isOngoing).sort(sortByEndTime); 45 | const upcomingContests = contests.filter(isUpcoming).sort(sortByStartTime); 46 | 47 | const resultsJson = JSON.stringify({ 48 | results: { 49 | timestamp: curTime, 50 | ongoing: ongoingContests, 51 | upcoming: upcomingContests, 52 | } 53 | }); 54 | 55 | const params = { 56 | Bucket: "codercalendar-api", 57 | Key: "response.json", 58 | Body: resultsJson, 59 | ContentType: "application/json;charset=UTF-8", 60 | ACL: 'public-read' 61 | }; 62 | 63 | return s3bucket.upload(params).promise().then((data) => { 64 | console.log(`File uploaded successfully at ${data.Location}`) 65 | }); 66 | }); 67 | }; 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/parsers/atcoder.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | const { parserErrorHandler } = require('./../utils'); 4 | 5 | const PLATFORM = 'ATCODER'; 6 | 7 | const parseDuration = (durationString) => { 8 | const durationParts = durationString.split(':'); 9 | const hours = Number(durationParts[0]); 10 | const minutes = Number(durationParts[1]); 11 | return (hours * 60 + minutes) * 60; 12 | }; 13 | 14 | const calcStartTimeUTC = (datetimeString) => { 15 | const nineHoursInSeconds = 9 * 60 * 60; 16 | 17 | const year = datetimeString.slice(0, 4); 18 | const month = datetimeString.slice(5, 7) - 1; 19 | const day = datetimeString.slice(8, 10); 20 | const hour = datetimeString.slice(11, 13); 21 | const minute = datetimeString.slice(14, 16); 22 | 23 | // Date provided by atcoder follows Tokyo timezone(GMT+09:00) 24 | return new Date(Date.UTC(year, month, day, hour, minute)).getTime() / 1000 - nineHoursInSeconds; 25 | }; 26 | 27 | const atcoder = () => { 28 | const config = { 29 | timeout: 30000, 30 | headers: { 31 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,im', 32 | }, 33 | }; 34 | return axios.get('https://atcoder.jp/contests', config) 35 | .then((response) => { 36 | const $ = cheerio.load(response.data); 37 | const contests = $('.table-bordered > tbody > tr').slice(1); 38 | 39 | return contests.map((_, contest) => { 40 | const details = $(contest).children('td'); 41 | const name = details.eq(1).find('a').text(); 42 | const startTime = calcStartTimeUTC(details.eq(0).find('a').text()); 43 | const duration = parseDuration(details.eq(2).text()); 44 | const url = "https://atcoder.jp" + details.eq(1).find('a').attr('href'); 45 | 46 | return { 47 | name, 48 | url, 49 | platform: 'atcoder', 50 | startTime, 51 | endTime: startTime + duration, 52 | }; 53 | }).get(); 54 | }) 55 | .catch(parserErrorHandler(PLATFORM)); 56 | }; 57 | 58 | module.exports = atcoder; 59 | -------------------------------------------------------------------------------- /src/parsers/codechef.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | 4 | function parseContestDetails($, contestRow) { 5 | const details = $(contestRow).find('td'); 6 | const startTime = new Date(details.eq(2).attr("data-starttime")).getTime() / 1000; 7 | const endTime = new Date(details.eq(3).attr("data-endtime")).getTime() / 1000; 8 | 9 | return { 10 | name: details.eq(1).text(), 11 | url: `http://www.codechef.com${details.eq(1).find('a').attr('href')}`, 12 | platform: 'codechef', 13 | startTime, 14 | endTime, 15 | }; 16 | } 17 | 18 | const codechef = () => axios.get('http://www.codechef.com/contests', { timeout: 30000 }) 19 | .then((response) => { 20 | const $ = cheerio.load(response.data); 21 | const statusdiv = $('table .dataTable'); 22 | const headings = $('h2'); 23 | const contestTables = { 'Upcoming Coding Contests': [], 'Present Coding Contests': [] }; 24 | 25 | for (let i = 0; i < headings.length; i++) { 26 | if (headings.eq(i).text() !== 'Past Coding Contests') { 27 | contestTables[headings.eq(i).text()] = statusdiv.eq(i).find('tr').slice(1); 28 | } 29 | } 30 | let contests = contestTables['Present Coding Contests'].map((i, elem) => parseContestDetails($, elem)).get(); 31 | 32 | contests = contests.concat(contestTables['Upcoming Coding Contests'].map((i, elem) => parseContestDetails($, elem)).get()); 33 | 34 | return contests; 35 | }) 36 | .catch((error) => { 37 | console.log('Codechef: ', error.toString()); 38 | }); 39 | 40 | module.exports = codechef; 41 | -------------------------------------------------------------------------------- /src/parsers/codeforces.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { parserErrorHandler } = require('./../utils'); 3 | 4 | const PLATFORM = 'codeforces'; 5 | const CODEFORCES_API_URL = 'http://codeforces.com/api/contest.list'; 6 | 7 | const isContestActive = contest => contest.phase.trim() !== 'FINISHED'; 8 | 9 | const convertToStandardContest = contest => ({ 10 | name: contest.name, 11 | url: `http://codeforces.com/contests/${contest.id}`, 12 | platform: PLATFORM, 13 | startTime: contest.startTimeSeconds, 14 | endTime: (contest.startTimeSeconds + contest.durationSeconds), 15 | }); 16 | 17 | const codeforces = () => axios.get(CODEFORCES_API_URL, { timeout: 15000 }) 18 | .then(response => response.data.result.filter(isContestActive).map(convertToStandardContest)) 19 | .catch(parserErrorHandler(PLATFORM)); 20 | 21 | module.exports = codeforces; 22 | -------------------------------------------------------------------------------- /src/parsers/coj.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | const { parserErrorHandler } = require('./../utils'); 4 | 5 | const PLATFORM = 'COJ'; 6 | 7 | const calcTimeUTC = (datetimeString) => { 8 | const fourHoursInSeconds = 4 * 60 * 60; 9 | 10 | const year = datetimeString.slice(0, 4); 11 | const month = datetimeString.slice(5, 7) - 1; 12 | const day = datetimeString.slice(8, 10); 13 | const hour = datetimeString.slice(11, 13); 14 | const minute = datetimeString.slice(14, 16); 15 | 16 | // Date provided by coj follows Cuba timezone(GMT+04:00) 17 | return new Date(Date.UTC(year, month, day, hour, minute)).getTime() / 1000 + fourHoursInSeconds; 18 | }; 19 | 20 | function getUpcomingContests() { 21 | return axios.get('http://coj.uci.cu/tables/coming.xhtml', { timeout: 15000 }); 22 | } 23 | 24 | function getOngoingContests() { 25 | return axios.get('http://coj.uci.cu/tables/running.xhtml', { timeout: 15000 }); 26 | } 27 | 28 | const coj = () => axios.all([getOngoingContests(), getUpcomingContests()]) 29 | .then((responses) => { 30 | let contests = []; 31 | responses.forEach((response) => { 32 | const $ = cheerio.load(response.data); 33 | const contestRows = $('table').eq(1).find('tr').slice(2); 34 | 35 | contests = contests.concat( 36 | contestRows.map((_, contest) => { 37 | const details = $(contest).children('td'); 38 | const name = details.eq(2).find('a').text(); 39 | const startTime = calcTimeUTC(details.eq(3).find('a').text().slice(17)); 40 | const endTime = calcTimeUTC(details.eq(4).find('a').text().slice(17)); 41 | const url = `http://coj.uci.cu/contest/${details.eq(2).find('a').attr('href')}`; 42 | 43 | return { 44 | name, 45 | url, 46 | platform: 'coj', 47 | startTime, 48 | endTime, 49 | }; 50 | }).get(), 51 | ); 52 | }); 53 | 54 | return contests; 55 | }) 56 | .catch(parserErrorHandler(PLATFORM)); 57 | 58 | module.exports = coj; 59 | -------------------------------------------------------------------------------- /src/parsers/csacademy.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { parserErrorHandler } = require('./../utils'); 3 | 4 | const PLATFORM = 'CSAcademy'; 5 | 6 | const csacademy = () => { 7 | const options = { 8 | headers: { 9 | 'x-requested-with': 'XMLHttpRequest', 10 | }, 11 | timeout: 15000, 12 | }; 13 | return axios.get('https://csacademy.com/contests', options) 14 | .then(response => response.data.state.Contest 15 | .filter(contest => contest.startTime != null).map(contest => ({ 16 | name: contest.longName, 17 | url: `https://csacademy.com/contest/${contest.name}`, 18 | platform: 'csacademy', 19 | startTime: contest.startTime, 20 | endTime: contest.endTime, 21 | }))) 22 | .catch(parserErrorHandler(PLATFORM)); 23 | }; 24 | 25 | module.exports = csacademy; 26 | -------------------------------------------------------------------------------- /src/parsers/hackerearth.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { parserErrorHandler } = require('./../utils'); 3 | 4 | const PLATFORM = 'HACKEREARTH'; 5 | 6 | const hackerearth = () => { 7 | const getStartTime = contest => new Date(contest.start_utc_tz).getTime() / 1000; 8 | const getEndTime = contest => new Date(contest.end_utc_tz).getTime() / 1000; 9 | 10 | return axios.get('https://www.hackerearth.com/chrome-extension/events/', { timeout: 15000 }) 11 | .then(response => response.data.response 12 | .map(contest => ({ 13 | name: contest.title, 14 | url: contest.url, 15 | platform: 'hackerearth', 16 | startTime: getStartTime(contest), 17 | endTime: getEndTime(contest), 18 | }))) 19 | .catch(parserErrorHandler(PLATFORM)); 20 | }; 21 | 22 | module.exports = hackerearth; 23 | -------------------------------------------------------------------------------- /src/parsers/hackerrank.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const HACKERRANK_GENERAL_CONTESTS_API = 'https://www.hackerrank.com/rest/contests/upcoming?limit=20'; 4 | const HACKERRANK_COLLEGE_CONTESTS_API = 'https://www.hackerrank.com/rest/contests/college?limit=20'; 5 | 6 | const hackerrank = () => axios.all([ 7 | axios.get(HACKERRANK_GENERAL_CONTESTS_API, { timeout: 15000 }), 8 | axios.get(HACKERRANK_COLLEGE_CONTESTS_API, { timeout: 15000 }), 9 | ]) 10 | .then((response) => { 11 | const contests = (response[0].data.models).concat(response[1].data.models); 12 | return contests.map((contest) => { 13 | const startTime = new Date(contest.get_starttimeiso).getTime() / 1000; 14 | const endTime = new Date(contest.get_endtimeiso).getTime() / 1000; 15 | return { 16 | name: contest.name, 17 | url: `https://www.hackerrank.com/${contest.slug}`, 18 | platform: 'hackerrank', 19 | startTime, 20 | endTime, 21 | }; 22 | }); 23 | }) 24 | .catch((error) => { 25 | console.log('Hackerrank: ', error.toString()); 26 | }); 27 | 28 | module.exports = hackerrank; 29 | -------------------------------------------------------------------------------- /src/parsers/kaggle.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { kaggleUsername, kaggleApiKey } = require("../../secrets.json"); 3 | const { parserErrorHandler, getCurrentTimeInSeconds } = require('./../utils'); 4 | 5 | const KAGGLE_API_URL = "https://www.kaggle.com/api/v1/competitions/list"; 6 | const KAGGLE = 'kaggle'; 7 | 8 | const isContestActive = (currentTimeInSeconds) => contest => contest.endTime > currentTimeInSeconds 9 | 10 | const convertToStandardContest = contest => ({ 11 | name: contest.title, 12 | url: contest.url, 13 | platform: KAGGLE, 14 | startTime: Date.parse(contest.enabledDate) / 1000, 15 | endTime: Date.parse(contest.deadline) / 1000, 16 | }); 17 | 18 | const kaggle = () => axios.get(KAGGLE_API_URL, { 19 | timeout: 15000, 20 | auth: { 21 | username: kaggleUsername, 22 | password: kaggleApiKey, 23 | }, 24 | }) 25 | .then(response => response.data 26 | .map(convertToStandardContest) 27 | .filter(isContestActive(getCurrentTimeInSeconds())) 28 | ) 29 | .catch(parserErrorHandler(KAGGLE)); 30 | 31 | module.exports = kaggle; -------------------------------------------------------------------------------- /src/parsers/leetcode.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { parserErrorHandler, getCurrentTimeInSeconds } = require('./../utils'); 3 | 4 | const LEETCODE_API_URL = 'https://leetcode.com/contest/api/list/'; 5 | const PLATFORM = 'LEETCODE'; 6 | 7 | const isContestActive = curTime => contest => (contest.start_time + contest.duration) > curTime; 8 | 9 | const convertToStandardContest = contest => ({ 10 | name: contest.title, 11 | url: `https://leetcode.com/contest/${contest.title_slug}`, 12 | platform: 'leetcode', 13 | startTime: contest.start_time, 14 | endTime: contest.start_time + contest.duration, 15 | }); 16 | 17 | const leetcode = () => axios.get(LEETCODE_API_URL, { timeout: 15000 }) 18 | .then(response => response.data.contests 19 | .filter(isContestActive(getCurrentTimeInSeconds())) 20 | .map(convertToStandardContest)) 21 | .catch(parserErrorHandler(PLATFORM)); 22 | 23 | module.exports = leetcode; 24 | -------------------------------------------------------------------------------- /src/parsers/topcoder.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { parserErrorHandler } = require('./../utils'); 3 | 4 | const PLATFORM = 'TOPCODER'; 5 | const TOPCODER_API_URL = 'https://clients6.google.com/calendar/v3/calendars/appirio.com_bhga3musitat85mhdrng9035jg@group.calendar.google.com/events?calendarId=appirio.com_bhga3musitat85mhdrng9035jg%40group.calendar.google.com&timeMin=2017-07-10T00%3A00%3A00-04%3A00&key=AIzaSyBNlYH01_9Hc5S1J9vuFmu2nUqBZJNAXxs'; 6 | 7 | const convertToStandardContest = contest => ({ 8 | name: contest.summary, 9 | url: 'http://topcoder.com', 10 | platform: 'topcoder', 11 | startTime: new Date(contest.start.dateTime).getTime() / 1000, 12 | endTime: new Date(contest.end.dateTime).getTime() / 1000, 13 | }); 14 | 15 | const hasStartAndEndDateTime = it => it.start && it.start.dateTime && it.end && it.end.dateTime 16 | 17 | const topcoder = () => axios.get(TOPCODER_API_URL, { timeout: 15000 }) 18 | .then(response => 19 | response.data.items 20 | .filter(hasStartAndEndDateTime) 21 | .map(convertToStandardContest) 22 | ) 23 | .catch(parserErrorHandler(PLATFORM)); 24 | 25 | module.exports = topcoder; 26 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const parserErrorHandler = platform => error => { 2 | console.log(new Date(), platform, error.toString()); 3 | return []; 4 | } 5 | 6 | const flat = arr => arr.reduce((res, it) => res.concat(Array.isArray(it) ? flat(it) : it), []); 7 | 8 | const getCurrentTimeInSeconds = () => new Date().getTime() / 1000; 9 | 10 | module.exports = { 11 | parserErrorHandler, 12 | flat, 13 | getCurrentTimeInSeconds, 14 | }; 15 | --------------------------------------------------------------------------------