├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .snyk ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── examples ├── assets │ └── submit_solution.gif └── submit.md ├── gulpfile.js ├── package.json ├── src ├── bin │ └── cli │ │ └── cf.js ├── index.js └── lib │ ├── api │ ├── Userrating.js │ ├── standings.js │ ├── submission.js │ ├── tags.js │ ├── userinfo.js │ └── usertags.js │ ├── countries.js │ ├── crawler │ ├── Countrystandings.js │ ├── Ratings.js │ ├── Sourcecode.js │ └── Submit.js │ ├── helpers.js │ ├── languages.js │ ├── utils │ └── cfdefault.js │ └── verdicts.js └── tests ├── api ├── test_standings.js ├── test_submission.js ├── test_tags.js ├── test_userinfo.js ├── test_userrating.js └── test_usertags.js ├── crawler ├── test_countrystandings.js ├── test_ratings.js ├── test_sourcecode.js └── test_submit.js └── helpers └── submission_helper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tests 3 | node_modules 4 | npm-debug.log 5 | *.yml 6 | coverage 7 | .nyc_output 8 | .babelrc 9 | .git 10 | gulpfile.js 11 | debug 12 | examples 13 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [2, 4, { 13 | "SwitchCase": 1, 14 | "MemberExpression": 1 15 | }], 16 | "eqeqeq": 0, 17 | "quotes": [2, "single"], 18 | "semi": [2, "always"], 19 | "no-multi-spaces": 2, 20 | "no-redeclare": 2, 21 | "no-lonely-if": 2, 22 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 23 | "newline-per-chained-call": [2, { "ignoreChainWithDepth": 2 }], 24 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 25 | "no-unused-vars": [2, { "vars": "all", "args": "none" }] 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | coverage 5 | .nyc_output 6 | build 7 | debug -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .idea 3 | tests 4 | node_modules 5 | npm-debug.log 6 | .travis.yml 7 | coverage 8 | .nyc_output 9 | .babelrc 10 | .git 11 | gulpfile.js 12 | debug 13 | examples -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.7.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | 'npm:marked:20170112': 7 | - blessed-contrib > marked: 8 | patched: '2017-02-10T14:48:34.665Z' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | os: 7 | - linux 8 | - osx 9 | notifications: 10 | email: false 11 | after_success: 12 | - npm run coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ahmed Dinar 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 | ```text 2 | ____ _ __ ____ _ ___ 3 | / ___|___ __| | ___ / _| ___ _ __ ___ ___ ___ / ___| | |_ _| 4 | | | / _ \ / _` |/ _ \ |_ / _ \| '__/ __/ _ \/ __| | | | | | | 5 | | |__| (_) | (_| | __/ _| (_) | | | (_| __/\__ \ | |___| |___ | | 6 | \____\___/ \__,_|\___|_| \___/|_| \___\___||___/ \____|_____|___| 7 | ``` 8 | A simple and useful Command Line tool for Codeforces coders 9 | 10 | [![Travis branch](https://img.shields.io/travis/ahmed-dinar/codeforces-cli/master.svg?label=unix&style=flat-square)](https://travis-ci.org/ahmed-dinar/codeforces-cli) [![AppVeyor branch](https://img.shields.io/appveyor/ci/ahmed-dinar/codeforces-cli/master.svg?label=windows&style=flat-square)](https://ci.appveyor.com/project/ahmed-dinar/codeforces-cli) [![Coveralls branch](https://img.shields.io/coveralls/ahmed-dinar/codeforces-cli/master.svg?style=flat-square)](https://coveralls.io/github/ahmed-dinar/codeforces-cli) [![David](https://img.shields.io/david/ahmed-dinar/codeforces-cli.svg?label=deps&style=flat-square)](https://david-dm.org/ahmed-dinar/codeforces-cli) 11 | 12 | 13 | 14 | ## Features 15 | - Submit code with remember password option in all language supported by Codeforces. 16 | - Live judge status after submission or anytime. 17 | - Download accepted solutions with problem statement. 18 | - User submission statistics based on codeforces tags. 19 | - Codeforces tags distribution. 20 | - Contest standings by user/country. 21 | - User rating with chart view/table view. 22 | - User rating by country. 23 | - User info (profile). 24 | - Cross-platform support.Tested on Linux, Mac and Windows. 25 | 26 | ## Releases 27 | #### Work in progress 28 | 29 | 30 | ## Inspirations 31 | - [Codeforces API](http://codeforces.com/api/help) 32 | 33 | ## License 34 | ##### [MIT](https://raw.githubusercontent.com/ahmed-dinar/codeforces-cli/master/LICENSE) © [Ahmed Dinar](https://ahmeddinar.com/) 35 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "4" 4 | - nodejs_version: "5" 5 | - nodejs_version: "6" 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - npm install 10 | 11 | build: off 12 | 13 | test_script: 14 | - node --version 15 | - npm --version 16 | - npm test 17 | 18 | version: "{build}" 19 | 20 | matrix: 21 | fast_finish: true 22 | 23 | cache: 24 | - node_modules -------------------------------------------------------------------------------- /examples/assets/submit_solution.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-dinar/codeforces-cli/95e0bab4837c2832148ad6394ae2f39c21a94ab8/examples/assets/submit_solution.gif -------------------------------------------------------------------------------- /examples/submit.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-dinar/codeforces-cli/95e0bab4837c2832148ad6394ae2f39c21a94ab8/examples/submit.md -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | 4 | gulp.task('src', () => { 5 | return gulp.src(['src/**/*.js']) 6 | .pipe(babel({ 7 | presets: ['es2015'], 8 | "plugins": [ 9 | "add-module-exports" 10 | ] 11 | })) 12 | .pipe(gulp.dest('build')); 13 | }); 14 | 15 | gulp.task('default', ['src'] , () => { 16 | gulp.watch('src/**/*', ['src']); 17 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeforces-cli", 3 | "version": "0.0.1", 4 | "description": "A simple Cli tool for Codeforces", 5 | "main": "build/index.js", 6 | "bin": { 7 | "cf": "build/bin/cli/cf.js" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --compilers js:babel-register \"tests/**/*.js\"", 11 | "coverage": "./node_modules/.bin/nyc report --reporter=text-lcov | coveralls", 12 | "cov-html": "./node_modules/.bin/nyc report --reporter=html", 13 | "lint": "./node_modules/.bin/eslint -c .eslintrc src/**", 14 | "single": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --compilers js:babel-register \"tests/crawler/test_ratings.js\" & npm run cov-html", 15 | "snyk-protect": "snyk protect", 16 | "prepublish": "npm run snyk-protect" 17 | }, 18 | "nyc": { 19 | "include": [ 20 | "src/lib/crawler/*.js", 21 | "src/lib/api/*.js", 22 | "src/lib/utils/*.js", 23 | "tests/**/*.js" 24 | ] 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/ahmed-dinar/codeforces-cli.git" 29 | }, 30 | "keywords": [ 31 | "Codeforces", 32 | "Cli", 33 | "Codeforces Cli", 34 | "Codeforces API", 35 | "Codeforces Submit", 36 | "Codeforces Tool" 37 | ], 38 | "author": "Ahmed Dinar ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/ahmed-dinar/codeforces-cli/issues" 42 | }, 43 | "contributors": [], 44 | "homepage": "https://github.com/ahmed-dinar/codeforces-cli", 45 | "devDependencies": { 46 | "babel-cli": "^6.16.0", 47 | "babel-plugin-add-module-exports": "^0.2.1", 48 | "babel-preset-es2015": "^6.16.0", 49 | "babel-register": "^6.16.3", 50 | "bluebird": "^3.4.6", 51 | "chai": "^3.5.0", 52 | "coveralls": "^2.11.14", 53 | "debug": "^2.2.0", 54 | "eslint": "^3.8.1", 55 | "gulp": "^3.9.1", 56 | "gulp-babel": "^6.1.2", 57 | "mocha": "^3.1.0", 58 | "mocha-lcov-reporter": "^1.2.0", 59 | "nyc": "^8.3.0", 60 | "sinon": "^1.17.6", 61 | "sinon-chai": "^2.8.0" 62 | }, 63 | "dependencies": { 64 | "JSONStream": "^1.2.1", 65 | "async": "^2.0.1", 66 | "blessed": "^0.1.81", 67 | "blessed-contrib": "^3.5.5", 68 | "chalk": "^1.1.3", 69 | "cheerio": "^0.22.0", 70 | "cli-table2": "^0.2.0", 71 | "commander": "^2.9.0", 72 | "crypto-js": "^3.1.7", 73 | "figlet": "^1.2.0", 74 | "has": "^1.0.1", 75 | "inquirer": "^1.2.1", 76 | "jsonfile": "^2.4.0", 77 | "lodash": "^4.16.3", 78 | "mkdirp": "^0.5.1", 79 | "moment": "^2.15.1", 80 | "ora": "^0.3.0", 81 | "qs": "^6.2.1", 82 | "request": "^2.75.0", 83 | "striptags": "^2.1.1", 84 | "snyk": "^1.25.0" 85 | }, 86 | "snyk": true 87 | } 88 | -------------------------------------------------------------------------------- /src/bin/cli/cf.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var program = require('commander'); 5 | 6 | import figlet from 'figlet'; 7 | import has from 'has'; 8 | import { version } from '../../../package.json'; 9 | import * as CF from '../../..'; 10 | import { log, logr } from '../../lib/helpers'; 11 | 12 | var DEFAULT_DELAY = 5000; 13 | var DEFAULT_ASYNC_LIMIT = 10; 14 | 15 | 16 | program 17 | .version(version) 18 | .usage('[options] [command]'); 19 | 20 | program.on('--help', () => { 21 | log(' All options:'); 22 | log(' -r, --remember save/update handle'); 23 | log(' -c, --count total items to fetch and show'); 24 | log(' -w, --watch watch submission status live'); 25 | log(' -p, --problem also download problem statement'); 26 | log(' -u, --user codeforces user handle'); 27 | log(' -l, --language programming language id of the solution'); 28 | log(' -d, --directory directory to save solutions'); 29 | log(' --logout delete saved password'); 30 | log(' --gym gym problem submit'); 31 | log(' --no-chart disable showing chart of user rating'); 32 | log(' --org show Organization of users'); 33 | log(' --unofficial unofficial standings'); 34 | log(' --handles comma separated codeforces handles'); 35 | log(' --offset offset of the items to fetch from'); 36 | log(' --limit maximum number of simultaneous downloads'); 37 | log(' --country country name for rating'); 38 | log(' --delay refreshing delay of live submission status [in millisecond]'); 39 | log(' --contest specific contest submissions'); 40 | }); 41 | 42 | program 43 | .command('runs') 44 | .option('-r, --remember', 'save/update handle') 45 | .option('-c, --count ', 'total submission status to display') 46 | .option('-w, --watch', 'watch submission status live') 47 | .option('--delay ', 'refreshing delay of live submission status [in millisecond]') 48 | .option('--contest ', 'specific contest submissions') 49 | .description('user submission status') 50 | .action( (prg) => { 51 | 52 | let options = { 53 | remember: has(prg,'remember'), 54 | watch: has(prg,'watch'), 55 | contest: has(prg,'contest'), 56 | count: has(prg,'count') 57 | ? parseInt(prg.count,10) 58 | : 1, 59 | delay: has(prg,'delay') 60 | ? parseInt(prg.delay,10) : DEFAULT_DELAY, 61 | contestId: has(prg,'contest') 62 | ? prg.contest 63 | : 0 64 | }; 65 | 66 | CF.submission(options); 67 | }); 68 | 69 | 70 | program 71 | .command('submit ') 72 | .option('-l, --language ', 'programming language id of the solution') 73 | .option('-r, --remember', 'save/update password for future login') 74 | .option('--logout', 'delete saved password') 75 | .option('-w, --watch', 'watch submission status live') 76 | .option('-c, --count ', 'total live submission status to display [max 10]') 77 | .option('--delay ', 'refreshing delay of live submission status [in millisecond]') 78 | .option('--gym', 'gym problem submit') 79 | .description('submit solution') 80 | .action( (cid,pnum,codeFile,prg) => { 81 | 82 | let remember = has(prg,'remember'); 83 | let logout = has(prg,'logout'); 84 | 85 | if( remember && logout ){ 86 | log(''); 87 | logr(' Error: Please select either remember or logout'); 88 | return; 89 | } 90 | 91 | let total = has(prg,'count') 92 | ? parseInt(prg.count,10) 93 | : 1; 94 | 95 | let delay = has(prg,'delay') 96 | ? parseInt(prg.delay,10) 97 | : DEFAULT_DELAY; 98 | 99 | let options = { 100 | contestId: cid, 101 | problemIndex: pnum, 102 | codeFile: codeFile, 103 | remember: remember, 104 | logout: logout, 105 | totalRuns: total, 106 | delay: delay, 107 | watch: has(prg,'watch'), 108 | gym: has(prg,'gym') 109 | }; 110 | 111 | if( has(prg,'language') ){ 112 | options['language'] = prg.language; 113 | } 114 | 115 | new CF.Submit(options).submit(); 116 | }); 117 | 118 | 119 | program 120 | .command('stat ') 121 | .description('user tags status') 122 | .action( (handle) => { 123 | CF.usertags({ handle: handle }); 124 | }); 125 | 126 | 127 | program 128 | .command('rating') 129 | .option('-u, --user ', 'codeforces user handle') 130 | .option('--country ', 'country name for rating') 131 | .option('--no-chart', 'disable showing chart of user rating') 132 | .option('--org', 'show Organization of users') 133 | .description('user ratings') 134 | .action( (prg) => { 135 | 136 | if( has(prg,'user') ){ 137 | let noChart = prg.parent.rawArgs.indexOf('--no-chart') !== -1; 138 | return new CF.Userrating(prg.user,noChart).getRating(); 139 | } 140 | 141 | if( has(prg,'country') ){ 142 | return new CF.Ratings({ 143 | country: prg.country, 144 | org: has(prg,'org') 145 | }).show(); 146 | } 147 | 148 | program.outputHelp(); 149 | }); 150 | 151 | 152 | program 153 | .command('tags') 154 | .description('all tags distribution') 155 | .action( () => { 156 | CF.tags(); 157 | }); 158 | 159 | program 160 | .command('lang') 161 | .description('all supported languages and typeId') 162 | .action( () => { 163 | CF.cfdefault.langs(); 164 | }); 165 | 166 | program 167 | .command('ext') 168 | .description('all supported extensions and language name') 169 | .action( () => { 170 | CF.cfdefault.exts(); 171 | }); 172 | 173 | 174 | program 175 | .command('country') 176 | .description('all supported country') 177 | .action( () => { 178 | CF.cfdefault.countrs(); 179 | }); 180 | 181 | /* 182 | program 183 | .command('contests') 184 | .option('--running', 'for running contests') 185 | .option('--future', 'for upcoming contests') 186 | .description('contest lists') 187 | .action( (prg) => { 188 | 189 | log('Not available yet.'); 190 | 191 | if( has(prg,'running') ){ 192 | log('Also running'); 193 | }else if( has(prg,'future') ){ 194 | log('Also future'); 195 | } 196 | 197 | });*/ 198 | 199 | 200 | program 201 | .command('info ') 202 | .description('user info') 203 | .action( (handles) => { 204 | CF.userinfo(handles); 205 | }); 206 | 207 | 208 | program 209 | .command('solutions ') 210 | .option('-d, --directory ','directory to save solutions') 211 | .option('-p, --problem','also download problem statement') 212 | .option('--limit ','maximum number of simultaneous downloads') 213 | .description('user solution download') 214 | .action( (handle,prg) => { 215 | 216 | let options = { 217 | handle: handle, 218 | withProblem: has(prg,'problem'), 219 | dir: has(prg,'directory') 220 | ? prg.directory 221 | : '.', 222 | limit: has(prg,'limit') 223 | ? parseInt(prg.limit,10) 224 | : DEFAULT_ASYNC_LIMIT 225 | }; 226 | 227 | new CF.Sourcecode(options).download(); 228 | }); 229 | 230 | 231 | program 232 | .command('standings ') 233 | .description('contest standings') 234 | .option('--handles ', 'comma separated codeforces handles') 235 | .option('--country ', 'country name for standings') 236 | .option('-c, --count ', 'total standings to display') 237 | .option('--offset ', 'standings offset') 238 | .option('--unofficial', 'unofficial standings') 239 | .action( (contestId,prg) => { 240 | 241 | if( has(prg,'country') ){ 242 | return new CF.Countrystandings({ 243 | contestId: parseInt(contestId,10), 244 | country: prg.country, 245 | total: has(prg,'count') 246 | ? prg.count 247 | : 50 248 | }).show(); 249 | } 250 | 251 | let options = { 252 | contestId: parseInt(contestId,10), 253 | unofficial: has(prg,'unofficial'), 254 | count: has(prg,'count') 255 | ? parseInt(prg.count,10) 256 | : 200, 257 | from: has(prg,'offset') 258 | ? parseInt(prg.offset,10) 259 | : 1 260 | }; 261 | 262 | if( has(prg,'handles') ){ 263 | options.handles = prg.handles; 264 | } 265 | 266 | CF.standings(options); 267 | }); 268 | 269 | 270 | program.parse(process.argv); 271 | 272 | /* 273 | if( !program.args.length && has(program,'help') ){ 274 | figlet('CF CLI', function(err, data) { 275 | log(''); 276 | log(''); 277 | if(!err){ 278 | log(data); 279 | log(''); 280 | } 281 | program.outputHelp(); 282 | }); 283 | }*/ 284 | 285 | 286 | // 287 | // Showing custom errors (NOT WORKING, need to modify) 288 | // https://github.com/tj/programer.js/issues/57 289 | // 290 | if (!program.args.length) { 291 | figlet('Codeforces CLI', (err, data) => { 292 | log(''); 293 | log(''); 294 | if(!err){ 295 | log(data); 296 | log(''); 297 | } 298 | program.outputHelp(); 299 | }); 300 | } /*else { 301 | 302 | //warn aboud invalid programs 303 | var validprograms = program.commands.map(function(cmd){ 304 | return cmd._name; 305 | }); 306 | 307 | var invalidprograms = program.args.map(function(cmd){ 308 | return cmd._name; 309 | }); 310 | 311 | 312 | if (invalidprograms.length) { 313 | log(''); 314 | log(` [ERROR] - Invalid command: ${invalidprograms.join(', ')}. run "cf --help" for a list of available commands.`); 315 | log(''); 316 | } 317 | } 318 | */ 319 | 320 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import standings from './lib/api/standings'; 4 | import tags from './lib/api/tags'; 5 | import userinfo from './lib/api/userinfo'; 6 | import Userrating from './lib/api/Userrating'; 7 | import usertags from './lib/api/usertags'; 8 | import Countrystandings from './lib/crawler/Countrystandings'; 9 | import Ratings from './lib/crawler/Ratings'; 10 | import Sourcecode from './lib/crawler/Sourcecode'; 11 | import Submit from './lib/crawler/Submit'; 12 | import submission from './lib/api/submission'; 13 | import cfdefault from './lib/utils/cfdefault'; 14 | 15 | 16 | export default { 17 | standings: standings, 18 | tags: tags, 19 | userinfo: userinfo, 20 | Userrating: Userrating, 21 | usertags: usertags, 22 | Countrystandings: Countrystandings, 23 | Ratings: Ratings, 24 | Sourcecode: Sourcecode, 25 | Submit: Submit, 26 | submission: submission, 27 | cfdefault: cfdefault 28 | }; 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/api/Userrating.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import request from 'request'; 4 | import ora from 'ora'; 5 | import Table from 'cli-table2'; 6 | import chalk from 'chalk'; 7 | import qs from 'qs'; 8 | import has from 'has'; 9 | import forEach from 'lodash/forEach'; 10 | import * as contrib from 'blessed-contrib'; 11 | import blessed from 'blessed'; 12 | import striptags from 'striptags'; 13 | import { log, logr } from '../helpers'; 14 | 15 | var spinner = ora({ spinner: 'line' }); 16 | var CB = chalk.bold.cyan; 17 | 18 | 19 | export default class Userrating { 20 | 21 | /** 22 | * Get and print user rating of all contests 23 | * @param {String} handle - user codefroces handle 24 | * @param {boolean} noChart - if true display table insted of chart 25 | */ 26 | constructor(handle = null, noChart = false){ 27 | 28 | if (handle === null || typeof handle !== 'string') { 29 | throw new Error('handle should be string and should not be empty or null'); 30 | } 31 | 32 | this.handle = handle; 33 | this.noChart = noChart; 34 | } 35 | 36 | 37 | getRating() { 38 | 39 | let self = this; 40 | 41 | let qsf = qs.stringify({ handle: self.handle }); 42 | let reqOptions = { 43 | uri: `http://codeforces.com/api/user.rating?${qsf}`, 44 | json: true 45 | }; 46 | 47 | spinner.text = 'Fetching rating...'; 48 | spinner.start(); 49 | 50 | request.get(reqOptions, (err, response, body) => { 51 | 52 | if (err) { 53 | spinner.fail(); 54 | return logr('Failed [Request]'); 55 | } 56 | 57 | let {statusCode} = response; 58 | if (statusCode !== 200) { 59 | spinner.fail(); 60 | return logr( has(body,'comment') ? body.comment : `HTTP Failed with status ${statusCode}`); 61 | } 62 | 63 | let contentType = response.headers['content-type']; 64 | if (contentType.indexOf('application/json') === -1) { 65 | spinner.fail(); 66 | return logr('Failed.Not valid data.'); 67 | } 68 | 69 | if (body.status !== 'OK') { 70 | spinner.fail(); 71 | return logr(body.comment); 72 | } 73 | spinner.succeed(); 74 | 75 | if ( self.noChart ) { 76 | let table = new Table({ 77 | head: [CB('Contest'), CB('Rank'), CB('Rating change'), CB('New rating')] 78 | }); 79 | 80 | forEach(body.result, (data) => { 81 | table.push([ 82 | striptags(data.contestName.toString()), 83 | data.rank, 84 | (parseInt(data.newRating, 10) - parseInt(data.oldRating, 10)).toString(), 85 | data.newRating 86 | ]); 87 | }); 88 | 89 | return log(table.toString()); 90 | } 91 | 92 | let axisX = []; 93 | let axisY = []; 94 | forEach(body.result, (data) => { 95 | axisY.push(data.newRating); 96 | axisX.push(data.newRating.toString()); 97 | }); 98 | 99 | this.showLineChart(axisX, axisY); 100 | }); 101 | } 102 | 103 | 104 | /** 105 | * Show user rating chart 106 | * @param {Array} axisX - x axis data (constest ratings) 107 | * @param {Array} axisY - y axis data (constest ratings) 108 | */ 109 | showLineChart(axisX, axisY) /* istanbul ignore next */ { 110 | 111 | let screen = blessed.screen(); 112 | 113 | let chartLine = contrib.line({ 114 | style: { 115 | line: 'white', 116 | text: 'green', 117 | baseline: 'black' 118 | }, 119 | width: '100%', 120 | height: '80%', 121 | top: 3, 122 | showLegend: false, 123 | wholeNumbersOnly: false, 124 | label: '' 125 | }); 126 | 127 | 128 | let chartData = { 129 | x: axisX, 130 | y: axisY 131 | }; 132 | 133 | screen.append(chartLine); 134 | chartLine.setData([chartData]); 135 | 136 | // 137 | // Exit when press esc, q or ctrl+c 138 | // 139 | screen.key(['escape', 'q', 'C-c'], function (ch, key) { 140 | return process.exit(0); 141 | }); 142 | 143 | screen.render(); 144 | } 145 | } 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/lib/api/standings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import qs from 'qs'; 4 | import request from 'request'; 5 | import debug from 'debug'; 6 | import JSONStream from 'JSONStream'; 7 | import Table from 'cli-table2'; 8 | import chalk from 'chalk'; 9 | import { forEach, split , isArray , join, map } from 'lodash'; 10 | import ora from 'ora'; 11 | import { duration } from 'moment'; 12 | import { log, logr } from '../helpers'; 13 | 14 | var debugs = debug('CF:standings'); 15 | var spinner = ora({ spinner: 'line' }); 16 | var GB = chalk.green.bold; 17 | var CB = chalk.cyan.bold; 18 | var RB = chalk.red.bold; 19 | 20 | 21 | /** 22 | * Main standings function 23 | * @param {Number} contestId id of the contest (*required) 24 | * @param {Number} count - total standings to fetch 25 | * @param {Boolean} unofficial - if true, also fetch unofficial standings 26 | * @param {Number} from - offset of rank to fetch from 27 | * @param {Array/String} handles - comma separated handles or array of handles 28 | */ 29 | export default ({ contestId = null, count = 200, unofficial = false, from = 1, handles = [] } = {}) => { 30 | 31 | if( contestId === null || !Number.isInteger(contestId) ){ 32 | throw new Error('contest id should not be empty or null and should be integer.'); 33 | } 34 | 35 | let options = { contestId, count, unofficial, from, handles }; 36 | let url = generateUrl(options); 37 | 38 | let apiFailed = false; 39 | let apiMsg = null; 40 | let responseCode = 404; 41 | let contentType = ''; 42 | let table = new Table(); 43 | let contestInfo = {}; 44 | let problemSet = []; 45 | 46 | let reqOptions = { 47 | uri: url, 48 | json: true, 49 | timeout: 30000 50 | }; 51 | 52 | debugs('Fetching standings..'); 53 | spinner.text = 'Fetching standings..'; 54 | spinner.start(); 55 | 56 | /* istanbul ignore next */ 57 | let reqStream = request.get(reqOptions); 58 | let jsonStream = reqStream.pipe( JSONStream.parse('result.rows.*') ); 59 | 60 | reqStream.on('error', (err) => { 61 | debugs('Failed: Request error'); 62 | logr(err); 63 | }); 64 | 65 | reqStream.on('complete', () => { 66 | debugs('parsing completed'); 67 | 68 | if( responseCode !== 200 ){ 69 | spinner.fail(); 70 | return logr(apiMsg || `HTTP Failed with status ${responseCode}`); 71 | } 72 | 73 | if( contentType.indexOf('application/json;') === -1 ){ 74 | spinner.fail(); 75 | return logr('Failed.Invalid data.'); 76 | } 77 | 78 | if( apiFailed ){ 79 | spinner.fail(); 80 | return logr(apiMsg || 'Unknown error.[Report?]'); 81 | } 82 | spinner.succeed(); 83 | 84 | let head = [ GB('Rank'), GB('Who'), GB('Points'), GB('Hacks') ]; 85 | forEach(problemSet, prb => { 86 | head.push( GB(prb.index) ); 87 | }); 88 | table.options.head = head; 89 | 90 | log(''); 91 | log( CB(` Title: ${contestInfo.name}`) ); 92 | log( CB(` Type : ${contestInfo.type}`) ); 93 | log( CB(` Phase: ${contestInfo.phase}`) ); 94 | log(table.toString()); 95 | }); 96 | 97 | reqStream.on('response', (response) => { 98 | responseCode = response.statusCode; 99 | contentType = response.headers['content-type']; 100 | 101 | debugs(`HTTP Code: ${responseCode}`); 102 | debugs(`Content-Type: ${contentType}`); 103 | }); 104 | 105 | jsonStream.on('header', (data) => { 106 | debugs(`API Status: ${data.status}`); 107 | 108 | if( data.status !== 'OK' ){ 109 | apiFailed = true; 110 | apiMsg = data.comment; 111 | return; 112 | } 113 | contestInfo = data.contest; 114 | problemSet = data.problems; 115 | }); 116 | 117 | jsonStream.on('data', (data) => { 118 | let hacks = ''; 119 | 120 | if( data.successfulHackCount > 0 ){ 121 | hacks = `+${data.successfulHackCount.toString()}`; 122 | } 123 | 124 | if( data.unsuccessfulHackCount > 0 ){ 125 | hacks = `${hacks} : -${data.unsuccessfulHackCount.toString()}`; // +x : -y 126 | } 127 | 128 | /**********TO-DO*************/ 129 | // handle Multiple members (team) 130 | // 131 | let chunk = [data.rank.toString(), CB(data.party.members[0].handle), data.points.toString(), hacks]; 132 | 133 | let results = map(data.problemResults, (result) => { 134 | 135 | if( result.points === 0 && result.rejectedAttemptCount > 0 ){ 136 | return RB(`-${result.rejectedAttemptCount.toString()}`); 137 | } 138 | else if( result.points === 0 && result.rejectedAttemptCount === 0 ){ 139 | return ''; 140 | } 141 | 142 | let subSecond = duration(result.bestSubmissionTimeSeconds, 'seconds'); 143 | let h = parseInt(subSecond.hours(),10); 144 | let s = parseInt(subSecond.minutes(),10); 145 | let subTime = `${Math.floor(h/10)}${h%10}:${Math.floor(s/10)}${s%10}`; 146 | 147 | return ` ${result.points.toString()}\n${subTime}`; 148 | }); 149 | 150 | table.push(chunk.concat(results)); 151 | }); 152 | }; 153 | 154 | 155 | /** 156 | * Generate API url from given parameters 157 | * @param {Object} options 158 | * @returns {string} - generated url 159 | */ 160 | function generateUrl(options) { 161 | 162 | let param = { 163 | contestId: options.contestId, 164 | count: options.count, 165 | from: options.from, 166 | showUnofficial: options.unofficial, 167 | }; 168 | 169 | let { handles } = options; 170 | if( isArray(handles) && handles.length ){ 171 | param['handles'] = join(handles,';'); 172 | } 173 | else if( typeof handles === 'string' ){ 174 | handles = split(handles,','); 175 | param['handles'] = join(handles,';'); 176 | } 177 | //else invalid handles..may be throw? currently ignore 178 | 179 | let sp = qs.stringify(param,{ encode: false }); 180 | 181 | return `http://codeforces.com/api/contest.standings?${sp}`; 182 | } 183 | -------------------------------------------------------------------------------- /src/lib/api/submission.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import has from 'has'; 5 | import forEach from 'lodash/forEach'; 6 | import jsonfile from 'jsonfile'; 7 | import inquirer from 'inquirer'; 8 | import qs from 'qs'; 9 | import request from 'request'; 10 | import debug from 'debug'; 11 | import chalk from 'chalk'; 12 | import Table from 'cli-table2'; 13 | import ora from 'ora'; 14 | import { waterfall, whilst } from 'async'; 15 | import verdicts from '../verdicts'; 16 | import { log, logr, getHomeDir, validateEmpty, clear } from '../helpers'; 17 | 18 | 19 | var debugs = debug('CF:submission'); 20 | var spinner = ora({ spinner: 'line' }); 21 | var GN = chalk.green; 22 | var GB = chalk.bold.green; 23 | var RB = chalk.bold.red; 24 | 25 | var TIME_OUT = 30000; //30 seconds 26 | var STATUS_DELAY = 5000; //5 seconds 27 | 28 | 29 | /** 30 | * @param {Number} count - total submisison to fetch 31 | * @param {Boolean} remember - if true, save handle in config file 32 | * @param {Boolean} watch - if true, fetch submission until testing done 33 | * @param {Boolean} contest - if true then a contestId must exists and fetch only contest submissions 34 | * @param {Number} contestId - contest id of submission 35 | * @param {Number} delay - live watch refresh delay [in milliseconds] 36 | * @param {Function} callback 37 | */ 38 | export default ({ count = 1, remember = false, watch = false, contest = false, contestId = null, delay = STATUS_DELAY, callback = null } = {}) => { 39 | 40 | let options = { count, watch, remember, contest, contestId, delay }; 41 | 42 | options.config = path.resolve(`${getHomeDir()}/.cfconfig`); 43 | 44 | /* istanbul ignore else */ 45 | if( options.delay >= 2000 ){ 46 | STATUS_DELAY = options.delay; 47 | } 48 | 49 | waterfall([ 50 | (next) => { 51 | readConfig(options, next); 52 | }, 53 | getSubmission 54 | ],(err) => { 55 | 56 | if( typeof callback === 'function' ){ 57 | spinner.stop(); 58 | return callback(err); 59 | } 60 | 61 | /* istanbul ignore else */ 62 | if(err){ 63 | spinner.fail(); 64 | logr(err); 65 | } 66 | }); 67 | }; 68 | 69 | 70 | /** 71 | * Reading config file and search saved credentials 72 | * @param {Object} options 73 | * @param {Function} next 74 | */ 75 | function readConfig(options, next) { 76 | 77 | spinner.text = 'Reading config file...'; 78 | spinner.start(); 79 | 80 | jsonfile.readFile(options.config, (err, obj) => { 81 | 82 | let askHandle = false; 83 | if( err ){ 84 | 85 | if( err.code === 'EPERM' ){ 86 | throw new Error(`Permission denied.Can't read config file '${options.config}'`); 87 | } 88 | 89 | if( err.code !== 'ENOENT' ){ 90 | return next(err); 91 | } 92 | 93 | debugs('Config file not found'); 94 | askHandle = true; 95 | } 96 | spinner.stop(); 97 | 98 | if( askHandle || !has(obj,'user') ){ 99 | 100 | let credentials = [{ 101 | name: 'handle', 102 | message: 'handle: ', 103 | validate: validateEmpty 104 | }]; 105 | 106 | return inquirer.prompt(credentials).then( (answer) => { 107 | options.handle = answer.handle; 108 | jsonfile.writeFileSync(options.config, { user: options.handle });//save handle 109 | 110 | return next(null, options); 111 | }); 112 | } 113 | 114 | spinner.text = `Saved handle found '${obj.user}'`; 115 | spinner.succeed(); 116 | 117 | options.handle = obj.user; 118 | return next(null, options); 119 | }); 120 | } 121 | 122 | 123 | /** 124 | * Get submission status and exit 125 | * @param {Object} options 126 | * @param {Function} next 127 | */ 128 | function getSubmission(options, next) { 129 | 130 | //go to live submssion status 131 | if( options.watch ){ 132 | return watchRun(options,next); 133 | } 134 | 135 | let url = generateUrl(options); 136 | debugs(`URL = ${url}`); 137 | 138 | let reqOptions = { 139 | uri: url, 140 | json: true, 141 | timeout: TIME_OUT 142 | }; 143 | 144 | spinner.text = 'Fetching submissions..'; 145 | spinner.start(); 146 | 147 | request 148 | .get(reqOptions, (error, response, body) => { 149 | 150 | if(error){ 151 | return next(error); 152 | } 153 | 154 | let { statusCode } = response; 155 | if( statusCode!==200 ){ 156 | return next( has(body,'comment') ? body.comment : `HTTP failed with status ${statusCode}`); 157 | } 158 | 159 | if( body.status !== 'OK' ){ 160 | return next(body.comment); 161 | } 162 | 163 | spinner.succeed(); 164 | generateTable(body.result); 165 | 166 | return next(); 167 | }); 168 | } 169 | 170 | 171 | /** 172 | * Getting submission status until testing done 173 | * @param {Object} options 174 | * @param {Function} next 175 | */ 176 | function watchRun(options, next) { 177 | 178 | let url = generateUrl(options); 179 | debugs(url); 180 | 181 | let reqOptions = { 182 | uri: url, 183 | json: true, 184 | timeout: TIME_OUT 185 | }; 186 | 187 | var keepWatching = true; 188 | whilst( 189 | () => { 190 | return keepWatching; 191 | }, 192 | (callback) => { 193 | 194 | spinner.text = 'Refreshing..'; 195 | spinner.start(); 196 | 197 | request 198 | .get(reqOptions, (error, response, body) => { 199 | 200 | if(error){ 201 | return callback(error); 202 | } 203 | 204 | let { statusCode } = response; 205 | if( statusCode !== 200 ){ 206 | return next( has(body,'comment') ? body.comment : `HTTP failed with status ${statusCode}`); 207 | } 208 | 209 | if( body.status !== 'OK' ){ 210 | return callback(body.comment); 211 | } 212 | 213 | spinner.succeed(); 214 | keepWatching = generateTable(body.result, true); 215 | 216 | // 217 | // Still testing, Wait x seconds and get status again 218 | // 219 | if( keepWatching ) { 220 | return setTimeout(() => { 221 | callback(); 222 | }, STATUS_DELAY); 223 | } 224 | 225 | return callback(); 226 | }); 227 | }, 228 | next 229 | ); 230 | } 231 | 232 | 233 | /** 234 | * Generate and print table 235 | * @param {Object} runs - submission object 236 | * @param {boolean} isWatch 237 | * @returns {boolean} - if still running return true for watch 238 | */ 239 | function generateTable(runs, isWatch = false){ 240 | 241 | let table = new Table({ 242 | head: [ GN('Id'), GN('Problem') , GN('Lang'), GN('Verdict'), GN('Time'), GN('Memory') ] 243 | }); 244 | 245 | let done = true; 246 | let who = ''; 247 | 248 | forEach(runs, (run) => { 249 | 250 | let { id, contestId, problem, programmingLanguage,verdict, passedTestCount, timeConsumedMillis, memoryConsumedBytes, author } = run; 251 | let memory = parseInt(memoryConsumedBytes,10) / 1000; 252 | let passed = parseInt(passedTestCount,10); 253 | who = author.members[0].handle; 254 | 255 | if( verdict === undefined || typeof verdict != 'string' ){ 256 | done = false; 257 | verdict = chalk.white.bold('In queue'); 258 | } 259 | else{ 260 | switch (verdict){ 261 | case 'TESTING': 262 | done = false; 263 | verdict = chalk.white.bold(verdicts[verdict]); 264 | break; 265 | case 'OK': 266 | verdict = GB(verdicts[verdict]); 267 | break; 268 | case 'RUNTIME_ERROR': 269 | case 'WRONG_ANSWER': 270 | case 'PRESENTATION_ERROR': 271 | case 'TIME_LIMIT_EXCEEDED': 272 | case 'MEMORY_LIMIT_EXCEEDED': 273 | case 'IDLENESS_LIMIT_EXCEEDED': 274 | verdict = RB(`${verdicts[verdict]} on test ${passed+1}`); 275 | break; 276 | default: 277 | verdict = RB(verdicts[verdict]); 278 | } 279 | } 280 | 281 | table.push([ 282 | id, 283 | `${contestId}${problem.index} - ${problem.name}`, 284 | programmingLanguage, 285 | verdict, 286 | `${timeConsumedMillis} MS`, 287 | `${memory} KB` 288 | ]); 289 | }); 290 | 291 | // in live watching mode, clear console in every refresh 292 | if( isWatch ){ 293 | clear(); 294 | } 295 | 296 | log(''); 297 | log(GB(`User: ${who}`)); 298 | log(table.toString()); 299 | 300 | return !done; 301 | } 302 | 303 | 304 | /** 305 | * Generate submission status url 306 | * @param options 307 | * @returns {string} 308 | */ 309 | function generateUrl(options) { 310 | 311 | if( options.contest ){ 312 | let params = qs.stringify({ 313 | handle: options.handle, 314 | from: 1, 315 | count: options.count, 316 | contestId: options.contestId 317 | }, { encode: false }); 318 | return `http://codeforces.com/api/contest.status?${params}`; 319 | } 320 | 321 | let params = qs.stringify({ 322 | handle: options.handle, 323 | from: 1, 324 | count: options.count 325 | }, { encode: false }); 326 | 327 | return `http://codeforces.com/api/user.status?${params}`; 328 | } -------------------------------------------------------------------------------- /src/lib/api/tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import JSONStream from 'JSONStream'; 4 | import debug from 'debug'; 5 | import request from 'request'; 6 | import ora from 'ora'; 7 | import has from 'has'; 8 | import _ from 'lodash'; 9 | import Table from 'cli-table2'; 10 | import chalk from 'chalk'; 11 | import { log, logr } from '../helpers'; 12 | 13 | var debugs = debug('CF:tags'); 14 | var spinner = ora({ spinner: 'line' }); 15 | 16 | /** 17 | * Get all tags and quantity 18 | * @param callback - if callback given, return tags, otherwise print 19 | */ 20 | export default (callback) => { 21 | 22 | let reqOptions = { 23 | uri: 'http://codeforces.com/api/problemset.problems', 24 | json: true 25 | }; 26 | 27 | let apiMsg = null; 28 | let responseCode = 404; 29 | let contentType = ''; 30 | let allTags = {}; 31 | let isCallback = typeof callback === 'function'; 32 | 33 | spinner.text = 'Fetching all tags...'; 34 | spinner.start(); 35 | 36 | let reqStream = request.get(reqOptions); 37 | let jsonStream = reqStream.pipe( JSONStream.parse('result.problems.*') ); 38 | 39 | reqStream.on('error', (err) => { 40 | debugs('Failed: Request error'); 41 | debugs(err); 42 | 43 | return isCallback ? callback(err) : logr('Request connection Failed.'); 44 | }); 45 | 46 | 47 | reqStream.on('complete', () => { 48 | debugs('parsing completed'); 49 | 50 | if( responseCode !== 200 ){ 51 | spinner.fail(); 52 | apiMsg = apiMsg || `HTTP Failed with status: ${responseCode}`; 53 | return isCallback ? callback(apiMsg) : logr(apiMsg); 54 | } 55 | 56 | // Content not json, request failed 57 | if( contentType.indexOf('application/json;') === -1 ){ 58 | spinner.fail(); 59 | apiMsg = 'Failed.Not valid data.'; 60 | return isCallback ? callback(apiMsg) : logr(apiMsg); 61 | } 62 | 63 | // API rejects the request 64 | if( apiMsg ){ 65 | spinner.fail(); 66 | return isCallback ? callback(apiMsg) : logr(apiMsg); 67 | } 68 | spinner.succeed(); 69 | 70 | if(isCallback){ 71 | return callback(null,allTags); 72 | } 73 | 74 | // Sort tags by problem count, need to update if better way 75 | allTags = _ 76 | .chain(allTags) 77 | .map((value, key) => { 78 | return {key, value}; 79 | }) 80 | .orderBy('value') 81 | .reverse() 82 | .keyBy('key') 83 | .mapValues('value') 84 | .value(); 85 | 86 | let table = new Table({ 87 | head: [ chalk.green('TAG'), chalk.green('Total Problem')] 88 | }); 89 | 90 | _.forEach(allTags, (value, key) => { 91 | table.push([key, value]); 92 | }); 93 | 94 | log(''); 95 | log(chalk.bold.green(` Total tag: ${table.length}`)); 96 | log(table.toString()); 97 | }); 98 | 99 | 100 | reqStream.on('response', (response) => { 101 | debugs(`HTTP Code: ${responseCode}`); 102 | debugs(`Content-Type: ${contentType}`); 103 | 104 | responseCode = response.statusCode; 105 | contentType = response.headers['content-type']; 106 | }); 107 | 108 | 109 | jsonStream.on('header', (data) => { 110 | debugs(`API Status: ${data.status}`); 111 | 112 | if( data.status !== 'OK' ){ 113 | apiMsg = data.comment || 'Unknown Error?'; 114 | } 115 | }); 116 | 117 | 118 | jsonStream.on('data', (data) => { 119 | if( has(data,'tags') ) { 120 | _.forEach(data.tags, (tag) => { 121 | if (has(allTags, tag)) { 122 | allTags[tag]++; 123 | } else { 124 | allTags[tag] = 1; 125 | } 126 | }); 127 | } 128 | }); 129 | }; 130 | -------------------------------------------------------------------------------- /src/lib/api/userinfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import request from 'request'; 4 | import debug from 'debug'; 5 | import Table from 'cli-table2'; 6 | import chalk from 'chalk'; 7 | import has from 'has'; 8 | import { isArray, join , split, forEach } from 'lodash'; 9 | import qs from 'qs'; 10 | import ora from 'ora'; 11 | import { log, logr } from '../helpers'; 12 | 13 | var debugs = debug('CF:userinfo'); 14 | var spinner = ora({ spinner: 'line' }); 15 | const GB = chalk.bold.green; 16 | 17 | /** 18 | * @param handles 19 | */ 20 | export default (handles) => { 21 | 22 | let invalidHandle = !isArray(handles) && typeof handles !== 'string'; 23 | if( invalidHandle ){ 24 | throw new Error('handles must be array or string'); 25 | } 26 | 27 | let reqOptions = { 28 | uri: '', 29 | json: true 30 | }; 31 | 32 | let handlesString = handles; 33 | if( isArray(handles) ){ 34 | handlesString = join(handles,';'); 35 | } 36 | else if( handles.indexOf(',') !== -1 ){ 37 | handlesString = split(handles,','); 38 | handlesString = join(handlesString,';'); 39 | } 40 | 41 | let qsf = qs.stringify({ handles: handlesString }, { encode: false }); 42 | reqOptions.uri = `http://codeforces.com/api/user.info?${qsf}`; 43 | 44 | debugs('Fetching user data...'); 45 | spinner.text = 'fetching user info...'; 46 | spinner.start(); 47 | 48 | request.get(reqOptions, (error, response, body) => { 49 | 50 | if(error){ 51 | spinner.fail(); 52 | return logr(error); 53 | } 54 | 55 | let { statusCode } = response; 56 | if( statusCode !== 200 ){ 57 | spinner.fail(); 58 | return logr( has(body,'comment') ? body.comment : `HTTP failed with status ${statusCode}`); 59 | } 60 | 61 | if( body.status !== 'OK' ){ 62 | spinner.fail(); 63 | return logr(body.comment); 64 | } 65 | spinner.succeed(); 66 | 67 | let table = new Table({ 68 | head: [ GB('Name'), GB('Handle'), GB('Rank'), GB('Rating'), GB('Max'), GB('Contrib.'), GB('Country'), GB('Organization') ] 69 | }); 70 | 71 | forEach(body.result, (data) => { 72 | 73 | let name = ''; 74 | if( has(data,'firstName') && has(data,'lastName') ){ 75 | name = `${data.firstName} ${data.lastName}`; 76 | } 77 | 78 | let info = [ 79 | name, 80 | data.handle || '', 81 | data.rank || '0', 82 | data.rating || '', 83 | data.maxRating || '', 84 | data.contribution || '', 85 | data.country || '', 86 | data.organization || '' 87 | ]; 88 | table.push(info); 89 | }); 90 | 91 | log(''); 92 | log(table.toString()); 93 | }); 94 | }; -------------------------------------------------------------------------------- /src/lib/api/usertags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import JSONStream from 'JSONStream'; 4 | import debug from 'debug'; 5 | import request from 'request'; 6 | import has from 'has'; 7 | import forEach from 'lodash/forEach'; 8 | import Table from 'cli-table2'; 9 | import chalk from 'chalk'; 10 | import ora from 'ora'; 11 | import qs from 'qs'; 12 | import waterfall from 'async/waterfall'; 13 | import { log, logr } from '../helpers'; 14 | import tags from './tags'; 15 | 16 | var spinner = ora({ spinner: 'line' }); 17 | var debugs = debug('CF:usertags'); 18 | const CB = chalk.cyan.bold; 19 | const PROGRESS_WIDTH = 20; 20 | 21 | 22 | /** 23 | * First fetch all user tags status and then all available tags , calculate and concat user tags progress 24 | * @param {String} handle - user handle 25 | */ 26 | export default ({ handle = null } = {}) => { 27 | 28 | if( handle === null || typeof handle !== 'string' ){ 29 | throw new Error('handle should be string and should not be empty or null'); 30 | } 31 | 32 | /* istanbul ignore next */ 33 | waterfall([ 34 | (next) => { 35 | getUserTags(handle, next); 36 | }, 37 | (userTags, totalSubmissions, totalAccepted, next) => { 38 | tags((error,allTags) => { 39 | if(error){ 40 | return next(error); 41 | } 42 | printTable(allTags, userTags, totalSubmissions, totalAccepted, handle); 43 | return next(); 44 | }); 45 | } 46 | ], (err, result) => { 47 | if(err){ 48 | logr(err); 49 | } 50 | }); 51 | }; 52 | 53 | 54 | /** 55 | * Fetch tags of accepted problems solved by the user 56 | * @param {String} handle - user handle 57 | * @param callback 58 | */ 59 | /* istanbul ignore next */ 60 | function getUserTags(handle, callback) { 61 | 62 | let reqOptions = { 63 | uri: '', 64 | json: true 65 | }; 66 | 67 | let par = qs.stringify({ handle: handle },{ encode: false }); 68 | reqOptions.uri = `http://codeforces.com/api/user.status?${par}`; 69 | 70 | let apiFailed= false; 71 | let apiMsg = null; 72 | let responseCode = 404; 73 | let contentType = ''; 74 | let totalSubmissions = 0; 75 | let totalAccepted = 0; 76 | let userTags = new Map(); 77 | 78 | spinner.text = 'Fetching user tags...'; 79 | spinner.start(); 80 | 81 | let reqStream = request.get(reqOptions); 82 | let jsonStream = reqStream.pipe( JSONStream.parse('result.*') ); 83 | 84 | reqStream.on('error', (err) => { 85 | debugs('Failed: Request error'); 86 | debugs(err); 87 | 88 | return callback(err); 89 | }); 90 | 91 | reqStream.on('complete', () => { 92 | debugs('parsing completed'); 93 | 94 | if( responseCode !== 200 ){ 95 | spinner.fail(); 96 | return callback(apiMsg || `HTTP failed with status ${responseCode}`); 97 | } 98 | 99 | if( contentType.indexOf('application/json;') === -1 ){ 100 | spinner.fail(); 101 | return callback('Failed.Invalid data.'); 102 | } 103 | 104 | if( apiFailed ){ 105 | spinner.fail(); 106 | return callback(apiMsg); 107 | } 108 | spinner.succeed(); 109 | 110 | return callback(null, userTags, totalSubmissions, totalAccepted); 111 | }); 112 | 113 | 114 | reqStream.on('response', (response) => { 115 | debugs(`HTTP Code: ${responseCode}`); 116 | debugs(`Content-Type: ${contentType}`); 117 | 118 | responseCode = response.statusCode; 119 | contentType = response.headers['content-type']; 120 | }); 121 | 122 | 123 | jsonStream.on('header', (data) => { 124 | debugs(`API Status: ${data.status}`); 125 | 126 | if( data.status !== 'OK' ){ 127 | apiFailed = true; 128 | apiMsg = data.comment; 129 | } 130 | }); 131 | 132 | 133 | jsonStream.on('data', (data) => { 134 | totalSubmissions++; 135 | 136 | if( has(data,'problem') && data.verdict === 'OK' ) { 137 | totalAccepted++; 138 | 139 | let prob = `${data.problem.contestId}${data.problem.index}`; 140 | let problemTags = data.problem.tags; 141 | 142 | forEach(problemTags, (tag) => { 143 | if( userTags.has(tag) ){ 144 | let mySet = userTags.get(tag); 145 | mySet.add(prob); 146 | userTags.set(tag, mySet); 147 | } 148 | else{ 149 | let mySet = new Set(); 150 | mySet.add(prob); 151 | userTags.set(tag,mySet); 152 | } 153 | }); 154 | } 155 | }); 156 | } 157 | 158 | 159 | /** 160 | * print user solved problem's tag status in console table 161 | * @param {Array} allTags - All tags list and count of codeforces 162 | * @param {Map} userTags - user solved problems tags 163 | * @param {Number} totalSubmissions - by the user 164 | * @param {Number} totalAccepted - by the user 165 | * @param {String} handle - the user handle 166 | */ 167 | /* istanbul ignore next */ 168 | function printTable(allTags, userTags, totalSubmissions, totalAccepted, handle) { 169 | 170 | let table = new Table({ 171 | head: [ CB('TAG'), CB('Total Problem'), CB('Solved'), CB('%'), CB('Progress') ] 172 | }); 173 | 174 | let completeColor = `${chalk.styles.bgGreen.open} ${chalk.styles.bgGreen.close}`; 175 | let incompleteColor = `${chalk.styles.bgBlack.open} ${chalk.styles.bgBlack.close}`; 176 | 177 | forEach(allTags, (value,tag) => { 178 | 179 | if( userTags.has(tag) && userTags.get(tag).size > 0 ){ 180 | 181 | let userTotal = userTags.get(tag).size; 182 | let ratio = parseInt(userTotal,10) / parseInt(value,10); 183 | ratio = Math.min(Math.max(ratio, 0), 1); 184 | let percent = ratio * 100; 185 | 186 | let comLenght = Math.round(PROGRESS_WIDTH * ratio); 187 | let incomLenght = PROGRESS_WIDTH - comLenght; 188 | 189 | let bar = ''; 190 | for (let i = 0; i < comLenght; i++) { 191 | bar += completeColor; 192 | } 193 | for (let i = 0; i < incomLenght; i++) { 194 | bar += incompleteColor; 195 | } 196 | 197 | table.push([tag, value, userTotal, `${percent.toFixed(0)}%`, bar]); 198 | } 199 | else{ 200 | let bar = ''; 201 | for(let i=0; i { 212 | let p1 = (parseInt(a[2],10)*100) / parseInt(a[1],10); 213 | let p2 = (parseInt(b[2],10)*100) / parseInt(b[1],10); 214 | return p2 - p1; 215 | }); 216 | 217 | log(''); 218 | log(chalk.bold.green(` User: ${handle}`)); 219 | log(chalk.green(` Total Submissions: ${totalSubmissions}`)); 220 | log(chalk.green(` Total Accepted: ${totalAccepted}`)); 221 | log(table.toString()); 222 | } -------------------------------------------------------------------------------- /src/lib/countries.js: -------------------------------------------------------------------------------- 1 | 2 | export default [ 3 | 'Afghanistan', 4 | 'Akatsuki', 5 | 'Albania', 6 | 'Algeria', 7 | 'Amman', 8 | 'Andorra', 9 | 'Argentina', 10 | 'Armenia', 11 | 'Aruba', 12 | 'Australia', 13 | 'Austria', 14 | 'Azerbaijan', 15 | 'Bahamas', 16 | 'Bahrain', 17 | 'Bangladesh', 18 | 'Beijing', 19 | 'Belarus', 20 | 'Belgium', 21 | 'Benin', 22 | 'Bermuda', 23 | 'BlueCountry', 24 | 'Bolivia', 25 | 'Bosnia and Herzegovina', 26 | 'Brasil', 27 | 'Brazil', 28 | 'Bulgaria', 29 | 'Burkina Faso', 30 | 'Burundi', 31 | 'Cairo', 32 | 'Cameroon', 33 | 'Canada', 34 | 'Candy Kingdom', 35 | 'Catalonia', 36 | 'Cayman Islands', 37 | 'Chechnya', 38 | 'Chile', 39 | 'China', 40 | 'Chinese', 41 | 'Chlenov', 42 | 'Christmas Island', 43 | 'Codeforces', 44 | 'Colombia', 45 | 'CostaRica', 46 | 'Croatia', 47 | 'Cuba', 48 | 'Cuban', 49 | 'Cyprus', 50 | 'Czech Republic', 51 | 'Damascus', 52 | 'Denmark', 53 | 'Djibouti', 54 | 'Dominican Republic', 55 | 'Dsdas', 56 | 'EN', 57 | 'Earth', 58 | 'Ecuador', 59 | 'Egypt', 60 | 'Equestria', 61 | 'Estonia', 62 | 'Ethiopia', 63 | 'Fakeland', 64 | 'Federated States of Micronesia', 65 | 'Finland', 66 | 'France', 67 | 'GDL', 68 | 'Gabon', 69 | 'Galicia', 70 | 'Gensokyo', 71 | 'Georgia', 72 | 'Germany', 73 | 'Gggga', 74 | 'Ghana', 75 | 'Giza', 76 | 'Greece', 77 | 'Haiti', 78 | 'Honduras', 79 | 'HongKong', 80 | 'Hubei', 81 | 'Hungary', 82 | 'Iceland', 83 | 'India', 84 | 'Indonesia', 85 | 'Iran', 86 | 'Iraq', 87 | 'Ireland', 88 | 'Israel', 89 | 'Italy', 90 | 'Japan', 91 | 'Japanistan', 92 | 'Jenabe Haghani Jahamitam', 93 | 'Jordan', 94 | 'Karp-ChantCountry', 95 | 'Kazakhstan', 96 | 'Kenya', 97 | 'Key', 98 | 'Kireston', 99 | 'Korea', 100 | 'Republicof', 101 | 'Korea', 102 | 'DPR', 103 | 'Krakozya', 104 | 'Kyrgyz', 105 | 'Kyrgyzstan', 106 | 'LOLIDONTKNOW', 107 | 'La Réunion', 108 | 'La royaume du KODIA', 109 | 'Land of Ooo', 110 | 'Laos', 111 | 'Latvia', 112 | 'Lebanon', 113 | 'Lithuania', 114 | 'Luxembourg', 115 | 'Macau', 116 | 'Macedonia', 117 | 'Madagascar', 118 | 'Malaysia', 119 | 'Marsplanet', 120 | 'Mauritius', 121 | 'Mete', 122 | 'Mexico', 123 | 'Moldova', 124 | 'Monaco', 125 | 'Mongolia', 126 | 'Montenegro', 127 | 'Morocco', 128 | 'Morroco', 129 | 'Mouood', 130 | 'Tehran', 131 | 'Iran', 132 | 'Mozambique', 133 | 'NSOI', 134 | 'Nada', 135 | 'Nepal', 136 | 'NewZealand', 137 | 'Nicaragua', 138 | 'Nigeria', 139 | 'Nirvana', 140 | 'Norway', 141 | 'Oman', 142 | 'PRC', 143 | 'Pakistan', 144 | 'Palestine', 145 | 'Paraguay', 146 | 'Peru', 147 | 'Philippines', 148 | 'PokemonCenter', 149 | 'Poland', 150 | 'Portugal', 151 | 'PuertoRico', 152 | 'Qatar', 153 | 'RF', 154 | 'Rff', 155 | 'Romania', 156 | 'Russia', 157 | 'Rwanda', 158 | 'Rysaldystan', 159 | 'S.Korea', 160 | 'SaudiArabia', 161 | 'Sdyu', 162 | 'Sea', 163 | 'Serbia', 164 | 'Singapore', 165 | 'SkyDynasty', 166 | 'Slovakia', 167 | 'Slovenia', 168 | 'South Africa', 169 | 'Spain', 170 | 'SriLanka', 171 | 'Sudan', 172 | 'Survivier', 173 | 'Sweden', 174 | 'Switzerland', 175 | 'Syria', 176 | 'Tabulistan', 177 | 'Taiwan', 178 | 'Tajikistan', 179 | 'Tanzania', 180 | 'Tatooine', 181 | 'Tehran', 182 | 'TerranDominion', 183 | 'Thailand', 184 | 'The Netherlands', 185 | 'The PacificOcean', 186 | 'The people\'s republicof CJ', 187 | 'Third Reich', 188 | 'Tierradelaraja', 189 | 'Togo', 190 | 'Tunisia', 191 | 'Tunisie', 192 | 'Turkey', 193 | 'Turkiye', 194 | 'Turkmenistan', 195 | 'UAE', 196 | 'UK', 197 | 'Ukraine', 198 | 'United Kingdom', 199 | 'United States (USA)', 200 | 'Uruguay', 201 | 'Uzbekistan', 202 | 'Valencian Country', 203 | 'Vatican', 204 | 'Venezuela', 205 | 'Vietnam', 206 | 'Westeros', 207 | 'Yakhchiabad', 208 | 'Yanghong', 209 | 'Yemen', 210 | 'Zambia', 211 | 'Zimbabwe' 212 | ]; 213 | -------------------------------------------------------------------------------- /src/lib/crawler/Countrystandings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import request from 'request'; 4 | import debug from 'debug'; 5 | import cheerio from 'cheerio'; 6 | import Table from 'cli-table2'; 7 | import chalk from 'chalk'; 8 | import _ from 'lodash'; 9 | import ora from 'ora'; 10 | import whilst from 'async/whilst'; 11 | import { log, logr, commonHeaders } from '../helpers'; 12 | import countries from '../countries'; 13 | 14 | var debugs = debug('CF:standings:c'); 15 | var spinner = ora({ spinner: 'line' }); 16 | const TIME_OUT = 30000; 17 | const GB = chalk.bold.green; 18 | const CB = chalk.bold.cyan; 19 | const RB = chalk.bold.red; 20 | 21 | 22 | 23 | export default class Countrystandings { 24 | 25 | /* 26 | * @param {Number} contestId 27 | * @param {String} country 28 | * @param {Number} total 29 | */ 30 | constructor({contestId = null, country = null, total = 50, offset = 1} = {}) { 31 | 32 | let isInvalid = contestId === null || country === null || !Number.isInteger(contestId) || typeof country !== 'string'; 33 | if (isInvalid) { 34 | throw new Error('contestId and country should not be null or empty.'); 35 | } 36 | 37 | this.error = null; 38 | if (countries.indexOf(country) === -1) { 39 | this.error = `'${country}' not found in supported country list.Please run 'cf country' to see all supported countries.`; 40 | return; 41 | } 42 | 43 | this.contestId = contestId; 44 | this.country = country; 45 | this.total = total; 46 | this.offset = offset; 47 | } 48 | 49 | 50 | /** 51 | * @param callback 52 | * @returns {*} 53 | */ 54 | show(callback){ 55 | 56 | let self = this; 57 | if( self.error ){ 58 | if( typeof callback === 'function' ){ 59 | return callback(self.error); 60 | } 61 | logr(self.error); 62 | return; 63 | } 64 | 65 | let headers = commonHeaders(); 66 | let reqOptions = { 67 | uri: '', 68 | headers: headers, 69 | timeout: TIME_OUT 70 | }; 71 | 72 | let head; 73 | let table = new Table(); 74 | var totalPage = 2; 75 | var count = 0; 76 | var found = 0; 77 | var page = 1; 78 | let contestName = ''; 79 | 80 | log(''); 81 | 82 | whilst( 83 | () => { 84 | return count < self.total && page <= totalPage; 85 | }, 86 | (next) => { 87 | 88 | let remain = (self.total - found) < 0 89 | ? 0 90 | : (self.total - found); 91 | spinner.text = `Fetching standings - ${found} participants found,${remain} remaining...`; 92 | spinner.start(); 93 | 94 | reqOptions.uri = `http://codeforces.com/contest/${self.contestId}/standings/page/${page}`; 95 | request.get(reqOptions, (err, response, body) => { 96 | 97 | if (err) { 98 | return next(err); 99 | } 100 | 101 | let {statusCode} = response; 102 | if (statusCode !== 200) { 103 | return next(`HTTP failed with status ${statusCode}`); 104 | } 105 | spinner.stop(); 106 | 107 | var $ = cheerio.load(body, {decodeEntities: true}); 108 | let standings = $(`table.standings .standings-flag[title="${self.country}"]`); 109 | found += standings.length; 110 | 111 | _.forEach(standings, (standing) => { 112 | 113 | let allData = $(standing) 114 | .parent() 115 | .parent() 116 | .children(); 117 | let data = [(count + 1).toString()]; 118 | 119 | _.forEach(allData, (info, key) => { 120 | 121 | let val = _.replace($(info).text(), /\s\s+/g, ''); 122 | 123 | /* istanbul ignore next */ 124 | switch (key) { 125 | case 0: 126 | break; 127 | case 1: 128 | val = CB(val); 129 | break; 130 | case 2: 131 | break; 132 | case 3: 133 | if (val.indexOf('+') !== -1) { 134 | val = GB(val); 135 | } 136 | else if (val.indexOf('-') !== -1) { 137 | val = RB(val); 138 | } 139 | else{ 140 | val = chalk.bold.white(val); 141 | } 142 | break; 143 | default: 144 | if (val.indexOf(':') !== -1) { 145 | val = self.splitPenalty(val, val.length - 5); 146 | } 147 | else { 148 | val = RB(val); 149 | } 150 | } 151 | 152 | data.push(val); 153 | }); 154 | count++; 155 | table.push(data); 156 | }); 157 | 158 | if (page === 1) { 159 | 160 | contestName = _.replace($('.contest-name a').text(), /\s\s+/g, ''); 161 | let pg = $('.page-index'); 162 | let indxes = pg.length; 163 | if (indxes) { 164 | pg = pg[indxes - 1]; 165 | totalPage = parseInt($(pg).attr('pageindex'), 10); 166 | debugs(`Total page: ${totalPage}`); 167 | } 168 | 169 | let tabHeads = $('table.standings tr')[0]; 170 | head = _.map( $(tabHeads).children(), (heads) => { 171 | let name = _.replace( $(heads).text() , /\s\s+/g, ''); 172 | if( name === '#' ){ 173 | name = 'Rank'; 174 | } 175 | else if( name.length > 1 && self.hasDigit(name) ){ 176 | name = self.splitPenalty(name, 1); 177 | } 178 | return GB(name); 179 | }); 180 | } 181 | page++; 182 | return next(); 183 | }); 184 | }, 185 | function (err, n) { 186 | 187 | if( typeof callback === 'function' ){ 188 | spinner.stop(); 189 | return callback(err); 190 | } 191 | 192 | if (err) { 193 | spinner.fail(); 194 | logr(err); 195 | return; 196 | } 197 | 198 | if (!table.length) { 199 | log('No standings found.'); 200 | return; 201 | } 202 | 203 | let colWidths = [ null, ...(_.map(head, (hd,key) => { //bad practice? who cares! eslint, is that you?? 204 | return key == 1 ? 30 : null; 205 | }))]; 206 | 207 | table.options.head = [ GB('#'), ...head ]; 208 | table.options.colWidths = colWidths; 209 | table.options.wordWrap = true; 210 | 211 | log(''); 212 | log(CB(`Contest: ${contestName}`)); 213 | log(CB(`Country: ${self.country}`)); 214 | log(table.toString()); 215 | } 216 | ); 217 | } 218 | 219 | 220 | /** 221 | * http://stackoverflow.com/questions/16441770/split-string-in-two-on-given-index-and-return-both-parts 222 | * @param value 223 | * @param index 224 | * @returns {string} 225 | */ 226 | splitPenalty(value, index) { 227 | /* istanbul ignore next */ 228 | return ` ${value.substring(0, index)}\n${value.substring(index)}`; 229 | } 230 | 231 | 232 | /** 233 | * Check if a string contains any digit 234 | * @param val 235 | */ 236 | hasDigit(val) { 237 | return val.match(/\d+/g) != null; 238 | } 239 | } -------------------------------------------------------------------------------- /src/lib/crawler/Ratings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import waterfall from 'async/waterfall'; 4 | import request from 'request'; 5 | import debug from 'debug'; 6 | import cheerio from 'cheerio'; 7 | import Table from 'cli-table2'; 8 | import chalk from 'chalk'; 9 | import { forEach, replace, map } from 'lodash'; 10 | import has from 'has'; 11 | import qs from 'qs'; 12 | import ora from 'ora'; 13 | import { log, logr, commonHeaders } from '../helpers'; 14 | import countries from '../countries'; 15 | 16 | var spinner = ora({ spinner: 'line' }); 17 | var debugs = debug('CF:standings:c'); 18 | const GB = chalk.bold.green; 19 | const CB = chalk.bold.cyan; 20 | const RB = chalk.bold.red; 21 | 22 | 23 | export default class Ratings { 24 | 25 | 26 | /** 27 | * @param country 28 | * @param org 29 | */ 30 | constructor({ country = null, org = false } = {}) { 31 | 32 | if ( country === null || typeof country !== 'string' ) { 33 | throw new Error('country should not be null or empty'); 34 | } 35 | 36 | this.error = null; 37 | if (countries.indexOf(country) === -1) { 38 | this.error = `Invalid country '${country}'.Please run 'cf country' to see supported country list.`; 39 | return; 40 | } 41 | 42 | this.country = country; 43 | this.withOrg = org; 44 | } 45 | 46 | 47 | /******* TO-DO ************************************************ 48 | * Add count and offset (and may be categorize by CF colors?) 49 | ************************************************************** 50 | * @param callback 51 | * @returns {*} 52 | */ 53 | show(callback){ 54 | 55 | let self = this; 56 | let isCallback = typeof callback === 'function'; 57 | if( self.error ){ 58 | return isCallback ? callback(self.error) : logr(self.error); 59 | } 60 | 61 | spinner.text = `fetching top few user ratings of ${self.country}...`; 62 | spinner.start(); 63 | 64 | waterfall([ 65 | (next) => { 66 | 67 | let reqOptions = { 68 | uri: `http://codeforces.com/ratings/country/${self.country}`, 69 | headers: commonHeaders() 70 | }; 71 | 72 | request.get(reqOptions, (err, response, body) => { 73 | 74 | if (err) { 75 | return next(err); 76 | } 77 | 78 | let {statusCode} = response; 79 | if (statusCode !== 200) { 80 | return next(`HTTP failed with status ${statusCode}`); 81 | } 82 | 83 | var $ = cheerio.load(body, {decodeEntities: true}); 84 | let ratings = $('div.ratingsDatatable tr'); 85 | let table = new Table({ 86 | head: [GB('#'), GB('Rank'), GB('Who'), GB('Title'), GB('Contests'), GB('Rating')] 87 | }); 88 | 89 | forEach(ratings, (rating, key) => { 90 | 91 | if (key === 0) { 92 | return; 93 | } //skip table header 94 | 95 | rating = $(rating).children(); 96 | let info = [(key).toString()]; 97 | 98 | forEach(rating, function (data, indx) { 99 | 100 | let inf = replace($(data).text(), /\s\s+/g, ''); //remove spaces and \n\r 101 | let title = ''; 102 | 103 | if (indx === 1) { 104 | title = $(data) 105 | .find($('.rated-user')) 106 | .attr('title'); 107 | title = replace(title, inf, ''); 108 | 109 | if (!self.withOrg) { 110 | if (title.toLowerCase().indexOf('grandmaster') !== -1) { 111 | inf = RB(inf); 112 | } 113 | else { 114 | inf = CB(inf); 115 | } 116 | } 117 | info.push(inf); 118 | info.push(title); 119 | } 120 | else { 121 | info.push(inf); 122 | } 123 | }); 124 | table.push(info); 125 | }); 126 | spinner.succeed(); 127 | return next(null, table); 128 | }); 129 | }, 130 | (table, next) => { 131 | if( this.withOrg ){ 132 | return self.getOrg(table, next); 133 | } 134 | return next(null, table); 135 | } 136 | ], (err, result) => { 137 | 138 | if( isCallback ){ 139 | spinner.stop(); 140 | return callback(err, result); 141 | } 142 | 143 | if(err){ 144 | spinner.fail(); 145 | logr(err); 146 | return; 147 | } 148 | 149 | log(''); 150 | log(CB(`Country: ${self.country}`)); 151 | log(result.toString()); 152 | }); 153 | } 154 | 155 | 156 | /** 157 | * get organization of the users 158 | * @param table 159 | * @param callback 160 | */ 161 | getOrg(table, callback) { 162 | 163 | let rOptions = { 164 | uri: '', 165 | json: true 166 | }; 167 | 168 | let handles = map(table, (info) => { 169 | return info[2]; 170 | }).join(';'); 171 | 172 | let qsf = qs.stringify({handles: handles}, {encode: false}); 173 | rOptions.uri = `http://codeforces.com/api/user.info?${qsf}`; 174 | 175 | debugs('fetching user\'s Organization...'); 176 | spinner.text = 'fetching user\'s Organization...'; 177 | spinner.start(); 178 | 179 | request 180 | .get(rOptions, (error, response, body) => { 181 | 182 | if (error) { 183 | return callback(error); 184 | } 185 | 186 | let {statusCode} = response; 187 | if (statusCode !== 200) { 188 | return callback( has(body, 'comment') ? body.comment : `HTTP failed with status ${statusCode}`); 189 | } 190 | 191 | if (body.status !== 'OK') { 192 | return callback(body.comment); 193 | } 194 | spinner.succeed(); 195 | 196 | forEach(body.result, (data, key) => { 197 | table[key].push(data.organization); 198 | }); 199 | table.options.head.push(GB('Organization')); 200 | 201 | return callback(null, table); 202 | }); 203 | } 204 | } -------------------------------------------------------------------------------- /src/lib/crawler/Sourcecode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import request from 'request'; 6 | import debug from 'debug'; 7 | import cheerio from 'cheerio'; 8 | import mkdirp from 'mkdirp'; 9 | import JSONStream from 'JSONStream'; 10 | import chalk from 'chalk'; 11 | import has from 'has'; 12 | import ora from 'ora'; 13 | import languages from '../languages'; 14 | import { waterfall, eachLimit, series, eachSeries } from 'async'; 15 | import { log, logr, checkPath, commonHeaders } from '../helpers'; 16 | 17 | 18 | var debugs = debug('CF:sourcecode'); 19 | var spinner = ora({ spinner: 'line' }); 20 | const GB = chalk.green.bold; 21 | 22 | const TIME_OUT = 60000; //1 minute 23 | var headers = commonHeaders(); 24 | var problemHeaders = commonHeaders(); 25 | 26 | 27 | export default class Sourcecode { 28 | 29 | 30 | /** 31 | * @param {String} handle 32 | * @param {Number} limit - connection (async) limit to during download 33 | * @param {boolean} withProblem - if true, also download problem statement 34 | * @param {string} dir - target directory to save code 35 | */ 36 | constructor({handle = null, limit = 10, withProblem = false, dir = '.'} = {}) { 37 | 38 | if (handle === null || typeof handle != 'string') { 39 | throw new Error('handle should not be null or empty'); 40 | } 41 | 42 | this.options = { handle, withProblem, limit, dir }; 43 | } 44 | 45 | /** 46 | * @param callback 47 | */ 48 | download(callback){ 49 | 50 | let self = this; 51 | let hrstart = process.hrtime(); 52 | let totalSubmissions = 0; 53 | headers['Referer'] = `http://codeforces.com/submissions/${self.options.handle}`; 54 | 55 | debugs(`Async limit: ${self.limit}`); 56 | 57 | waterfall([ 58 | (next) => { 59 | self.createOutputDir(self.options, next); 60 | }, 61 | (dir, next) => { 62 | self.getSubmissions(dir, self.options, next); 63 | }, 64 | (dir, submissions, next) => { 65 | 66 | totalSubmissions = submissions.length; 67 | 68 | if (self.options.withProblem) { 69 | return eachLimit(submissions, self.options.limit, self.getResource.bind(self, dir), next); 70 | } 71 | 72 | eachLimit(submissions, self.options.limit, self.getOnlySource.bind(self, dir), next); 73 | } 74 | ], (err, res) => { 75 | 76 | if (typeof callback === 'function') { 77 | spinner.stop(); 78 | return callback(err, res); 79 | } 80 | 81 | if (err) { 82 | spinner.fail(); 83 | logr(err); 84 | return; 85 | } 86 | 87 | let hrend = process.hrtime(hrstart); 88 | log(` Total ${totalSubmissions} submissions saved`); 89 | log(` Execution time: ${hrend[0]}s ${Math.round(hrend[1] / 1000000)}ms`); 90 | }); 91 | } 92 | 93 | /** 94 | * ceate target directory where sourcecode will download, use handle 95 | * @param {Object} options 96 | * @param callback 97 | */ 98 | createOutputDir(options, callback) { 99 | 100 | let {dir, handle} = options; 101 | waterfall([ 102 | (next) => { 103 | if (dir !== '.') { 104 | return checkPath(dir, next); 105 | } 106 | return next(); 107 | }, 108 | (next) => { 109 | 110 | if (dir === '.') { 111 | dir = path.join(path.resolve(process.cwd()), handle); 112 | } 113 | else { 114 | dir = path.join(dir, handle); 115 | } 116 | 117 | spinner.text = `creating directory ${dir}`; 118 | spinner.start(); 119 | 120 | return mkdirp(dir, (err) => { 121 | if (err) { 122 | return next(err); 123 | } 124 | spinner.succeed(); 125 | return next(null, dir); 126 | }); 127 | } 128 | ], callback); 129 | } 130 | 131 | 132 | /** 133 | * Get all submission status using Codeforces API 134 | * @param {string} dir - target directory 135 | * @param {Object} options 136 | * @param callback 137 | */ 138 | getSubmissions(dir, options, callback) { 139 | 140 | let {handle, withProblem} = options; 141 | let url = `http://codeforces.com/api/user.status?handle=${handle}`; 142 | 143 | let apiFailed = false; 144 | let apiMsg = null; 145 | let responseCode = 404; 146 | let contentType = ''; 147 | let acSubmissions = []; 148 | 149 | let reqOptions = { 150 | uri: url, 151 | json: true, 152 | headers: headers, 153 | timeout: TIME_OUT 154 | }; 155 | 156 | spinner.text = 'fetching submissions..'; 157 | spinner.start(); 158 | 159 | let reqStream = request.get(reqOptions); 160 | let jsonStream = reqStream.pipe(JSONStream.parse('result.*')); 161 | 162 | reqStream.on('error', (err) => { 163 | debugs('Failed: Request error'); 164 | debugs(err); 165 | 166 | return callback(err); 167 | }); 168 | 169 | 170 | reqStream.on('complete', () => { 171 | debugs('parsing completed'); 172 | 173 | if (responseCode !== 200) { 174 | return callback(apiMsg || `HTTP failed with status ${responseCode}`); 175 | } 176 | 177 | if (contentType.indexOf('application/json;') === -1) { 178 | return callback('Failed.Not valid data.'); 179 | } 180 | 181 | if (apiFailed) { 182 | return callback(apiMsg); 183 | } 184 | 185 | spinner.stop(); 186 | spinner.text = `total accepted submission: ${acSubmissions.length}`; 187 | spinner.succeed(); 188 | 189 | return callback(null, dir, acSubmissions); 190 | }); 191 | 192 | 193 | reqStream.on('response', (response) => { 194 | debugs(`HTTP Code: ${responseCode}`); 195 | debugs(`Content-Type: ${contentType}`); 196 | 197 | responseCode = response.statusCode; 198 | contentType = response.headers['content-type']; 199 | }); 200 | 201 | 202 | jsonStream.on('header', (data) => { 203 | debugs(`API Status: ${data.status}`); 204 | 205 | if (data.status !== 'OK') { 206 | apiFailed = true; 207 | apiMsg = data.comment; 208 | } 209 | }); 210 | 211 | 212 | jsonStream.on('data', (data) => { 213 | 214 | // `data.contestId < 10000` is for detecting gym.Useless now.Need authorization 215 | if (has(data, 'problem') && data.verdict === 'OK' && data.contestId < 10000) { 216 | 217 | let {problem, id, contestId, programmingLanguage} = data; 218 | let {index} = problem; 219 | let problemId = `${contestId}${index}`; 220 | let root = contestId > 10000 221 | ? 'gym' 222 | : 'contest'; //currently gym not working. need authorization 223 | let submissionUrl = `http://codeforces.com/${root}/${contestId}/submission/${id}`; 224 | let problemUrl = `http://codeforces.com/${root}/${contestId}/problem/${index}`; 225 | 226 | acSubmissions.push({ 227 | submissionId: id, 228 | contestId: contestId, 229 | problemIndex: index, 230 | problemId: problemId, 231 | submissionUrl: submissionUrl, 232 | problemUrl: withProblem 233 | ? problemUrl 234 | : null, 235 | language: programmingLanguage 236 | }); 237 | } 238 | }); 239 | } 240 | 241 | /** 242 | * Fetch sourcecode and problem statement 243 | * @param {string} dir - target directory 244 | * @param {Array} submission - all submission status from API 245 | * @param callback 246 | */ 247 | getResource(dir, submission, callback) { 248 | 249 | let self = this; 250 | let {submissionId, submissionUrl, problemUrl, problemId, language} = submission; 251 | let outputPath = path.join(dir, `${problemId}`); 252 | let ext = languages.getExtension(language); 253 | 254 | series([ 255 | (next) => { 256 | log(GB(` fetching sourecode ${problemId}_${submissionId}`)); 257 | 258 | let filePath = path.join(outputPath, `${problemId}_${submissionId}.${ext}`); 259 | self.getSourceCode(submissionUrl, filePath, `code ${problemId}_${submissionId}`, next); 260 | }, 261 | (next) => { 262 | log(GB(` fetching problem ${problemId}`)); 263 | 264 | let filePath = path.join(outputPath, `${problemId}.html`); 265 | self.getProblem(problemUrl, filePath, `problem ${problemId}`, next); 266 | }, 267 | (next) => { 268 | mkdirp(outputPath, next); 269 | } 270 | ], (err, data) => { 271 | 272 | if (err) { 273 | return callback(err); 274 | } 275 | 276 | if (data.length < 2) { 277 | return callback(); 278 | } 279 | 280 | eachSeries([data[0], data[1]], self.writeOutputs, callback); 281 | }); 282 | } 283 | 284 | 285 | /** 286 | * Only download source code, no problem statement 287 | * @param {string} dir - target directory 288 | * @param {Array} submission - all submission status from API 289 | * @param callback 290 | */ 291 | getOnlySource(dir, submission, callback) { 292 | 293 | let self = this; 294 | let {submissionId, submissionUrl, problemId, language} = submission; 295 | let outputPath = path.join(dir, `${problemId}`); 296 | let ext = languages.getExtension(language); 297 | 298 | series([ 299 | (next) => { 300 | log(GB(` fetching sourecode ${problemId}_${submissionId}`)); 301 | 302 | let filePath = path.join(outputPath, `${problemId}_${submissionId}.${ext}`); 303 | self.getSourceCode(submissionUrl, filePath, `code ${problemId}_${submissionId}`, next); 304 | }, 305 | (next) => { 306 | mkdirp(outputPath, next); 307 | } 308 | ], (err, data) => { 309 | 310 | if (err) { 311 | return callback(err); 312 | } 313 | 314 | if (!data.length) { 315 | return callback(); 316 | } 317 | 318 | self.writeOutputs(data[0], callback); 319 | }); 320 | } 321 | 322 | 323 | /** 324 | * Scrape sourcecode 325 | * @param {string} submissionUrl - the code url 326 | * @param {string} filePath - code path to save 327 | * @param {string} name - name of the code, contestid+problemIndex+submissionId 328 | * @param callback 329 | */ 330 | getSourceCode(submissionUrl, filePath, name, callback) { 331 | 332 | let reqOptions = { 333 | uri: submissionUrl, 334 | headers: headers 335 | }; 336 | 337 | request.get(reqOptions, (err, response, body) => { 338 | 339 | if (err) { 340 | return callback(err); 341 | } 342 | 343 | let $ = cheerio.load(body, {decodeEntities: true}); 344 | let source = $('.program-source'); 345 | 346 | if (!source.length) { 347 | logr(` no soure code found ${submissionUrl}`); 348 | return callback(); 349 | } 350 | 351 | let code = $(source).text(); 352 | 353 | return callback(null, {content: code, path: filePath, name: name}); 354 | }); 355 | } 356 | 357 | 358 | /** 359 | * Scrape problem statement 360 | * @param problemUrl 361 | * @param filePath 362 | * @param name 363 | * @param callback 364 | */ 365 | getProblem(problemUrl, filePath, name, callback) { 366 | 367 | let reqOptions = { 368 | uri: problemUrl, 369 | headers: problemHeaders 370 | }; 371 | 372 | request.get(reqOptions, (err, response, body) => { 373 | 374 | if (err) { 375 | return callback(err); 376 | } 377 | 378 | let $ = cheerio.load(body, {decodeEntities: true}); 379 | let pst = $('.problem-statement'); 380 | 381 | if (!pst.length) { 382 | logr(` no problem statement found ${problemUrl}`); 383 | return callback(); 384 | } 385 | 386 | let statement = $(pst).html(); 387 | 388 | return callback(null, {content: statement, path: filePath, name: name}); 389 | }); 390 | } 391 | 392 | 393 | /** 394 | * save content to file 395 | * @param {Object} output 396 | * @param callback 397 | */ 398 | writeOutputs(output, callback) { 399 | 400 | //gym contest not permitted 401 | if (typeof output === 'undefined' || output === null) { 402 | return callback(); 403 | } 404 | 405 | log(GB(` saving ${output.name}`)); 406 | 407 | fs.writeFile(output.path, output.content, callback); 408 | } 409 | } -------------------------------------------------------------------------------- /src/lib/crawler/Submit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import waterfall from 'async/waterfall'; 4 | import request from 'request'; 5 | import has from 'has'; 6 | import debug from 'debug'; 7 | import cheerio from 'cheerio'; 8 | import chalk from 'chalk'; 9 | import path from 'path'; 10 | import fs from 'fs'; 11 | import ora from 'ora'; 12 | import jsonfile from 'jsonfile'; 13 | import CryptoJS from 'crypto-js'; 14 | import inquirer from 'inquirer'; 15 | import languages from '../languages'; 16 | import { getCWD, checkPath, log, logr, getHomeDir, validateEmpty, commonHeaders } from '../helpers'; 17 | import submission from '../api/submission'; 18 | 19 | 20 | // 21 | // these Should not be here. should avoid hard coded 22 | // 23 | const HASH_SECRET = 'thisisverysecret'; 24 | const TIME_OUT = 30000; 25 | 26 | var cookieJar = request.jar(); 27 | var debugs = debug('CF:submit'); 28 | var spinner = ora({ spinner: 'line' }); 29 | 30 | 31 | export default class Submit { 32 | 33 | 34 | /** 35 | * @param {Number} contestId 36 | * @param {char} problemIndex - problem index of the contest, [A - Z] 37 | * @param {string} codeFile - code file path 38 | * @param {Boolean} watch - if true then show live submission status after submission 39 | * @param {Number} totalRuns - total submission to display if watch 40 | * @param {Number} delay - refreshing time of watch [in millisecond] 41 | * @param {Number} language - language id [see lib/languages.js].If given, use language id, otherwise use given file extension. 42 | * @param {Boolean} remember - if true , save credentials after successfully login 43 | * @param {Boolean} logout - if true, delete credentials after successfully login 44 | * @param {Boolean} gym - if true, submit as gym contest solution 45 | */ 46 | constructor ({ 47 | contestId = null, 48 | problemIndex = null, 49 | codeFile = null, 50 | watch = false, 51 | totalRuns = 1, 52 | delay = 1000, 53 | language = null, 54 | remember = false, 55 | logout = false, 56 | gym = false 57 | } = {}) { 58 | 59 | let isMissing = contestId === null || problemIndex === null || codeFile === null; 60 | if (isMissing) { 61 | throw new Error('codeFile, contestId and problemIndex required'); 62 | } 63 | 64 | let options = {contestId, problemIndex, codeFile, watch, totalRuns, delay, remember, logout}; 65 | options.codePath = getCWD(options.codeFile); 66 | options.form = 'http://codeforces.com/enter'; //login url 67 | options.nextForm = 'http://codeforces.com/problemset/submit'; 68 | options.type = gym 69 | ? 'gym' 70 | : 'contest'; 71 | 72 | /* istanbul ignore else */ 73 | if (language !== null) { 74 | options['language'] = language; 75 | } 76 | 77 | this.options = options; 78 | } 79 | 80 | 81 | /** 82 | * @param next 83 | */ 84 | submit(next) { 85 | 86 | let self = this; 87 | 88 | log(''); 89 | 90 | waterfall([ 91 | (callback) => { 92 | checkPath(self.options.codePath, true, callback); 93 | }, 94 | (callback) => { 95 | self.prepareInput(self.options, callback); 96 | }, 97 | self.getCSRFToken, 98 | self.login, 99 | // self.getCSRFToken, //ok great, same csrf token working for both login and submit! 100 | self.submitSolution 101 | ], (err, res) => { 102 | 103 | if( typeof next === 'function' ){ 104 | return next(err,res); 105 | } 106 | 107 | /* istanbul ignore else */ 108 | if (err) { 109 | 110 | spinner.fail(); 111 | 112 | /* istanbul ignore else */ 113 | if (typeof err === 'string') { 114 | spinner.text = chalk.bold.red(err); 115 | spinner.start(); 116 | spinner.fail(); 117 | return; 118 | } 119 | return logr(err); 120 | } 121 | 122 | /* istanbul ignore next */ 123 | if (self.options.watch) { 124 | 125 | let suboptions = { 126 | remember: false, 127 | count: self.options.totalRuns, 128 | watch: true, 129 | delay: self.options.delay, 130 | contest: true, 131 | contestId: self.options.contestId 132 | }; 133 | 134 | return submission(suboptions); 135 | } 136 | }); 137 | } 138 | 139 | 140 | /** 141 | * Check config file for credentials if previously saved 142 | * If not found, ask handle and password from user console input 143 | * @param {Object} options 144 | * @param callback 145 | * @returns {*} 146 | */ 147 | prepareInput(options, callback) { 148 | 149 | if (has(options, 'language')) { 150 | let {typeId} = languages; 151 | let tid = options.language; 152 | 153 | /* istanbul ignore else */ 154 | if (!has(typeId, tid)) { 155 | return callback(` Error: Invalid language id '${tid}', Please type 'cf lang' to see supported language list`); 156 | } 157 | } 158 | else { 159 | let lang = path 160 | .extname(options.codeFile) 161 | .split('.') 162 | .pop(); 163 | let {extensions} = languages; 164 | 165 | if (!has(extensions, lang)) { 166 | return callback(` Error: Invalid language extension .${lang}, Please type 'cf lang' to see supported language list`); 167 | } 168 | options.language = extensions[lang]; 169 | } 170 | 171 | options.config = path.resolve(`${getHomeDir()}/.cfconfig`); 172 | 173 | waterfall([ 174 | (next) => { 175 | 176 | /* istanbul ignore else */ 177 | if (options.remember) { 178 | debugs('remember me, skip reading config file'); 179 | return next(null, false); 180 | }/* istanbul ignore next */ 181 | 182 | 183 | debugs(`Reading config file ${options.config}`);/* istanbul ignore next */ 184 | spinner.text = 'Reading config file...';/* istanbul ignore next */ 185 | spinner.start();/* istanbul ignore next */ 186 | 187 | jsonfile.readFile(options.config, (err, obj) => { 188 | 189 | if (err) { 190 | 191 | if (err.code === 'EPERM') { 192 | return next('Permission denied config file.'); 193 | } 194 | 195 | if (err.code === 'ENOENT') { 196 | debugs('Config file not found'); 197 | return next(null, false); //send next step to enter manually 198 | } 199 | 200 | return next(err); 201 | } 202 | 203 | spinner.stop(); 204 | debugs('Config file found'); 205 | 206 | // 207 | // Config file may be corrupted or changed 208 | // 209 | if (!has(obj, 'user') || !has(obj, 'pass')) { 210 | return next(null, false); //send next step to enter manually 211 | } 212 | 213 | options.handle = obj.user; 214 | options.password = CryptoJS.AES.decrypt(obj.pass, HASH_SECRET).toString(CryptoJS.enc.Utf8); 215 | 216 | debugs('creadentials found!'); 217 | debugs(obj.pass); 218 | debugs(`decrypt pass: ${options.password}`); 219 | 220 | spinner.text = `Saved handle found '${obj.user}'`; 221 | spinner.succeed(); 222 | 223 | return next(null, true); 224 | }); 225 | }, 226 | (skip, next) => { 227 | 228 | // 229 | // Already credentials found in config file from previous step 230 | // Any better approch to skip async step? 231 | // 232 | if (skip) { 233 | return next(null, options); 234 | } 235 | 236 | // 237 | // handle and password options 238 | // 239 | let credentials = [{ 240 | name: 'handle', 241 | message: 'handle: ', 242 | validate: validateEmpty 243 | }, { 244 | name: 'password', 245 | message: 'password: ', 246 | type: 'password', 247 | validate: validateEmpty 248 | }]; 249 | 250 | spinner.stop(); 251 | 252 | // 253 | // Ask for handle and password 254 | // 255 | inquirer.prompt(credentials).then((answers) => { 256 | options.handle = answers.handle; 257 | options.password = answers.password; 258 | 259 | return next(null, options); 260 | }); 261 | } 262 | ], callback); 263 | } 264 | 265 | 266 | /** 267 | * Load a url, search for from and scrape the csrf token 268 | * @param {Object} options 269 | * @param callback 270 | */ 271 | getCSRFToken(options, callback) { 272 | 273 | let headers = commonHeaders(); 274 | 275 | let opts = { 276 | headers: headers, 277 | uri: options.form, 278 | timeout: TIME_OUT, 279 | jar: cookieJar 280 | }; 281 | 282 | spinner.text = 'Loading token...'; 283 | spinner.start(); 284 | 285 | debugs(`Loading csrf token from ${options.form}...`); 286 | 287 | request.get(opts, (err, httpResponse, body) => { 288 | 289 | if (err) { 290 | return callback(err); 291 | } 292 | 293 | let $ = cheerio.load(body, {decodeEntities: false}); 294 | let csrf_token = $('form input[name="csrf_token"]').attr('value'); 295 | 296 | if (csrf_token === null || csrf_token === undefined) { 297 | debugs($.html()); 298 | return callback('token not found'); 299 | } 300 | 301 | debugs(`Csrf token found! ${csrf_token}`); 302 | 303 | options.form = options.nextForm; 304 | spinner.succeed(); 305 | 306 | return callback(null, csrf_token, options); 307 | }); 308 | } 309 | 310 | 311 | /** 312 | * @param {String} csrf_token - login form token 313 | * @param {Object} options 314 | * @param callback 315 | */ 316 | login(csrf_token, options, callback) { 317 | 318 | let URL = 'http://codeforces.com/enter'; 319 | 320 | let headers = commonHeaders(); 321 | headers['Origin'] = 'http://codeforces.com'; 322 | headers['Referer'] = 'http://codeforces.com/enter'; 323 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 324 | 325 | 326 | // 327 | // Form input fields.Key should match with input fields name 328 | // 329 | let form = { 330 | csrf_token: csrf_token, 331 | action: 'enter', 332 | handle: options.handle, 333 | password: options.password 334 | }; 335 | 336 | let opts = { 337 | headers: headers, 338 | form: form, 339 | url: URL, 340 | timeout: TIME_OUT, 341 | jar: cookieJar 342 | }; 343 | 344 | spinner.text = 'Logging in...'; 345 | spinner.start(); 346 | debugs('Sending login request...'); 347 | 348 | request.post(opts, (err, httpResponse, body) => { 349 | 350 | if (err) { 351 | return callback(err); 352 | } 353 | 354 | var $ = cheerio.load(body, {decodeEntities: false}); 355 | var resHeaders = httpResponse.headers; 356 | 357 | 358 | if (!has(resHeaders, 'location') || resHeaders.location !== '/') { 359 | 360 | debugs($.html()); 361 | 362 | let logError = $('form .for__password'); 363 | if( logError.length ){ 364 | return callback($(logError).text()); 365 | } 366 | 367 | return callback('Login failed.Please try again.[Issue?]'); 368 | } 369 | 370 | 371 | // 372 | // Save credentials into config file.HASH the password first. 373 | // 374 | if (options.remember) { 375 | let hashs = { 376 | user: options.handle, 377 | pass: CryptoJS.AES.encrypt(options.password, HASH_SECRET).toString() 378 | }; 379 | debugs('Saving credentials in config file...'); 380 | jsonfile.writeFileSync(options.config, hashs); //sync? 381 | } 382 | else if (options.logout) { //delete handle and password 383 | debugs('Deleting credentials from config file...'); 384 | jsonfile.writeFileSync(options.config, {}); 385 | } 386 | 387 | debugs('Successfully logged in'); 388 | spinner.succeed(); 389 | 390 | return callback(null, csrf_token, options); 391 | }); 392 | } 393 | 394 | 395 | /** 396 | * *************** TO-DO *********************** 397 | * May be problemset?? 398 | * ********************************************** 399 | * Sumbit code codeforces.com 400 | * @param {String} csrf_token - submit form token 401 | * @param {Object} options 402 | * @param callback 403 | */ 404 | submitSolution(csrf_token, options, callback) { 405 | 406 | let URL = `http://codeforces.com/${options.type}/${options.contestId}/submit?csrf_token=${csrf_token}`; 407 | 408 | let headers = commonHeaders(); 409 | headers['Origin'] = 'http://codeforces.com'; 410 | headers['Referer'] = `http://codeforces.com/${options.type}/${options.contestId}/submit`; 411 | headers['Content-Type'] = 'multipart/form-data; boundary=----WebKitFormBoundaryv9DeqLHW1rFHNpiY'; 412 | 413 | // 414 | // Form input fields.Key should match with input fields name 415 | // 416 | let formData = { 417 | csrf_token: csrf_token, 418 | action: 'submitSolutionFormSubmitted', 419 | contestId: options.contestId, 420 | submittedProblemIndex: options.problemIndex, 421 | programTypeId: options.language, 422 | source: fs.createReadStream(options.codePath), // give `request` module to handle it 423 | tabSize: '4', 424 | sourceFile: '' 425 | }; 426 | 427 | let opts = { 428 | headers: headers, 429 | formData: formData, 430 | url: URL, 431 | timeout: TIME_OUT, 432 | jar: cookieJar 433 | }; 434 | 435 | spinner.text = 'Submitting solution...'; 436 | spinner.start(); 437 | debugs(`Submitting solution ${URL}...`); 438 | 439 | request.post(opts, (err, httpResponse, body) => { 440 | 441 | if (err) { 442 | return callback(err); 443 | } 444 | 445 | var $ = cheerio.load(body, {decodeEntities: false}); 446 | var location = httpResponse.headers; 447 | var expectedLocation = `/${options.type}/${options.contestId}/my`; 448 | 449 | if (!has(location, 'location') || location['location'] !== expectedLocation) { 450 | 451 | // 452 | // Codeforces provided error 453 | // 454 | var for__source = $('.for__source'); 455 | if (for__source.length) { 456 | return callback($(for__source).text()); 457 | } 458 | 459 | debugs('something wrong!.'); 460 | debugs($.html()); 461 | debugs(location); 462 | 463 | //something wrong! 464 | return callback('Error: Submission failed.Please check your options.'); 465 | } 466 | 467 | spinner.succeed(); 468 | spinner.text = chalk.bold.green(`Submitted at ${location.date}`); 469 | spinner.start(); 470 | spinner.succeed(); 471 | 472 | return callback(null, location.date); 473 | }); 474 | } 475 | } 476 | 477 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import debug from 'debug'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | 6 | var debugs = debug('helpers:checkPath'); 7 | 8 | 9 | /** 10 | * console.error() 11 | * @param text 12 | */ 13 | export function logr(text) { 14 | if( typeof text === 'string' ){ 15 | text = chalk.red.bold(` Error: ${text}`); 16 | } 17 | console.error(text); 18 | } 19 | 20 | 21 | /** 22 | * console.log() 23 | * @param text 24 | */ 25 | export function log(text) { 26 | console.log(text); 27 | } 28 | 29 | 30 | /** 31 | * Detect platform 32 | * @returns {boolean} 33 | */ 34 | export function isUnix() { 35 | return process.platform.indexOf('win32') === -1; 36 | } 37 | 38 | 39 | /** 40 | * Clear console 41 | */ 42 | export function clear() { 43 | process.stdout.write('\x1B[H\x1B[J'); 44 | } 45 | 46 | 47 | /** 48 | * Get system home dir ( Windows C:/Users/{user}, Unix ~/ ) 49 | * @returns {*} 50 | */ 51 | export function getHomeDir() { 52 | return process.env[isUnix() ? 'HOME' : 'USERPROFILE']; 53 | } 54 | 55 | 56 | /** 57 | * Check a path if exists.Also check if it is file or directory 58 | * @param {String} directory - target directory 59 | * @param {boolean} isFile - if true then check if it is a file 60 | * @param callback 61 | */ 62 | export function checkPath(directory, isFile, callback) { 63 | 64 | if( typeof isFile === 'function' ){ 65 | callback = isFile; 66 | isFile = false; 67 | } 68 | 69 | debugs(`checking directory ${directory}...`); 70 | 71 | fs.stat(directory, function(err, stats) { 72 | 73 | if(err){ 74 | 75 | if( err.code === 'EPERM' ){ 76 | return callback(` Error: Permission denied.Please make sure you have permission for '${directory}'`); 77 | } 78 | 79 | if( err.code === 'ENOENT' ){ 80 | return callback(` Error: No such file or directory '${directory}'`); 81 | } 82 | 83 | return callback(err); 84 | } 85 | 86 | if( isFile && !stats.isFile() ){ 87 | return callback(` Error: Not a file '${directory}'.`); 88 | } 89 | 90 | if( !isFile && !stats.isDirectory() ){ 91 | return callback(` Error: Not a directory '${directory}'.`); 92 | } 93 | 94 | return callback(); 95 | }); 96 | } 97 | 98 | 99 | /** 100 | * Get cwd and join a file name 101 | * @param {String} fileName 102 | * @returns {string|*} 103 | */ 104 | export function getCWD(fileName) { 105 | return path.join(path.resolve(process.cwd()), fileName); 106 | } 107 | 108 | 109 | /** 110 | * Validator for console promt [inquirer] 111 | * @param inpt 112 | * @returns {boolean} 113 | */ 114 | export function validateEmpty(inpt) { 115 | return inpt.length > 0; 116 | } 117 | 118 | 119 | 120 | /** 121 | * Common headers for crawler 122 | * @returns {{Host: string, Upgrade-Insecure-Requests: number, User-Agent: string, Accept: string, Accept-Language: string}} 123 | */ 124 | export function commonHeaders() { 125 | return { 126 | 'Host': 'codeforces.com', 127 | 'Upgrade-Insecure-Requests': 1, 128 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', 129 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 130 | 'Accept-Language': 'en-US,en;q=0.8' 131 | }; 132 | } -------------------------------------------------------------------------------- /src/lib/languages.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | typeId: { 4 | '10': 'GNU GCC 5.1.0', 5 | '43': 'GNU GCC C11 5.1.0', 6 | '1': 'GNU G++ 5.1.0', 7 | '42': 'GNU G++11 5.1.0', 8 | '50': 'GNU G++14 6.2.0', 9 | '2': 'Microsoft Visual C++ 2010', 10 | '9': 'C# Mono 3.12.1.0', 11 | '29': 'MS C# .NET 4.0.30319', 12 | '28': 'D DMD32 v2.069.2', 13 | '32': 'Go 1.5.2', 14 | '12': 'Haskell GHC 7.8.3', 15 | '36': 'Java 1.8.0_66', 16 | '48': 'Kotlin 1.0.1', 17 | '19': 'OCaml 4.02.1', 18 | '3': 'Delphi 7', 19 | '4': 'Free Pascal 2.6.4', 20 | '13': 'Perl 5.20.1', 21 | '6': 'PHP 5.4.42', 22 | '7': 'Python 2.7.10', 23 | '31': 'Python 3.5.1', 24 | '40': 'PyPy 2.7.10 (2.6.1)', 25 | '41': 'PyPy 3.2.5 (2.4.0)', 26 | '8': 'Ruby 2.0.0p645', 27 | '49': 'Rust 1.10', 28 | '20': 'Scala 2.11.7', 29 | '34': 'JavaScript V8 4.8.0', 30 | '14': 'ActiveTcl 8.5', 31 | '15': 'Io-2008-01-07 (Win32)', 32 | '17': 'Pike 7.8', 33 | '18': 'Befunge', 34 | '22': 'OpenCobol 1.0', 35 | '25': 'Factor', 36 | '26': 'Secret_171', 37 | '27': 'Roco', 38 | '33': 'Ada GNAT 4', 39 | '38': 'Mysterious Language', 40 | '39': 'FALSE', 41 | '44': 'Picat 0.9', 42 | '45': 'GNU C++11 5 ZIP', 43 | '46': 'Java 8 ZIP', 44 | '47': 'J' 45 | }, 46 | extensions: { 47 | 'c': '10', 48 | 'cpp': '1', 49 | 'cc': '50', 50 | 'java': '36', 51 | 'py': '31', 52 | 'cs': '29', 53 | 'go': '32', 54 | 'hs': '12', 55 | 'ml': '19', 56 | 'pas': '4', 57 | 'php': '6', 58 | 'js': '34', 59 | 'pl': '13', 60 | 'rb': '8', 61 | 'rs': '49', 62 | 'scala': '20', 63 | 'sc': '20' 64 | }, 65 | getExtension (language = 'text') { 66 | 67 | if( language.toLowerCase().indexOf('c++') !== -1 || language.toLowerCase().indexOf('g++') !== -1 ){ 68 | return 'cpp'; 69 | } 70 | if( language.toLowerCase().indexOf('gcc') !== -1 ){ 71 | return 'c'; 72 | } 73 | if( language.toLowerCase().indexOf('java') !== -1 ){ 74 | return 'java'; 75 | } 76 | if( language.toLowerCase().indexOf('python') !== -1 ){ 77 | return 'py'; 78 | } 79 | if( language.toLowerCase().indexOf('pascal') !== -1 ){ 80 | return 'pas'; 81 | } 82 | if( language.toLowerCase().indexOf('ruby') !== -1 ){ 83 | return 'rb'; 84 | } 85 | if( language.toLowerCase().indexOf('c#') !== -1 ){ 86 | return 'cs'; 87 | } 88 | if( language.toLowerCase().indexOf('perl') !== -1 ){ 89 | return 'pl'; 90 | } 91 | if( language.toLowerCase().indexOf('scala') !== -1 ){ 92 | return 'sc'; 93 | } 94 | if( language.toLowerCase().indexOf('php') !== -1 ){ 95 | return 'php'; 96 | } 97 | if( language.toLowerCase().indexOf('go') !== -1 ){ 98 | return 'go'; 99 | } 100 | if( language.toLowerCase().indexOf('haskell') !== -1 ){ 101 | return 'hs'; 102 | } 103 | return language; 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/lib/utils/cfdefault.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import Table from 'cli-table2'; 5 | import chalk from 'chalk'; 6 | import { log } from '../helpers'; 7 | import countries from '../countries'; 8 | import languages from '../languages'; 9 | 10 | var GB = chalk.green.bold; 11 | 12 | export default { 13 | 14 | countrs() { 15 | 16 | let table = new Table({ 17 | head: [GB('Country')] 18 | }); 19 | 20 | _.forEach(countries, (country) => { 21 | table.push([country]); 22 | }); 23 | 24 | log(''); 25 | log(table.toString()); 26 | }, 27 | exts() { 28 | 29 | let table = new Table({ 30 | head: [GB('Language'), GB('Extension')] 31 | }); 32 | 33 | _.forEach(languages.extensions, (typeId, extension) => { 34 | table.push([languages.typeId[typeId], `.${extension}`]); 35 | }); 36 | 37 | log(''); 38 | log(table.toString()); 39 | }, 40 | langs() { 41 | 42 | let table = new Table({ 43 | head: [GB('Language'), GB('Id')] 44 | }); 45 | 46 | _.forEach(languages.typeId, (language, typeId) => { 47 | table.push([language, typeId]); 48 | }); 49 | 50 | log(''); 51 | log(table.toString()); 52 | } 53 | }; -------------------------------------------------------------------------------- /src/lib/verdicts.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | FAILED: 'Failed', 4 | OK: 'Accepted', 5 | PARTIAL: 'Partial', 6 | COMPILATION_ERROR: 'Compilation error', 7 | RUNTIME_ERROR: 'Runtime error', 8 | WRONG_ANSWER: 'Wrong answer', 9 | PRESENTATION_ERROR: 'Presentation error', 10 | TIME_LIMIT_EXCEEDED: 'Time limit exceeded', 11 | MEMORY_LIMIT_EXCEEDED: 'Memory limit exceeded', 12 | IDLENESS_LIMIT_EXCEEDED: 'Idleness limit exceeded', 13 | SECURITY_VIOLATED: 'Security Violated', 14 | CRASHED: 'Crashed', 15 | INPUT_PREPARATION_CRASHED: 'Input Preparation Crashed', 16 | CHALLENGED: 'Challenged', 17 | SKIPPED: 'Skipped', 18 | TESTING: 'Running on tests', 19 | REJECTED: 'Rejected' 20 | }; 21 | -------------------------------------------------------------------------------- /tests/api/test_standings.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | import has from 'has'; 6 | var sinonChai = require('sinon-chai'); 7 | 8 | var helpers = require('../../src/lib/helpers'); 9 | import standings from '../../src/lib/api/standings'; 10 | 11 | chai.use(sinonChai); 12 | 13 | var mockResponse = { 14 | statusCode: 200, 15 | headers: { 16 | 'content-type': 'application/json;' 17 | } 18 | }; 19 | 20 | var mockBody = { 21 | "status":"OK", 22 | "result":{ 23 | "contest":{ 24 | "id":550, 25 | "name":"Codeforces Round #306 (Div. 2)", 26 | "type":"CF", 27 | "phase":"FINISHED", 28 | "frozen":false, 29 | "durationSeconds":7200, 30 | "startTimeSeconds":1433435400, 31 | "relativeTimeSeconds":43653316 32 | }, 33 | "problems":[], 34 | "rows":[] 35 | } 36 | }; 37 | 38 | 39 | 40 | describe('Codeforces', function() { 41 | describe('#standings', function() { 42 | describe('[core]', function() { 43 | 44 | beforeEach(function(){ 45 | sinon.stub(process.stderr,'write'); 46 | }); 47 | afterEach(function(){ 48 | process.stderr.write.restore(); 49 | }); 50 | 51 | it('should throw error when parameter is empty', function(done) { 52 | expect(function () { 53 | standings(); 54 | }).to.throw(Error); 55 | done(); 56 | }); 57 | 58 | it('should throw error when contest id is not integer', function(done) { 59 | expect(function () { 60 | standings({ contestId: null }); 61 | }).to.throw(Error); 62 | expect(function () { 63 | standings({ contestId: '211' }); 64 | }).to.throw(Error); 65 | expect(function () { 66 | standings({ contestId: undefined }); 67 | }).to.throw(Error); 68 | expect(function () { 69 | standings({ contestId: {} }); 70 | }).to.throw(Error); 71 | done(); 72 | }); 73 | }); 74 | 75 | describe('[event]', function() { 76 | 77 | var eventStub = { 78 | on: function(eventName , data) { return this; }, 79 | pipe: function() { return this; } 80 | }; 81 | var spy; 82 | 83 | beforeEach(function(){ 84 | sinon.stub(process.stderr,'write'); 85 | sinon.stub(request, 'get').returns(eventStub); 86 | spy = sinon.spy(eventStub,'on'); 87 | }); 88 | 89 | afterEach(function(){ 90 | process.stderr.write.restore(); 91 | request.get.restore(); 92 | eventStub.on.restore(); 93 | }); 94 | 95 | it('should call request', function(done) { 96 | standings({ contestId: 550, count: 2 }); 97 | expect(request.get.called).to.be.true; 98 | done(); 99 | }); 100 | 101 | it('should call all 5 event', function(done) { 102 | standings({ contestId: 550, count: 2 }); 103 | expect(spy.callCount).to.equal(5); 104 | done(); 105 | }); 106 | 107 | it('should call all 5 event with handles', function(done) { 108 | standings({ contestId: 550, count: 2, handles: ['ad'] }); 109 | expect(spy.callCount).to.equal(5); 110 | done(); 111 | }); 112 | 113 | it('should call all 5 event with unofficial', function(done) { 114 | standings({ contestId: 550, count: 2, handles: 'ada,adad,ad', unofficial: true, from: 2 }); 115 | expect(spy.callCount).to.equal(5); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | -------------------------------------------------------------------------------- /tests/api/test_submission.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | import jsonfile from 'jsonfile'; 7 | import inquirer from 'inquirer'; 8 | var Promise = require("bluebird"); 9 | 10 | var helpers = require('../../src/lib/helpers'); 11 | import submission from '../../src/lib/api/submission'; 12 | var beforeHelpers = require('../helpers/submission_helper'); 13 | 14 | chai.use(sinonChai); 15 | 16 | describe('Codeforces', function() { 17 | describe('#submission', function() { 18 | 19 | describe('[No callback]', function() { 20 | beforeHelpers( 21 | { err: new MyError('Unknown'), obj: null }, 22 | { handle: 'someHandle' }, 23 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 24 | ); 25 | it('should call console.error when callback absent', function(done) { 26 | setTimeout(function() { 27 | submission(); 28 | expect(helpers.logr.called).to.true; 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('[EPERM]', function() { 35 | beforeHelpers( 36 | { err: new MyError('EPERM'), obj: null }, 37 | { handle: 'someHandle' }, 38 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 39 | ); 40 | it('should throw error when reading config file permission denied', function(done) { 41 | expect(function () { 42 | submission(); 43 | }).to.throw(Error); 44 | expect(inquirer.prompt.called).to.be.false; 45 | expect(request.get.called).to.be.false; 46 | expect(helpers.logr.called).to.false; 47 | done(); 48 | }); 49 | }); 50 | 51 | describe('[Unknown]', function() { 52 | beforeHelpers( 53 | { err: new MyError('Unknown'), obj: null }, 54 | { handle: 'someHandle' }, 55 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 56 | ); 57 | it('should return jsonfile error', function(done) { 58 | submission({ callback: function (err) { 59 | setTimeout(function() { 60 | expect(err.code).to.equal(new MyError('Unknown').code); 61 | expect(inquirer.prompt.called).to.be.false; 62 | expect(request.get.called).to.be.false; 63 | expect(helpers.logr.called).to.false; 64 | done(); 65 | }); 66 | }}); 67 | }); 68 | }); 69 | 70 | describe('[ENOENT]', function() { 71 | beforeHelpers( 72 | { err: new MyError('ENOENT'), obj: null }, 73 | { handle: 'someHandle' }, 74 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 75 | ); 76 | it('should ask prompt to enter handle', function(done) { 77 | submission({ callback: function (err) { 78 | setTimeout(function() { 79 | expect(inquirer.prompt.called).to.be.true; 80 | expect(jsonfile.writeFileSync.called).to.be.true; 81 | expect(request.get.called).to.be.true; 82 | expect(helpers.logr.called).to.false; 83 | done(); 84 | }); 85 | }}); 86 | }); 87 | }); 88 | 89 | describe('[No user]', function() { 90 | beforeHelpers( 91 | { err: null, obj: {} }, 92 | { handle: 'someHandle' }, 93 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 94 | ); 95 | it('should ask prompt to enter handle when user not in config file', function(done) { 96 | submission({ callback: function (err) { 97 | setTimeout(function() { 98 | expect(inquirer.prompt.called).to.be.true; 99 | expect(jsonfile.writeFileSync.called).to.be.true; 100 | expect(request.get.called).to.be.true; 101 | expect(helpers.logr.called).to.false; 102 | done(); 103 | }); 104 | }}); 105 | }); 106 | }); 107 | 108 | describe('{request.get}', function() { 109 | 110 | describe('[error]', function() { 111 | beforeHelpers( 112 | { err: null, obj: { user: 'someuser' } }, 113 | { handle: 'someHandle' }, 114 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 115 | ); 116 | it('should not ask prompt to enter handle when user found in config file', function(done) { 117 | submission({ callback: function (err) { 118 | setTimeout(function() { 119 | expect(inquirer.prompt.called).to.be.false; 120 | expect(jsonfile.writeFileSync.called).to.be.false; 121 | expect(request.get.called).to.be.true; 122 | expect(helpers.logr.called).to.false; 123 | done(); 124 | }); 125 | }}); 126 | }); 127 | it('should match request error', function(done) { 128 | submission({ callback: function (err) { 129 | setTimeout(function() { 130 | expect(inquirer.prompt.called).to.be.false; 131 | expect(jsonfile.writeFileSync.called).to.be.false; 132 | expect(request.get.called).to.be.true; 133 | expect(helpers.logr.called).to.false; 134 | expect(err).to.equal('reqerror'); 135 | done(); 136 | }); 137 | }}); 138 | }); 139 | }); 140 | 141 | describe('[error - watch]', function() { 142 | beforeHelpers( 143 | { err: null, obj: { user: 'someuser' } }, 144 | { handle: 'someHandle' }, 145 | { err: 'reqerror', res: { statusCode: 404 }, body: null } 146 | ); 147 | it('should not ask prompt to enter handle when user found in config file', function(done) { 148 | submission({ watch: true, callback: function (err) { 149 | setTimeout(function() { 150 | expect(inquirer.prompt.called).to.be.false; 151 | expect(jsonfile.writeFileSync.called).to.be.false; 152 | expect(request.get.called).to.be.true; 153 | expect(helpers.logr.called).to.false; 154 | done(); 155 | }); 156 | }}); 157 | }); 158 | it('should match request error', function(done) { 159 | submission({ callback: function (err) { 160 | setTimeout(function() { 161 | expect(inquirer.prompt.called).to.be.false; 162 | expect(jsonfile.writeFileSync.called).to.be.false; 163 | expect(request.get.called).to.be.true; 164 | expect(helpers.logr.called).to.false; 165 | expect(err).to.equal('reqerror'); 166 | done(); 167 | }); 168 | }}); 169 | }); 170 | }); 171 | 172 | describe('[api error]', function() { 173 | beforeHelpers( 174 | { err: null, obj: { user: 'someuser' } }, 175 | { handle: 'someHandle' }, 176 | { err: null, res: { statusCode: 404 }, body: { comment: 'someAPIerror' } } 177 | ); 178 | it('should match comment error', function(done) { 179 | submission({ callback: function (err) { 180 | setTimeout(function() { 181 | expect(inquirer.prompt.called).to.be.false; 182 | expect(jsonfile.writeFileSync.called).to.be.false; 183 | expect(request.get.called).to.be.true; 184 | expect(helpers.logr.called).to.false; 185 | expect(err).to.equal('someAPIerror'); 186 | done(); 187 | }); 188 | }}); 189 | }); 190 | }); 191 | 192 | describe('[api error - watch]', function() { 193 | beforeHelpers( 194 | { err: null, obj: { user: 'someuser' } }, 195 | { handle: 'someHandle' }, 196 | { err: null, res: { statusCode: 404 }, body: { comment: 'someAPIerror' } } 197 | ); 198 | it('should match comment error', function(done) { 199 | submission({ watch: true, callback: function (err) { 200 | setTimeout(function() { 201 | expect(inquirer.prompt.called).to.be.false; 202 | expect(jsonfile.writeFileSync.called).to.be.false; 203 | expect(request.get.called).to.be.true; 204 | expect(helpers.logr.called).to.false; 205 | expect(err).to.equal('someAPIerror'); 206 | done(); 207 | }); 208 | }}); 209 | }); 210 | }); 211 | 212 | describe('[HTTP error]', function() { 213 | beforeHelpers( 214 | { err: null, obj: { user: 'someuser' } }, 215 | { handle: 'someHandle' }, 216 | { err: null, res: { statusCode: 404 }, body: {} } 217 | ); 218 | it('should match HTTP error', function(done) { 219 | submission({ callback: function (err) { 220 | setTimeout(function() { 221 | expect(inquirer.prompt.called).to.be.false; 222 | expect(jsonfile.writeFileSync.called).to.be.false; 223 | expect(request.get.called).to.be.true; 224 | expect(helpers.logr.called).to.false; 225 | expect(err).to.equal('HTTP failed with status 404'); 226 | done(); 227 | }); 228 | }}); 229 | }); 230 | }); 231 | 232 | describe('[HTTP error - watch]', function() { 233 | beforeHelpers( 234 | { err: null, obj: { user: 'someuser' } }, 235 | { handle: 'someHandle' }, 236 | { err: null, res: { statusCode: 404 }, body: {} } 237 | ); 238 | it('should match HTTP error', function(done) { 239 | submission({ watch: true, callback: function (err) { 240 | setTimeout(function() { 241 | expect(inquirer.prompt.called).to.be.false; 242 | expect(jsonfile.writeFileSync.called).to.be.false; 243 | expect(request.get.called).to.be.true; 244 | expect(helpers.logr.called).to.false; 245 | expect(err).to.equal('HTTP failed with status 404'); 246 | done(); 247 | }); 248 | }}); 249 | }); 250 | }); 251 | 252 | describe('[api error2]', function() { 253 | beforeHelpers( 254 | { err: null, obj: { user: 'someuser' } }, 255 | { handle: 'someHandle' }, 256 | { err: null, res: { statusCode: 200 }, body: { status: 'Failed', comment: 'failed api' } } 257 | ); 258 | it('should match API error', function(done) { 259 | submission({ callback: function (err) { 260 | setTimeout(function() { 261 | expect(inquirer.prompt.called).to.be.false; 262 | expect(jsonfile.writeFileSync.called).to.be.false; 263 | expect(request.get.called).to.be.true; 264 | expect(helpers.logr.called).to.false; 265 | expect(err).to.equal('failed api'); 266 | done(); 267 | }); 268 | }}); 269 | }); 270 | }); 271 | 272 | describe('[api error2 - watch]', function() { 273 | beforeHelpers( 274 | { err: null, obj: { user: 'someuser' } }, 275 | { handle: 'someHandle' }, 276 | { err: null, res: { statusCode: 200 }, body: { status: 'Failed', comment: 'failed api' } } 277 | ); 278 | it('should match API error', function(done) { 279 | submission({ watch: true, callback: function (err) { 280 | setTimeout(function() { 281 | expect(inquirer.prompt.called).to.be.false; 282 | expect(jsonfile.writeFileSync.called).to.be.false; 283 | expect(request.get.called).to.be.true; 284 | expect(helpers.logr.called).to.false; 285 | expect(err).to.equal('failed api'); 286 | done(); 287 | }); 288 | }}); 289 | }); 290 | }); 291 | 292 | describe('[onsuccess]', function() { 293 | var mockData = [ 294 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"OK","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 295 | ]; 296 | beforeHelpers( 297 | { err: null, obj: { user: 'someuser' } }, 298 | { handle: 'someHandle' }, 299 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 300 | ); 301 | it('should not have error', function(done) { 302 | submission({ callback: function (err) { 303 | setTimeout(function() { 304 | expect(inquirer.prompt.called).to.be.false; 305 | expect(jsonfile.writeFileSync.called).to.be.false; 306 | expect(request.get.called).to.be.true; 307 | expect(helpers.logr.called).to.false; 308 | expect(err).to.be.null; 309 | done(); 310 | }); 311 | }}); 312 | }); 313 | it('should not have error - watch mode', function(done) { 314 | submission({ watch: true, callback: function (err) { 315 | setTimeout(function() { 316 | expect(inquirer.prompt.called).to.be.false; 317 | expect(jsonfile.writeFileSync.called).to.be.false; 318 | expect(request.get.called).to.be.true; 319 | expect(helpers.logr.called).to.false; 320 | expect(err).to.be.null; 321 | done(); 322 | }); 323 | }}); 324 | }); 325 | }); 326 | 327 | describe('[all params]', function() { 328 | var mockData = [ 329 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"OK","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 330 | ]; 331 | beforeHelpers( 332 | { err: null, obj: { user: 'someuser' } }, 333 | { handle: 'someHandle' }, 334 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 335 | ); 336 | it('should not have error', function(done) { 337 | submission({ count: 1, remember: false, watch: false, contest: true, contestId: 550, delay: 2000, callback: function (err) { 338 | setTimeout(function() { 339 | expect(inquirer.prompt.called).to.be.false; 340 | expect(jsonfile.writeFileSync.called).to.be.false; 341 | expect(request.get.called).to.be.true; 342 | expect(helpers.logr.called).to.false; 343 | expect(err).to.be.null; 344 | done(); 345 | }); 346 | }}); 347 | }); 348 | }); 349 | 350 | describe('[verdicts]', function() { 351 | describe('[RUNTIME_ERROR]', function() { 352 | var mockData = [ 353 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"RUNTIME_ERROR","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 354 | ]; 355 | beforeHelpers( 356 | { err: null, obj: { user: 'someuser' } }, 357 | { handle: 'someHandle' }, 358 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 359 | ); 360 | it('should not have error', function(done) { 361 | submission({ callback: function (err) { 362 | setTimeout(function() { 363 | expect(inquirer.prompt.called).to.be.false; 364 | expect(jsonfile.writeFileSync.called).to.be.false; 365 | expect(request.get.called).to.be.true; 366 | expect(helpers.logr.called).to.false; 367 | expect(err).to.be.null; 368 | done(); 369 | }); 370 | }}); 371 | }); 372 | }); 373 | describe('[WRONG_ANSWER]', function() { 374 | var mockData = [ 375 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"WRONG_ANSWER","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 376 | ]; 377 | beforeHelpers( 378 | { err: null, obj: { user: 'someuser' } }, 379 | { handle: 'someHandle' }, 380 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 381 | ); 382 | it('should not have error', function(done) { 383 | submission({ callback: function (err) { 384 | setTimeout(function() { 385 | expect(inquirer.prompt.called).to.be.false; 386 | expect(jsonfile.writeFileSync.called).to.be.false; 387 | expect(request.get.called).to.be.true; 388 | expect(helpers.logr.called).to.false; 389 | expect(err).to.be.null; 390 | done(); 391 | }); 392 | }}); 393 | }); 394 | }); 395 | describe('[PRESENTATION_ERROR]', function() { 396 | var mockData = [ 397 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"PRESENTATION_ERROR","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 398 | ]; 399 | beforeHelpers( 400 | { err: null, obj: { user: 'someuser' } }, 401 | { handle: 'someHandle' }, 402 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 403 | ); 404 | it('should not have error', function(done) { 405 | submission({ callback: function (err) { 406 | setTimeout(function() { 407 | expect(inquirer.prompt.called).to.be.false; 408 | expect(jsonfile.writeFileSync.called).to.be.false; 409 | expect(request.get.called).to.be.true; 410 | expect(helpers.logr.called).to.false; 411 | expect(err).to.be.null; 412 | done(); 413 | }); 414 | }}); 415 | }); 416 | }); 417 | describe('[TIME_LIMIT_EXCEEDED]', function() { 418 | var mockData = [ 419 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"TIME_LIMIT_EXCEEDED","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 420 | ]; 421 | beforeHelpers( 422 | { err: null, obj: { user: 'someuser' } }, 423 | { handle: 'someHandle' }, 424 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 425 | ); 426 | it('should not have error', function(done) { 427 | submission({ callback: function (err) { 428 | setTimeout(function() { 429 | expect(inquirer.prompt.called).to.be.false; 430 | expect(jsonfile.writeFileSync.called).to.be.false; 431 | expect(request.get.called).to.be.true; 432 | expect(helpers.logr.called).to.false; 433 | expect(err).to.be.null; 434 | done(); 435 | }); 436 | }}); 437 | }); 438 | }); 439 | describe('[MEMORY_LIMIT_EXCEEDED]', function() { 440 | var mockData = [ 441 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"MEMORY_LIMIT_EXCEEDED","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 442 | ]; 443 | beforeHelpers( 444 | { err: null, obj: { user: 'someuser' } }, 445 | { handle: 'someHandle' }, 446 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 447 | ); 448 | it('should not have error', function(done) { 449 | submission({ callback: function (err) { 450 | setTimeout(function() { 451 | expect(inquirer.prompt.called).to.be.false; 452 | expect(jsonfile.writeFileSync.called).to.be.false; 453 | expect(request.get.called).to.be.true; 454 | expect(helpers.logr.called).to.false; 455 | expect(err).to.be.null; 456 | done(); 457 | }); 458 | }}); 459 | }); 460 | }); 461 | describe('[IDLENESS_LIMIT_EXCEEDED]', function() { 462 | var mockData = [ 463 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"IDLENESS_LIMIT_EXCEEDED","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 464 | ]; 465 | beforeHelpers( 466 | { err: null, obj: { user: 'someuser' } }, 467 | { handle: 'someHandle' }, 468 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 469 | ); 470 | it('should not have error', function(done) { 471 | submission({ callback: function (err) { 472 | setTimeout(function() { 473 | expect(inquirer.prompt.called).to.be.false; 474 | expect(jsonfile.writeFileSync.called).to.be.false; 475 | expect(request.get.called).to.be.true; 476 | expect(helpers.logr.called).to.false; 477 | expect(err).to.be.null; 478 | done(); 479 | }); 480 | }}); 481 | }); 482 | }); 483 | describe('[DEFAULTS]', function() { 484 | var mockData = [ 485 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","verdict":"DEFAULTS","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 486 | ]; 487 | beforeHelpers( 488 | { err: null, obj: { user: 'someuser' } }, 489 | { handle: 'someHandle' }, 490 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } } 491 | ); 492 | it('should not have error', function(done) { 493 | submission({ callback: function (err) { 494 | setTimeout(function() { 495 | expect(inquirer.prompt.called).to.be.false; 496 | expect(jsonfile.writeFileSync.called).to.be.false; 497 | expect(request.get.called).to.be.true; 498 | expect(helpers.logr.called).to.false; 499 | expect(err).to.be.null; 500 | done(); 501 | }); 502 | }}); 503 | }); 504 | }); 505 | }); 506 | 507 | 508 | describe('[watch]', function() { 509 | 510 | this.timeout(20000); 511 | 512 | var mockData = [ 513 | {"id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 514 | ]; 515 | var mockData2= [ 516 | {"verdict":"OK","id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 517 | ]; 518 | beforeHelpers( 519 | { err: null, obj: { user: 'someuser' } }, 520 | { handle: 'someHandle' }, 521 | { 522 | ignore: true, 523 | data: 524 | [ 525 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } }, 526 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData2 } } 527 | ] 528 | } 529 | ); 530 | 531 | it('keep watching on undefined verdict', function(done) { 532 | submission({ watch: true,callback: function (err) { 533 | setTimeout(function() { 534 | expect(inquirer.prompt.called).to.be.false; 535 | expect(jsonfile.writeFileSync.called).to.be.false; 536 | expect(request.get.called).to.be.true; 537 | expect(helpers.logr.called).to.false; 538 | expect(err).to.be.null; 539 | done(); 540 | }); 541 | }}); 542 | }); 543 | }); 544 | 545 | describe('[watch]', function() { 546 | 547 | this.timeout(20000); 548 | 549 | var mockData = [ 550 | {"verdict":"TESTING","id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 551 | ]; 552 | var mockData2= [ 553 | {"verdict":"OK","id":20289294,"contestId":708,"creationTimeSeconds":1472588188,"relativeTimeSeconds":2147483647,"problem":{"contestId":708,"index":"C","name":"Centroids","type":"PROGRAMMING","points":1500.0,"tags":["data structures","dfs and similar","dp","greedy","trees"]},"author":{"contestId":708,"members":[{"handle":"Fefer_Ivan"}],"participantType":"PRACTICE","ghost":false,"startTimeSeconds":1472056500},"programmingLanguage":"GNU C++11","testset":"TESTS","passedTestCount":119,"timeConsumedMillis":701,"memoryConsumedBytes":50995200} 554 | ]; 555 | beforeHelpers( 556 | { err: null, obj: { user: 'someuser' } }, 557 | { handle: 'someHandle' }, 558 | { 559 | ignore: true, 560 | data: 561 | [ 562 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData } }, 563 | { err: null, res: { statusCode: 200 }, body: { status: 'OK', result: mockData2 } } 564 | ] 565 | } 566 | ); 567 | 568 | it('keep watching on TESTING verdict', function(done) { 569 | submission({ watch: true,callback: function (err) { 570 | setTimeout(function() { 571 | expect(inquirer.prompt.called).to.be.false; 572 | expect(jsonfile.writeFileSync.called).to.be.false; 573 | expect(request.get.called).to.be.true; 574 | expect(helpers.logr.called).to.false; 575 | expect(err).to.be.null; 576 | done(); 577 | }); 578 | }}); 579 | }); 580 | }); 581 | 582 | }); 583 | 584 | }); 585 | }); 586 | 587 | 588 | 589 | function MyError(code) { 590 | this.name = 'MyError'; 591 | this.code = code; 592 | this.message = 'nothing to say'; 593 | this.stack = (new Error()).stack; 594 | } 595 | MyError.prototype = new Error; -------------------------------------------------------------------------------- /tests/api/test_tags.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | 7 | var helpers = require('../../src/lib/helpers'); 8 | import tags from '../../src/lib/api/tags'; 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('Codeforces', function() { 13 | describe('#tags', function() { 14 | 15 | var eventStub = { 16 | on: function(eventName , data) { return this; }, 17 | pipe: function() { return this; } 18 | }; 19 | var spy; 20 | 21 | beforeEach(function(){ 22 | sinon.stub(process.stderr,'write'); 23 | sinon.stub(request, 'get').returns(eventStub); 24 | spy = sinon.spy(eventStub,'on'); 25 | }); 26 | 27 | afterEach(function(){ 28 | process.stderr.write.restore(); 29 | request.get.restore(); 30 | eventStub.on.restore(); 31 | }); 32 | 33 | it('should call request', function(done) { 34 | tags(); 35 | expect(request.get.called).to.be.true; 36 | done(); 37 | }); 38 | 39 | it('should call all 5 event', function(done) { 40 | tags(); 41 | expect(spy.callCount).to.equal(5); 42 | done(); 43 | }); 44 | }) 45 | }); 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/api/test_userinfo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-dinar/codeforces-cli/95e0bab4837c2832148ad6394ae2f39c21a94ab8/tests/api/test_userinfo.js -------------------------------------------------------------------------------- /tests/api/test_userrating.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import request from 'request'; 3 | import sinon from 'sinon'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | 7 | var helpers = require('../../src/lib/helpers'); 8 | import Userrating from '../../src/lib/api/Userrating'; 9 | 10 | chai.use(sinonChai); 11 | 12 | var mockResponse = { 13 | statusCode: 200, 14 | headers: { 15 | 'content-type': 'application/json;' 16 | } 17 | }; 18 | var mockBody = { 19 | status: 'OK', 20 | comment: '', 21 | result: [{ 22 | contestName: 'Roud test', 23 | rank: 1, 24 | newRating: 2000, 25 | oldRating: 1900 26 | }] 27 | }; 28 | 29 | describe('Codeforces', function() { 30 | describe('#Userrating', function() { 31 | describe('[core]', function() { 32 | 33 | it('should throw error when parameter is empty', function(done) { 34 | expect(function () { 35 | new Userrating().getRating(); 36 | }).to.throw(Error); 37 | done(); 38 | }); 39 | 40 | it('should throw error when handle is not a string', function(done) { 41 | 42 | expect(function () { 43 | new Userrating({}).getRating(); 44 | }).to.throw(Error); 45 | 46 | expect(function () { 47 | new Userrating(null).getRating(); 48 | }).to.throw(Error); 49 | 50 | expect(function () { 51 | new Userrating(undefined).getRating(); 52 | }).to.throw(Error); 53 | 54 | done(); 55 | }); 56 | }); 57 | 58 | describe('[onsuccess]', function() { 59 | describe('{No chart}', function() { 60 | 61 | var logCalled; 62 | var instanceOf; 63 | 64 | beforeEach(function(){ 65 | instanceOf = new Userrating('Ahmed_Dinar', true); 66 | logCalled = false; 67 | sinon.stub(helpers, 'log', function () { logCalled = true; }); 68 | sinon.stub(process.stderr,'write'); //disable spinner 69 | sinon 70 | .stub(request, 'get') 71 | .yields(null, mockResponse, mockBody); 72 | }); 73 | 74 | afterEach(function(){ 75 | helpers.log.restore(); 76 | process.stderr.write.restore(); 77 | request.get.restore(); 78 | }); 79 | 80 | it('should call console.log when response is successful', function(done){ 81 | expect(logCalled).to.be.false; 82 | instanceOf.getRating(); 83 | expect(logCalled).to.be.true; 84 | done(); 85 | }); 86 | 87 | it('should call request', function(done){ 88 | instanceOf.getRating(); 89 | expect(request.get.called).to.be.true; 90 | done(); 91 | }); 92 | }); 93 | describe('{Chart}', function() { 94 | 95 | var chartCalled; 96 | var instanceOf; 97 | 98 | beforeEach(function(){ 99 | instanceOf = new Userrating('Ahmed_Dinar'); 100 | chartCalled = false; 101 | sinon.stub(instanceOf, 'showLineChart', function () { 102 | chartCalled = true; 103 | }); 104 | sinon.stub(process.stderr,'write'); //disable spinner 105 | sinon 106 | .stub(request, 'get') 107 | .yields(null, mockResponse, mockBody); 108 | }); 109 | 110 | afterEach(function(){ 111 | instanceOf.showLineChart.restore(); 112 | process.stderr.write.restore(); 113 | request.get.restore(); 114 | }); 115 | 116 | it('should call request', function(done){ 117 | instanceOf.getRating(); 118 | expect(request.get.called).to.be.true; 119 | done(); 120 | }); 121 | 122 | it('should call showchart', function(done){ 123 | expect(chartCalled).to.be.false; 124 | instanceOf.getRating(); 125 | expect(chartCalled).to.be.true; 126 | done(); 127 | }); 128 | 129 | }); 130 | }); 131 | 132 | describe('[onerror]', function() { 133 | describe('{Request error}', function() { 134 | 135 | var logCalled; 136 | var instanceOf; 137 | 138 | beforeEach(function(){ 139 | instanceOf = new Userrating('Ahmed_Dinar', true); 140 | logCalled = false; 141 | sinon.stub(helpers, 'logr', function () { logCalled = true; }); 142 | sinon.stub(process.stderr,'write'); //disable spinner 143 | sinon 144 | .stub(request, 'get') 145 | .yields(new Error('Error'), mockResponse,mockBody); 146 | }); 147 | 148 | afterEach(function(){ 149 | helpers.logr.restore(); 150 | process.stderr.write.restore(); 151 | request.get.restore(); 152 | }); 153 | 154 | it('should call request', function(done){ 155 | instanceOf.getRating(); 156 | expect(request.get.called).to.be.true; 157 | done(); 158 | }); 159 | 160 | it('should call console.error when request failed', function(done){ 161 | expect(logCalled).to.be.false; 162 | instanceOf.getRating(); 163 | expect(logCalled).to.be.true; 164 | done(); 165 | }); 166 | }); 167 | 168 | describe('{HTTP error}', function() { 169 | 170 | var logCalled; 171 | var instanceOf; 172 | 173 | beforeEach(function(){ 174 | instanceOf = new Userrating('Ahmed_Dinar', true); 175 | logCalled = false; 176 | sinon.stub(helpers, 'logr', function () { logCalled = true; }); 177 | sinon.stub(process.stderr,'write'); //disable spinner 178 | mockResponse.statusCode = 404; 179 | sinon 180 | .stub(request, 'get') 181 | .yields(null, mockResponse,mockBody); 182 | }); 183 | 184 | afterEach(function(){ 185 | helpers.logr.restore(); 186 | process.stderr.write.restore(); 187 | request.get.restore(); 188 | mockResponse.statusCode = 200; 189 | }); 190 | 191 | it('should call request', function(done){ 192 | instanceOf.getRating(); 193 | expect(request.get.called).to.be.true; 194 | done(); 195 | }); 196 | 197 | it('should call console.error when HTTP response status not 200', function(done){ 198 | expect(logCalled).to.be.false; 199 | instanceOf.getRating(); 200 | expect(logCalled).to.be.true; 201 | done(); 202 | }); 203 | }); 204 | 205 | describe('{Invalid JSON}', function() { 206 | 207 | var logCalled; 208 | var instanceOf; 209 | 210 | beforeEach(function(){ 211 | instanceOf = new Userrating('Ahmed_Dinar', true); 212 | logCalled = false; 213 | sinon.stub(helpers, 'logr', function () { logCalled = true; }); 214 | sinon.stub(process.stderr,'write'); //disable spinner 215 | mockResponse.headers['content-type'] = 'html'; 216 | sinon 217 | .stub(request, 'get') 218 | .yields(null, mockResponse,mockBody); 219 | }); 220 | 221 | afterEach(function(){ 222 | helpers.logr.restore(); 223 | process.stderr.write.restore(); 224 | request.get.restore(); 225 | mockResponse.headers['content-type'] = 'application/json;'; 226 | }); 227 | 228 | it('should call request', function(done){ 229 | instanceOf.getRating(); 230 | expect(request.get.called).to.be.true; 231 | done(); 232 | }); 233 | 234 | it('should call console.error when content is not json', function(done){ 235 | expect(logCalled).to.be.false; 236 | instanceOf.getRating(); 237 | expect(logCalled).to.be.true; 238 | done(); 239 | }); 240 | }); 241 | 242 | describe('{API error}', function() { 243 | 244 | var logCalled; 245 | var instanceOf; 246 | 247 | beforeEach(function(){ 248 | instanceOf = new Userrating('Ahmed_Dinar', true); 249 | logCalled = false; 250 | sinon.stub(helpers, 'logr', function () { logCalled = true; }); 251 | sinon.stub(process.stderr,'write'); //disable spinner 252 | mockBody.status = 'FAILED'; 253 | sinon 254 | .stub(request, 'get') 255 | .yields(null, mockResponse,mockBody); 256 | }); 257 | 258 | afterEach(function(){ 259 | helpers.logr.restore(); 260 | process.stderr.write.restore(); 261 | request.get.restore(); 262 | mockBody.status = 'OK'; 263 | }); 264 | 265 | it('should call request', function(done){ 266 | instanceOf.getRating(); 267 | expect(request.get.called).to.be.true; 268 | done(); 269 | }); 270 | 271 | it('should call console.error when API is not OK', function(done){ 272 | expect(logCalled).to.be.false; 273 | instanceOf.getRating(); 274 | expect(logCalled).to.be.true; 275 | done(); 276 | }); 277 | }); 278 | }); 279 | }); 280 | }); 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /tests/api/test_usertags.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import usertags from '../../src/lib/api/usertags'; 3 | 4 | 5 | describe('Codeforces', function() { 6 | describe('#usertags', function() { 7 | 8 | it('should throw error when parameter is empty', function(done) { 9 | expect(function () { 10 | usertags(); 11 | }).to.throw(Error); 12 | done(); 13 | }); 14 | 15 | it('should throw error when handle is empty', function(done) { 16 | expect(function () { 17 | usertags({}); 18 | }).to.throw(Error); 19 | done(); 20 | }); 21 | 22 | it('should throw error when handle is not a string', function(done) { 23 | 24 | expect(function () { 25 | usertags({ handle: {} }); 26 | }).to.throw(Error); 27 | 28 | expect(function () { 29 | usertags({ handle: null }); 30 | }).to.throw(Error); 31 | 32 | expect(function () { 33 | usertags({ handle: undefined }); 34 | }).to.throw(Error); 35 | 36 | done(); 37 | }); 38 | 39 | }); 40 | }); 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/crawler/test_countrystandings.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | 7 | var helpers = require('../../src/lib/helpers'); 8 | import Countrystandings from '../../src/lib/crawler/Countrystandings'; 9 | 10 | chai.use(sinonChai); 11 | 12 | describe('Codeforces',function () { 13 | describe('#Countrystandings',function () { 14 | describe('.constructor()',function () { 15 | 16 | it('should throw error when no parameter', function (done) { 17 | expect(function () { 18 | new Countrystandings(); 19 | }).to.throw(Error); 20 | done(); 21 | }); 22 | 23 | it('should throw error when contestId not exists', function (done) { 24 | expect(function () { 25 | new Countrystandings({}); 26 | }).to.throw(Error); 27 | done(); 28 | }); 29 | 30 | it('should throw error when country is not integer', function (done) { 31 | expect(function () { 32 | new Countrystandings({ contestId: '10' }); 33 | }).to.throw(Error); 34 | done(); 35 | }); 36 | 37 | it('should throw error when country not exists', function (done) { 38 | expect(function () { 39 | new Countrystandings({ contestId: 10 }); 40 | }).to.throw(Error); 41 | done(); 42 | }); 43 | 44 | it('should not throw error', function (done) { 45 | expect(function () { 46 | new Countrystandings({ contestId: 10, country: 'Bangladesh' }); 47 | }).to.not.throw(Error); 48 | done(); 49 | }); 50 | 51 | }); 52 | describe('.show()',function () { 53 | describe('basic error',function () { 54 | 55 | this.timeout(20000); 56 | 57 | beforeEach(function () { 58 | sinon.stub(request, 'get').yields('requestError', { statusCode: 404 }, ''); 59 | sinon.stub(helpers,'log'); 60 | sinon.stub(helpers,'logr'); 61 | sinon.stub(process.stderr,'write'); 62 | }); 63 | 64 | afterEach(function () { 65 | request.get.restore(); 66 | helpers.logr.restore(); 67 | helpers.log.restore(); 68 | process.stderr.write.restore(); 69 | }); 70 | 71 | it('should return and match error when country invalid', function (done) { 72 | new Countrystandings({ contestId: 10, country: 'invalid', total: 1 }).show(); 73 | expect(request.get.called).to.be.false; 74 | expect(helpers.logr.called).to.be.true; 75 | done(); 76 | }); 77 | 78 | it('should return and match error when country invalid - callabck', function (done) { 79 | new Countrystandings({ contestId: 10, country: 'invalid', total: 1 }).show(function (err) { 80 | expect(request.get.called).to.be.false; 81 | expect(helpers.logr.called).to.be.false; 82 | expect(err).to.equal(`'invalid' not found in supported country list.Please run 'cf country' to see all supported countries.`); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | describe('request error',function () { 88 | 89 | this.timeout(20000); 90 | 91 | beforeEach(function () { 92 | sinon.stub(request, 'get').yields('requestError', { statusCode: 404 }, ''); 93 | sinon.stub(helpers,'log'); 94 | sinon.stub(helpers,'logr'); 95 | sinon.stub(process.stderr,'write'); 96 | }); 97 | 98 | afterEach(function () { 99 | request.get.restore(); 100 | helpers.logr.restore(); 101 | helpers.log.restore(); 102 | process.stderr.write.restore(); 103 | }); 104 | 105 | it('should return and match error when request error', function (done) { 106 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(function (err) { 107 | expect(request.get.called).to.be.true; 108 | expect(helpers.logr.called).to.be.false; 109 | expect(err).to.equal(`requestError`); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | describe('HTTP error',function () { 115 | 116 | this.timeout(20000); 117 | 118 | beforeEach(function () { 119 | sinon.stub(request, 'get').yields(null, { statusCode: 404 }, ''); 120 | sinon.stub(helpers,'log'); 121 | sinon.stub(helpers,'logr'); 122 | sinon.stub(process.stderr,'write'); 123 | }); 124 | 125 | afterEach(function () { 126 | request.get.restore(); 127 | helpers.logr.restore(); 128 | helpers.log.restore(); 129 | process.stderr.write.restore(); 130 | }); 131 | 132 | it('should return and match error when connection error', function (done) { 133 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(function (err) { 134 | expect(request.get.called).to.be.true; 135 | expect(helpers.logr.called).to.be.false; 136 | expect(err).to.equal(`HTTP failed with status 404`); 137 | done(); 138 | }); 139 | }); 140 | }); 141 | describe('error - no callback',function () { 142 | 143 | this.timeout(20000); 144 | 145 | beforeEach(function () { 146 | sinon.stub(request, 'get').yields(null, { statusCode: 404 }, ''); 147 | sinon.stub(helpers,'log'); 148 | sinon.stub(helpers,'logr'); 149 | sinon.stub(process.stderr,'write'); 150 | }); 151 | 152 | afterEach(function () { 153 | request.get.restore(); 154 | helpers.logr.restore(); 155 | helpers.log.restore(); 156 | process.stderr.write.restore(); 157 | }); 158 | 159 | it('should return and match error when connection error', function (done) { 160 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(); 161 | expect(request.get.called).to.be.true; 162 | expect(helpers.logr.called).to.be.true; 163 | done(); 164 | }); 165 | }); 166 | describe('no user',function () { 167 | 168 | this.timeout(20000); 169 | 170 | beforeEach(function () { 171 | sinon.stub(request, 'get').yields(null, { statusCode: 200 }, ''); 172 | sinon.stub(helpers,'log'); 173 | sinon.stub(helpers,'logr'); 174 | sinon.stub(process.stderr,'write'); 175 | }); 176 | 177 | afterEach(function () { 178 | request.get.restore(); 179 | helpers.logr.restore(); 180 | helpers.log.restore(); 181 | process.stderr.write.restore(); 182 | }); 183 | 184 | it('should request call 5 times', function (done) { 185 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(function (err) { 186 | expect(request.get.called).to.be.true; 187 | expect(request.get.callCount).to.equal(2); 188 | expect(err).to.be.null; 189 | done(); 190 | }); 191 | }); 192 | }); 193 | describe('no user - log',function () { 194 | 195 | this.timeout(20000); 196 | 197 | beforeEach(function () { 198 | sinon.stub(request, 'get').yields(null, { statusCode: 200 }, ''); 199 | sinon.stub(helpers,'log'); 200 | sinon.stub(helpers,'logr'); 201 | sinon.stub(process.stderr,'write'); 202 | }); 203 | 204 | afterEach(function () { 205 | request.get.restore(); 206 | helpers.logr.restore(); 207 | helpers.log.restore(); 208 | process.stderr.write.restore(); 209 | }); 210 | 211 | it('should request call 5 times and call log', function (done) { 212 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(); 213 | expect(request.get.called).to.be.true; 214 | expect(request.get.callCount).to.equal(2); 215 | expect(helpers.log.called).to.be.true; 216 | done(); 217 | }); 218 | }); 219 | describe('user in 2 page',function () { 220 | 221 | this.timeout(20000); 222 | 223 | var pageIndx = ``; 224 | var page1Data = `
${pageIndx}`; 225 | var stubReq; 226 | 227 | beforeEach(function () { 228 | stubReq = sinon.stub(request, 'get'); 229 | stubReq.onCall(0).yields(null, {statusCode: 200}, page1Data); 230 | stubReq.onCall(1).yields(null, {statusCode: 200}, page1Data); 231 | stubReq.onCall(2).yields('more than 2 times error', {statusCode: 200}, ''); 232 | sinon.stub(helpers,'log'); 233 | sinon.stub(helpers,'logr'); 234 | sinon.stub(process.stderr,'write'); 235 | }); 236 | 237 | afterEach(function () { 238 | stubReq.restore(); 239 | helpers.logr.restore(); 240 | helpers.log.restore(); 241 | process.stderr.write.restore(); 242 | }); 243 | 244 | it('should not return error and call 2 times', function (done) { 245 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 2 }).show(function (err) { 246 | expect(request.get.called).to.be.true; 247 | expect(request.get.callCount).to.equal(2); 248 | expect(err).to.be.null; 249 | done(); 250 | }); 251 | }); 252 | }); 253 | describe('show table',function () { 254 | 255 | this.timeout(20000); 256 | 257 | var pageIndx = ``; 258 | var page1Data = `
${pageIndx}`; 259 | var stubReq; 260 | 261 | beforeEach(function () { 262 | stubReq = sinon.stub(request, 'get'); 263 | stubReq.onCall(0).yields(null, {statusCode: 200}, page1Data); 264 | stubReq.onCall(1).yields('more than 1 times error', {statusCode: 200}, ''); 265 | sinon.stub(helpers,'log'); 266 | sinon.stub(helpers,'logr'); 267 | sinon.stub(process.stderr,'write'); 268 | }); 269 | 270 | afterEach(function () { 271 | stubReq.restore(); 272 | helpers.logr.restore(); 273 | helpers.log.restore(); 274 | process.stderr.write.restore(); 275 | }); 276 | 277 | it('should not return error and call 2 times', function (done) { 278 | new Countrystandings({ contestId: 10, country: 'Bangladesh', total: 1 }).show(); 279 | expect(request.get.called).to.be.true; 280 | expect(request.get.callCount).to.equal(1); 281 | expect(helpers.log.called).to.be.true; 282 | done(); 283 | }); 284 | }); 285 | }); 286 | }); 287 | }); -------------------------------------------------------------------------------- /tests/crawler/test_ratings.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | 7 | var helpers = require('../../src/lib/helpers'); 8 | import Ratings from '../../src/lib/crawler/Ratings'; 9 | 10 | chai.use(sinonChai); 11 | 12 | var sampleData = `
(181)nfssdq1322435
(181)nfssdq1322435
385342252
`; 13 | 14 | describe('Codeforces',function () { 15 | describe('#Ratings',function () { 16 | describe('.constructor()',function () { 17 | 18 | it('should throw error when no parameter', function (done) { 19 | expect(function () { 20 | new Ratings(); 21 | }).to.throw(Error); 22 | done(); 23 | }); 24 | 25 | it('should throw error when country is not string', function (done) { 26 | expect(function () { 27 | new Ratings({ country: {} }); 28 | }).to.throw(Error); 29 | expect(function () { 30 | new Ratings({ country: 123 }); 31 | }).to.throw(Error); 32 | done(); 33 | }); 34 | 35 | it('should not throw error', function (done) { 36 | expect(function () { 37 | new Ratings({ country: 'Bangladesh' }); 38 | }).to.not.throw(Error); 39 | done(); 40 | }); 41 | 42 | }); 43 | describe('.show()',function () { 44 | describe('[basic error]',function () { 45 | 46 | beforeEach(function () { 47 | sinon.stub(request, 'get').yields('requestError', { statusCode: 404 }, ''); 48 | sinon.stub(helpers,'log'); 49 | sinon.stub(helpers,'logr'); 50 | sinon.stub(process.stderr,'write'); 51 | }); 52 | 53 | afterEach(function () { 54 | request.get.restore(); 55 | helpers.logr.restore(); 56 | helpers.log.restore(); 57 | process.stderr.write.restore(); 58 | }); 59 | 60 | it('should return and match error when country invalid', function (done) { 61 | new Ratings({ country: 'invlid' }).show(); 62 | expect(request.get.called).to.be.false; 63 | expect(helpers.logr.called).to.be.true; 64 | done(); 65 | }); 66 | 67 | it('should return and match error when country invalid - callback', function (done) { 68 | new Ratings({ country: 'invlid' }).show(function (err,result) { 69 | expect(request.get.called).to.be.false; 70 | expect(helpers.logr.called).to.be.false; 71 | expect(err).to.equal(`Invalid country 'invlid'.Please run 'cf country' to see supported country list.`); 72 | done(); 73 | }); 74 | }); 75 | 76 | }); 77 | describe('[request error]',function () { 78 | 79 | var instnecRating; 80 | beforeEach(function () { 81 | instnecRating = new Ratings({ country: 'Bangladesh' }); 82 | sinon.stub(instnecRating,'getOrg').yields('orgError'); 83 | sinon.stub(request, 'get').yields('requestError', { statusCode: 404 }, ''); 84 | sinon.stub(helpers,'log'); 85 | sinon.stub(helpers,'logr'); 86 | sinon.stub(process.stderr,'write'); 87 | }); 88 | 89 | afterEach(function () { 90 | instnecRating.getOrg.restore(); 91 | request.get.restore(); 92 | helpers.logr.restore(); 93 | helpers.log.restore(); 94 | process.stderr.write.restore(); 95 | }); 96 | 97 | it('should return and match error when request error', function (done) { 98 | instnecRating.show(); 99 | expect(request.get.called).to.be.true; 100 | expect(helpers.logr.called).to.be.true; 101 | expect(instnecRating.getOrg.called).to.be.false; 102 | done(); 103 | }); 104 | 105 | it('should return and match error when request error - callback', function (done) { 106 | instnecRating.show(function (err,result) { 107 | expect(request.get.called).to.be.true; 108 | expect(helpers.logr.called).to.be.false; 109 | expect(err).to.equal('requestError'); 110 | expect(instnecRating.getOrg.called).to.be.false; 111 | done(); 112 | }); 113 | }); 114 | 115 | }); 116 | describe('[HTTP error]',function () { 117 | 118 | var instnecRating; 119 | beforeEach(function () { 120 | instnecRating = new Ratings({ country: 'Bangladesh' }); 121 | sinon.stub(instnecRating,'getOrg').yields('orgError'); 122 | sinon.stub(request, 'get').yields(null, { statusCode: 404 }, ''); 123 | sinon.stub(helpers,'log'); 124 | sinon.stub(helpers,'logr'); 125 | sinon.stub(process.stderr,'write'); 126 | }); 127 | 128 | afterEach(function () { 129 | instnecRating.getOrg.restore(); 130 | request.get.restore(); 131 | helpers.logr.restore(); 132 | helpers.log.restore(); 133 | process.stderr.write.restore(); 134 | }); 135 | 136 | it('should return and match error when request error', function (done) { 137 | instnecRating.show(); 138 | expect(request.get.called).to.be.true; 139 | expect(helpers.logr.called).to.be.true; 140 | expect(instnecRating.getOrg.called).to.be.false; 141 | done(); 142 | }); 143 | 144 | it('should return and match error when request error - callback', function (done) { 145 | instnecRating.show(function (err,result) { 146 | expect(request.get.called).to.be.true; 147 | expect(helpers.logr.called).to.be.false; 148 | expect(err).to.equal('HTTP failed with status 404'); 149 | expect(instnecRating.getOrg.called).to.be.false; 150 | done(); 151 | }); 152 | }); 153 | 154 | }); 155 | describe('[success]',function () { 156 | 157 | var instnecRating; 158 | beforeEach(function () { 159 | instnecRating = new Ratings({ country: 'Bangladesh' }); 160 | sinon.stub(instnecRating,'getOrg').yields('orgError'); 161 | sinon.stub(request, 'get').yields(null, { statusCode: 200 }, sampleData); 162 | sinon.stub(helpers,'log'); 163 | sinon.stub(helpers,'logr'); 164 | sinon.stub(process.stderr,'write'); 165 | }); 166 | 167 | afterEach(function () { 168 | instnecRating.getOrg.restore(); 169 | request.get.restore(); 170 | helpers.logr.restore(); 171 | helpers.log.restore(); 172 | process.stderr.write.restore(); 173 | }); 174 | 175 | it('should not have error', function (done) { 176 | instnecRating.show(); 177 | expect(request.get.called).to.be.true; 178 | expect(helpers.logr.called).to.be.false; 179 | expect(helpers.log.called).to.be.true; 180 | expect(instnecRating.getOrg.called).to.be.false; 181 | done(); 182 | }); 183 | 184 | it('should not have error - callback', function (done) { 185 | instnecRating.show(function (err,result) { 186 | expect(request.get.called).to.be.true; 187 | expect(helpers.logr.called).to.be.false; 188 | expect(helpers.log.called).to.be.false; 189 | expect(err).to.equal(null); 190 | expect(instnecRating.getOrg.called).to.be.false; 191 | done(); 192 | }); 193 | }); 194 | 195 | }); 196 | }); 197 | describe('.getOrg()',function () { 198 | 199 | describe('[req error]',function () { 200 | 201 | var stubReq; 202 | beforeEach(function () { 203 | stubReq = sinon.stub(request, 'get'); 204 | stubReq.onCall(0).yields(null, { statusCode: 200 }, sampleData); 205 | stubReq.onCall(1).yields('orgReqError', { statusCode: 404 }, ''); 206 | sinon.stub(helpers,'log'); 207 | sinon.stub(helpers,'logr'); 208 | sinon.stub(process.stderr,'write'); 209 | }); 210 | 211 | afterEach(function () { 212 | stubReq.restore(); 213 | helpers.logr.restore(); 214 | helpers.log.restore(); 215 | process.stderr.write.restore(); 216 | }); 217 | 218 | it('should have error', function (done) { 219 | new Ratings({ country: 'Bangladesh', org: true }).show(); 220 | expect(request.get.called).to.be.true; 221 | expect(request.get.callCount).to.equal(2); 222 | expect(helpers.logr.called).to.be.true; 223 | expect(helpers.log.called).to.be.false; 224 | done(); 225 | }); 226 | 227 | it('should have error - callback', function (done) { 228 | new Ratings({ country: 'Bangladesh', org: true }).show(function (err,result) { 229 | expect(request.get.called).to.be.true; 230 | expect(helpers.logr.called).to.be.false; 231 | expect(helpers.log.called).to.be.false; 232 | expect(err).to.equal('orgReqError'); 233 | done(); 234 | }); 235 | }); 236 | }); 237 | describe('[HTTP error]',function () { 238 | 239 | var stubReq; 240 | beforeEach(function () { 241 | stubReq = sinon.stub(request, 'get'); 242 | stubReq.onCall(0).yields(null, { statusCode: 200 }, sampleData); 243 | stubReq.onCall(1).yields(null, { statusCode: 404 }, ''); 244 | sinon.stub(helpers,'log'); 245 | sinon.stub(helpers,'logr'); 246 | sinon.stub(process.stderr,'write'); 247 | }); 248 | 249 | afterEach(function () { 250 | stubReq.restore(); 251 | helpers.logr.restore(); 252 | helpers.log.restore(); 253 | process.stderr.write.restore(); 254 | }); 255 | 256 | it('should have error', function (done) { 257 | new Ratings({ country: 'Bangladesh', org: true }).show(); 258 | expect(request.get.called).to.be.true; 259 | expect(request.get.callCount).to.equal(2); 260 | expect(helpers.logr.called).to.be.true; 261 | expect(helpers.log.called).to.be.false; 262 | done(); 263 | }); 264 | 265 | it('should have error - callback', function (done) { 266 | new Ratings({ country: 'Bangladesh', org: true }).show(function (err,result) { 267 | expect(request.get.called).to.be.true; 268 | expect(helpers.logr.called).to.be.false; 269 | expect(helpers.log.called).to.be.false; 270 | expect(err).to.equal('HTTP failed with status 404'); 271 | done(); 272 | }); 273 | }); 274 | }); 275 | describe('[HTTP error - API]',function () { 276 | 277 | var stubReq; 278 | beforeEach(function () { 279 | stubReq = sinon.stub(request, 'get'); 280 | stubReq.onCall(0).yields(null, { statusCode: 200 }, sampleData); 281 | stubReq.onCall(1).yields(null, { statusCode: 404 }, { comment: 'API error' }); 282 | sinon.stub(helpers,'log'); 283 | sinon.stub(helpers,'logr'); 284 | sinon.stub(process.stderr,'write'); 285 | }); 286 | 287 | afterEach(function () { 288 | stubReq.restore(); 289 | helpers.logr.restore(); 290 | helpers.log.restore(); 291 | process.stderr.write.restore(); 292 | }); 293 | 294 | it('should have error', function (done) { 295 | new Ratings({ country: 'Bangladesh', org: true }).show(); 296 | expect(request.get.called).to.be.true; 297 | expect(request.get.callCount).to.equal(2); 298 | expect(helpers.logr.called).to.be.true; 299 | expect(helpers.log.called).to.be.false; 300 | done(); 301 | }); 302 | 303 | it('should have error - callback', function (done) { 304 | new Ratings({ country: 'Bangladesh', org: true }).show(function (err,result) { 305 | expect(request.get.called).to.be.true; 306 | expect(helpers.logr.called).to.be.false; 307 | expect(helpers.log.called).to.be.false; 308 | expect(err).to.equal('API error'); 309 | done(); 310 | }); 311 | }); 312 | }); 313 | describe('[API error]',function () { 314 | 315 | var stubReq; 316 | beforeEach(function () { 317 | stubReq = sinon.stub(request, 'get'); 318 | stubReq.onCall(0).yields(null, { statusCode: 200 }, sampleData); 319 | stubReq.onCall(1).yields(null, { statusCode: 200 }, { status: 'FAILED', comment: 'API errors', result: {} }); 320 | sinon.stub(helpers,'log'); 321 | sinon.stub(helpers,'logr'); 322 | sinon.stub(process.stderr,'write'); 323 | }); 324 | 325 | afterEach(function () { 326 | stubReq.restore(); 327 | helpers.logr.restore(); 328 | helpers.log.restore(); 329 | process.stderr.write.restore(); 330 | }); 331 | 332 | it('should have error', function (done) { 333 | new Ratings({ country: 'Bangladesh', org: true }).show(); 334 | expect(request.get.called).to.be.true; 335 | expect(request.get.callCount).to.equal(2); 336 | expect(helpers.logr.called).to.be.true; 337 | expect(helpers.log.called).to.be.false; 338 | done(); 339 | }); 340 | 341 | it('should have error - callback', function (done) { 342 | new Ratings({ country: 'Bangladesh', org: true }).show(function (err,result) { 343 | expect(request.get.called).to.be.true; 344 | expect(request.get.callCount).to.equal(2); 345 | expect(helpers.logr.called).to.be.false; 346 | expect(helpers.log.called).to.be.false; 347 | expect(err).to.equal('API errors'); 348 | done(); 349 | }); 350 | }); 351 | }); 352 | describe('[successs]',function () { 353 | 354 | var stubReq; 355 | beforeEach(function () { 356 | stubReq = sinon.stub(request, 'get'); 357 | stubReq.onCall(0).yields(null, { statusCode: 200 }, sampleData); 358 | stubReq.onCall(1).yields(null, { statusCode: 200 }, { status: 'OK', result: [ { nfssdq: { organization: 'JU'} } ] }); 359 | sinon.stub(helpers,'log'); 360 | sinon.stub(helpers,'logr'); 361 | sinon.stub(process.stderr,'write'); 362 | }); 363 | 364 | afterEach(function () { 365 | stubReq.restore(); 366 | helpers.logr.restore(); 367 | helpers.log.restore(); 368 | process.stderr.write.restore(); 369 | }); 370 | 371 | it('should have error', function (done) { 372 | new Ratings({ country: 'Bangladesh', org: true }).show(); 373 | expect(request.get.called).to.be.true; 374 | expect(request.get.callCount).to.equal(2); 375 | expect(helpers.logr.called).to.be.false; 376 | expect(helpers.log.called).to.be.true; 377 | done(); 378 | }); 379 | 380 | it('should have error - callback', function (done) { 381 | new Ratings({ country: 'Bangladesh', org: true }).show(function (err,result) { 382 | expect(request.get.called).to.be.true; 383 | expect(request.get.callCount).to.equal(2); 384 | expect(helpers.logr.called).to.be.false; 385 | expect(helpers.log.called).to.be.false; 386 | expect(err).to.equal(null); 387 | done(); 388 | }); 389 | }); 390 | }); 391 | 392 | }); 393 | }); 394 | }); -------------------------------------------------------------------------------- /tests/crawler/test_sourcecode.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-dinar/codeforces-cli/95e0bab4837c2832148ad6394ae2f39c21a94ab8/tests/crawler/test_sourcecode.js -------------------------------------------------------------------------------- /tests/helpers/submission_helper.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import request from 'request'; 4 | var chai = require('chai'); 5 | var sinonChai = require('sinon-chai'); 6 | import jsonfile from 'jsonfile'; 7 | import inquirer from 'inquirer'; 8 | var Promise = require("bluebird"); 9 | 10 | var helpers = require('../../src/lib/helpers'); 11 | 12 | chai.use(sinonChai); 13 | 14 | module.exports = function(readF,promptF,getF){ 15 | beforeEach(function(){ 16 | this.s1 = sinon.stub(jsonfile, 'readFile').yields(readF.err, readF.obj); 17 | this.s2 = sinon.stub(jsonfile, 'writeFileSync'); 18 | this.s3 = sinon.stub(inquirer, 'prompt').returns( Promise.resolve(promptF) ); 19 | if(getF.ignore) { 20 | getF = getF.data; 21 | this.ignore = true; 22 | this.s4 = sinon.stub(request, 'get'); 23 | this.s4.onCall(0).yields(getF[0].err, getF[0].res, getF[0].body); 24 | this.s4.onCall(1).yields(getF[1].err, getF[1].res, getF[1].body); 25 | }else{ 26 | this.ignore = false; 27 | this.s4 = sinon.stub(request, 'get').yields(getF.err, getF.res, getF.body); 28 | } 29 | this.s5 = sinon.stub(process.stderr,'write'); 30 | this.s6 = sinon.stub(helpers,'logr'); 31 | this.s7 = sinon.stub(helpers,'log'); 32 | this.s8 = sinon.stub(helpers,'clear'); 33 | }); 34 | afterEach(function(){ 35 | this.s1.restore(); 36 | this.s2.restore(); 37 | this.s3.restore(); 38 | this.s4.restore(); 39 | this.s5.restore(); 40 | this.s6.restore(); 41 | this.s7.restore(); 42 | this.s8.restore(); 43 | }); 44 | }; 45 | 46 | --------------------------------------------------------------------------------