commit d5b1f623f827e53a2a43c2c840bad94edbc4c5cb Author: masnn Date: Wed Apr 1 18:13:04 2020 +0800 module.add(user,problem,record) diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..6aa2dae5d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "env": { + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "warn", + 4 + ], + "linebreak-style": [ + "warn", + "unix" + ], + "quotes": [ + "warn", + "single" + ], + "semi": [ + "warn", + "always" + ], + "no-unused-vars": "warn", + "no-unreachable": "warn", + "require-atomic-updates": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4d4681f1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +config.yaml \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..4f2074466 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.autoSave": "onFocusChange", + "files.encoding": "utf8", + "files.autoGuessEncoding": false, + "editor.detectIndentation": false, + "editor.formatOnSave": false +} \ No newline at end of file diff --git a/hydro/development.js b/hydro/development.js new file mode 100644 index 000000000..e88733392 --- /dev/null +++ b/hydro/development.js @@ -0,0 +1,41 @@ +process.stdin.setEncoding('utf8'); +process.stdin.on('data', async input => { + try { + let t = eval(input.toString().trim()); + if (t instanceof Promise) console.log(await t); + else console.log(t); + } catch (e) { + console.warn(e); + } +}); +process.on('restart', async () => { + console.log('Signal detected, restarting...'); + await global.Hydro.stop(); + await global.Hydro.destory(); + delete global.Hydro; + delete require.cache; + run(); +}); +const EventEmitter = require('events'); +global.bus = new EventEmitter(); +async function run() { + require('./service/db'); + await new Promise((resolve) => { + global.bus.once('connected', () => { + console.log('Database connected'); + resolve(); + }); + }); + let server = require('./service/server'); + require('./handler/base'); + require('./handler/home'); + require('./handler/user'); + require('./handler/problem'); + require('./handler/record'); + require('./handler/judge'); + server.start(); +} +run().catch(e => { + console.error(e); + process.exit(1); +}); \ No newline at end of file diff --git a/hydro/error.js b/hydro/error.js new file mode 100644 index 000000000..46854397d --- /dev/null +++ b/hydro/error.js @@ -0,0 +1,419 @@ +const { PRIV_USER_PROFILE } = require('./privilege'); + +class UserFacingError extends Error { + constructor(type) { + super(type); + this.template = 'error'; + this.code = 500; + //this.stack = ''; + this.params = []; + } +} +class BadRequestError extends UserFacingError { + constructor(type) { + super(type); + this.code = 400; + } +} +class ForbiddenError extends UserFacingError { + constructor(type) { + super(type); + this.code = 403; + } +} +class NotFoundError extends UserFacingError { + constructor(type) { + super(type); + this.code = 404; + } +} + +class LoginError extends ForbiddenError { + constructor(uname) { + super('LoginError'); + this.params = [uname]; + } +} +class UserAlreadyExistError extends ForbiddenError { + constructor(uname) { + super('UserAlreadyExistError'); + this.params = [uname]; + } +} +class InvalidTokenError extends ForbiddenError { + constructor(token) { + super('InvalidTokenError'); + this.params = [token]; + } +} +class UserNotFoundError extends NotFoundError { + constructor(user) { + super('UserNotFoundError'); + this.params = [user]; + } +} +class NoProblemError extends NotFoundError { + constructor() { + super('NoProblemError'); + } +} +class VerifyPasswordError extends ForbiddenError { + constructor() { + super('VerifyPasswordError'); + } +} +class PrivilegeError extends ForbiddenError { + constructor(priv) { + super('PrivilegeError'); + this.params = [priv]; + } +} +class OpcountExceededError extends ForbiddenError { + constructor(op, period_secs, max_operations) { + super('OpcountExceededError'); + this.params = [op, period_secs, max_operations]; + } +} + +class DomainAlreadyExistError extends ForbiddenError { + constructor(domain) { + super('DomainAlreadyExistError'); + this.params = [domain]; + } +} +class DomainNotFoundError extends ForbiddenError { + constructor(domain) { + super('DomainNotFoundError'); + this.params = [domain]; + } +} + +class UserAlreadyDomainMemberError extends ForbiddenError { + constructor(domainId, uid) { + super('UserAlreadyDomainMemberError'); + this.params = [domainId, uid]; + } +} +class PermissionError extends ForbiddenError { + constructor(perm) { + super('PermissionError'); + this.params = [perm]; + } +} +class ValidationError extends ForbiddenError { + constructor(field0, field1) { + super('ValidationError'); + if (!field1) this.params = [field0]; + else this.params = [field0, field1]; + } +} +class ProblemNotFoundError extends NotFoundError { + constructor(domainId, pid) { + super('ProblemNotFoundError'); + this.params = [domainId, pid]; + } +} +class RecordNotFoundError extends NotFoundError { + constructor(domainId, rid) { + super('RecordNotFoundError'); + this.params = [domainId, rid]; + } +} +class TrainingNotFoundError extends NotFoundError { + constructor(domainId, tid) { + super('TrainingNotFoundError'); + this.params = [domainId, tid]; + } +} +class ContestNotFoundError extends NotFoundError { + constructor(domainId, cid) { + super('ContestNotFoundError'); + this.params = [domainId, cid]; + } +} +module.exports = { + BadRequestError, ForbiddenError, NotFoundError, + LoginError, UserAlreadyExistError, InvalidTokenError, + UserNotFoundError, VerifyPasswordError, PrivilegeError, + OpcountExceededError, DomainAlreadyExistError, DomainNotFoundError, + UserAlreadyDomainMemberError, PermissionError, NoProblemError, + ValidationError, ProblemNotFoundError, TrainingNotFoundError, + ContestNotFoundError, RecordNotFoundError +}; + +/* +from vj4.model import builtin + + +class Error(Exception): + pass + + +class HashError(Error): + pass + + +class InvalidStateError(Error): + pass + + +class UserFacingError(Error): + """Error which faces end user.""" + + def to_dict(self): + return {'name': self.__class__.__name__, 'args': self.args} + + @property + def http_status(self): + return 500 + + @property + def template_name(self): + return 'error.html' + + @property + def message(self): + return 'An error has occurred.' + + +class BuiltinDomainError(ForbiddenError): + @property + def message(self): + return 'Domain {0} is bulit-in and cannot be modified.' + + +class BlacklistedError(ForbiddenError): + @property + def message(self): + return 'Address {0} is blacklisted.' + + +class FileTooLongError(ValidationError): + @property + def message(self): + return 'The uploaded file is too long.' + + +class FileTypeNotAllowedError(ValidationError): + @property + def message(self): + return 'This type of files are not allowed to be uploaded.' + + +class UnknownFieldError(ForbiddenError): + @property + def message(self): + return 'Unknown field {0}.' + + +class DocumentNotFoundError(NotFoundError): + @property + def message(self): + return 'Document {2} not found.' + + +class ProblemDataNotFoundError(NotFoundError): + @property + def message(self): + return 'Data of problem {1} not found.' + + +class RecordDataNotFoundError(NotFoundError): + @property + def message(self): + return 'Data of record {0} not found.' + + +class CsrfTokenError(ForbiddenError): + pass + + +class InvalidOperationError(ForbiddenError): + pass + + +class AlreadyVotedError(ForbiddenError): + @property + def message(self): + return "You've already voted." + + +class InvalidTokenDigestError(ForbiddenError): + pass + + +class CurrentPasswordError(ForbiddenError): + @property + def message(self): + return "Current password doesn't match." + + +class DiscussionCategoryAlreadyExistError(ForbiddenError): + @property + def message(self): + return 'Discussion category {1} already exists.' + + +class DiscussionCategoryNotFoundError(NotFoundError): + @property + def message(self): + return 'Discussion category {1} not found.' + + +class DiscussionNodeAlreadyExistError(ForbiddenError): + @property + def message(self): + return 'Discussion node {1} already exists.' + + +class DiscussionNodeNotFoundError(NotFoundError): + @property + def message(self): + return 'Discussion node {1} not found.' + + +class DiscussionNotFoundError(DocumentNotFoundError): + @property + def message(self): + return 'Discussion {1} not found.' + + +class MessageNotFoundError(NotFoundError): + @property + def message(self): + return 'Message {0} not found.' + + + + +class DomainJoinForbiddenError(ForbiddenError): + @property + def message(self): + return 'You are not allowed to join the domain. The link is either invalid or expired.' + + +class DomainJoinAlreadyMemberError(ForbiddenError): + @property + def message(self): + return 'Failed to join the domain. You are already a member.' + + +class InvalidJoinInvitationCodeError(ForbiddenError): + @property + def message(self): + return 'The invitation code you provided is invalid.' + + +class ContestAlreadyAttendedError(ForbiddenError): + @property + def message(self): + return "You've already attended this contest." + + +class ContestNotAttendedError(ForbiddenError): + @property + def message(self): + return "You haven't attended this contest yet." + + +class ContestScoreboardHiddenError(ForbiddenError): + @property + def message(self): + return 'Contest scoreboard is not visible.' + + +class ContestNotLiveError(ForbiddenError): + @property + def message(self): + return 'This contest is not live.' + + +class HomeworkScoreboardHiddenError(ForbiddenError): + @property + def message(self): + return 'Homework scoreboard is not visible.' + + +class HomeworkNotLiveError(ForbiddenError): + @property + def message(self): + return 'This homework is not open.' + + +class HomeworkAlreadyAttendedError(ForbiddenError): + @property + def message(self): + return "You've already claimed this homework." + + +class HomeworkNotAttendedError(ForbiddenError): + @property + def message(self): + return "You haven't claimed this homework yet." + + +class TrainingRequirementNotSatisfiedError(ForbiddenError): + @property + def message(self): + return 'Training requirement is not satisfied.' + + +class TrainingAlreadyEnrollError(ForbiddenError): + @property + def message(self): + return "You've already enrolled this training." + + +class UsageExceededError(ForbiddenError): + @property + def message(self): + return 'Usage exceeded.' + + +class DomainRoleAlreadyExistError(ForbiddenError): + @property + def message(self): + return 'Role {1} already exists in domain {0}.' + + +class ModifyBuiltinRoleError(ForbiddenError): + @property + def message(self): + return 'Built-in roles cannot be modified.' + + +class InvalidArgumentError(BadRequestError): + @property + def message(self): + return 'Argument {0} is invalid.' + + +class BatchCopyLimitExceededError(ForbiddenError): + @property + def message(self): + return 'Only {0} problems can be copied in one request, got {1}.' + + +class UpgradeLockAcquireError(Error): + @property + def message(self): + return 'Failed to acquire the upgrade lock. There may be another ongoing upgrade process, or a previous process is exited unexpectedly.' + + +class UpgradeLockReleaseError(Error): + @property + def message(self): + return 'Failed to release the upgrade lock. The database is malformed during the upgrade.' + + +class DatabaseVersionMismatchError(Error): + @property + def message(self): + return 'Database version mismatch, got {0}, expect {1}. You need to invoke database upgrades.' + + +class SendMailError(UserFacingError): + @property + def message(self): + return 'Failed to send mail to {0}.' +*/ \ No newline at end of file diff --git a/hydro/handler/base.js b/hydro/handler/base.js new file mode 100644 index 000000000..38ca61db4 --- /dev/null +++ b/hydro/handler/base.js @@ -0,0 +1,98 @@ +const + { MIDDLEWARE } = require('../service/server.js'), + user = require('../model/user'), + domain = require('../model/domain'), + blacklist = require('../model/blacklist'), + token = require('../model/token'), + options = require('../options'), + { + UserNotFoundError, + BlacklistedError, + PermissionError, + PrivilegeError + } = require('../error'); + +MIDDLEWARE(async (ctx, next) => { + try { + ctx.state.domainId = 'system'; + let sid = ctx.cookies.get('sid'); + let save = ctx.cookies.get('save'); + let tokenType, expireSeconds; + if (save) { + tokenType = token.TYPE_SAVED_SESSION; + expireSeconds = options.session.saved_expire_seconds; + } else { + tokenType = token.TYPE_UNSAVED_SESSION; + expireSeconds = options.session.unsaved_expire_seconds; + } + ctx.session = sid ? + await token.update(sid, tokenType, expireSeconds, Object.assign({ + update_ip: ctx.request.ip, + update_ua: ctx.request.headers['user-agent'] || '' + })) : { uid: 1 }; + if (!ctx.session) ctx.session = { uid: 1 }; + let bdoc = await blacklist.get(ctx.request.ip); + if (bdoc) throw new BlacklistedError(ctx.request.ip); + console.log(ctx.session.uid); + [ctx.state.user, ctx.state.domain] = await Promise.all([ + user.getById(ctx.session.uid, ctx.state.domainId), + domain.get(ctx.state.domainId) + ]); + if (!ctx.state.user) throw new UserNotFoundError(ctx.session.uid); + ctx.checkPerm = function (perm) { + for (let i in arguments) { + if (arguments[i] instanceof Array) { + let p = false; + for (let j in arguments) + if (ctx.state.user.hasPerm(arguments[i][j])) { + p = true; + break; + } + if (!p) throw new PermissionError([arguments[i]]); + } else { + if (ctx.state.user.hasPerm(arguments[i])) continue; + else throw new PermissionError([[arguments[i]]]); + } + } + }; + ctx.checkPriv = function (priv) { + for (let i in arguments) { + if (arguments[i] instanceof Array) { + let p = false; + for (let j in arguments) + if (ctx.state.user.hasPriv(arguments[i][j])) { + p = true; + break; + } + if (!p) throw new PrivilegeError([arguments[i]]); + } else { + if (ctx.state.user.hasPriv(arguments[i])) continue; + else throw new PrivilegeError([[arguments[i]]]); + } + } + }; + await next(); + if (ctx.session.sid) + await token.update(ctx.session.sid, tokenType, expireSeconds, Object.assign({ + updateIp: ctx.request.ip, + updateUa: ctx.request.headers['user-agent'] || '' + })); + else + [ctx.session.sid] = await token.add(tokenType, expireSeconds, Object.assign({ + create_ip: ctx.request.ip, + create_ua: ctx.request.headers['user-agent'] || '', + update_ip: ctx.request.ip, + update_ua: ctx.request.headers['user-agent'] || '' + }, ctx.session)); + let cookie = { domain: options.session.domain, secure: options.session.secure, httponly: true }; + if (save) { + cookie.expires = ctx.session.expireAt, cookie.maxAge = expireSeconds; + ctx.cookies.set('save', 'true', cookie); + } + ctx.cookies.set('sid', ctx.session.sid, cookie); + } catch (e) { + console.error(e.message, e.params); + console.error(e.stack); + ctx.body = { error: { message: e.message, params: e.params, stack: e.stack } }; + } +}); \ No newline at end of file diff --git a/hydro/handler/home.js b/hydro/handler/home.js new file mode 100644 index 000000000..a1c235cfe --- /dev/null +++ b/hydro/handler/home.js @@ -0,0 +1,5 @@ +const { GET } = require('../service/server'); + +GET('/', async ctx => { + ctx.body = {}; +}); diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js new file mode 100644 index 000000000..046127bf1 --- /dev/null +++ b/hydro/handler/judge.js @@ -0,0 +1,434 @@ +const + { GET, SOCKET } = require('../service/server'), + { assertQueue, consume } = require('../service/queue'), + { requirePriv } = require('./tools'), + record = require('../model/record'), + token = require('../model/token'), + user = require('../model/user'), + { PRIV_JUDGE } = require('../privilege'); + +assertQueue('judge'); + +GET('/judge/noop', requirePriv(PRIV_JUDGE), async ctx => { + ctx.body = {}; +}); +SOCKET('/judge/conn', async conn => { + let sid = await new Promise((resolve, reject) => { + setTimeout(reject, 5000); + conn.once('data', async (data) => { + console.log(data); + resolve(data); + }); + }); + let session = await token.get(sid, token.TYPE_SESSION); + let u = await user.getById(session.uid); + if (!u.hasPriv(PRIV_JUDGE)) conn.close(1000, 'PrivilegeError'); + let h = 1; + let interval = setInterval(() => { + if (!h) h.close(); + h = 0; + conn.write('h'); + }, 30000); + conn.on('data', (data) => { + data = JSON.parse(data); + h++; + if (data.event == 'next') { + // TODO (masnn) + } else if (data.event == 'end') { + // TODO (masnn) + } + }); + consume('judge', async rid => { + rid = rid.toString(); + let rdoc = await record.get(rid); + let data = { + event: 'judge', + rid, type: 0, + domainId: rdoc.domainId, + pid: rdoc.pid, + data: rdoc.data, + lang: rdoc.lang, + code: rdoc.code + }; + conn.write(JSON.stringify(data)); + }); + conn.on('close', () => { + clearInterval(interval); + }); +}); + +/* + +@app.route('/p/category/{category:[^/]*}/random', 'problem_category_random') +class ProblemCategoryRandomHandler(base.Handler): + @base.requirePerm(builtin.PERM_VIEW_PROBLEM) + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, category: str): + if not self.has_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN): + f = {'hidden': False} + else: + f = {} + query = ProblemCategoryHandler.build_query(category) + pid = await problem.get_random_id(self.domainId, **query, **f) + if pid: + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + else: + self.json_or_redirect(self.referer_or_main) + + +@app.route('/p/{pid}/submit', 'problem_submit') +class ProblemSubmitHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2): check status, eg. test, hidden problem, ... + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + if uid == None: + rdocs = [] + else: + # TODO(iceboy): needs to be in sync with contest_detail_problem_submit + rdocs = await record \ + .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ + .sort([('_id', -1)]) \ + .limit(10) \ + .to_list() + if not self.prefer_json: + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_submit'), None)) + self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + else: + self.json({'rdocs': rdocs}) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str): + # TODO(twd2): check status, eg. test, hidden problem, ... + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, + self.user['_id'], lang, code) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.route('/p/{pid}/pretest', 'problem_pretest') +class ProblemPretestHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str, + data_input: str, data_output: str): + pdoc = await problem.get(self.domainId, pid) + # don't need to check hidden status + # create zip file, TODO(twd2): check file size + post = await self.request.post() + content = list(zip(post.getall('data_input'), post.getall('data_output'))) + output_buffer = io.BytesIO() + zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) + config_content = str(len(content)) + '\n' + for i, (data_input, data_output) in enumerate(content): + input_file = 'input{0}.txt'.format(i) + output_file = 'output{0}.txt'.format(i) + config_content += '{0}|{1}|1|10|262144\n'.format(input_file, output_file) + zip_file.writestr('Input/{0}'.format(input_file), data_input) + zip_file.writestr('Output/{0}'.format(output_file), data_output) + zip_file.writestr('Config.ini', config_content) + # mark all files as created in Windows :p + for zfile in zip_file.filelist: + zfile.create_system = 0 + zip_file.close() + fid = await fs.add_data('application/zip', output_buffer.getvalue()) + output_buffer.close() + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, + self.user['_id'], lang, code, fid) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.connection_route('/p/{pid}/pretest-conn', 'problem_pretest-conn') +class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connection): + async def on_open(self): + await super(ProblemPretestConnection, self).on_open() + self.pid = document.convert_doc_id(self.request.match_info['pid']) + bus.subscribe(self.on_record_change, ['record_change']) + + async def on_record_change(self, e): + rdoc = e['value'] + if rdoc['uid'] != self.user['_id'] or \ + rdoc['domainId'] != self.domainId or rdoc['pid'] != self.pid: + return + # check permission for visibility: contest + if rdoc['tid']: + show_status, tdoc = await self.rdoc_contest_visible(rdoc) + if not show_status: + return + self.send(rdoc=rdoc) + + async def on_close(self): + bus.unsubscribe(self.on_record_change) + +@app.route('/p/{pid}/data', 'problem_data') +class ProblemDataHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # Judges will have PRIV_READ_PROBLEM_DATA, + # domain administrators will have PERM_READ_PROBLEM_DATA, + # problem owner will have PERM_READ_PROBLEM_DATA_SELF. + pdoc = await problem.get(self.domainId, pid) + if type(pdoc['data']) is dict: + return self.redirect(self.reverse_url('problem_data', + domainId=pdoc['data']['domain'], + pid=pdoc['data']['pid'])) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + fdoc = await problem.get_data(pdoc) + if not fdoc: + raise error.ProblemDataNotFoundError(self.domainId, pid) + self.redirect(options.cdn_prefix.rstrip('/') + \ + self.reverse_url('fs_get', domainId=builtin.domainId_SYSTEM, + secret=fdoc['metadata']['secret'])) + + +@app.route('/p/copy', 'problem_copy') +class ProblemCopyHandler(base.Handler): + MAX_PROBLEMS_PER_REQUEST = 20 + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + async def get(self): + self.render('problem_copy.html') + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('copy_problems', 30, 10) + async def post(self, *, src_domainId: str, src_pids: str, + numeric_pid: bool=False, hidden: bool=False): + src_ddoc, src_dudoc = await asyncio.gather(domain.get(src_domainId), + domain.get_user(src_domainId, self.user['_id'])) + if not src_dudoc: + src_dudoc = {} + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM) + + src_pids = misc.dedupe(map(document.convert_doc_id, src_pids.replace('\r\n', '\n').split('\n'))) + if len(src_pids) > self.MAX_PROBLEMS_PER_REQUEST: + raise error.BatchCopyLimitExceededError(self.MAX_PROBLEMS_PER_REQUEST, len(src_pids)) + pdocs = await problem.get_multi(domainId=src_domainId, doc_id={'$in': src_pids}) \ + .sort('doc_id', 1) \ + .to_list() + + exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + if len(src_pids) != len(exist_pids): + for pid in src_pids: + if pid not in exist_pids: + raise error.ProblemNotFoundError(src_domainId, pid) + + for pdoc in pdocs: + if pdoc.get('hidden', False): + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM_HIDDEN): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM_HIDDEN) + + for pdoc in pdocs: + pid = None + if numeric_pid: + pid = await domain.inc_pid_counter(self.domainId) + await problem.copy(pdoc, self.domainId, self.user['_id'], pid, hidden) + + self.redirect(self.reverse_url('problem_main')) + + +@app.route('/p/{pid}/edit', 'problem_edit') +class ProblemEditHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_edit'), None)) + self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, title: str, content: str): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/settings', 'problem_settings') +class ProblemSettingsHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_settings'), None)) + self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + categories=problem.get_categories(), + page_title=pdoc['title'], path_components=path_components) + + def split_tags(self, s): + s = s.replace(',', ',') # Chinese ', ' + return list(filter(lambda _: _ != '', map(lambda _: _.strip(), s.split(',')))) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, hidden: bool=False, + category: str, tag: str, + difficulty_setting: int, difficulty_admin: str=''): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + category = self.split_tags(category) + tag = self.split_tags(tag) + for c in category: + if not (c in builtin.PROBLEM_CATEGORIES + or c in builtin.PROBLEM_SUB_CATEGORIES): + raise error.ValidationError('category') + if difficulty_setting not in problem.SETTING_DIFFICULTY_RANGE: + raise error.ValidationError('difficulty_setting') + if difficulty_admin: + try: + difficulty_admin = int(difficulty_admin) + except ValueError: + raise error.ValidationError('difficulty_admin') + else: + difficulty_admin = None + await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, + category=category, tag=tag, + difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) + await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/upload', 'problem_upload') +class ProblemUploadHandler(base.Handler): + def get_content_type(self, filename): + if os.path.splitext(filename)[1].lower() != '.zip': + raise error.FileTypeNotAllowedError(filename) + return 'application/zip' + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + md5 = await fs.get_md5(await problem.get_data(pdoc)) + self.render('problem_upload.html', pdoc=pdoc, md5=md5) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.multipart_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, file: objectid.ObjectId): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + if pdoc.get('data') and type(pdoc['data']) is objectid.ObjectId: + await fs.unlink(pdoc['data']) + await problem.set_data(self.domainId, pid, file) + self.json_or_redirect(self.url) + + +@app.route('/p/{pid}/statistics', 'problem_statistics') +class ProblemStatisticsHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2) + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_statistics'), None)) + self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + +@app.route('/p/search', 'problem_search') +class ProblemSearchHandler(base.Handler): + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, q: str): + q = q.strip() + if not q: + self.json_or_redirect(self.referer_or_main) + return + try: + pdoc = await problem.get(self.domainId, document.convert_doc_id(q)) + except error.ProblemNotFoundError: + pdoc = None + if pdoc: + self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) + return + self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ + .format(parse.quote(q), parse.quote(options.url_prefix))) + +*/ \ No newline at end of file diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js new file mode 100644 index 000000000..96776bde2 --- /dev/null +++ b/hydro/handler/problem.js @@ -0,0 +1,448 @@ +const + { GET, POST } = require('../service/server'), + { assertQueue, publish } = require('../service/queue'), + { requirePerm, requirePriv } = require('./tools'), + validator = require('../lib/validator'), + problem = require('../model/problem'), + training = require('../model/training'), + contest = require('../model/contest'), + record = require('../model/record'), + domain = require('../model/domain'), + user = require('../model/user'), + { NoProblemError } = require('../error'), + { PRIV_USER_PROFILE } = require('../privilege'), + { + PERM_VIEW_PROBLEM, + PERM_VIEW_PROBLEM_HIDDEN, + PERM_VIEW_TRAINING, + PERM_VIEW_CONTEST, + PERM_VIEW_HOMEWORK, + PERM_SUBMIT_PROBLEM, + PERM_CREATE_PROBLEM + } = require('../permission'); + +const PROBLEMS_PER_PAGE = 100; +assertQueue('judge'); + +GET('/p', requirePerm(PERM_VIEW_PROBLEM), async ctx => { + let q = { domainId: ctx.state.domainId }, + page = ctx.query.page || 1; + if (!ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + let pdocs = await problem.getMany(q, { pid: 1 }, page, PROBLEMS_PER_PAGE); + ctx.body = { code: 0, page, pdocs, category: '' }; +}); +GET('/problem/random', requirePerm(PERM_VIEW_PROBLEM), async ctx => { + let q = { domainId: ctx.state.domainId }; + if (!ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + let pid = await problem.random(q); + if (!pid) throw new NoProblemError(); + ctx.body = { pid }; +}); +POST('/p/:pid/submit', requirePerm(PERM_SUBMIT_PROBLEM), async ctx => { + let rid = await record.add({ + creator: ctx.state.user._id, + lang: ctx.request.body.language, + code: ctx.request.body.code, + domainId: ctx.state.domainId, + pid: ctx.params.pid + }); + await publish('judge', rid); + ctx.state.user.nSubmit++; + ctx.body = { rid }; +}); +GET('/p/:pid', requirePerm(PERM_VIEW_PROBLEM), async ctx => { + let uid = null, + pid = Number(ctx.params.pid); + if (ctx.state.user.hasPriv(PRIV_USER_PROFILE)) uid = ctx.state.user._id; + let pdoc = await problem.get({ domainId: ctx.state.domainId, pid, uid }); + if (pdoc.hidden) ctx.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + let udoc = await user.getById(pdoc.owner); + ctx.body = { pdoc, udoc, title: pdoc.title }; +}); +POST('/problem/create', + requirePriv(PRIV_USER_PROFILE), + requirePerm(PERM_CREATE_PROBLEM), + async ctx => { + let { title, pid, content, hidden } = ctx.request.body; + validator.checkPid(pid); + pid = pid || await domain.incPidCounter(ctx.state.domainId); + await problem.add({ domainId: ctx.state.domainId, title, content, owner: ctx.state.user._id, pid, hidden }); + ctx.body = { pid }; + } +); + +/* + +@app.route('/p/category/{category:[^/]*}/random', 'problem_category_random') +class ProblemCategoryRandomHandler(base.Handler): + @base.requirePerm(builtin.PERM_VIEW_PROBLEM) + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, category: str): + if not self.has_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN): + f = {'hidden': False} + else: + f = {} + query = ProblemCategoryHandler.build_query(category) + pid = await problem.get_random_id(self.domainId, **query, **f) + if pid: + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + else: + self.json_or_redirect(self.referer_or_main) + + +@app.route('/p/{pid}/submit', 'problem_submit') +class ProblemSubmitHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2): check status, eg. test, hidden problem, ... + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + if uid == None: + rdocs = [] + else: + # TODO(iceboy): needs to be in sync with contest_detail_problem_submit + rdocs = await record \ + .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ + .sort([('_id', -1)]) \ + .limit(10) \ + .to_list() + if not self.prefer_json: + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_submit'), None)) + self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + else: + self.json({'rdocs': rdocs}) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str): + # TODO(twd2): check status, eg. test, hidden problem, ... + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, + self.user['_id'], lang, code) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.route('/p/{pid}/pretest', 'problem_pretest') +class ProblemPretestHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str, + data_input: str, data_output: str): + pdoc = await problem.get(self.domainId, pid) + # don't need to check hidden status + # create zip file, TODO(twd2): check file size + post = await self.request.post() + content = list(zip(post.getall('data_input'), post.getall('data_output'))) + output_buffer = io.BytesIO() + zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) + config_content = str(len(content)) + '\n' + for i, (data_input, data_output) in enumerate(content): + input_file = 'input{0}.txt'.format(i) + output_file = 'output{0}.txt'.format(i) + config_content += '{0}|{1}|1|10|262144\n'.format(input_file, output_file) + zip_file.writestr('Input/{0}'.format(input_file), data_input) + zip_file.writestr('Output/{0}'.format(output_file), data_output) + zip_file.writestr('Config.ini', config_content) + # mark all files as created in Windows :p + for zfile in zip_file.filelist: + zfile.create_system = 0 + zip_file.close() + fid = await fs.add_data('application/zip', output_buffer.getvalue()) + output_buffer.close() + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, + self.user['_id'], lang, code, fid) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.connection_route('/p/{pid}/pretest-conn', 'problem_pretest-conn') +class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connection): + async def on_open(self): + await super(ProblemPretestConnection, self).on_open() + self.pid = document.convert_doc_id(self.request.match_info['pid']) + bus.subscribe(self.on_record_change, ['record_change']) + + async def on_record_change(self, e): + rdoc = e['value'] + if rdoc['uid'] != self.user['_id'] or \ + rdoc['domainId'] != self.domainId or rdoc['pid'] != self.pid: + return + # check permission for visibility: contest + if rdoc['tid']: + show_status, tdoc = await self.rdoc_contest_visible(rdoc) + if not show_status: + return + self.send(rdoc=rdoc) + + async def on_close(self): + bus.unsubscribe(self.on_record_change) + +@app.route('/p/{pid}/data', 'problem_data') +class ProblemDataHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # Judges will have PRIV_READ_PROBLEM_DATA, + # domain administrators will have PERM_READ_PROBLEM_DATA, + # problem owner will have PERM_READ_PROBLEM_DATA_SELF. + pdoc = await problem.get(self.domainId, pid) + if type(pdoc['data']) is dict: + return self.redirect(self.reverse_url('problem_data', + domainId=pdoc['data']['domain'], + pid=pdoc['data']['pid'])) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + fdoc = await problem.get_data(pdoc) + if not fdoc: + raise error.ProblemDataNotFoundError(self.domainId, pid) + self.redirect(options.cdn_prefix.rstrip('/') + \ + self.reverse_url('fs_get', domainId=builtin.domainId_SYSTEM, + secret=fdoc['metadata']['secret'])) + + +@app.route('/p/copy', 'problem_copy') +class ProblemCopyHandler(base.Handler): + MAX_PROBLEMS_PER_REQUEST = 20 + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + async def get(self): + self.render('problem_copy.html') + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('copy_problems', 30, 10) + async def post(self, *, src_domainId: str, src_pids: str, + numeric_pid: bool=False, hidden: bool=False): + src_ddoc, src_dudoc = await asyncio.gather(domain.get(src_domainId), + domain.get_user(src_domainId, self.user['_id'])) + if not src_dudoc: + src_dudoc = {} + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM) + + src_pids = misc.dedupe(map(document.convert_doc_id, src_pids.replace('\r\n', '\n').split('\n'))) + if len(src_pids) > self.MAX_PROBLEMS_PER_REQUEST: + raise error.BatchCopyLimitExceededError(self.MAX_PROBLEMS_PER_REQUEST, len(src_pids)) + pdocs = await problem.get_multi(domainId=src_domainId, doc_id={'$in': src_pids}) \ + .sort('doc_id', 1) \ + .to_list() + + exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + if len(src_pids) != len(exist_pids): + for pid in src_pids: + if pid not in exist_pids: + raise error.ProblemNotFoundError(src_domainId, pid) + + for pdoc in pdocs: + if pdoc.get('hidden', False): + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM_HIDDEN): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM_HIDDEN) + + for pdoc in pdocs: + pid = None + if numeric_pid: + pid = await domain.inc_pid_counter(self.domainId) + await problem.copy(pdoc, self.domainId, self.user['_id'], pid, hidden) + + self.redirect(self.reverse_url('problem_main')) + + +@app.route('/p/{pid}/edit', 'problem_edit') +class ProblemEditHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_edit'), None)) + self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, title: str, content: str): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/settings', 'problem_settings') +class ProblemSettingsHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_settings'), None)) + self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + categories=problem.get_categories(), + page_title=pdoc['title'], path_components=path_components) + + def split_tags(self, s): + s = s.replace(',', ',') # Chinese ', ' + return list(filter(lambda _: _ != '', map(lambda _: _.strip(), s.split(',')))) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, hidden: bool=False, + category: str, tag: str, + difficulty_setting: int, difficulty_admin: str=''): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + category = self.split_tags(category) + tag = self.split_tags(tag) + for c in category: + if not (c in builtin.PROBLEM_CATEGORIES + or c in builtin.PROBLEM_SUB_CATEGORIES): + raise error.ValidationError('category') + if difficulty_setting not in problem.SETTING_DIFFICULTY_RANGE: + raise error.ValidationError('difficulty_setting') + if difficulty_admin: + try: + difficulty_admin = int(difficulty_admin) + except ValueError: + raise error.ValidationError('difficulty_admin') + else: + difficulty_admin = None + await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, + category=category, tag=tag, + difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) + await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/upload', 'problem_upload') +class ProblemUploadHandler(base.Handler): + def get_content_type(self, filename): + if os.path.splitext(filename)[1].lower() != '.zip': + raise error.FileTypeNotAllowedError(filename) + return 'application/zip' + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + md5 = await fs.get_md5(await problem.get_data(pdoc)) + self.render('problem_upload.html', pdoc=pdoc, md5=md5) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.multipart_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, file: objectid.ObjectId): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + if pdoc.get('data') and type(pdoc['data']) is objectid.ObjectId: + await fs.unlink(pdoc['data']) + await problem.set_data(self.domainId, pid, file) + self.json_or_redirect(self.url) + + +@app.route('/p/{pid}/statistics', 'problem_statistics') +class ProblemStatisticsHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2) + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_statistics'), None)) + self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + +@app.route('/p/search', 'problem_search') +class ProblemSearchHandler(base.Handler): + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, q: str): + q = q.strip() + if not q: + self.json_or_redirect(self.referer_or_main) + return + try: + pdoc = await problem.get(self.domainId, document.convert_doc_id(q)) + except error.ProblemNotFoundError: + pdoc = None + if pdoc: + self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) + return + self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ + .format(parse.quote(q), parse.quote(options.url_prefix))) + +*/ \ No newline at end of file diff --git a/hydro/handler/record.js b/hydro/handler/record.js new file mode 100644 index 000000000..90260207e --- /dev/null +++ b/hydro/handler/record.js @@ -0,0 +1,403 @@ +const + bson = require('bson'), + { constants } = require('../options'), + { GET, POST } = require('../service/server'), + { publish } = require('../service/queue'), + { requirePerm, requirePriv } = require('./tools'), + validator = require('../lib/validator'), + problem = require('../model/problem'), + record = require('../model/record'), + domain = require('../model/domain'), + user = require('../model/user'), + { PRIV_USER_PROFILE } = require('../privilege'), + { PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'); + +GET('/r', async ctx => { + let q = { domainId: ctx.state.domainId }, + page = ctx.query.page || 1; + let rdocs = await record.getMany(q, { rid: 1 }, page, constants.RECORD_PER_PAGE); + ctx.body = { page, rdocs }; +}); +GET('/r/:rid', async ctx => { + let uid = ctx.state.user._id, rid = new bson.ObjectID(ctx.params.rid); + let rdoc = await record.get(rid); + if (rdoc.hidden) ctx.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + if (rdoc.creator != uid && !ctx.state.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; + ctx.body = { rdoc }; +}); + +/* + +@app.route('/p/category/{category:[^/]*}/random', 'problem_category_random') +class ProblemCategoryRandomHandler(base.Handler): + @base.requirePerm(builtin.PERM_VIEW_PROBLEM) + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, category: str): + if not self.has_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN): + f = {'hidden': False} + else: + f = {} + query = ProblemCategoryHandler.build_query(category) + pid = await problem.get_random_id(self.domainId, **query, **f) + if pid: + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + else: + self.json_or_redirect(self.referer_or_main) + + +@app.route('/p/{pid}/submit', 'problem_submit') +class ProblemSubmitHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2): check status, eg. test, hidden problem, ... + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + if uid == None: + rdocs = [] + else: + # TODO(iceboy): needs to be in sync with contest_detail_problem_submit + rdocs = await record \ + .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ + .sort([('_id', -1)]) \ + .limit(10) \ + .to_list() + if not self.prefer_json: + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_submit'), None)) + self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + else: + self.json({'rdocs': rdocs}) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str): + # TODO(twd2): check status, eg. test, hidden problem, ... + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, + self.user['_id'], lang, code) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.route('/p/{pid}/pretest', 'problem_pretest') +class ProblemPretestHandler(base.Handler): + @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('add_record', 60, 100) + async def post(self, *, pid: document.convert_doc_id, lang: str, code: str, + data_input: str, data_output: str): + pdoc = await problem.get(self.domainId, pid) + # don't need to check hidden status + # create zip file, TODO(twd2): check file size + post = await self.request.post() + content = list(zip(post.getall('data_input'), post.getall('data_output'))) + output_buffer = io.BytesIO() + zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) + config_content = str(len(content)) + '\n' + for i, (data_input, data_output) in enumerate(content): + input_file = 'input{0}.txt'.format(i) + output_file = 'output{0}.txt'.format(i) + config_content += '{0}|{1}|1|10|262144\n'.format(input_file, output_file) + zip_file.writestr('Input/{0}'.format(input_file), data_input) + zip_file.writestr('Output/{0}'.format(output_file), data_output) + zip_file.writestr('Config.ini', config_content) + # mark all files as created in Windows :p + for zfile in zip_file.filelist: + zfile.create_system = 0 + zip_file.close() + fid = await fs.add_data('application/zip', output_buffer.getvalue()) + output_buffer.close() + rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, + self.user['_id'], lang, code, fid) + self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) + + +@app.connection_route('/p/{pid}/pretest-conn', 'problem_pretest-conn') +class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connection): + async def on_open(self): + await super(ProblemPretestConnection, self).on_open() + self.pid = document.convert_doc_id(self.request.match_info['pid']) + bus.subscribe(self.on_record_change, ['record_change']) + + async def on_record_change(self, e): + rdoc = e['value'] + if rdoc['uid'] != self.user['_id'] or \ + rdoc['domainId'] != self.domainId or rdoc['pid'] != self.pid: + return + # check permission for visibility: contest + if rdoc['tid']: + show_status, tdoc = await self.rdoc_contest_visible(rdoc) + if not show_status: + return + self.send(rdoc=rdoc) + + async def on_close(self): + bus.unsubscribe(self.on_record_change) + +@app.route('/p/{pid}/data', 'problem_data') +class ProblemDataHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # Judges will have PRIV_READ_PROBLEM_DATA, + # domain administrators will have PERM_READ_PROBLEM_DATA, + # problem owner will have PERM_READ_PROBLEM_DATA_SELF. + pdoc = await problem.get(self.domainId, pid) + if type(pdoc['data']) is dict: + return self.redirect(self.reverse_url('problem_data', + domainId=pdoc['data']['domain'], + pid=pdoc['data']['pid'])) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + fdoc = await problem.get_data(pdoc) + if not fdoc: + raise error.ProblemDataNotFoundError(self.domainId, pid) + self.redirect(options.cdn_prefix.rstrip('/') + \ + self.reverse_url('fs_get', domainId=builtin.domainId_SYSTEM, + secret=fdoc['metadata']['secret'])) + + +@app.route('/p/copy', 'problem_copy') +class ProblemCopyHandler(base.Handler): + MAX_PROBLEMS_PER_REQUEST = 20 + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + async def get(self): + self.render('problem_copy.html') + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM) + @base.post_argument + @base.require_csrf_token + @base.sanitize + @base.limitRate('copy_problems', 30, 10) + async def post(self, *, src_domainId: str, src_pids: str, + numeric_pid: bool=False, hidden: bool=False): + src_ddoc, src_dudoc = await asyncio.gather(domain.get(src_domainId), + domain.get_user(src_domainId, self.user['_id'])) + if not src_dudoc: + src_dudoc = {} + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM) + + src_pids = misc.dedupe(map(document.convert_doc_id, src_pids.replace('\r\n', '\n').split('\n'))) + if len(src_pids) > self.MAX_PROBLEMS_PER_REQUEST: + raise error.BatchCopyLimitExceededError(self.MAX_PROBLEMS_PER_REQUEST, len(src_pids)) + pdocs = await problem.get_multi(domainId=src_domainId, doc_id={'$in': src_pids}) \ + .sort('doc_id', 1) \ + .to_list() + + exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + if len(src_pids) != len(exist_pids): + for pid in src_pids: + if pid not in exist_pids: + raise error.ProblemNotFoundError(src_domainId, pid) + + for pdoc in pdocs: + if pdoc.get('hidden', False): + if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, + perm=builtin.PERM_VIEW_PROBLEM_HIDDEN): + # TODO: This is the source domain's PermissionError. + raise error.PermissionError(builtin.PERM_VIEW_PROBLEM_HIDDEN) + + for pdoc in pdocs: + pid = None + if numeric_pid: + pid = await domain.inc_pid_counter(self.domainId) + await problem.copy(pdoc, self.domainId, self.user['_id'], pid, hidden) + + self.redirect(self.reverse_url('problem_main')) + + +@app.route('/p/{pid}/edit', 'problem_edit') +class ProblemEditHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_edit'), None)) + self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, title: str, content: str): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/settings', 'problem_settings') +class ProblemSettingsHandler(base.Handler): + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_settings'), None)) + self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + categories=problem.get_categories(), + page_title=pdoc['title'], path_components=path_components) + + def split_tags(self, s): + s = s.replace(',', ',') # Chinese ', ' + return list(filter(lambda _: _ != '', map(lambda _: _.strip(), s.split(',')))) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, hidden: bool=False, + category: str, tag: str, + difficulty_setting: int, difficulty_admin: str=''): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + category = self.split_tags(category) + tag = self.split_tags(tag) + for c in category: + if not (c in builtin.PROBLEM_CATEGORIES + or c in builtin.PROBLEM_SUB_CATEGORIES): + raise error.ValidationError('category') + if difficulty_setting not in problem.SETTING_DIFFICULTY_RANGE: + raise error.ValidationError('difficulty_setting') + if difficulty_admin: + try: + difficulty_admin = int(difficulty_admin) + except ValueError: + raise error.ValidationError('difficulty_admin') + else: + difficulty_admin = None + await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, + category=category, tag=tag, + difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) + await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) + self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) + + +@app.route('/p/{pid}/upload', 'problem_upload') +class ProblemUploadHandler(base.Handler): + def get_content_type(self, filename): + if os.path.splitext(filename)[1].lower() != '.zip': + raise error.FileTypeNotAllowedError(filename) + return 'application/zip' + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + md5 = await fs.get_md5(await problem.get_data(pdoc)) + self.render('problem_upload.html', pdoc=pdoc, md5=md5) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.multipart_argument + @base.require_csrf_token + @base.sanitize + async def post(self, *, pid: document.convert_doc_id, file: objectid.ObjectId): + pdoc = await problem.get(self.domainId, pid) + if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM) + if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) + and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): + self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) + if pdoc.get('data') and type(pdoc['data']) is objectid.ObjectId: + await fs.unlink(pdoc['data']) + await problem.set_data(self.domainId, pid, file) + self.json_or_redirect(self.url) + + +@app.route('/p/{pid}/statistics', 'problem_statistics') +class ProblemStatisticsHandler(base.Handler): + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id): + # TODO(twd2) + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), + domain.get_user(self.domainId, pdoc['owner_uid'])) + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_statistics'), None)) + self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, + page_title=pdoc['title'], path_components=path_components) + + +@app.route('/p/search', 'problem_search') +class ProblemSearchHandler(base.Handler): + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, q: str): + q = q.strip() + if not q: + self.json_or_redirect(self.referer_or_main) + return + try: + pdoc = await problem.get(self.domainId, document.convert_doc_id(q)) + except error.ProblemNotFoundError: + pdoc = None + if pdoc: + self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) + return + self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ + .format(parse.quote(q), parse.quote(options.url_prefix))) + +*/ \ No newline at end of file diff --git a/hydro/handler/solution.js b/hydro/handler/solution.js new file mode 100644 index 000000000..10b5c750b --- /dev/null +++ b/hydro/handler/solution.js @@ -0,0 +1,184 @@ +/* +@app.route('/p/{pid}/solution', 'problem_solution') +class ProblemSolutionHandler(base.OperationHandler): + SOLUTIONS_PER_PAGE = 20 + + @base.requirePerm(builtin.PERM_VIEW_PROBLEM_SOLUTION) + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id, page: int=1): + uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None + pdoc = await problem.get(self.domainId, pid, uid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdocs, pcount, pscount = await pagination.paginate( + problem.get_multi_solution(self.domainId, pdoc['doc_id']), + page, self.SOLUTIONS_PER_PAGE) + uids = {pdoc['owner_uid']} + uids.update(psdoc['owner_uid'] for psdoc in psdocs) + for psdoc in psdocs: + if 'reply' in psdoc: + uids.update(psrdoc['owner_uid'] for psrdoc in psdoc['reply']) + udict, dudict, pssdict = await asyncio.gather( + user.get_dict(uids), + domain.get_dict_user_by_uid(self.domainId, uids), + problem.get_dict_solution_status( + self.domainId, (psdoc['doc_id'] for psdoc in psdocs), self.user['_id'])) + dudict[self.user['_id']] = self.domain_user + path_components = self.build_path( + (self.translate('problem_main'), self.reverse_url('problem_main')), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (self.translate('problem_solution'), None)) + self.render('problem_solution.html', path_components=path_components, + pdoc=pdoc, psdocs=psdocs, page=page, pcount=pcount, pscount=pscount, + udict=udict, dudict=dudict, pssdict=pssdict) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_CREATE_PROBLEM_SOLUTION) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_submit(self, *, pid: document.convert_doc_id, content: str): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + await problem.add_solution(self.domainId, pdoc['doc_id'], self.user['_id'], content) + self.json_or_redirect(self.url) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_edit_solution(self, *, pid: document.convert_doc_id, + psid: document.convert_doc_id, content: str): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc = await problem.get_solution(self.domainId, psid, pdoc['doc_id']) + if not self.own(psdoc, builtin.PERM_EDIT_PROBLEM_SOLUTION_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM_SOLUTION) + psdoc = await problem.set_solution(self.domainId, psdoc['doc_id'], + content=content) + self.json_or_redirect(self.url) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_delete_solution(self, *, pid: document.convert_doc_id, + psid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc = await problem.get_solution(self.domainId, psid, pdoc['doc_id']) + if not self.own(psdoc, builtin.PERM_DELETE_PROBLEM_SOLUTION_SELF): + self.check_perm(builtin.PERM_DELETE_PROBLEM_SOLUTION) + await oplog.add(self.user['_id'], oplog.TYPE_DELETE_DOCUMENT, doc=psdoc) + await problem.delete_solution(self.domainId, psdoc['doc_id']) + self.json_or_redirect(self.url) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_edit_reply(self, *, pid: document.convert_doc_id, + psid: document.convert_doc_id, psrid: document.convert_doc_id, + content: str): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc, psrdoc = await problem.get_solution_reply(self.domainId, psid, psrid) + if not psdoc or psdoc['parent_doc_id'] != pdoc['doc_id']: + raise error.DocumentNotFoundError(self.domainId, document.TYPE_PROBLEM_SOLUTION, psid) + if not self.own(psrdoc, builtin.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF): + self.check_perm(builtin.PERM_EDIT_PROBLEM_SOLUTION_REPLY) + await problem.edit_solution_reply(self.domainId, psid, psrid, content) + self.json_or_redirect(self.url) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_delete_reply(self, *, pid: document.convert_doc_id, + psid: document.convert_doc_id, psrid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc, psrdoc = await problem.get_solution_reply(self.domainId, psid, psrid) + if not psdoc or psdoc['parent_doc_id'] != pdoc['doc_id']: + raise error.DocumentNotFoundError(self.domainId, document.TYPE_PROBLEM_SOLUTION, psid) + if not self.own(psrdoc, builtin.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF): + self.check_perm(builtin.PERM_DELETE_PROBLEM_SOLUTION_REPLY) + await oplog.add(self.user['_id'], oplog.TYPE_DELETE_SUB_DOCUMENT, sub_doc=psrdoc, + doc_type=psdoc['doc_type'], doc_id=psdoc['doc_id']) + await problem.delete_solution_reply(self.domainId, psid, psrid) + self.json_or_redirect(self.url) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_VOTE_PROBLEM_SOLUTION) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def upvote_downvote(self, *, + pid: document.convert_doc_id, + psid: document.convert_doc_id, + value: int): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc = await problem.get_solution(self.domainId, psid, pdoc['doc_id']) + psdoc, pssdoc = await problem.vote_solution(self.domainId, psdoc['doc_id'], + self.user['_id'], value) + self.json_or_redirect(self.url, vote=psdoc['vote'], user_vote=pssdoc['vote']) + + post_upvote = functools.partialmethod(upvote_downvote, value=1) + post_downvote = functools.partialmethod(upvote_downvote, value=-1) + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.requirePerm(builtin.PERM_REPLY_PROBLEM_SOLUTION) + @base.route_argument + @base.require_csrf_token + @base.sanitize + async def post_reply(self, *, + pid: document.convert_doc_id, + psid: document.convert_doc_id, + content: str): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc = await problem.get_solution(self.domainId, psid, pdoc['doc_id']) + await problem.reply_solution(self.domainId, psdoc['doc_id'], self.user['_id'], content) + self.json_or_redirect(self.url) + + +@app.route('/p/{pid}/solution/{psid:\w{24}}/raw', 'problem_solution_raw') +class ProblemSolutionRawHandler(base.Handler): + @base.requirePerm(builtin.PERM_VIEW_PROBLEM_SOLUTION) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id, psid: document.convert_doc_id): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc = await problem.get_solution(self.domainId, psid, pdoc['doc_id']) + self.response.content_type = 'text/markdown' + self.response.text = psdoc['content'] + + +@app.route('/p/{pid}/solution/{psid:\w{24}}/{psrid:\w{24}}/raw', 'problem_solution_reply_raw') +class ProblemSolutionReplyRawHandler(base.Handler): + @base.requirePerm(builtin.PERM_VIEW_PROBLEM_SOLUTION) + @base.route_argument + @base.sanitize + async def get(self, *, pid: document.convert_doc_id, psid: document.convert_doc_id, + psrid: objectid.ObjectId): + pdoc = await problem.get(self.domainId, pid) + if pdoc.get('hidden', False): + self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) + psdoc, psrdoc = await problem.get_solution_reply(self.domainId, psid, psrid) + if not psdoc or psdoc['parent_doc_id'] != pdoc['doc_id']: + raise error.DocumentNotFoundError(self.domainId, document.TYPE_PROBLEM_SOLUTION, psid) + self.response.content_type = 'text/markdown' + self.response.text = psrdoc['content'] + */ \ No newline at end of file diff --git a/hydro/handler/tools.js b/hydro/handler/tools.js new file mode 100644 index 000000000..3ef5d12ab --- /dev/null +++ b/hydro/handler/tools.js @@ -0,0 +1,50 @@ +const + opcount = require('../model/opcount'), + { PermissionError, PrivilegeError } = require('../error'); + +module.exports = { + requirePerm(perm) { + return async (ctx, next) => { + for (let i in arguments) { + if (arguments[i] instanceof Array) { + let p = false; + for (let j in arguments) + if (ctx.state.user.hasPerm(arguments[i][j])) { + p = true; + break; + } + if (!p) throw new PermissionError([arguments[i]]); + } else { + if (ctx.state.user.hasPerm(arguments[i])) continue; + else throw new PermissionError([arguments[i]]); + } + } + await next(); + }; + }, + requirePriv(priv) { + return async (ctx, next) => { + for (let i in arguments) { + if (arguments[i] instanceof Array) { + let p = false; + for (let j in arguments) + if (ctx.state.user.hasPriv(arguments[i][j])) { + p = true; + break; + } + if (!p) throw new PrivilegeError([arguments[i]]); + } else { + if (ctx.state.user.hasPriv(arguments[i])) continue; + else throw new PrivilegeError([arguments[i]]); + } + } + await next(); + }; + }, + limitRate(op, period_secs, max_operations) { + return async (ctx, next) => { + await opcount.inc(op, ctx.request.ip, period_secs, max_operations); + await next(); + }; + } +}; \ No newline at end of file diff --git a/hydro/handler/user.js b/hydro/handler/user.js new file mode 100644 index 000000000..80a6e4f14 --- /dev/null +++ b/hydro/handler/user.js @@ -0,0 +1,182 @@ +const + { GET, POST } = require('../service/server.js'), + user = require('../model/user'), + token = require('../model/token'), + system = require('../model/system'), + mail = require('../lib/mail'), + validator = require('../lib/validator'), + options = require('../options'), + { PRIV_REGISTER_USER, PRIV_USER_PROFILE } = require('../privilege'), + { requirePriv, limitRate } = require('./tools'), + { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError } = require('../error'); + +GET('/user', async ctx => { + let udoc = await user.getById(ctx.session.uid); + ctx.body = { udoc }; +}); +POST('/login', async ctx => { + let { username, password, rememberme } = ctx.request.body; + let udoc = await user.getByUname(username); + if (!udoc) throw new LoginError(username); + if (udoc) udoc.checkPassword(password); + await user.setById(udoc._id, { loginat: new Date(), loginip: ctx.request.ip }); + udoc.salt = ''; + udoc.password = ''; + console.log(udoc); + ctx.session.uid = udoc._id; + ctx.session.rememberme = rememberme; + ctx.body = { udoc }; +}); +POST('/logout', requirePriv(PRIV_USER_PROFILE), async ctx => { + ctx.session = { uid: 1 }; + ctx.body = {}; +}); +GET('/register/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + let code = ctx.request.body.code; + let { mail } = await token.get(code, token.TYPE_REGISTRATION); + if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); + ctx.body = { mail }; +}); +GET('/register/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + let { code, password, verify_password, uname } = ctx.request.body; + let { mail } = await token.get(code, token.TYPE_REGISTRATION); + if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); + if (password != verify_password) throw new VerifyPasswordError(); + let uid = await system.incUserCounter(); + await user.add(uid, uname, password, mail, ctx.remote_ip); + await token.delete(code, token.TYPE_REGISTRATION); + ctx.session.uid = uid; + ctx.body = {}; +}); + +if (options.smtp.user) { + POST('/register', requirePriv(PRIV_REGISTER_USER), limitRate('send_mail', 3600, 30), async ctx => { + let email = ctx.request.body.email; + validator.check_mail(email); + if (await user.get_by_mail(email)) throw new UserAlreadyExistError(email); + let rid = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { mail: email }); + let m = await ctx.render('user_register_mail', { url: `/register/${rid}` }, true); + await mail.send_mail(email, 'Sign Up', 'user_register_mail', m); + ctx.body = {}; + }); + POST('/lostpass', requirePriv(PRIV_REGISTER_USER), limitRate('send_mail', 3600, 30), async ctx => { + let email = ctx.request.body.mail; + validator.check_mail(email); + let udoc = await user.getByEmail(email); + if (!udoc) throw new UserNotFoundError(email); + let tid = await token.add( + token.TYPE_LOSTPASS, + options.lostpass_token_expire_seconds, + { uid: udoc._id } + ); + let m = await ctx.render('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }, true); + await mail.send_mail(email, 'Lost Password', 'user_lostpass_mail', m); + ctx.body = {}; + }); + GET('/lostpass/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + let code = ctx.params.code; + let tdoc = await token.get(code, token.TYPE_LOSTPASS); + if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); + let udoc = await user.getById(tdoc.uid); + ctx.body = { uname: udoc.uname }; + }); + POST('/lostpass/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + let code = ctx.params.code; + let password = ctx.request.body.password; + let verify_password = ctx.request.body.verify_password; + let tdoc = await token.get(code, token.TYPE_LOSTPASS); + if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); + if (password != verify_password) throw new VerifyPasswordError(); + await user.set_password(tdoc.uid, password); + await token.delete(code, token.TYPE_LOSTPASS); + ctx.redirect('/'); + }); +} else + POST('/register', requirePriv(PRIV_REGISTER_USER), async ctx => { + let email = ctx.request.body.email; + validator.check_mail(email); + if (await user.get_by_mail(email)) throw new UserAlreadyExistError(email); + let token = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { mail: email }); + ctx.body = { token }; + }); + +/* + +@app.route('/user/{uid:-?\d+}', 'user_detail') +class UserDetailHandler(base.Handler, UserSettingsMixin): + @base.route_argument + @base.sanitize + async def get(self, *, uid: int): + is_self_profile = self.has_priv(builtin.PRIV_USER_PROFILE) and self.user['_id'] == uid + udoc = await user.get_by_uid(uid) + if not udoc: + raise error.UserNotFoundError(uid) + dudoc, sdoc = await asyncio.gather(domain.get_user(self.domainId, udoc['_id']), + token.get_most_recent_session_by_uid(udoc['_id'])) + + rdocs = record.get_multi(get_hidden=self.has_priv(builtin.PRIV_VIEW_HIDDEN_RECORD), + uid=uid).sort([('_id', -1)]) + rdocs = await rdocs.limit(10).to_list() + pdict = await problem.get_dict_multi_domain((rdoc['domainId'], rdoc['pid']) for rdoc in rdocs) + # check hidden problem + if not self.has_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN): + f = {'hidden': False} + else: + f = {} + pdocs = problem.get_multi(domainId=self.domainId, owner_uid=uid, **f).sort([('_id', -1)]) + pcount = await pdocs.count() + pdocs = await pdocs.limit(10).to_list() + + psdocs = problem.get_multi_solution_by_uid(self.domainId, uid) + psdocs_hot = problem.get_multi_solution_by_uid(self.domainId, uid) + pscount = await psdocs.count() + psdocs = await psdocs.limit(10).to_list() + psdocs_hot = await psdocs_hot.sort([('vote', -1), ('doc_id', -1)]).limit(10).to_list() + + if self.has_perm(builtin.PERM_VIEW_DISCUSSION): + ddocs = discussion.get_multi(self.domainId, owner_uid=uid) + dcount = await ddocs.count() + ddocs = await ddocs.limit(10).to_list() + vndict = await discussion.get_dict_vnodes(self.domainId, map(discussion.node_id, ddocs)) + else: + ddocs = [] + vndict = {} + dcount = 0 + + self.render('user_detail.html', is_self_profile=is_self_profile, + udoc=udoc, dudoc=dudoc, sdoc=sdoc, + rdocs=rdocs, pdict=pdict, pdocs=pdocs, pcount=pcount, + psdocs=psdocs, pscount=pscount, psdocs_hot=psdocs_hot, + ddocs=ddocs, dcount=dcount, vndict=vndict) + + +@app.route('/user/search', 'user_search') +class UserSearchHandler(base.Handler): + def modify_udoc(self, udict, key): + udoc = udict[key] + gravatar_url = misc.gravatar_url(udoc.get('gravatar')) + if 'gravatar' in udoc and udoc['gravatar']: + udict[key] = {**udoc, + 'gravatar_url': gravatar_url, + 'gravatar': ''} + + @base.requirePriv(builtin.PRIV_USER_PROFILE) + @base.get_argument + @base.route_argument + @base.sanitize + async def get(self, *, q: str, exact_match: bool=False): + if exact_match: + udocs = [] + else: + udocs = await user.get_prefix_list(q, user.PROJECTION_PUBLIC, 20) + try: + udoc = await user.get_by_uid(int(q), user.PROJECTION_PUBLIC) + if udoc: + udocs.insert(0, udoc) + except ValueError as e: + pass + for i in range(len(udocs)): + self.modify_udoc(udocs, i) + self.json(udocs) + +*/ diff --git a/hydro/install.js b/hydro/install.js new file mode 100644 index 000000000..c5a20f0b8 --- /dev/null +++ b/hydro/install.js @@ -0,0 +1,50 @@ +const + Mongo = require('mongodb'), + { pwhash } = require('./utils'), + priv = require('./privilege'), + builtin = require('./model/builtin'), + options = require('./lib/options'); + +async function run() { + let mongourl = 'mongodb://'; + if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; + mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; + let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); + let db = Database.db(options.db.name); + let coll_user = db.collection('user'); + let coll_domain = db.collection('domain'); + let coll_domain_user = db.collection('domain_user'); + let coll_blacklist = db.collection('blacklist'); + async function createRootUser() { + let salt = pwhash.salt(); + await coll_user.insertOne({ + _id: -1, + email: 'root@hydro', + emailLower: 'root@hydro', + uname: 'Root', + unameLower: 'root', + password: pwhash.hash('root', salt), + salt, + regat: new Date(), + regip: '127.0.0.1', + loginat: new Date(), + loginip: '127.0.0.1', + priv: priv.PRIV_ALL, + gravatar: 'root@hydro' + }); + await coll_domain_user.insertOne({ domainId: 'system', uid: -1, role: 'admin', joinAt: new Date() }); + } + await coll_user.createIndex('unameLower', { unique: true }); + await coll_user.createIndex('emailLower', { sparse: true }); + await coll_user.insertMany(builtin.BUILTIN_USERS); + await coll_domain.insertMany(builtin.BUILTIN_DOMAINS); + await coll_blacklist.createIndex('expireAt', { expireAfterSeconds: 0 }); + await createRootUser(); + console.log('Installed'); + process.exit(0); +} + +run().catch(e => { + console.error(e); + process.exit(1); +}); \ No newline at end of file diff --git a/hydro/interface.ts b/hydro/interface.ts new file mode 100644 index 000000000..beee4c3fb --- /dev/null +++ b/hydro/interface.ts @@ -0,0 +1,55 @@ +export class User { + _id: number + email: string + emailLower: string + uname: string + unameLower: string + salt: string + hash: string + priv: number + domainId: string = 'system' + displayName: string = '' + nAccept: number = 0 + nSubmit: number = 0 + perm: string = '0' + constructor(user, domainUser) { } + hasPriv(priv) { } + hasPerm(perm) { } + checkPassword(password) { } + async joinDomain(role, joinAt) { } +} +export interface TestCase { + time: number, + memory: number, + status: number, + message: string +} +export interface Record { + _id: import('bson').ObjectID, + domainId: string, + pid: string | boolean, + creator: number, + lang: string, + code: string, + score: number, + memory: number, + time: number, + judgeTexts: string[], + compilerTexts: string[], + testCases: TestCase[], + judger: string, + judgeAt: Date, + status: number +} +export interface Problem { + _id: number | string, + domainId: string, + title: string, + content: string, + timeLimit: number, + memoryLimit: number, + nSubmit: number, + nAccept: number, + tags: string[], + categories: string[] +} \ No newline at end of file diff --git a/hydro/lib/mail.js b/hydro/lib/mail.js new file mode 100644 index 000000000..aa49cf2f8 --- /dev/null +++ b/hydro/lib/mail.js @@ -0,0 +1,45 @@ +const + nodemailer = require('nodemailer'), + options = require('../options'), + { SendMailError } = require('../error'); + +let transporter = null; +try { + transporter = nodemailer.createTransport({ + host: options.VJ_SMTP_HOST, + port: options.VJ_DB_PORT, + secure: options.VJ_SMTP_SECURE, + auth: { + user: options.VJ_SMTP_USER, + pass: options.VJ_SMTP_PASSWORD + } + }); +} catch (e) { + console.error(e); +} + +module.exports = { + /** + * @param {string} to + * @param {string} subject + * @param {string} text + * @param {string} html + */ + async send_mail(to, subject, text, html) { + let t; + try { + t = await new Promise((resolve, reject) => { + transporter.sendMail({ + from: options.VJ_SMTP_FROM, + to, subject, text, html + }, (err, info) => { + if (err) reject(err); + else resolve(info); + }); + }); + } catch (e) { + throw new SendMailError(to); + } + return t; + } +}; \ No newline at end of file diff --git a/hydro/lib/validator.js b/hydro/lib/validator.js new file mode 100644 index 000000000..f3cc2f82d --- /dev/null +++ b/hydro/lib/validator.js @@ -0,0 +1,118 @@ +const + { ValidationError } = require('../error'), + RE_UID = /^-?\d+$/i, + RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i, + RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i, + RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i; + +const + isTitle = s => s && s.length < 64, + checkTitle = s => { if (!isTitle(s)) throw new ValidationError('title'); }, + isUid = s => RE_UID.test(s), + checkUid = s => { if (!isUid(s)) throw new ValidationError('uid'); }, + isUname = s => RE_UNAME.test(s), + checkUname = s => { if (!isUname(s)) throw new ValidationError('uname'); }, + isPassword = s => s.length >= 5, + checkPassword = s => { if (!isPassword(s)) throw new ValidationError('password'); }, + isEmail = s => RE_MAIL.test(s), + checkEmail = s => { if (!isEmail(s)) throw new ValidationError('mail'); }, + isContent = s => s && s.length < 65536, + checkContent = s => { if (!isContent(s)) throw new ValidationError('content'); }, + isName = s => s && s.length < 256, + checkName = s => { if (!isName(s)) throw new ValidationError('name'); }, + isPid = s => RE_PID.test(s.toString()), + checkPid = s => { if (!RE_PID.test(s)) throw new ValidationError('pid'); }; + +module.exports = { + isTitle, checkTitle, + isUid, checkUid, + isUname, checkUname, + isPassword, checkPassword, + isEmail, checkEmail, + isContent, checkContent, + isName, checkName, + isPid, checkPid +}; +/* +domainId_RE = re.compile(r'[_A-Za-z][_0-9A-Za-z]{3,255}') +ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?') +ROLE_RE = re.compile(r'[_0-9A-Za-z]{1,256}') +DOMAIN_INVITATION_CODE_RE = re.compile(r'[0-9A-Za-z]{1,64}') + + +def is_domainId(s): + return bool(domainId_RE.fullmatch(s)) + + +def check_domainId(s): + if not is_domainId(s): + raise error.ValidationError('domainId') + + +def is_id(s): + return bool(ID_RE.fullmatch(s)) + + +def check_category_name(s): + if not is_id(s): + raise error.ValidationError('category_name') + + +def check_node_name(s): + if not is_id(s): + raise error.ValidationError('node_name') + + +def is_role(s): + return bool(ROLE_RE.fullmatch(s)) + + +def check_role(s): + if not is_role(s): + raise error.ValidationError('role') + +def is_intro(s): + return isinstance(s, str) and 0 < len(s.strip()) < 500 + + +def check_intro(s): + if not is_intro(s): + raise error.ValidationError('intro') + + +def is_description(s): + return isinstance(s, str) and len(s) < 65536 + + +def check_description(s): + if not is_description(s): + raise error.ValidationError('description') + + +def is_bulletin(s): + return isinstance(s, str) and len(s) < 65536 + + +def check_bulletin(s): + if not is_bulletin(s): + raise error.ValidationError('bulletin') + + +def is_lang(i): + return i in constant.language.LANG_TEXTS + + +def check_lang(i): + if not is_lang(i): + raise error.ValidationError('lang') + + +def is_domain_invitation_code(s): + return bool(DOMAIN_INVITATION_CODE_RE.fullmatch(s)) + + +def check_domain_invitation_code(s): + if not is_domain_invitation_code(s): + raise error.ValidationError('invitation_code') + +*/ \ No newline at end of file diff --git a/hydro/model/blacklist.js b/hydro/model/blacklist.js new file mode 100644 index 000000000..bb78cd7e5 --- /dev/null +++ b/hydro/model/blacklist.js @@ -0,0 +1,12 @@ +const + db = require('../service/db.js'), + coll = db.collection('blacklist'); + +module.exports = { + add(ip) { + let expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000); + return coll.findOneAndUpdate({ _id: ip }, { $set: { expireAt } }, { upsert: true }); + }, + get: ip => coll.findOne({ _id: ip }), + delete: ip => coll.deleteOne({ _id: ip }) +}; \ No newline at end of file diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js new file mode 100644 index 000000000..0a99dc400 --- /dev/null +++ b/hydro/model/builtin.js @@ -0,0 +1,52 @@ +const + perm = require('../permission'), + priv = require('../privilege'); +const BUILTIN_USERS = [ + { + _id: 0, + uname: 'Hydro', + unameLower: 'hydro', + email: '', + emailLower: '', + salt: '', + hash: '', + gender: 'other', + regat: new Date(0), + regip: '127.0.0.1', + priv: priv.PRIV_NONE, + gravatar: '', + loginat: new Date(), + loginip: '127.0.0.1' + }, + { + _id: 1, + email: 'guest@hydro', + emailLower: 'guest@hydro', + uname: 'Guest', + unameLower: 'guest', + hash: '', + salt: '', + regat: new Date(), + regip: '127.0.0.1', + loginat: new Date(), + loginip: '127.0.0.1', + priv: priv.PRIV_DEFAULT, + gravatar: 'guest@hydro' + } +]; +const BUILTIN_DOMAINS = [{ + _id: 'system', + owner: 0, + roles: { + guest: perm.PERM_BASIC, + default: perm.PERM_DEFAULT, + member: perm.PERM_DEFAULT, + admin: perm.PERM_ADMIN + }, + gravatar: '', + name: 'System', + bulletin: '' +}]; +module.exports = { + BUILTIN_USERS, BUILTIN_DOMAINS +}; \ No newline at end of file diff --git a/hydro/model/contest.js b/hydro/model/contest.js new file mode 100644 index 000000000..3b59edadc --- /dev/null +++ b/hydro/model/contest.js @@ -0,0 +1,338 @@ +const + domain = require('./domain'), + validator = require('../lib/validator'), + { ValidationError, ContestNotFoundError } = require('../error'), + RULE_HOMEWORK = require('../module/contest/homework'), + RULE_OI = require('../module/contest/oi'), + RULE_ACM = require('../module/contest/acm'), + db = require('../service/db.js'), + coll = db.collection('contest'), + coll_status = db.collection('contest.status'); + + +const RULES = { + homework: RULE_HOMEWORK, + oi: RULE_OI, + acm: RULE_ACM +}; + +module.exports = { + RULES, + async add(domainId, title, content, owner, rule, + beginAt = new Date(), endAt = new Date(), pids = [], data) { + validator.checkTitle(title); + validator.checkContent(content); + if (!this.RULES[rule]) throw new ValidationError('rule'); + if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); + Object.assign(data, { domainId, content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); + this.RULES[rule].check(data); + return await coll.insertOne(data); + }, + count: query => coll.find(query).count(), + async edit(domainId, cid, $set) { + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkIntro($set.content); + if ($set.rule) + if (!this.RULES[$set.rule]) throw new ValidationError('rule'); + if ($set.beginAt && $set.endAt) + if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt'); + let cdoc = await coll.findOne({ domainId, cid }); + if (!cdoc) throw new ContestNotFoundError(domainId, cid); + this.RULES[$set.rule || cdoc.rule].check(Object.assign(cdoc, $set)); + await coll.findOneAndUpdate({ domainId, cid }, { $set }); + return cdoc; + }, + async get(domainId, cid) { + let tdoc = await coll.findOne({ domainId, cid }); + if (!tdoc) throw new ContestNotFoundError(domainId, cid); + return tdoc; + }, + get_multi: (domainId, query, fields) => coll.find(Object.assign({ domainId }, query), { fields }), + get_multi_status: (query, fields) => coll.find(query, { fields }), + async get_random_id(query) { + let pdocs = coll.find(query); + let pcount = await pdocs.count(); + if (pcount) { + let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; + return pdoc.pid; + } else return null; + }, + get_status: (domainId, cid, uid, fields) => coll_status.findOne({ domainId, cid, uid }, { fields }), + set_status: (domainId, cid, uid, $set) => coll_status.findOneAndUpdate({ domainId, cid, uid }, { $set }) +}; + +/* + +journal_key_func = lambda j: j['rid'] + +Rule = collections.namedtuple('Rule', ['show_record_func', + 'show_scoreboard_func', + 'stat_func', + 'status_sort', + 'rank_func', + 'scoreboard_func']) + +def _oi_equ_func(a, b): + return a.get('score', 0) == b.get('score', 0) + +RULES = { + constant.contest.RULE_OI: Rule(lambda tdoc, now: now > tdoc['end_at'], + lambda tdoc, now: now > tdoc['end_at'], + _oi_stat, + [('score', -1)], + functools.partial(rank.ranked, equ_func=_oi_equ_func), + _oi_scoreboard), + constant.contest.RULE_ACM: Rule(lambda tdoc, now: now >= tdoc['begin_at'], + lambda tdoc, now: now >= tdoc['begin_at'], + _acm_stat, + [('accept', -1), ('time', 1)], + functools.partial(enumerate, start=1), + _acm_scoreboard), + constant.contest.RULE_ASSIGNMENT: Rule(lambda tdoc, now: now >= tdoc['begin_at'], + lambda tdoc, now: False, # TODO: show scoreboard according to assignment preference + _assignment_stat, + [('penalty_score', -1), ('time', 1)], + functools.partial(enumerate, start=1), + _assignment_scoreboard), +} + +def get_multi(domainId: str, doc_type: int, fields=None, **kwargs): + # TODO(twd2): projection. + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + return document.get_multi(domainId=domainId, + doc_type=doc_type, + fields=fields, + **kwargs) \ + .sort([('doc_id', -1)]) + + +@argmethod.wrap +async def attend(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int): + # TODO(iceboy): check time. + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + try: + await document.capped_inc_status(domainId, doc_type, cid, + uid, 'attend', 1, 0, 1) + except errors.DuplicateKeyError: + if doc_type == document.TYPE_CONTEST: + raise error.ContestAlreadyAttendedError(domainId, cid, uid) from None + elif doc_type == document.TYPE_HOMEWORK: + raise error.HomeworkAlreadyAttendedError(domainId, cid, uid) from None + return await document.inc(domainId, doc_type, cid, 'attend', 1) + + +@argmethod.wrap +async def get_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int, fields=None): + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + return await document.get_status(domainId, doc_type, doc_id=cid, + uid=uid, fields=fields) + + +def get_multi_status(doc_type: int, *, fields=None, **kwargs): + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + return document.get_multi_status(doc_type=doc_type, + fields=fields, **kwargs) + + +async def get_dict_status(domainId, uid, doc_type, cids, *, fields=None): + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + result = dict() + async for tsdoc in get_multi_status(domainId=domainId, + uid=uid, + doc_type=doc_type, + doc_id={'$in': list(set(cids))}, + fields=fields): + result[tsdoc['doc_id']] = tsdoc + return result + + +@argmethod.wrap +async def get_and_list_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, fields=None): + # TODO(iceboy): projection, pagination. + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + tdoc = await get(domainId, doc_type, cid) + tsdocs = await document.get_multi_status(domainId=domainId, + doc_type=doc_type, + doc_id=tdoc['doc_id'], + fields=fields) \ + .sort(RULES[tdoc['rule']].status_sort) \ + .to_list() + return tdoc, tsdocs + + +def _get_status_journal(tsdoc): + # Sort and uniquify journal of the contest status document, by rid. + return [list(g)[-1] for _, g in itertools.groupby(sorted(tsdoc['journal'], key=journal_key_func), + key=journal_key_func)] + + +@argmethod.wrap +async def update_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int, + rid: objeccid.Objeccid, pid: document.convert_doc_id, + accept: bool, score: int): + """This method returns None when the modification has been superseded by a parallel operation.""" + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + tdoc = await document.get(domainId, doc_type, cid) + tsdoc = await document.rev_push_status( + domainId, tdoc['doc_type'], tdoc['doc_id'], uid, + 'journal', {'rid': rid, 'pid': pid, 'accept': accept, 'score': score}) + if 'attend' not in tsdoc or not tsdoc['attend']: + if tdoc['doc_type'] == document.TYPE_CONTEST: + raise error.ContestNotAttendedError(domainId, cid, uid) + elif tdoc['doc_type'] == document.TYPE_HOMEWORK: + raise error.HomeworkNotAttendedError(domainId, cid, uid) + else: + raise error.InvalidArgumentError('doc_type') + + journal = _get_status_journal(tsdoc) + stats = RULES[tdoc['rule']].stat_func(tdoc, journal) + tsdoc = await document.rev_set_status(domainId, tdoc['doc_type'], cid, uid, tsdoc['rev'], + journal=journal, **stats) + return tsdoc + + +@argmethod.wrap +async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + tdoc = await document.get(domainId, doc_type, cid) + async with document.get_multi_status(domainId=domainId, + doc_type=doc_type, + doc_id=tdoc['doc_id']) as tsdocs: + async for tsdoc in tsdocs: + if 'journal' not in tsdoc or not tsdoc['journal']: + continue + journal = _get_status_journal(tsdoc) + stats = RULES[tdoc['rule']].stat_func(tdoc, journal) + await document.rev_set_status(domainId, doc_type, cid, tsdoc['uid'], tsdoc['rev'], + return_doc=False, journal=journal, **stats) + + +def _parse_pids(pids_str): + pids = misc.dedupe(map(document.convert_doc_id, pids_str.split(','))) + return pids + + +def _format_pids(pids_list): + return ','.join([str(pid) for pid in pids_list]) + + + +class ContestStatusMixin(object): + @property + @functools.lru_cache() + def now(self): + # TODO(iceboy): This does not work on multi-machine environment. + return datetime.datetime.utcnow() + + def is_new(self, tdoc): + ready_at = tdoc['begin_at'] - datetime.timedelta(days=1) + return self.now < ready_at + + def is_upcoming(self, tdoc): + ready_at = tdoc['begin_at'] - datetime.timedelta(days=1) + return ready_at <= self.now < tdoc['begin_at'] + + def is_not_started(self, tdoc): + return self.now < tdoc['begin_at'] + + def is_ongoing(self, tdoc): + return tdoc['begin_at'] <= self.now < tdoc['end_at'] + + def is_done(self, tdoc): + return tdoc['end_at'] <= self.now + + def is_homework_extended(self, tdoc): + return tdoc['penalty_since'] <= self.now < tdoc['end_at'] + + def status_text(self, tdoc): + if self.is_new(tdoc): + return 'New' + elif self.is_upcoming(tdoc): + return 'Ready (☆▽☆)' + elif self.is_ongoing(tdoc): + return 'Live...' + else: + return 'Done' + + def get_status(self, tdoc): + if self.is_not_started(tdoc): + return 'not_started' + elif self.is_ongoing(tdoc): + return 'ongoing' + else: + return 'finished' + + +class ContestVisibilityMixin(object): + def can_view_hidden_scoreboard(self, tdoc): + if self.domainId != tdoc['domainId']: + return False + if tdoc['doc_type'] == document.TYPE_CONTEST: + return self.has_perm(builtin.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) + elif tdoc['doc_type'] == document.TYPE_HOMEWORK: + return self.has_perm(builtin.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) + else: + return False + + def can_show_record(self, tdoc, allow_perm_override=True): + if RULES[tdoc['rule']].show_record_func(tdoc, datetime.datetime.utcnow()): + return True + if allow_perm_override and self.can_view_hidden_scoreboard(tdoc): + return True + return False + + def can_show_scoreboard(self, tdoc, allow_perm_override=True): + if RULES[tdoc['rule']].show_scoreboard_func(tdoc, datetime.datetime.utcnow()): + return True + if allow_perm_override and self.can_view_hidden_scoreboard(tdoc): + return True + return False + + +class ContestCommonOperationMixin(object): + async def get_scoreboard(self, doc_type: int, cid: objeccid.Objeccid, is_export: bool=False): + if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: + raise error.InvalidArgumentError('doc_type') + tdoc, tsdocs = await get_and_list_status(self.domainId, doc_type, cid) + if not self.can_show_scoreboard(tdoc): + if doc_type == document.TYPE_CONTEST: + raise error.ContestScoreboardHiddenError(self.domainId, cid) + elif doc_type == document.TYPE_HOMEWORK: + raise error.HomeworkScoreboardHiddenError(self.domainId, cid) + udict, dudict, pdict = await asyncio.gather( + user.get_dict([tsdoc['uid'] for tsdoc in tsdocs]), + domain.get_dict_user_by_uid(self.domainId, [tsdoc['uid'] for tsdoc in tsdocs]), + problem.get_dict(self.domainId, tdoc['pids'])) + ranked_tsdocs = RULES[tdoc['rule']].rank_func(tsdocs) + rows = RULES[tdoc['rule']].scoreboard_func(is_export, self.translate, tdoc, + ranked_tsdocs, udict, dudict, pdict) + return tdoc, rows, udict + + async def verify_problems(self, pids): + pdocs = await problem.get_multi(domainId=self.domainId, doc_id={'$in': pids}, + fields={'doc_id': 1}) \ + .sort('doc_id', 1) \ + .to_list() + exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + if len(pids) != len(exist_pids): + for pid in pids: + if pid not in exist_pids: + raise error.ProblemNotFoundError(self.domainId, pid) + return pids + + async def hide_problems(self, pids): + await asyncio.gather(*[problem.set_hidden(self.domainId, pid, True) for pid in pids]) + + +class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin): + pass + +*/ \ No newline at end of file diff --git a/hydro/model/domain.js b/hydro/model/domain.js new file mode 100644 index 000000000..dbc4444d2 --- /dev/null +++ b/hydro/model/domain.js @@ -0,0 +1,315 @@ +const + validator = require('../utils').validator, + user = require('./user'), + { DomainAlreadyExistError, DomainNotFoundError } = require('../error'), + db = require('../service/db.js'), + coll = db.collection('domain'); + +module.exports = { + /** + * @param {string} domainId + * @param {int} owner + * @param {roles} roles + * @param {str} name + * @param {str} gravatar + * @param {str} bulletin + */ + async add(domainId, owner, roles, name, gravatar, bulletin = '') { + validator.checkDomainId(domainId); + validator.checkName(name); + validator.checkBulletin(bulletin); + try { + let result = await coll.insertOne({ + _id: domainId, pending: true, owner, roles, name, gravatar, bulletin + }); + domainId = result.insertedId; + } catch (e) { + throw new DomainAlreadyExistError(domainId); + } + await (await user.getById(owner, domainId)).joinDomain('root'); + await coll.updateOne({ _id: domainId }, { $unset: { pending: '' } }); + return domainId; + }, + /** + * @param {string} domainId + * @param {object} fields + */ + async get(domainId, fields) { + let ddoc = await coll.findOne({ _id: domainId }, fields); + if (!ddoc) throw new DomainNotFoundError(domainId); + return ddoc; + }, + /** + * Increments the problem ID counter. + * @param {string} domainId + * @returns {number} Integer value before increment. + */ + async incPidCounter(domainId) { + await coll.updateOne({ _id: domainId, pidCounter: { $exists: false } }, + { $set: { pidCounter: 1000 } }); + let doc = await coll.findOneAndUpdate({ _id: domainId }, { $inc: { pidCounter: 1 } }); + return doc.value.pidCounter; + }, + async incUser(domainId, uid, $inc) { + let res = await coll.findOneAndUpdate({ domainId, uid }, { $inc }, { upsert: true }); + return res.value; + } +}; +/* + +async def add_continue(domainId: str, ensure_owner_uid: int=None): + ddoc = await get(domainId) + if 'pending' not in ddoc: + raise error.DomainNotFoundError(domainId) + owner_uid = ddoc['owner_uid'] + if ensure_owner_uid != None and owner_uid != ensure_owner_uid: + raise error.DomainNotFoundError(domainId) + try: + await add_user_role(domainId, owner_uid, builtin.ROLE_ROOT) + except error.UserAlreadyDomainMemberError: + pass + coll = db.coll('domain') + await coll.update_one({'_id': domainId}, + {'$unset': {'pending': ''}}) + +def get_multi(*, fields=None, **kwargs): + coll = db.coll('domain') + return coll.find(kwargs, fields) + + +@argmethod.wrap +async def get_list(*, fields=None, limit: int=None, **kwargs): + coll = db.coll('domain') + return await coll.find(kwargs, fields).limit(limit).to_list(None) + + +def get_pending(**kwargs): + return get_multi(pending=True, **kwargs) + + +@argmethod.wrap +async def edit(domainId: str, **kwargs): + for domain in builtin.DOMAINS: + if domain['_id'] == domainId: + raise error.BuiltinDomainError(domainId) + coll = db.coll('domain') + if 'owner_uid' in kwargs: + del kwargs['owner_uid'] + if 'name' in kwargs: + validator.check_name(kwargs['name']) + # TODO(twd2): check kwargs + return await coll.find_one_and_update(filter={'_id': domainId}, + update={'$set': {**kwargs}}, + return_document=ReturnDocument.AFTER) + + +async def unset(domainId, fields): + # TODO(twd2): check fields + coll = db.coll('domain') + return await coll.find_one_and_update(filter={'_id': domainId}, + update={'$unset': dict((f, '') for f in set(fields))}, + return_document=ReturnDocument.AFTER) + + +@argmethod.wrap +async def set_role(domainId: str, role: str, perm: int): + return await set_roles(domainId, {role: perm}) + + +@argmethod.wrap +async def set_roles(domainId: str, roles): + roles = {str(role): int(perm) for role, perm in roles.items()} + update = {} + for role in roles: + validator.check_role(role) + if role in builtin.BUILTIN_ROLE_DESCRIPTORS: + if not builtin.BUILTIN_ROLE_DESCRIPTORS[role].modifiable: + raise error.ModifyBuiltinRoleError(domainId, role) + update['roles.{0}'.format(role)] = roles[role] + for domain in builtin.DOMAINS: + if domain['_id'] == domainId: + raise error.BuiltinDomainError(domainId) + coll = db.coll('domain') + return await coll.find_one_and_update(filter={'_id': domainId}, + update={'$set': update}, + return_document=ReturnDocument.AFTER) + + +@argmethod.wrap +async def delete_role(domainId: str, role: str): + return await delete_roles(domainId, [role]) + + +async def delete_roles(domainId: str, roles): + roles = list(set(roles)) + for role in roles: + validator.check_role(role) + if role in builtin.BUILTIN_ROLE_DESCRIPTORS: + raise error.ModifyBuiltinRoleError(domainId, role) + for domain in builtin.DOMAINS: + if domain['_id'] == domainId: + raise error.BuiltinDomainError(domainId) + user_coll = db.coll('domain.user') + await user_coll.update_many({'domainId': domainId, 'role': {'$in': list(roles)}}, + {'$unset': {'role': ''}}) + coll = db.coll('domain') + return await coll.find_one_and_update(filter={'_id': domainId}, + update={'$unset': dict(('roles.{0}'.format(role), '') + for role in roles)}, + return_document=ReturnDocument.AFTER) + + +@argmethod.wrap +async def transfer(domainId: str, old_owner_uid: int, new_owner_uid: int): + for domain in builtin.DOMAINS: + if domain['_id'] == domainId: + raise error.BuiltinDomainError(domainId) + coll = db.coll('domain') + return await coll.find_one_and_update(filter={'_id': domainId, 'owner_uid': old_owner_uid}, + update={'$set': {'owner_uid': new_owner_uid}}, + return_document=ReturnDocument.AFTER) + + +async def set_user(domainId, uid, **kwargs): + coll = db.coll('domain.user') + return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid}, + update={'$set': kwargs}, + upsert=True, + return_document=ReturnDocument.AFTER) + + +async def unset_user(domainId, uid, fields): + coll = db.coll('domain.user') + return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid}, + update={'$unset': dict((f, '') for f in set(fields))}, + upsert=True, + return_document=ReturnDocument.AFTER) + + +async def set_users(domainId, uids, **kwargs): + coll = db.coll('domain.user') + await coll.update_many({'domainId': domainId, 'uid': {'$in': list(set(uids))}}, + {'$set': kwargs}, + upsert=False) + + +async def unset_users(domainId, uids, fields): + coll = db.coll('domain.user') + await coll.update_many({'domainId': domainId, 'uid': {'$in': list(set(uids))}}, + {'$unset': dict((f, '') for f in set(fields))}, + upsert=True) + + + +@argmethod.wrap +async def set_user_role(domainId: str, uid: int, role: str): + validator.check_role(role) + # use set_users to utilize "upsert=False" + return await set_users(domainId, [uid], role=role) + + +@argmethod.wrap +async def unset_user_role(domainId: str, uid: int): + return await unset_user(domainId, uid, ['role']) + + +async def set_users_role(domainId: str, uids, role: str): + validator.check_role(role) + await set_users(domainId, uids, role=role) + + +async def unset_users_role(domainId: str, uids): + await unset_users(domainId, uids, ['role']) + + +async def inc_user_usage(domainId: str, uid: int, usage_field: str, usage: int, quota: int): + coll = db.coll('domain.user') + try: + return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid, + usage_field: {'$not': {'$gte': quota - usage}}}, + update={'$inc': {usage_field: usage}}, + upsert=True, + return_document=ReturnDocument.AFTER) + except errors.DuplicateKeyError: + raise error.UsageExceededError(domainId, uid, usage_field, usage, quota) + + +def get_multi_user(*, fields=None, **kwargs): + coll = db.coll('domain.user') + return coll.find(kwargs, fields) + + +async def get_dict_user_by_uid(domainId, uids, *, fields=None): + result = dict() + async for dudoc in get_multi_user( + domainId=domainId, uid={'$in': list(set(uids))}, fields=fields): + result[dudoc['uid']] = dudoc + return result + + +async def get_dict_user_by_domainId(uid, *, fields=None): + result = dict() + async for dudoc in get_multi_user(uid=uid, fields=fields): + result[dudoc['domainId']] = dudoc + return result + + +def get_all_roles(ddoc): + builtin_roles = {role: rd.default_permission for role, rd in builtin.BUILTIN_ROLE_DESCRIPTORS.items()} + domain_roles = ddoc['roles'] + return {**builtin_roles, **domain_roles} + + +def get_join_settings(ddoc, now): + if 'join' not in ddoc: + return None + join_settings = ddoc['join'] + if not join_settings: + return None + if join_settings['method'] == constant.domain.JOIN_METHOD_NONE: + return None + if join_settings['role'] not in ddoc['roles']: + return None + if join_settings['expire'] != None and join_settings['expire'] < now: + return None + return join_settings + + +@argmethod.wrap +async def get_prefix_search(prefix: str, fields={}, limit: int=50): + prefix_lower = prefix.lower() + builtin_ddocs = [] + for domain in builtin.DOMAINS: + if domain['_id'].lower().startswith(prefix_lower) or \ + domain['name'].lower().startswith(prefix_lower): + builtin_ddocs.append(domain) + regex = r'\A\Q{0}\E'.format(prefix.replace(r'\E', r'\E\\E\Q')) + coll = db.coll('domain') + ddocs = await coll.find({'$or': [{'_id': {'$regex': regex}}, + {'name': {'$regex': regex}}]}, + projection=fields) \ + .limit(limit) \ + .to_list() + return builtin_ddocs + ddocs + + +@argmethod.wrap +async def ensure_indexes(): + coll = db.coll('domain') + await coll.create_index('owner_uid') + await coll.create_index('name') + user_coll = db.coll('domain.user') + await user_coll.create_index('uid') + await user_coll.create_index([('domainId', 1), + ('uid', 1)], unique=True) + await user_coll.create_index([('domainId', 1), + ('role', 1)], sparse=True) + await user_coll.create_index([('domainId', 1), + ('rp', -1)]) + await user_coll.create_index([('domainId', 1), + ('rank', 1)]) + + +if __name__ == '__main__': + argmethod.invoke_by_args() +*/ \ No newline at end of file diff --git a/hydro/model/opcount.js b/hydro/model/opcount.js new file mode 100644 index 000000000..ad8327119 --- /dev/null +++ b/hydro/model/opcount.js @@ -0,0 +1,35 @@ +const + { OpcountExceededError } = require('../error'), + db = require('../service/db.js'), + coll = db.collection('opcount'); + +module.exports = { + /** + * @param {string} op + * @param {string} ident + * @param {number} period_secs + * @param {number} max_operations + */ + async inc(op, ident, period_secs, max_operations) { + let cur_time = new Date().getTime(); + let begin_at = new Date(cur_time - cur_time % (period_secs * 1000)); + let expire_at = new Date(begin_at.getTime() + period_secs * 1000); + try { + await coll.find_one_and_update({ + ident, begin_at, expire_at, + op: { $not: { $gte: max_operations } } + }, { $inc: { op: 1 } }, { upsert: true }); + } catch (e) { + throw new OpcountExceededError(op, period_secs, max_operations); + } + } +}; +/* +@argmethod.wrap +async def ensure_indexes(): + coll = db.coll('opcount') + await coll.create_index([('ident', 1), + ('begin_at', 1), + ('expire_at', 1)], unique=True) + await coll.create_index('expire_at', expireAfterSeconds=0) +*/ \ No newline at end of file diff --git a/hydro/model/problem.js b/hydro/model/problem.js new file mode 100644 index 000000000..a24dbc799 --- /dev/null +++ b/hydro/model/problem.js @@ -0,0 +1,87 @@ +const + validator = require('../lib/validator'), + { ProblemNotFoundError } = require('../error'), + db = require('../service/db.js'), + coll = db.collection('problem'), + coll_status = db.collection('problem.status'); + +/** + * @param {string} domainId + * @param {string} title + * @param {string} content + * @param {number} owner + * @param {number} pid + * @param {import('bson').ObjectID} data + * @param {string[]} category + * @param {string[]} tag + * @param {boolean} hidden + */ +async function add({ + domainId, + title, + content, + owner, + pid = null, + data = null, + category = [], + tag = [], + hidden = false +}) { + validator.checkTitle(title); + validator.checkContent(content); + await coll.insertOne({ + domainId, + content, + owner, + pid, + title, + data, + category, + tag, + hidden, + nSubmit: 0, + nAccept: 0 + }); + return pid; +} +async function get({ domainId, pid, uid }) { + pid = parseInt(pid) || pid; + let pdoc = await coll.findOne({ domainId, pid }); + if (!pdoc) throw new ProblemNotFoundError(domainId, pid); + pdoc.psdoc = uid ? + await coll_status.findOne({ domainId, pid, uid }) : + null; + return pdoc; +} +async function getMany(query, sort, page, limit) { + return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); +} +async function edit(domainId, pid, $set) { + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkContent($set.content); + await coll.findOneAndUpdate({ domainId, pid }, { $set }); + let pdoc = await coll.findOne({ domainId, pid }); + if (!pdoc) throw new ProblemNotFoundError(domainId, pid); + return pdoc; +} +async function count(query) { + return await coll.find(query).count(); +} +async function random(query) { + let pdocs = coll.find(query); + let pcount = await pdocs.count(); + if (pcount) { + let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; + return pdoc.pid; + } else return null; + +} + +module.exports = { + add, + get, + getMany, + edit, + count, + random +}; \ No newline at end of file diff --git a/hydro/model/record.js b/hydro/model/record.js new file mode 100644 index 000000000..8c64e730f --- /dev/null +++ b/hydro/model/record.js @@ -0,0 +1,55 @@ +const + _ = require('lodash'), + { RecordNotFoundError } = require('../error'), + { STATUS_WAITING } = require('../status'), + db = require('../service/db.js'), + coll = db.collection('record'); + +/** + * @param {import('../interface').Record} data + */ +async function add(data) { + _.defaults(data, { + status: STATUS_WAITING, + score: 0, + time: 0, + memory: 0, + rejudged: false, + judgeTexts: [], + compilerTexts: [], + testCases: [], + judger: null, + judgeAt: null + }); + let res = await coll.insertOne(data); + return res.insertedId; +} +/** + * @param {string} rid + * @returns {import('../interface').Record} + */ +async function get(rid) { + let rdoc = await coll.findOne({ _id: rid }); + if (!rdoc) throw new RecordNotFoundError(rid); + return rdoc; +} +async function getMany(query, sort, page, limit) { + return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); +} +async function update(domainId, rid, $set) { + await coll.findOneAndUpdate({ domainId, _id: rid }, { $set }); + let rdoc = await coll.findOne({ domainId, _id: rid }); + if (!rdoc) throw new RecordNotFoundError(domainId, rid); + return rdoc; +} +async function count(query) { + return await coll.find(query).count(); +} + +module.exports = { + add, + get, + getMany, + update, + count +}; \ No newline at end of file diff --git a/hydro/model/system.js b/hydro/model/system.js new file mode 100644 index 000000000..81093f4f3 --- /dev/null +++ b/hydro/model/system.js @@ -0,0 +1,110 @@ +const + db = require('../service/db.js'), + coll = db.collection('system'); + +async function get(_id) { + let doc = await coll.findOne({ _id }); + if (doc) return doc.value; +} +async function update(_id, update, config) { + await coll.findOneAndUpdate({ _id }, update, config); + return this.get(_id); +} +/** + * Increments the user counter. + * @returns {Number} Integer value after increment. + */ +async function incUserCounter() { + return this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); +} +async function acquireLock(name) { + let value = Math.floor(Math.random() * 0xFFFFFFFF); + try { + await coll.updateOne({ _id: 'lock_' + name, value: 0 }, { $set: { value } }, { upsert: true }); + } catch (e) { return null; } + return value; +} +async function releaseLock(name, value) { + let result = await coll.updateOne({ _id: 'lock_' + name, value }, { $set: { value: 0 } }); + return !!result.matchedCount; +} +async function releaseLockAnyway(name) { + await coll.updateOne({ _id: 'lock_' + name }, { $set: { value: 0 } }); + return true; +} +module.exports = { + get, + update, + incUserCounter, + acquireLock, + releaseLock, + releaseLockAnyway +}; + +/* + +EXPECTED_DB_VERSION = 1 + + +async def acquire_upgrade_lock(): + lock = await acquire_lock('upgrade') + if not lock: + raise error.UpgradeLockAcquireError() + return lock + + +async def release_upgrade_lock(lock: int): + success = await release_lock('upgrade', lock) + if not success: + raise error.UpgradeLockReleaseError() + return True + + +@argmethod.wrap +async def release_upgrade_lock_anyway(): + return await release_lock_anyway('upgrade') + + +@argmethod.wrap +async def get_db_version(): + coll = db.coll('system') + doc = await coll.find_one({'_id': 'db_version'}) + if doc is None: + return 0 + else: + return doc['value'] + + +async def set_db_version(version: int): + coll = db.coll('system') + result = await coll.update_one(filter={'_id': 'db_version'}, + update={'$set': {'value': version}}, + upsert=True) + return result.modified_count + + +async def ensure_db_version(allowed_version=None): + if allowed_version is None: + allowed_version = EXPECTED_DB_VERSION + current_version = await get_db_version() + if current_version != allowed_version: + raise error.DatabaseVersionMismatchError(current_version, allowed_version) + + +@argmethod.wrap +async def setup(): + """ + Set up for fresh install + """ + coll = db.coll('system') + fdoc = await coll.find_one({'_id': 'user_counter'}) + if fdoc: + # skip if not fresh install + return + await set_db_version(EXPECTED_DB_VERSION) + + +if __name__ == '__main__': + argmethod.invoke_by_args() + +*/ \ No newline at end of file diff --git a/hydro/model/token.js b/hydro/model/token.js new file mode 100644 index 000000000..8718e0eb8 --- /dev/null +++ b/hydro/model/token.js @@ -0,0 +1,72 @@ +const + db = require('../service/db.js'), + coll = db.collection('token'); + +module.exports = { + TYPE_SESSION: 0, + TYPE_REGISTER: 2, + /** + * Add a token. + * @param {number} tokenType type of the token. + * @param {number} expireSeconds expire time, in seconds. + * @param {object} data extra data. + * @returns {string} token ID + */ + async add(tokenType, expireSeconds, data) { + let now = new Date(); + let str = String.random(32); + let res = await coll.insertOne(Object.assign({}, data, { + _id: str, + tokenType, + createAt: now, + updateAt: now, + expireAt: new Date(now.getTime() + expireSeconds * 1000) + })); + return [str, res.ops]; + }, + + /** + * Get a token. + * @param {string} tokenId token ID. + * @param {number} tokenType type of the token. + * @returns {object} The token document, or null. + */ + get(tokenId, tokenType) { + return coll.findOne({ _id: tokenId, tokenType }); + }, + + /** + * Update a token. + * @param {string} tokenId token ID. + * @param {number} tokenType type of the token. + * @param {number} expireSeconds expire time, in seconds. + * @param {object} data extra data. + * @returns {object} The token document, or null. + */ + async update(tokenId, tokenType, expireSeconds, data) { + let now = new Date(); + let res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, { + $set: Object.assign({ + updateAt: now, + expireAt: new Date(now.getTime() + expireSeconds * 1000) + }, data, { tokenType }) + }); + return res.value; + }, + + /** + * Delete a token. + * @param {string} tokenId token ID. + * @param {int} tokenType type of the token. + * @returns {boolean} true if deleted, or false. + */ + async delete(tokenId, tokenType) { + let result = await coll.delete_one({ _id: tokenId, tokenType }); + return !!result.deletedCount; + }, + + init: () => Promise.all([ + coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }), + coll.createIndex('expireAt', { expireAfterSeconds: 0 }) + ]) +}; \ No newline at end of file diff --git a/hydro/model/training.js b/hydro/model/training.js new file mode 100644 index 000000000..384bc6bcd --- /dev/null +++ b/hydro/model/training.js @@ -0,0 +1,78 @@ +const + domain = require('./domain'), + validator = require('../lib/validator'), + { ValidationError, TrainingNotFoundError } = require('../error'), + db = require('../service/db.js'), + coll = db.collection('traning'), + coll_status = db.collection('training.status'); + +module.exports = { + SETTING_DIFFICULTY_ALGORITHM: 0, + SETTING_DIFFICULTY_ADMIN: 1, + SETTING_DIFFICULTY_AVERAGE: 2, + + async add(domainId, title, content, owner_uid, dag = [], desc = '') { + validator.check_title(title); + validator.check_intro(content); + validator.check_description(desc); + for (let node of dag) + for (let nid of node['require_nids']) + if (nid >= node._id) + throw new ValidationError('dag'); + return await coll.insertOne({ + domainId, content, owner_uid, + dag, title, desc, enroll: 0 + }); + }, + count: (domainId, query) => coll.find(Object.assign({ domainId }, query)).count(), + async edit(domainId, tid, $set) { + if ($set.title) validator.check_title($set.title); + if ($set.content) validator.check_intro($set.content); + if ($set.desc) validator.check_description($set.desc); + if ($set.dag) + for (let node of $set.dag) + for (let nid of node.require_nids) + if (nid >= node._id) throw new ValidationError('dag'); + await coll.findOneAndUpdate({ domainId, tid }, { $set }); + let tdoc = await coll.findOne({ domainId, tid }); + if (!tdoc) throw new TrainingNotFoundError(domainId, tid); + return tdoc; + }, + async get(domainId, tid) { + let tdoc = await coll.findOne({ domainId, tid }); + if (!tdoc) throw new TrainingNotFoundError(domainId, tid); + return tdoc; + }, + get_multi: (domainId, query) => coll.find(Object.assign({ domainId }, query)), + get_multi_status: (query) => coll.find(query), + async get_random_id(query) { + let pdocs = coll.find(query); + let pcount = await pdocs.count(); + if (pcount) { + let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; + return pdoc.pid; + } else return null; + }, + get_status: (domainId, tid, uid) => coll_status.findOne({ domainId, tid, uid }), + set_status: (domainId, tid, uid, $set) => coll_status.findOneAndUpdate({ domainId, tid, uid }, { $set }) + +}; + +/* +async def get_dict_status(domainId, uid, tids, *, fields=None): + result = dict() + async for tsdoc in get_multi_status(domainId=domainId, + uid=uid, + doc_id={'$in': list(set(tids))}, + fields=fields): + result[tsdoc['doc_id']] = tsdoc + return result + +async def get_dict(domainId, tids, *, fields=None): + result = dict() + async for tdoc in get_multi(domainId=domainId, + doc_id={'$in': list(set(tids))}, + fields=fields): + result[tdoc['doc_id']] = tdoc + return result +*/ \ No newline at end of file diff --git a/hydro/model/user.js b/hydro/model/user.js new file mode 100644 index 000000000..648557329 --- /dev/null +++ b/hydro/model/user.js @@ -0,0 +1,137 @@ +const + { UserNotFoundError, UserAlreadyExistError, DomainNotFoundError, + UserAlreadyDomainMemberError } = require('../error'), + system = require('./system'), + domain = require('./domain'), + { pwhash, validator } = require('../utils'), + db = require('../service/db.js'), + coll = db.collection('user'), + coll_domain = db.collection('domain_user'); + +class USER { + constructor(user, domainUser = {}) { + if (!user) throw new UserNotFoundError(); + this._id = user._id; + this.email = user.email; + this.uname = user.uname; + this.priv = user.priv; + this.salt = user.salt; + this.hash = user.hash; + this.perm = domainUser.perm || '0'; + this.displayName = domainUser.displayName || null; + this.domainId = domainUser.domainId || 'system'; + } + hasPriv(priv) { + return this.priv & priv; + } + hasPerm(perm) { + console.log('PERM', perm, this.perm); + return this.perm == '-' || (this.perm || '').includes(perm); + } + checkPassword(password) { + return pwhash.check(password, this.salt, this.hash); + } + /** + * @param {string} role + * @param {date} joinAt + */ + async joinDomain(role, joinAt) { + validator.checkRole(role); + try { + await coll_domain.updateOne( + { domainId: this.domainId, uid: this._id, role: { $exists: false } }, + { $set: { role, joinAt: joinAt || new Date() } }, + { upsert: true } + ); + } catch (e) { + throw new UserAlreadyDomainMemberError(this.domainId, this._id); + } + return true; + } +} +async function getById(_id, domainId) { + let dudoc, udoc = await coll.findOne({ _id }); + if (domainId) { + let ddoc = await domain.get(domainId); + if (!ddoc) throw new DomainNotFoundError(domainId); + dudoc = (await coll_domain.findOne({ uid: _id, domainId })) || {}; + console.log(dudoc.role, ddoc.roles, { uid: _id, domainId }); + dudoc.perm = ddoc.roles[dudoc.role || 'default']; + } + return new USER(udoc, dudoc); +} +async function getByUname(uname) { + let unameLower = uname.trim().toLowerCase(); + let udoc = await coll.findOne({ unameLower }); + return new USER(udoc); +} +async function getByEmail(email) { + let emailLower = email.trim().toLowerCase(); + let udoc = await coll.findOne({ emailLower }); + return new USER(udoc); +} +async function setPassword(uid, password) { + validator.checkPassword(password); + let salt = pwhash.salt(); + return await coll.findOneAndUpdate({ _id: uid }, { + $set: { salt, hash: pwhash.hash(password, salt) } + }); +} +async function setEmail(uid, email) { + validator.checkEmail(email); + return await setById(uid, { email, emailLower: email.trim().toLowerCase() }); +} +function setById(uid, args) { + coll.findOneAndUpdate({ _id: uid }, { $set: args }); +} + +function setPriv(uid, priv) { + setById(uid, { priv }); +} +async function changePassword(uid, currentPassword, newPassword) { + validator.checkPassword(newPassword); + let udoc = await getById(uid); + udoc.checkPassword(currentPassword); + let salt = pwhash.salt(); + return await coll.findOneAndUpdate({ + _id: udoc._id + }, { + $set: { salt, hash: pwhash.hash(newPassword, salt) } + }); +} +async function create({ uid, email, uname, password, regip, perm = this.perm.PERM_DEFAULT || 0 }) { + validator.checkUname(uname); + validator.checkPassword(password); + validator.checkEmail(email); + let salt = pwhash.salt(); + if (!uid) uid = system.incUserCounter(); + try { + await coll.insertOne({ + email, + emailLower: email.strip().toLowerCase(), + uname, + unameLower: uname.strip().toLowerCase(), + password: pwhash.hash(password, salt), + salt, + regat: new Date(), + regip, + loginat: new Date(), + loginip: regip, + perm, + gravatar: email + }); + } catch (e) { + throw new UserAlreadyExistError(uid, uname, email); + } +} + +function getMany(params) { + return coll.find(params); +} + +module.exports = { + changePassword, create, getByEmail, + getById, getByUname, getMany, + setById, setEmail, + setPassword, setPriv +}; \ No newline at end of file diff --git a/hydro/module/contest/acm.js b/hydro/module/contest/acm.js new file mode 100644 index 000000000..f7f6897fc --- /dev/null +++ b/hydro/module/contest/acm.js @@ -0,0 +1,102 @@ +module.exports = { + check: () => { }, + stat: (tdoc, journal) => { + let naccept = {}, effective = {}, detail = [], accept = 0, time = 0; + for (let j in journal) { + if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) + effective[j.pid] = j; + if (!j.accept) + naccept[j.pid]++; + } + function _time(jdoc) { + let real = jdoc.rid.generation_time - Math.floor(tdoc.begin_at / 1000); + let penalty = 20 * 60 * naccept[jdoc.pid]; + return real + penalty; + } + for (let j of effective) + detail.push(Object.assign({}, j, { + naccept: naccept[j.pid], time: _time(j) + })); + for (let d of detail) { + accept += d.accept; + if (d.accept) time += d.time; + } + return { accept, time, detail }; + }, + scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { + let columns = [ + { type: 'rank', value: _('Rank') }, + { type: 'user', value: _('User') }, + { type: 'display_name', value: _('Display Name') }, + { type: 'solved_problems', value: _('Solved Problems') } + ]; + if (is_export) { + columns.push({ 'type': 'total_time', 'value': _('Total Time (Seconds)') }); + columns.push({ 'type': 'total_time_str', 'value': _('Total Time') }); + } + for (let i in tdoc.pids) + if (is_export) { + columns.push({ + type: 'problem_flag', + value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) + }); + columns.push({ + type: 'problem_time', + value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) + }); + columns.push({ + type: 'problem_time_str', + value: '#{0} {1}'.format(i + 1, _('Time')) + }); + } else + columns.push({ + type: 'problem_detail', + value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] + }); + let rows = [columns]; + for (let [rank, tsdoc] of ranked_tsdocs) { + let tsddict = {}; + if (tdoc.detail) + for (let item of tsdoc.detail) + tsddict[item.pid] = item; + let row = []; + row.push( + { type: 'string', value: rank }, + { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, + { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, + { type: 'string', value: tsdoc.accept || 0 }); + if (is_export) + row.push( + { type: 'string', value: tsdoc.time || 0.0 }, + { type: 'string', value: tsdoc.time || 0.0 } + ); + for (let pid of tdoc.pids) { + let rdoc, col_accepted, col_time, col_time_str; + if ((tsddict[pid] || {}).accept) { + rdoc = tsddict[pid].rid; + col_accepted = _('Accepted'); + col_time = tsddict[pid].time; + col_time_str = col_time; + } else { + rdoc = null; + col_accepted = '-'; + col_time = '-'; + col_time_str = '-'; + } + if (is_export) { + row.push({ type: 'string', value: col_accepted }); + row.push({ type: 'string', value: col_time }); + row.push({ type: 'string', value: col_time_str }); + } else { + row.push({ + type: 'record', + value: '{0}\n{1}'.format(col_accepted, col_time_str), + raw: rdoc + }); + } + rows.push(row); + } + } + return rows; + } +}; \ No newline at end of file diff --git a/hydro/module/contest/homework.js b/hydro/module/contest/homework.js new file mode 100644 index 000000000..6059ee672 --- /dev/null +++ b/hydro/module/contest/homework.js @@ -0,0 +1,93 @@ +const { ValidationError } = require('../../error'); +module.exports = { + check(data) { + if (!data.penalty_since) throw new ValidationError('penalty_since'); + if (data.penalty_since < data.begin_at) throw new ValidationError('penalty_since', 'begin_at'); + if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at'); + }, + stat(tdoc, journal) { + let effective = {}, detail = [], score = 0, penalty_score = 0, time = 0; + for (let j in journal) + if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) + effective[j.pid] = j; + const _time = jdoc => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); + function _penalty_score(jdoc) { + let exceed_seconds = jdoc.rid.generation_time - Math.floor(tdoc.penalty_since / 1000); + if (exceed_seconds < 0) return jdoc.score; + else return 0.5 * jdoc.score; + } + for (let j in effective) { + detail.push(Object.assign({}, effective[j], { + penalty_score: _penalty_score(effective[j]), time: _time(effective[j]) + })); + } + for (let d of detail) { + score += d.score; + penalty_score += d.penalty_score; + time += d.time; + } + return { score, penalty_score, time, detail }; + }, + scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { + let columns = [ + { 'type': 'rank', 'value': _('Rank') }, + { 'type': 'user', 'value': _('User') }, + { 'type': 'display_name', 'value': _('Display Name') }, + { 'type': 'total_score', 'value': _('Score') } + ]; + if (is_export) + columns.push( + { type: 'total_original_score', value: _('Original Score') }, + { type: 'total_time', value: _('Total Time (Seconds)') } + ); + columns.push({ type: 'total_time_str', value: _('Total Time') }); + for (let i in tdoc.pids) + if (is_export) + columns.push( + { type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }, + { type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) }, + { type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) }, + { type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) } + ); + else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), 'raw': pdict[tdoc.pids[i]] }); + let rows = [columns]; + for (let [rank, tsdoc] in ranked_tsdocs) { + let tsddict = {}; + if (tsdoc.detail) + for (let item of tsdoc.detail) + tsddict[item.pid] = item; + let row = [ + { type: 'string', value: rank }, + { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, + { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, + { type: 'string', value: tsdoc.penalty_score || 0 } + ]; + if (is_export) + row.push( + { type: 'string', value: tsdoc.score || 0 }, + { type: 'string', value: tsdoc.time || 0.0 } + ); + row.push({ type: 'string', value: tsdoc.time || 0 }); + for (let pid of tdoc.pids) { + let rdoc = (tsddict[pid] || {}).rid || null; + let col_score = (tsddict[pid] || {}).penalty_score || '-'; + let col_original_score = (tsddict[pid] || {}).score || '-'; + let col_time = (tsddict[pid] || {}).time || '-'; + let col_time_str = col_time != '-' ? col_time : '-'; + if (is_export) row.push( + { type: 'string', value: col_score }, + { type: 'string', value: col_original_score }, + { type: 'string', value: col_time }, + { type: 'string', value: col_time_str }, + ); + else row.push({ + type: 'record', + value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str), + raw: rdoc + }); + } + rows.push(row); + } + return rows; + } +}; \ No newline at end of file diff --git a/hydro/module/contest/oi.js b/hydro/module/contest/oi.js new file mode 100644 index 000000000..0be8accf1 --- /dev/null +++ b/hydro/module/contest/oi.js @@ -0,0 +1,49 @@ +module.exports = { + check: () => { }, + stat: (tdoc, journal) => { + let detail = {}, score = 0; + for (let j in journal) + if (tdoc.pids.includes(j.pid)) { + detail[j.pid] = j; + score += j.score; + } + return { score, detail }; + }, + scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { + let columns = [ + { type: 'rank', value: _('Rank') }, + { type: 'user', value: _('User') }, + { type: 'display_name', value: _('Display Name') }, + { type: 'total_score', value: _('Total Score') } + ]; + for (let i in tdoc.pids) + if (is_export) + columns.push({ type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }); + else + columns.push({ + type: 'problem_detail', + value: '#{0}'.format(i + 1), + raw: pdict[tdoc.pids[i]] + }); + let rows = [columns]; + for (let [rank, tsdoc] of ranked_tsdocs) { + let tsddict = {}; + if (tdoc.detail) + for (let item of tsdoc.detail) + tsddict[item.pid] = item; + let row = []; + row.push({ type: 'string', value: rank }); + row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); + row.push({ type: 'display_name', value: dudict[tsdoc.uid].display_name || '' }); + row.push({ type: 'string', value: tsdoc.score || 0 }); + for (let pid of tdoc.pids) + row.push({ + type: 'record', + value: (tsddict[pid] || {}).score || '-', + raw: (tsddict[pid] || {}).rid || null + }); + rows.push(row); + } + return rows; + } +}; \ No newline at end of file diff --git a/hydro/options.js b/hydro/options.js new file mode 100644 index 000000000..c4358ebe9 --- /dev/null +++ b/hydro/options.js @@ -0,0 +1,49 @@ +const + yaml = require('js-yaml'), + { defaults } = require('lodash'), + fs = require('fs'), + path = require('path'); + +let options = { + db: { + name: 'hydro', + host: '127.0.0.1', + port: 27017, + username: '', + password: '' + }, + mq: '', + listen: { + host: '127.0.0.1', + port: 8888, + domain: '', + https: false + }, + smtp: { + from: '', + host: '', + port: 465, + user: '', + pass: '', + secure: false + }, + session: { + keys: ['Hydro'], + secure: false, + domain: '127.0.0.1', + saved_expire_seconds: 3600 * 24, + unsaved_expire_seconds: 600 + }, + constants: { + PROBLEM_PER_PAGE: 100, + RECORD_PER_PAGE: 100 + } +}; + +try { + let t = yaml.safeLoad(fs.readFileSync(path.resolve(process.cwd(), 'config.yaml'))); + options = defaults(t, options); +} catch (e) { + console.error('Cannot load config'); +} +module.exports = options; \ No newline at end of file diff --git a/hydro/permission.js b/hydro/permission.js new file mode 100644 index 000000000..4214d0c68 --- /dev/null +++ b/hydro/permission.js @@ -0,0 +1,67 @@ +let permissions = { + PERM_NONE: '0', + PERM_ALL: '-', + PERM_VIEW: '1', + PERM_EDIT_PERM: '2', + PERM_MOD_BADGE: '3', + PERM_EDIT_DESCRIPTION: '4', + PERM_CREATE_PROBLEM: '5', + PERM_EDIT_PROBLEM: '6', + PERM_VIEW_PROBLEM: '7', + PERM_VIEW_PROBLEM_HIDDEN: '8', + PERM_SUBMIT_PROBLEM: '9', + PERM_READ_PROBLEM_DATA: 'A', + PERM_READ_RECORD_CODE: 'B', + PERM_REJUDGE_PROBLEM: 'C', + PERM_REJUDGE: 'D', + PERM_VIEW_PROBLEM_SOLUTION: 'E', + PERM_CREATE_PROBLEM_SOLUTION: 'F', + PERM_VOTE_PROBLEM_SOLUTION: 'G', + PERM_EDIT_PROBLEM_SOLUTION: 'H', + PERM_DELETE_PROBLEM_SOLUTION: 'I', + PERM_REPLY_PROBLEM_SOLUTION: 'J', + PERM_EDIT_PROBLEM_SOLUTION_REPLY: 'K', + PERM_DELETE_PROBLEM_SOLUTION_REPLY: 'L', + PERM_VIEW_DISCUSSION: 'M', + PERM_CREATE_DISCUSSION: 'N', + PERM_HIGHLIGHT_DISCUSSION: 'O', + PERM_EDIT_DISCUSSION: 'P', + PERM_DELETE_DISCUSSION: 'Q', + PERM_REPLY_DISCUSSION: 'R', + PERM_EDIT_DISCUSSION_REPLY: 'S', + PERM_DELETE_DISCUSSION_REPLY: 'T', + PERM_VIEW_CONTEST: 'U', + PERM_VIEW_CONTEST_SCOREBOARD: 'V', + PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 'W', + PERM_CREATE_CONTEST: 'X', + PERM_ATTEND_CONTEST: 'Y', + PERM_EDIT_CONTEST: 'Z', + PERM_VIEW_TRAINING: 'a', + PERM_CREATE_TRAINING: 'b', + PERM_EDIT_TRAINING: 'c' +}; +permissions.PERM_BASIC = + permissions.PERM_VIEW + + permissions.PERM_VIEW_PROBLEM + + permissions.PERM_VIEW_PROBLEM_SOLUTION + + permissions.PERM_VIEW_DISCUSSION + + permissions.PERM_VIEW_CONTEST + + permissions.PERM_VIEW_CONTEST_SCOREBOARD; +permissions.PERM_DEFAULT = + permissions.PERM_VIEW + + permissions.PERM_VIEW_PROBLEM + + permissions.PERM_SUBMIT_PROBLEM + + permissions.PERM_VIEW_PROBLEM_SOLUTION + + permissions.PERM_CREATE_PROBLEM_SOLUTION + + permissions.PERM_VOTE_PROBLEM_SOLUTION + + permissions.PERM_REPLY_PROBLEM_SOLUTION + + permissions.PERM_VIEW_DISCUSSION + + permissions.PERM_CREATE_DISCUSSION + + permissions.PERM_REPLY_DISCUSSION + + permissions.PERM_VIEW_CONTEST + + permissions.PERM_VIEW_CONTEST_SCOREBOARD + + permissions.PERM_ATTEND_CONTEST + + permissions.PERM_VIEW_TRAINING + + permissions.PERM_CREATE_TRAINING; +permissions.PERM_ADMIN = permissions.PERM_ALL; +module.exports = permissions; diff --git a/hydro/privilege.js b/hydro/privilege.js new file mode 100644 index 000000000..2f39dcc45 --- /dev/null +++ b/hydro/privilege.js @@ -0,0 +1,32 @@ +let privileges = { + PRIV_NONE: 0, + PRIV_ALL: -1, + PRIV_SET_PRIV: 1 << 0, + PRIV_SET_PERM: 1 << 1, + PRIV_USER_PROFILE: 1 << 2, + PRIV_REGISTER_USER: 1 << 3, + PRIV_READ_PROBLEM_DATA: 1 << 4, + PRIV_READ_PRETEST_DATA: 1 << 5, + PRIV_READ_PRETEST_DATA_SELF: 1 << 6, + PRIV_READ_RECORD_CODE: 1 << 7, + PRIV_VIEW_HIDDEN_RECORD: 1 << 8, + PRIV_WRITE_RECORD: 1 << 9, + PRIV_CREATE_DOMAIN: 1 << 10, + PRIV_VIEW_ALL_DOMAIN: 1 << 11, + PRIV_MANAGE_ALL_DOMAIN: 1 << 12, + PRIV_REJUDGE: 1 << 13, + PRIV_VIEW_USER_SECRET: 1 << 14, + PRIV_VIEW_JUDGE_STATISTICS: 1 << 15, + PRIV_CREATE_FILE: 1 << 16, + PRIV_UNLIMITED_QUOTA: 1 << 17, + PRIV_DELETE_FILE: 1 << 18, + PRIV_DELETE_FILE_SELF: 1 << 19 +}; +privileges.PRIV_DEFAULT = + privileges.PRIV_USER_PROFILE | privileges.PRIV_CREATE_DOMAIN + | privileges.PRIV_CREATE_FILE | privileges.PRIV_DELETE_FILE_SELF; +privileges.PRIV_JUDGE = + privileges.PRIV_USER_PROFILE | privileges.PRIV_VIEW_ALL_DOMAIN + | privileges.PRIV_READ_PROBLEM_DATA | privileges.PRIV_READ_PRETEST_DATA + | privileges.PRIV_READ_RECORD_CODE | privileges.PRIV_WRITE_RECORD; +module.exports = privileges; \ No newline at end of file diff --git a/hydro/service/db.js b/hydro/service/db.js new file mode 100644 index 000000000..db031ea37 --- /dev/null +++ b/hydro/service/db.js @@ -0,0 +1,18 @@ +const Mongo = require('mongodb'); +const options = require('../options'); +let mongourl = 'mongodb://'; +if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; +mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; +/** + * @type {import('mongodb').Db} + */ +let db = null; +Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(Database => { + db = Database.db(options.db.name); + global.bus.emit('connected'); + }); + +module.exports = { + collection: c => db.collection(c) +}; diff --git a/hydro/service/queue.js b/hydro/service/queue.js new file mode 100644 index 000000000..9b9ee6ea6 --- /dev/null +++ b/hydro/service/queue.js @@ -0,0 +1,53 @@ +const options = require('../options'); +if (options.mq) { + const amqplib = require('amqplib'); + /** + * @type {import('amqplib').Connection} + */ + let conn = null; + let ch = null; + + module.exports = { + async connect() { + conn = await amqplib.connect(options.mq.url); + ch = conn.createChannel(); + }, + async assertQueue(queue) { + return await ch.assertQueue(queue); + }, + async publish(queue, data) { + return await ch.sendToQueue(queue, Buffer.from(data)); + }, + async consume(queue, callback) { + ch.consume(queue, function (msg) { + if (msg !== null) { + callback(msg.content.toString(), () => { ch.ack(msg); }); + ch.ack(msg); + } + }); + } + }; +} else { + let q = {}; + module.exports = { + async connect() { }, + async assertQueue(queue) { + q[queue] = q[queue] || []; + }, + async publish(queue, data) { + q[queue].push(data); + }, + async consume(queue, callback) { + while (1) { + if (q[queue].length) { + let data = q[queue][0]; + q[queue].splice(0, 1); + callback(data, () => { }); + } + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + } + } + }; +} \ No newline at end of file diff --git a/hydro/service/server.js b/hydro/service/server.js new file mode 100644 index 000000000..c1a930939 --- /dev/null +++ b/hydro/service/server.js @@ -0,0 +1,51 @@ +const options = require('../options'); +let http = options.listen.https ? require('https') : require('http'); +let app = new (require('koa'))(); +let server = http.createServer(app.callback()); +app.keys = options.session.keys; +app.use(require('koa-morgan')(':method :url :status :res[content-length] - :response-time ms')); +app.use(require('koa-body')()); +let router = (new require('koa-router'))(); +const sockjs = require('sockjs'); +/** + * @param {import('koa').Middleware} middleware + */ +function MIDDLEWARE(middleware) { + app.use(middleware); +} +/** + * @param {string} route + * @param {...import('koa').Middleware} handler + */ +function GET(route, ...handler) { + router.get(route, ...handler); +} +/** + * @param {string} route + * @param {...import('koa').Middleware} handler + */ +function POST(route, ...handler) { + router.post(route, ...handler); +} +/** + * @callback SockJSHandler + * @param {import('sockjs').Connection} conn + */ +/** + * @param {string} prefix + * @param {SockJSHandler} handler + */ +function SOCKET(prefix, handler) { + const sock = sockjs.createServer({ prefix }); + sock.on('connection', handler); + sock.installHandlers(server); +} +exports.MIDDLEWARE = MIDDLEWARE; +exports.GET = GET; +exports.POST = POST; +exports.SOCKET = SOCKET; +exports.start = function start() { + app.use(router.routes()).use(router.allowedMethods()); + app.listen(options.listen.port); + console.log('Server listening at: %s', options.listen.port); +}; \ No newline at end of file diff --git a/hydro/status.js b/hydro/status.js new file mode 100644 index 000000000..5e75212ea --- /dev/null +++ b/hydro/status.js @@ -0,0 +1,32 @@ +exports.STATUS_WAITING = 0; +exports.STATUS_ACCEPTED = 1; +exports.STATUS_WRONG_ANSWER = 2; +exports.STATUS_TIME_LIMIT_EXCEEDED = 3; +exports.STATUS_MEMORY_LIMIT_EXCEEDED = 4; +exports.STATUS_OUTPUT_LIMIT_EXCEEDED = 5; +exports.STATUS_RUNTIME_ERROR = 6; +exports.STATUS_COMPILE_ERROR = 7; +exports.STATUS_SYSTEM_ERROR = 8; +exports.STATUS_CANCELED = 9; +exports.STATUS_ETC = 10; +exports.STATUS_JUDGING = 20; +exports.STATUS_COMPILING = 21; +exports.STATUS_FETCHED = 22; +exports.STATUS_IGNORED = 30; +exports.STATUS_TEXTS = { + [exports.STATUS_WAITING]: 'Waiting', + [exports.STATUS_ACCEPTED]: 'Accepted', + [exports.STATUS_WRONG_ANSWER]: 'Wrong Answer', + [exports.STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded', + [exports.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded', + [exports.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded', + [exports.STATUS_RUNTIME_ERROR]: 'Runtime Error', + [exports.STATUS_COMPILE_ERROR]: 'Compile Error', + [exports.STATUS_SYSTEM_ERROR]: 'System Error', + [exports.STATUS_CANCELED]: 'Cancelled', + [exports.STATUS_ETC]: 'Unknown Error', + [exports.STATUS_JUDGING]: 'Running', + [exports.STATUS_COMPILING]: 'Compiling', + [exports.STATUS_FETCHED]: 'Fetched', + [exports.STATUS_IGNORED]: 'Ignored', +}; \ No newline at end of file diff --git a/hydro/uninstall.js b/hydro/uninstall.js new file mode 100644 index 000000000..89a35f23b --- /dev/null +++ b/hydro/uninstall.js @@ -0,0 +1,19 @@ +const + Mongo = require('mongodb'), + options = require('./lib/options'); + +async function run() { + let mongourl = 'mongodb://'; + if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; + mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; + let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); + let db = Database.db(options.db.name); + await db.dropDatabase(); + console.log('Dropped'); + process.exit(0); +} + +run().catch(e => { + console.error(e); + process.exit(1); +}); \ No newline at end of file diff --git a/hydro/utils.js b/hydro/utils.js new file mode 100644 index 000000000..447394888 --- /dev/null +++ b/hydro/utils.js @@ -0,0 +1,44 @@ +const + crypto = require('crypto'), + map = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', + '9', '0' + ]; + +String.random = function (digit) { + let str = ''; + for (let i = 1; i <= digit; i++) + str += map[Math.floor(Math.random() * 62)]; + return str; +}; + +exports.pwhash = { + _b64encode: str => new Buffer(str).toString('base64'), + _b64decode: str => new Buffer(str, 'base64').toString(), + _md5: str => crypto.createHash('md5').update(str).digest('hex'), + _sha1: str => crypto.createHash('sha1').update(str).digest('hex'), + /** + * @param {string} password + * @param {string} salt + * @param {string} hash + */ + check(password, salt, hash) { + return hash == this.hash(password, salt); + }, + /** + * @param {string} password + * @param {string} salt + */ + hash(password, salt) { + let res = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256'); + return res.toString('Hex'); + }, + salt() { + return String.random(); + } +}; diff --git a/interfaces.d.ts b/interfaces.d.ts new file mode 100644 index 000000000..31e58351d --- /dev/null +++ b/interfaces.d.ts @@ -0,0 +1,3 @@ +export interface { + +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 000000000..0d2d8e4c0 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "hydrofrontend", + "version": "0.0.0", + "main": "vj5/development.js", + "repository": "https://github.com/masnn/vj5.git", + "author": "masnn", + "license": "MIT", + "dependencies": { + "amqplib": "^0.5.5", + "axios": "^0.19.0", + "bson": "^4.0.2", + "js-yaml": "^3.13.1", + "koa": "^2.11.0", + "koa-body": "^4.1.1", + "koa-morgan": "^1.0.1", + "koa-router": "^7.4.0", + "lodash": "^4.17.15", + "mongodb": "^3.3.4", + "nodemailer": "^6.3.1", + "sockjs": "^0.3.20" + }, + "devDependencies": { + "eslint": "^6.6.0" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..a8063384c --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1460 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/formidable@^1.0.31": + version "1.0.31" + resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.0.31.tgz#274f9dc2d0a1a9ce1feef48c24ca0859e7ec947b" + integrity sha512-dIhM5t8lRP0oWe2HF8MuPvdd1TpPTjhDMAqemcq6oIZQCBQTovhBAdTQ5L5veJB4pdQChadmHuxtB0YzqvfU3Q== + dependencies: + "@types/events" "*" + "@types/node" "*" + +"@types/node@*": + version "12.12.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2" + integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA== + +accepts@^1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-jsx@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" + integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== + +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +amqplib@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.5.5.tgz#698f0cb577e0591954a90572fcb3b8998a76fd40" + integrity sha512-sWx1hbfHbyKMw6bXOK2k6+lHL8TESWxjAx5hG8fBtT7wcxoXNIsFxZMnFyBjxt3yL14vn7WqBDe5U6BGOadtLg== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +ansi-escapes@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" + integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg== + dependencies: + type-fest "^0.8.1" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +basic-auth@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bitsyntax@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" + integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== + dependencies: + buffer-more-ints "~1.0.0" + debug "~2.6.9" + safe-buffer "~5.1.2" + +bluebird@^3.5.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bson@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== + +bson@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bson/-/bson-4.0.2.tgz#2929fdfed1a145b1c361908a2b950a6cf4bed2c2" + integrity sha512-rBdCxMBCg2aR420e1oKUejjcuPZLTibA7zEhWAlliFWEwzuBCC9Dkp5r7VFFIQB2t1WVsvTbohry575mc7Xw5A== + dependencies: + buffer "^5.1.0" + long "^4.0.0" + +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== + +buffer@^5.1.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +co-body@^5.1.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124" + integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ== + dependencies: + inflation "^2.0.0" + qs "^6.4.0" + raw-body "^2.2.0" + type-is "^1.6.14" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +content-disposition@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookies@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@2.6.9, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@=3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.6.0.tgz#4a01a2fb48d32aacef5530ee9c5a78f11a8afd04" + integrity sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^11.7.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== + dependencies: + acorn "^7.1.0" + acorn-jsx "^5.1.0" + eslint-visitor-keys "^1.1.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + +formidable@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" + integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +glob-parent@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +http-assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" + integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.2" + +http-errors@1.7.3, http-errors@^1.3.1, http-errors@^1.6.3, http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@~2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" + integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.2" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^4.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +is-buffer@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + +koa-body@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-4.1.1.tgz#50686d290891fc6f1acb986cf7cfcd605f855ef0" + integrity sha512-rLb/KVD8qplEcK8Qsu6F4Xw+uHkmx3MWogDVmMX07DpjXizhw3pOEp1ja1MqqAcl0ei75AsrbGVDlySmsUrreA== + dependencies: + "@types/formidable" "^1.0.31" + co-body "^5.1.1" + formidable "^1.1.1" + +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-morgan@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/koa-morgan/-/koa-morgan-1.0.1.tgz#08052e0ce0d839d3c43178b90a5bb3424bef1f99" + integrity sha1-CAUuDODYOdPEMXi5CluzQkvvH5k= + dependencies: + morgan "^1.6.1" + +koa-router@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" + integrity sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g== + dependencies: + debug "^3.1.0" + http-errors "^1.3.1" + koa-compose "^3.0.0" + methods "^1.0.1" + path-to-regexp "^1.1.1" + urijs "^1.19.0" + +koa@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4" + integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.8.0" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + encodeurl "^1.0.2" + error-inject "^1.0.0" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.14, lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +methods@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.42.0: + version "1.42.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" + integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + +mime-types@^2.1.18, mime-types@~2.1.24: + version "2.1.25" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" + integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== + dependencies: + mime-db "1.42.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mongodb@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.4.tgz#f52eec4a04005101e63715d4d1f67bf784fa0aef" + integrity sha512-6fmHu3FJTpeZxacJcfjUGIP3BSteG0l2cxLkSrf1nnnS1OrlnVGiP9P/wAC4aB6dM6H4vQ2io8YDjkuPkje7AA== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +morgan@^1.6.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" + integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== + dependencies: + basic-auth "~2.0.0" + debug "2.6.9" + depd "~1.1.2" + on-finished "~2.3.0" + on-headers "~1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +nodemailer@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346" + integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ== + +on-finished@^2.3.0, on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= + +optionator@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parseurl@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-to-regexp@^1.1.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@^6.4.0: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +raw-body@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + +"readable-stream@1.x >=1.1.9": + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rxjs@^6.4.0: + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +semver@^5.1.0, semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +sockjs@^0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.4.0" + websocket-driver "0.6.5" + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +"statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.14, type-is@^1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urijs@^1.19.0: + version "1.19.2" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a" + integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w== + +url-parse@~1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +ylru@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==