文件结构更改

This commit is contained in:
undefined committed 2020-08-04 13:42:42 +08:00
1 parent b3af28dc43
commit 1f1d12ad73
109 files changed
+544 -3296

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
// TODO check more
import os from 'os';
import superagent from 'superagent';
import { Dictionary } from 'lodash';
import * as system from './model/system';
import * as db from './service/db';
const c = {};
type CheckItem = (context: any, log: Function, warn: Function, error: Function) => Promise<void>;
const checks: Dictionary<CheckItem> = {
async checkDb(ctx, log, warn, error) {
try {
const coll = db.collection('check');
const d = await coll.findOne({ _id: 'check' });
if (!d) await coll.insertOne({ _id: 'check', value: 'check' });
await coll.createIndex('check');
await coll.deleteOne({ _id: 'check' });
await coll.drop();
} catch (e) {
error(`Mongo Error: Database read-write failed.\n${e.message}`);
}
},
async checkPerm(ctx, log, warn) {
const { username } = os.userInfo();
if (username === 'root') warn('Hydro should not be run as root.');
// TODO check cwd read-write
},
async checkSystem(ctx, log, warn) {
const platform = os.platform();
if (platform !== 'linux') {
warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`);
}
},
async checkMail(ctx, log, warn) {
const from = await system.get('smtp.from');
if (!from) warn('SMTP account was not provided, email verification disabled.');
},
async checkProxy(ctx, log, warn) {
const [github, google, proxy] = await system.getMany([
'oauth.githubappid', 'oauth.googleappid', 'proxy',
]);
if (proxy) {
superagent.get('https://www.google.com/').timeout(10000).proxy(proxy)
.catch(() => warn('The proxy configured seem to be invalid.'));
} else if (github || google) {
warn('OAuth enabled. But for well-known reasons, a proxy is required.');
}
},
async checkIpHeader(ctx, log, warn) {
const header = await system.get('server.xff');
if (header && !ctx.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.');
},
};
export async function start(ctx, log, warn, error, cb) {
const id = String.random(6);
cb(id);
for (const name in checks) {
if (c[id]) {
delete c[id];
return;
}
// eslint-disable-next-line no-await-in-loop
await checks[name](ctx, log, warn, error);
}
}
export async function cancel(id: string) {
c[id] = true;
}
+71
View File
@@ -0,0 +1,71 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable import/no-dynamic-require */
import { argv } from 'yargs';
import {
lib, service, model,
builtinLib, builtinModel,
} from './common';
const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
const ARR = /=>.*$/mg;
function parseParameters(fn: Function) {
const code = fn.toString()
.replace(COMMENTS, '')
.replace(ARR, '');
const result = code.slice(code.indexOf('(') + 1, code.indexOf(')'))
.match(/([^,]+)/g).map((i) => i.trim());
return result === null ? [] : result;
}
async function cli() {
const [, modelName, func, ...args] = argv._;
if (!global.Hydro.model[modelName]) {
return console.error(`Model ${modelName} doesn't exist.`);
}
if (!global.Hydro.model[modelName][func]) {
return console.error(`Function ${func} doesn't exist in model ${modelName}.`);
}
if (typeof global.Hydro.model[modelName][func] !== 'function') {
return console.error(`${func} in model ${modelName} is not a function.`);
}
const parameterMin = global.Hydro.model[modelName][func].length;
const parameters = parseParameters(global.Hydro.model[modelName][func]);
const parameterMax = parameters.length;
if (args.length > parameterMax) {
console.error(`Too many arguments. Max ${parameterMax}`);
return console.error(parameters.join(', '));
}
if (args.length < parameterMin) {
console.error(`Too few arguments. Min ${parameterMin}`);
return console.error(parameters.join(', '));
}
let result = global.Hydro.model[modelName][func](...args);
if (result instanceof Promise) result = await result;
return console.log(result);
}
export async function load() {
const pending = global.addons;
const fail = [];
require('../lib/i18n');
require('../utils');
require('../error');
require('../options');
const bus = require('../service/bus');
await new Promise((resolve) => {
const h = () => {
bus.unsubscribe(['system_database_connected'], h);
resolve();
};
bus.subscribe(['system_database_connected'], h);
require('../service/db');
});
for (const i of builtinLib) require(`../lib/${i}`);
await lib(pending, fail);
require('../service/gridfs');
await service(pending, fail);
for (const i of builtinModel) require(`../model/${i}`);
await model(pending, fail);
for (const postInit of global.Hydro.postInit) await postInit();
await cli();
}
+221
View File
@@ -0,0 +1,221 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-eval */
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { argv } from 'yargs';
export const builtinLib = [
'jwt', 'download', 'i18n', 'mail', 'useragent',
'md5', 'misc', 'paginate', 'hash.hydro', 'rank',
'validator', 'nav', 'sysinfo', 'testdata.convert.ini', 'testdataConfig',
'logger',
];
export const builtinModel = [
'builtin', 'document', 'domain', 'blacklist', 'opcount',
'setting', 'token', 'user', 'problem', 'record',
'contest', 'message', 'solution', 'training', 'file',
'discussion', 'system',
];
export const builtinHandler = [
'home', 'problem', 'record', 'judge', 'user',
'contest', 'training', 'discussion', 'manage', 'import',
'misc', 'homework', 'domain',
];
export const builtinScript = [
'rating', 'register', 'problemStat', 'blacklist', 'setSuperadmin',
'deleteUser',
'upgrade0_1',
];
function getFiles(folder: string, base = ''): string[] {
const files = [];
const f = fs.readdirSync(folder);
for (const i of f) {
if (fs.statSync(path.join(folder, i)).isDirectory()) {
files.push(...getFiles(path.join(folder, i), path.join(base, i)));
} else files.push(path.join(base, i));
}
return files.map((item) => item.replace(/\\/gmi, '/'));
}
export async function handler(pending: string[], fail: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'handler.js');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
console.log(`Handler init: ${i}`);
console.time(`Handler init: ${i}`);
eval('require')(p);
console.timeEnd(`Handler init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Handler Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function locale(pending: string[], fail: string[]) {
for (const i of pending) {
let p = path.resolve(i, 'locales');
if (!fs.existsSync(p)) p = path.resolve(i, 'locale');
if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !fail.includes(i)) {
try {
const files = fs.readdirSync(p);
const locales = {};
for (const file of files) {
const content = fs.readFileSync(path.resolve(p, file)).toString();
locales[file.split('.')[0]] = yaml.safeLoad(content);
}
global.Hydro.lib.i18n(locales);
console.log(`Locale init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Locale Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function setting(pending: string[], fail: string[], modelSetting: typeof import('../model/setting')) {
const map = {
system: modelSetting.SystemSetting,
account: modelSetting.AccountSetting,
preference: modelSetting.PreferenceSetting,
};
for (const i of pending) {
let p = path.resolve(i, 'setting.yaml');
const t = i.split(path.sep);
const name = t[t.length - 1];
if (!fs.existsSync(p)) p = path.resolve(i, 'settings.yaml');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
const cfg: any = yaml.safeLoad(fs.readFileSync(p).toString());
for (const key in cfg) {
map[cfg[key].category || 'system'](
modelSetting.Setting(
name, `${name}.${key}`, cfg[key].range, cfg[key].default,
cfg[key].type || 'text', cfg[key].name || key, cfg[key].desc || '',
),
);
}
} catch (e) {
console.error(`Config Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function template(pending: string[], fail: string[]) {
for (const i of pending) {
let p = path.resolve(i, 'templates');
if (!fs.existsSync(p)) p = path.resolve(i, 'template');
if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !fail.includes(i)) {
try {
const files = getFiles(p);
for (const file of files) {
global.Hydro.ui.template[file] = fs.readFileSync(
path.resolve(p, file),
).toString();
}
console.log(`Template init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Template Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function uistatic(pending: string[], fail: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'public', 'static-manifest.json');
if (fs.existsSync(p) && fs.statSync(p).isFile() && !fail.includes(i)) {
try {
Object.assign(global.Hydro.ui.manifest, eval('require')(p));
} catch (e) {
fail.push(i);
}
}
}
}
export async function model(pending: string[], fail: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'model.js');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
console.log(`Model init: ${i}`);
console.time(`Model init: ${i}`);
eval('require')(p);
console.timeEnd(`Model init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Model Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function lib(pending: string[], fail: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'lib.js');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
console.log(`Lib init: ${i}`);
console.time(`Lib init: ${i}`);
eval('require')(p);
console.timeEnd(`Lib init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Lib Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function service(pending: string[], fail: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'service.js');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
console.log(`Service init: ${i}`);
console.time(`Service init: ${i}`);
eval('require')(p);
console.timeEnd(`Service init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Service Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
}
}
export async function script(pending: string[], fail: string[], active: string[]) {
for (const i of pending) {
const p = path.resolve(i, 'script.js');
if (fs.existsSync(p) && !fail.includes(i)) {
try {
console.time(`Script init: ${i}`);
eval('require')(p);
console.timeEnd(`Script init: ${i}`);
} catch (e) {
fail.push(i);
console.error(`Script Load Fail: ${i}`);
if (argv.debug) console.error(e);
}
}
active.push(i);
}
}
+64
View File
@@ -0,0 +1,64 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable import/no-dynamic-require */
import os from 'os';
import path from 'path';
import fs from 'fs-extra';
import pslist from 'ps-list';
import { argv } from 'yargs';
import { builtinModel } from './common';
import { Entry } from '../loader';
const tmpdir = path.resolve(os.tmpdir(), 'hydro');
const lockfile = path.resolve(tmpdir, 'lock.json');
export async function load(call: Entry) {
if (fs.existsSync(lockfile) && !argv.ignorelock) {
const file = require(lockfile);
const plist = await pslist();
if (file.pid && plist.map((i) => i.pid).includes(file.pid)) {
console.error(`Lockfile exists, pid=${file.pid}`);
process.exit(1);
}
}
const context = {
addons: global.addons,
pid: process.pid,
ppid: process.ppid,
};
await fs.writeFile(lockfile, JSON.stringify(context));
global.onDestory.push(() => {
fs.removeSync(lockfile);
});
require('../lib/i18n');
require('../utils');
require('../error');
const config = require('../options').default();
if (!config) {
console.log('Starting setup');
await call({ entry: 'setup', newProcess: true }).catch((err) => {
console.error('Cannot start setup process.', err);
process.exit(1);
});
}
const bus = require('../service/bus');
await new Promise((resolve) => {
const h = () => {
console.log('Database connected');
bus.unsubscribe(['system_database_connected'], h);
resolve();
};
bus.subscribe(['system_database_connected'], h);
require('../service/db');
});
for (const i of builtinModel) require(`../model/${i}`);
const modelSystem = require('../model/system');
const dbVer = await modelSystem.get('db.ver');
if (dbVer !== 1) {
const ins = require('../script/upgrade0_1');
await ins.run({ username: 'Root', password: 'rootroot' });
}
for (const postInit of global.Hydro.postInit) {
await postInit().catch(console.error);
}
return await modelSystem.get('server.worker');
}
+169
View File
@@ -0,0 +1,169 @@
import os from 'os';
import path from 'path';
import http from 'http';
import fs from 'fs-extra';
import Koa, { Context } from 'koa';
import Body from 'koa-body';
import cache from 'koa-static-cache';
import nunjucks from 'nunjucks';
import mongodb from 'mongodb';
import { argv } from 'yargs';
const listenPort = argv.port || 8888;
class Loader {
getSource(name: string) { // eslint-disable-line class-methods-use-this
return {
src: `
<!DOCTYPE html>
<html data-page="setup" data-layout="immersive" class="layout--immersive page--setup nojs">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="msapplication-TileColor" content="#579e9a">
<meta name="theme-color" content="#56758f">
<link rel="stylesheet" media="all" href="/vj4.css">
<title>{{ _('Setup') }}</title>
</head>
<body>
<div class="slideout-panel" id="panel">
<div class="main">
<div class="row"><div class="columns">
<div class="immersive--content immersive--center">
<h1>{{ _('Setup') }}</h1>
<form method="POST">
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Host') }}
<input name="host" type="text" value="127.0.0.1" autofocus>
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Port') }}
<input name="port" type="number" value="27017">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Name') }}
<input name="name" type="text" value="hydro">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Username') }}
<input name="username" type="text" placeholder="{{ _('Leave blank if none') }}">
</label>
</div></div>
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Database Password') }}
<input name="password" type="password" placeholder="{{ _('Leave blank if none') }}">
</label>
</div></div>
<div class="row"><div class="columns">
<div class="text-center">
<input type="submit" value="{{ _('Confirm') }}" class="inverse expanded rounded primary button">
</div>
</div></div>
</form>
</div>
</div></div>
</div>
</div>
</body>
</html>
`,
path: name,
noCache: false,
};
}
}
const env = new nunjucks.Environment(new Loader(), { autoescape: true, trimBlocks: true });
function render(name: string) {
return new Promise((resolve, reject) => {
env.render(name, {
_: (str: string) => str,
}, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
}
async function get(ctx: Context) {
ctx.body = await render('setup.html');
ctx.response.type = 'text/html';
}
async function post(ctx: Context) {
const {
host, port, name, username, password,
} = ctx.request.body;
let mongourl = 'mongodb://';
if (username) mongourl += `${username}:${password}@`;
mongourl += `${host}:${port}/${name}`;
try {
const Database = await mongodb.MongoClient.connect(mongourl, {
useNewUrlParser: true, useUnifiedTopology: true,
});
const db = Database.db(name);
await Promise.all([
db.collection('system').updateOne(
{ _id: 'server.host' },
{ $set: { value: ctx.request.host } },
{ upsert: true },
),
db.collection('system').updateOne(
{ _id: 'server.hostname' },
{ $set: { value: ctx.request.hostname } },
{ upsert: true },
),
db.collection('system').updateOne(
{ _id: 'server.url' },
{ $set: { value: ctx.request.href } },
{ upsert: true },
),
db.collection('system').updateOne(
{ _id: 'server.port' },
{ $set: { value: parseInt(listenPort as string, 10) } },
{ upsert: true },
),
]);
fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({
host, port, name, username, password,
}));
ctx.body = `<h1>This page will reload in 3 secs.</h1>
<script>
setTimeout(function (){
window.location.href = '/';
}, 3000);
</script>`;
setTimeout(() => process.exit(0), 500);
} catch (e) {
ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`;
}
}
export function load() {
const app = new Koa();
const server = http.createServer(app.callback());
app.keys = ['Hydro'];
app
.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), {
maxAge: 365 * 24 * 60 * 60,
}))
.use(Body())
.use((ctx) => {
if (ctx.request.method.toLowerCase() === 'post') return post(ctx);
return get(ctx);
});
server.listen(listenPort);
console.log('Server listening at:', listenPort);
}
+59
View File
@@ -0,0 +1,59 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable import/no-dynamic-require */
import {
locale, template, lib, service, model, handler, script, setting, uistatic,
builtinLib, builtinScript, builtinHandler, builtinModel,
} from './common';
export async function load() {
let pending = global.addons;
const fail = [];
const active = [];
require('../lib/i18n');
require('../utils');
require('../error');
require('../options');
await Promise.all([
locale(pending, fail),
template(pending, fail),
uistatic(pending, fail),
]);
const bus = require('../service/bus');
await new Promise((resolve) => {
const h = () => {
console.log('Database connected');
bus.unsubscribe(['system_database_connected'], h);
resolve();
};
bus.subscribe(['system_database_connected'], h);
require('../service/db');
});
for (const i of builtinLib) require(`../lib/${i}`);
await lib(pending, fail);
require('../service/gridfs');
require('../service/monitor');
const server = require('../service/server');
await server.prepare();
await service(pending, fail);
for (const i of builtinModel) require(`../model/${i}`);
for (const i of builtinHandler) require(`../handler/${i}`);
await model(pending, fail);
const modelSetting = require('../model/setting');
await setting(pending, fail, modelSetting);
await handler(pending, fail);
for (const i in global.Hydro.handler) {
await global.Hydro.handler[i]();
}
const notfound = require('../handler/notfound');
await notfound.apply();
for (const i of builtinScript) require(`../script/${i}`);
await script(pending, fail, active);
for (const postInit of global.Hydro.postInit) await postInit();
pending = [];
await server.start();
setInterval(() => {
process.send({ event: 'stat', count: global.Hydro.stat.reqCount });
global.Hydro.stat.reqCount = 0;
}, 30 * 1000);
return { active, fail };
}
+196
View File
@@ -0,0 +1,196 @@
/* eslint-disable func-names */
import { isClass } from './utils';
interface IHydroError {
new(...args: any[]): HydroError
}
function isHydroError(item: any): item is IHydroError {
return isClass(item);
}
const Err = (name: string, ...info: Array<IHydroError | (() => string) | string | number>) => {
let Class: IHydroError;
let msg: () => string;
let code: number;
for (const item of info) {
if (typeof item === 'number') {
code = item;
} else if (typeof item === 'string') {
msg = function () { return item; };
} else if (isHydroError(item)) {
Class = item;
} else if (typeof item === 'function') {
msg = item;
}
}
const HydroError = class extends Class { };
HydroError.prototype.name = name;
if (msg) HydroError.prototype.msg = msg;
if (code) HydroError.prototype.code = code;
return HydroError;
};
export class HydroError extends Error {
params: any[];
code: number;
constructor(...params: any[]) {
super();
this.params = params;
}
// eslint-disable-next-line class-methods-use-this
msg() {
return 'HydroError';
}
}
export const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
export const SystemError = Err('SystemError', HydroError, 'SystemError', 500);
export const BadRequestError = Err('BadRequestError', UserFacingError, 'BadRequestError', 400);
export const ForbiddenError = Err('ForbiddenError', UserFacingError, 'ForbiddenError', 403);
export const NotFoundError = Err('NotFoundError', UserFacingError, 'NotFoundError', 404);
export const MethodNotAllowedError = Err('MethodNotAllowedError', UserFacingError, 'MethodNotAllowedError', 405);
export const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingError, 'RemoteOnlineJudgeError', 500);
export const SendMailError = Err('SendMailError', UserFacingError, 'Failed to send mail to {0}.', 500);
export const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted.");
export const LoginError = Err('LoginError', ForbiddenError, 'Invalid password for user {0}.');
export const UserAlreadyExistError = Err('UserAlreadyExistError', ForbiddenError, 'User {0} already exists.');
export const InvalidTokenError = Err('InvalidTokenError', ForbiddenError);
export const BlacklistedError = Err('BlacklistedError', ForbiddenError, 'Address or user {0} is blacklisted.');
export const VerifyPasswordError = Err('VerifyPasswordError', ForbiddenError, "Passwords don't match.");
export const OpcountExceededError = Err('OpcountExceededError', ForbiddenError, 'Too frequent operations of {0} (limit: {2} operations in {1} seconds).');
export const PermissionError = Err('PermissionError', ForbiddenError, "You don't have the required permission ({0}) in this domain.");
export const PrivilegeError = Err('PrivilegeError', ForbiddenError, function () {
if (this.params.includes(global.Hydro.model.builtin.PRIV.PRIV_USER_PROFILE)) {
return "You're not logged in.";
}
return "You don't have the required privilege.";
});
export const ValidationError = Err('ValidationError', ForbiddenError, function () {
if (this.params.length === 1) return 'Field {0} validation failed.';
return 'Field {0} or {1} validation failed.';
});
export const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenError, "You haven't attended this contest yet.");
export const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest.");
export const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.');
export const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.');
export const TrainingAlreadyEnrollError = Err('TrainingAlreadyEnrollError', ForbiddenError, "You've already enrolled this training.");
export const HomeworkNotLiveError = Err('HomeworkNotLiveError', ForbiddenError, 'This homework is not open.');
export const HomeworkNotAttendedError = Err('HomeworkNotAttendedError', ForbiddenError, "You haven't claimed this homework yet.");
export const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.');
export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError');
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {0} not found.');
export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
export const ProblemNotFoundError = Err('ProblemNotFountError', DocumentNotFoundError, 'Problem {0} not found.');
export const SolutionNotFoundError = Err('SolutionNotFoundError', DocumentNotFoundError, 'Solution {0} not found.');
export const TrainingNotFoundError = Err('TrainingNotFoundError', DocumentNotFoundError, 'Training {0} not found.');
export const ContestNotFoundError = Err('ContestNotFoundError', DocumentNotFoundError, 'Contest {0} not found.');
export const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNotFoundError, 'Discussion {0} not found.');
export const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.');
export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError);
global.Hydro.error = {
HydroError,
BadRequestError,
BlacklistedError,
ForbiddenError,
NotFoundError,
SendMailError,
LoginError,
CsrfTokenError,
MethodNotAllowedError,
InvalidOperationError,
UserAlreadyExistError,
InvalidTokenError,
UserNotFoundError,
VerifyPasswordError,
ProblemDataNotFoundError,
OpcountExceededError,
PermissionError,
PrivilegeError,
NoProblemError,
ValidationError,
ProblemNotFoundError,
TrainingNotFoundError,
ContestNotFoundError,
RecordNotFoundError,
SolutionNotFoundError,
AlreadyVotedError,
ContestNotAttendedError,
ContestNotLiveError,
ContestScoreboardHiddenError,
ContestAlreadyAttendedError,
UserFacingError,
SystemError,
TrainingAlreadyEnrollError,
HomeworkNotLiveError,
HomeworkNotAttendedError,
RemoteOnlineJudgeError,
DiscussionNodeNotFoundError,
DocumentNotFoundError,
DiscussionNotFoundError,
RoleAlreadyExistError,
MessageNotFoundError,
};
/*
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 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 TrainingRequirementNotSatisfiedError(ForbiddenError):
@property
def message(self):
return 'Training requirement is not satisfied.'
class UsageExceededError(ForbiddenError):
@property
def message(self):
return 'Usage exceeded.'
class InvalidArgumentError(BadRequestError):
@property
def message(self):
return 'Argument {0} is invalid.'
*/
+420
View File
@@ -0,0 +1,420 @@
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import {
ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError, PermissionError,
} from '../error';
import { isContent, isTitle } from '../lib/validator';
import paginate from '../lib/paginate';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as contest from '../model/contest';
import * as document from '../model/document';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as user from '../model/user';
import * as message from '../model/message';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as bus from '../service/bus';
const ContestHandler = contest.ContestHandlerMixin(Handler);
class ContestListHandler extends ContestHandler {
@param('rule', Types.String, true)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, rule = '', page = 1) {
this.response.template = 'contest_main.html';
let tdocs;
let qs;
let tpcount;
if (!rule) {
tdocs = contest.getMulti(domainId).sort({ beginAt: -1 });
qs = '';
} else {
if (!contest.RULES[rule]) throw new ValidationError('rule');
tdocs = contest.getMulti(domainId, { rule }).sort({ beginAt: -1 });
qs = `rule=${rule}`;
}
// eslint-disable-next-line prefer-const
[tdocs, tpcount] = await paginate(tdocs, page, CONSTANT.CONTEST_PER_PAGE);
const tids = [];
for (const tdoc of tdocs) tids.push(tdoc.docId);
const tsdict = await contest.getListStatus(domainId, this.user._id, tids);
const path = [
['Hydro', 'homepage'],
['contest_main', null],
];
this.response.body = {
page, tpcount, qs, rule, tdocs, tsdict, path,
};
}
}
class ContestDetailHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid);
this.response.template = 'contest_detail.html';
const [tsdoc, pdict] = await Promise.all([
contest.getStatus(domainId, tdoc.docId, this.user._id),
problem.getList(domainId, tdoc.pids, true),
]);
const psdict = {};
let rdict = {};
let attended: boolean;
if (tsdoc) {
attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail;
if (this.canShowRecord(tdoc)) {
const q = [];
for (const i in psdict) q.push(psdict[i].rid);
rdict = await record.getList(domainId, q, true);
} else {
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
}
} else attended = false;
const udict = await user.getList(domainId, [tdoc.owner]);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, null, null, true],
];
this.response.body = {
path, tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
};
}
@param('tid', Types.ObjectID)
async postAttend(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (contest.isDone(tdoc)) throw new ContestNotLiveError(tid);
await contest.attend(domainId, tid, this.user._id);
this.back();
}
}
class ContestBoardcastHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, 'contest_detail', { tid }, true],
['contest_boardcast'],
];
this.response.template = 'contest_boardcast.html';
this.response.body = path;
}
@param('tid', Types.ObjectID)
@param('content', Types.String, isContent)
async post(domainId: string, tid: ObjectID, content: string) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray();
const uids = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
await Promise.all(
uids.map((uid) => message.send(this.user._id, uid, content, message.FLAG_ALERT)),
);
this.response.redirect = this.url('contest_detail', { tid });
}
}
class ContestScoreboardHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const [tdoc, rows, udict] = await this.getScoreboard(domainId, tid);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[tdoc.title, 'contest_detail', { tid }, true],
['contest_scoreboard', null],
];
this.response.template = 'contest_scoreboard.html';
this.response.body = {
tdoc, rows, path, udict,
};
}
}
class ContestScoreboardDownloadHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('ext', Types.String)
async get(domainId: string, tid: ObjectID, ext: string) {
const getContent = {
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
};
if (!getContent[ext]) throw new ValidationError('ext');
const [, rows] = await this.getScoreboard(domainId, tid, true);
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
}
}
class ContestEditHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await contest.get(domainId, tid);
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST);
else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF);
}
async get() {
this.response.template = 'contest_edit.html';
const rules = {};
for (const i in contest.RULES) {
rules[i] = contest.RULES[i].TEXT;
}
const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
['contest_edit', null],
];
const dt = this.tdoc.beginAt;
this.response.body = {
rules,
tdoc: this.tdoc,
duration,
path,
pids: this.tdoc.pids.join(','),
date_text: dt.format('%Y-%m-%d'),
time_text: dt.format('%H:%M'),
page_name: 'contest_edit',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('duration', Types.Float)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('rule', Types.String)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
) {
let pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAtMoment.isValid()) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
const beginAt = beginAtMoment.toDate();
pids = await this.verifyProblems(domainId, pids);
await contest.edit(domainId, this.tdoc.docId, {
title, content, rule, beginAt, endAt, pids, rated,
});
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
await contest.recalcStatus(domainId, this.tdoc.docId);
}
this.response.body = { tid: this.tdoc.docId };
this.response.redirect = this.url('contest_detail', { tid: this.tdoc.docId });
}
}
class ContestProblemHandler extends ContestHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async _prepare(domainId: string, tid: ObjectID, pid: number) {
[this.tdoc, this.pdoc] = await Promise.all([
contest.get(domainId, tid),
problem.get(domainId, pid, this.user._id),
]);
[this.tsdoc, this.udoc] = await Promise.all([
contest.getStatus(domainId, this.tdoc.docId, this.user._id),
user.getById(domainId, this.tdoc.owner),
]);
this.attended = this.tsdoc && this.tsdoc.attend === 1;
this.response.template = 'problem_detail.html';
if (contest.isDone(this.tdoc)) {
if (!this.attended) throw new ContestNotAttendedError(this.tdoc.docId);
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId);
}
if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc.docId.toString())) {
throw new ProblemNotFoundError(pid, this.tdoc.docId);
}
}
async get() {
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
[this.pdoc.title, null, null, true],
];
this.response.body = {
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attended,
path,
};
}
}
class ContestProblemSubmitHandler extends ContestProblemHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async prepare(domainId: string, tid: ObjectID, pid: number) {
let rdocs = [];
if (this.canShowRecord(this.tdoc)) {
rdocs = await record.getUserInProblemMulti(
domainId, this.user._id,
this.pdoc.docId, true,
).sort({ _id: -1 }).limit(10).toArray();
}
this.response.template = 'problem_submit.html';
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
[this.tdoc.title, 'contest_detail', { tid }, true],
[this.pdoc.title, 'contest_detail_problem', { tid, pid }, true],
['contest_detail_problem_submit', null],
];
this.response.body = {
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attend,
path,
rdocs,
};
}
@param('tid', Types.ObjectID)
@param('lang', Types.String)
@param('code', Types.String)
async post(domainId: string, tid: ObjectID, lang: string, code: string) {
await this.limitRate('add_record', 60, 100);
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, {
type: document.TYPE_CONTEST,
tid,
});
const [rdoc] = await Promise.all([
record.get(domainId, rid),
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
]);
bus.publish('record_change', rdoc);
if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
} else {
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
}
class ContestCodeHandler extends ContestHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
}
this.limitRate('homework_code', 3600, 60);
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
const rnames = {};
for (const tsdoc of tsdocs) {
for (const pdetail of tsdoc.detail || []) {
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
}
}
const zip = new AdmZip();
const rdocs = await record.getMulti(domainId, {
_id: {
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
},
}).toArray();
for (const rdoc of rdocs) {
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
}
class ContestCreateHandler extends ContestHandler {
async get() {
this.response.template = 'contest_edit.html';
const rules = {};
for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
const now = new Date();
let ts = now.getTime();
ts = ts - (ts % (15 * 60 * 1000)) + 15 * 60 * 1000;
const dt = new Date(ts);
const path = [
['Hydro', 'homepage'],
['contest_main', 'contest_main'],
['contest_create', null],
];
this.response.body = {
rules,
path,
page_name: 'contest_create',
date_text: dt.format('%Y-%m-%d'),
time_text: dt.format('%M:%S'),
pids: '1000, 1001',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('duration', Types.Float)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('rule', Types.String)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
) {
let pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAt.clone().add(duration, 'hours');
pids = await this.verifyProblems(domainId, pids);
const tid = await contest.add(
domainId, title, content,
this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated,
);
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
}
}
export async function apply() {
Route('contest_create', '/contest/create', ContestCreateHandler, PERM.PERM_CREATE_CONTEST);
Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler);
Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
}
global.Hydro.handler.contest = apply;
+372
View File
@@ -0,0 +1,372 @@
import { ObjectID } from 'mongodb';
import { isSafeInteger } from 'lodash';
import { DiscussionNotFoundError, DocumentNotFoundError } from '../error';
import {
Mdoc, Drdoc, Ddoc, Drrdoc,
} from '../interface';
import paginate from '../lib/paginate';
import * as user from '../model/user';
import * as message from '../model/message';
import * as discussion from '../model/discussion';
import * as document from '../model/document';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import {
Route, Handler, Types, param,
} from '../service/server';
import { isTitle, isContent } from '../lib/validator';
export const typeMapper = {
problem: document.TYPE_PROBLEM,
contest: document.TYPE_CONTEST,
node: document.TYPE_DISCUSSION_NODE,
training: document.TYPE_TRAINING,
homework: document.TYPE_HOMEWORK,
};
class DiscussionHandler extends Handler {
ddoc?: Ddoc;
drdoc?: Drdoc;
drrdoc?: Drrdoc;
vnode?: any;
@param('type', Types.String, true)
@param('name', Types.String, true)
@param('did', Types.ObjectID, true)
@param('drid', Types.ObjectID, true)
@param('drrid', Types.ObjectID, true)
async _prepare(
domainId: string, type: string, name: string,
did: ObjectID, drid: ObjectID, drrid: ObjectID,
) {
this.checkPerm(PERM.PERM_VIEW_DISCUSSION);
if (did) {
this.ddoc = await discussion.get(domainId, did);
if (!this.ddoc) throw new DiscussionNotFoundError(did);
type = discussion.typeDisplay[this.ddoc.parentType];
name = this.ddoc.parentId.toString();
if (drrid) {
[this.drdoc, this.drrdoc] = await discussion.getTailReply(domainId, drid, drrid);
if (!this.drrdoc) throw new DiscussionNotFoundError(drrid);
} else if (drid) {
this.drdoc = await discussion.getReply(domainId, drid);
if (!this.drdoc) throw new DiscussionNotFoundError(drid);
if (!this.drdoc.parentId.equals(this.ddoc._id)) {
throw new DocumentNotFoundError(drid);
}
}
}
// TODO(twd2): do more visibility check eg. contest
// TODO(twd2): exclude problem/contest discussions?
// TODO(iceboy): continuation based pagination.
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name);
if (this.vnode.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (this.ddoc) {
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
this.ddoc.parentId = this.ddoc.parentId || this.vnode.id;
}
}
}
class DiscussionMainHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner));
const path = [
['Hydro', 'homepage'],
['discussion_main', null],
];
const [vndict, vnodes] = await Promise.all([
discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
),
discussion.getNodes(domainId),
]);
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, vnodes,
};
}
}
class DiscussionNodeHandler extends DiscussionHandler {
@param('type', Types.String)
@param('name', Types.String)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, type: string, _name: string, page = 1) {
let name: ObjectID | string | number;
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
else name = _name;
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const uids = ddocs.map((ddoc) => ddoc.owner);
uids.push(this.vnode.owner);
const [udict, vnodes] = await Promise.all([
user.getList(domainId, uids),
discussion.getNodes(domainId),
]);
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, null, true],
];
const vndict = { [typeMapper[type]]: { [name.toString()]: this.vnode } };
this.response.template = 'discussion_main_or_node.html';
this.response.body = {
ddocs,
dpcount,
udict,
path,
page,
vndict,
vnode: this.vnode,
page_name: 'discussion_node',
vnodes,
};
}
}
class DiscussionCreateHandler extends DiscussionHandler {
async get({ type, name }) {
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type, name }, true],
['discussion_create', null],
];
this.response.template = 'discussion_create.html';
this.response.body = { path, vnode: this.vnode };
}
@param('type', Types.String)
@param('name', Types.String)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('highlight', Types.Boolean)
@param('pin', Types.Boolean)
async post(
domainId: string, type: string, _name: string,
title: string, content: string, highlight = false, pin = false,
) {
this.limitRate('add_discussion', 3600, 30);
let name: ObjectID | string | number;
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
else name = _name;
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
const did = await discussion.add(
domainId, typeMapper[type], name, this.user._id,
title, content, this.request.ip, highlight, pin,
);
this.response.body = { did };
this.response.redirect = this.url('discussion_detail', { did });
}
}
class DiscussionDetailHandler extends DiscussionHandler {
@param('did', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, did: ObjectID, page = 1) {
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
? await discussion.getStatus(domainId, did, this.user._id)
: null;
const [drdocs, pcount, drcount] = await paginate(
discussion.getMultiReply(domainId, did),
page,
CONSTANT.REPLY_PER_PAGE,
);
const uids = drdocs.map((drdoc) => drdoc.owner);
uids.push(this.ddoc.owner);
for (const drdoc of drdocs) {
if (drdoc.reply) {
drdocs.forEach((drrdoc) => uids.push(drrdoc.owner));
}
}
const udict = await user.getList(domainId, uids);
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
[this.ddoc.title, null, null, true],
];
this.response.template = 'discussion_detail.html';
this.response.body = {
path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict, vnode: this.vnode,
};
}
async post() {
this.checkPriv(PRIV.PRIV_USER_PROFILE);
}
@param('did', Types.ObjectID)
@param('content', Types.String, isContent)
async postReply(domainId: string, did: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
this.limitRate('add_discussion', 3600, 30);
// Notify related users
const replies: Drdoc[] = await discussion.getMultiReply(domainId, did).toArray();
const uids = Array.from(new Set(replies.map((drdoc) => drdoc.owner)));
const tasks = uids.map((uid) => {
if (uid !== this.user._id) {
return message.send(1, uid, `Discussion update: ${domainId} ${did}`);
}
return Promise.resolve(null as Mdoc);
});
await Promise.all(tasks);
await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
this.back();
}
@param('drid', Types.ObjectID)
@param('content', Types.String, isContent)
async postTailReply(domainId: string, drid: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
this.limitRate('add_discussion', 3600, 30);
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
this.back();
}
@param('drid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditReply(domainId: string, drid: ObjectID, content: string) {
if (!(this.ddoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION))) {
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
}
await discussion.editReply(domainId, drid, content);
this.back();
}
@param('drid', Types.ObjectID)
async postDeleteReply(domainId: string, drid: ObjectID) {
if (!(this.ddoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION))) {
if (this.drdoc.owner !== this.user._id) {
this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
} else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
}
await discussion.delReply(domainId, drid);
this.back();
}
@param('drid', Types.ObjectID)
@param('drrid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) {
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
await discussion.editTailReply(domainId, drid, drrid, content);
this.back();
}
@param('drid', Types.ObjectID)
@param('drrid', Types.ObjectID)
async postDeleteTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
await discussion.delTailReply(domainId, drid, drrid);
this.back();
}
@param('did', Types.ObjectID)
async postStar(domainId: string, did: ObjectID) {
await discussion.setStar(domainId, did, this.user._id, true);
this.back({ star: true });
}
@param('did', Types.ObjectID)
async postUnstar(domainId: string, did: ObjectID) {
await discussion.setStar(domainId, did, this.user._id, false);
this.back({ star: false });
}
}
class DiscussionDetailRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.ddoc.content;
}
}
class DiscussionReplyRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.drdoc.content;
}
}
class DiscussionTailReplyRawHandler extends DiscussionHandler {
async get() {
this.response.type = 'text/markdown';
this.response.body = this.drrdoc.content;
}
}
class DiscussionEditHandler extends DiscussionHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['discussion_main', 'discussion_main'],
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
[this.ddoc.title, 'discussion_detail', { did: this.ddoc.docId }, true],
['discussion_edit', null],
];
this.response.template = 'discussion_edit.html';
this.response.body = { ddoc: this.ddoc, path };
}
@param('did', Types.ObjectID)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('highlight', Types.Boolean)
@param('pin', Types.Boolean)
async postUpdate(
domainId: string, did: ObjectID, title: string, content: string,
highlight = false, pin = false,
) {
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION);
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF);
if (highlight !== this.ddoc.highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
if (pin !== this.ddoc.pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
await discussion.edit(domainId, did, title, content, highlight, pin);
this.response.body = { did };
this.response.redirect = this.url('discussion_detail', { did });
}
@param('did', Types.ObjectID)
async postDelete(domainId: string, did: ObjectID) {
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION);
else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
await discussion.del(domainId, did);
this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId };
this.response.redirect = this.url('discussion_node', {
type: discussion.typeDisplay[this.ddoc.parentType],
name: this.ddoc.parentId,
});
}
}
export async function apply() {
Route('discussion_main', '/discuss', DiscussionMainHandler);
Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
Route('discussion_detail', '/discuss/:did/raw', DiscussionDetailRawHandler);
Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionReplyRawHandler);
Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionTailReplyRawHandler);
Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
Route('discussion_create', '/discuss/:type/:name/create', DiscussionCreateHandler, PRIV.PRIV_USER_PROFILE, PERM.PERM_CREATE_DISCUSSION);
}
global.Hydro.handler.discussion = apply;
+208
View File
@@ -0,0 +1,208 @@
import { RoleAlreadyExistError, ValidationError } from '../error';
import * as user from '../model/user';
import * as domain from '../model/domain';
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
import { gravatar } from '../lib/misc';
import paginate from '../lib/paginate';
import {
Route, Handler, Types, param,
} from '../service/server';
class DomainRankHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [dudocs, upcount, ucount] = await paginate(
domain.getMultiInDomain(domainId).sort({ rp: -1 }),
page,
100,
);
let udocs = [];
for (const dudoc of dudocs) {
udocs.push(user.getById(domainId, dudoc.uid));
}
udocs = await Promise.all(udocs);
const path = [
['Hydro', 'homepage'],
['ranking', null],
];
this.response.template = 'ranking.html';
this.response.body = {
udocs, upcount, ucount, page, path,
};
}
}
class ManageHandler extends Handler {
domain: any;
async prepare({ domainId }) {
this.checkPerm(PERM.PERM_EDIT_DOMAIN);
this.domain = await domain.get(domainId);
}
}
class DomainEditHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_edit', null],
];
this.response.template = 'domain_edit.html';
this.response.body = { current: this.domain, settings: DOMAIN_SETTINGS, path };
}
async post(args) {
const $set = {};
for (const key in args) {
if (DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key];
}
await domain.edit(args.domainId, $set);
this.response.redirect = this.url('domain_dashboard');
}
}
class DomainDashboardHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_dashboard', null],
];
this.response.template = 'domain_dashboard.html';
this.response.body = { domain: this.domain, path };
}
}
class DomainUserHandler extends ManageHandler {
async get({ domainId }) {
const rudocs = {};
const [dudocs, roles] = await Promise.all([
domain.getMultiInDomain(domainId, {
$and: [
{ role: { $ne: 'default' } },
{ role: { $ne: null } },
],
}).toArray(),
domain.getRoles(domainId),
]);
const uids = dudocs.map((dudoc) => dudoc.uid);
const udict = await user.getList(domainId, uids);
for (const role of roles) rudocs[role._id] = [];
for (const dudoc of dudocs) {
const ud = udict[dudoc.uid];
rudocs[ud.role].push(ud);
}
const rolesSelect = roles.map((role) => [role._id, role._id]);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_user', null],
];
this.response.template = 'domain_user.html';
this.response.body = {
roles, rolesSelect, rudocs, udict, path, domain: this.domain,
};
}
@param('uid', Types.Int)
@param('role', Types.String)
async postSetUser(domainId: string, uid: number, role: string) {
await domain.setUserRole(domainId, uid, role);
this.back();
}
}
class DomainPermissionHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const log2 = (val: bigint) => {
// @ts-ignore
for (let i = 0n; ; i++) {
// @ts-ignore
if ((val >> i) === 0n) return parseInt(i.toString(), 10) - 1;
}
};
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_permission', null],
];
this.response.template = 'domain_permission.html';
this.response.body = {
roles, PERMS_BY_FAMILY, domain: this.domain, path, log2,
};
}
async post({ domainId }) {
const roles = {};
for (const role in this.request.body) {
if (this.request.body[role] instanceof Array) {
const perms = this.request.body[role];
// @ts-ignore
roles[role] = 0n;
// @ts-ignore
for (const r of perms) roles[role] += 1n << BigInt(r);
}
}
await domain.setRoles(domainId, roles);
this.back();
}
}
class DomainRoleHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_role', null],
];
this.response.template = 'domain_role.html';
this.response.body = { roles, domain: this.domain, path };
}
@param('role', Types.String)
async postAdd(domainId: string, role: string) {
const roles = await domain.getRoles(this.domain);
const rdict: any = {};
for (const r of roles) rdict[r._id] = r.perm;
if (rdict[role]) throw new RoleAlreadyExistError(role);
await domain.addRole(domainId, role, rdict.default.perm);
this.back();
}
async postDelete({ domainId, roles }) {
for (const role of roles) {
if (['root', 'default', 'guest'].includes(role)) {
throw new ValidationError('role');
}
}
await domain.deleteRoles(domainId, roles);
this.back();
}
}
class DomainSearchHandler extends Handler {
@param('q', Types.String)
async get(domainId: string, q: string) {
const ddocs = await domain.getPrefixSearch(q, 20);
for (let i = 0; i < ddocs.length; i++) {
ddocs[i].gravatar = ddocs[i].gravatar ? gravatar(ddocs[i].gravatar) : '/img/team_avatar.png';
}
this.response.body = ddocs;
}
}
export async function apply() {
Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
Route('domain_edit', '/domain/edit', DomainEditHandler);
Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.domain = apply;
+410
View File
@@ -0,0 +1,410 @@
import { ObjectID } from 'mongodb';
import {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError, UserNotFoundError, PermissionError,
} from '../error';
import * as bus from '../service/bus';
import {
Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server';
import * as misc from '../lib/misc';
import md5 from '../lib/md5';
import * as mail from '../lib/mail';
import * as contest from '../model/contest';
import * as message from '../model/message';
import * as document from '../model/document';
import * as system from '../model/system';
import * as user from '../model/user';
import * as file from '../model/file';
import * as setting from '../model/setting';
import * as domain from '../model/domain';
import * as discussion from '../model/discussion';
import * as token from '../model/token';
import * as training from '../model/training';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import {
isContent, isPassword, isEmail, isTitle,
} from '../lib/validator';
const { geoip, useragent } = global.Hydro.lib;
class HomeHandler extends Handler {
async homework(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) {
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK)
.sort('beginAt', -1)
.limit(CONSTANT.HOMEWORK_ON_MAIN)
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id,
tdocs.map((tdoc) => tdoc.docId), document.TYPE_HOMEWORK,
);
return [tdocs, tsdict];
}
return [[], {}];
}
async contest(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) {
const tdocs = await contest.getMulti(domainId)
.sort('beginAt', -1)
.limit(CONSTANT.CONTEST_ON_MAIN)
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
);
return [tdocs, tsdict];
}
return [[], {}];
}
async training(domainId: string) {
if (this.user.hasPerm(PERM.PERM_VIEW_TRAINING)) {
const tdocs = await training.getMulti(domainId)
.sort('_id', 1)
.limit(CONSTANT.TRAINING_ON_MAIN)
.toArray();
const tsdict = await training.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
);
return [tdocs, tsdict];
}
return [[], {}];
}
async discussion(domainId: string): Promise<[any[], any]> {
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
const ddocs = await discussion.getMulti(domainId)
.limit(CONSTANT.DISCUSSION_ON_MAIN)
.toArray();
const vndict = await discussion.getListVnodes(
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
);
return [ddocs, vndict];
}
return [[], {}];
}
async get({ domainId }) {
const [
[htdocs, htsdict], [tdocs, tsdict],
[trdocs, trsdict], [ddocs, vndict],
] = await Promise.all([
this.homework(domainId), this.contest(domainId),
this.training(domainId), this.discussion(domainId),
]);
const [udict, dodoc, vnodes] = await Promise.all([
user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)),
domain.get(domainId),
discussion.getNodes(domainId),
]);
this.response.template = 'main.html';
this.response.body = {
htdocs,
htsdict,
tdocs,
tsdict,
trdocs,
trsdict,
ddocs,
vndict,
udict,
domain: dodoc,
vnodes,
};
}
}
class HomeSecurityHandler extends Handler {
async get() {
// TODO(iceboy): pagination? or limit session count for uid?
const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) {
session.isCurrent = session._id === this.session._id;
session._id = md5(session._id);
if (useragent) session.updateUa = useragent.parse(session.updateUa || session.createUa || '');
if (geoip) {
session.updateGeoip = geoip.lookup(
session.updateIp || session.createIp,
this.translate('geoip_locale'),
);
}
}
const path = [
['Hydro', 'homepage'],
['home_security', null],
];
this.response.template = 'home_security.html';
this.response.body = { sessions, geoipProvider: geoip?.provider, path };
if (useragent) this.response.body.icon = useragent.icon;
}
@param('current', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
async postChangePassword(_: string, current: string, password: string, verify: string) {
if (password !== verify) throw new VerifyPasswordError();
this.user.checkPassword(current);
await user.setPassword(this.user._id, password);
await token.delByUid(this.user._id);
this.response.redirect = this.url('user_login');
}
@param('currentPassword', Types.String)
@param('mail', Types.String, isEmail)
async postChangeMail(domainId: string, current: string, email: string) {
this.limitRate('send_mail', 3600, 30);
this.user.checkPassword(current);
const udoc = await user.getByEmail(domainId, email);
if (udoc) throw new UserAlreadyExistError(email);
const [code] = await token.add(
token.TYPE_CHANGEMAIL,
await system.get('changemail_token_expire_seconds'),
{ uid: this.user._id, email },
);
const m = await this.renderHTML('user_changemail_mail.html', {
path: `home/changeMail/${code}`,
uname: this.user.uname,
url_prefix: await system.get('server.url'),
});
await mail.sendMail(email, 'Change Email', 'user_changemail_mail', m);
this.response.template = 'user_changemail_mail_sent.html';
}
@param('tokenDigest', Types.String)
async postDeleteToken(domainId: string, tokenDigest: string) {
const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) {
if (tokenDigest === md5(session._id)) {
// eslint-disable-next-line no-await-in-loop
await token.del(session._id, token.TYPE_SESSION);
return this.back();
}
}
throw new InvalidTokenError(tokenDigest);
}
async postDeleteAllTokens() {
await token.delByUid(this.user._id);
this.response.redirect = this.url('user_login');
}
}
class HomeSettingsHandler extends Handler {
@param('category', Types.String)
async get(domainId: string, category: string) {
const path = [
['Hydro', 'homepage'],
[`home_${category}`, null],
];
this.response.template = 'home_settings.html';
this.response.body = {
category,
page_name: `home_${category}`,
current: this.user,
path,
};
if (category === 'preference') {
this.response.body.settings = setting.PREFERENCE_SETTINGS;
} else if (category === 'account') {
this.response.body.settings = setting.ACCOUNT_SETTINGS;
} else if (category === 'domain') {
this.response.body.settings = setting.DOMAIN_USER_SETTINGS;
} else throw new NotFoundError();
}
async post(args: any) {
const $set = {};
if (args.category === 'domain') {
for (const key in args) {
if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key]
&& !(setting.DOMAIN_USER_SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await domain.setUserInDomain(args.domainId, this.user._id, $set);
} else {
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
await user.setById(this.user._id, $set);
}
this.back();
}
}
class UserChangemailWithCodeHandler extends Handler {
@param('code', Types.String)
async get(domainId: string, code: string) {
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code);
}
const udoc = await user.getByEmail(domainId, tdoc.email);
if (udoc) throw new UserAlreadyExistError(tdoc.email);
await Promise.all([
user.setEmail(this.user._id, tdoc.email),
token.del(code, token.TYPE_CHANGEMAIL),
]);
this.response.redirect = this.url('home_security');
}
}
class HomeDomainHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['home_domain', null],
];
const dudict = await domain.getDictUserByDomainId(this.user._id);
const dids = Object.keys(dudict);
const ddocs = await domain.getMulti({ _id: { $in: dids } }).toArray();
const canManage = {};
for (const ddoc of ddocs) {
// eslint-disable-next-line no-await-in-loop
const udoc = await user.getById(ddoc._id, this.user._id);
canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)
|| udoc.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN);
}
this.response.template = 'home_domain.html';
this.response.body = {
ddocs, dudict, canManage, path,
};
}
}
class HomeDomainCreateHandler extends Handler {
async get() {
this.response.body = {
path: [
['Hydro', 'homepage'],
['domain_create', null],
],
};
this.response.template = 'domain_create.html';
}
@param('id', Types.String)
@param('name', Types.String, isTitle)
@param('bulletin', Types.String, isContent)
@param('gravatar', Types.String, true, isEmail)
async post(_: string, id: string, name: string, bulletin: string, gravatar: string) {
gravatar = gravatar || this.user.gravatar || this.user.mail || 'guest@hydro.local';
const domainId = await domain.add(id, this.user._id, name, bulletin);
await domain.edit(domainId, { gravatar });
this.response.redirect = this.url('domain_manage', { domainId });
this.response.body = { domainId };
}
}
class HomeMessagesHandler extends Handler {
async get() {
// TODO(iceboy): projection, pagination.
const messages = await message.getByUser(this.user._id);
const udict = await user.getList('system', [
...messages.map((mdoc) => mdoc.from),
...messages.map((mdoc) => mdoc.to),
]);
// TODO(twd2): improve here:
const parsed = {};
for (const m of messages) {
const target = m.from === this.user._id ? m.to : m.from;
if (!parsed[target]) {
parsed[target] = {
_id: target,
udoc: { ...udict[target], gravatar: misc.gravatar(udict[target].gravatar) },
messages: [],
};
}
parsed[target].messages.push(m);
}
const path = [
['Hydro', 'homepage'],
['home_messages', null],
];
this.response.body = { messages: parsed, path };
this.response.template = 'home_messages.html';
}
@param('uid', Types.Int)
@param('content', Types.String, isContent)
async postSend(domainId: string, uid: number, content: string) {
const udoc = await user.getById('system', uid);
if (!udoc) throw new UserNotFoundError(uid);
if (udoc.gravatar) udoc.gravatar = misc.gravatar(udoc.gravatar);
const mdoc = await message.send(this.user._id, uid, content, message.FLAG_UNREAD);
this.back({ mdoc, udoc });
}
@param('messageId', Types.ObjectID)
async postDeleteMessage(domainId: string, messageId: ObjectID) {
const msg = await message.get(messageId);
if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId);
else throw new PermissionError();
this.back();
}
@param('messageId', Types.ObjectID)
async postRead(domainId: string, messageId: ObjectID) {
const msg = await message.get(messageId);
if ([msg.from, msg.to].includes(this.user._id)) {
await message.setFlag(messageId, message.FLAG_UNREAD);
} else throw new PermissionError();
this.back();
}
}
class HomeMessagesConnectionHandler extends ConnectionHandler {
id: string;
async prepare() {
bus.subscribe([`user_message-${this.user._id}`], this.onMessageReceived.bind(this));
}
async onMessageReceived(e: any) {
this.send(e.value);
}
async cleanup() {
bus.unsubscribe([`user_message-${this.user._id}`], this.id);
}
}
class HomeFileHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain_file', null],
];
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
this.response.template = 'home_file.html';
this.response.body = { ufdocs, path };
}
@param('ufid', Types.ObjectID)
async postDelete(domainId: string, ufid: ObjectID) {
const ufdoc = await file.getMeta(ufid);
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
const result = await file.del(ufdoc._id);
if (result) await user.inc(this.user._id, 'usage', -ufdoc.size);
this.back();
}
}
async function apply() {
Route('homepage', '/', HomeHandler);
Route('home_security', '/home/security', HomeSecurityHandler, PRIV.PRIV_USER_PROFILE);
Route('user_changemail_with_code', '/home/changeMail/:code', UserChangemailWithCodeHandler, PRIV.PRIV_USER_PROFILE);
Route('home_settings', '/home/settings/:category', HomeSettingsHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain', '/home/domain', HomeDomainHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain_create', '/home/domain/create', HomeDomainCreateHandler, PRIV.PRIV_CREATE_DOMAIN);
Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV.PRIV_USER_PROFILE);
Route('home_file', '/home/file', HomeFileHandler, PRIV.PRIV_USER_PROFILE);
Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.home = module.exports = apply;
+416
View File
@@ -0,0 +1,416 @@
import yaml from 'js-yaml';
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import AdmZip from 'adm-zip';
import { isSafeInteger } from 'lodash';
import {
ValidationError, HomeworkNotLiveError, ProblemNotFoundError,
HomeworkNotAttendedError,
} from '../error';
import { PenaltyRules } from '../interface';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as bus from '../service/bus';
import { PERM, CONSTANT } from '../model/builtin';
import * as user from '../model/user';
import * as contest from '../model/contest';
import * as discussion from '../model/discussion';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as document from '../model/document';
import paginate from '../lib/paginate';
import { isTitle, isContent } from '../lib/validator';
const validatePenaltyRules = (input: string) => {
const s = yaml.safeLoad(input);
return s;
};
const convertPenaltyRules = validatePenaltyRules;
const HomeworkHandler = contest.ContestHandlerMixin(Handler);
class HomeworkMainHandler extends HomeworkHandler {
async get({ domainId }) {
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray();
const calendar = [];
for (const tdoc of tdocs) {
const cal = { ...tdoc, url: this.url('homework_detail', { tid: tdoc.docId }) };
if (contest.isExtended(tdoc) || contest.isDone(tdoc)) {
cal.endAt = tdoc.endAt;
cal.penaltySince = tdoc.penaltySince;
} else {
cal.endAt = tdoc.penaltySince;
}
calendar.push(cal);
}
const path = [
['Hydro', 'homepage'],
['homework_main', null],
];
this.response.body = { tdocs, calendar, path };
this.response.template = 'homework_main.html';
}
}
class HomeworkDetailHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true)
async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
const [tsdoc, pdict] = await Promise.all([
contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK),
problem.getList(domainId, tdoc.pids, true),
]);
const psdict = {};
let rdict = {};
let attended = false;
if (tsdoc) {
attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) {
psdict[pdetail.pid] = pdetail;
rdict[pdetail.rid] = { _id: pdetail.rid };
}
if (this.canShowRecord(tdoc) && tsdoc.journal) {
rdict = await record.getList(
domainId,
tsdoc.journal.map((pdetail) => pdetail.rid),
true,
);
}
}
// discussion
const [ddocs, dpcount, dcount] = await paginate(
discussion.getMulti(domainId, { parentType: tdoc.docType, parentId: tdoc.docId }),
page,
CONSTANT.DISCUSSION_PER_PAGE,
);
const uids = ddocs.map((ddoc) => ddoc.owner);
uids.push(tdoc.owner);
const udict = await user.getList(domainId, uids);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, null, null, true],
];
this.response.template = 'homework_detail.html';
this.response.body = {
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount, path,
};
}
@param('tid', Types.ObjectID)
async postAttend(domainId: string, tid: ObjectID) {
this.checkPerm(PERM.PERM_ATTEND_HOMEWORK);
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId);
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
this.back();
}
}
class HomeworkDetailProblemHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async prepare(domainId: string, tid: ObjectID, pid: number) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
[this.tdoc, this.pdoc, this.tsdoc] = await Promise.all([
contest.get(domainId, tid, document.TYPE_HOMEWORK),
problem.get(domainId, pid, this.user._id),
contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK),
]);
}
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!contest.isDone(this.tdoc)) {
if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
}
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true],
[this.pdoc.title, null, null, true],
];
this.response.template = 'problem_detail.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path,
};
}
}
class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectID, pid: number) {
const udoc = await user.getById(domainId, this.tdoc.owner);
const attended = this.tsdoc && this.tsdoc.attend === 1;
if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const rdocs = this.canShowRecord(this.tdoc)
? await record.getUserInProblemMulti(domainId, this.user._id, pid, true)
.sort('_id', -1).limit(10).toArray()
: [];
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[this.tdoc.title, 'homework_detail', { tid }, true],
[this.pdoc.title, 'homework_detail_problem', { tid, pid }, true],
['homework_detail_problem_submit', null],
];
this.response.template = 'problem_submit.html';
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, rdocs,
};
}
@param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt)
@param('code', Types.String)
@param('lang', Types.String)
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
this.limitRate('add_record', 3600, 100);
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid);
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, {
type: document.TYPE_HOMEWORK,
tid,
});
const [rdoc] = await Promise.all([
record.get(domainId, rid),
contest.updateStatus(domainId, tid, this.user._id,
rid, pid, false, 0, document.TYPE_HOMEWORK),
]);
bus.publish('record_change', rdoc);
this.response.body = { tid, rid };
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
else this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkCreateHandler extends HomeworkHandler {
async get() {
const beginAt = moment().add(1, 'day');
console.log(beginAt, beginAt.tz(this.user.timeZone));
const penaltySince = beginAt.clone().add(7, 'days');
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
['homework_create', null],
];
this.response.template = 'homework_edit.html';
this.response.body = {
path,
dateBeginText: beginAt.tz(this.user.timeZone).format('YYYY-M-D'),
timeBeginText: '00:00',
datePenaltyText: penaltySince.tz(this.user.timeZone).format('YYYY-M-D'),
timePenaltyText: '23:59',
pids: '1000, 1001',
extensionDays: 1,
page_name: 'homework_create',
};
}
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('penaltySinceDate', Types.Date)
@param('penaltySinceTime', Types.Time)
@param('extensionDays', Types.Float)
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, beginAtDate: string, beginAtTime: string,
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
) {
const pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime');
const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
if (!penaltySince.isValid()) throw new ValidationError('endAtDate', 'endAtTime');
const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
await this.verifyProblems(domainId, pids);
const tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
this.response.body = { tid };
this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkEditHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
const extensionDays = Math.round(
(tdoc.endAt.getTime() - tdoc.penaltySince.getTime()) / 36000 / 24,
) / 100;
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, 'homework_detail', { tid }, true],
['homework_edit', null],
];
const beginAt = moment(tdoc.beginAt).tz(this.user.timeZone);
const penaltySince = moment(tdoc.penaltySince).tz(this.user.timeZone);
this.response.template = 'homework_edit.html';
this.response.body = {
tdoc,
dateBeginText: beginAt.format('YYYY-M-D'),
timeBeginText: beginAt.format('hh:mm'),
datePenaltyText: penaltySince.format('YYYY-M-D'),
timePenaltyText: penaltySince.format('hh:mm'),
extensionDays,
penaltyRules: yaml.safeDump(tdoc.penaltyRules),
pids: tdoc.pids.join(','),
path,
};
}
@param('tid', Types.ObjectID)
@param('beginAtDate', Types.Date)
@param('beginAtTime', Types.Time)
@param('penaltySinceDate', Types.Date)
@param('penaltySinceTime', Types.Time)
@param('extensionDays', Types.Float)
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('pids', Types.String)
@param('rated', Types.Boolean)
async post(
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string,
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: string, title: string, content: string, _pids: string, rated = false,
) {
const pids = _pids.split(',').map((i) => {
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
return i;
});
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
let beginAt;
let penaltySince;
try {
beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
try {
penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
} catch (e) {
throw new ValidationError('endAtDate', 'endAtTime');
}
let endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
await this.verifyProblems(domainId, pids);
beginAt = beginAt.toDate();
endAt = endAt.toDate();
penaltySince = penaltySince.toDate();
await contest.edit(domainId, tid, {
title, content, beginAt, endAt, pids, penaltySince, penaltyRules, rated,
}, document.TYPE_HOMEWORK);
if (tdoc.beginAt !== beginAt
|| tdoc.endAt !== endAt
|| tdoc.penaltySince !== penaltySince
|| new Set(tdoc.pids) !== new Set(pids)) {
await contest.recalcStatus(domainId, tdoc.docId, document.TYPE_HOMEWORK);
}
this.response.body = { tid };
this.response.redirect = this.url('homework_detail', { tid });
}
}
class HomeworkScoreboardHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
console.log(123);
const [tdoc, rows, udict] = await this.getScoreboard(
domainId, tid, false, document.TYPE_HOMEWORK,
);
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
[tdoc.title, 'homework_detail', { tid }, true],
['homework_scoreboard', null],
];
this.response.template = 'contest_scoreboard.html';
this.response.body = {
tdoc, rows, path, udict,
};
}
}
class HomeworkScoreboardDownloadHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
@param('ext', Types.String)
async get(domainId: string, tid: ObjectID, ext: string) {
const getContent = {
csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
};
if (!getContent[ext]) throw new ValidationError('ext');
const [, rows] = await this.getScoreboard(domainId, tid, true, document.TYPE_CONTEST);
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
}
}
class HomeworkCodeHandler extends HomeworkHandler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
this.limitRate('homework_code', 3600, 60);
const [tdoc, tsdocs] = await contest.getAndListStatus(
domainId, tid, document.TYPE_HOMEWORK,
);
const rnames = {};
for (const tsdoc of tsdocs) {
for (const pdetail of tsdoc.detail || []) {
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
}
}
const zip = new AdmZip();
const rdocs = await record.getMulti(domainId, {
_id: {
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
},
}).toArray();
for (const rdoc of rdocs) {
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
}
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
}
}
export async function apply() {
Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM.PERM_CREATE_HOMEWORK);
Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler);
}
global.Hydro.handler.homework = apply;
+61
View File
@@ -0,0 +1,61 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { ValidationError } from '../error';
import * as problem from '../model/problem';
import { PERM } from '../model/builtin';
import {
Route, Handler, Types, param,
} from '../service/server';
import { isPid } from '../lib/validator';
class ProblemImportHandler extends Handler {
async prepare() {
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
}
async get() {
this.response.template = 'problem_import.html';
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
['problem_import', null],
],
};
}
@param('url', Types.String)
@param('pid', Types.String, isPid)
@param('hidden', Types.Boolean)
@param('remoteType', Types.String)
async post(domainId: string, url: string, pid: string, hidden = false, remoteType: string) {
if (typeof global.Hydro.lib[`import.${remoteType}`] !== 'function') {
throw new ValidationError('remoteType');
}
const [pdoc, testdata] = await global.Hydro.lib[`import.${remoteType}`](url, this);
if (hidden) pdoc.hidden = true;
const docId = await problem.add(
domainId, pid || pdoc.pid, pdoc.title, pdoc.content, this.user._id,
pdoc.tag || [], pdoc.category || [], null, pdoc.hidden,
);
if (testdata) {
const file = path.resolve(os.tmpdir(), 'hydro', `import_${domainId}_${pid || pdoc.pid || docId}.zip`);
const w = fs.createWriteStream(file);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', reject);
testdata.pipe(w);
});
await problem.setTestdata(domainId, docId, file);
}
this.response.body = { pid: pid || docId };
this.response.redirect = this.url('problem_settings', { pid: pid || docId });
}
}
export async function apply() {
Route('problem_import', '/problem/import', ProblemImportHandler);
}
global.Hydro.handler.import = apply;
+146
View File
@@ -0,0 +1,146 @@
import { ObjectID } from 'mongodb';
import { JudgeResultBody, Rdoc } from '../interface';
import * as record from '../model/record';
import * as problem from '../model/problem';
import * as builtin from '../model/builtin';
import * as contest from '../model/contest';
import * as domain from '../model/domain';
import * as task from '../model/task';
import * as bus from '../service/bus';
import {
Route, Handler, Connection, ConnectionHandler,
} from '../service/server';
async function _postJudge(rdoc: Rdoc) {
const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED;
const tasks = [];
if (rdoc.contest) {
tasks.push(
contest.updateStatus(
rdoc.domainId, rdoc.contest.tid, rdoc.uid,
rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.contest.type,
),
);
}
if (await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) {
if (accept && !rdoc.rejudged) {
tasks.push(
problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1),
domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1),
);
}
}
await Promise.all(tasks);
}
export async function next(body: JudgeResultBody) {
if (body.rid) body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.domainId, body.rid);
const $set: any = {};
const $push: any = {};
if (body.case) {
const c: any = {};
c.memory = body.case.memory;
c.time = body.case.time;
c.message = body.case.message;
c.status = body.case.status;
rdoc.testCases.push(c);
$push.testCases = c;
}
if (body.message) {
rdoc.judgeTexts.push(body.message);
$push.judgeTexts = body.message;
}
if (body.compilerText) {
rdoc.compilerTexts.push(body.compilerText);
$push.compilerTexts = body.compilerText;
}
if (body.status) $set.status = body.status;
if (body.score) $set.score = body.score;
if (body.time) $set.time = body.time;
if (body.memory) $set.memory = body.memory;
if (body.progress) $set.progress = body.progress;
rdoc = await record.update(body.domainId, body.rid, $set, $push);
bus.publish('record_change', { rdoc, $set, $push });
}
export async function end(body: JudgeResultBody) {
if (body.rid) body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.domainId, body.rid);
const $set: any = {};
const $push: any = {};
const $unset = { progress: '' };
if (body.message) {
rdoc.judgeTexts.push(body.message);
$push.judgeTexts = body.message;
}
if (body.compilerText) {
rdoc.compilerTexts.push(body.compilerText);
$push.compilerTexts = body.compilerText;
}
if (body.status) $set.status = body.status;
if (body.score) $set.score = body.score;
if (body.stdout) $set.stdout = body.stdout;
if (body.stderr) $set.stderr = body.stderr;
if (body.time) $set.time = body.time;
if (body.memory) $set.memory = body.memory;
$set.judgeAt = new Date();
$set.judger = body.judger;
rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset);
await _postJudge(rdoc);
bus.publish('record_change', { rdoc }); // trigger a full update
}
class JudgeHandler extends Handler {
async get({ check = false }) {
if (check) return;
const tasks = [];
let t = await task.getFirst({ type: 'judge' });
while (t) {
tasks.push(t);
t = await task.getFirst({ type: 'judge' }); // eslint-disable-line no-await-in-loop
}
this.response.body = { tasks };
}
async postNext() {
await next(this.request.body);
}
async postEnd() {
this.request.body.judger = this.user._id;
await end(this.request.body);
}
}
class JudgeConnectionHandler extends ConnectionHandler {
processing: any;
async message(msg) {
if (msg.key === 'next') await next(msg);
else if (msg.key === 'end') {
await end({ judger: this.user._id, ...msg });
this.processing = null;
const t = await task.getFirst({ type: 'judge' });
this.send({ task: t });
this.processing = t;
}
}
async cleanup() {
if (this.processing) {
await record.reset(this.processing.domainId, this.processing.rid, false);
task.add(this.processing);
}
}
}
export async function apply() {
Route('judge', '/judge', JudgeHandler, builtin.PRIV.PRIV_JUDGE);
Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
}
apply.next = next;
apply.end = end;
global.Hydro.handler.judge = apply;
+170
View File
@@ -0,0 +1,170 @@
import * as judge from './judge';
import { ValidationError } from '../error';
import * as check from '../check';
import * as setting from '../model/setting';
import * as system from '../model/system';
import { STATUS, PRIV } from '../model/builtin';
import * as record from '../model/record';
import {
Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server';
import { validate } from '../lib/validator';
function set(key, value) {
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
if (s.flag & setting.FLAG_DISABLED) return undefined;
if (s.flag & setting.FLAG_SECRET && !value) return undefined;
if (s.type === 'boolean') {
if (value === 'on') return true;
return false;
}
if (s.type === 'number') {
if (!Number.isSafeInteger(parseInt(value, 10))) {
throw new ValidationError(key);
}
return parseInt(value, 10);
}
return value;
}
return undefined;
}
class SystemHandler extends Handler {
async prepare() {
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
this.response.body = {
path: [
['Hydro', 'homepage'],
],
};
}
}
class SystemMainHandler extends SystemHandler {
async get() {
this.response.redirect = '/manage/dashboard';
}
}
class SystemCheckConnHandler extends ConnectionHandler {
id: any;
async prepare() {
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
await this.check();
}
async check() {
const log = (payload: any) => this.send({ type: 'log', payload });
const warn = (payload: any) => this.send({ type: 'warn', payload });
const error = (payload: any) => this.send({ type: 'error', payload });
await check.start(this, log, warn, error, (id) => { this.id = id; });
}
async cleanup() {
check.cancel(this.id);
}
}
class SystemDashboardHandler extends SystemHandler {
async get() {
this.response.template = 'manage_dashboard.html';
this.response.body.path.push(['manage_dashboard', null]);
}
async postRestart() {
const count = await system.get('server.worker');
process.send({ event: 'restart', count });
this.back();
}
}
class SystemScriptHandler extends SystemHandler {
async get() {
this.response.template = 'manage_script.html';
this.response.body.scripts = global.Hydro.script;
this.response.body.path.push(['manage_script', null]);
}
@param('id', Types.String)
@param('args', Types.String, true)
async post(domainId: string, id: string, raw = '{}') {
if (!global.Hydro.script[id]) throw new ValidationError('id');
const args = JSON.parse(raw);
validate(global.Hydro.script[id].validate, args);
const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: 'raw' });
const report = (data) => judge.next({ domainId, rid, ...data });
report({ message: `Running script: ${id}`, status: STATUS.STATUS_JUDGING });
const start = new Date().getTime();
// Maybe async?
global.Hydro.script[id].run(args, report)
.then((ret: any) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_ACCEPTED,
message: ret.toString(),
judger: 1,
time,
memory: 0,
});
})
.catch((err: Error) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_SYSTEM_ERROR,
message: `${err}\n${err.stack}`,
judger: 1,
time,
memory: 0,
});
});
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
class SystemSettingHandler extends SystemHandler {
async get() {
this.response.template = 'manage_setting.html';
this.response.body.path.push(['manage_setting', null]);
this.response.body.current = {};
this.response.body.settings = setting.SYSTEM_SETTINGS;
for (const s of this.response.body.settings) {
// FIXME no-await-in-loop
// eslint-disable-next-line no-await-in-loop
this.response.body.current[s.key] = await system.get(s.key);
}
}
async post(args: any) {
const tasks = [];
for (const key in args) {
if (typeof args[key] === 'object') {
for (const subkey in args[key]) {
if (typeof set(`${key}.${subkey}`, args[key][subkey]) !== 'undefined') {
tasks.push(system.set(`${key}.${subkey}`, set(`${key}.${subkey}`, args[key][subkey])));
}
}
} else if (typeof set(key, args[key]) !== 'undefined') {
tasks.push(system.set(key, set(key, args[key])));
}
}
await Promise.all(tasks);
this.back();
}
}
async function apply() {
Route('manage', '/manage', SystemMainHandler);
Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
Route('manage_script', '/manage/script', SystemScriptHandler);
Route('manage_setting', '/manage/setting', SystemSettingHandler);
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
}
global.Hydro.handler.manage = apply;
+114
View File
@@ -0,0 +1,114 @@
/* eslint-disable camelcase */
import fs from 'fs';
import { ObjectID } from 'mongodb';
import { PRIV } from '../model/builtin';
import * as system from '../model/system';
import * as file from '../model/file';
import * as user from '../model/user';
import * as db from '../service/db';
import {
Route, Handler, Types, param, multipart,
} from '../service/server';
import { BadRequestError } from '../error';
const coll = db.collection('status');
class FileDownloadHandler extends Handler {
@param('fileId', Types.ObjectID)
@param('secret', Types.String)
@param('name', Types.String, true)
async get(domainId: string, fileId: ObjectID, secret: string, name: string) {
if (name) name = Buffer.from(name, 'base64').toString();
this.response.attachment(name || fileId.toHexString());
this.response.body = await file.getWithSecret(fileId, secret);
}
}
class FileUploadHandler extends Handler {
async getQuota() {
let quota = await system.get('user.quota');
if (this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) {
quota = 2 ** 63 - 1;
}
return quota;
}
async get() {
this.response.template = 'fs_upload.html';
this.response.body = { fdoc: null, usage: this.user.usage, quota: await this.getQuota() };
}
@multipart(256 * 1024)
@param('title', Types.String)
async post(domainId: string, title: string) {
if (!this.request.files.file) throw new BadRequestError();
const quota = await this.getQuota();
const lfdoc = await fs.promises.stat(this.request.files.file.path);
let ufid: ObjectID;
const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
try {
ufid = await file.add(this.request.files.file.path, title, this.user._id);
} catch (e) {
await user.inc(this.user._id, 'usage', -lfdoc.size);
throw e;
}
const fdoc = await file.getMeta(ufid);
this.response.template = 'fs_upload.html';
this.response.body = {
fdoc, ufid, usage: udoc.usage, quota,
};
}
}
class StatusHandler extends Handler {
async get() {
const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray();
for (const i in stats) {
let desc = '';
const online = new Date(stats[i].updateAt).getTime() > new Date().getTime() - 300000;
if (!online) desc = 'Offline';
desc = desc || 'Online';
stats[i].isOnline = online;
stats[i].status = desc;
}
const path = [
['Hydro', 'homepage'],
['status', null],
];
this.response.body = { stats, path };
this.response.template = 'status.html';
}
}
class StatusUpdateHandler extends Handler {
async post(args) {
this.checkPriv(PRIV.PRIV_JUDGE);
args.type = 'judger';
return coll.updateOne(
{ mid: args.mid, type: 'judger' },
{ $set: args },
{ upsert: true },
);
}
}
class SwitchLanguageHandler extends Handler {
@param('lang', Types.String)
async get(domainId: string, lang: string) {
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
await user.setById(this.user._id, { viewLang: lang });
} else this.session.viewLang = lang;
this.back();
}
}
export async function apply() {
Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler);
Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler);
Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
Route('status', '/status', StatusHandler);
Route('status_update', '/status/update', StatusUpdateHandler);
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
}
global.Hydro.handler.misc = apply;
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable no-empty-function */
/* eslint-disable class-methods-use-this */
import { NotFoundError } from '../error';
import { Route, Handler } from '../service/server';
class NotFoundHandler extends Handler {
async prepare() {
throw new NotFoundError();
}
all() { }
}
export async function apply() {
Route('notfound', '*', NotFoundHandler);
}
global.Hydro.handler.notfound = apply;
+635
View File
@@ -0,0 +1,635 @@
import { isSafeInteger, flatten } from 'lodash';
import yaml from 'js-yaml';
import { ObjectID } from 'mongodb';
import {
NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
PermissionError,
} from '../error';
import {
Pdoc, User, Rdoc, PathComponent,
} from '../interface';
import paginate from '../lib/paginate';
import {
checkPid, isTitle, isContent, isPid,
} from '../lib/validator';
import * as file from '../model/file';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as domain from '../model/domain';
import * as user from '../model/user';
import * as solution from '../model/solution';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as bus from '../service/bus';
import {
Route, Connection, Handler, ConnectionHandler, Types, param, multipart,
} from '../service/server';
const parseCategory = (value: string) => flatten(value.split('+').map((e) => e.split(','))).map((e) => e.trim());
const parsePid = (value: string) => (isSafeInteger(value) ? parseInt(value, 10) : value);
class ProblemHandler extends Handler {
async __prepare() {
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
}
async cleanup() {
if (this.response.template === 'problem_main.html' && this.request.json) {
const {
path, page, pcount, ppcount, pdocs, psdict, category,
} = this.response.body;
this.response.body = {
title: this.renderTitle(category),
fragments: (await Promise.all([
this.renderHTML('partials/problem_list.html', {
page, ppcount, pcount, pdocs, psdict,
}),
this.renderHTML('partials/problem_stat.html', { pcount }),
this.renderHTML('partials/problem_lucky.html', { category }),
this.renderHTML('partials/path.html', { path }),
])).map((i) => ({ html: i })),
raw: {
path, page, pcount, ppcount, pdocs, psdict, category,
},
};
}
}
}
class ProblemMainHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true)
@param('q', Types.String, true)
async get(domainId: string, page = 1, q = '') {
this.response.template = 'problem_main.html';
const query: any = {};
let psdict = {};
const path: PathComponent[] = [
['Hydro', 'homepage'],
['problem_main', null],
];
if (q) {
q = q.toLowerCase();
const $regex = new RegExp(`\\A\\Q${q}\\E`, 'gmi');
query.title = { $regex };
path.push([q, null, null, true]);
}
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false;
const [pdocs, ppcount, pcount] = await paginate(
problem.getMulti(domainId, query).sort({ pid: 1 }),
page,
CONSTANT.PROBLEM_PER_PAGE,
);
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
psdict = await problem.getListStatus(
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
);
}
this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: q,
};
}
@param('pid', Types.UnsignedInt)
async postStar(domainId: string, pid: number) {
await problem.setStar(domainId, pid, this.user._id, true);
this.back({ star: true });
}
@param('pid', Types.UnsignedInt)
async postUnstar(domainId: string, pid: number) {
await problem.setStar(domainId, pid, this.user._id, false);
this.back({ star: false });
}
}
class ProblemCategoryHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true)
@param('category', Types.String, null, parseCategory)
async get(domainId: string, page = 1, category: string[]) {
this.response.template = 'problem_main.html';
const q: any = { $and: [] };
for (const name of category) {
q.$and.push({
$or: [
{ category: { $elemMatch: { $eq: name } } },
{ tag: { $elemMatch: { $eq: name } } },
],
});
}
let psdict = {};
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
const [pdocs, ppcount, pcount] = await paginate(
problem.getMulti(domainId, q).sort({ pid: 1 }),
page,
CONSTANT.PROBLEM_PER_PAGE,
);
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
psdict = await problem.getListStatus(
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
);
}
const path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[category, null, null, true],
];
this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'),
};
}
}
class ProblemRandomHandler extends ProblemHandler {
@param('category', Types.String, true, null, parseCategory)
async get(domainId: string, category: string[] = []) {
const q: any = category.length ? { $and: [] } : {};
for (const name of category) {
if (name) {
q.$and.push({
$or: [
{ category: { $elemMatch: { $eq: name } } },
{ tag: { $elemMatch: { $eq: name } } },
],
});
}
}
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
const pid = await problem.random(domainId, q);
if (!pid) throw new NoProblemError();
this.response.body = { pid };
this.response.redirect = this.url('problem_detail', { pid });
}
}
class ProblemDetailHandler extends ProblemHandler {
pdoc: Pdoc;
udoc: User;
@param('pid', Types.String, null, parsePid)
async _prepare(domainId: string, pid: number | string) {
this.response.template = 'problem_detail.html';
this.pdoc = await problem.get(domainId, pid, this.user._id);
if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid);
if (this.pdoc.hidden && this.pdoc.owner !== this.user._id) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
this.udoc = await user.getById(domainId, this.pdoc.owner);
this.response.body = {
pdoc: this.pdoc,
udoc: this.udoc,
title: this.pdoc.title,
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, null, true],
],
};
}
// eslint-disable-next-line
async get(...args: any[]) { }
@param('pid', Types.ObjectID)
@param('dest', Types.String)
@param('hidden', Types.Boolean)
async postCopy(domainId: string, pid: number, destDomainId: string, hidden = false) {
const udoc = await user.getById(destDomainId, this.user._id);
if (!udoc.hasPerm(PERM.PERM_CREATE_PROBLEM)) {
throw new PermissionError(PERM.PERM_CREATE_PROBLEM);
}
if (!this.pdoc.data) {
// Copy Without Data
pid = await problem.add(
destDomainId, this.pdoc.pid, this.pdoc.title,
this.pdoc.content, this.user._id, this.pdoc.tag,
this.pdoc.category, null, hidden,
);
} else if (this.pdoc.data instanceof ObjectID) {
// With data
pid = await problem.add(
destDomainId, this.pdoc.pid, this.pdoc.title,
this.pdoc.content, this.user._id, this.pdoc.tag,
this.pdoc.category, { domainId, pid }, hidden,
);
} else {
// TODO better message
// Data should only be copied once.
throw new BadRequestError('Cannot copy this problem.');
}
this.response.redirect = this.url('problem_settings', { domainId: destDomainId, pid });
}
}
class ProblemSubmitHandler extends ProblemDetailHandler {
@param('pid', Types.String, null, parsePid)
async get(domainId: string, pid: string | number) {
this.response.template = 'problem_submit.html';
const rdocs = await record
.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId)
.sort({ _id: -1 })
.limit(10)
.toArray();
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_submit', null],
],
pdoc: this.pdoc,
udoc: this.udoc,
rdocs,
title: this.pdoc.title,
};
}
@param('lang', Types.String)
@param('code', Types.String)
async post(domainId: string, lang: string, code: string) {
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true);
const [rdoc] = await Promise.all([
record.get(domainId, rid),
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
]);
bus.publish('record_change', { rdoc });
this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid });
}
}
class ProblemPretestHandler extends ProblemDetailHandler {
@param('lang', Types.String)
@param('code', Types.String)
@param('input', Types.String, true)
async post(domainId: string, lang: string, code: string, input: string = '') {
this.limitRate('add_record', 3600, 100);
// TODO parseConfig
const rid = await record.add(
domainId, this.pdoc.docId, this.user._id,
lang, code, true,
{
input,
time: 1000,
memory: 256,
},
);
const rdoc = await record.get(domainId, rid);
bus.publish('record_change', { rdoc });
this.response.body = { rid };
}
}
class ProblemPretestConnectionHandler extends ConnectionHandler {
pid: string;
domainId: string;
id: string;
@param('pid', Types.String)
async prepare(domainId: string, pid: string) {
const pdoc = await problem.get(domainId, pid);
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
this.pid = pdoc.docId.toString();
this.domainId = domainId;
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
if (
rdoc.uid !== this.user._id
|| rdoc.pid.toString() !== this.pid
|| rdoc.domainId !== this.domainId
) return;
rdoc.compilerTexts = [];
rdoc.judgeTexts = [];
// @ts-ignore
rdoc.testCases = rdoc.testCases.map((c) => ({
status: c.status,
}));
// TODO handle update
if (rdoc.contest) return;
this.send({ rdoc });
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
}
class ProblemStatisticsHandler extends ProblemDetailHandler {
async get(domainId: string) {
const udoc = await user.getById(domainId, this.pdoc.owner);
const path = [
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
['problem_statistics', null],
];
this.response.template = 'problem_statistics.html';
this.response.body = { pdoc: this.pdoc, udoc, path };
}
}
class ProblemManageHandler extends ProblemDetailHandler {
async prepare() {
if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF);
}
}
class ProblemSettingsHandler extends ProblemManageHandler {
@param('pid', Types.String)
async get(domainId: string, pid: string) {
this.response.template = 'problem_settings.html';
this.response.body.path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_settings', null],
];
if (this.response.body.config) {
this.response.body.config = yaml.safeDump(this.response.body.config);
}
}
@param('pid', Types.String, null, parsePid)
@param('yaml', Types.String)
async postConfig(domainId: string, pid: string | number, cfg: string) {
const pdoc = await problem.get(domainId, pid);
// TODO validate
const config = yaml.safeLoad(cfg);
await problem.edit(domainId, pdoc.docId, { config });
this.back();
}
@param('pid', Types.String, null, parsePid)
@param('hidden', Types.Boolean)
@param('category', Types.String, true, null, parseCategory)
@param('tag', Types.String, true, null, parseCategory)
async postSetting(
domainId: string, pid: string | number, hidden = false,
category: string[] = [], tag: string[] = [],
) {
const pdoc = await problem.get(domainId, pid);
await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
this.back();
}
}
class ProblemEditHandler extends ProblemManageHandler {
async get({ pid }) {
this.response.template = 'problem_edit.html';
this.response.body.path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid }, true],
['problem_edit', null],
];
}
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
async post(domainId: string, title: string, content: string) {
const pid = checkPid(this.request.body.pid);
const pdoc = await problem.get(domainId, this.request.params.pid);
await problem.edit(domainId, pdoc.docId, { title, content, pid });
this.response.redirect = this.url('problem_detail', { pid: this.request.params.pid });
}
}
class ProblemDataUploadHandler extends ProblemManageHandler {
async get() {
if (this.pdoc.data instanceof ObjectID) {
const f = await file.getMeta(this.pdoc.data);
this.response.body.md5 = f.md5;
} else if (this.pdoc.data) {
this.response.body.from = this.pdoc.data;
}
this.response.template = 'problem_upload.html';
}
@multipart(256 * 1024)
async post({ domainId }) {
if (!this.request.files.file) throw new ValidationError('file');
this.pdoc = await problem.setTestdata(
domainId, this.pdoc.docId, this.request.files.file.path,
);
if (this.pdoc.data instanceof ObjectID) {
const f = await file.getMeta(this.pdoc.data);
this.response.body.md5 = f.md5;
}
this.response.template = 'problem_upload.html';
}
}
class ProblemDataDownloadHandler extends ProblemDetailHandler {
async get({ pid }) {
if (!this.user.hasPriv(PRIV.PRIV_JUDGE)) {
if (this.user._id !== this.pdoc.owner) {
this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
} else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
}
if (this.pdoc.data instanceof ObjectID) {
this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`);
} else if (this.pdoc.data) {
if (!this.pdoc.data.host) {
this.response.redirect = this.url('problem_data', {
domainId: this.pdoc.data.domainId,
pid: this.pdoc.data.pid,
});
} else {
const [scheme, raw] = this.pdoc.data.host.split('//');
const args = JSON.parse(Buffer.from(raw, 'base64').toString());
if (scheme === 'hydro') {
const [secure, host, port, domainId, id] = args;
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/d/${domainId}/p/${id}/data`;
} else if (scheme === 'syzoj') {
const [secure, host, port, id] = args;
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/problem/${id}/testdata/download`;
} else {
throw new ProblemDataNotFoundError(pid);
}
}
} else throw new ProblemDataNotFoundError(pid);
}
}
class ProblemSolutionHandler extends ProblemDetailHandler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
this.response.template = 'problem_solution.html';
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const [psdocs, pcount, pscount] = await paginate(
solution.getMulti(domainId, this.pdoc.docId),
page,
CONSTANT.SOLUTION_PER_PAGE,
);
const uids = [this.pdoc.owner];
const docids = [];
for (const psdoc of psdocs) {
docids.push(psdoc.docId);
uids.push(psdoc.owner);
if (psdoc.reply.length) {
for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner);
}
}
const udict = await user.getList(domainId, uids);
const pssdict = solution.getListStatus(domainId, docids, this.user._id);
const path = [
['problem_main', 'problem_main'],
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
['problem_solution', null],
];
this.response.body = {
path, psdocs, page, pcount, pscount, udict, pssdict, pdoc: this.pdoc,
};
}
@param('content', Types.String, isContent)
async postSubmit(domainId: string, content: string) {
this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION);
await solution.add(domainId, this.pdoc.docId, this.user._id, content);
this.back();
}
@param('content', Types.String, isContent)
@param('psid', Types.ObjectID)
async postEditSolution(domainId: string, content: string, psid: ObjectID) {
let psdoc = await solution.get(domainId, psid);
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION);
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF);
psdoc = await solution.edit(domainId, psdoc.docId, content);
this.back({ psdoc });
}
@param('psid', Types.ObjectID)
async postDeleteSolution(domainId: string, psid: ObjectID) {
const psdoc = await solution.get(domainId, psid);
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION);
else this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF);
await solution.del(domainId, psdoc.docId);
this.back();
}
@param('psid', Types.ObjectID)
@param('content', Types.String, isContent)
async postReply(domainId: string, psid: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_PROBLEM_SOLUTION);
const psdoc = await solution.get(domainId, psid);
await solution.reply(domainId, psdoc.docId, this.user._id, content);
this.back();
}
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
@param('content', Types.String, isContent)
async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY);
}
await solution.editReply(domainId, psid, psrid, content);
this.back();
}
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
}
await solution.delReply(domainId, psid, psrid);
this.back();
}
@param('psid', Types.ObjectID)
async postUpvote(domainId: string, psid: ObjectID) {
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, 1);
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
}
@param('psid', Types.ObjectID)
async postDownvote(domainId: string, psid: ObjectID) {
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, -1);
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
}
}
class ProblemSolutionRawHandler extends ProblemDetailHandler {
@param('psid', Types.ObjectID)
async get(domainId: string, psid: ObjectID) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const psdoc = await solution.get(domainId, psid);
this.response.type = 'text/markdown';
this.response.body = psdoc.content;
}
}
class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
@param('psid', Types.ObjectID)
@param('psrid', Types.ObjectID)
async get(domainId: string, psid: ObjectID, psrid: ObjectID) {
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid);
this.response.type = 'text/markdown';
this.response.body = psrdoc.content;
}
}
class ProblemCreateHandler extends Handler {
async get() {
this.response.template = 'problem_edit.html';
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
this.response.body = {
path: [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
['problem_create', null],
],
page_name: 'problem_create',
};
}
@param('title', Types.String, isTitle)
@param('pid', Types.String, isPid, true)
@param('content', Types.String, isContent)
@param('hidden', Types.Boolean)
async post(domainId: string, title: string, pid: string, content: string, hidden = false) {
const docId = await problem.add(
domainId, pid, title, content,
this.user._id, [], [], null, hidden,
);
this.response.body = { pid: docId };
this.response.redirect = this.url('problem_settings', { pid: docId });
}
}
export async function apply() {
Route('problem_main', '/p', ProblemMainHandler);
Route('problem_category', '/p/category/:category', ProblemCategoryHandler);
Route('problem_random', '/problem/random', ProblemRandomHandler);
Route('problem_detail', '/p/:pid', ProblemDetailHandler);
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler);
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler);
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
Route('problem_upload', '/p/:pid/upload', ProblemDataUploadHandler);
Route('problem_data', '/p/:pid/data', ProblemDataDownloadHandler);
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler);
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
Route('problem_create', '/problem/create', ProblemCreateHandler);
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
}
global.Hydro.handler.problem = apply;
+190
View File
@@ -0,0 +1,190 @@
import { ObjectID } from 'mongodb';
import { PermissionError, RecordNotFoundError } from '../error';
import { PERM, CONSTANT } from '../model/builtin';
import * as problem from '../model/problem';
import * as record from '../model/record';
import * as contest from '../model/contest';
import * as user from '../model/user';
import paginate from '../lib/paginate';
import * as bus from '../service/bus';
import {
Route, Handler, Connection, ConnectionHandler, Types, param,
} from '../service/server';
import { Rdoc } from '../interface';
const RecordHandler = contest.ContestHandlerMixin(Handler);
class RecordListHandler extends RecordHandler {
@param('page', Types.UnsignedInt, true)
@param('pid', Types.String, true)
@param('tid', Types.ObjectID, true)
@param('uidOrName', Types.String, true)
async get(domainId: string, page = 1, pid?: string, tid?: ObjectID, uidOrName?: string) {
this.response.template = 'record_main.html';
if (tid) tid = new ObjectID(tid);
const q: any = { tid, hidden: false };
if (uidOrName) {
q.$or = [
{ unameLower: uidOrName.toLowerCase() },
{ _id: parseInt(uidOrName, 10) },
];
}
if (pid) q.pid = pid;
const [rdocs] = await paginate(
record.getMulti(domainId, q).sort('_id', -1),
page,
CONSTANT.RECORD_PER_PAGE,
);
const canViewProblemHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
const [udict, pdict] = await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false),
]);
const path = [
['Hydro', 'homepage'],
['record_main', null],
];
this.response.body = {
path,
page,
rdocs,
pdict,
udict,
filterPid: pid,
filterTid: tid,
filterUidOrName: uidOrName,
};
}
}
class RecordDetailHandler extends RecordHandler {
@param('rid', Types.ObjectID)
async get(domainId: string, rid: ObjectID) {
this.response.template = 'record_detail.html';
const rdoc = await record.get(domainId, rid);
if (!rdoc) throw new RecordNotFoundError(rid);
if (rdoc.contest) {
const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid);
}
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) {
rdoc.code = null;
}
const [pdoc, udoc] = await Promise.all([
problem.get(domainId, rdoc.pid),
user.getById(domainId, rdoc.uid),
]);
this.response.body = {
path: [
['Hydro', 'homepage'],
['record_detail', null],
],
udoc,
rdoc,
pdoc,
};
}
}
class RecordRejudgeHandler extends Handler {
@param('rid', Types.ObjectID)
async post(domainId: string, rid: ObjectID) {
this.checkPerm(PERM.PERM_REJUDGE);
const rdoc = await record.get(domainId, rid);
if (rdoc) {
await record.reset(domainId, rid, true);
await record.judge(domainId, rid);
}
this.back();
}
}
const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler);
class RecordMainConnectionHandler extends RecordConnectionHandler {
@param('tid', Types.ObjectID, true)
async prepare(domainId: string, tid: ObjectID) {
this.domainId = domainId;
if (tid) {
const tdoc = await contest.get(domainId, new ObjectID(tid), -1);
if (this.canShowRecord(tdoc)) this.tid = tid;
else {
this.close();
return;
}
}
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
}
async message(msg) {
if (msg.rids instanceof Array) {
const rids = msg.rids.map((id: string) => new ObjectID(id));
const rdocs = await record.getMulti(
this.domainId, { _id: { $in: rids } },
).toArray();
for (const rdoc of rdocs) {
this.onRecordChange({ value: { rdoc } });
}
}
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
if (rdoc.contest && rdoc.contest.tid.toString() !== this.tid) return;
// eslint-disable-next-line prefer-const
let [udoc, pdoc] = await Promise.all([
user.getById(this.domainId, rdoc.uid),
problem.get(this.domainId, rdoc.pid, null),
]);
if (pdoc && pdoc.hidden && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
else this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) });
}
}
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) {
@param('rid', Types.ObjectID)
async prepare(domainId: string, rid: ObjectID) {
const rdoc = await record.get(domainId, rid);
if (rdoc.contest) {
const tdoc = await contest.get(domainId, rdoc.contest.tid, -1);
if (!this.canShowRecord(tdoc)) {
this.close();
return;
}
}
this.rid = rid.toString();
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
this.onRecordChange({ value: { rdoc } });
}
async onRecordChange(data) {
const rdoc: Rdoc = data.value.rdoc;
const { $set, $push } = data.value;
if (rdoc._id.toString() !== this.rid) return;
if ($set) this.send({ $set, $push });
else {
this.send({
status_html: await this.renderHTML('record_detail_status.html', { rdoc }),
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }),
});
}
}
async cleanup() {
bus.unsubscribe(['record_change'], this.id);
}
}
export async function apply() {
Route('record_main', '/record', RecordListHandler);
Route('record_detail', '/record/:rid', RecordDetailHandler);
Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler);
Connection('record_conn', '/record-conn', RecordMainConnectionHandler);
Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler);
}
global.Hydro.handler.record = apply;
+272
View File
@@ -0,0 +1,272 @@
import assert from 'assert';
import { ObjectID } from 'mongodb';
import { ValidationError, ProblemNotFoundError } from '../error';
import { isTitle, isContent, isDescription } from '../lib/validator';
import paginate from '../lib/paginate';
import { PERM, PRIV, CONSTANT } from '../model/builtin';
import * as problem from '../model/problem';
import * as builtin from '../model/builtin';
import * as training from '../model/training';
import * as user from '../model/user';
import {
Route, Handler, Types, param,
} from '../service/server';
import { Tdoc } from '../interface';
async function _parseDagJson(domainId, dag) {
const parsed = [];
try {
dag = JSON.parse(dag);
assert(dag instanceof Array, 'dag must be an array');
const ids = new Set(dag.map((s) => s._id));
assert(dag.length === ids.size, '_id must be unique');
for (const node of dag) {
assert(node._id, 'each node should have a _id');
assert(node.title, 'each node shoule have a title');
assert(node.requireNids instanceof Array);
assert(node.pids instanceof Array);
assert(node.pids.length);
for (const nid of node.requireNids) {
assert(ids.has(nid), `required nid ${nid} not found`);
}
const tasks = [];
for (const i in node.pids) {
tasks.push(problem.get(domainId, node.pids[i]).then((pdoc) => {
if (!pdoc) throw new ProblemNotFoundError(domainId, node.pids[i]);
node.pids[i] = pdoc.docId;
}));
}
// FIXME no-await-in-loop
// eslint-disable-next-line no-await-in-loop
await Promise.all(tasks);
const newNode = {
_id: parseInt(node._id, 10),
title: node.title,
requireNids: Array.from(new Set(node.requireNids)),
pids: Array.from(new Set(node.pids)),
};
parsed.push(newNode);
}
} catch (e) {
throw new ValidationError('dag', [e.message]);
}
return parsed;
}
class TrainingMainHandler extends Handler {
@param('page', Types.UnsignedInt, true)
async get(domainId: string, page = 1) {
const [tdocs, tpcount] = await paginate(
training.getMulti(domainId),
page,
CONSTANT.TRAINING_PER_PAGE,
);
const tids: Set<ObjectID> = new Set();
for (const tdoc of tdocs) tids.add(tdoc.docId);
const tsdict = {};
let tdict = {};
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
const enrolledTids: Set<ObjectID> = new Set();
const tsdocs = await training.getMultiStatus(domainId, {
uid: this.user._id,
$or: [{ docId: { $in: Array.from(tids) } }, { enroll: 1 }],
}).toArray();
for (const tsdoc of tsdocs) {
tsdict[tsdoc.docId] = tsdoc;
enrolledTids.add(tsdoc.docId);
}
for (const tid of tids) enrolledTids.delete(tid);
if (enrolledTids.size) {
tdict = await training.getList(domainId, Array.from(enrolledTids));
}
}
for (const tdoc of tdocs) tdict[tdoc.docId] = tdoc;
const path = [
['Hydro', 'homepage'],
['training_main', null],
];
this.response.template = 'training_main.html';
this.response.body = {
tdocs, page, tpcount, tsdict, tdict, path,
};
}
}
class TrainingDetailHandler extends Handler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
const tdoc = await training.get(domainId, tid);
const pids = training.getPids(tdoc);
const [owner, pdict] = await Promise.all([
user.getById(domainId, tdoc.owner),
problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)),
]);
const psdict = await problem.getListStatus(domainId, this.user._id, pids);
const donePids = new Set();
const progPids = new Set();
for (const pid in psdict) {
const psdoc = psdict[pid];
if (psdoc.status) {
if (psdoc.status === builtin.STATUS.STATUS_ACCEPTED) {
donePids.add(parseInt(pid, 10));
} else progPids.add(parseInt(pid, 10));
}
}
const nsdict = {};
const ndict = {};
const doneNids = new Set();
for (const node of tdoc.dag) {
ndict[node._id] = node;
const totalCount = node.pids.length;
const doneCount = Set.union(new Set(node.pids), donePids).size;
const nsdoc = {
progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100,
isDone: training.isDone(node, doneNids, donePids),
isProgress: training.isProgress(node, doneNids, donePids, progPids),
isOpen: training.isOpen(node, doneNids, donePids, progPids),
isInvalid: training.isInvalid(node, doneNids),
};
if (nsdoc.isDone) doneNids.add(node._id);
nsdict[node._id] = nsdoc;
}
const tsdoc = await training.setStatus(domainId, tdoc.docId, this.user._id, {
doneNids: Array.from(doneNids),
donePids: Array.from(donePids),
done: doneNids.size === tdoc.dag.length,
});
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
[tdoc.title, null, null, true],
];
this.response.template = 'training_detail.html';
this.response.body = {
path, tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, owner,
};
}
@param('tid', Types.ObjectID)
async postEnroll(domainId: string, tid: ObjectID) {
this.checkPriv(PRIV.PRIV_USER_PROFILE);
const tdoc = await training.get(domainId, tid);
await training.enroll(domainId, tdoc.docId, this.user._id);
this.back();
}
}
class TrainingCreateHandler extends Handler {
async get() {
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
['training_create', null],
];
this.response.template = 'training_edit.html';
this.response.body = { page_name: 'training_create', path };
}
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('dag', Types.String, isContent)
@param('description', Types.String, isDescription)
async post(
domainId: string, title: string, content: string,
_dag: string, description: string,
) {
const dag = await _parseDagJson(domainId, _dag);
const pids = training.getPids({ dag });
assert(pids.length, new ValidationError('dag'));
const pdocs = await problem.getMulti(domainId, {
$or: [{ docId: { $in: pids } }, { pid: { $in: pids } }],
}).sort('_id', 1).toArray();
const existPids = pdocs.map((pdoc) => pdoc.docId);
const existPnames = pdocs.map((pdoc) => pdoc.pid);
if (pids.length !== existPids.length) {
for (const pid of pids) {
assert(
existPids.includes(pid) || existPnames.includes(pid),
new ProblemNotFoundError(pid),
);
}
}
for (const pdoc of pdocs) {
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
const tid = await training.add(domainId, title, content, this.user._id, dag, description);
this.response.body = { tid };
this.response.redirect = this.url('training_detail', { tid });
}
}
class TrainingEditHandler extends Handler {
tdoc: Tdoc;
@param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await training.get(domainId, tid);
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_TRAINING);
else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF);
}
async get({ tid }) {
const dag = JSON.stringify(this.tdoc.dag, null, 2);
const path = [
['Hydro', 'homepage'],
['training_main', 'training_main'],
[this.tdoc.title, 'training_detail', { tid }, true],
['training_edit', null],
];
this.response.template = 'training_edit.html';
this.response.body = {
tdoc: this.tdoc, dag, path, page_name: 'training_edit',
};
}
@param('tid', Types.ObjectID)
@param('title', Types.String, isTitle)
@param('content', Types.String, isContent)
@param('dag', Types.String, isContent)
@param('description', Types.String, isDescription)
async post(
domainId: string, tid: ObjectID,
title: string, content: string,
_dag: string, description: string,
) {
const dag = await _parseDagJson(domainId, _dag);
const pids = training.getPids({ dag });
assert(pids.length, new ValidationError('dag'));
const pdocs = await problem.getMulti(domainId, {
$or: [
{ docId: { $in: pids } },
{ pid: { $in: pids } },
],
}).sort('_id', 1).toArray();
const existPids = pdocs.map((pdoc) => pdoc.docId);
const existPnames = pdocs.map((pdoc) => pdoc.pid);
if (pids.length !== existPids.length) {
for (const pid in pids) {
assert(
existPids.includes(pid) || existPnames.includes(pid),
new ProblemNotFoundError(pid),
);
}
}
for (const pdoc of pdocs) {
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
await training.edit(domainId, tid, {
title, content, dag, description,
});
this.response.body = { tid };
this.response.redirect = this.url('training_detail', { tid });
}
}
export async function apply() {
Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING);
Route('training_create', '/training/create', TrainingCreateHandler, PERM.PERM_CREATE_TRAINING);
Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING);
Route('training_edit', '/training/:tid/edit', TrainingEditHandler);
}
global.Hydro.handler.training = apply;
+274
View File
@@ -0,0 +1,274 @@
import moment from 'moment-timezone';
import {
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, SystemError, BlacklistedError,
UserFacingError,
} from '../error';
import {
Route, Handler, Types, param,
} from '../service/server';
import * as user from '../model/user';
import * as token from '../model/token';
import * as record from '../model/record';
import * as problem from '../model/problem';
import * as task from '../model/task';
import * as system from '../model/system';
import { PERM, PRIV } from '../model/builtin';
import { isEmail, isPassword, isUname } from '../lib/validator';
import { sendMail } from '../lib/mail';
import * as misc from '../lib/misc';
import paginate from '../lib/paginate';
class UserLoginHandler extends Handler {
async get() {
this.response.template = 'user_login.html';
}
@param('uname', Types.String)
@param('password', Types.String)
@param('rememberme', Types.Boolean)
async post(domainId: string, uname: string, password: string, rememberme = false) {
const udoc = await user.getByUname(domainId, uname);
if (!udoc) throw new UserNotFoundError(uname);
udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
if (udoc.priv === PRIV.PRIV_NONE) throw new BlacklistedError(uname);
this.session.uid = udoc._id;
this.session.save = rememberme;
this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer;
}
}
class UserLogoutHandler extends Handler {
async get() {
this.response.template = 'user_logout.html';
}
async post() {
this.session.uid = 0;
}
}
class UserRegisterHandler extends Handler {
async get() {
this.response.template = 'user_register.html';
}
@param('mail', Types.String, isEmail)
async post(domainId: string, mail: string) {
if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail);
this.limitRate('send_mail', 3600, 30);
const t = await token.add(
token.TYPE_REGISTRATION,
await system.get('registration_token_expire_seconds'),
{ mail },
);
if (await system.get('smtp.user')) {
const m = await this.renderHTML('user_register_mail.html', {
path: `register/${t[0]}`,
url_prefix: await system.get('server.url'),
});
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
this.response.template = 'user_register_mail_sent.html';
} else {
this.response.redirect = this.url('user_register_with_code', { code: t[0] });
}
}
}
class UserRegisterWithCodeHandler extends Handler {
@param('code', Types.String)
async get(domainId: string, code: string) {
this.response.template = 'user_register_with_code.html';
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
this.response.body = { mail };
}
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
@param('uname', Types.String, isUname)
@param('code', Types.String)
async post(
domainId: string, password: string, verify: string,
uname: string, code: string,
) {
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (password !== verify) throw new VerifyPasswordError();
const uid = await user.create(mail, uname, password, undefined, this.request.ip);
await token.del(code, token.TYPE_REGISTRATION);
this.session.uid = uid;
this.response.redirect = this.url('homepage');
}
}
class UserLostPassHandler extends Handler {
async get() {
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
this.response.template = 'user_lostpass.html';
}
@param('mail', Types.String, isEmail)
async post(domainId: string, mail: string) {
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
const udoc = await user.getByEmail('system', mail);
if (!udoc) throw new UserNotFoundError(mail);
const [tid] = await token.add(
token.TYPE_LOSTPASS,
await system.get('lostpass_token_expire_seconds'),
{ uid: udoc._id },
);
const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname });
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
this.response.template = 'user_lostpass_mail_sent.html';
}
}
class UserLostPassWithCodeHandler extends Handler {
async get({ domainId, code }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
const udoc = await user.getById(domainId, tdoc.uid);
this.response.body = { uname: udoc.uname };
}
@param('code', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
async post(domainId: string, code: string, password: string, verifyPassword: string) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.setPassword(tdoc.uid, password);
await token.del(code, token.TYPE_LOSTPASS);
this.response.redirect = this.url('homepage');
}
}
class UserDetailHandler extends Handler {
@param('uid', Types.Int)
async get(domainId: string, uid: number) {
const isSelfProfile = this.user._id === uid;
const udoc = await user.getById(domainId, uid);
if (!udoc) throw new UserNotFoundError(uid);
const [sdoc, rdocs, [pdocs, pcount]] = await Promise.all([
token.getMostRecentSessionByUid(uid),
record.getByUid(domainId, uid, 30),
paginate(
problem.getMulti(domainId, { owner: this.user._id }),
1,
100,
),
]);
const pdict = await problem.getList(
domainId, rdocs.map((rdoc) => rdoc.pid),
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
);
// Remove sensitive data
if (!isSelfProfile && sdoc) {
sdoc.createIp = '';
sdoc.updateIp = '';
sdoc._id = '';
}
const path = [
['Hydro', 'homepage'],
['user_detail', 'user_detail', { uid }],
];
this.response.template = 'user_detail.html';
this.response.body = {
isSelfProfile, udoc, sdoc, rdocs, pdocs, pcount, pdict, path,
};
}
}
class UserDeleteHandler extends Handler {
async post({ password }) {
this.user.checkPassword(password);
const tid = await task.add({
executeAfter: moment().add(7, 'days').toDate(),
type: 'script',
id: 'deleteUser',
args: { uid: this.user._id },
});
await user.setById(this.user._id, { del: tid });
this.response.template = 'user_delete_pending.html';
}
}
class UserSearchHandler extends Handler {
@param('q', Types.String)
@param('exectMatch', Types.Boolean)
async get(domainId: string, q: string, exactMatch = false) {
let udoc = await user.getById(domainId, parseInt(q, 10));
const udocs = udoc ? [udoc] : [];
udoc = await user.getByUname(domainId, q);
if (udoc) udocs.push(udoc);
udoc = await user.getByEmail(domainId, q);
if (udoc) udocs.push(udoc);
if (!exactMatch) udocs.push(...await user.getPrefixList(q, 20));
for (const i in udocs) {
udocs[i].gravatar = misc.gravatar(udocs[i].gravatar || '');
}
this.response.body = udocs;
}
}
class OauthHandler extends Handler {
@param('type', Types.String)
async get(domainId: string, type: string) {
if (global.Hydro.lib[`oauth_${type}`]) await global.Hydro.lib[`oauth_${type}`].get.call(this);
}
}
class OauthCallbackHandler extends Handler {
async get(args: any) {
let r;
if (global.Hydro.lib[`oauth_${args.type}`]) r = await global.Hydro.lib[`oauth_${args.type}`].callback(args);
else throw new UserFacingError('Oauth type');
const udoc = await user.getByEmail('system', r.email);
if (udoc) {
this.session.uid = udoc._id;
} else {
this.checkPriv(PRIV.PRIV_REGISTER_USER);
let username = '';
r.uname = r.uname || [];
r.uname.push(String.random(16));
for (const uname of r.uname) {
// eslint-disable-next-line no-await-in-loop
const nudoc = await user.getByUname('system', uname);
if (!nudoc) {
username = uname;
break;
}
}
const uid = await user.create(
r.email, username, String.random(32),
undefined, this.request.ip,
);
const $set: any = {
oauth: args.type,
};
if (r.bio) $set.bio = r.bio;
if (r.viewLang) $set.viewLang = r.viewLang;
await user.setById(uid, $set);
this.session.uid = uid;
}
}
}
export async function apply() {
Route('user_login', '/login', UserLoginHandler);
Route('user_oauth', '/oauth/:type', OauthHandler);
Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
Route('user_logout', '/logout', UserLogoutHandler, PRIV.PRIV_USER_PROFILE);
Route('user_lostpass', '/lostpass', UserLostPassHandler);
Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
Route('user_search', '/user/search', UserSearchHandler, PRIV.PRIV_USER_PROFILE);
Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE);
Route('user_detail', '/user/:uid', UserDetailHandler);
}
global.Hydro.handler.user = apply;
+435
View File
@@ -0,0 +1,435 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ObjectID, GridFSBucket } from 'mongodb';
import fs from 'fs';
import { Dictionary, NumericDictionary } from 'lodash';
export interface Setting {
family: string,
key: string,
range: Array<[string, string]> | Dictionary<string>,
value: any,
type: string,
name: string,
desc: string,
flag: number,
}
export interface Udoc extends Dictionary<any> {
_id: number,
mail: string,
mailLower: string,
uname: string,
unameLower: string,
salt: string,
hash: string,
hashType: string,
priv: number,
regat: Date,
loginat: Date,
regip: string,
loginip: string,
}
export interface User extends Dictionary<any> {
udoc: () => any,
dudoc: () => any,
_id: number,
mail: string,
uname: string,
salt: () => string,
hash: () => string,
hashType: string,
priv: number,
regat: Date,
loginat: Date,
perm: () => bigint,
role: string,
regip: () => string,
loginip: () => string,
hasPerm: (perm: bigint) => boolean,
hasPriv: (priv: number) => boolean,
checkPassword: (password: string) => void,
}
export type Udict = NumericDictionary<User>;
export interface ProblemDataSource {
host?: string, // Empty for local
domainId: string,
pid: number,
}
// ObjectID for built-in files, ProblemDataSource for other source (RemoteJudge, etc.)
export type ProblemData = ObjectID | ProblemDataSource;
export interface TestCaseConfig {
input: string,
output: string,
time?: number,
memory?: number,
}
export enum LocalProblemType {
Default = 'default',
SubmitAnswer = 'submit_answer',
Interactive = 'interactive',
}
export enum RemoteProblemType {
RemoteJudge = 'remotejudge',
}
export enum RemoteServerType {
vj4 = 'vj4',
syzoj = 'syzoj',
}
export enum SubtaskType {
min = 'min',
max = 'max',
sum = 'sum',
}
export interface SubtaskConfig {
time?: number,
memory?: number,
score?: number,
type?: SubtaskType,
cases?: TestCaseConfig[],
}
export interface LocalProblemConfig {
type?: LocalProblemType,
score?: number,
time?: number,
memory?: number,
checker_type?: string,
checker?: string,
interactor?: string,
user_extra_files?: string[],
judge_extra_files?: string[],
cases?: TestCaseConfig[],
subtasks?: SubtaskConfig[],
}
export interface RemoteProblemConfig {
type?: RemoteProblemType,
server_type?: RemoteServerType,
url?: string,
// TODO handle username&password storage
pid?: string,
}
export type ProblemConfig = LocalProblemConfig | RemoteProblemConfig;
export interface Pdoc {
_id: ObjectID,
domainId: string,
docType: number,
docId: number,
pid: string,
owner: number,
title: string,
content: string,
nSubmit: number,
nAccept: number,
tag: string[],
category: string[],
difficulty: number,
data?: ProblemData,
hidden: boolean,
config: ProblemConfig,
acMsg?: string,
}
export type Pdict = NumericDictionary<Pdoc>;
export interface ProblemStatusDoc {
_id: ObjectID,
docId: number,
docType: number,
domainId: string,
uid: number,
rid?: ObjectID,
status?: number,
star?: boolean,
}
export interface TestCase {
time: number,
memory: number,
status: number,
message: string,
}
export interface PretestConfig {
time?: number,
memory?: number,
input: string,
}
export interface ContestInfo {
type: number,
tid: ObjectID,
}
export interface Rdoc {
_id: ObjectID,
domainId: string,
pid: number
uid: number,
lang: string,
code: string,
score: number,
memory: number,
time: number,
judgeTexts: string[],
compilerTexts: string[],
testCases: TestCase[],
rejudged: boolean,
judger: string,
judgeAt: Date,
status: number,
hidden: boolean,
config?: PretestConfig,
contest?: ContestInfo,
}
export interface ScoreboardNode {
type: string,
value: string,
raw?: any,
}
export type PenaltyRules = Dictionary<number>;
export interface Tdoc {
_id: ObjectID,
domainId: string,
docId: ObjectID,
docType: number,
owner: number,
beginAt: Date,
endAt: Date,
attend: number,
title: string,
content: string,
rule: string,
pids: number[],
// For homework
penaltySince?: Date,
penaltyRules?: PenaltyRules,
// For training
dag: any
}
export interface DomainDoc extends Dictionary<any> {
_id: string,
owner: number,
roles: Dictionary<string>,
gravatar: string,
bulletin: string,
pidCounter: number,
}
// Message
export interface Mdoc {
_id: ObjectID,
from: number,
to: number,
content: string,
flag: number,
}
// Userfile
export interface Ufdoc {
_id: ObjectID,
secret: string,
md5: string,
owner: number,
size: number,
}
// Blacklist
export interface Bdoc {
_id: string, // ip
expireAt: Date,
}
// Discussion
export interface Ddoc {
_id: ObjectID,
docType: number,
docId: ObjectID,
parentType: number,
parentId: ObjectID | number,
owner: number,
title: string,
content: string,
ip: string,
pin: boolean,
highlight: boolean,
}
// Discussion reply
export interface Drdoc {
_id: ObjectID,
docType: number,
docId: ObjectID,
parentType: number,
parentId: ObjectID,
owner: number,
ip: string,
content: string,
reply: Drrdoc[],
}
// Discussion Tail Reply
export interface Drrdoc {
_id: ObjectID,
owner: number,
content: string,
ip: string,
}
export interface TokenDoc {
_id: string,
tokenType: number,
createAt: Date,
updateAt: Date,
expireAt: Date,
[key: string]: any,
}
export interface ContestStat extends Dictionary<any> {
detail: any,
}
export interface ContestRule {
TEXT: string,
check: (args: any) => any,
statusSort: any,
showScoreboard: (tdoc: Tdoc, now: Date) => boolean,
showRecord: (tdoc: Tdoc, now: Date) => boolean,
stat: (tdoc: Tdoc, journal: any[]) => ContestStat,
scoreboard: (
isExport: boolean, _: (s: string) => string,
tdoc: Tdoc, rankedTsdocs: any[] | Generator<any>, udict: Udict, pdict: Pdict
) => ScoreboardNode[][],
rank: (tsdocs: any[]) => any[] | Generator<any>,
}
export type ContestRules = Dictionary<ContestRule>;
export type ProblemImporter = (url: string, handler: any) =>
Promise<[Pdoc, fs.ReadStream?]> | [Pdoc, fs.ReadStream?];
export interface Script {
run: (args: any, report: Function) => any,
description: string,
validate: any,
}
export interface JudgeResultBody {
domainId: string,
rid: ObjectID,
judger?: number,
progress?: number
case?: {
status: number,
time: number,
memory: number,
message?: string,
},
status?: number,
score?: number,
time?: number,
memory?: number,
message?: string,
compilerText?: string,
// For pretest
stdout?: string,
stderr?: string,
}
export type PathComponent = [string, string, any?, boolean?];
declare global {
namespace NodeJS {
interface Global {
Hydro: {
model: {
blacklist: typeof import('./model/blacklist'),
builtin: typeof import('./model/builtin'),
contest: typeof import('./model/contest'),
discussion: typeof import('./model/discussion'),
document: typeof import('./model/document'),
domain: typeof import('./model/domain'),
file: typeof import('./model/file'),
message: typeof import('./model/message'),
opcount: typeof import('./model/opcount'),
problem: typeof import('./model/problem'),
record: typeof import('./model/record'),
setting: typeof import('./model/setting'),
solution: typeof import('./model/solution'),
system: typeof import('./model/system'),
task: typeof import('./model/task'),
token: typeof import('./model/token'),
training: typeof import('./model/training'),
user: typeof import('./model/user'),
[key: string]: any,
},
handler: { [key: string]: Function },
script: { [key: string]: Script },
service: {
bus: typeof import('./service/bus'),
db: typeof import('./service/db'),
gridfs: GridFSBucket,
monitor: typeof import('./service/monitor'),
server: typeof import('./service/server'),
[key: string]: any,
},
lib: {
download: typeof import('./lib/download').default,
'hash.hydro': typeof import('./lib/hash.hydro').default,
i18n: typeof import('./lib/i18n').default,
'import.syzoj': typeof import('./lib/import.syzoj').syzoj,
jwt: typeof import('./lib/jwt'),
logger: typeof import('./lib/logger').default,
mail: typeof import('./lib/mail'),
md5: typeof import('./lib/md5').default,
misc: typeof import('./lib/misc'),
nav: typeof import('./lib/nav').default,
paginate: typeof import('./lib/paginate').default,
rank: typeof import('./lib/rank').default,
rating: typeof import('./lib/rating').default,
testdataConfig: typeof import('./lib/testdataConfig'),
sha1: typeof import('./lib/sha1').default,
sysinfo: typeof import('./lib/sysinfo'),
'testdata.convert.ini': typeof import('./lib/testdata.convert.ini').default,
useragent: typeof import('./lib/useragent'),
validator: typeof import('./lib/validator'),
[key: string]: any
},
stat: any,
ui: {
manifest: Dict<string>,
nav: any,
template: { [key: string]: string },
},
error: typeof import('./error'),
locales: Dict<Dict<string>>,
postInit: Array<() => Promise<any>>,
},
onDestory: Array<() => void | Promise<void>>,
addons: string[],
}
}
}
declare module 'cluster' {
let isFirstWorker: boolean;
}
+19
View File
@@ -0,0 +1,19 @@
import superagent from 'superagent';
import fs from 'fs';
async function _download(url: string, path: string, retry: number) {
const w = fs.createWriteStream(path);
superagent.get(url).retry(retry).pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', reject);
});
return path;
}
export default function download(url: string, path?: string, retry = 3) {
if (path) return _download(url, path, retry);
return superagent.get(url).retry(retry);
}
global.Hydro.lib.download = download;
+7
View File
@@ -0,0 +1,7 @@
import crypto from 'crypto';
export default function hash(password: string, salt: string) {
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256').toString('hex').substr(0, 64);
}
global.Hydro.lib['hash.hydro'] = hash;
+56
View File
@@ -0,0 +1,56 @@
const locales = {};
declare global {
interface String {
format: (...args: Array<any>) => string;
rawformat: (object: any) => string;
translate: (...languages: string[]) => string;
}
}
String.prototype.format = function formatStr(...args) {
let result = this;
if (args.length > 0) {
if (args.length === 1 && typeof (args[0]) === 'object') {
for (const key in args) {
if (args[key] !== undefined) {
const reg = new RegExp(`(\\{${key}\\})`, 'g');
result = result.replace(reg, args[key]);
}
}
} else {
for (let i = 0; i < args.length; i++) {
if (args[i] !== undefined) {
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
result = result.replace(reg, args[i]);
}
}
}
}
return result;
};
String.prototype.rawformat = function rawFormat(object) {
const res = this.split('{@}');
return [res[0], object, res[1]].join();
};
String.prototype.translate = function translate(...languages: string[]) {
if (languages.length === 0) languages = ['zh_CN'];
for (const language of languages) {
if (locales[language]) {
if (locales[language][this] !== undefined) return locales[language][this];
}
}
return this;
};
export default function load(data) {
for (const i in data) {
if (!locales[i]) locales[i] = data[i];
else locales[i] = Object.assign(locales[i], data[i]);
}
}
global.Hydro.locales = locales;
global.Hydro.lib.i18n = load;
+71
View File
@@ -0,0 +1,71 @@
import assert from 'assert';
import superagent from 'superagent';
import download from './download';
import { ValidationError, RemoteOnlineJudgeError } from '../error';
const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9.]+)\/problem\/([0-9]+)\/?/i;
export async function syzoj(url: string, handler: any) {
assert(url.match(RE_SYZOJ), new ValidationError('url'));
if (!url.endsWith('/')) url += '/';
const [, host, pid] = RE_SYZOJ.exec(url);
const res = await superagent.get(`${url}export`);
assert(res.status === 200, new RemoteOnlineJudgeError('Cannot connect to target server'));
assert(res.body.success, new RemoteOnlineJudgeError((res.body.error || {}).message));
const p = res.body.obj;
const content = [];
if (p.description) {
content.push(
handler.translate('problem.import.problem_description'),
p.description,
);
}
if (p.input_format) {
content.push(
handler.translate('problem.import.input_format'),
p.input_format,
);
}
if (p.output_format) {
content.push(
handler.translate('problem.import.output_format'),
p.output_format,
);
}
if (p.hint) {
content.push(
handler.translate('problem.import.hint'),
p.hint,
);
}
if (p.limit_and_hint) {
content.push(
handler.translate('problem.import.limit_and_hint'),
p.limit_and_hint,
);
}
if (p.have_additional_file) {
content.push(
handler.translate('problem.import.additional_file'),
`${url}download/additional_file`,
);
}
const pdoc = {
title: p.title,
content: content.join(' \n'),
owner: handler.user._id,
from: url,
pid: `${host}_${pid}`,
tags: p.tags,
config: {
time: p.time_limit,
memory: p.memory_limit * 1024,
filename: p.file_io_input_name,
type: p.type,
},
};
const r = download(`${url}testdata/download`);
return [pdoc, r];
}
global.Hydro.lib['import.syzoj'] = syzoj;
+35
View File
@@ -0,0 +1,35 @@
function decodeBase64WithUriEncoding(encodedText: string) {
return Buffer.from(encodedText, 'base64').toString('utf8');
}
function unescapedString(escapedString: string) {
escapedString += new Array(5 - (escapedString.length % 4)).join('=');
return escapedString.replace(/-/g, '+').replace(/_/g, '/');
}
function decodeJWT(idToken: string) {
const token = idToken.split('.');
if (token.length !== 3) {
throw new Error('Invalid idToken');
}
try {
const headerSegment = JSON.parse(decodeBase64WithUriEncoding(token[0]));
const payloadSegment = JSON.parse(decodeBase64WithUriEncoding(token[1]));
const signature = unescapedString(token[2]);
return {
dataToSign: [token[0], token[1]].join('.'),
header: headerSegment,
payload: payloadSegment,
signature,
};
} catch (e) {
throw new Error('Invalid payload');
}
}
export function decode(idToken: string) {
const decodedJWT = decodeJWT(idToken);
return decodedJWT.payload;
}
global.Hydro.lib.jwt = { decode };
+41
View File
@@ -0,0 +1,41 @@
import serialize from 'serialize-javascript';
function wrap(func: Function, type: string) {
return (...args: any) => {
const time = new Date();
func(...args);
if (global.Hydro.model.message) {
try {
global.Hydro.model.message.send(1, 1, serialize({ time, type, args }), 0);
} catch (e) {
func(e.message);
}
}
};
}
class Logger {
log: (...args: any[]) => void;
error: (...args: any[]) => void;
info: (...args: any[]) => void;
warn: (...args: any[]) => void;
debug: (...args: any[]) => void;
constructor() {
this.log = wrap(console.log, 'log');
this.error = wrap(console.error, 'error');
this.info = wrap(console.info, 'info');
this.warn = wrap(console.warn, 'warn');
this.debug = wrap(console.debug, 'debug');
}
}
const exp = new Logger();
global.Hydro.lib.logger = exp;
export default exp;
+39
View File
@@ -0,0 +1,39 @@
import nodemailer from 'nodemailer';
import { SendMailError } from '../error';
export async function sendMail(to: string, subject: string, text: string, html: string) {
let t: unknown;
try {
const { system } = global.Hydro.model;
const [host, port, secure, user, pass, from] = await Promise.all([
system.get('smtp.host'),
system.get('smtp.port'),
system.get('smtp.secure'),
system.get('smtp.user'),
system.get('smtp.pass'),
system.get('smtp.from'),
]);
const transporter = nodemailer.createTransport({
host, port, secure, auth: { user, pass },
});
t = await new Promise((resolve, reject) => {
transporter.sendMail({
from,
to,
subject,
text,
html,
}, (err, info) => {
if (err) reject(err);
else resolve(info);
});
});
} catch (e) {
throw new SendMailError(to);
}
return t;
}
global.Hydro.lib.mail = {
sendMail,
};
+137
View File
@@ -0,0 +1,137 @@
// From https://github.com/pvorb/node-md5/blob/master/md5.js
// BSD 3-Clause "New" or "Revised" License
/* eslint-disable max-len */
function safeAdd(x, y) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt));
const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t);
const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t);
const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t);
const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t);
function binlMD5(x, len) {
x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len;
let i;
let olda;
let oldb;
let oldc;
let oldd;
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (i = 0; i < x.length; i += 16) {
olda = a;
oldb = b;
oldc = c;
oldd = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936);
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5hh(d, a, b, c, x[i], 11, -358537222);
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844);
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, olda);
b = safeAdd(b, oldb);
c = safeAdd(c, oldc);
d = safeAdd(d, oldd);
}
return [a, b, c, d];
}
function binl2rstr(input) {
let i;
let output = '';
const length32 = input.length * 32;
for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); }
return output;
}
function rstr2binl(input) {
let i;
const output = [];
output[(input.length >> 2) - 1] = undefined;
for (i = 0; i < output.length; i += 1) { output[i] = 0; }
const length8 = input.length * 8;
for (i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
return output;
}
const rstrMD5 = (s) => binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
function rstr2hex(input) {
const hexTab = '0123456789abcdef';
let output = '';
let x;
let i;
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i);
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
}
return output;
}
export default function md5(s: string) {
return rstr2hex(rstrMD5(unescape(encodeURIComponent(s))));
}
global.Hydro.lib.md5 = md5;
+78
View File
@@ -0,0 +1,78 @@
import AnsiUp from 'ansi_up';
import { ObjectID } from 'mongodb';
import md5 from './md5';
const AU = new AnsiUp();
export function ansiToHtml(str: string) {
return AU.ansi_to_html(str);
}
export function gravatar(email: string, s = 32) {
return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${s}`;
}
export function datetimeSpan(dt: Date | ObjectID, relative = true, format = '%Y-%m-%d %H:%M:%S') {
if (!dt) return 'DATETIME_SPAN_ERROR';
if (dt instanceof ObjectID) dt = new Date(dt.generationTime * 1000);
else if (typeof dt === 'number' || typeof dt === 'string') dt = new Date(dt);
return '<span class="time{0}" data-timestamp="{1}">{2}</span>'.format(
relative ? ' relative' : '',
dt.getTime() / 1000,
dt.format(format),
);
}
export function* paginate(page: number, numPages: number) {
const radius = 2;
let first: number;
let last: number;
if (page > 1) {
if (page > 2) yield ['first', 1];
yield ['previous', page - 1];
}
if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, numPages)];
else if (page >= numPages - radius) {
[first, last] = [Math.max(1, numPages - radius * 2), numPages];
} else {
[first, last] = [page - radius, page + radius];
}
if (first > 1) yield ['ellipsis', 0];
for (let page0 = first; page0 < last + 1; page0++) {
if (page0 !== page) yield ['page', page0];
else yield ['current', page];
}
if (last < numPages) yield ['ellipsis', 0];
if (page < numPages) {
yield ['next', page + 1];
if (page < numPages - 1) yield ['last', numPages];
}
}
export function size(s: number, base = 1) {
s *= base;
const unit = 1024;
const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
for (const unitName of unitNames) {
if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName);
s /= unit;
}
return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`;
}
function _digit2(number: number) {
return number < 10 ? `0${number}` : number.toString();
}
export function formatSeconds(_seconds = '0') {
const seconds = parseInt(_seconds, 10);
return '{0}:{1}:{2}'.format(
_digit2(Math.floor(seconds / 3600)),
_digit2(Math.floor((seconds % 3600) / 60)),
_digit2(seconds % 60),
);
}
global.Hydro.lib.misc = {
gravatar, datetimeSpan, paginate, size, formatSeconds, ansiToHtml,
};
+57
View File
@@ -0,0 +1,57 @@
import { isNull } from 'lodash';
import { PERM, PRIV } from '../model/builtin';
global.Hydro.ui.nav = [];
const trueChecker = () => true;
const Checker = (perm: bigint, priv: number, checker: Function = trueChecker) => (handler) => (
checker(handler)
&& (perm ? handler.user.hasPerm(perm) : true)
&& (priv ? handler.user.hasPriv(priv) : true)
);
const Item = (
name: string, args: any, prefix: string,
...permPrivChecker: Array<number | bigint | Function>
) => {
let _priv: number;
let _perm: bigint;
let checker: Function = trueChecker;
for (const item of permPrivChecker) {
if (item instanceof Object && !isNull(item)) {
if (item instanceof Array) {
if (typeof item[0] === 'number') {
// @ts-ignore
_priv = item;
} else if (typeof item[0] === 'bigint') {
// @ts-ignore
_perm = item;
}
} else if (typeof item.call !== 'undefined') {
checker = item;
}
} else if (typeof item === 'number') {
_priv = item;
} else if (typeof item === 'bigint') {
_perm = item;
}
}
checker = Checker(_perm, _priv, checker);
global.Hydro.ui.nav.push({
name, args: args || {}, prefix, checker,
});
};
Item('homepage', null, 'homepage');
Item('problem_main', null, 'problem', PERM.PERM_VIEW_PROBLEM);
Item('training_main', null, 'training', PERM.PERM_VIEW_TRAINING);
Item('homework_main', null, 'homework', PERM.PERM_VIEW_HOMEWORK);
Item('discussion_main', null, 'discussion', PERM.PERM_VIEW_DISCUSSION);
Item('contest_main', null, 'contest', PERM.PERM_VIEW_CONTEST);
Item('record_main', null, 'record');
Item('ranking', null, 'ranking', PERM.PERM_VIEW_RANKING);
Item('domain_dashboard', null, 'domain', PERM.PERM_EDIT_DOMAIN);
Item('manage_dashboard', null, 'manage', PRIV.PRIV_EDIT_SYSTEM);
global.Hydro.lib.nav = Item;
export default Item;
+16
View File
@@ -0,0 +1,16 @@
import { Cursor } from 'mongodb';
import { ValidationError } from '../error';
export default async function paginate(
cursor: Cursor, page: number, pageSize: number,
): Promise<[any[]/* docs */, number/* numPages */, number/* count */]> {
if (page <= 0) throw new ValidationError('page');
const [count, pageDocs] = await Promise.all([
cursor.count(),
cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
]);
const numPages = Math.floor((count + pageSize - 1) / pageSize);
return [pageDocs, numPages, count];
}
global.Hydro.lib.paginate = paginate;
+14
View File
@@ -0,0 +1,14 @@
function* ranked<T>(diter: T[], equ = (a: T, b: T) => a === b): Generator<[number, T], void> {
let last = null;
let r = 0;
let count = 0;
for (const doc of diter) {
count++;
if (count === 1 || !equ(last, doc)) r = count;
last = doc;
yield [r, doc];
}
}
global.Hydro.lib.rank = ranked;
export default ranked;
+127
View File
@@ -0,0 +1,127 @@
// Based on https://github.com/QAQrz/Codeforces-Rating-System/blob/master/rating.py
class User {
uid: number;
rank: number;
old: number;
seed?: number;
delta?: number;
new?: number;
constructor(rank: number, old: number, uid = 0) {
this.rank = rank;
this.old = old;
this.seed = 1;
this.uid = uid;
}
}
class RatingCalculator {
users: User[];
constructor(users: User[]) {
this.users = [];
for (const user of users) {
this.users.push(new User(user.rank, user.old, user.uid));
}
}
// eslint-disable-next-line class-methods-use-this
calP(a: User, b: User) {
return 1 / (1 + 10 ** ((b.old - a.old) / 400));
}
getExSeed(rating: number, ownUser: User) {
const exUser = new User(0.0, rating);
let result = 1;
for (const user of this.users) {
if (user !== ownUser) result += this.calP(user, exUser);
}
return result;
}
calRating(rank: number, user: User) {
let left = 1;
let right = 8000;
while (right - left > 1) {
const mid = Math.floor((left + right) / 2);
if (this.getExSeed(mid, user) < rank) right = mid;
else left = mid;
}
return left;
}
calculate() {
// Calculate seed
for (let i = 0; i < this.users.length; i++) {
this.users[i].seed = 1;
for (let j = 0; j < this.users.length; j++) {
if (i !== j) {
this.users[i].seed += this.calP(this.users[j], this.users[i]);
}
}
}
// Calculate initial delta and sum_delta
let sumDelta = 0;
for (const user of this.users) {
user.delta = (this.calRating(Math.sqrt(user.rank * user.seed), user) - user.old) / 2;
sumDelta += user.delta;
}
// Calculate first inc
let inc = Math.floor(-sumDelta / this.users.length) - 1;
for (const user of this.users) user.delta += inc;
// Calculate second inc
this.users = this.users.sort((a, b) => b.old - a.old);
const s = Math.min(
this.users.length,
Math.floor(4 * Math.round(Math.sqrt(this.users.length))),
);
let sumS = 0;
for (let i = 0; i < s; i++) {
sumS += this.users[i].delta;
}
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
// Calculate new rating
for (const user of this.users) {
user.delta += inc;
user.new = user.old + user.delta;
}
this.users = this.users.sort((a, b) => a.rank - b.rank);
return this.users;
}
}
interface RatingInputUser {
old: number,
uid: number,
rank: number,
}
interface RatingOutputUser {
new: number,
uid: number,
}
export default function calculate(users: RatingInputUser[]): RatingOutputUser[] {
let last_idx = 0;
let last_rank = 1;
for (let i = 1; i < users.length; i++) {
if (users[i].rank > last_rank) {
for (let j = last_idx; j < i; j++) users[j].rank = i;
last_idx = i;
last_rank = users[i].rank;
}
}
for (let i = last_idx; i < users.length; i++) {
users[i].rank = users.length;
}
const calculator = new RatingCalculator(users);
return calculator.calculate() as RatingOutputUser[];
}
global.Hydro.lib.rating = calculate;
+10
View File
@@ -0,0 +1,10 @@
import crypto from 'crypto';
function sha1(content: string) {
const hash = crypto.createHash('sha1');
hash.update(content);
return hash.digest('hex');
}
global.Hydro.lib.sha1 = sha1;
export default sha1;
+62
View File
@@ -0,0 +1,62 @@
import systeminformation from 'systeminformation';
import { size } from './misc';
const cache = {
mid: '', cpu: '', osinfo: '', flags: '',
};
export async function get() {
const [Cpu, Memory, OsInfo, CurrentLoad, CpuFlags, CpuTemp, Battery] = await Promise.all([
systeminformation.cpu(),
systeminformation.mem(),
systeminformation.osInfo(),
systeminformation.currentLoad(),
systeminformation.cpuFlags(),
systeminformation.cpuTemperature(),
systeminformation.battery(),
]);
const cpu = `${Cpu.manufacturer} ${Cpu.brand}`;
const memory = `${size(Memory.active)}/${size(Memory.total)}`;
const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`;
const load = `${CurrentLoad.avgload}`;
const flags = CpuFlags;
let battery;
if (!Battery.hasbattery) battery = 'No battery';
else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`;
const mid = OsInfo.serial;
cache.cpu = cpu;
cache.osinfo = osinfo;
cache.flags = flags;
cache.mid = mid;
return {
mid, cpu, memory, osinfo, load, flags, CpuTemp, battery,
};
}
export async function update(): Promise<[string, any, any]> {
const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([
systeminformation.mem(),
systeminformation.currentLoad(),
systeminformation.cpuTemperature(),
systeminformation.battery(),
]);
const {
mid, cpu, osinfo, flags,
} = cache;
const memory = `${size(Memory.active)}/${size(Memory.total)}`;
const load = `${CurrentLoad.avgload}`;
let battery;
if (!Battery.hasbattery) battery = 'No battery';
else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`;
return [
mid,
{
memory, load, battery, CpuTemp,
},
{
mid, cpu, memory, osinfo, load, flags, battery, CpuTemp,
},
];
}
global.Hydro.lib.sysinfo = { get, update };
@@ -0,0 +1,32 @@
import yaml from 'js-yaml';
export default function convert(ini: string) {
const f = ini.split('\n');
const count = parseInt(f[0], 10);
const res = { cases: [] };
for (let i = 1; i <= count; i++) {
const [input, output, time, score, memory] = f[i].split('|');
const cur = {
input, output, score: parseInt(score, 10), time: `${time}s`, memory: '128m',
};
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
res.cases.push(cur);
}
if (res.cases.length > 0) {
const { time, score, memory } = res.cases[0];
for (let i = 1; i < res.cases.length; i++) {
if (res.cases[i].time !== time
|| res.cases[i].score !== score
|| res.cases[i].memory !== memory) {
return yaml.safeDump(res);
}
}
}
return yaml.safeDump({
time: res.cases[0].time,
score: res.cases[0].score,
memory: res.cases[0].memory,
});
}
global.Hydro.lib['testdata.convert.ini'] = convert;
@@ -0,0 +1,60 @@
import Zip from 'adm-zip';
import { safeLoad } from 'js-yaml';
import { LocalProblemConfig } from '../interface';
export async function readConfig(filePath: string) {
const data = new Zip(filePath);
const entries = data.getEntries();
for (const entry of entries) {
if (entry.name.toLowerCase() === 'config.yaml') {
return entry.getData().toString();
}
if (entry.name.toLowerCase() === 'config.ini') {
const ini = entry.getData().toString();
const conv = global.Hydro.lib['testdata.convert.ini'];
return conv(ini);
}
}
return '';
}
interface ParseResult {
count: number,
memoryMax: number,
memoryMin: number,
timeMax: number,
timeMin: number,
}
export async function parseConfig(config: string | LocalProblemConfig) {
const result: ParseResult = {
count: 0,
memoryMin: Number.MAX_SAFE_INTEGER,
memoryMax: 0,
timeMin: Number.MAX_SAFE_INTEGER,
timeMax: 0,
};
let cfg: LocalProblemConfig = {};
if (typeof config === 'string') {
// TODO should validate here?
cfg = safeLoad(config) as LocalProblemConfig;
} else cfg = config;
if (cfg.cases) {
for (const c of cfg.cases) {
result.memoryMax = Math.max(result.memoryMax, c.memory);
result.memoryMin = Math.min(result.memoryMin, c.memory);
result.timeMax = Math.max(result.timeMax, c.time);
result.timeMin = Math.min(result.timeMin, c.time);
result.count++;
}
} else if (cfg.subtasks) {
for (const subtask of cfg.subtasks) {
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
result.timeMax = Math.max(result.timeMax, subtask.time);
result.timeMin = Math.min(result.timeMin, subtask.time);
}
}
}
global.Hydro.lib.testdataConfig = { readConfig, parseConfig };
+11
View File
@@ -0,0 +1,11 @@
import * as browser from 'detect-browser';
export function parse(str: string) {
return browser.parseUserAgent(str);
}
export function icon(str: string) {
return str.split(' ')[0].toLowerCase();
}
global.Hydro.lib.useragent = { parse, icon };
+157
View File
@@ -0,0 +1,157 @@
/* eslint-disable no-empty */
import { ValidationError } from '../error';
const RE_UID = /^-?\d+$/i;
const RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i;
const RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i;
const RE_ROLE = /^[_0-9A-Za-z]{1,256}$/i;
const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
// eslint-disable-next-line consistent-return
function _validate(scheme: any, arg: any, base: string): void {
if (!(scheme instanceof Object)) return _validate({ $type: scheme }, arg, base);
if (scheme.$validate) {
const result = scheme.$validate(arg);
if (result === false) throw new ValidationError(base);
}
if (scheme.$group) _validate(scheme.$group, arg, base);
if (scheme.$or) {
if (scheme.$or instanceof Array) {
for (const s of scheme.$or) {
let success = false;
try {
_validate(s, arg, base);
success = true;
} catch { }
// TODO beautify output
if (!success) throw new ValidationError(scheme.$or);
}
} else {
for (const skey in scheme.$or) {
let success = false;
try {
_validate(scheme.$or[skey], arg[skey], `${base}.${skey}`);
success = true;
} catch { }
// TODO beautify output
if (!success) throw new ValidationError(base, scheme.$or);
}
}
}
if (scheme.$and) for (const s of scheme.$and) _validate(s, arg, base);
if (scheme.$type) {
if (typeof scheme.$type === 'string') {
if (!scheme.$type.endsWith('?')) {
if (!typeof arg === scheme.$type) {
throw new ValidationError(base);
}
} else if (arg && !scheme.$type.includes(typeof arg)) {
throw new ValidationError(base);
}
} else if (!(arg instanceof scheme.$type)) {
throw new ValidationError(base);
}
}
if (scheme.$eq && scheme.$eq !== arg) throw new ValidationError(base);
if (scheme.$not && scheme.$not === arg) throw new ValidationError(base);
if (scheme.$in && !(scheme.$in.includes(arg))) throw new ValidationError(base);
if (scheme.$nin && scheme.$nin.includes(arg)) throw new ValidationError(base);
for (const key in scheme) {
if (key[0] !== '$') {
if (arg instanceof Object) {
_validate(scheme[key], arg[key], `${base}.${key}`);
} else {
throw new ValidationError(base);
}
}
}
}
export function validate(scheme: any, arg: any) {
return _validate(scheme, arg, '');
}
export const isTitle = (s) => s && s.length < 64;
export const checkTitle = (s) => { if (!(s && s.length < 64)) throw new ValidationError('title'); else return s; };
export const isUid = (s) => RE_UID.test(s);
export const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; };
export const isUname = (s) => RE_UNAME.test(s);
export const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; };
export const isRole = (s) => RE_ROLE.test(s);
export const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; };
export const isPassword = (s) => s.length >= 5;
export const checkPassword = (s) => { if (!(s && s.length >= 5)) throw new ValidationError('password'); else return s; };
export const isEmail = (s) => RE_MAIL.test(s);
export const checkEmail = (s) => { if (!RE_MAIL.test(s)) throw new ValidationError('mail'); else return s; };
export const isContent = (s: any) => s && s.length < 65536;
export const checkContent = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('content'); else return s; };
export const isName = (s) => s && s.length < 256;
export const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
export const isPid = (s) => RE_PID.test(s.toString());
export const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
export const isIntro = () => true;
export const checkIntro = (s) => { if (!isIntro()) throw new ValidationError('intro'); else return s; };
export const isDescription = (s: any) => s && s.length < 65536;
export const checkDescription = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('description'); else return s; };
global.Hydro.lib.validator = {
validate,
isTitle,
checkTitle,
isUid,
checkUid,
isUname,
checkUname,
isRole,
checkRole,
isPassword,
checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isName,
checkName,
isPid,
checkPid,
isIntro,
checkIntro,
isDescription,
checkDescription,
};
/*
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
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_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_lang(i):
return i in export const ant.language.LANG_TEXTS
def check_lang(i):
if not is_lang(i):
raise error.ValidationError('lang')
*/
+241
View File
@@ -0,0 +1,241 @@
/* eslint-disable no-continue */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-eval */
import './interface';
import os from 'os';
import path from 'path';
import cluster from 'cluster';
import fs from 'fs-extra';
import { argv } from 'yargs';
import AdmZip from 'adm-zip';
export * from './interface';
if (!global.Hydro) {
global.Hydro = {
stat: { reqCount: 0 },
handler: {},
// @ts-ignore
service: {},
// @ts-ignore
model: {},
script: {},
// @ts-ignore
lib: {},
ui: {
manifest: {},
nav: [],
template: {},
},
// @ts-ignore
error: {},
locales: {},
postInit: [],
};
global.onDestory = [];
global.addons = [];
}
if (argv.debug) {
console.log(process.argv);
console.log(argv);
}
async function terminate() {
try {
for (const task of global.onDestory) await task();
} catch (e) {
process.exit(1);
}
process.exit(0);
}
async function fork(args: string[] = []) {
const _args = process.argv.slice(2);
_args.push(...args, `--addons=${Buffer.from(JSON.stringify(global.addons)).toString('base64')}`);
cluster.setupMaster({ args: _args });
return cluster.fork();
}
interface EntryConfig {
entry: string,
newProcess?: boolean,
}
async function entry(config: EntryConfig) {
if (config.entry) {
if (config.newProcess) {
const p = await fork([`--entry=${config.entry}`]);
await new Promise((resolve, reject) => {
p.on('exit', (code, signal) => {
if (code === 0) resolve();
else reject(signal);
});
p.on('error', (err: Error) => {
p.kill();
reject(err);
});
});
} else {
const loader = require(`./entry/${config.entry}`);
return await loader.load(entry, global.addons);
}
}
return null;
}
export type Entry = typeof entry;
async function stopWorker() {
cluster.disconnect();
}
async function startWorker(cnt: number) {
await fork(['--firstWorker']);
for (let i = 1; i < cnt; i++) await fork();
}
async function executeCommand(input: string) {
try {
const t = eval(input.toString().trim());
if (t instanceof Promise) console.log(await t);
else console.log(t);
} catch (e) {
console.warn(e);
}
}
async function messageHandler(worker: cluster.Worker, msg: any) {
if (!msg) msg = worker;
if (msg.event) {
if (msg.event === 'bus') {
if (cluster.isMaster) {
for (const i in cluster.workers) {
cluster.workers[i].send(msg);
}
} else {
global.Hydro.service.bus.publish(msg.eventName, msg.payload, false);
}
} else if (msg.event === 'stat') {
global.Hydro.stat.reqCount += msg.count;
} else if (msg.event === 'restart') {
console.log('Restarting');
await stopWorker();
console.log('Worker stopped');
await startWorker(msg.count);
} else if (msg.event === 'run') {
await executeCommand(msg.command);
}
}
}
const moduleTemp = path.resolve(os.tmpdir(), 'hydro', 'module');
const publicTemp = path.resolve(os.tmpdir(), 'hydro', 'public');
const tmp = path.resolve(os.tmpdir(), 'hydro', '__');
export function addon(addonPath: string) {
let modulePath = path.resolve(process.cwd(), addonPath);
if (!(fs.existsSync(addonPath) && fs.statSync(addonPath).isFile())) {
try {
// Is a npm package
const packagejson = require.resolve(`${addonPath}/package.json`);
modulePath = path.dirname(packagejson);
const publicPath = path.resolve(modulePath, 'public');
if (fs.existsSync(publicPath)) fs.copySync(publicPath, publicTemp);
global.addons.push(modulePath);
} catch (e) {
throw new Error(`Addon not found: ${addonPath}`);
}
} else if (modulePath.endsWith('.hydro')) {
try {
// Is *.hydro module
const t = modulePath.split(path.sep);
const name = t[t.length - 1].split('.')[0];
const zip = new AdmZip(modulePath);
const targetPath = path.resolve(moduleTemp, name);
zip.extractAllTo(targetPath, true);
const content = fs.readdirSync(targetPath);
const ipath = path.join(targetPath, content[0]);
if (content.length === 1 && fs.statSync(ipath).isDirectory()) {
fs.moveSync(ipath, tmp);
fs.rmdirSync(targetPath);
fs.moveSync(tmp, targetPath);
}
const publicPath = path.resolve(targetPath, 'public');
if (fs.existsSync(publicPath)) fs.copySync(publicPath, publicTemp);
global.addons.push(targetPath);
} catch (e) {
console.error('Addon load fail: ', e);
throw e;
}
} else throw new Error(`Addon not found: ${addonPath}`);
}
process.on('unhandledRejection', (e) => console.error(e));
process.on('SIGINT', terminate);
export async function load() {
addon(path.resolve(__dirname, '..'));
Error.stackTraceLimit = 50;
process.on('message', messageHandler);
cluster.on('message', messageHandler);
if (cluster.isMaster || argv.startAsMaster) {
console.log(`Master ${process.pid} Starting`);
process.stdin.setEncoding('utf8');
process.stdin.on('data', (buf) => {
const input = buf.toString();
if (input[0] === '@') {
for (const i in cluster.workers) {
cluster.workers[i].send({ event: 'run', command: input.substr(1, input.length - 1) });
break;
}
} else {
executeCommand(input);
}
});
const cnt = await entry({ entry: 'master' });
console.log('Master started');
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} ${worker.id} exit: ${code} ${signal}`);
});
cluster.on('disconnect', (worker) => {
console.log(`Worker ${worker.process.pid} ${worker.id} disconnected`);
});
cluster.on('listening', (worker, address) => {
console.log(`Worker ${worker.process.pid} ${worker.id} listening at `, address);
});
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} ${worker.id} is online`);
});
await startWorker(cnt);
} else {
global.addons = JSON.parse(Buffer.from(argv.addons as string, 'base64').toString());
console.log(global.addons);
if (argv.entry) {
console.log(`Worker ${process.pid} Starting as ${argv.entry}`);
await entry({ entry: argv.entry as string });
console.log(`Worker ${process.pid} Started as ${argv.entry}`);
} else {
if (argv.firstWorker) cluster.isFirstWorker = true;
else cluster.isFirstWorker = false;
console.log(`Worker ${process.pid} Starting`);
await entry({ entry: 'worker' });
console.log(`Worker ${process.pid} Started`);
}
}
if (global.gc) global.gc();
}
export async function loadCli() {
await entry({ entry: 'cli' });
return terminate();
}
if (argv.pandora || !module.parent) {
const func = argv._[0] === 'cli' ? load : loadCli;
func().catch((e) => {
console.error(e);
process.exit(1);
});
}
+39
View File
@@ -0,0 +1,39 @@
import * as db from '../service/db';
import { Bdoc } from '../interface';
const coll = db.collection('blacklist');
export async function add(ip: string): Promise<Bdoc> {
/**
* Add a ip into blacklist.
* @param {string} ip
* @returns {Promise<Bdoc>}
*/
const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
const res = await coll.findOneAndUpdate(
{ _id: ip },
{ $set: { expireAt } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export function get(ip: string): Promise<Bdoc | null> {
/**
* Get a ip, return null if not.
* @param {string} ip
* @returns {Promise<Bdoc>}
*/
return coll.findOne({ _id: ip });
}
export function del(ip: string) {
return coll.deleteOne({ _id: ip });
}
function ensureIndexes() {
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.blacklist = { add, get, del };
+643
View File
@@ -0,0 +1,643 @@
// @ts-nocheck
import { Udoc } from '../interface';
/* Why nocheck?
* BitInt requires at least ES2020, but we can't use it as
* NodeJS doesn't support some of this new features.
* e.g.: object?.property
*/
export const PERM = {
PERM_NONE: 0n,
// Domain Settings
PERM_VIEW: 1n << 0n,
PERM_EDIT_DOMAIN: 1n << 1n,
PERM_MOD_BADGE: 1n << 2n,
// Problem and Record
PERM_CREATE_PROBLEM: 1n << 4n,
PERM_EDIT_PROBLEM: 1n << 5n,
PERM_EDIT_PROBLEM_SELF: 1n << 6n,
PERM_VIEW_PROBLEM: 1n << 7n,
PERM_VIEW_PROBLEM_HIDDEN: 1n << 8n,
PERM_SUBMIT_PROBLEM: 1n << 9n,
PERM_READ_PROBLEM_DATA: 1n << 10n,
PERM_READ_PROBLEM_DATA_SELF: 1n << 11n,
PERM_READ_RECORD_CODE: 1n << 12n,
PERM_REJUDGE_PROBLEM: 1n << 13n,
PERM_REJUDGE: 1n << 14n,
// Problem Solution
PERM_VIEW_PROBLEM_SOLUTION: 1n << 15n,
PERM_CREATE_PROBLEM_SOLUTION: 1n << 16n,
PERM_VOTE_PROBLEM_SOLUTION: 1n << 17n,
PERM_EDIT_PROBLEM_SOLUTION: 1n << 18n,
PERM_EDIT_PROBLEM_SOLUTION_SELF: 1n << 19n,
PERM_DELETE_PROBLEM_SOLUTION: 1n << 20n,
PERM_DELETE_PROBLEM_SOLUTION_SELF: 1n << 21n,
PERM_REPLY_PROBLEM_SOLUTION: 1n << 22n,
PERM_EDIT_PROBLEM_SOLUTION_REPLY: 1n << 23n,
PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 1n << 24n,
PERM_DELETE_PROBLEM_SOLUTION_REPLY: 1n << 25n,
PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 1n << 26n,
// Discussion
PERM_VIEW_DISCUSSION: 1n << 27n,
PERM_CREATE_DISCUSSION: 1n << 28n,
PERM_HIGHLIGHT_DISCUSSION: 1n << 29n,
PERM_PIN_DISCUSSION: 1n << 59n,
PERM_EDIT_DISCUSSION: 1n << 30n,
PERM_EDIT_DISCUSSION_SELF: 1n << 31n,
PERM_DELETE_DISCUSSION: 1n << 32n,
PERM_DELETE_DISCUSSION_SELF: 1n << 33n,
PERM_REPLY_DISCUSSION: 1n << 34n,
PERM_EDIT_DISCUSSION_REPLY: 1n << 35n,
PERM_EDIT_DISCUSSION_REPLY_SELF: 1n << 36n,
PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 37n,
PERM_DELETE_DISCUSSION_REPLY: 1n << 38n,
PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n,
PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n,
// Contest
PERM_VIEW_CONTEST: 1n << 41n,
PERM_VIEW_CONTEST_SCOREBOARD: 1n << 42n,
PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 1n << 43n,
PERM_CREATE_CONTEST: 1n << 44n,
PERM_ATTEND_CONTEST: 1n << 45n,
PERM_EDIT_CONTEST: 1n << 50n,
PERM_EDIT_CONTEST_SELF: 1n << 51n,
// Homework
PERM_VIEW_HOMEWORK: 1n << 52n,
PERM_VIEW_HOMEWORK_SCOREBOARD: 1n << 53n,
PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 1n << 54n,
PERM_CREATE_HOMEWORK: 1n << 55n,
PERM_ATTEND_HOMEWORK: 1n << 56n,
PERM_EDIT_HOMEWORK: 1n << 57n,
PERM_EDIT_HOMEWORK_SELF: 1n << 58n,
// Training
PERM_VIEW_TRAINING: 1n << 46n,
PERM_CREATE_TRAINING: 1n << 47n,
PERM_EDIT_TRAINING: 1n << 48n,
PERM_EDIT_TRAINING_SELF: 1n << 49n,
// Ranking
PERM_VIEW_RANKING: 1n << 50n,
// Placeholder
PERM_ALL: -1n,
PERM_BASIC: 0n,
PERM_DEFAULT: 0n,
PERM_ADMIN: -1n,
};
export const Permission = (family: string, key: BigInt, desc: string) => ({ family, key, desc });
export const PERMS = [
Permission('perm_general', PERM.PERM_VIEW, 'View this domain'),
Permission('perm_general', PERM.PERM_EDIT_DOMAIN, 'Edit domain settings'),
Permission('perm_general', PERM.PERM_MOD_BADGE, 'Show MOD badge'),
Permission('perm_problem', PERM.PERM_CREATE_PROBLEM, 'Create problems'),
Permission('perm_problem', PERM.PERM_EDIT_PROBLEM, 'Edit problems'),
Permission('perm_problem', PERM.PERM_EDIT_PROBLEM_SELF, 'Edit own problems'),
Permission('perm_problem', PERM.PERM_VIEW_PROBLEM, 'View problems'),
Permission('perm_problem', PERM.PERM_VIEW_PROBLEM_HIDDEN, 'View hidden problems'),
Permission('perm_problem', PERM.PERM_SUBMIT_PROBLEM, 'Submit problem'),
Permission('perm_problem', PERM.PERM_READ_PROBLEM_DATA, 'Read data of problem'),
Permission('perm_problem', PERM.PERM_READ_PROBLEM_DATA_SELF, 'Read data of own problems'),
Permission('perm_record', PERM.PERM_READ_RECORD_CODE, 'Read record codes'),
Permission('perm_record', PERM.PERM_REJUDGE_PROBLEM, 'Rejudge problems'),
Permission('perm_record', PERM.PERM_REJUDGE, 'Rejudge records'),
Permission('perm_problem_solution', PERM.PERM_VIEW_PROBLEM_SOLUTION, 'View problem solutions'),
Permission('perm_problem_solution', PERM.PERM_CREATE_PROBLEM_SOLUTION, 'Create problem solutions'),
Permission('perm_problem_solution', PERM.PERM_VOTE_PROBLEM_SOLUTION, 'Vote problem solutions'),
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION, 'Edit problem solutions'),
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF, 'Edit own problem solutions'),
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION, 'Delete problem solutions'),
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF, 'Delete own problem solutions'),
Permission('perm_problem_solution', PERM.PERM_REPLY_PROBLEM_SOLUTION, 'Reply problem solutions'),
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY, 'Edit problem solution replies'),
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF, 'Edit own problem solution replies'),
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY, 'Delete problem solution replies'),
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF, 'Delete own problem solution replies'),
Permission('perm_discussion', PERM.PERM_VIEW_DISCUSSION, 'View discussions'),
Permission('perm_discussion', PERM.PERM_CREATE_DISCUSSION, 'Create discussions'),
Permission('perm_discussion', PERM.PERM_HIGHLIGHT_DISCUSSION, 'Highlight discussions'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION, 'Edit discussions'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_SELF, 'Edit own discussions'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'),
Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY, 'Edit discussion replies'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF, 'Edit own discussion replies'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION, 'Edit discussion replies of own discussion'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY, 'Delete discussion replies'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF, 'Delete own discussion replies'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION, 'Delete discussion replies of own discussion'),
Permission('perm_contest', PERM.PERM_VIEW_CONTEST, 'View contests'),
Permission('perm_contest', PERM.PERM_VIEW_CONTEST_SCOREBOARD, 'View contest scoreboard'),
Permission('perm_contest', PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, 'View hidden contest submission status and scoreboard'),
Permission('perm_contest', PERM.PERM_CREATE_CONTEST, 'Create contests'),
Permission('perm_contest', PERM.PERM_ATTEND_CONTEST, 'Attend contests'),
Permission('perm_contest', PERM.PERM_EDIT_CONTEST, 'Edit any contests'),
Permission('perm_contest', PERM.PERM_EDIT_CONTEST_SELF, 'Edit own contests'),
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK, 'View homework'),
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK_SCOREBOARD, 'View homework scoreboard'),
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD, 'View hidden homework submission status and scoreboard'),
Permission('perm_homework', PERM.PERM_CREATE_HOMEWORK, 'Create homework'),
Permission('perm_homework', PERM.PERM_ATTEND_HOMEWORK, 'Claim homework'),
Permission('perm_homework', PERM.PERM_EDIT_HOMEWORK, 'Edit any homework'),
Permission('perm_homework', PERM.PERM_EDIT_HOMEWORK_SELF, 'Edit own homework'),
Permission('perm_training', PERM.PERM_VIEW_TRAINING, 'View training plans'),
Permission('perm_training', PERM.PERM_CREATE_TRAINING, 'Create training plans'),
Permission('perm_training', PERM.PERM_EDIT_TRAINING, 'Edit training plans'),
Permission('perm_training', PERM.PERM_EDIT_TRAINING_SELF, 'Edit own training plans'),
Permission('perm_ranking', PERM.PERM_VIEW_RANKING, 'View ranking'),
];
export const PERMS_BY_FAMILY = {};
for (const p of PERMS) {
if (!PERMS_BY_FAMILY[p.family]) PERMS_BY_FAMILY[p.family] = [p];
else PERMS_BY_FAMILY[p.family].push(p);
}
PERM.PERM_BASIC = PERM.PERM_VIEW
| PERM.PERM_VIEW_PROBLEM
| PERM.PERM_VIEW_PROBLEM_SOLUTION
| PERM.PERM_VIEW_DISCUSSION
| PERM.PERM_VIEW_CONTEST
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
| PERM.PERM_VIEW_HOMEWORK
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
| PERM.PERM_VIEW_TRAINING
| PERM.PERM_VIEW_RANKING;
PERM.PERM_DEFAULT = PERM.PERM_VIEW
| PERM.PERM_VIEW_PROBLEM
| PERM.PERM_EDIT_PROBLEM_SELF
| PERM.PERM_SUBMIT_PROBLEM
| PERM.PERM_READ_PROBLEM_DATA_SELF
| PERM.PERM_VIEW_PROBLEM_SOLUTION
| PERM.PERM_CREATE_PROBLEM_SOLUTION
| PERM.PERM_VOTE_PROBLEM_SOLUTION
| PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF
| PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF
| PERM.PERM_REPLY_PROBLEM_SOLUTION
| PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF
| PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF
| PERM.PERM_VIEW_DISCUSSION
| PERM.PERM_CREATE_DISCUSSION
| PERM.PERM_EDIT_DISCUSSION_SELF
| PERM.PERM_REPLY_DISCUSSION
| PERM.PERM_EDIT_DISCUSSION_REPLY_SELF
// PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION
| PERM.PERM_VIEW_CONTEST
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
| PERM.PERM_ATTEND_CONTEST
| PERM.PERM_EDIT_CONTEST_SELF
| PERM.PERM_VIEW_HOMEWORK
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
| PERM.PERM_ATTEND_HOMEWORK
| PERM.PERM_EDIT_HOMEWORK_SELF
| PERM.PERM_VIEW_TRAINING
| PERM.PERM_CREATE_TRAINING
| PERM.PERM_EDIT_TRAINING_SELF
| PERM.PERM_VIEW_RANKING
| PERM.PERM_SUBMIT_PROBLEM
| PERM.PERM_CREATE_PROBLEM_SOLUTION
| PERM.PERM_VOTE_PROBLEM_SOLUTION
| PERM.PERM_REPLY_PROBLEM_SOLUTION
| PERM.PERM_CREATE_DISCUSSION
| PERM.PERM_REPLY_DISCUSSION
| PERM.PERM_ATTEND_CONTEST
| PERM.PERM_CREATE_TRAINING
| PERM.PERM_ATTEND_HOMEWORK
| PERM.PERM_VIEW_RANKING;
PERM.PERM_ADMIN = PERM.PERM_ALL;
export const PRIV = {
PRIV_NONE: 0,
PRIV_EDIT_SYSTEM: 1 << 0, // renamed from PRIV_SET_PRIV
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, // deprecated
PRIV_READ_PRETEST_DATA_SELF: 1 << 6, // deprecated
PRIV_READ_RECORD_CODE: 1 << 7,
PRIV_VIEW_HIDDEN_RECORD: 1 << 8,
PRIV_JUDGE: 1 << 9, // (renamed)
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,
PRIV_ALL: -1,
PRIV_DEFAULT: 0,
};
PRIV.PRIV_DEFAULT = PRIV.PRIV_USER_PROFILE
+ PRIV.PRIV_REGISTER_USER
+ PRIV.PRIV_READ_PRETEST_DATA_SELF
+ PRIV.PRIV_CREATE_DOMAIN
+ PRIV.PRIV_CREATE_FILE
+ PRIV.PRIV_DELETE_FILE_SELF;
// [10, 1] means that people whose rank is less than 1% will get Level 10
export const LEVELS = [
[10, 1],
[9, 2],
[8, 10],
[7, 20],
[6, 30],
[5, 40],
[4, 70],
[3, 90],
[2, 95],
[1, 100],
];
export const BUILTIN_USERS: Udoc[] = [
{
_id: 0,
mail: 'Guest@hydro.local',
mailLower: 'guest@hydro.local',
uname: 'Guest',
unameLower: 'guest',
salt: '',
hash: '',
hashType: 'hydro',
regat: new Date(0),
regip: '127.0.0.1',
loginat: new Date(0),
loginip: '127.0.0.1',
priv: PRIV.PRIV_REGISTER_USER,
},
{
_id: 1,
mail: 'Hydro@hydro.local',
mailLower: 'hydro@hydro.local',
uname: 'Hydro',
unameLower: 'hydro',
salt: '',
hash: '',
hashType: 'hydro',
regat: new Date(0),
regip: '127.0.0.1',
loginat: new Date(0),
loginip: '127.0.0.1',
priv: PRIV.PRIV_USER_PROFILE,
},
];
export const BUILTIN_ROLES = {
guest: PERM.PERM_BASIC,
default: PERM.PERM_DEFAULT,
admin: PERM.PERM_ADMIN,
};
export const DEFAULT_NODES = {
探索: [
{ pic: 'qa', name: '问答' },
{ pic: 'share', name: '分享' },
{ pic: 'solution', name: '题解' },
],
Vijos: [
{ pic: 'hydro', name: 'Hydro' },
{ pic: null, name: '团队' },
{ pic: null, name: '月赛' },
{ pic: 'advice', name: '建议' },
],
数据结构: [
{ pic: null, name: '散列表' },
{ pic: null, name: '搜索树' },
{ pic: null, name: '栈和队列' },
{ pic: null, name: '图' },
{ pic: null, name: '堆' },
],
算法: [
{ pic: null, name: '数论' },
{ pic: null, name: '几何' },
{ pic: null, name: '图论' },
{ pic: null, name: '网络流' },
{ pic: null, name: '动态规划' },
{ pic: null, name: '背包' },
{ pic: null, name: '排序' },
{ pic: null, name: '搜索' },
{ pic: null, name: '并查集' },
{ pic: null, name: '贪心' },
{ pic: null, name: '博弈论' },
],
在线题库: [
{ pic: null, name: 'CodeForces' },
{ pic: null, name: 'TopCoder' },
{ pic: null, name: 'POJ' },
{ pic: null, name: 'BZOJ' },
{ pic: null, name: 'USACO' },
{ pic: null, name: 'RQNOJ' },
{ pic: null, name: 'UOJ' },
{ pic: null, name: 'LOJ' },
{ pic: null, name: '洛谷' },
],
泛: [
{ pic: null, name: '数学' },
{ pic: null, name: '编程' },
{ pic: null, name: '数据库' },
{ pic: null, name: 'C' },
{ pic: null, name: 'C++' },
{ pic: null, name: 'Pascal' },
{ pic: null, name: 'Java' },
{ pic: null, name: 'PHP' },
{ pic: null, name: 'Python' },
{ pic: null, name: '游戏' },
{ pic: null, name: '保送' },
{ pic: null, name: 'ACM' },
],
};
export const CATEGORIES = {
动态规划: [
'LCS',
'LIS',
'背包',
'单调性DP',
'环形DP',
'树形DP',
'状态压缩DP',
],
搜索: [
'枚举',
'搜索与剪枝',
'启发式搜索',
'DLX',
'双向搜索',
'折半搜索',
'记忆化搜索',
'模拟退火',
],
计算几何: [
'半平面交',
'凸包',
'几何图形的交与并',
'旋转卡壳',
'点定位',
'坐标变换',
'离散化与扫描',
'反演',
'Voronoi图',
'平面图的对偶图',
'三角剖分',
'梯形剖分',
'几何知识',
],
贪心: [],
树结构: [
'最近公共祖先',
'生成树',
'DFS序列',
'树上倍增',
'树的分治',
'树链剖分',
'Link-Cut-Tree',
],
图结构: [
'平面图',
'二分图',
'二分图匹配',
'最短路',
'差分约束',
'拓扑排序',
'网络流',
'强连通分量',
'割点割边',
'欧拉回路',
'2-SAT',
],
数论: [
'素数判定',
'欧几里得算法',
'不定方程',
'数位统计',
'解线性同余方程',
'baby-step-giant-step',
'Pell方程',
'大整数质因数分解',
'勾股方程',
'积性函数',
'Fibonacci数列',
],
模拟: [],
数据结构: [
'栈',
'队列',
'链表',
'单调队列',
'并查集',
'堆',
'平衡树',
'线段树',
'树状数组',
'树套树',
'四分树',
'划分树',
'归并树',
'k-d树',
'块状链表',
'Hashing',
'函数式编程',
],
博弈论: [],
字符串: [
'KMP',
'后缀数据结构',
'Trie树',
'AC自动机',
'Manacher',
'表达式处理',
'最小表示法',
],
组合数学: [
'生成函数',
'容斥原理',
'康托展开',
'Catalan数列',
'Stirling数',
'差分',
'Polya定理',
],
线性代数: [
'矩阵乘法',
'高斯消元',
'线性规划',
],
高精度: [
'FFT',
],
递推: [],
概率论: [
'随机化',
],
NPC: [],
其他: [
'二分查找',
'三分查找',
'双指针扫描',
'分治',
'分块',
'RMQ',
'快速幂',
'数学',
'排序',
'构造',
],
};
export const LANG_TEXTS = {
c: 'C',
cc: 'C++',
cs: 'C#',
pas: 'Pascal',
java: 'Java',
py: 'Python',
py3: 'Python 3',
php: 'PHP',
rs: 'Rust',
hs: 'Haskell',
js: 'JavaScript',
go: 'Go',
rb: 'Ruby',
};
export const LANG_HIGHLIGHT_ID = {
c: 'c',
cc: 'cpp',
cs: 'csharp',
pas: 'pascal',
java: 'java',
py: 'python',
py3: 'python',
php: 'php',
rs: 'rust',
hs: 'haskell',
js: 'javascript',
go: 'go',
rb: 'ruby',
};
export const STATUS = {
STATUS_WAITING: 0,
STATUS_ACCEPTED: 1,
STATUS_WRONG_ANSWER: 2,
STATUS_TIME_LIMIT_EXCEEDED: 3,
STATUS_MEMORY_LIMIT_EXCEEDED: 4,
STATUS_OUTPUT_LIMIT_EXCEEDED: 5,
STATUS_RUNTIME_ERROR: 6,
STATUS_COMPILE_ERROR: 7,
STATUS_SYSTEM_ERROR: 8,
STATUS_CANCELED: 9,
STATUS_ETC: 10,
STATUS_JUDGING: 20,
STATUS_COMPILING: 21,
STATUS_FETCHED: 22,
STATUS_IGNORED: 30,
};
export const STATUS_TEXTS = {
[STATUS.STATUS_WAITING]: 'Waiting',
[STATUS.STATUS_ACCEPTED]: 'Accepted',
[STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer',
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded',
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded',
[STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded',
[STATUS.STATUS_RUNTIME_ERROR]: 'Runtime Error',
[STATUS.STATUS_COMPILE_ERROR]: 'Compile Error',
[STATUS.STATUS_SYSTEM_ERROR]: 'System Error',
[STATUS.STATUS_CANCELED]: 'Cancelled',
[STATUS.STATUS_ETC]: 'Unknown Error',
[STATUS.STATUS_JUDGING]: 'Running',
[STATUS.STATUS_COMPILING]: 'Compiling',
[STATUS.STATUS_FETCHED]: 'Fetched',
[STATUS.STATUS_IGNORED]: 'Ignored',
};
export const STATUS_CODES = {
0: 'pending',
1: 'pass',
2: 'fail',
3: 'fail',
4: 'fail',
5: 'fail',
6: 'fail',
7: 'fail',
8: 'fail',
9: 'ignored',
10: 'fail',
20: 'progress',
21: 'progress',
22: 'progress',
30: 'ignored',
};
export const USER_GENDER_MALE = 0;
export const USER_GENDER_FEMALE = 1;
export const USER_GENDER_OTHER = 2;
export const USER_GENDERS = [USER_GENDER_MALE, USER_GENDER_FEMALE, USER_GENDER_OTHER];
export const USER_GENDER_RANGE = {
[USER_GENDER_MALE]: 'Boy ♂',
[USER_GENDER_FEMALE]: 'Girl ♀',
[USER_GENDER_OTHER]: 'Other',
};
export const USER_GENDER_ICONS = {
[USER_GENDER_MALE]: '♂',
[USER_GENDER_FEMALE]: '♀',
[USER_GENDER_OTHER]: '?',
};
export const CONSTANT = {
PROBLEM_PER_PAGE: 100,
CONTEST_PER_PAGE: 20,
DISCUSSION_PER_PAGE: 50,
RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20,
TRAINING_PER_PAGE: 10,
REPLY_PER_PAGE: 50,
HOMEWORK_ON_MAIN: 5,
DISCUSSION_ON_MAIN: 20,
CONTEST_ON_MAIN: 10,
TRAINING_ON_MAIN: 10,
};
global.Hydro.model.builtin = {
Permission,
PERM,
PERMS,
PERMS_BY_FAMILY,
PRIV,
LEVELS,
BUILTIN_USERS,
BUILTIN_ROLES,
DEFAULT_NODES,
CATEGORIES,
LANG_TEXTS,
LANG_HIGHLIGHT_ID,
STATUS,
STATUS_TEXTS,
STATUS_CODES,
USER_GENDER_MALE,
USER_GENDER_FEMALE,
USER_GENDER_OTHER,
USER_GENDERS,
USER_GENDER_RANGE,
USER_GENDER_ICONS,
};
+604
View File
@@ -0,0 +1,604 @@
import { ObjectID } from 'mongodb';
import * as user from './user';
import * as problem from './problem';
import * as document from './document';
import { PERM } from './builtin';
import {
ValidationError, ContestNotFoundError, ContestAlreadyAttendedError,
ContestNotAttendedError, ContestScoreboardHiddenError,
} from '../error';
import {
ContestRule, ContestRules, ScoreboardNode, Tdoc, Udict,
} from '../interface';
import * as misc from '../lib/misc';
import ranked from '../lib/rank';
const acm: ContestRule = {
TEXT: 'ACM/ICPC',
check: () => { },
statusSort: { accept: -1, time: 1 },
showScoreboard: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
stat: (tdoc, journal) => {
const naccept = {};
const effective = {};
const detail = [];
let accept = 0;
let time = 0;
for (const j of journal) {
if (tdoc.pids.includes(j.pid)
&& !(effective[j.pid] && effective[j.pid].accept)) {
effective[j.pid] = j;
if (!j.accept) naccept[j.pid] = (naccept[j.pid] || 0) + 1;
}
}
function _time(jdoc) {
const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
const penalty = 20 * 60 * naccept[jdoc.pid];
return real + penalty;
}
for (const key in effective) {
const j = effective[key];
detail.push({
...j, naccept: naccept[j.pid], time: _time(j),
});
}
for (const d of detail) {
accept += d.accept;
if (d.accept) time += d.time;
}
return { accept, time, detail };
},
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'solved_problems', value: _('Solved Problems') },
];
if (isExport) {
columns.push(
{ type: 'total_time', value: _('Total Time (Seconds)') },
{ type: 'total_time_str', value: _('Total Time') },
);
}
for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1];
if (isExport) {
columns.push(
{
type: 'problem_flag',
value: '#{0} {1}'.format(i, pdict[pid].title),
},
{
type: 'problem_time',
value: '#{0} {1}'.format(i, _('Time (Seconds)')),
},
{
type: 'problem_time_str',
value: '#{0} {1}'.format(i, _('Time')),
},
);
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i),
raw: pid,
});
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
if (tsdoc.detail) {
for (const item of tsdoc.detail) tsddict[item.pid] = item;
}
const row = [];
row.push(
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.accept || 0 },
);
if (isExport) {
row.push(
{ type: 'string', value: tsdoc.time || 0.0 },
{ type: 'string', value: tsdoc.time || 0.0 },
);
}
for (const pid of tdoc.pids) {
let rid;
let colAccepted;
let colTime;
let colTimeStr;
if (tsddict[pid]?.accept) {
rid = tsddict[pid].rid;
colAccepted = _('Accepted');
colTime = tsddict[pid].time;
colTimeStr = misc.formatSeconds(colTime);
} else {
rid = null;
colAccepted = '-';
colTime = '-';
colTimeStr = '-';
}
if (isExport) {
row.push({ type: 'string', value: colAccepted });
row.push({ type: 'string', value: colTime });
row.push({ type: 'string', value: colTimeStr });
} else {
row.push({
type: 'record',
value: '{0}\n{1}'.format(colAccepted, colTimeStr),
raw: rid,
});
}
}
rows.push(row);
}
return rows;
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
const oi: ContestRule = {
TEXT: 'OI',
check: () => { },
statusSort: { score: -1 },
stat: (tdoc, journal) => {
const detail = {};
let score = 0;
for (const j of journal) {
if (tdoc.pids.includes(j.pid)) {
detail[j.pid] = j;
score += j.score;
}
}
return { score, detail };
},
showScoreboard: (tdoc, now) => now > tdoc.endAt,
showRecord: (tdoc, now) => now > tdoc.endAt,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'total_score', value: _('Total Score') },
];
for (let i = 1; i <= tdoc.pids.length; i++) {
if (isExport) {
columns.push({
type: 'problem_score',
value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title),
});
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i),
raw: tdoc.pids[i - 1],
});
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
if (tsdoc.journal) {
for (const item of tsdoc.journal) tsddict[item.pid] = item;
}
const row = [];
row.push(
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.score || 0 },
);
for (const pid of tdoc.pids) {
row.push({
type: 'record',
value: tsddict[pid]?.score || '-',
raw: tsddict[pid]?.rid || null,
});
}
rows.push(row);
}
return rows;
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
const homework: ContestRule = {
TEXT: 'Assignment',
check: () => { },
statusSort: { penaltyScore: -1, time: 1 },
stat: (tdoc, journal) => {
const effective = {};
for (const j of journal) {
if (tdoc.pids.includes(j.pid)
&& !effective[j.pid]
&& journal[j.pid].accept) {
effective[j.pid] = j;
}
}
function time(jdoc) {
const real = jdoc.rid.generationTime - tdoc.beginAt.getTime() / 1000;
return Math.floor(real);
}
function penaltyScore(jdoc) {
const { score } = jdoc;
const exceedSeconds = Math.floor(
jdoc.rid.generationTime - tdoc.penaltySince.getTime() / 1000,
);
if (exceedSeconds < 0) return score;
let coefficient = 1;
const keys = Object.keys(tdoc.penaltyRules)
.map((k) => parseFloat(k)).sort((a, b) => a - b);
for (const i of keys) {
if (i * 3600 <= exceedSeconds) coefficient = tdoc.penaltyRules[i];
else break;
}
return score * coefficient;
}
const detail = [];
for (const j in effective) {
detail.push({
...effective[j],
penaltyScore: penaltyScore(effective[j]),
time: time(j),
});
}
return {
score: Math.sum(detail.map((d) => d.score)),
penaltyScore: Math.sum(detail.map((d) => d.penaltySince)),
time: Math.sum(detail.map((d) => d.time)),
detail,
};
},
showScoreboard: () => true,
showRecord: () => true,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'total_score', value: _('Score') },
];
if (isExport) {
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 = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1];
if (isExport) {
columns.push(
{
type: 'problem_score',
value: '#{0} {1}'.format(i, pdict[pid].title),
},
{
type: 'problem_original_score',
value: '#{0} {1}'.format(i, _('Original Score')),
},
{
type: 'problem_time',
value: '#{0} {1}'.format(i, _('Time (Seconds)')),
},
{
type: 'problem_time_str',
value: '#{0} {1}'.format(i, _('Time')),
},
);
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i),
raw: pid,
});
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
for (const item of tsdoc.journal || []) {
tsddict[item.pid] = item;
}
const row = [
{ type: 'string', value: rank },
{
type: 'user',
value: udict[tsdoc.uid].uname,
raw: tsdoc.uid,
},
{
type: 'string',
value: tsdoc.penaltyScore || 0,
},
];
if (isExport) {
row.push({ type: 'string', value: tsdoc.score || 0 });
row.push({ type: 'string', value: tsdoc.time || 0.0 });
}
row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) });
for (const pid of tdoc.pids) {
const rid = tsddict[pid]?.rid;
const colScore = tsddict[pid]?.penaltyScore || '-';
const colOriginalScore = tsddict[pid]?.score || '-';
const colTime = tsddict[pid]?.time || '-';
const colTimeStr = colTime !== '-' ? misc.formatSeconds(colTime || 0) : '-';
if (isExport) {
row.push(
{ type: 'string', value: colScore },
{ type: 'string', value: colOriginalScore },
{ type: 'string', value: colTime },
{ type: 'string', value: colTimeStr },
);
} else {
row.push({
type: 'record',
value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr),
raw: rid,
});
}
}
rows.push(row);
}
return rows;
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
export const RULES: ContestRules = {
acm, oi, homework,
};
function _getStatusJournal(tsdoc) {
return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime));
}
export function add(
domainId: string, title: string, content: string, owner: number,
rule: string, beginAt = new Date(), endAt = new Date(), pids = [],
rated = false, data = {}, type = document.TYPE_CONTEST,
) {
if (!this.RULES[rule]) throw new ValidationError('rule');
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
Object.assign(data, {
content, owner, title, rule, beginAt, endAt, pids, attend: 0,
});
this.RULES[rule].check(data);
return document.add(domainId, content, owner, type, null, null, null, {
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
});
}
export async function edit(
domainId: string, tid: ObjectID,
$set: any, type = document.TYPE_CONTEST,
) {
if ($set.rule) {
if (!this.RULES[$set.rule]) throw new ValidationError('rule');
}
const tdoc = await document.get(domainId, type, tid);
if (!tdoc) throw new ContestNotFoundError(tid);
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
return await document.set(domainId, type, tid, $set);
}
export async function get(
domainId: string, tid: ObjectID, type = document.TYPE_CONTEST,
) {
let tdoc: Tdoc;
if (type === -1) {
tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
if (!tdoc) tdoc = await document.get(domainId, document.TYPE_HOMEWORK, tid);
} else tdoc = await document.get(domainId, type, tid);
if (!tdoc) throw new ContestNotFoundError(tid);
return tdoc;
}
export async function updateStatus(
domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number,
accept = false, score = 0, type = document.TYPE_CONTEST,
) {
const tdoc = await get(domainId, tid, type);
const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', {
rid, pid, accept, score,
});
if (!tsdoc.attend) throw new ContestNotAttendedError(tid, uid);
const journal = _getStatusJournal(tsdoc);
const stats = RULES[tdoc.rule].stat(tdoc, journal);
return await document.revSetStatus(domainId, type, tid, uid, tsdoc.rev, { journal, ...stats });
}
export function getStatus(
domainId: string, tid: ObjectID, uid: number,
type = document.TYPE_CONTEST,
) {
return document.getStatus(domainId, type, tid, uid);
}
export async function getListStatus(
domainId: string, uid: number, tids: ObjectID[],
type = document.TYPE_CONTEST,
) {
const r = {};
// eslint-disable-next-line no-await-in-loop
for (const tid of tids) r[tid.toHexString()] = await getStatus(domainId, tid, uid, type);
return r;
}
export async function attend(
domainId: string, tid: ObjectID, uid: number,
type = document.TYPE_CONTEST,
) {
try {
await document.cappedIncStatus(domainId, type, tid, uid, 'attend', 1, 0, 1);
} catch (e) {
throw new ContestAlreadyAttendedError(tid, uid);
}
await document.inc(domainId, type, tid, 'attend', 1);
return {};
}
export function getMultiStatus(domainId: string, query: any, docType = document.TYPE_CONTEST) {
return document.getMultiStatus(domainId, docType, query);
}
export function isNew(tdoc: Tdoc, days = 1) {
const now = new Date().getTime();
const readyAt = tdoc.beginAt.getTime();
return (now < readyAt - days * 24 * 3600 * 1000);
}
export function isUpcoming(tdoc: Tdoc, days = 1) {
const now = new Date().getTime();
const readyAt = tdoc.beginAt.getTime();
return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt.getTime());
}
export function isNotStarted(tdoc: Tdoc) {
return (new Date()) < tdoc.beginAt;
}
export function isOngoing(tdoc: Tdoc) {
const now = new Date();
return (tdoc.beginAt <= now && now < tdoc.endAt);
}
export function isDone(tdoc: Tdoc) {
return tdoc.endAt <= new Date();
}
export function isExtended(tdoc: Tdoc) {
const now = new Date().getTime();
return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime();
}
export const ContestHandlerMixin = (c) => class extends c {
canViewHiddenScoreboard() {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
canShowRecord(tdoc: Tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
canShowScoreboard(tdoc: Tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
async getScoreboard(
domainId: string, tid: ObjectID,
isExport = false, docType = document.TYPE_CONTEST,
): Promise<[Tdoc, ScoreboardNode[][], Udict]> {
const tdoc = await get(domainId, tid, docType);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).toArray();
const uids = tsdocs.map((tsdoc) => tsdoc.uid);
const [udict, pdict] = await Promise.all([
user.getList(domainId, uids),
problem.getList(domainId, tdoc.pids, true),
]);
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
const rows = RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
tdoc, rankedTsdocs, udict, pdict,
);
return [tdoc, rows, udict];
}
// eslint-disable-next-line class-methods-use-this
async verifyProblems(domainId: string, pids: Array<number | string>) {
const pdict = await problem.getList(domainId, pids, true);
const _pids: Set<number> = new Set();
for (const i in pdict) _pids.add(pdict[i].docId);
return Array.from(_pids);
}
};
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
}
export function count(domainId: string, query: any, type = document.TYPE_CONTEST) {
return document.count(domainId, type, query);
}
export function getMulti(domainId: string, query = {}, type = document.TYPE_CONTEST) {
return document.getMulti(domainId, type, query);
}
export async function getAndListStatus(
domainId: string, tid: ObjectID, docType = document.TYPE_CONTEST,
): Promise<[Tdoc, any[]]> {
// TODO(iceboy): projection, pagination.
const tdoc = await get(domainId, tid, docType);
const tsdocs = await document.getMultiStatus(domainId, docType, { docId: tid })
.sort(RULES[tdoc.rule].statusSort).toArray();
return [tdoc, tsdocs];
}
export async function recalcStatus(domainId: string, tid: ObjectID, type = document.TYPE_CONTEST) {
const [tdoc, tsdocs] = await Promise.all([
document.get(domainId, type, tid),
document.getMultiStatus(domainId, type, { docId: tid }).toArray(),
]);
const tasks = [];
for (const tsdoc of tsdocs || []) {
if (tsdoc.journal) {
const journal = _getStatusJournal(tsdoc);
const stats = RULES[tdoc.rule].stat(tdoc, journal);
tasks.push(
document.revSetStatus(
domainId, type, tid,
tsdoc.uid, tsdoc.rev, { journal, ...stats }, false,
),
);
}
}
return await Promise.all(tasks);
}
export const statusText = (tdoc: Tdoc) => (
isNew(tdoc)
? 'New'
: isUpcoming(tdoc)
? 'Ready (☆▽☆)'
: isOngoing(tdoc)
? 'Live...'
: 'Done');
export const getStatusText = (tdoc: Tdoc) => (
isNotStarted(tdoc)
? 'not_started'
: isOngoing(tdoc)
? 'ongoing'
: 'finished');
global.Hydro.model.contest = {
RULES,
ContestHandlerMixin,
add,
getListStatus,
getMultiStatus,
attend,
edit,
get,
updateStatus,
getStatus,
count,
getMulti,
setStatus,
getAndListStatus,
recalcStatus,
isNew,
isUpcoming,
isNotStarted,
isOngoing,
isDone,
isExtended,
statusText,
getStatusText,
};
+243
View File
@@ -0,0 +1,243 @@
import { ObjectID } from 'mongodb';
import * as problem from './problem';
import * as contest from './contest';
import * as training from './training';
import * as document from './document';
import { DocumentNotFoundError } from '../error';
import { Ddoc, Drdoc, Drrdoc } from '../interface';
export const typeDisplay = {
[document.TYPE_PROBLEM]: 'problem',
[document.TYPE_CONTEST]: 'contest',
[document.TYPE_DISCUSSION_NODE]: 'node',
[document.TYPE_TRAINING]: 'training',
[document.TYPE_HOMEWORK]: 'homework',
};
export function add(
domainId: string, parentType: number, parentId: ObjectID | number | string,
owner: number, title: string, content: string,
ip: string | null = null, highlight: boolean, pin: boolean,
): Promise<ObjectID> {
return document.add(
domainId, content, owner, document.TYPE_DISCUSSION,
null, parentType, parentId,
{
title,
ip,
nReply: 0,
highlight,
pin,
updateAt: new Date(),
views: 0,
},
);
}
export function get(domainId: string, did: ObjectID): Promise<Ddoc | null> {
return document.get(domainId, document.TYPE_DISCUSSION, did);
}
export function edit(
domainId: string, did: ObjectID,
title: string, content: string, highlight: boolean, pin: boolean,
): Promise<Ddoc | null> {
return document.set(domainId, document.TYPE_DISCUSSION, did, {
title, content, highlight, pin,
});
}
export function del(domainId: string, did: ObjectID) {
return Promise.all([
document.deleteOne(domainId, document.TYPE_DISCUSSION, did),
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
parentType: document.TYPE_DISCUSSION, parentId: did,
}),
document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: did }),
]);
}
export function count(domainId: string, query: any) {
return document.count(domainId, document.TYPE_DISCUSSION, query);
}
export function getMulti(domainId: string, query: any = {}) {
return document.getMulti(domainId, document.TYPE_DISCUSSION, query)
.sort({ pin: -1, updateAt: -1 });
}
export async function addReply(
domainId: string, did: ObjectID, owner: number,
content: string, ip: string,
): Promise<Drdoc> {
const [drdoc] = await Promise.all([
document.add(
domainId, content, owner, document.TYPE_DISCUSSION_REPLY,
null, document.TYPE_DISCUSSION, did, { ip },
),
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: new Date() }),
]);
return drdoc;
}
export function getReply(domainId: string, drid: ObjectID): Promise<Drdoc | null> {
return document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid);
}
export async function editReply(
domainId: string, drid: ObjectID, content: string,
): Promise<Drdoc | null> {
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content });
}
export async function delReply(domainId: string, drid: ObjectID) {
const drdoc = await getReply(domainId, drid);
if (!drdoc) throw new DocumentNotFoundError(drid);
return await Promise.all([
document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid),
document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1),
]);
}
export function getMultiReply(domainId: string, did: ObjectID) {
return document.getMulti(
domainId, document.TYPE_DISCUSSION_REPLY,
{ parentType: document.TYPE_DISCUSSION, parentId: did },
).sort('_id', -1);
}
export function getListReply(domainId: string, did: ObjectID): Promise<Drdoc[]> {
return getMultiReply(domainId, did).toArray();
}
export async function addTailReply(
domainId: string, drid: ObjectID,
owner: number, content: string, ip: string,
): Promise<[Drdoc, ObjectID]> {
const [drdoc, subId] = await document.push(
domainId, document.TYPE_DISCUSSION_REPLY, drid,
'reply', content, owner, { ip },
);
await document.set(
domainId, document.TYPE_DISCUSSION, drdoc.parentId,
{ updateAt: new Date() },
);
return [drdoc, subId];
}
export function getTailReply(
domainId: string, drid: ObjectID, drrid: ObjectID,
): Promise<[Drdoc, Drrdoc] | [null, null]> {
// @ts-ignore
return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
}
export function editTailReply(
domainId: string, drid: ObjectID, drrid: ObjectID, content: string,
): Promise<Drrdoc> {
return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content });
}
export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
}
export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) {
return document.setStatus(domainId, document.TYPE_DISCUSSION, did, uid, { star });
}
export function getStatus(domainId: string, did: ObjectID, uid: number) {
return document.getStatus(domainId, document.TYPE_DISCUSSION, did, uid);
}
export function addNode(domainId: string, _id: string, category: string, args: any = {}) {
return document.add(
domainId, category, 1, document.TYPE_DISCUSSION_NODE,
_id, null, null, args,
);
}
export function getNode(domainId: string, _id: string) {
return document.get(domainId, document.TYPE_DISCUSSION_NODE, _id);
}
export async function getVnode(domainId: string, type: number, id: string) {
if (type === document.TYPE_PROBLEM) {
// @ts-ignore
if (Number.isSafeInteger(parseInt(id, 10))) id = parseInt(id, 10);
const pdoc = await problem.get(domainId, id);
if (!pdoc) return null;
return { ...pdoc, type, id };
}
if (type === document.TYPE_CONTEST) {
const tdoc = await contest.get(domainId, new ObjectID(id));
return { ...tdoc, type, id };
}
if (type === document.TYPE_DISCUSSION_NODE) {
const ndoc = await getNode(domainId, id);
return {
...ndoc,
title: id,
type,
id,
};
}
if (type === document.TYPE_TRAINING) {
const tdoc = await training.get(domainId, id);
return { ...tdoc, type, id };
}
if (type === document.TYPE_HOMEWORK) {
const tdoc = await contest.get(domainId, new ObjectID(id), document.TYPE_HOMEWORK);
return { ...tdoc, type, id };
}
return {
title: 'Missing Node',
type: 'Unknown',
id: new ObjectID(),
};
}
export function getNodes(domainId: string) {
return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray();
}
export async function getListVnodes(domainId: string, ddocs: any, getHidden: boolean) {
const tasks = [];
const res = {};
async function task(ddoc: Ddoc) {
const vnode = await getVnode(domainId, ddoc.parentType, ddoc.parentId.toString());
if (!res[ddoc.parentType]) res[ddoc.parentType] = {};
if (vnode.hidden && !getHidden) return;
res[ddoc.parentType][ddoc.parentId] = vnode;
}
for (const ddoc of ddocs) tasks.push(task(ddoc));
await Promise.all(tasks);
return res;
}
global.Hydro.model.discussion = {
typeDisplay,
add,
get,
edit,
del,
count,
getMulti,
addReply,
getReply,
editReply,
delReply,
getMultiReply,
getListReply,
addTailReply,
getTailReply,
editTailReply,
delTailReply,
setStar,
getStatus,
addNode,
getNode,
getNodes,
getVnode,
getListVnodes,
};
+407
View File
@@ -0,0 +1,407 @@
import assert from 'assert';
import { ObjectID } from 'mongodb';
import * as db from '../service/db';
type DocID = ObjectID | string | number;
const coll = db.collection('document');
const collStatus = db.collection('document.status');
export const TYPE_PROBLEM = 10;
export const TYPE_PROBLEM_SOLUTION = 11;
export const TYPE_PROBLEM_LIST = 12;
export const TYPE_DISCUSSION_NODE = 20;
export const TYPE_DISCUSSION = 21;
export const TYPE_DISCUSSION_REPLY = 22;
export const TYPE_CONTEST = 30;
export const TYPE_TRAINING = 40;
export const TYPE_FILE = 50;
export const TYPE_HOMEWORK = 60;
export async function add<T extends DocID>(
domainId: string, content: string, owner: number,
docType: number, docId: T,
parentType?: number | null, parentId?: DocID,
args?: any,
): Promise<T>
export async function add(
domainId: string, content: string, owner: number,
docType: number, docId: null,
parentType?: number, parentId?: DocID,
args?: any,
): Promise<ObjectID>
export async function add(
domainId: string, content: string, owner: number,
docType: number, docId: DocID = null,
parentType: number | null = null, parentId: DocID = null,
args: any = {},
) {
const _id = new ObjectID();
const doc: any = {
_id,
content,
owner,
domainId,
docType,
docId: docId || _id,
...args,
};
if (parentType || parentId) {
assert(parentType && parentId);
doc.parentType = parentType;
doc.parentId = parentId;
}
const res = await coll.insertOne(doc);
return docId || res.insertedId;
}
export function get(domainId: string, docType: number, docId: DocID) {
return coll.findOne({ domainId, docType, docId });
}
export async function set(domainId: string, docType: number, docId: DocID, args: any) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $set: args },
{ returnOriginal: false, upsert: true },
);
return res.value;
}
export function deleteOne(domainId: string, docType: number, docId: DocID) {
return Promise.all([
collStatus.deleteMany({ domainId, docType, docId }),
coll.deleteOne({ domainId, docType, docId }),
]);
}
export function deleteMulti(domainId: string, docType: number, args: any = {}) {
return coll.deleteMany({ ...args, domainId, docType });
}
export function deleteMultiStatus(domainId: string, docType: number, args: any = {}) {
return collStatus.deleteMany({ ...args, domainId, docType });
}
export function getMulti(domainId: string, docType: number, args: any = {}) {
return coll.find({ ...args, docType, domainId });
}
export async function inc(
domainId: string, docType: number, docId: DocID,
key: string, value: number,
) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $inc: { [key]: value } },
{ returnOriginal: false },
);
return res.value;
}
export async function incAndSet(
domainId: string, docType: number, docId: DocID,
key: string, value: number, args: any,
) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $inc: { [key]: value }, $set: args },
{ returnOriginal: false },
);
return res.value;
}
export function count(domainId: string, docType: number, query: any) {
return coll.find({ ...query, docType, domainId }).count();
}
export async function push(
domainId: string, docType: number, docId: DocID,
key: string, content: string, owner: number, args: any = {},
) {
const _id = new ObjectID();
const doc = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{
$push: {
[key]: {
...args, content, owner, _id,
},
},
},
{ returnOriginal: false },
);
return [doc.value, _id];
}
export async function pull(
domainId: string, docType: number, docId: DocID,
setKey: string, contents: string[],
) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $pull: { [setKey]: { $in: contents } } },
{ returnOriginal: false },
);
return res.value;
}
export async function deleteSub(
domainId: string, docType: number, docId: DocID,
key: string, subId: ObjectID,
) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $pull: { [key]: { _id: subId } } },
{ returnOriginal: false },
);
return res.value;
}
export async function getSub(
domainId: string, docType: number, docId: DocID,
key: string, subId: ObjectID,
) {
const doc = await coll.findOne({
domainId,
docType,
docId,
[key]: { $elemMatch: { _id: subId } },
});
if (!doc) return [null, null];
for (const sdoc of doc[key] || []) {
if (sdoc._id === subId) return [doc, sdoc];
}
return [doc, null];
}
export async function setSub(
domainId: string, docType: number, docId: DocID,
key: string, subId: ObjectID, args: any,
) {
const $set = {};
for (const k in args) {
$set[`${key}.$.${k}`] = args[k];
}
const res = await coll.findOneAndUpdate(
{
domainId,
docType,
docId,
[key]: { $elemMatch: { _id: subId } },
},
{ $set },
{ returnOriginal: false },
);
return res.value;
}
export async function addToSet(
domainId: string, docType: number, docId: DocID,
setKey: string, content: string,
) {
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
{ $addToSet: { [setKey]: content } },
{ returnOriginal: false },
);
return res.value;
}
export function getStatus(domainId: string, docType: number, docId: DocID, uid: number) {
return collStatus.findOne({
domainId, docType, docId, uid,
});
}
export function getMultiStatus(domainId: string, docType: number, args: any) {
return collStatus.find({ ...args, docType, domainId });
}
export function getMultiStatusWithoutDomain(docType: number, args: any) {
return collStatus.find({ ...args, docType });
}
export async function setStatus(
domainId: string, docType: number, docId: DocID, uid: number, args: any,
) {
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid,
},
{ $set: args },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export function setMultiStatus(domainId: string, docType: number, query: any, args: any) {
return collStatus.updateMany(
{ domainId, docType, ...query },
{ $set: args },
);
}
export async function setIfNotStatus(
domainId: string, docType: number, docId: DocID, uid: number,
key: string, value: number, ifNot: any, args: any,
) {
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid, key: { $not: { $eq: ifNot } },
},
{ $set: { [key]: value, ...args } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export async function cappedIncStatus(
domainId: string, docType: number, docId: DocID, uid: number,
key: string, value: number, minValue = -1, maxValue = 1,
) {
assert(value !== 0);
const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue };
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid, [key]: { $not },
},
{ $inc: { [key]: value } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export async function incStatus(
domainId: string, docType: number, docId: DocID, uid: number,
key: string, value: number,
) {
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid,
},
{ $inc: { [key]: value } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export async function revPushStatus(
domainId: string, docType: number, docId: DocID, uid: number,
key: string, value: any,
) {
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid,
},
{ $push: { [key]: value }, $inc: { rev: 1 } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export async function revInitStatus(
domainId: string, docType: number, docId: DocID, uid: number,
) {
const res = await collStatus.findOneAndUpdate(
{
domainId, docType, docId, uid,
},
{ $inc: { rev: 1 } },
{ upsert: true, returnOriginal: false },
);
return res.value;
}
export async function revSetStatus(
domainId: string, docType: number, docId: DocID, uid: number,
rev: number, args: any, returnDoc = true,
) {
const filter = {
domainId, docType, docId, uid, rev,
};
const update = { $set: args, $inc: { rev: 1 } };
if (returnDoc) {
const res = await collStatus.findOneAndUpdate(filter, update, { returnOriginal: false });
return res.value;
}
return await collStatus.updateOne(filter, update);
}
/* eslint-disable object-curly-newline */
async function ensureIndexes() {
const ic = coll.createIndex.bind(coll);
const is = collStatus.createIndex.bind(coll);
const u = { unique: true };
const s = { sparse: true };
await ic({ domainId: 1, docType: 1, docId: 1 }, u);
await ic({ domainId: 1, docType: 1, owner: 1, docId: -1 });
// For problem
await ic({ domainId: 1, docType: 1, category: 1, docId: 1 }, s);
await ic({ domainId: 1, docType: 1, hidden: 1, category: 1, docId: 1 }, s);
await ic({ domainId: 1, docType: 1, tag: 1, docId: 1 }, s);
await ic({ domainId: 1, docType: 1, hidden: 1, tag: 1, docId: 1 }, s);
// For problem solution
await ic({ domainId: 1, docType: 1, parentType: 1, parentId: 1, vote: -1, docId: -1 }, s);
// For discussion
await ic({ domainId: 1, docType: 1, pin: -1, updateAt: -1, docId: -1 }, s);
await ic({ domainId: 1, docType: 1, parentType: 1, parentId: 1, updateAt: -1, docId: -1 }, s);
// Hidden doc
await ic({ domainId: 1, docType: 1, hidden: 1, docId: -1 }, s);
// For contest
await ic({ domainId: 1, docType: 1, pids: 1 }, s);
await ic({ domainId: 1, docType: 1, rule: 1, docId: -1 }, s);
// For training
await ic({ domainId: 1, docType: 1, 'dag.pids': 1 }, s);
await is({ domainId: 1, docType: 1, uid: 1, docId: 1 }, u);
// For rp system
await is({ domainId: 1, docType: 1, docId: 1, status: 1, rid: 1, rp: 1 }, s);
// For contest rule OI
await is({ domainId: 1, docType: 1, docId: 1, score: -1 }, s);
// For contest rule ACM
await is({ domainId: 1, docType: 1, docId: 1, accept: -1, time: 1 }, s);
// For training
await is({ domainId: 1, docType: 1, uid: 1, enroll: 1, docId: 1 }, s);
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.document = {
add,
addToSet,
cappedIncStatus,
count,
deleteMulti,
deleteMultiStatus,
deleteOne,
deleteSub,
get,
getMulti,
getMultiStatus,
getMultiStatusWithoutDomain,
getStatus,
getSub,
inc,
incAndSet,
incStatus,
pull,
push,
revInitStatus,
revPushStatus,
revSetStatus,
set,
setIfNotStatus,
setStatus,
setMultiStatus,
setSub,
TYPE_CONTEST,
TYPE_DISCUSSION,
TYPE_DISCUSSION_NODE,
TYPE_DISCUSSION_REPLY,
TYPE_HOMEWORK,
TYPE_PROBLEM,
TYPE_PROBLEM_LIST,
TYPE_PROBLEM_SOLUTION,
TYPE_FILE,
TYPE_TRAINING,
};
+174
View File
@@ -0,0 +1,174 @@
import { Dictionary } from 'lodash';
import { BUILTIN_ROLES, PRIV } from './builtin';
import { DomainDoc } from '../interface';
import * as db from '../service/db';
const coll = db.collection('domain');
const collUser = db.collection('domain.user');
export async function add(domainId: string, owner: number, name: string, bulletin: string) {
const ddoc: DomainDoc = {
_id: domainId,
owner,
name,
bulletin,
roles: {},
gravatar: '',
pidCounter: 0,
};
await coll.insertOne(ddoc);
return domainId;
}
export function get(domainId: string): Promise<DomainDoc | null> {
return coll.findOne({ _id: domainId });
}
export function getMulti(query: any = {}) {
return coll.find(query);
}
export function edit(domainId: string, $set: any) {
return coll.updateOne({ _id: domainId }, { $set });
}
export async function inc(domainId: string, field: string, n: number): Promise<number | null> {
const res = await coll.findOneAndUpdate(
{ _id: domainId },
{ $inc: { [field]: n } },
{ returnOriginal: false },
);
return res.value[field];
}
export async function getList(domainIds: string[]): Promise<Dictionary<DomainDoc>> {
const r = {};
// eslint-disable-next-line no-await-in-loop
for (const domainId of domainIds) r[domainId] = await get(domainId);
return r;
}
export function setUserRole(domainId: string, uid: number, role: string) {
return collUser.updateOne({ uid, domainId }, { role }, { upsert: true });
}
export async function getRoles(arg: string | DomainDoc) {
let ddoc: DomainDoc;
if (typeof arg === 'string') ddoc = await get(arg);
else ddoc = arg;
const roles = [];
const r = [];
for (const role in ddoc.roles) {
roles.push({ _id: role, perm: BigInt(ddoc.roles[role]) });
r.push(role);
}
for (const role in BUILTIN_ROLES) {
if (!r.includes(role)) {
roles.push({ _id: role, perm: BUILTIN_ROLES[role] });
}
}
return roles;
}
export async function setRoles(domainId: string, roles: Dictionary<bigint>) {
const current = await get(domainId);
for (const role in roles) {
current.roles[role] = roles[role].toString();
}
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
}
export async function addRole(domainId: string, name: string, permission: bigint) {
const current = await get(domainId);
current.roles[name] = permission.toString();
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
}
export async function deleteRoles(domainId: string, roles: string[]) {
const current = await get(domainId);
for (const role of roles) delete current.roles[role];
await Promise.all([
coll.updateOne({ _id: domainId }, { $set: current }),
collUser.updateMany({ domainId, role: { $in: roles } }, { $set: { role: 'default' } }),
]);
}
interface DomainUserArg {
_id: number,
priv: number,
}
export async function getDomainUser(domainId: string, udoc: DomainUserArg) {
let dudoc = await collUser.findOne({ domainId, uid: udoc._id });
dudoc = dudoc || {};
if (!(udoc.priv & PRIV.PRIV_USER_PROFILE)) dudoc.role = 'guest';
if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin';
dudoc.role = dudoc.role || 'default';
const ddoc = await get(domainId);
dudoc.perm = ddoc.roles[dudoc.role]
? BigInt(ddoc.roles[dudoc.role])
: BUILTIN_ROLES[dudoc.role];
return dudoc;
}
export function setMultiUserInDomain(domainId: string, query: any, params: any) {
return collUser.updateMany({ domainId, ...query }, { $set: params });
}
export function getMultiInDomain(domainId: string, query: any = {}) {
return collUser.find({ domainId, ...query });
}
export function setUserInDomain(domainId: string, uid: number, params: any) {
return collUser.updateOne({ domainId, uid }, { $set: params });
}
export async function incUserInDomain(domainId: string, uid: number, field: string, n = 1) {
// @ts-ignore
const dudoc = await getDomainUser(domainId, { _id: uid });
dudoc[field] = dudoc[field] + n || n;
await setUserInDomain(domainId, uid, { [field]: dudoc[field] });
return dudoc;
}
export async function getDictUserByDomainId(uid: number) {
const dudocs = await collUser.find({ uid }).toArray();
const ddocs = await coll.find({ owner: uid }).toArray();
const dudict = {};
for (const dudoc of dudocs) {
// eslint-disable-next-line no-await-in-loop
dudict[dudoc.domainId] = await get(dudoc.domainId);
}
for (const ddoc of ddocs) {
dudict[ddoc._id] = ddoc;
}
return dudict;
}
export async function getPrefixSearch(prefix: string, limit = 50) {
const $regex = new RegExp(prefix, 'mi');
const ddocs = await coll.find({
$or: [{ _id: { $regex } }, { name: { $regex } }],
}).limit(limit).toArray();
return ddocs;
}
global.Hydro.model.domain = {
getRoles,
add,
inc,
get,
edit,
getMulti,
getList,
setRoles,
addRole,
deleteRoles,
setUserRole,
getDomainUser,
setMultiUserInDomain,
setUserInDomain,
incUserInDomain,
getMultiInDomain,
getDictUserByDomainId,
getPrefixSearch,
};
+148
View File
@@ -0,0 +1,148 @@
import { ObjectID } from 'mongodb';
import * as fs from 'fs';
import { Dictionary } from 'lodash';
import { ForbiddenError, NotFoundError } from '../error';
import * as db from '../service/db';
import gridfs from '../service/gridfs';
import hash from '../lib/hash.hydro';
import { Ufdoc } from '../interface';
const coll = db.collection('file');
const collFile = db.collection('fs.files');
const collChunk = db.collection('fs.chunks');
function _timestamp() {
return Math.floor(Number(new Date()) / 1000000);
}
export async function add(
streamOrPath: fs.ReadStream | string, filename: string,
owner = 1, meta = {},
) {
let stream: fs.ReadStream;
if (typeof streamOrPath === 'string') stream = fs.createReadStream(streamOrPath);
else stream = streamOrPath;
const w = gridfs.openUploadStream(filename);
await coll.insertOne({
...meta,
_id: w.id,
secret: String.random(32),
owner,
filename,
count: 1,
});
await new Promise((resolve, reject) => {
w.on('error', reject);
w.on('finish', resolve);
stream.pipe(w);
});
const c = await gridfs.find({ _id: w.id }).toArray();
await coll.updateOne({ _id: w.id }, { $set: { md5: c[0].md5, size: c[0].length } });
return w.id as ObjectID;
}
export function del(_id: ObjectID) {
return Promise.all([
coll.deleteOne({ _id }),
collFile.deleteOne({ _id }),
collChunk.deleteMany({ files_id: _id }),
]);
}
export async function inc(_id: ObjectID): Promise<number> {
const doc = await coll.findOneAndUpdate(
{ _id },
{ $inc: { count: 1 } },
{ returnOriginal: false },
);
return doc.value?.count;
}
export async function dec(_id: ObjectID): Promise<number> {
const file = await coll.findOneAndUpdate(
{ _id },
{ $inc: { count: -1 } },
{ returnOriginal: false },
);
if (!file.value) return 0;
if (!file.value.count) await del(_id);
return file.value.count;
}
export async function getWithSecret(_id: ObjectID, secret: string, reject?: Function) {
const file = await coll.findOne({ _id });
if (!file) throw new NotFoundError(_id);
const timestamp = _timestamp();
if (!(hash(file.secret, timestamp.toString()) === secret)) {
if (!(hash(file.secret, (timestamp - 1).toString()) === secret)) {
throw new ForbiddenError();
}
}
const stream = gridfs.openDownloadStream(_id);
stream.on('error', (err) => {
console.error(err);
if (reject) reject();
});
return stream;
}
export async function get(_id: ObjectID, reject?: Function) {
const file = await coll.findOne({ _id });
if (!file) throw new NotFoundError(_id);
const stream = gridfs.openDownloadStream(_id);
stream.on('error', (err) => {
console.error(err);
if (reject) reject();
});
return stream;
}
export function getMeta(_id: ObjectID): Promise<Ufdoc> {
return coll.findOne({ _id });
}
export async function getMetaDict(ufids: ObjectID[]): Promise<Dictionary<Ufdoc>> {
const r = {};
ufids = Array.from(new Set(ufids));
const ufdocs = await coll.find({ _id: { $in: ufids } }).toArray();
for (const ufdoc of ufdocs) {
r[ufdoc._id.toHexString()] = ufdoc;
}
return r;
}
export function getMulti(query: any) {
return coll.find(query);
}
async function _url(_id: ObjectID, name: string | undefined) {
const file = await coll.findOne({ _id });
const secret = hash(file.secret, _timestamp().toString());
if (name) return `/fs/${_id}/${Buffer.from(name).toString('base64')}/${secret}`;
return `/fs/${_id}/${secret}`;
}
function __url(file: any, name: string | undefined) {
const secret = hash(file.secret, _timestamp().toString());
if (name) return `/fs/${file._id}/${Buffer.from(name).toString('base64')}/${secret}`;
return `/fs/${file._id}/${secret}`;
}
export function url(_id: ObjectID, name: string | undefined): Promise<string>;
export function url(file: any, name: string | undefined): string;
export function url(arg0: any, name: any) {
if (arg0 instanceof ObjectID) return _url(arg0, name);
return __url(arg0, name);
}
function ensureIndexes() {
return Promise.all([
collChunk.createIndex({ files_id: 1, n: 1 }, { unique: true }),
collFile.createIndex({ md5: 1 }),
]);
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.file = {
add, get, del, inc, dec, getMeta, getWithSecret, getMulti, getMetaDict, url,
};
+85
View File
@@ -0,0 +1,85 @@
import { ObjectID } from 'mongodb';
import * as user from './user';
import { Mdoc } from '../interface';
import * as db from '../service/db';
import * as bus from '../service/bus';
const coll = db.collection('message');
export const FLAG_UNREAD = 1;
export const FLAG_ALERT = 2;
export async function send(
from: number, to: number,
content: string, flag: number = FLAG_UNREAD,
): Promise<Mdoc> {
const res = await coll.insertOne({
from, to, content, flag,
});
const mdoc = {
from, to, content, _id: res.insertedId, flag,
};
if (from !== to) {
// ENHANCE domainId?
const udoc = await user.getById('system', to);
bus.publish(`user_message-${to}`, { mdoc, udoc });
}
return mdoc;
}
export async function get(_id: ObjectID): Promise<Mdoc | null> {
return await coll.findOne({ _id });
}
export async function getByUser(uid: number): Promise<Mdoc[]> {
return await coll.find({ $or: [{ from: uid }, { to: uid }] }).sort('_id', 1).toArray();
}
export async function getMany(query: any, sort: any, page: number, limit: number): Promise<Mdoc[]> {
return await coll.find(query).sort(sort)
.skip((page - 1) * limit).limit(limit)
.toArray();
}
export async function setFlag(messageId: ObjectID, flag: number): Promise<Mdoc | null> {
const result = await coll.findOneAndUpdate(
{ _id: messageId },
{ $bit: { flag: { xor: flag } } },
{ returnOriginal: false },
);
return result.value;
}
export async function del(_id: ObjectID) {
return await coll.deleteOne({ _id });
}
export function count(query: any) {
return coll.find(query).count();
}
export function getMulti(uid: number) {
return coll.find({ $or: [{ from: uid }, { to: uid }] });
}
function ensureIndexes() {
return Promise.all([
coll.createIndex({ to: 1, _id: -1 }),
coll.createIndex({ from: 1, _id: -1 }),
]);
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.message = {
FLAG_UNREAD,
FLAG_ALERT,
count,
get,
getByUser,
del,
setFlag,
getMany,
getMulti,
send,
};
+27
View File
@@ -0,0 +1,27 @@
import { OpcountExceededError } from '../error';
import * as db from '../service/db';
const coll = db.collection('opcount');
export async function inc(op: string, ident: string, periodSecs: number, maxOperations: number) {
const curTime = new Date().getTime();
const beginAt = new Date(curTime - (curTime % (periodSecs * 1000)));
const expireAt = new Date(beginAt.getTime() + periodSecs * 1000);
try {
await coll.findOneAndUpdate({
ident,
beginAt,
expireAt,
op: { $not: { $gte: maxOperations } },
}, { $inc: { op: 1 } }, { upsert: true });
} catch (e) {
throw new OpcountExceededError(op, periodSecs, maxOperations);
}
}
function ensureIndexes() {
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.opcount = { inc };
+169
View File
@@ -0,0 +1,169 @@
import { ObjectID } from 'mongodb';
import { Dictionary } from 'lodash';
import { STATUS } from './builtin';
import * as file from './file';
import * as document from './document';
import * as domain from './domain';
import {
Pdoc, Pdict, ProblemDataSource, ProblemStatusDoc,
} from '../interface';
import { ProblemNotFoundError } from '../error';
import * as testdataConfig from '../lib/testdataConfig';
export const pdocHidden: Pdoc = {
_id: new ObjectID(),
domainId: 'system',
docType: document.TYPE_PROBLEM,
docId: -1,
pid: '',
owner: 1,
title: '*',
content: '',
nSubmit: 0,
nAccept: 0,
difficulty: 0,
tag: [],
category: [],
data: null,
hidden: true,
config: {},
acMsg: '',
};
export async function add(
domainId: string, pid: string = null, title: string, content: string, owner: number,
tag: string[] = [], category: string[] = [], data: ProblemDataSource = null, hidden = false,
) {
const pidCounter = await domain.inc(domainId, 'pidCounter', 1);
if (!pid) pid = pidCounter.toString();
return await document.add(
domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null,
{
pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0,
},
) as number;
}
export async function get(
domainId: string, pid: string | number,
uid: number = null,
): Promise<Pdoc> {
if (typeof pid !== 'number') {
if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10);
}
const pdoc = Number.isInteger(pid)
? await document.get(domainId, document.TYPE_PROBLEM, pid)
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
if (!pdoc) return null;
if (uid) {
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
}
return pdoc;
}
export function getMulti(domainId: string, query: object) {
return document.getMulti(domainId, document.TYPE_PROBLEM, query);
}
export function getMultiStatus(domainId: string, query: object) {
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
}
export function edit(domainId: string, _id: number, $set: any): Promise<Pdoc> {
return document.set(domainId, document.TYPE_PROBLEM, _id, $set);
}
export function inc(domainId: string, _id: number, field: string, n: number): Promise<Pdoc> {
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
}
export function count(domainId: string, query: any) {
return document.count(domainId, document.TYPE_PROBLEM, query);
}
export async function random(domainId: string, query: any): Promise<string | null> {
const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query);
const pcount = await cursor.count();
if (pcount) {
const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray();
return pdoc[0].pid;
} return null;
}
export async function getList(
domainId: string, pids: Array<number | string>,
getHidden = false, doThrow = true,
): Promise<Pdict> {
pids = Array.from(new Set(pids));
const r = {};
const l = {};
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
if (!getHidden) q.hidden = false;
const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q).toArray();
for (const pdoc of pdocs) {
r[pdoc.docId] = pdoc;
l[pdoc.pid] = pdoc;
}
// TODO enhance
if (pdocs.length !== pids.length) {
for (const pid of pids) {
if (!(r[pid] || l[pid])) {
if (doThrow) {
throw new ProblemNotFoundError(domainId, pid);
} else {
r[pid] = pdocHidden;
}
}
}
}
return r;
}
export async function getListStatus(domainId: string, uid: number, pids: number[]) {
const psdocs = await getMultiStatus(
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
).toArray();
const r: Dictionary<ProblemStatusDoc> = {};
for (const psdoc of psdocs) r[psdoc.docId] = psdoc;
return r;
}
export async function updateStatus(
domainId: string, pid: number, uid: number,
rid: ObjectID, status: number,
) {
const res = await document.setIfNotStatus(
domainId, document.TYPE_PROBLEM, pid, uid,
'status', status, STATUS.STATUS_ACCEPTED, { rid },
);
return !!res;
}
export function setStar(domainId: string, pid: number, uid: number, star: boolean) {
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
}
export async function setTestdata(domainId: string, _id: number, filePath: string) {
const pdoc = await get(domainId, _id);
const config = await testdataConfig.readConfig(filePath);
const id = await file.add(filePath, 'data.zip', 1);
if (pdoc.data instanceof ObjectID) file.del(pdoc.data);
return await edit(domainId, _id, { data: id, config });
}
global.Hydro.model.problem = {
pdocHidden,
add,
inc,
get,
edit,
count,
random,
getMulti,
getList,
getListStatus,
getMultiStatus,
setStar,
setTestdata,
updateStatus,
};
+189
View File
@@ -0,0 +1,189 @@
import { ObjectID } from 'mongodb';
import { Dictionary } from 'lodash';
import { STATUS } from './builtin';
import * as task from './task';
import * as problem from './problem';
import {
Rdoc, TestCase, PretestConfig, ContestInfo, ProblemConfig,
} from '../interface';
import * as db from '../service/db';
const coll = db.collection('record');
export interface RdocBase {
_id?: ObjectID,
domainId?: string,
pid: number
uid: number,
lang: string,
code: string,
score?: number,
memory?: number,
time?: number,
judgeTexts?: string[],
compilerTexts?: string[],
testCases?: TestCase[],
rejudged?: boolean,
judger?: string,
judgeAt?: Date,
status?: number,
type?: string,
hidden?: boolean,
input?: string,
tid?: ObjectID,
ttype?: number,
}
export interface JudgeTask {
_id: ObjectID,
rid: ObjectID,
domainId: string,
pid: number
lang: string,
code: string,
data?: ObjectID,
config: string,
type?: string,
}
export async function get(domainId: string, _id: ObjectID): Promise<Rdoc | null> {
const res = await coll.findOne({ _id });
if (res && res.domainId === domainId) return res;
return null;
}
export async function judge(domainId: string, rid: ObjectID) {
const rdoc = await get(domainId, rid);
let config: ProblemConfig = rdoc.config;
if (!config) {
const pdoc = await problem.get(domainId, rdoc.pid);
config = pdoc?.config || {};
}
await task.add({
...rdoc,
type: 'judge',
rid,
domainId,
config,
});
}
export async function add(
domainId: string, pid: number, uid: number,
lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | PretestConfig,
) {
const data: Rdoc = {
status: STATUS.STATUS_WAITING,
_id: new ObjectID(),
uid,
code,
lang,
pid,
domainId,
score: 0,
time: 0,
memory: 0,
hidden: !!contestOrConfig,
judgeTexts: [],
compilerTexts: [],
testCases: [],
judger: null,
judgeAt: null,
rejudged: false,
};
if (contestOrConfig) {
if ((contestOrConfig as ContestInfo).type) {
data.contest = contestOrConfig as ContestInfo;
} else {
data.config = contestOrConfig as PretestConfig;
}
}
const res = await coll.insertOne(data);
if (addTask) await judge(domainId, res.insertedId);
return res.insertedId;
}
export function getMulti(domainId: string, query: any) {
return coll.find({ ...query, domainId });
}
export async function update(
domainId: string, _id: ObjectID,
$set: any = {}, $push: any = {}, $unset: any = {},
): Promise<Rdoc | null> {
const $update: any = {};
if ($set && Object.keys($set).length) $update.$set = $set;
if ($push && Object.keys($push).length) $update.$push = $push;
if ($unset && Object.keys($unset).length) $update.$unset = $unset;
if (Object.keys($update).length) {
const res = await coll.findOneAndUpdate(
{ _id, domainId },
$update,
{ returnOriginal: false },
);
return res.value;
}
return await get(domainId, _id);
}
export function reset(domainId: string, rid: ObjectID, isRejudge: boolean) {
const upd: any = {
score: 0,
status: STATUS.STATUS_WAITING,
time: 0,
memory: 0,
testCases: [],
judgeTexts: [],
compilerTexts: [],
judgeAt: null,
judger: null,
};
if (isRejudge) upd.rejudged = true;
return update(domainId, rid, upd);
}
export function count(domainId: string, query: any) {
return coll.find({ domainId, ...query }).count();
}
export async function getList(
domainId: string, rids: ObjectID[], showHidden: boolean,
): Promise<Dictionary<Rdoc>> {
const r = {};
rids = Array.from(new Set(rids));
const rdocs = await coll.find({ domainId, _id: { $in: rids } }).toArray();
for (const rdoc of rdocs) {
if (rdoc.hidden && !showHidden) r[rdoc._id.toHexString()] = null;
else r[rdoc._id.toHexString()] = rdoc;
}
return r;
}
export function getUserInProblemMulti(
domainId: string, uid: number, pid: number,
getHidden = false,
) {
if (!getHidden) {
return coll.find({
domainId, uid, pid, hidden: false,
});
}
return coll.find({ domainId, uid, pid });
}
export function getByUid(domainId: string, uid: number, limit: number): Promise<Rdoc[]> {
return coll.find({ domainId, uid }).limit(limit).toArray();
}
global.Hydro.model.record = {
add,
get,
getMulti,
update,
count,
reset,
getList,
getUserInProblemMulti,
getByUid,
judge,
};
+170
View File
@@ -0,0 +1,170 @@
/* eslint-disable no-await-in-loop */
import moment from 'moment-timezone';
import { Dictionary } from 'lodash';
import * as builtin from './builtin';
import { Setting as _Setting } from '../interface';
type SettingDict = Dictionary<_Setting>;
const countries = moment.tz.countries();
const tzs = new Set();
for (const country of countries) {
const tz = moment.tz.zonesForCountry(country);
for (const t of tz) tzs.add(t);
}
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
export const langRange: Dictionary<string> = {};
for (const lang in global.Hydro.locales) {
langRange[lang] = global.Hydro.locales[lang].__langname;
}
export const FLAG_HIDDEN = 1;
export const FLAG_DISABLED = 2;
export const FLAG_SECRET = 4;
export const PREFERENCE_SETTINGS: _Setting[] = [];
export const ACCOUNT_SETTINGS: _Setting[] = [];
export const DOMAIN_SETTINGS: _Setting[] = [];
export const DOMAIN_USER_SETTINGS: _Setting[] = [];
export const SYSTEM_SETTINGS: _Setting[] = [];
export const SETTINGS: _Setting[] = [];
export const SETTINGS_BY_KEY: SettingDict = {};
export const DOMAIN_USER_SETTINGS_BY_KEY: SettingDict = {};
export const DOMAIN_SETTINGS_BY_KEY: SettingDict = {};
export const SYSTEM_SETTINGS_BY_KEY: SettingDict = {};
export const Setting = (
family: string, key: string, range: Array<[string, string]> | Dictionary<string> = null,
value: any = null, type = 'text', name = '',
desc = '', flag = 0,
): _Setting => ({
family, key, range, value, type, name, desc, flag,
});
export const PreferenceSetting = (...settings: _Setting[]) => {
for (const setting of settings) {
PREFERENCE_SETTINGS.push(setting);
SETTINGS.push(setting);
SETTINGS_BY_KEY[setting.key] = setting;
}
};
export const AccountSetting = (...settings: _Setting[]) => {
for (const setting of settings) {
ACCOUNT_SETTINGS.push(setting);
SETTINGS.push(setting);
SETTINGS_BY_KEY[setting.key] = setting;
}
};
export const DomainUserSetting = (...settings: _Setting[]) => {
for (const setting of settings) {
DOMAIN_USER_SETTINGS.push(setting);
DOMAIN_USER_SETTINGS_BY_KEY[setting.key] = setting;
}
};
export const DomainSetting = (...settings: _Setting[]) => {
for (const setting of settings) {
DOMAIN_SETTINGS.push(setting);
DOMAIN_SETTINGS_BY_KEY[setting.key] = setting;
}
};
export const SystemSetting = (...settings: _Setting[]) => {
for (const setting of settings) {
SYSTEM_SETTINGS.push(setting);
SYSTEM_SETTINGS_BY_KEY[setting.key] = setting;
}
};
PreferenceSetting(
Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'),
Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'),
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, 'c', 'select', 'Default Code Language'),
Setting('setting_usage', 'codeTemplate', null, '', 'textarea', 'Default Code Template',
'If left blank, the built-in template of the corresponding language will be used.'),
);
AccountSetting(
Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email',
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
Setting('setting_info', 'qq', null, null, 'text', 'QQ'),
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, builtin.USER_GENDER_OTHER, 'select', 'Gender'),
Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'),
Setting('setting_customize', 'backgroundImage', null,
'/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image',
'Choose the background image in your profile page.'),
Setting('setting_storage', 'usage', null, 0, 'number', 'Userfile Usage', null, FLAG_DISABLED | FLAG_HIDDEN),
);
DomainSetting(
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
);
DomainUserSetting(
Setting('setting_info', 'displayName', null, null, 'string', 'display name'),
Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('setting_storage', 'rp', null, 1500, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED),
);
SystemSetting(
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET),
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'),
Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'),
Setting('setting_smtp', 'smtp.secure', null, false, 'boolean', 'SSL'),
Setting('setting_server', 'server.name', null, 'Hydro', 'text', 'Server Name'),
Setting('setting_server', 'server.worker', null, 1, 'number', 'Server Workers Number'),
Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'),
Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'),
Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'),
Setting('setting_server', 'server.cdn', null, '/', 'text', 'CDN Prefix', 'Ends with /'),
Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'),
Setting('setting_server', 'server.xff', null, null, 'text', 'IP Header', 'e.g. x-forwarded-for (lowercase)'),
Setting('setting_session', 'session.keys', null, [String.random(32)], 'text', 'session.keys', null, FLAG_HIDDEN),
Setting('setting_session', 'session.secure', null, false, 'boolean', 'session.secure'),
Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'),
Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'),
Setting('setting_storage', 'db.ver', null, 1, 'number', 'Database version', null, FLAG_DISABLED | FLAG_HIDDEN),
Setting('setting_storage', 'user', null, 1, 'number', 'User Counter', null, FLAG_DISABLED | FLAG_HIDDEN),
);
global.Hydro.postInit.push(
async () => {
for (const setting of SYSTEM_SETTINGS) {
if (setting.value) {
const current = await global.Hydro.model.system.get(setting.key);
if (current === null || current === '') {
await global.Hydro.model.system.set(setting.key, setting.value);
}
}
}
},
);
global.Hydro.model.setting = {
langRange,
Setting,
PreferenceSetting,
AccountSetting,
DomainSetting,
DomainUserSetting,
SystemSetting,
FLAG_HIDDEN,
FLAG_DISABLED,
FLAG_SECRET,
PREFERENCE_SETTINGS,
ACCOUNT_SETTINGS,
SETTINGS,
SETTINGS_BY_KEY,
SYSTEM_SETTINGS,
SYSTEM_SETTINGS_BY_KEY,
DOMAIN_SETTINGS,
DOMAIN_SETTINGS_BY_KEY,
DOMAIN_USER_SETTINGS,
DOMAIN_USER_SETTINGS_BY_KEY,
};
+92
View File
@@ -0,0 +1,92 @@
import { ObjectID } from 'mongodb';
import * as document from './document';
import { SolutionNotFoundError } from '../error';
export function add(domainId: string, pid: number, owner: number, content: string) {
return document.add(
domainId, content, owner, document.TYPE_PROBLEM_SOLUTION,
null, document.TYPE_PROBLEM, pid, { reply: [], vote: 0 },
);
}
export async function get(domainId: string, psid: ObjectID) {
const psdoc = await document.get(domainId, document.TYPE_PROBLEM_SOLUTION, psid);
if (!psdoc) throw new SolutionNotFoundError();
return psdoc;
}
export function getMany(domainId: string, query: any, sort: any, page: number, limit: number) {
return document.getMulti(domainId, document.TYPE_PROBLEM_SOLUTION, query)
.sort(sort)
.skip((page - 1) * limit).limit(limit)
.toArray();
}
export function edit(domainId: string, psid: ObjectID, content: string) {
return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content });
}
export function del(domainId: string, psid: ObjectID) {
return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid);
}
export function count(domainId: string, query: any) {
return document.count(domainId, document.TYPE_PROBLEM_SOLUTION, query);
}
export function getMulti(domainId: string, pid: number) {
return document.getMulti(
domainId, document.TYPE_PROBLEM_SOLUTION,
{ parentType: document.TYPE_PROBLEM, parentId: pid },
).sort({ vote: -1 });
}
export function reply(domainId: string, psid: ObjectID, owner: number, content: string) {
return document.push(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', content, owner);
}
export function getReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
return document.getSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid);
}
export function editReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
return document.setSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid, { content });
}
export function delReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
return document.deleteSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid);
}
export async function vote(domainId: string, psid: ObjectID, uid: number, value: number) {
let pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid);
await document.setStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid, { vote: value });
if (pssdoc) value += -pssdoc.vote;
const psdoc = await document.inc(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'vote', value);
pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid);
return [psdoc, pssdoc];
}
export async function getListStatus(domainId: string, psids: ObjectID[], uid: number) {
const result = {};
const res = await document.getMultiStatus(
domainId, document.TYPE_PROBLEM_SOLUTION, { uid, psid: { $in: psids } },
).toArray();
for (const i of res) result[i.psid] = i;
return result;
}
global.Hydro.model.solution = {
count,
add,
get,
edit,
del,
getMany,
getMulti,
reply,
getReply,
editReply,
delReply,
vote,
getListStatus,
};
+66
View File
@@ -0,0 +1,66 @@
import Lru from 'lru-cache';
import * as db from '../service/db';
const coll = db.collection('system');
const cache = new Lru({
maxAge: 5000,
});
export async function get(_id: string) {
const res = cache.get(_id);
if (res !== undefined) return res;
const doc = await coll.findOne({ _id });
if (doc) {
cache.set(_id, doc.value);
return doc.value;
}
return null;
}
export async function getMany(keys: string[]): Promise<any[]> {
const r = [];
let success = true;
for (const key of keys) {
const res = cache.get(key);
if (res !== undefined) r.push(res);
else success = false;
}
if (success) return r;
const docs = await coll.find({ _id: { $in: keys } }).toArray();
const dict = {};
const res = [];
for (const doc of docs) {
dict[doc._id] = doc.value;
}
for (const key of keys) {
res.push(dict[key] || null);
}
return res;
}
export async function set(_id: string, value: any) {
cache.set(_id, value);
const res = await coll.findOneAndUpdate(
{ _id },
{ $set: { value } },
{ upsert: true, returnOriginal: false },
);
return res.value.value;
}
export async function inc(_id: string) {
const res = await coll.findOneAndUpdate(
{ _id },
{ $inc: { value: 1 } },
{ upsert: true, returnOriginal: false },
);
cache.set(_id, res.value.value);
return res.value.value;
}
global.Hydro.model.system = {
get,
getMany,
inc,
set,
};
+97
View File
@@ -0,0 +1,97 @@
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import * as db from '../service/db';
const coll = db.collection('task');
export async function add(task: any) {
const t = { ...task };
if (typeof t.executeAfter === 'object') t.executeAfter = t.executeAfter.getTime();
t.count = t.count || 1;
t.executeAfter = t.executeAfter || new Date().getTime();
const res = await coll.insertOne(t);
return res.insertedId;
}
export function get(_id: ObjectID) {
return coll.findOne({ _id });
}
export function count(query: any) {
return coll.find(query).count();
}
export function del(_id: ObjectID) {
return coll.deleteOne({ _id });
}
export async function getFirst(query: any) {
const q = { ...query };
q.executeAfter = q.executeAfter || { $lt: new Date().getTime() };
const res = await coll.findOneAndDelete(q);
if (res.value) {
if (res.value.interval) {
await coll.insertOne({
...res.value, executeAfter: moment().add(...res.value.interval).toDate(),
});
}
return res.value;
}
return null;
}
export async function consume(query: any, cb: Function) {
let isRunning = false;
const interval = setInterval(async () => {
if (isRunning) return;
isRunning = true;
const res = await getFirst(query);
if (res) {
try {
await cb(res);
} catch (e) {
clearInterval(interval);
}
}
isRunning = false;
}, 100);
}
export class Consumer {
consuming: boolean;
running?: Promise<any>;
interval: NodeJS.Timeout;
filter: any;
func: Function;
constructor(filter: any, func: Function) {
this.consuming = true;
this.filter = filter;
this.func = func;
this.interval = setInterval(this.consume.bind(this), 100);
}
async consume() {
if (this.running || !this.consuming) return;
const res = await getFirst(this.filter);
if (res) {
this.running = this.func(res);
if (this.running instanceof Promise) await this.running;
this.running = null;
}
}
async destory() {
this.consuming = false;
clearInterval(this.interval);
if (this.running) await this.running;
}
}
global.Hydro.model.task = {
Consumer, add, get, del, count, getFirst, consume,
};
+104
View File
@@ -0,0 +1,104 @@
import { TokenDoc } from '../interface';
import * as db from '../service/db';
const coll = db.collection('token');
export const TYPE_SESSION = 0;
export const TYPE_REGISTRATION = 2;
export const TYPE_CHANGEMAIL = 3;
export const TYPE_OAUTH = 4;
export const TYPE_LOSTPASS = 5;
function ensureIndexes() {
return Promise.all([
coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }),
coll.createIndex('expireAt', { expireAfterSeconds: 0 }),
]);
}
export async function add(
tokenType: number, expireSeconds: number, data: any,
): Promise<[string, TokenDoc]> {
const now = new Date();
const str = String.random(32);
const res = await coll.insertOne({
...data,
_id: str,
tokenType,
createAt: now,
updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000),
});
return [str, res.ops[0]];
}
export async function get(tokenId: string, tokenType: number): Promise<TokenDoc | null> {
return await coll.findOne({ _id: tokenId, tokenType });
}
export async function update(
tokenId: string, tokenType: number, expireSeconds: number,
data: object,
) {
const now = new Date();
const res = await coll.findOneAndUpdate(
{ _id: tokenId, tokenType },
{
$set: {
...data,
updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000),
tokenType,
},
},
{ returnOriginal: false },
);
return res.value;
}
export async function del(tokenId: string, tokenType: number) {
const result = await coll.deleteOne({ _id: tokenId, tokenType });
return !!result.deletedCount;
}
export async function createOrUpdate(
tokenType: number, expireSeconds: number, data: any,
): Promise<string> {
const d = await coll.findOne({ tokenType, ...data });
if (!d) {
const res = await add(tokenType, expireSeconds, data);
return res[0];
}
await update(d._id, tokenType, expireSeconds, data);
return d._id;
}
export function getSessionListByUid(uid: number) {
return coll.find({ uid, tokenType: TYPE_SESSION }).sort('updateAt', -1).toArray();
}
export function getMostRecentSessionByUid(uid: number) {
return coll.findOne({ uid, tokenType: TYPE_SESSION }, { sort: { updateAt: -1 } });
}
export function delByUid(uid: number) {
return coll.deleteMany({ uid });
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.token = {
TYPE_SESSION,
TYPE_CHANGEMAIL,
TYPE_OAUTH,
TYPE_REGISTRATION,
TYPE_LOSTPASS,
add,
createOrUpdate,
get,
update,
del,
delByUid,
getMostRecentSessionByUid,
getSessionListByUid,
};
+130
View File
@@ -0,0 +1,130 @@
import { ObjectID } from 'mongodb';
import * as document from './document';
import { TrainingNotFoundError, TrainingAlreadyEnrollError } from '../error';
export function getStatus(domainId: string, tid: ObjectID, uid: number) {
return document.getStatus(domainId, document.TYPE_TRAINING, tid, uid);
}
export function getMultiStatus(domainId: string, query: any) {
return document.getMultiStatus(domainId, document.TYPE_TRAINING, query);
}
export async function getListStatus(domainId: string, uid: number, tids: ObjectID[]) {
const tsdocs = await getMultiStatus(
domainId, { uid, tid: { $in: Array.from(new Set(tids)) } },
).toArray();
const r = {};
for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc;
return r;
}
export async function enroll(domainId: string, tid: ObjectID, uid: number) {
try {
await document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, { enroll: 1 });
} catch (e) {
throw new TrainingAlreadyEnrollError(tid, uid);
}
return await document.inc(domainId, document.TYPE_TRAINING, tid, 'enroll', 1);
}
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
return document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, $set);
}
export function add(
domainId: string, title: string, content: string,
owner: number, dag = [], description = '',
) {
return document.add(domainId, content, owner, document.TYPE_TRAINING, null, null, null, {
dag,
title,
description,
enroll: 0,
});
}
export function edit(domainId: string, tid: ObjectID, $set) {
return document.set(domainId, document.TYPE_TRAINING, tid, $set);
}
export function getPids(tdoc): number[] {
const pids: Set<number> = new Set();
for (const node of tdoc.dag) {
for (const pid of node.pids) pids.add(pid);
}
return Array.from(pids);
}
export function isDone(node, doneNids, donePids) {
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
&& Set.isSuperset(new Set(donePids), new Set(node.pids)));
}
export function isProgress(node, doneNids, donePids, progPids) {
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
&& Set.intersection(
Set.union(new Set(donePids), new Set(progPids)),
new Set(node.pids),
).size);
}
export function isOpen(node, doneNids, donePids, progPids) {
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
&& !Set.intersection(
Set.union(new Set(donePids), new Set(progPids)),
new Set(node.pids),
).size);
}
export const isInvalid = (node, doneNids) =>
!Set.isSuperset(new Set(doneNids), new Set(node.requireNids));
export const count = (domainId: string, query: any) =>
document.count(domainId, document.TYPE_TRAINING, query);
export async function get(domainId, tid) {
const tdoc = await document.get(domainId, document.TYPE_TRAINING, tid);
if (!tdoc) throw new TrainingNotFoundError(tid);
for (const i in tdoc.dag) {
for (const j in tdoc.dag[i].pids) {
if (Number.isSafeInteger(parseInt(tdoc.dag[i].pids[j], 10))) {
tdoc.dag[i].pids[j] = parseInt(tdoc.dag[i].pids[j], 10);
}
}
}
return tdoc;
}
export async function getList(domainId: string, tids: ObjectID[]) {
const tdocs = await this.getMulti(
domainId, { _id: { $in: Array.from(new Set(tids)) } },
).toArray();
const r = {};
for (const tdoc of tdocs) r[tdoc.docId] = tdoc;
return r;
}
export const getMulti = (domainId: string, query: any = {}) =>
document.getMulti(domainId, document.TYPE_TRAINING, query).sort('_id', 1);
global.Hydro.model.training = {
getPids,
isDone,
isProgress,
isOpen,
isInvalid,
add,
edit,
count,
get,
getList,
getMulti,
getMultiStatus,
getStatus,
enroll,
setStatus,
getListStatus,
};
+252
View File
@@ -0,0 +1,252 @@
import * as system from './system';
import * as token from './token';
import * as setting from './setting';
import * as domain from './domain';
import { BUILTIN_USERS, PRIV } from './builtin';
import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error';
import pwhash from '../lib/hash.hydro';
import * as db from '../service/db';
import { User as _User, Udoc, Udict } from '../interface';
const coll = db.collection('user');
export async function setPassword(uid: number, password: string): Promise<Udoc> {
const salt = String.random();
const res = await coll.findOneAndUpdate(
{ _id: uid },
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
{ returnOriginal: false },
);
return res.value;
}
export class User implements _User {
udoc: () => any;
dudoc: () => any;
_id: number;
mail: string;
uname: string;
salt: () => string;
hash: () => string;
hashType: string;
priv: number;
regat: Date;
loginat: Date;
perm: () => bigint;
role: string;
regip: () => string;
loginip: () => string;
[key: string]: any;
constructor(udoc, dudoc) {
this.udoc = () => udoc;
this.dudoc = () => dudoc;
this._id = udoc._id;
this.mail = udoc.mail;
this.uname = udoc.uname;
this.salt = () => udoc.salt;
this.hash = () => udoc.hash;
this.hashType = udoc.hashType || 'hydro';
this.priv = udoc.priv;
this.regat = udoc.regat;
this.regip = () => udoc.regip;
this.loginat = udoc.loginat;
this.loginip = () => udoc.loginip;
this.perm = () => dudoc.perm;
this.role = dudoc.role || 'default';
for (const key in setting.SETTINGS_BY_KEY) {
if (udoc[key]) this[key] = udoc[key];
else if (setting.SETTINGS_BY_KEY[key].value !== null) {
this[key] = setting.SETTINGS_BY_KEY[key].value;
}
}
for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) {
if (dudoc[key]) this[key] = dudoc[key];
else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value !== null) {
this[key] = setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value;
}
}
}
hasPerm(...perm: bigint[]) {
for (const i in perm) {
if ((this.perm() & perm[i]) === perm[i]) return true;
}
return false;
}
hasPriv(...priv: number[]) {
for (const i in priv) {
if ((this.priv & priv[i]) === priv[i]) return true;
}
return false;
}
checkPassword(password: string) {
const h = global.Hydro.lib[`hash.${this.hashType}`];
if (!h) throw new Error('Unknown hash method');
if (!(h(password, this.salt(), this) === this.hash())) {
throw new LoginError(this.uname);
} else if (this.hashType !== 'hydro') {
setPassword(this._id, password);
}
}
}
export async function getById(domainId: string, _id: number): Promise<User | null> {
const udoc = _id === 0 || _id === 1
? BUILTIN_USERS[_id]
: await coll.findOne({ _id });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new User(udoc, dudoc);
}
export async function getList(domainId: string, uids: number[]): Promise<Udict> {
const _uids = new Set(uids);
const r = {};
// eslint-disable-next-line no-await-in-loop
for (const uid of _uids) r[uid] = await getById(domainId, uid);
return r;
}
export async function getByUname(domainId: string, uname: string): Promise<User | null> {
const unameLower = uname.trim().toLowerCase();
const udoc = (unameLower === 'guest')
? BUILTIN_USERS[0]
: unameLower === 'hydro'
? BUILTIN_USERS[1]
: await coll.findOne({ unameLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new User(udoc, dudoc);
}
export async function getByEmail(domainId: string, mail: string): Promise<User | null> {
const mailLower = mail.trim().toLowerCase();
const udoc = (mailLower === 'guest@hydro.local')
? BUILTIN_USERS[0]
: mailLower === 'hydro@hydro.local'
? BUILTIN_USERS[1]
: await coll.findOne({ mailLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new User(udoc, dudoc);
}
export function setById(uid: number, $set = {}, $unset = {}) {
const op: any = {};
if ($set && Object.keys($set).length) op.$set = $set;
if ($unset && Object.keys($unset).length) op.$unset = $unset;
return coll.findOneAndUpdate({ _id: uid }, op);
}
export function setEmail(uid: number, mail: string) {
return setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
}
export async function inc(_id: number, field: string, n = 1) {
const udoc = await coll.findOne({ _id });
if (!udoc) throw new UserNotFoundError(_id);
udoc[field] = udoc[field] + n || n;
await coll.updateOne({ _id }, { $set: { [field]: udoc[field] } });
return udoc;
}
export async function create(
mail: string, uname: string, password: string,
uid: number, regip = '127.0.0.1', priv = PRIV.PRIV_DEFAULT,
) {
const salt = String.random();
if (!uid) uid = await system.inc('user');
try {
await coll.insertOne({
_id: uid,
mail,
mailLower: mail.trim().toLowerCase(),
uname,
unameLower: uname.trim().toLowerCase(),
hash: pwhash(password, salt),
salt,
hashType: 'hydro',
regat: new Date(),
regip,
loginat: new Date(),
loginip: regip,
priv,
gravatar: mail,
});
} catch (e) {
throw new UserAlreadyExistError([uid, uname, mail]);
}
return uid;
}
export function getMulti(params: any) {
return coll.find(params);
}
export async function getPrefixList(prefix: string, limit = 50) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A\\Q${prefix}\\E`, 'gmi');
const udocs = await coll.find({ unameLower: { $regex } }).limit(limit).toArray();
return udocs;
}
export async function setPriv(uid: number, priv: number): Promise<Udoc | null> {
const udoc = await coll.findOneAndUpdate(
{ _id: uid },
{ $set: { priv } },
{ returnOriginal: false },
);
return udoc.value;
}
export function ban(uid: number) {
return Promise.all([
setPriv(uid, PRIV.PRIV_NONE),
token.delByUid(uid),
]);
}
function ensureIndexes() {
return Promise.all([
coll.createIndex('unameLower', { unique: true }),
coll.createIndex('mailLower', { sparse: true }),
]);
}
global.Hydro.postInit.push(ensureIndexes);
global.Hydro.model.user = {
User,
create,
getByEmail,
getById,
getByUname,
getMulti,
inc,
setById,
setEmail,
setPassword,
getPrefixList,
setPriv,
getList,
ban,
};
+20
View File
@@ -0,0 +1,20 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
export default function load() {
let f = path.resolve(process.cwd(), 'config.json');
if (!fs.existsSync(f)) f = path.resolve(__dirname, 'config.json');
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json');
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.hydro', 'config.json');
if (!fs.existsSync(f)) f = path.resolve('/config/config.json');
if (!fs.existsSync(f)) return null;
let result: any = {};
try {
result = JSON.parse(fs.readFileSync(f).toString());
} catch (e) {
console.error('Cannot read config file', e);
result = {};
}
return result;
}
+87
View File
@@ -0,0 +1,87 @@
/* eslint-disable no-use-before-define */
import { ObjectID } from 'mongodb';
import * as document from '../model/document';
import * as discussion from '../model/discussion';
import * as user from '../model/user';
import * as blacklist from '../model/blacklist';
import * as db from '../service/db';
export const description = 'Add blacklist by ip, uid';
async function _address(
ip: string,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (bset.has(ip)) return;
bset.add(ip);
report({ message: `ip ${ip}` });
const users = await db.collection('user').find({ loginip: ip }).toArray();
const tasks = [];
for (const udoc of users) {
tasks.push(_user(udoc._id, bset, uset, dset, dryrun, report));
}
await Promise.all(tasks);
if (!dryrun) await blacklist.add(ip);
}
async function _discussion(
domainId: string, did: ObjectID,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (dset.has(did)) return;
dset.add(did);
const ddoc = await discussion.get(domainId, did);
if (!ddoc) return;
report({ message: `discussion ${ddoc.title}` });
await _user(ddoc.owner, bset, uset, dset, dryrun, report);
if (ddoc.ip) await _address(ddoc.ip, bset, uset, dset, dryrun, report);
if (!dryrun) await discussion.del(domainId, ddoc.docId);
}
async function _user(
uid: number,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (uset.has(uid)) return;
uset.add(uid);
const udoc = await user.getById('system', uid);
if (!udoc) return;
report({ message: `user ${udoc._id} ${udoc.uname}` });
await _address(udoc.loginip(), bset, uset, dset, dryrun, report);
const ddocs = await db.collection('document').find({ docType: document.TYPE_DISCUSSION })
.sort({ domainId: 1, docId: 1 }).toArray();
const tasks = [];
for (const ddoc of ddocs) {
tasks.push(_discussion(ddoc.domainId, ddoc.docId, bset, uset, dset, dryrun, report));
}
await Promise.all(tasks);
if (!dryrun) await user.ban(uid);
}
export async function run({
// eslint-disable-next-line no-shadow
address = null, discuss = null, user = null, dryrun = true,
}, report) {
if (address) await _address(address, new Set(), new Set(), new Set(), dryrun, report);
if (discuss) {
await _discussion(
discuss.domainId, new ObjectID(discuss.did),
new Set(), new Set(), new Set(), dryrun, report,
);
}
if (user) await _user(user, new Set(), new Set(), new Set(), dryrun, report);
}
export const validate = {
$or: {
address: 'string',
discuss: ObjectID,
user: 'number',
},
dryrun: 'boolean',
};
global.Hydro.script.blacklist = { run, description, validate };
+94
View File
@@ -0,0 +1,94 @@
/* eslint-disable no-await-in-loop */
import * as user from '../model/user';
import * as document from '../model/document';
import * as setting from '../model/setting';
import * as db from '../service/db';
import paginate from '../lib/paginate';
import { STATUS, PRIV } from '../model/builtin';
export const description = 'Delete a user';
const collDocument = db.collection('document');
const collDomainUser = db.collection('domain.user');
const collRecord = db.collection('record');
const $unset = {
regip: '', regat: '', loginat: '', loginip: '',
};
for (const s of setting.PREFERENCE_SETTINGS) {
$unset[s.key] = '';
}
async function deleteRecord(uid: number) {
const [, pcount] = await paginate(
collRecord.find(),
1,
100,
);
for (let i = 1; i <= pcount; i++) {
const [rdocs] = await paginate(
collRecord.find(),
1,
100,
);
const tasks = [];
for (const rdoc of rdocs) {
if (rdoc.pid) {
tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nSubmit', -1));
}
if (rdoc.status === STATUS.STATUS_ACCEPTED) {
tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nAccept', -1));
}
}
await Promise.all(tasks);
}
await collRecord.deleteMany({ uid });
}
async function deleteDiscussion(uid: number) {
const [, pcount] = await paginate(
collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }),
1,
100,
);
for (let i = 1; i <= pcount; i++) {
const [ddocs] = await paginate(
collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }),
1,
100,
);
const tasks = [];
for (const ddoc of ddocs) {
tasks.push(document.deleteMulti(ddoc.domainId, document.TYPE_DISCUSSION_REPLY, {
parentId: ddoc.docId,
parentType: document.TYPE_DISCUSSION,
}));
}
await Promise.all(tasks);
}
await collDocument.deleteMany({ docType: document.TYPE_DISCUSSION, owner: uid });
// TODO delete replies
}
export async function run({
uid,
}) {
await deleteRecord(uid);
await deleteDiscussion(uid);
await collDomainUser.deleteMany({ uid });
const uname = String.random(32);
await user.setById(uid, {
uname,
unameLower: uname.toLowerCase(),
priv: PRIV.PRIV_NONE,
}, $unset);
await user.setPassword(uid, String.random(32));
// TODO delete contest, homework, etc
return uid;
}
export const validate = {
uid: 'number',
};
global.Hydro.script.deleteUser = { run, description, validate };
@@ -0,0 +1,49 @@
import * as db from '../service/db';
import { STATUS } from '../model/builtin';
import * as document from '../model/document';
export const description = 'Recalcuates nSubmit and nAccept in problem status.';
export async function run() {
const pipeline = [
{
$match: { hidden: false, type: { $ne: 'run' } },
},
{
$group: {
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
nSubmit: { $sum: 1 },
nAccept: {
$sum: {
$cond: [{ $eq: ['$status', STATUS.STATUS_ACCEPTED] }, 1, 0],
},
},
},
},
{
$group: {
_id: { domainId: '$_id.domainId', pid: '$_id.pid' },
nSubmit: { $sum: '$nSubmit' },
nAccept: { $sum: { $min: ['$nAccept', 1] } },
},
},
];
const bulk = db.collection('document').initializeUnorderedBulkOp();
db.collection('record').aggregate(pipeline).each(
(err, adoc: any) => bulk.find({
domainId: adoc._id.domainId,
docType: document.TYPE_PROBLEM,
docId: adoc._id.pid,
}).updateOne({
$set: {
nSubmit: adoc.nSubmit,
nAccept: adoc.nAccept,
},
}),
);
await bulk.execute();
}
export const validate = {};
global.Hydro.script.problemStat = { run, description, validate };
+148
View File
@@ -0,0 +1,148 @@
/* eslint-disable no-await-in-loop */
import { NumericDictionary } from 'lodash';
import { ObjectID } from 'mongodb';
import { Tdoc, Pdoc } from '../interface';
import * as domain from '../model/domain';
import * as contest from '../model/contest';
import * as problem from '../model/problem';
import * as record from '../model/record';
import { STATUS } from '../model/builtin';
import rating from '../lib/rating';
import paginate from '../lib/paginate';
export const description = 'Calculate rp of a domain, or all domains';
type ND = NumericDictionary<number>
async function runProblem(pdoc: Pdoc, udict: ND): Promise<void>
async function runProblem(domainId: string, pid: number, udict: ND): Promise<void>
async function runProblem(...arg: any[]) {
const pdoc: Pdoc = (typeof arg[0] === 'string')
? await problem.get(arg[0], arg[1], -1)
: arg[0];
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
const [, nPages] = await paginate(
problem.getMultiStatus(
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
),
1,
100,
);
const nAccept = await problem.getMultiStatus(
pdoc.domainId, { docId: pdoc.docId, status: STATUS.STATUS_ACCEPTED },
).count();
const p = (pdoc.difficulty || 5) / (Math.sqrt(Math.sqrt(nAccept)) + 1);
for (let page = 1; page <= nPages; page++) {
const [psdocs] = await paginate(
problem.getMultiStatus(
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
),
page,
100,
);
const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true);
for (const psdoc of psdocs) {
if (rdict[psdoc.rid]) {
const rp = rdict[psdoc.rid].score * p;
udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp;
}
}
}
}
async function runContest(tdoc: Tdoc, udict: ND, report: Function): Promise<void>
async function runContest(
domainId: string, tid: ObjectID, udict: ND, report: Function
): Promise<void>
async function runContest(...arg: any[]) {
const start = new Date().getTime();
const tdoc: Tdoc = (typeof arg[0] === 'string')
? await contest.get(arg[0], arg[1], -1)
: arg[0];
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
const report = (typeof arg[0] === 'string') ? arg[3] : arg[2];
const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType)
.sort(contest.RULES[tdoc.rule].statusSort).toArray();
if (!tsdocs.length) return;
const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs);
const users = [];
for (const result of rankedTsdocs) {
users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 });
}
// FIXME sum(rating.new) always less than sum(rating.old)
const rated = rating(users);
for (const udoc of rated) udict[udoc.uid] = udoc.new;
await report({
case: {
status: STATUS.STATUS_ACCEPTED,
message: `Contest ${tdoc.title} finished`,
time: new Date().getTime() - start,
memory: 0,
score: 0,
},
});
}
async function runInDomain(domainId: string, isSub: boolean, report: Function) {
const udict: ND = {};
const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray();
await report({ message: `Found ${contests.length} contests in ${domainId}` });
for (const i in contests) {
const tdoc = contests[i];
await runContest(tdoc, udict, report);
if (!isSub) {
await report({
progress: Math.floor(((parseInt(i, 10) + 1) / contests.length) * 100),
});
}
}
// TODO pagination
const problems = await problem.getMulti(domainId, { hidden: false }).toArray();
await report({ message: `Found ${problems.length} problems in ${domainId}` });
for (const i in problems) {
const pdoc = problems[i];
await runProblem(pdoc, udict);
if (!isSub) {
await report({
progress: Math.floor(((parseInt(i, 10) + 1) / problems.length) * 100),
});
}
}
await domain.setMultiUserInDomain(domainId, {}, { rp: 1500 });
const tasks = [];
for (const uid in udict) {
tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rp: udict[uid] }));
}
await Promise.all(tasks);
}
export async function run({ domainId }, report: Function) {
if (!domainId) {
const domains = await domain.getMulti().toArray();
await report({ message: `Found ${domains.length} domains` });
for (const i in domains) {
const start = new Date().getTime();
await runInDomain(domains[i]._id, true, report);
await report({
case: {
status: STATUS.STATUS_ACCEPTED,
message: `Domain ${domains[i]._id} finished`,
time: new Date().getTime() - start,
memory: 0,
score: 0,
},
progress: Math.floor(((parseInt(i, 10) + 1) / domains.length) * 100),
});
}
} else await runInDomain(domainId, false, report);
return true;
}
export const validate = {
$or: [
{ domainId: 'string' },
{ domainId: 'undefined' },
],
};
global.Hydro.script.rp = { run, description, validate };
+20
View File
@@ -0,0 +1,20 @@
import * as user from '../model/user';
export const description = 'Create a new user';
export async function run({
uname, password, mail, uid,
}) {
if (uid) uid = parseInt(uid, 10);
if (uid && Number.isNaN(uid)) throw new Error('uid');
uid = await user.create(mail, uname, password, uid);
return uid;
}
export const validate = {
uname: { $type: 'string' },
password: { $type: 'string' },
mail: { $type: 'string' },
};
global.Hydro.script.register = { run, description, validate };
@@ -0,0 +1,18 @@
// TODO add a set_priv interface
import * as user from '../model/user';
import { PRIV } from '../model/builtin';
export const description = 'Set a user as superadmin.';
export async function run({ uid }) {
uid = parseInt(uid, 10);
if (Number.isNaN(uid)) throw new Error('uid');
await user.setPriv(uid, PRIV.PRIV_ALL);
return uid;
}
export const validate = {
uid: { $type: 'number' },
};
global.Hydro.script.setSuperadmin = { run, description, validate };
+13
View File
@@ -0,0 +1,13 @@
import * as domain from '../model/domain';
export const description = 'Upgrade database from 0 to 1';
export async function run() {
const ddoc = await domain.get('system');
if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System');
// TODO discussion node?
}
export const validate = {};
global.Hydro.script.upgrade0_1 = { run, description, validate };
+41
View File
@@ -0,0 +1,41 @@
import cluster from 'cluster';
const bus = {};
export function subscribe(events: string[], handler: any) {
const id = String.random(16);
for (const event of events) {
if (!bus[event]) bus[event] = {};
bus[event][id] = handler;
}
return id;
}
export function unsubscribe(events: string[], id: string) {
for (const event of events) {
if (!bus[event]) bus[event] = {};
delete bus[event][id];
}
}
export function publish(event: string, payload: any, isMaster = true) {
// Process forked by pm2 would also have process.send
if (isMaster && process.send && !cluster.isMaster) {
process.send({
event: 'bus',
eventName: event,
payload,
});
} else {
if (!bus[event]) bus[event] = {};
const funcs = Object.keys(bus[event]);
Promise.all(funcs.map((func) => bus[event][func]({
event,
value: payload,
})));
}
}
global.Hydro.service.bus = {
subscribe, unsubscribe, publish,
};
+32
View File
@@ -0,0 +1,32 @@
import mongodb from 'mongodb';
import MongoWatcher from 'mongodb-watcher';
import * as bus from './bus';
import options from '../options';
const opts = options();
let mongourl = 'mongodb://';
if (opts.username) mongourl += `${opts.username}:${opts.password}@`;
mongourl += `${opts.host}:${opts.port}/${opts.name}`;
// eslint-disable-next-line import/no-mutable-exports
export let db: mongodb.Db = null;
mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then((Client) => {
db = Client.db(opts.name);
global.Hydro.service.db.db = db;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const watcher = new MongoWatcher(db, {
longCursorThreshold: 100,
largeInsertThreshold: 1024 * 30,
largeFetchThreshold: 1024 * 30,
});
bus.publish('system_database_connected', null);
});
export function collection(c: string) {
return db.collection(c);
}
global.Hydro.service.db = { collection, db };
+8
View File
@@ -0,0 +1,8 @@
import { GridFSBucket } from 'mongodb';
import { db } from './db';
export const fs = new GridFSBucket(db);
global.Hydro.service.gridfs = fs;
export default fs;
+40
View File
@@ -0,0 +1,40 @@
import cluster from 'cluster';
import * as db from './db';
import * as sysinfo from '../lib/sysinfo';
const coll = db.collection('status');
export async function update() {
const [mid, $set] = await sysinfo.update();
await coll.updateOne(
{ mid, type: 'server' },
{ $set: { ...$set, updateAt: new Date(), reqCount: global.Hydro.stat.reqCount } },
{ upsert: true },
);
global.Hydro.stat.reqCount = 0;
}
export function updateJudger(args) {
args.type = 'judger';
return coll.updateOne(
{ mid: args.mid, type: 'judger' },
{ $set: args },
{ upsert: true },
);
}
if (cluster.isMaster) {
global.Hydro.postInit.push(
async () => {
const info = await sysinfo.get();
await coll.updateOne(
{ mid: info.mid, type: 'server' },
{ $set: { ...info, updateAt: new Date(), type: 'server' } },
{ upsert: true },
);
setInterval(update, 60 * 1000);
},
);
}
global.Hydro.service.monitor = { update, updateJudger };
+782
View File
@@ -0,0 +1,782 @@
/* eslint-disable prefer-destructuring */
import assert from 'assert';
import path from 'path';
import os from 'os';
import http from 'http';
import moment from 'moment-timezone';
import { isSafeInteger, Dictionary, filter } from 'lodash';
import { ObjectID } from 'mongodb';
import Koa from 'koa';
import morgan from 'koa-morgan';
import Body from 'koa-body';
import Router from 'koa-router';
import cache from 'koa-static-cache';
import sockjs from 'sockjs';
import { SetOption } from 'cookies';
import serialize, { SerializeJSOptions } from 'serialize-javascript';
import { argv } from 'yargs';
import { lrucache } from '../utils';
import { User, DomainDoc } from '../interface';
import {
UserNotFoundError, BlacklistedError, PermissionError,
UserFacingError, ValidationError, PrivilegeError,
CsrfTokenError, InvalidOperationError, MethodNotAllowedError,
NotFoundError, HydroError,
} from '../error';
import hash from '../lib/hash.hydro';
import * as misc from '../lib/misc';
import * as user from '../model/user';
import * as domain from '../model/domain';
import * as system from '../model/system';
import * as blacklist from '../model/blacklist';
import * as token from '../model/token';
import * as opcount from '../model/opcount';
export const app = new Koa();
export const server = http.createServer(app.callback());
export const router = new Router();
type MethodDecorator = (target: any, name: string, obj: any) => any;
type Converter = (value: any) => any;
type Validator = (value: any) => boolean;
interface ParamOption {
name: string,
isOptional?: boolean,
convert?: Converter,
validate?: Validator,
}
// eslint-disable-next-line no-shadow
export enum Types { String, Int, UnsignedInt, Float, ObjectID, Boolean, Date, Time }
const Tools: Array<[Converter, Validator, boolean?]> = [
[(v) => v.toString(), null],
[(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))],
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0],
[(v) => parseFloat(v), (v) => {
const t = parseFloat(v);
console.log(t);
return t && !Number.isNaN(t) && Number.isFinite(t);
}],
[(v) => new ObjectID(v), ObjectID.isValid],
[(v) => !!v, null, true],
[
(v) => {
const d = v.split('-');
assert(d.length === 3);
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
},
(v) => {
const d = v.split('-');
assert(d.length === 3);
const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
return moment(st).isValid();
}],
[
(v) => {
const t = v.split(':');
assert(t.length === 2);
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
},
(v) => {
const t = v.split(':');
assert(t.length === 2);
return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid();
},
],
];
export function param(name: string, type: Types, validate: Validator): MethodDecorator;
export function param(name: string, type?: Types, isOptional?: boolean): MethodDecorator;
export function param(
name: string, type: Types, validate: null, convert: Converter
): MethodDecorator;
export function param(
name: string, type: Types, validate?: Validator, convert?: Converter,
): MethodDecorator;
export function param(
name: string, type: Types, isOptional?: boolean, validate?: Validator, convert?: Converter,
): MethodDecorator;
export function param(
name: string, ...args: Array<Types | boolean | Converter | Validator>
): MethodDecorator;
export function param(name: string, ...args: any): MethodDecorator {
let cursor = 0;
const v: ParamOption = { name };
let isValidate = true;
while (cursor < args.length) {
if (typeof args[cursor] === 'number') {
const type = args[cursor];
if (Tools[type]) {
if (Tools[type][0]) v.convert = Tools[type][0];
if (Tools[type][1]) v.validate = Tools[type][1];
if (Tools[type][2]) v.isOptional = Tools[type][2];
}
} else if (typeof args[cursor] === 'boolean') v.isOptional = args[cursor];
else if (isValidate) {
if (args[cursor] !== null) v.validate = args[cursor];
isValidate = false;
} else {
const I = args[cursor];
v.convert = I;
}
cursor++;
}
return function desc(target: any, funcName: string, obj: any) {
if (!target.__param) target.__param = {};
if (!target.__param[target.constructor.name]) target.__param[target.constructor.name] = {};
if (!target.__param[target.constructor.name][funcName]) {
target.__param[target.constructor.name][funcName] = [{ name: 'domainId', type: 'string' }];
const originalMethod = obj.value;
obj.value = function validate(rawArgs: any) {
const c = [];
const arglist: ParamOption[] = this.__param[target.constructor.name][funcName];
for (const item of arglist) {
if (!item.isOptional || rawArgs[item.name]) {
if (!rawArgs[item.name]) throw new ValidationError(item.name);
if (item.validate) {
if (!item.validate(rawArgs[item.name])) {
throw new ValidationError(item.name);
}
}
if (item.convert) c.push(item.convert(rawArgs[item.name]));
else c.push(rawArgs[item.name]);
} else c.push(undefined);
}
return originalMethod.call(this, ...c);
};
}
target.__param[target.constructor.name][funcName].splice(1, 0, v);
return obj;
};
}
export function multipart(maxSizeKiB: number): MethodDecorator {
return function multipartDecorator(target: any, funcName: string, obj: any) {
const originalMethod = obj.value;
obj.value = async function checkCsrfToken(...args: any[]) {
const body = Body({
multipart: true,
formidable: {
maxFileSize: maxSizeKiB * 1024,
},
});
return await body.call(this.ctx, this.ctx, () => originalMethod.call(this, ...args));
};
return obj;
};
}
export function requireCsrfToken(target: any, funcName: string, obj: any) {
const originalMethod = obj.value;
obj.value = async function checkCsrfToken(...args: any[]) {
if (this.getCsrfToken(this.session._id) !== this.args.csrfToken) {
throw new CsrfTokenError(this.args.csrfToken);
}
return await originalMethod.call(this, ...args);
};
return obj;
}
export async function prepare() {
app.keys = await system.get('session.keys');
if (argv.public) {
app.use(cache(argv.public, {
maxAge: 0,
}));
} else {
app.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), {
maxAge: 365 * 24 * 60 * 60,
}));
}
app.use(Body());
}
export class Handler {
UIContext: any;
args: any;
ctx: Koa.Context;
request: {
host: string,
hostname: string,
ip: string,
headers: any,
cookies: any,
body: any,
files: any,
query: any,
path: string,
params: any,
referer: string,
json: boolean,
};
response: {
body: any,
type: string,
status: number,
template?: string,
redirect?: string,
disposition?: string,
attachment: (name: string) => void,
};
session: any;
csrfToken: string;
user: User;
domain: DomainDoc;
loginMethods: any;
constructor(ctx: Koa.Context) {
this.ctx = ctx;
this.request = {
host: ctx.request.host,
hostname: ctx.request.hostname,
ip: ctx.request.ip,
headers: ctx.request.headers,
cookies: ctx.cookies,
body: ctx.request.body,
files: ctx.request.files,
query: ctx.query,
path: ctx.path,
params: ctx.params,
referer: ctx.request.headers.referer || '/',
json: (ctx.request.headers.accept || '').includes('application/json'),
};
this.response = {
body: {},
type: '',
status: null,
template: null,
redirect: null,
attachment: (name) => ctx.attachment(name),
disposition: null,
};
this.UIContext = {
cdn_prefix: '/',
url_prefix: '/',
};
this.session = {};
}
@lrucache
// eslint-disable-next-line class-methods-use-this
getCsrfToken(id: string) {
return hash('csrf_token', id);
}
async renderHTML(name: string, context: any): Promise<string> {
const UserContext = {
...this.user,
gravatar: misc.gravatar(this.user.gravatar || '', 128),
perm: this.user.perm.toString(),
};
if (!global.Hydro.lib.template) return JSON.stringify(context);
const res = await global.Hydro.lib.template.render(name, {
handler: this,
UserContext,
url: this.url.bind(this),
_: this.translate.bind(this),
...context,
});
return res;
}
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str: string) {
if (!str) return '';
return str.toString().translate(this.user.viewLang, this.session.viewLang);
}
renderTitle(str: string) {
return `${this.translate(str)} - Hydro`;
}
checkPerm(...args: bigint[]) {
// @ts-ignore
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs = {}) {
let res = '#';
const args: any = { ...kwargs };
try {
if (this.args.domainId !== 'system' || args.domainId) {
name += '_with_domainId';
args.domainId = args.domainId || this.args.domainId;
}
const { anchor, query } = args;
if (query) res = router.url(name, args, { query });
else res = router.url(name, args);
if (anchor) return `${res}#${anchor}`;
} catch (e) {
console.error(e.message);
console.log(name, args);
}
return res;
}
async render(name: string, context: any) {
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
}
back(body?: any) {
this.response.body = body || this.response.body || {};
this.response.redirect = this.request.headers.referer || '/';
}
binary(data: any, name: string) {
this.response.body = data;
this.response.template = null;
this.response.type = 'application/octet-stream';
this.response.disposition = `attachment; filename="${name}"`;
}
async getSession() {
const sid = this.request.cookies.get('sid');
this.session = await token.get(sid, token.TYPE_SESSION);
if (!this.session) this.session = { uid: 0 };
}
async getBdoc() {
const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip);
}
async init({ domainId }) {
const xff = await system.get('server.xff');
if (xff) this.request.ip = this.request.headers[xff.toLowerCase()];
[this.domain] = await Promise.all([
domain.get(domainId),
this.getSession(),
this.getBdoc(),
]);
if (!this.domain) {
this.args.domainId = 'system';
this.user = await user.getById('system', this.session.uid);
if (!this.user) this.user = await user.getById('system', 0);
throw new NotFoundError(domainId);
}
this.user = await user.getById(domainId, this.session.uid);
if (!this.user) {
this.session.uid = 0;
this.user = await user.getById(domainId, this.session.uid);
}
this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
this.UIContext.csrfToken = this.csrfToken;
this.loginMethods = filter(Object.keys(global.Hydro.lib), (str) => str.startsWith('oauth_'))
.map((key) => ({
id: key.split('_')[1],
icon: global.Hydro.lib[key].icon,
text: global.Hydro.lib[key].text,
}));
}
async finish() {
try {
await this.renderBody();
} catch (error) {
this.response.status = error instanceof UserFacingError ? error.code : 500;
if (this.request.json) this.response.body = { error };
else {
try {
await this.render(error instanceof UserFacingError ? 'error.html' : 'bsod.html', { error });
} catch (e) {
console.error(e);
// this.response.body.error = {};
}
}
}
await this.putResponse();
await this.saveCookie();
}
async renderBody() {
if (this.response.redirect) {
this.response.body = this.response.body || {};
this.response.body.url = this.response.redirect;
}
if (this.response.type) return;
if (
this.request.json || this.response.redirect
|| this.request.query.noTemplate || !this.response.template) {
try {
this.response.body = JSON.stringify(this.response.body);
} catch (e) {
const opt: SerializeJSOptions = { ignoreFunction: true };
if (this.request.query.noTemplate) opt.space = 2;
this.response.body = serialize(this.response.body, opt);
}
this.response.type = 'application/json';
} else if (this.response.body || this.response.template) {
const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {};
await this.render(templateName, this.response.body);
}
}
}
async putResponse() {
if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition);
if (this.response.redirect && !this.request.json) {
this.ctx.response.type = 'application/octet-stream';
this.ctx.response.status = 302;
this.ctx.redirect(this.response.redirect);
} else {
if (this.response.body != null) {
this.ctx.response.body = this.response.body;
this.ctx.response.status = this.response.status || 200;
}
this.ctx.response.type = this.request.json
? 'application/json'
: this.response.type
? this.response.type
: this.ctx.response.type;
}
}
async saveCookie() {
const expireSeconds = this.session.save
? await system.get('session.expire_seconds')
: await system.get('session.unsaved_expire_seconds');
if (this.session._id) {
await token.update(
this.session._id,
token.TYPE_SESSION,
expireSeconds,
{
...this.session,
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
},
);
} else {
[, this.session] = await token.add(
token.TYPE_SESSION,
expireSeconds,
{
...this.session,
createIp: this.request.ip,
createUa: this.request.headers['user-agent'] || '',
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
},
);
}
const cookie: SetOption = {
secure: await system.get('session.secure'),
httpOnly: false,
};
if (this.session.save) {
cookie.expires = this.session.expireAt;
cookie.maxAge = expireSeconds;
}
this.ctx.cookies.set('sid', this.session._id, cookie);
}
async onerror(error: HydroError) {
if (!error.msg) error.msg = () => error.message;
console.error(error.msg(), error.params);
console.error(error.stack);
this.response.status = error instanceof UserFacingError ? error.code : 500;
this.response.template = error instanceof UserFacingError ? 'error.html' : 'bsod.html';
this.response.body = {
error: { message: error.msg(), params: error.params, stack: error.stack },
};
await this.finish().catch(() => { });
}
}
async function handle(ctx, HandlerClass, checker) {
global.Hydro.stat.reqCount++;
const args = {
domainId: 'system', ...ctx.params, ...ctx.query, ...ctx.request.body,
};
const h = new HandlerClass(ctx);
h.args = args;
h.domainId = args.domainId;
try {
const method = ctx.method.toLowerCase();
let operation: string;
if (method === 'post' && ctx.request.body.operation) {
operation = `_${ctx.request.body.operation}`
.replace(/_([a-z])/gm, (s) => s[1].toUpperCase());
}
await h.init(args);
if (checker) checker.call(h);
if (method === 'post') {
if (operation) {
if (typeof h[`post${operation}`] !== 'function') {
throw new InvalidOperationError(operation);
}
} else if (typeof h.post !== 'function') {
throw new MethodNotAllowedError(method);
}
} else if (typeof h[method] !== 'function' && typeof h.all !== 'function') {
throw new MethodNotAllowedError(method);
}
if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args);
if (h[method]) await h[method](args);
if (operation) await h[`post${operation}`](args);
if (h.cleanup) await h.cleanup(args);
if (h.finish) await h.finish(args);
} catch (e) {
try {
await h.onerror(e);
} catch (err) {
h.response.code = 500;
h.response.body = `${err.message}\n${err.stack}`;
}
}
}
const Checker = (permPrivChecker) => {
let perm: bigint;
let priv: number;
let checker = () => { };
for (const item of permPrivChecker) {
if (typeof item === 'object') {
if (typeof item.call !== 'undefined') {
checker = item;
} if (typeof item[0] === 'number') {
priv = item;
} else if (typeof item[0] === 'bigint') {
perm = item;
}
} else if (typeof item === 'number') {
priv = item;
} else if (typeof item === 'bigint') {
perm = item;
}
}
return function check() {
checker();
if (perm) this.checkPerm(perm);
if (priv) this.checkPriv(priv);
};
};
export function Route(name: string, route: string, RouteHandler: any, ...permPrivChecker) {
const checker = Checker(permPrivChecker);
router.all(name, route, (ctx) => handle(ctx, RouteHandler, checker));
router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker));
}
export class ConnectionHandler {
conn: sockjs.Connection;
request: {
params: any
headers: any
ip: string
}
session: any
args: any
user: any
domain: DomainDoc
constructor(conn: sockjs.Connection) {
this.conn = conn;
this.request = {
params: {},
headers: conn.headers,
ip: this.conn.remoteAddress,
};
this.session = {};
const p: any = (conn.url.split('?')[1] || '').split('&');
for (const i in p) p[i] = p[i].split('=');
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
}
async renderHTML(name: string, context: any): Promise<string> {
const res = await global.Hydro.lib.template.render(name, Object.assign(context, {
handler: this,
url: this.url.bind(this),
_: this.translate.bind(this),
}));
return res;
}
async limitRate(op: string, periodSecs: number, maxOperations: number) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str: string) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
}
renderTitle(str: string) {
return `${this.translate(str)} - Hydro`;
}
checkPerm(...args: bigint[]) {
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
}
checkPriv(...args: number[]) {
// @ts-ignore
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
}
url(name: string, kwargs = {}) {
let res = '#';
const args: any = { ...kwargs };
try {
if (this.args.domainId !== 'system' || args.domainId) {
name += '_with_domainId';
args.domainId = args.domainId || this.args.domainId;
}
const { anchor, query } = args;
if (query) res = router.url(name, args, { query });
else res = router.url(name, args);
if (anchor) return `${res}#${anchor}`;
} catch (e) {
console.error(e.message);
console.log(name, args);
}
return res;
}
send(data: any) {
this.conn.write(JSON.stringify(data));
}
close(code: number, reason: string) {
this.conn.close(code.toString(), reason);
}
async message(message: any) { } // eslint-disable-line
onerror(err: HydroError) {
console.error(err);
this.send({
error: {
name: err.name,
params: err.params || [],
},
});
this.close(1001, err.toString());
}
async getSession(cookieHeader: string) {
const cookies: any = {};
const ref = cookieHeader.split(';');
for (let j = 0; j < ref.length; j++) {
const cookie = ref[j];
const parts = cookie.split('=');
cookies[parts[0].trim()] = (parts[1] || '').trim();
}
this.session = await token.get(cookies.sid || '', token.TYPE_SESSION);
if (!this.session) this.session = { uid: 0, domainId: 'system' };
}
@param('cookie', Types.String)
async init(domainId: string, cookie: string) {
[this.domain] = await Promise.all([
domain.get(domainId),
this.getSession(cookie),
]);
const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip);
this.user = await user.getById(domainId, this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid);
}
}
export function Connection(
name: string, prefix: string,
RouteConnHandler: any,
...permPrivChecker: Array<number | bigint | Function>
) {
const log = argv.debug ? console.log : () => { };
const sock = sockjs.createServer({ prefix, log });
const checker = Checker(permPrivChecker);
sock.on('connection', async (conn) => {
const h: Dictionary<any> = new RouteConnHandler(conn);
try {
const args = { domainId: 'system', ...h.request.params };
h.args = args;
const cookie = await new Promise((resolve) => {
conn.once('data', (c) => {
resolve(c);
});
});
args.cookie = cookie;
await h.init(args);
conn.write(JSON.stringify({ event: 'auth' }));
checker.call(h);
if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args);
if (h.message) {
conn.on('data', (data) => {
h.message(JSON.parse(data));
});
}
conn.on('close', async () => {
if (h.cleanup) await h.cleanup(args);
if (h.finish) await h.finish(args);
});
} catch (e) {
console.log(e);
await h.onerror(e);
}
});
sock.installHandlers(server);
}
// TODO use postInit?
export async function start() {
const port = await system.get('server.port');
if (argv.debug) {
app.use(morgan(':method :url :status :res[content-length] - :response-time ms', {
skip: (req, res) => res.hasHeader('nolog'),
}));
}
app.use(router.routes()).use(router.allowedMethods());
Route('notfound_handler', '*', Handler);
server.listen(argv.port || port);
console.log('Server listening at: %s', argv.port || port);
}
global.Hydro.service.server = {
Types,
app,
server,
router,
param,
multipart,
requireCsrfToken,
Handler,
ConnectionHandler,
Route,
Connection,
prepare,
start,
};
+220
View File
@@ -0,0 +1,220 @@
import fs from 'fs';
import cluster from 'cluster';
import path from 'path';
import * as superagent from 'superagent';
import proxyAgent from 'proxy-agent';
import serialize from 'serialize-javascript';
import Lru from 'lru-cache';
declare global {
interface StringConstructor {
random: (digit?: number) => string;
}
interface ArrayConstructor {
isDiff: (a: any[], b: any[]) => boolean;
}
interface Date {
format: (fmt?: string) => string;
}
interface Math {
sum: (...args: Array<number[] | number>) => number;
}
interface SetConstructor {
isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
intersection: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
union: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
}
}
const lru = new Lru(1000);
export function lrucache(target: any, funcName: string, obj: any) {
const originalMethod = obj.value;
obj.value = function func(...args: any[]) {
const key = serialize({ funcName, args });
let val = lru.get(key);
if (val) return val;
val = originalMethod.call(this, ...args);
lru.set(key, val);
return val;
};
return obj;
}
namespace Proxy {
function getUrl() {
return this._url;
}
function setUrl(v: string) {
this._url = v;
// eslint-disable-next-line no-use-before-define
proxy.call(this, this._proxyUri);
}
function setupUrl(req: superagent.Request) {
const desc = Object.getOwnPropertyDescriptor(req, 'url');
if (desc.get === getUrl && desc.set === setUrl) return;
// @ts-ignore
req._url = req.url;
desc.get = getUrl;
desc.set = setUrl;
delete desc.value;
delete desc.writable;
Object.defineProperty(req, 'url', desc);
}
function proxy(uri: string) {
if (!uri) return this;
setupUrl(this);
const agent = proxyAgent(uri);
if (agent) this.agent(agent);
this._proxyUri = uri;
return this;
}
export function setup(agent, uri = '') {
const Request = agent.Request;
if (Request) {
Request.prototype.proxy = proxy;
return agent;
}
return proxy.call(agent, uri);
}
}
declare module 'superagent' {
interface SuperAgentRequest {
proxy(url: string): this;
}
}
Proxy.setup(superagent);
if (!cluster.worker) {
// @ts-ignore
cluster.worker = { id: 0 };
}
const dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
String.random = function random(digit = 32) {
let str = '';
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * 62)];
return str;
};
Array.isDiff = function isDiff(a, b) {
if (a.length !== b.length) return true;
a.sort();
b.sort();
for (const i in a) {
if (a[i] !== b[i]) return true;
}
return false;
};
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
let m = this.getMonth() + 1;
if (m < 10) m = `0${m}`;
let d = this.getDate();
if (d < 10) d = `0${d}`;
let H = this.getHours();
if (H < 10) H = `0${H}`;
let M = this.getMinutes();
if (M < 10) M = `0${M}`;
let S = this.getSeconds();
if (S < 10) S = `0${S}`;
return fmt
.replace('%Y', this.getFullYear())
.replace('%m', m)
.replace('%d', d)
.replace('%H', H)
.replace('%M', M)
.replace('%S', S);
};
Math.sum = function sum(...args) {
let s = 0;
for (const i of args) {
if (i instanceof Array) {
for (const j of i) {
s += j;
}
} else s += i;
}
return s;
};
Set.isSuperset = function isSuperset(set, subset) {
for (const elem of subset) {
if (!set.has(elem)) return false;
}
return true;
};
Set.union = function Union<T>(setA: Set<T>, setB: Set<T>) {
const union = new Set(setA);
for (const elem of setB) union.add(elem);
return union;
};
Set.intersection = function Intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const intersection = new Set();
for (const elem of setB) {
if (setA.has(elem)) intersection.add(elem);
}
// @ts-ignore
return intersection;
};
export function folderSize(folderPath: string) {
let size = 0;
const _next = function a(p: string) {
if (p) {
const stats = fs.statSync(p);
if (!stats.isDirectory() || stats.isSymbolicLink()) {
if (!stats.isSymbolicLink()) size += stats.size;
} else {
size += stats.size;
const files = fs.readdirSync(p);
if (Array.isArray(files)) {
files.forEach((file) => {
_next(path.join(p, file));
});
}
}
}
};
_next(folderPath);
return size;
}
const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i;
const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
const MEMORY_UNITS = { k: 0.1, m: 1, g: 1024 };
export function parseTimeMS(str: string) {
const match = TIME_RE.exec(str);
if (!match) throw new Error(`${str} error parsing time`);
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]);
}
export function parseMemoryMB(str: string) {
const match = MEMORY_RE.exec(str);
if (!match) throw new Error(`${str} error parsing memory`);
return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]);
}
export function isClass(obj: any, strict = false) {
if (typeof obj !== 'function') return false;
const str = obj.toString();
if (obj.prototype === undefined) return false;
if (obj.prototype.constructor !== obj) return false;
if (str.slice(0, 5) === 'class') return true;
if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true;
if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false;
if (strict && /^function\s+[A-Z]/.test(str)) return true;
if (/\b\(this\b|\bthis[.[]\b/.test(str)) {
if (!strict || /classCallCheck\(this/.test(str)) return true;
return /^function\sdefault_\d+\s*\(/.test(str);
}
return false;
}