mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 09:46:28 +08:00
文件结构更改
This commit is contained in:
1 parent
b3af28dc43
commit
1f1d12ad73
109 files changed
+544
-3296
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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.'
|
||||
*/
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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')
|
||||
|
||||
*/
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user