diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a05749c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true diff --git a/handler.js b/handler.js index 07fe301..f20ec95 100644 --- a/handler.js +++ b/handler.js @@ -2,6 +2,8 @@ const scores = require("./lib/scores") const activities = require("./lib/activities") const status = require("./lib/status") +const submissions = require("./lib/submissions"); +const problems = require("./lib/problems"); function db_client() { let pg = require('pg'); @@ -51,3 +53,25 @@ completed for the first time since the last time it was checked. module.exports.activities_compute = (event, context, callback) => { return activities.compute(db_client(), callback); }; + +/** + * Gather all the submissions of a given user + */ +module.exports.submissions_user = (event, context, callback) => { + return submissions.user(db_client(), event, callback); +}; + +/** + * Gather all the problems + */ +module.exports.problems_all = (event, context, callback) => { + return problems.all(db_client(), callback); +}; + +/** + * Update UVa problems + */ +module.exports.uva_update_problems = (event, context, callback) => { + require('./lib/platforms/uva.js').update_problems(db_client()); + callback(); +}; diff --git a/lib/platforms/uva.js b/lib/platforms/uva.js new file mode 100644 index 0000000..9237beb --- /dev/null +++ b/lib/platforms/uva.js @@ -0,0 +1,174 @@ +//================= +//==== UVA API ==== +//================= + +/** + * This file includes all the logic behind the UVa/uHunt part of the API. + */ + +'use strict'; + +const request = require('request'); +const sprintf = require('sprintf-js').sprintf; +const _ = require('lodash'); + +const UVA_USERNAME_TO_ID = 'https://uhunt.onlinejudge.org/api/uname2uid/%s'; +const UVA_USER_SUBMISSIONS = 'http://uhunt.onlinejudge.org/api/subs-user/%s'; +const UVA_PROBLEMS = 'http://uhunt.onlinejudge.org/api/p'; + +/** + * Make sure the user has the correct user id in the database. + */ +function update_user_id(db, uva_username, callback) { + // uHunt query + request(sprintf(UVA_USERNAME_TO_ID, uva_username), (err, res, body) => { + if(err) throw err; + + // Check for user in the db + db.query('SELECT * FROM uva_users WHERE uva_username = $1 LIMIT 1;', [uva_username], (err, result) => { + if(err) throw err; + var user = result.rows[0]; + // If the user is not in db, add it + if(!user) { + // Don't add non existing users + if(body == 0) + return; + + db.query('INSERT INTO uva_users (uva_id, uva_username) VALUES ($1::integer, $2) RETURNING *;', [body, uva_username], (err, result) => { + if(err) throw err; + callback(db, result.rows); + }); + } + // If the user changed name, remove the submissions and update the user in the db + else if(body != user.uva_id) { + db.query("BEGIN;"); + db.query("DELETE FROM submissions WHERE platform = 'UVa' AND username = $1;", [user.uva_username], (err) => { + if(err) throw err; + }); + // Update if the user still has a valid ID + if(body != 0) + db.query("UPDATE uva_users SET uva_id = $1::integer WHERE uva_username = $2;", [body, user.uva_username], (err) => { + if(err) throw err; + }); + // Remove if the user doesn't exist anymore + else + db.query("DELETE FROM uva_users WHERE uva_username = $1;", [user.uva_username], (err) => { + if(err) throw err; + }); + + db.query("COMMIT;", (err) => { + if(err) throw err; + + // Only fetch the submissions if the user actually exists + if(body != 0) + callback(db, body); + }); + } + else + callback(db, user); + }); + }); +} + +/** + * Update the submissions for user + */ +function update_user_submissions(db, user) { + request(sprintf(UVA_USER_SUBMISSIONS, user.uva_id), (err, res, body) => { + if(err) throw err; + + /* + * Possible improvement: only ask for new submissions (with id > than the last one in the db) + * Drawbacks: more complicated and may cause missing data issues + * + * Also, doing all the updates in one query might actually be better. + */ + + var data = JSON.parse(body).subs; + _.forEach(data, (sub) => { + db.query( + // SQL Query + 'INSERT INTO submissions VALUES\n' + + '(\'UVa\', $1::integer, $2, $3, $4, $5, $6::integer, $7::bigint)\n' + + 'ON CONFLICT(platform, platform_submission_id) DO UPDATE SET\n' + + 'verdict = EXCLUDED.verdict,\n' + + 'runtime = EXCLUDED.runtime;\n', + // Parameters + [sub[0], sub[1], user.uva_username, get_verdict_text(sub[2]), get_language_text(sub[5]),sub[3], sub[4]], + // Callback + (err) => { + if(err) throw err; + }); + }); + }); +} + +/** + * Get the submissions for user with username + */ +function get_user_submissions(db, uva_username) { + return new Promise((resolve, reject) => { + db.query('SELECT * FROM submissions WHERE platform = \'UVa\' AND username = $1;', [uva_username], (err, result) => { + if(err) throw err; // Should this reject ? + + resolve(result.rows); + update_user_id(db, uva_username, update_user_submissions); + }); + }); +} + +/** + * Update the stored problems + */ +function update_problems(db) { + request(UVA_PROBLEMS, (err, res, body) => { + if(err) throw err; + + var data = JSON.parse(body); + _.forEach(data, (p) => { + db.query('INSERT INTO problems VALUES (\'UVa\', $1, $2, $3);', [p[0], p[1], p[2]], (err) => { + if(err) throw err; + }); + }); + }); +} + +module.exports = { + get_user_submissions: get_user_submissions, + update_problems: update_problems, +}; + +//============================= +//==== UTILITARY FUNCTIONS ==== +//============================= +var verdicts = { + 10: 'Submission error', + 15: 'Can\'t be judged', + 20: 'In judge queue', + 30: 'Compile error', + 35: 'Restricted function', + 40: 'Runtime error', + 45: 'Output limit exceeded', + 50: 'Time limit exceeded', + 60: 'Memory limit exceeded', + 70: 'Wrong answer', + 80: 'Presentation error', + 90: 'Accepted', +}; + +function get_verdict_text(verdict) { + return verdicts[verdict] || 'In judge queue'; +} + +var languages = { + 1: 'C', + 2: 'Java', + 3: 'C++', + 4: 'Pascal', + 5: 'C++', // C++11 + 6: 'Python', // Python 3 +}; + +function get_language_text(language) { + return languages[language]; +} diff --git a/lib/problems.js b/lib/problems.js new file mode 100644 index 0000000..a0e58d0 --- /dev/null +++ b/lib/problems.js @@ -0,0 +1,20 @@ +var _ = require('lodash'); + +function all(db, callback) { + db.query('SELECT * FROM problems;', (err, result) => { + if(err) throw err; + + const res = { + statusCode: 200, + body: { + problems: _.map(result.rows, p => [p.platform, p.platform_problem_id, p.platform_extra_data, p.title]), + }, + }; + callback(null, res); + }); +} + + +module.exports = { + all, +}; diff --git a/lib/submissions.js b/lib/submissions.js new file mode 100644 index 0000000..58c6969 --- /dev/null +++ b/lib/submissions.js @@ -0,0 +1,40 @@ +'use strict'; + +var _ = require('lodash'); +var uva_submissions = require('./platforms/uva').get_user_submissions; + +function user(db, event, callback) { + var ret = []; + + var user = {}; + + // Submission requests for all supported platforms + var arr = []; + + // Allow custom id for better user identification + if(event.id) { + user.id = event.id; + } + // UVa + if(event.uva) { + user.uva = event.uva; + arr.push(uva_submissions(db, event.uva)); + } + + // Return them back to the client + Promise.all(arr).then(subs => { + const res = { + statusCode: 200, + body: { + user: user, + submissions: _.flatten(subs).map(s => [s.platform, s.platform_submission_id, s.platform_problem_id, s.username, s.verdict, s.language, s.runtime, s.time]), + }, + }; + callback(null, res); + }); +} + + +module.exports = { + user, +}; diff --git a/migrations/20170520181147-uva-users.js b/migrations/20170520181147-uva-users.js new file mode 100644 index 0000000..6f2dbd6 --- /dev/null +++ b/migrations/20170520181147-uva-users.js @@ -0,0 +1,65 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function(db) { + + return db.createTable('uva_users', { + uva_user_id: { type: 'serial', primaryKey: true }, + uva_id: { type: 'int' }, + uva_username: { type: 'string', length: 50, notNull: true, unique: true }, + }).then( + function(result) { + db.runSql( + 'CREATE TABLE submissions (\n' + + 'platform varchar(50),\n' + + 'platform_submission_id integer,\n' + + 'platform_problem_id varchar(50) NOT NULL,\n' + + 'username text NOT NULL,\n' + + 'verdict varchar(50) NOT NULL,\n' + + 'language varchar(50) NOT NULL,\n' + + 'runtime integer,\n' + + 'time bigint,\n' + + 'primary key (platform, platform_submission_id)\n' + + ');\n' + ); + } + ).then( + function(result) { + db.runSql( + 'CREATE TABLE problems (\n' + + 'platform varchar(50),\n' + + 'platform_problem_id varchar(50),\n' + + 'platform_extra_data varchar,\n' + + 'title varchar NOT NULL,\n' + + 'primary key (platform, platform_problem_id)\n' + + ');\n' + ); + } + ); +}; + +exports.down = function(db) { + return db.dropTable('uva_users'). + then( function(result) { + db.dropTable('submissions'); + }).then( function(result) { + db.dropTable('problems'); + }); +}; + +exports._meta = { + "version": 1 +}; diff --git a/package.json b/package.json index 1f1f230..1309096 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,13 @@ "db-migrate": "^0.10.0-beta.20", "db-migrate-pg": "^0.1.11", "pg": "^6.1.4", - "serverless-secrets-plugin": "0.0.1" + "serverless-secrets-plugin": "0.0.1", + "request": "*", + "lodash": "*", + "sprintf-js": "*" }, "devDependencies": { - "serverless": "^1.10.0" + "serverless": "^1.10.0", + "serverless-mocha-plugin": "^1.3.5" } } diff --git a/serverless.yml b/serverless.yml index 0ad0c78..fdf3f44 100644 --- a/serverless.yml +++ b/serverless.yml @@ -3,6 +3,9 @@ service: beoi-api frameworkVersion: ">=1.10.0 <1.11.0" +plugins: + - serverless-mocha-plugin # Might be useful + provider: name: aws runtime: nodejs6.10 @@ -92,3 +95,33 @@ functions: method: POST cors: true - schedule: rate(1 hour) + + submissions_user: + handler: handler.submissions_user + vpc: ${self:custom.dbvpc} + events: + - http: + path: submissions/user + method: GET + cors: true + request: + application/json: > + { + "id": "$input.params('id')", + "uva": "$input.params('uva')" + } + + problems_all: + handler: handler.problems_all + vpc: ${self:custom.dbvpc} + events: + - http: + path: problems + method: GET + cors: true + + uva_update_problems: + handler: handler.uva_update_problems + vpc: ${self:custom.dbvpc} + events: + - schedule: rate(1 hour)