module.add(user,problem,record)

This commit is contained in:
masnn committed 2020-04-01 18:13:04 +08:00
commit d5b1f623f8
43 files changed
+5965

No files matched your search

+36
View File
@@ -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"
}
}
+2
View File
@@ -0,0 +1,2 @@
node_modules/
config.yaml
+7
View File
@@ -0,0 +1,7 @@
{
"files.autoSave": "onFocusChange",
"files.encoding": "utf8",
"files.autoGuessEncoding": false,
"editor.detectIndentation": false,
"editor.formatOnSave": false
}
+41
View File
@@ -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);
});
+419
View File
@@ -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}.'
*/
+98
View File
@@ -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 } };
}
});
+5
View File
@@ -0,0 +1,5 @@
const { GET } = require('../service/server');
GET('/', async ctx => {
ctx.body = {};
});
+434
View File
@@ -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)))
*/
+448
View File
@@ -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)))
*/
+403
View File
@@ -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)))
*/
+184
View File
@@ -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']
*/
+50
View File
@@ -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();
};
}
};
+182
View File
@@ -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)
*/
+50
View File
@@ -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);
});
+55
View File
@@ -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[]
}
+45
View File
@@ -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;
}
};
+118
View File
@@ -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')
*/
+12
View File
@@ -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 })
};
+52
View File
@@ -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
};
+338
View File
@@ -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
*/
+315
View File
@@ -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()
*/
+35
View File
@@ -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)
*/
+87
View File
@@ -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
};
+55
View File
@@ -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
};
+110
View File
@@ -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()
*/
+72
View File
@@ -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 })
])
};
+78
View File
@@ -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
*/
+137
View File
@@ -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
};
+102
View File
@@ -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;
}
};
+93
View File
@@ -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;
}
};
+49
View File
@@ -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;
}
};
+49
View File
@@ -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;
+67
View File
@@ -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;
+32
View File
@@ -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;
+18
View File
@@ -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)
};
+53
View File
@@ -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);
});
}
}
};
}
+51
View File
@@ -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);
};
+32
View File
@@ -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',
};
+19
View File
@@ -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);
});
+44
View File
@@ -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();
}
};
+3
View File
@@ -0,0 +1,3 @@
export interface {
}
+25
View File
@@ -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"
}
}
+1460
View File
File diff suppressed because it is too large. Load diff