From ba03d7a7bf43f5fdbf16065055aec1fa40abd46e Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 7 Jul 2020 19:58:21 +0800 Subject: [PATCH] Hello Typescript! --- .eslintrc.js | 39 +- .github/workflows/build.yml | 3 +- .gitignore | 1 + build/webpack.js | 2 +- hydro/{check.js => check.ts} | 15 +- hydro/declare.ts | 28 + hydro/entry/{common.js => common.ts} | 47 +- hydro/entry/{master.js => master.ts} | 14 +- hydro/entry/{setup.js => setup.ts} | 27 +- hydro/entry/{unzip.js => unzip.ts} | 17 +- hydro/entry/{worker.js => worker.ts} | 12 +- hydro/error.js | 179 ------ hydro/error.ts | 200 +++++++ hydro/handler/{contest.js => contest.ts} | 83 ++- .../handler/{discussion.js => discussion.ts} | 63 +- hydro/handler/{domain.js => domain.ts} | 26 +- hydro/handler/{home.js => home.ts} | 90 ++- hydro/handler/{homework.js => homework.ts} | 218 ++----- hydro/handler/{import.js => import.ts} | 20 +- hydro/handler/{judge.js => judge.ts} | 52 +- hydro/handler/{manage.js => manage.ts} | 21 +- hydro/handler/{misc.js => misc.ts} | 22 +- hydro/handler/{notfound.js => notfound.ts} | 8 +- hydro/handler/{problem.js => problem.ts} | 130 +++-- hydro/handler/{record.js => record.ts} | 36 +- hydro/handler/{training.js => training.ts} | 65 +-- hydro/handler/{user.js => user.ts} | 63 +- hydro/handler/{wiki.js => wiki.ts} | 8 +- hydro/interface.js | 77 --- hydro/interface.ts | 71 --- hydro/lib/download.js | 19 - hydro/lib/download.ts | 19 + hydro/lib/hash.hydro.js | 7 - hydro/lib/hash.hydro.ts | 7 + hydro/lib/{hpm.js => hpm.ts} | 25 +- hydro/lib/{i18n.js => i18n.ts} | 14 +- hydro/lib/{jwt.js => jwt.ts} | 11 +- hydro/lib/{logger.js => logger.ts} | 32 +- hydro/lib/{mail.js => mail.ts} | 10 +- hydro/lib/{markdown.js => markdown.ts} | 33 +- hydro/lib/{md5.js => md5.ts} | 6 +- hydro/lib/{misc.js => misc.ts} | 22 +- hydro/lib/{nav.js => nav.ts} | 36 +- hydro/lib/{paginate.js => paginate.ts} | 13 +- hydro/lib/{rank.js => rank.ts} | 3 +- hydro/lib/{rating.js => rating.ts} | 17 +- hydro/lib/{readConfig.js => readConfig.ts} | 7 +- hydro/lib/sha1.js | 10 - hydro/lib/sha1.ts | 10 + hydro/lib/{sysinfo.js => sysinfo.ts} | 14 +- hydro/lib/{template.js => template.ts} | 14 +- ...convert.ini.js => testdata.convert.ini.ts} | 14 +- hydro/lib/useragent.js | 11 - hydro/lib/useragent.ts | 12 + hydro/lib/validator.js | 91 --- hydro/lib/validator.ts | 91 +++ hydro/{loader.js => loader.ts} | 53 +- hydro/model/{blacklist.js => blacklist.ts} | 12 +- hydro/model/{builtin.js => builtin.ts} | 74 ++- hydro/model/{contest.js => contest.ts} | 360 +++++++++--- hydro/model/{discussion.js => discussion.ts} | 81 +-- hydro/model/{document.js => document.ts} | 140 +++-- hydro/model/{domain.js => domain.ts} | 54 +- hydro/model/file.js | 83 --- hydro/model/file.ts | 104 ++++ hydro/model/{message.js => message.ts} | 23 +- hydro/model/{opcount.js => opcount.ts} | 16 +- hydro/model/problem.js | 230 -------- hydro/model/problem.ts | 194 ++++++ hydro/model/record.js | 161 ----- hydro/model/record.ts | 233 ++++++++ hydro/model/{setting.js => setting.ts} | 44 +- hydro/model/{solution.js => solution.ts} | 40 +- hydro/model/{system.js => system.ts} | 20 +- hydro/model/{task.js => task.ts} | 25 +- hydro/model/token.js | 121 ---- hydro/model/token.ts | 103 ++++ hydro/model/training.js | 107 ---- hydro/model/training.ts | 130 +++++ hydro/model/{user.js => user.ts} | 124 ++-- hydro/{options.js => options.ts} | 25 +- hydro/script/blacklist.js | 2 +- hydro/script/install.js | 47 +- hydro/service/{bus.js => bus.ts} | 14 +- hydro/service/db.js | 22 - hydro/service/db.ts | 23 + hydro/service/gridfs.js | 6 - hydro/service/gridfs.ts | 8 + hydro/service/{monitor.js => monitor.ts} | 8 +- hydro/service/{server.js => server.ts} | 210 ++++--- hydro/{utils.js => utils.ts} | 101 ++-- package.json | 45 +- templates/homework_main.html | 2 +- templates/main.html | 2 +- .../partials/problem_sidebar_homework.html | 2 +- tsconfig.json | 16 + wiki/admin/deploy.md | 2 +- yarn.lock | 550 +++++++++++++++--- 98 files changed, 3186 insertions(+), 2616 deletions(-) rename hydro/{check.js => check.ts} (86%) create mode 100644 hydro/declare.ts rename hydro/entry/{common.js => common.ts} (86%) rename hydro/entry/{master.js => master.ts} (90%) rename hydro/entry/{setup.js => setup.ts} (93%) rename hydro/entry/{unzip.js => unzip.ts} (94%) rename hydro/entry/{worker.js => worker.ts} (93%) delete mode 100644 hydro/error.js create mode 100644 hydro/error.ts rename hydro/handler/{contest.js => contest.ts} (86%) rename hydro/handler/{discussion.js => discussion.ts} (87%) rename hydro/handler/{domain.js => domain.ts} (89%) rename hydro/handler/{home.js => home.ts} (82%) rename hydro/handler/{homework.js => homework.ts} (63%) rename hydro/handler/{import.js => import.ts} (78%) rename hydro/handler/{judge.js => judge.ts} (81%) rename hydro/handler/{manage.js => manage.ts} (93%) rename hydro/handler/{misc.js => misc.ts} (81%) rename hydro/handler/{notfound.js => notfound.ts} (60%) rename hydro/handler/{problem.js => problem.ts} (83%) rename hydro/handler/{record.js => record.ts} (86%) rename hydro/handler/{training.js => training.ts} (85%) rename hydro/handler/{user.js => user.ts} (90%) rename hydro/handler/{wiki.js => wiki.ts} (80%) delete mode 100644 hydro/interface.js delete mode 100644 hydro/interface.ts delete mode 100644 hydro/lib/download.js create mode 100644 hydro/lib/download.ts delete mode 100644 hydro/lib/hash.hydro.js create mode 100644 hydro/lib/hash.hydro.ts rename hydro/lib/{hpm.js => hpm.ts} (79%) rename hydro/lib/{i18n.js => i18n.ts} (79%) rename hydro/lib/{jwt.js => jwt.ts} (75%) rename hydro/lib/{logger.js => logger.ts} (50%) rename hydro/lib/{mail.js => mail.ts} (82%) rename hydro/lib/{markdown.js => markdown.ts} (81%) rename hydro/lib/{md5.js => md5.ts} (97%) rename hydro/lib/{misc.js => misc.ts} (81%) rename hydro/lib/{nav.js => nav.ts} (56%) rename hydro/lib/{paginate.js => paginate.ts} (50%) rename hydro/lib/{rank.js => rank.ts} (82%) rename hydro/lib/{rating.js => rating.ts} (91%) rename hydro/lib/{readConfig.js => readConfig.ts} (76%) delete mode 100644 hydro/lib/sha1.js create mode 100644 hydro/lib/sha1.ts rename hydro/lib/{sysinfo.js => sysinfo.ts} (86%) rename hydro/lib/{template.js => template.ts} (81%) rename hydro/lib/{testdata.convert.ini.js => testdata.convert.ini.ts} (73%) delete mode 100644 hydro/lib/useragent.js create mode 100644 hydro/lib/useragent.ts delete mode 100644 hydro/lib/validator.js create mode 100644 hydro/lib/validator.ts rename hydro/{loader.js => loader.ts} (83%) rename hydro/model/{blacklist.js => blacklist.ts} (65%) rename hydro/model/{builtin.js => builtin.ts} (92%) rename hydro/model/{contest.js => contest.ts} (53%) rename hydro/model/{discussion.js => discussion.ts} (68%) rename hydro/model/{document.js => document.ts} (68%) rename hydro/model/{domain.js => domain.ts} (59%) delete mode 100644 hydro/model/file.js create mode 100644 hydro/model/file.ts rename hydro/model/{message.js => message.ts} (61%) rename hydro/model/{opcount.js => opcount.ts} (62%) delete mode 100644 hydro/model/problem.js create mode 100644 hydro/model/problem.ts delete mode 100644 hydro/model/record.js create mode 100644 hydro/model/record.ts rename hydro/model/{setting.js => setting.ts} (87%) rename hydro/model/{solution.js => solution.ts} (64%) rename hydro/model/{system.js => system.ts} (65%) rename hydro/model/{task.js => task.ts} (73%) delete mode 100644 hydro/model/token.js create mode 100644 hydro/model/token.ts delete mode 100644 hydro/model/training.js create mode 100644 hydro/model/training.ts rename hydro/model/{user.js => user.ts} (71%) rename hydro/{options.js => options.ts} (55%) rename hydro/service/{bus.js => bus.ts} (75%) delete mode 100644 hydro/service/db.js create mode 100644 hydro/service/db.ts delete mode 100644 hydro/service/gridfs.js create mode 100644 hydro/service/gridfs.ts rename hydro/service/{monitor.js => monitor.ts} (80%) rename hydro/service/{server.js => server.ts} (78%) rename hydro/{utils.js => utils.ts} (63%) create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 459254b0b..8176c19d9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,6 @@ module.exports = { root: true, env: { - commonjs: true, node: true, }, extends: [ @@ -11,23 +10,37 @@ module.exports = { Atomics: 'readonly', SharedArrayBuffer: 'readonly', }, - parserOptions: { - ecmaVersion: 2018, - }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], rules: { + '@typescript-eslint/no-unused-vars': 'error', + 'global-require': 'off', + 'guard-for-in': 'off', + 'implicit-arrow-linebreak': 'off', + 'import/extensions': 'off', + 'import/prefer-default-export': 'off', indent: ['warn', 4], - 'no-plusplus': 'off', + 'max-classes-per-file': 'off', + 'no-bitwise': 'off', 'no-console': 'off', 'no-extend-native': 'off', - 'no-underscore-dangle': 'off', - 'no-restricted-syntax': 'off', - 'max-classes-per-file': 'off', - 'guard-for-in': 'off', - 'no-param-reassign': 'off', - 'global-require': 'off', - 'no-nested-ternary': 'off', 'no-multi-assign': 'off', + 'no-nested-ternary': 'off', + 'no-param-reassign': 'off', + 'no-plusplus': 'off', + 'no-restricted-syntax': 'off', 'no-return-await': 'off', - 'no-bitwise': 'off', + 'no-underscore-dangle': 'off', + 'no-unused-vars': 'off', + }, + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts'], + }, + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, }, }; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff1418b8b..4392a9525 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,8 @@ jobs: path: .uibuild - run: | yarn - yarn build:production + yarn build + yarn pack:production - uses: actions/upload-artifact@v1 with: name: app.js diff --git a/.gitignore b/.gitignore index f5214ea48..6b85c108f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ __* ui/misc/.iconfont ui/static/locale *.log +*.hydro !.gitkeep diff --git a/build/webpack.js b/build/webpack.js index 3f4436c11..5abf38f23 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -7,7 +7,7 @@ const build = async (type) => { const config = { mode: type, entry: { - app: root('hydro/loader.js'), + app: root('dist/loader.js'), }, output: { filename: '[name].js', diff --git a/hydro/check.js b/hydro/check.ts similarity index 86% rename from hydro/check.js rename to hydro/check.ts index f0ab293aa..8f314ed6c 100644 --- a/hydro/check.js +++ b/hydro/check.ts @@ -1,9 +1,9 @@ // TODO check more -const os = require('os'); -const superagent = require('superagent'); -const system = require('./model/system'); -const db = require('./service/db'); +import os from 'os'; +import superagent from 'superagent'; +import * as system from './model/system'; +import * as db from './service/db'; const c = {}; @@ -14,6 +14,7 @@ async function checkDb(log, warn, error) { 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}`); } @@ -49,7 +50,7 @@ const checks = [ checkProxy, ]; -async function start(log, warn, error, cb) { +export async function start(log, warn, error, cb) { const id = String.random(6); cb(id); for (const check of checks) { @@ -62,8 +63,6 @@ async function start(log, warn, error, cb) { } } -async function cancel(id) { +export async function cancel(id) { c[id] = true; } - -module.exports = { start, cancel }; diff --git a/hydro/declare.ts b/hydro/declare.ts new file mode 100644 index 000000000..2dbe926de --- /dev/null +++ b/hydro/declare.ts @@ -0,0 +1,28 @@ +declare global { + namespace NodeJS { + interface Global { + Hydro: { + model: any + handler: any + script: any + service: any + lib: any + stat: any + wiki: any + template: any + ui: any + error: any + locales: any + } + nodeModules: any + onDestory: Function[] + isFirstWorker: boolean + } + } +} + +declare module 'cluster' { + var isFirstWorker: boolean; +} + +export { }; diff --git a/hydro/entry/common.js b/hydro/entry/common.ts similarity index 86% rename from hydro/entry/common.js rename to hydro/entry/common.ts index d1ae4eb71..5f9cd56b0 100644 --- a/hydro/entry/common.js +++ b/hydro/entry/common.ts @@ -1,35 +1,35 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ -const os = require('os'); -const fs = require('fs'); -const yaml = require('js-yaml'); +import os from 'os'; +import fs from 'fs'; +import yaml from 'js-yaml'; -const builtinLib = [ +export const builtinLib = [ 'jwt', 'download', 'i18n', 'mail', 'markdown', 'md5', 'misc', 'paginate', 'hash.hydro', 'rank', 'template', 'validator', 'nav', 'sysinfo', 'testdata.convert.ini', 'readConfig', 'logger', 'useragent', ]; -const builtinModel = [ +export const builtinModel = [ 'builtin', 'document', 'domain', 'blacklist', 'opcount', 'setting', 'token', 'user', 'problem', 'record', 'contest', 'message', 'solution', 'training', 'file', 'discussion', 'system', ]; -const builtinHandler = [ +export const builtinHandler = [ 'home', 'problem', 'record', 'judge', 'user', 'contest', 'training', 'discussion', 'manage', 'import', 'misc', 'homework', 'domain', 'wiki', ]; -const builtinScript = [ +export const builtinScript = [ 'install', 'rating', 'recalcRating', 'register', 'problemStat', 'blacklist', 'setSuperadmin', 'deleteUser', ]; -async function handler(pending, fail) { +export async function handler(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/handler.js`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -46,7 +46,7 @@ async function handler(pending, fail) { } } -async function locale(pending, fail) { +export async function locale(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/locale.json`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -61,7 +61,7 @@ async function locale(pending, fail) { } } -async function setting(pending, fail, modelSetting, modelSystem) { +export async function setting(pending, fail, modelSetting, modelSystem = null) { const map = { system: modelSetting.SystemSetting, account: modelSetting.AccountSetting, @@ -71,7 +71,7 @@ async function setting(pending, fail, modelSetting, modelSystem) { const p = `${os.tmpdir()}/hydro/tmp/${i}/setting.yaml`; if (fs.existsSync(p) && !fail.includes(i)) { try { - const cfg = yaml.safeLoad(fs.readFileSync(p).toString()); + const cfg: any = yaml.safeLoad(fs.readFileSync(p).toString()); for (const key in cfg) { if (cfg[key].default && modelSystem) { const current = await modelSystem.get(`${i}.${key}`); @@ -91,7 +91,7 @@ async function setting(pending, fail, modelSetting, modelSystem) { } } -async function template(pending, fail) { +export async function template(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/template.json`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -106,7 +106,7 @@ async function template(pending, fail) { } } -async function model(pending, fail) { +export async function model(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/model.js`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -123,7 +123,7 @@ async function model(pending, fail) { } } -async function lib(pending, fail) { +export async function lib(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/lib.js`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -141,7 +141,7 @@ async function lib(pending, fail) { } } -async function service(pending, fail) { +export async function service(pending, fail) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/service.js`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -159,7 +159,7 @@ async function service(pending, fail) { } } -async function script(pending, fail, active) { +export async function script(pending, fail, active) { for (const i of pending) { const p = `${os.tmpdir()}/hydro/tmp/${i}/script.js`; if (fs.existsSync(p) && !fail.includes(i)) { @@ -176,18 +176,3 @@ async function script(pending, fail, active) { active.push(i); } } - -module.exports = { - builtinLib, - builtinModel, - builtinHandler, - builtinScript, - handler, - lib, - locale, - model, - script, - service, - template, - setting, -}; diff --git a/hydro/entry/master.js b/hydro/entry/master.ts similarity index 90% rename from hydro/entry/master.js rename to hydro/entry/master.ts index c9dd946f2..72abc4fa3 100644 --- a/hydro/entry/master.js +++ b/hydro/entry/master.ts @@ -1,11 +1,11 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable import/no-dynamic-require */ -const { +import { lib, service, model, setting, builtinLib, builtinHandler, builtinModel, -} = require('./common'); +} from './common'; -async function load(call) { +export async function load(call) { let pending = await require('../lib/hpm').getInstalled(); const fail = []; require('../lib/i18n'); @@ -14,10 +14,10 @@ async function load(call) { try { require('../options'); } catch (e) { - await call({ entry: 'install' }); + await call({ entry: 'setup' }); require('../options'); } - const bus = require('../service/bus'); + const bus = require('../service/bus').default; await new Promise((resolve) => { const h = () => { console.log('Database connected'); @@ -32,7 +32,7 @@ async function load(call) { require('../service/gridfs'); require('../service/monitor'); const server = require('../service/server'); - await server.prepare(); + await server.default.prepare(); await service(pending, fail); for (const i of builtinModel) require(`../model/${i}`); for (const i of builtinHandler) require(`../handler/${i}`); @@ -62,5 +62,3 @@ async function load(call) { pending = []; return await modelSystem.get('server.worker'); } - -module.exports = load; diff --git a/hydro/entry/setup.js b/hydro/entry/setup.ts similarity index 93% rename from hydro/entry/setup.js rename to hydro/entry/setup.ts index b9842005f..bd36ffee9 100644 --- a/hydro/entry/setup.js +++ b/hydro/entry/setup.ts @@ -1,16 +1,16 @@ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const http = require('http'); -const Koa = require('koa'); -const Body = require('koa-body'); -const Router = require('koa-router'); -const cache = require('koa-static-cache'); -const nunjucks = require('nunjucks'); -const mongodb = require('mongodb'); +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import http from 'http'; +import Koa from 'koa'; +import Body from 'koa-body'; +import Router from 'koa-router'; +import cache from 'koa-static-cache'; +import nunjucks from 'nunjucks'; +import mongodb from 'mongodb'; class Loader { - getSource(name) { // eslint-disable-line class-methods-use-this + getSource(name: string) { // eslint-disable-line class-methods-use-this return { src: ` @@ -76,6 +76,7 @@ class Loader { `, path: name, + noCache: false, }; } } @@ -93,7 +94,7 @@ function render(name) { }); } -async function load() { +export async function load() { const app = new Koa(); const server = http.createServer(app.callback()); const router = new Router(); @@ -158,5 +159,3 @@ async function load() { }); return 'Done! Restarting...'; } - -module.exports = load; diff --git a/hydro/entry/unzip.js b/hydro/entry/unzip.ts similarity index 94% rename from hydro/entry/unzip.js rename to hydro/entry/unzip.ts index 9396d016f..8a022c7f5 100644 --- a/hydro/entry/unzip.js +++ b/hydro/entry/unzip.ts @@ -1,8 +1,8 @@ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const zlib = require('zlib'); -const yaml = require('js-yaml'); +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import zlib from 'zlib'; +import yaml from 'js-yaml'; function ensureDir(dir) { if (!fs.existsSync(dir)) { @@ -30,7 +30,8 @@ for (const i of moduleRoots) { break; } } -async function load() { + +export async function load() { process.on('unhandledRejection', (e) => { console.error(e); process.exit(1); @@ -45,7 +46,7 @@ async function load() { try { const f = fs.readFileSync(root(`${moduleRoot}/${file}`)); const s = fs.statSync(root(`${moduleRoot}/${file}`)); - const m = yaml.safeLoad(zlib.gunzipSync(f)); + const m: any = yaml.safeLoad(zlib.gunzipSync(f).toString()); const { id } = m; if (m.os) { if (!m.os.includes(os.platform().toLowerCase())) { @@ -123,5 +124,3 @@ async function load() { console.log('Unzip done.'); process.exit(0); } - -module.exports = load; diff --git a/hydro/entry/worker.js b/hydro/entry/worker.ts similarity index 93% rename from hydro/entry/worker.js rename to hydro/entry/worker.ts index c4b9dc31b..fd49b8d91 100644 --- a/hydro/entry/worker.js +++ b/hydro/entry/worker.ts @@ -1,11 +1,11 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable import/no-dynamic-require */ -const { +import { locale, template, lib, service, model, handler, script, setting, builtinLib, builtinScript, builtinHandler, builtinModel, -} = require('./common'); +} from './common'; -async function load() { +export async function load() { let pending = await require('../lib/hpm').getInstalled(); const fail = []; const active = []; @@ -27,7 +27,7 @@ async function load() { for (const i of builtinLib) require(`../lib/${i}`); await lib(pending, fail); require('../service/gridfs'); - const server = require('../service/server'); + const server = require('../service/server').default; await server.prepare(); await service(pending, fail); for (const i of builtinModel) require(`../model/${i}`); @@ -40,7 +40,7 @@ async function load() { await global.Hydro.handler[i](); } const notfound = require('../handler/notfound'); - await notfound(); + await notfound.apply(); for (const i in global.Hydro.service) { if (global.Hydro.service[i].postInit) { try { @@ -60,5 +60,3 @@ async function load() { }, 30 * 1000); return { active, fail }; } - -module.exports = load; diff --git a/hydro/error.js b/hydro/error.js deleted file mode 100644 index 0a9263744..000000000 --- a/hydro/error.js +++ /dev/null @@ -1,179 +0,0 @@ -/* eslint-disable func-names */ -const { isClass } = require('./utils'); - -const Err = (name, ...classMessageCode) => { - let Class; - let msg; - let code; - for (const item of classMessageCode) { - if (typeof item === 'number') { - code = item; - } else if (typeof item === 'string') { - msg = function () { return item; }; - } else if (isClass(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; -}; - -class HydroError extends Error { - constructor(...params) { - super(); - this.params = params; - } -} - -const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400); -const SystemError = Err('SystemError', HydroError, 'SystemError', 500); - -const BadRequestError = Err('BadRequestError', UserFacingError, 'BadRequestError', 400); -const ForbiddenError = Err('ForbiddenError', UserFacingError, 'ForbiddenError', 403); -const NotFoundError = Err('NotFoundError', UserFacingError, 'NotFoundError', 404); -const MethodNotAllowedError = Err('MethodNotAllowedError', UserFacingError, 'MethodNotAllowedError', 405); -const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingError, 'RemoteOnlineJudgeError', 500); - -const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted."); -const LoginError = Err('LoginError', ForbiddenError, 'Invalid password for user {0}.'); -const UserAlreadyExistError = Err('UserAlreadyExistError', ForbiddenError, 'User {0} already exists.'); -const InvalidTokenError = Err('InvalidTokenError', ForbiddenError); -const BlacklistedError = Err('BlacklistedError', ForbiddenError, 'Address or user {0} is blacklisted.'); -const VerifyPasswordError = Err('VerifyPasswordError', ForbiddenError, "Passwords don't match."); -const OpcountExceededError = Err('OpcountExceededError', ForbiddenError, 'Too frequent operations of {0} (limit: {2} operations in {1} seconds).'); -const PermissionError = Err('PermissionError', ForbiddenError, "You don't have the required permission ({0}) in this domain."); -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."; -}); -const ValidationError = Err('ValidationError', ForbiddenError, function () { - if (this.params.length === 1) return 'Field {0} validation failed.'; - return 'Field {0} or {1} validation failed.'; -}); -const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenError, "You haven't attended this contest yet."); -const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest."); -const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.'); -const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.'); -const TrainingAlreadyEnrollError = Err('TrainingAlreadyEnrollError', ForbiddenError, "You've already enrolled this training."); -const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.'); -const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError'); - -const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.'); -const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.'); -const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.'); -const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {1} not found.'); -const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.'); -const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.'); - -const ProblemNotFoundError = Err('ProblemNotFountError', DocumentNotFoundError, 'Problem {0} not found.'); -const SolutionNotFoundError = Err('SolutionNotFoundError', DocumentNotFoundError, 'Solution {0} not found.'); -const TrainingNotFoundError = Err('TrainingNotFoundError', DocumentNotFoundError, 'Training {0} not found.'); -const ContestNotFoundError = Err('ContestNotFoundError', DocumentNotFoundError, 'Contest {0} not found.'); -const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNotFoundError, 'Discussion {0} not found.'); -const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.'); - -const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError); - -global.Hydro.error = module.exports = { - Err, - HydroError, - BadRequestError, - BlacklistedError, - ForbiddenError, - NotFoundError, - 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, - 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.' - -class SendMailError(UserFacingError): - @property - def message(self): - return 'Failed to send mail to {0}.' -*/ diff --git a/hydro/error.ts b/hydro/error.ts new file mode 100644 index 000000000..08a42fcbd --- /dev/null +++ b/hydro/error.ts @@ -0,0 +1,200 @@ +/* eslint-disable func-names */ +const { isClass } = require('./utils'); + +interface IHydroError { + new(...args: any[]): HydroError +} + +const Err = (name: string, ...info: Array) => { + let Class: IHydroError; + let msg; + let code; + for (const item of info) { + if (typeof item === 'number') { + code = item; + } else if (typeof item === 'string') { + msg = function () { return item; }; + } else if (isClass(item)) { + // @ts-ignore + 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[]; + + constructor(...params: any[]) { + super(); + this.params = params; + } + + // eslint-disable-next-line class-methods-use-this + msg() { + return 'HydroError'; + } + + // eslint-disable-next-line class-methods-use-this + code() { + return 500; + } +} + +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 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 {1} 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 = { + Err, + HydroError, + BadRequestError, + BlacklistedError, + ForbiddenError, + NotFoundError, + 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.' + +class SendMailError(UserFacingError): + @property + def message(self): + return 'Failed to send mail to {0}.' +*/ diff --git a/hydro/handler/contest.js b/hydro/handler/contest.ts similarity index 86% rename from hydro/handler/contest.js rename to hydro/handler/contest.ts index c851f257f..1b625b2a2 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.ts @@ -1,27 +1,19 @@ -const moment = require('moment-timezone'); -const { ObjectID } = require('mongodb'); -const AdmZip = require('adm-zip'); -const { +import moment from 'moment-timezone'; +import { ObjectID } from 'mongodb'; +import AdmZip from 'adm-zip'; +import { ContestNotLiveError, ValidationError, ProblemNotFoundError, ContestNotAttendedError, -} = require('../error'); -const paginate = require('../lib/paginate'); -const { - PERM: { - PERM_CREATE_CONTEST, PERM_EDIT_CONTEST, PERM_READ_RECORD_CODE, - PERM_VIEW_CONTEST, PERM_EDIT_CONTEST_SELF, - }, - PRIV: { - PRIV_READ_RECORD_CODE, - }, -} = require('../model/builtin'); -const contest = require('../model/contest'); -const document = require('../model/document'); -const problem = require('../model/problem'); -const record = require('../model/record'); -const user = require('../model/user'); -const system = require('../model/system'); -const { Route, Handler } = require('../service/server'); +} from '../error'; +import paginate from '../lib/paginate'; +import { PERM, PRIV } 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 system from '../model/system'; +import { Route, Handler } from '../service/server'; const ContestHandler = contest.ContestHandlerMixin(Handler); @@ -35,7 +27,7 @@ class ContestListHandler extends ContestHandler { tdocs = contest.getMulti(domainId).sort({ beginAt: -1 }); qs = ''; } else { - if (!contest.CONTEST_RULES.includes(rule)) throw new ValidationError('rule'); + if (!contest.RULES[rule]) throw new ValidationError('rule'); tdocs = contest.getMulti(domainId, { rule }).sort({ beginAt: -1 }); qs = 'rule={0}'.format(rule); } @@ -127,8 +119,8 @@ class ContestScoreboardDownloadHandler extends ContestDetailHandler { class ContestEditHandler extends ContestDetailHandler { async prepare() { - if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_CONTEST); - else this.checkPerm(PERM_EDIT_CONTEST_SELF); + if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST); + else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF); } async get() { @@ -160,20 +152,20 @@ class ContestEditHandler extends ContestDetailHandler { async post({ domainId, beginAtDate, beginAtTime, duration, title, content, rule, pids, rated = false, }) { - let beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); - if (!beginAt.isValid()) { + const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); + if (!beginAtMoment.isValid()) { throw new ValidationError('beginAtDate', 'beginAtTime'); } - const endAt = beginAt.clone().add(duration, 'hours').toDate(); - if (beginAt.isSameOrAfter(endAt)) throw new ValidationError('duration'); - beginAt = beginAt.toDate(); + 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(this.tdoc.docId); + 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 }); @@ -201,6 +193,7 @@ class ContestProblemHandler extends ContestDetailHandler { } } + // @ts-ignore async get({ tid }) { const path = [ ['Hydro', 'homepage'], @@ -272,7 +265,9 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { class ContestCodeHandler extends ContestDetailHandler { async get({ domainId, tid }) { - if (!this.user.hasPriv(PRIV_READ_RECORD_CODE)) this.checkPerm(PERM_READ_RECORD_CODE); + 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 = {}; @@ -329,23 +324,23 @@ class ContestCreateHandler extends ContestHandler { pids = await this.verifyProblems(domainId, pids); const tid = await contest.add( domainId, title, content, - this.user._id, rule, beginAt, endAt, pids, rated, + this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated, ); this.response.body = { tid }; this.response.redirect = this.url('contest_detail', { tid }); } } -async function apply() { - Route('contest_main', '/contest', ContestListHandler, PERM_VIEW_CONTEST); - Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM_VIEW_CONTEST); - Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM_VIEW_CONTEST); - Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM_VIEW_CONTEST); - Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM_VIEW_CONTEST); - Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM_VIEW_CONTEST); - Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM_VIEW_CONTEST); - Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM_VIEW_CONTEST); - Route('contest_create', '/contest/create', ContestCreateHandler, PERM_CREATE_CONTEST); +export async function apply() { + Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST); + Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST); + 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); + Route('contest_create', '/contest/create', ContestCreateHandler, PERM.PERM_CREATE_CONTEST); } -global.Hydro.handler.contest = module.exports = apply; +global.Hydro.handler.contest = apply; diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.ts similarity index 87% rename from hydro/handler/discussion.js rename to hydro/handler/discussion.ts index 3a53299ae..586f78034 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.ts @@ -1,23 +1,14 @@ -const { ObjectID } = require('mongodb'); -const paginate = require('../lib/paginate'); -const system = require('../model/system'); -const user = require('../model/user'); -const discussion = require('../model/discussion'); -const document = require('../model/document'); -const { - PERM: { - PERM_VIEW_DISCUSSION, PERM_EDIT_DISCUSSION, PERM_EDIT_DISCUSSION_REPLY, - PERM_DELETE_DISCUSSION, PERM_DELETE_DISCUSSION_REPLY, PERM_HIGHLIGHT_DISCUSSION, - PERM_CREATE_DISCUSSION, PERM_REPLY_DISCUSSION, - }, - PRIV: { - PRIV_USER_PROFILE, - }, -} = require('../model/builtin'); -const { Route, Handler } = require('../service/server'); -const { DiscussionNotFoundError, DocumentNotFoundError } = require('../error'); +import { ObjectID } from 'mongodb'; +import { DiscussionNotFoundError, DocumentNotFoundError } from '../error'; +import paginate from '../lib/paginate'; +import * as system from '../model/system'; +import * as user from '../model/user'; +import * as discussion from '../model/discussion'; +import * as document from '../model/document'; +import { PERM, PRIV } from '../model/builtin'; +import { Route, Handler } from '../service/server'; -const typeMapper = { +export const typeMapper = { problem: document.TYPE_PROBLEM, contest: document.TYPE_CONTEST, node: document.TYPE_DISCUSSION_NODE, @@ -29,7 +20,7 @@ class DiscussionHandler extends Handler { async _prepare({ domainId, type, name, did, drid, drrid, }) { - this.checkPerm(PERM_VIEW_DISCUSSION); + this.checkPerm(PERM.PERM_VIEW_DISCUSSION); if (did) { this.ddoc = await discussion.get(domainId, did); if (!this.ddoc) throw new DiscussionNotFoundError(did); @@ -124,8 +115,8 @@ class DiscussionNodeHandler extends DiscussionHandler { class DiscussionCreateHandler extends DiscussionHandler { async prepare() { - this.checkPriv(PRIV_USER_PROFILE); - this.checkPerm(PERM_CREATE_DISCUSSION); + this.checkPriv(PRIV.PRIV_USER_PROFILE); + this.checkPerm(PERM.PERM_CREATE_DISCUSSION); } async get({ type, name }) { @@ -144,7 +135,7 @@ class DiscussionCreateHandler extends DiscussionHandler { }) { this.limitRate('add_discussion', 3600, 30); if (ObjectID.isValid(name)) name = new ObjectID(name); - if (highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION); + if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION); const did = await discussion.add( domainId, typeMapper[type], name, this.user._id, title, content, this.request.ip, highlight, @@ -156,7 +147,7 @@ class DiscussionCreateHandler extends DiscussionHandler { class DiscussionDetailHandler extends DiscussionHandler { async get({ domainId, did, page = 1 }) { - const dsdoc = this.user.hasPriv(PRIV_USER_PROFILE) + const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE) ? await discussion.getStatus(domainId, did, this.user._id) : null; const [drdocs, pcount, drcount] = await paginate( @@ -187,31 +178,31 @@ class DiscussionDetailHandler extends DiscussionHandler { } async post() { - this.checkPriv(PRIV_USER_PROFILE); + this.checkPriv(PRIV.PRIV_USER_PROFILE); } async postReply({ domainId, did, content }) { - this.checkPerm(PERM_REPLY_DISCUSSION); + this.checkPerm(PERM.PERM_REPLY_DISCUSSION); this.limitRate('add_discussion', 3600, 30); await discussion.addReply(domainId, did, this.user._id, content, this.request.ip); this.back(); } async postTailReply({ domainId, drid, content }) { - this.checkPerm(PERM_REPLY_DISCUSSION); + 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(); } async postEditReply({ domainId, drid, content }) { - if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY); + if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY); await discussion.editReply(domainId, drid, content); this.back(); } async postDeleteReply({ domainId, drid }) { - if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY); + if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); await discussion.delReply(domainId, drid); this.back(); } @@ -219,13 +210,13 @@ class DiscussionDetailHandler extends DiscussionHandler { async postEditTailReply({ domainId, drid, drrid, content, }) { - if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY); + if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY); await discussion.editTailReply(domainId, drid, drrid, content); this.back(); } async postDeleteTailReply({ domainId, drid, drrid }) { - if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY); + if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); await discussion.delTailReply(domainId, drid, drrid); this.back(); } @@ -278,15 +269,15 @@ class DiscussionEditHandler extends DiscussionHandler { async postUpdate({ domainId, did, title, content, highlight, }) { - if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION); - if (highlight && !this.ddoc.highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION); + if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION); + if (highlight && !this.ddoc.highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION); await discussion.edit(domainId, did, title, content, highlight); this.response.body = { did }; this.response.redirect = this.url('discussion_detail', { did }); } async postDelete({ domainId, did }) { - if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION); + if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION); await discussion.del(domainId, did); this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId }; this.response.redirect = this.url('discussion_node', { @@ -296,7 +287,7 @@ class DiscussionEditHandler extends DiscussionHandler { } } -async function apply() { +export async function apply() { Route('discussion_main', '/discuss', DiscussionMainHandler); Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler); Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler); @@ -307,4 +298,4 @@ async function apply() { Route('discussion_create', '/discuss/:type/:name/create', DiscussionCreateHandler); } -global.Hydro.handler.discussion = module.exports = apply; +global.Hydro.handler.discussion = apply; diff --git a/hydro/handler/domain.js b/hydro/handler/domain.ts similarity index 89% rename from hydro/handler/domain.js rename to hydro/handler/domain.ts index 88096445b..c666e4306 100644 --- a/hydro/handler/domain.js +++ b/hydro/handler/domain.ts @@ -1,11 +1,11 @@ -const user = require('../model/user'); -const domain = require('../model/domain'); -const system = require('../model/system'); -const { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } = require('../model/setting'); -const { PERM: { PERM_EDIT_DOMAIN }, PERMS_BY_FAMILY } = require('../model/builtin'); -const paginate = require('../lib/paginate'); -const { Route, Handler } = require('../service/server'); -const { RoleAlreadyExistError, ValidationError, PermissionError } = require('../error'); +import { RoleAlreadyExistError, ValidationError, PermissionError } from '../error'; +import * as user from '../model/user'; +import * as domain from '../model/domain'; +import * as system from '../model/system'; +import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; +import { PERM, PERMS_BY_FAMILY } from '../model/builtin'; +import paginate from '../lib/paginate'; +import { Route, Handler } from '../service/server'; class DomainRankHandler extends Handler { async get({ domainId, page = 1 }) { @@ -55,7 +55,7 @@ class DomainCreateHandler extends Handler { class ManageHandler extends Handler { async prepare({ domainId }) { - this.checkPerm(PERM_EDIT_DOMAIN); + this.checkPerm(PERM.PERM_EDIT_DOMAIN); this.domain = await domain.get(domainId); } } @@ -178,18 +178,18 @@ class DomainRoleHandler extends ManageHandler { this.back(); } - async postDelete({ roles }) { + async postDelete({ domainId, roles }) { for (const role of roles) { if (['root', 'default', 'guest'].includes(role)) { throw new ValidationError('role'); } } - await user.deleteRoles(roles); + await user.deleteRoles(domainId, roles); this.back(); } } -async function apply() { +export async function apply() { Route('ranking', '/ranking', DomainRankHandler); Route('domain_create', '/domain/create', DomainCreateHandler); Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler); @@ -199,4 +199,4 @@ async function apply() { Route('domain_role', '/domain/role', DomainRoleHandler); } -global.Hydro.handler.domain = module.exports = apply; +global.Hydro.handler.domain = apply; diff --git a/hydro/handler/home.js b/hydro/handler/home.ts similarity index 82% rename from hydro/handler/home.js rename to hydro/handler/home.ts index 7294c9162..7b9875355 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.ts @@ -1,40 +1,32 @@ -const fs = require('fs'); -const { +import fs from 'fs'; +import { VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, NotFoundError, -} = require('../error'); -const bus = require('../service/bus'); -const { +} from '../error'; +import * as bus from '../service/bus'; +import { Route, Connection, Handler, ConnectionHandler, -} = require('../service/server'); -const misc = require('../lib/misc'); -const md5 = require('../lib/md5'); -const contest = require('../model/contest'); -const message = require('../model/message'); -const document = require('../model/document'); -const system = require('../model/system'); -const user = require('../model/user'); -const file = require('../model/file'); -const setting = require('../model/setting'); -const domain = require('../model/domain'); -const discussion = require('../model/discussion'); -const token = require('../model/token'); -const training = require('../model/training'); -const { - PERM: { - PERM_VIEW_TRAINING, PERM_VIEW_CONTEST, PERM_VIEW_DISCUSSION, - PERM_VIEW_HOMEWORK, - }, - PRIV: { - PRIV_USER_PROFILE, PRIV_DELETE_FILE, PRIV_DELETE_FILE_SELF, - }, -} = require('../model/builtin'); +} from '../service/server'; +import * as misc from '../lib/misc'; +import md5 from '../lib/md5'; +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 } from '../model/builtin'; const { geoip, useragent } = global.Hydro.lib; class HomeHandler extends Handler { async homework(domainId) { - if (this.user.hasPerm(PERM_VIEW_HOMEWORK)) { + if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) { const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK) .sort('beginAt', -1) .limit(await system.get('HOMEWORK_ON_MAIN')) @@ -49,7 +41,7 @@ class HomeHandler extends Handler { } async contest(domainId) { - if (this.user.hasPerm(PERM_VIEW_CONTEST)) { + if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) { const tdocs = await contest.getMulti(domainId) .sort('beginAt', -1) .limit(await system.get('CONTESTS_ON_MAIN')) @@ -63,7 +55,7 @@ class HomeHandler extends Handler { } async training(domainId) { - if (this.user.hasPerm(PERM_VIEW_TRAINING)) { + if (this.user.hasPerm(PERM.PERM_VIEW_TRAINING)) { const tdocs = await training.getMulti(domainId) .sort('_id', 1) .limit(await system.get('TRAININGS_ON_MAIN')) @@ -76,8 +68,8 @@ class HomeHandler extends Handler { return [[], {}]; } - async discussion(domainId) { - if (this.user.hasPerm(PERM_VIEW_DISCUSSION)) { + async discussion(domainId): Promise<[any[], any]> { + if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) { const ddocs = await discussion.getMulti(domainId) .limit(await system.get('DISCUSSIONS_ON_MAIN')) .toArray(); @@ -147,10 +139,10 @@ class HomeSecurityHandler extends Handler { this.back(); } - async postChangeMail({ currentPassword, mail }) { + async postChangeMail({ domainId, currentPassword, mail }) { this.limitRate('send_mail', 3600, 30); this.user.checkPassword(currentPassword); - const udoc = await user.getByMail(mail); + const udoc = await user.getByEmail(domainId, mail, true); if (udoc) throw new UserAlreadyExistError(mail); const [code] = await token.add( token.TYPE_CHANGEMAIL, @@ -216,12 +208,12 @@ class HomeSettingsHandler extends Handler { } class UserChangemailWithCodeHandler extends Handler { - async get({ code }) { + async get({ domainId, code }) { 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(tdoc.mail); + const udoc = await user.getByEmail(domainId, tdoc.mail, true); if (udoc) throw new UserAlreadyExistError(tdoc.mail); await Promise.all([ user.setEmail(this.user._id, tdoc.mail), @@ -271,7 +263,7 @@ class HomeMessagesHandler extends Handler { } async postDeleteMessage({ messageId }) { - await message.delete(messageId, this.user._id); + await message.del(messageId); this.back(); } } @@ -293,29 +285,29 @@ class HomeMessagesConnectionHandler extends ConnectionHandler { class HomeFileHandler extends Handler { async get() { const ufdocs = await file.getMulti({ owner: this.user._id }).toArray(); - const fdict = await fs.getMetaDict(ufdocs.map((ufdoc) => ufdoc._id)); + const fdict = await file.getMetaDict(ufdocs.map((ufdoc) => ufdoc._id)); this.response.template = 'home_file.html'; this.response.body = { ufdocs, fdict }; } async postDelete(ufid) { - const ufdoc = await file.get(ufid); - if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV_DELETE_FILE); - else this.checkPriv(PRIV_DELETE_FILE_SELF); + 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 file.decUsage(this.user._id, ufdoc.length); + if (result) await user.inc(this.user._id, 'usage', ufdoc.length); this.back(); } } async function apply() { Route('homepage', '/', HomeHandler); - Route('home_security', '/home/security', HomeSecurityHandler, PRIV_USER_PROFILE); - Route('user_changemail_with_code', '/home/changeMail/:code', UserChangemailWithCodeHandler, PRIV_USER_PROFILE); - Route('home_settings', '/home/settings/:category', HomeSettingsHandler, PRIV_USER_PROFILE); - Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV_USER_PROFILE); - Route('home_file', '/home/file', HomeFileHandler, PRIV_USER_PROFILE); - Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV_USER_PROFILE); + 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_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; diff --git a/hydro/handler/homework.js b/hydro/handler/homework.ts similarity index 63% rename from hydro/handler/homework.js rename to hydro/handler/homework.ts index c855b82e4..0027ab47f 100644 --- a/hydro/handler/homework.js +++ b/hydro/handler/homework.ts @@ -1,27 +1,21 @@ -const yaml = require('js-yaml'); -const moment = require('moment-timezone'); -const { ObjectID } = require('mongodb'); -const AdmZip = require('adm-zip'); -const { +import yaml from 'js-yaml'; +import moment from 'moment-timezone'; +import { ObjectID } from 'mongodb'; +import AdmZip from 'adm-zip'; +import { ValidationError, HomeworkNotLiveError, ProblemNotFoundError, HomeworkNotAttendedError, -} = require('../error'); -const { Route, Handler } = require('../service/server'); -const { - PERM_VIEW_HOMEWORK, PERM_ATTEND_HOMEWORK, PERM_VIEW_PROBLEM, - PERM_SUBMIT_PROBLEM, PERM_CREATE_HOMEWORK, PERM_EDIT_HOMEWORK, - PERM_VIEW_HOMEWORK_SCOREBOARD, PERM_READ_RECORD_CODE, -} = require('../model/builtin').PERM; -const system = require('../model/system'); -const user = require('../model/user'); -const contest = require('../model/contest'); -const discussion = require('../model/discussion'); -const problem = require('../model/problem'); -const record = require('../model/record'); -const document = require('../model/document'); -const paginate = require('../lib/paginate'); -const misc = require('../lib/misc'); -const ranked = require('../lib/rank'); +} from '../error'; +import { Route, Handler } from '../service/server'; +import { PERM } from '../model/builtin'; +import * as system from '../model/system'; +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'; const HomeworkHandler = contest.ContestHandlerMixin(Handler); @@ -31,7 +25,7 @@ class HomeworkMainHandler extends HomeworkHandler { const calendar = []; for (const tdoc of tdocs) { const cal = { ...tdoc, url: `/homework/${tdoc.docId}` }; - if (contest.isHomeworkExtended(tdoc) || contest.isDone(tdoc)) { + if (contest.isExtended(tdoc) || contest.isDone(tdoc)) { cal.endAt = tdoc.endAt; cal.penaltySince = tdoc.penaltySince; } else { @@ -93,7 +87,7 @@ class HomeworkDetailHandler extends HomeworkHandler { } async postAttend({ domainId, tid }) { - this.checkPerm(PERM_ATTEND_HOMEWORK); + 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); @@ -103,7 +97,7 @@ class HomeworkDetailHandler extends HomeworkHandler { class HomeworkDetailProblemHandler extends HomeworkHandler { async prepare({ domainId, tid, pid }) { - this.checkPerm(PERM_VIEW_PROBLEM); + 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), @@ -121,7 +115,7 @@ class HomeworkDetailProblemHandler extends HomeworkHandler { if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid); const path = [ ['Hydro', 'homepage'], - ['homework_main', '/homework'], + ['homework_main', 'homework_main'], [this.tdoc.title, 'homework_detail', { tid }, true], [this.pdoc.title, null, null, true], ]; @@ -170,8 +164,8 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { await contest.updateStatus(domainId, tid, this.user._id, rid, pid, false, 0, document.TYPE_HOMEWORK); this.response.body = { tid, rid }; - if (this.canShowRecord(this.tdoc)) this.response.redirect = `/record/${rid}`; - else this.response.redirect = `/homework/${tid}`; + if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid }); + else this.response.redirect = this.url('homework_detail', { tid }); } } @@ -230,7 +224,8 @@ class HomeworkCreateHandler extends HomeworkHandler { class HomeworkEditHandler extends HomeworkHandler { async get({ domainId, tid }) { const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); - if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_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; @@ -262,7 +257,8 @@ class HomeworkEditHandler extends HomeworkHandler { penaltyRules, pids, tid, }) { const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); - if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_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 { @@ -288,7 +284,7 @@ class HomeworkEditHandler extends HomeworkHandler { || tdoc.endAt !== endAt || tdoc.penaltySince !== penaltySince || new Set(tdoc.pids) !== new Set(pids)) { - await contest.recalcStatus(domainId, document.TYPE_HOMEWORK, tdoc.docId); + await contest.recalcStatus(domainId, tdoc.docId, document.TYPE_HOMEWORK); } this.response.body = { tid }; this.response.redirect = this.url('homework_detail', { tid }); @@ -327,7 +323,7 @@ class HomeworkScoreboardDownloadHandler extends HomeworkHandler { class HomeworkCodeHandler extends HomeworkHandler { async get({ domainId, tid }) { - this.checkPerm(PERM_READ_RECORD_CODE); + this.checkPerm(PERM.PERM_READ_RECORD_CODE); this.limitRate('homework_code', 3600, 60); const [tdoc, tsdocs] = await contest.getAndListStatus( domainId, tid, document.TYPE_HOMEWORK, @@ -352,155 +348,15 @@ class HomeworkCodeHandler extends HomeworkHandler { } async function apply() { - contest.isHomeworkExtended = (tdoc) => { - const now = new Date().getTime(); - return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); - }; - contest.RULES.homework = { - TEXT: 'Assignment', - check: () => { }, - stat: (tdoc, journal) => { - const effective = {}; - for (const j of journal) { - if (tdoc.pids.includes(j.pid) - && !effective.includes(j.pid) - && effective[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 of effective) { - detail.push({ ...j, penaltyScore: penaltyScore(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 = [ - { 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 (const index in tdoc.pids) { - const pid = tdoc.pids[index]; - if (isExport) { - columns.push( - { - type: 'problem_score', - value: '#{0} {1}'.format(index + 1, pdict[pid].title), - }, - { - type: 'problem_original_score', - value: '#{0} {1}'.format(index + 1, _('Original Score')), - }, - { - type: 'problem_time', - value: '#{0} {1}'.format(index + 1, _('Time (Seconds)')), - }, - { - type: 'problem_time_str', - value: '#{0} {1}'.format(index + 1, _('Time')), - }, - ); - } else { - columns.push({ - type: 'problem_detail', - value: '#{0}'.format(index + 1), - raw: pdict[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: udict[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 rdoc = (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: rdoc, - }); - } - } - rows.push(row); - } - return rows; - }, - rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score), - }; - Route('homework_main', '/homework', HomeworkMainHandler, PERM_VIEW_HOMEWORK); - Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM_CREATE_HOMEWORK); - Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM_VIEW_HOMEWORK); - Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); - Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); - Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); - Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); - Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM_VIEW_HOMEWORK); + 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 = module.exports = apply; +global.Hydro.handler.homework = apply; diff --git a/hydro/handler/import.js b/hydro/handler/import.ts similarity index 78% rename from hydro/handler/import.js rename to hydro/handler/import.ts index 03fa8cae6..949ab74e7 100644 --- a/hydro/handler/import.js +++ b/hydro/handler/import.ts @@ -1,14 +1,14 @@ -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const problem = require('../model/problem'); -const { PERM_CREATE_PROBLEM } = require('../model/builtin').PERM; -const { Route, Handler } = require('../service/server'); -const { ValidationError } = require('../error'); +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 } from '../service/server' class ProblemImportHandler extends Handler { async prepare() { - this.checkPerm(PERM_CREATE_PROBLEM); + this.checkPerm(PERM.PERM_CREATE_PROBLEM); } async get() { @@ -47,8 +47,8 @@ class ProblemImportHandler extends Handler { } } -async function apply() { +export async function apply() { Route('problem_import', '/problem/import', ProblemImportHandler); } -global.Hydro.handler.import = module.exports = apply; +global.Hydro.handler.import = apply; diff --git a/hydro/handler/judge.js b/hydro/handler/judge.ts similarity index 81% rename from hydro/handler/judge.js rename to hydro/handler/judge.ts index 824ddc7f2..b2b183612 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.ts @@ -1,18 +1,18 @@ -const yaml = require('js-yaml'); -const { parseTimeMS, parseMemoryMB } = require('../utils'); -const record = require('../model/record'); -const problem = require('../model/problem'); -const builtin = require('../model/builtin'); -const contest = require('../model/contest'); -const user = require('../model/user'); -const task = require('../model/task'); -const bus = require('../service/bus'); -const { +import yaml from 'js-yaml'; +import { parseTimeMS, parseMemoryMB } from '../utils'; +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 user from '../model/user'; +import * as task from '../model/task'; +import * as bus from '../service/bus'; +import { Route, Handler, Connection, ConnectionHandler, -} = require('../service/server'); +} from '../service/server'; async function _postJudge(rdoc) { - const accept = rdoc.status === builtin.STATUS_ACCEPTED; + const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED; bus.publish('record_change', rdoc); if (rdoc.type === 'run') return; const tasks = []; @@ -24,7 +24,7 @@ async function _postJudge(rdoc) { ), ); } - if (await problem.updateStatus(rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) { + 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), @@ -35,12 +35,12 @@ async function _postJudge(rdoc) { await Promise.all(tasks); } -async function next(body) { +export async function next(body) { let rdoc = await record.get(body.domainId, body.rid); - const $set = {}; - const $push = {}; + const $set: any = {}; + const $push: any = {}; if (body.case) { - const c = {}; + const c: any = {}; c.memory = body.case.memory_kb || body.case.memory; c.time = body.case.time_ms || body.case.time; c.judgeText = body.case.judge_text || body.case.judgeText || body.case.message; @@ -65,10 +65,10 @@ async function next(body) { bus.publish('record_change', rdoc); } -async function end(body) { +export async function end(body) { let rdoc = await record.get(body.domainId, body.rid); - const $set = {}; - const $push = {}; + const $set: any = {}; + const $push: any = {}; const $unset = { progress: '' }; if (body.judge_text) { rdoc.judgeTexts.push(body.judge_text); @@ -98,7 +98,7 @@ class PretestHandler extends Handler { if (pid) { const pdoc = await problem.get(domainId, pid); if (pdoc.config) { - const config = yaml.safeLoad(pdoc.config); + const config: any = yaml.safeLoad(pdoc.config); if (config.time) time = config.time; if (config.memory) memory = config.memory; } @@ -155,19 +155,19 @@ class JudgeConnectionHandler extends ConnectionHandler { async cleanup() { if (this.processing) { - await record.reset(this.processing); + await record.reset(this.processing.domainId, this.processing.rid); task.add(this.processing); } } } -async function apply() { +export async function apply() { Route('pretest', '/pretest', PretestHandler); - Route('judge', '/judge', JudgeHandler, builtin.PERM.PERM_JUDGE); - Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PERM.PERM_JUDGE); + 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 = module.exports = apply; +global.Hydro.handler.judge = apply; diff --git a/hydro/handler/manage.js b/hydro/handler/manage.ts similarity index 93% rename from hydro/handler/manage.js rename to hydro/handler/manage.ts index 210ab8781..d52515a8f 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.ts @@ -1,14 +1,14 @@ -const setting = require('../model/setting'); -const system = require('../model/system'); -const { STATUS, PRIV } = require('../model/builtin'); -const record = require('../model/record'); -const judge = require('./judge'); -const { +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, -} = require('../service/server'); -const hpm = require('../lib/hpm'); -const loader = require('../loader'); -const check = require('../check'); +} from '../service/server'; +import * as hpm from '../lib/hpm'; function set(key, value) { if (setting.SYSTEM_SETTINGS_BY_KEY[key]) { @@ -81,7 +81,6 @@ class SystemModuleHandler extends SystemHandler { async get() { this.response.body.path.push(['manage_module', null]); this.response.body.installed = await hpm.getDetail(); - this.response.active = loader.active; this.response.template = 'manage_module.html'; } diff --git a/hydro/handler/misc.js b/hydro/handler/misc.ts similarity index 81% rename from hydro/handler/misc.js rename to hydro/handler/misc.ts index d37817204..ef35792e3 100644 --- a/hydro/handler/misc.js +++ b/hydro/handler/misc.ts @@ -1,9 +1,9 @@ -const { PRIV_JUDGE, PRIV_USER_PROFILE } = require('../model/builtin').PRIV; -const file = require('../model/file'); -const user = require('../model/user'); -const markdown = require('../lib/markdown'); -const db = require('../service/db'); -const { Route, Handler } = require('../service/server'); +import { PRIV } from '../model/builtin'; +import * as file from '../model/file'; +import * as user from '../model/user'; +import * as markdown from '../lib/markdown'; +import * as db from '../service/db'; +import { Route, Handler } from '../service/server'; const coll = db.collection('status'); @@ -11,7 +11,7 @@ class FileDownloadHandler extends Handler { async get({ docId, secret, name }) { if (name) name = Buffer.from(name, 'base64').toString(); this.response.attachment(name || docId); - this.response.body = await file.get(docId, secret); + this.response.body = await file.getWithSecret(docId, secret); } } @@ -37,7 +37,7 @@ class StatusHandler extends Handler { class StatusUpdateHandler extends Handler { async post(args) { - this.checkPerm(PRIV_JUDGE); + this.checkPerm(PRIV.PRIV_JUDGE); args.type = 'judger'; return coll.updateOne( { mid: args.mid, type: 'judger' }, @@ -49,7 +49,7 @@ class StatusUpdateHandler extends Handler { class SwitchLanguageHandler extends Handler { async get({ lang }) { - if (this.user.hasPriv(PRIV_USER_PROFILE)) { + if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { await user.setById(this.user._id, { viewLang: lang }); } else this.session.viewLang = lang; this.back(); @@ -70,7 +70,7 @@ class SockToken extends Handler { } } -async function apply() { +export async function apply() { Route('file_download', '/fs/:docId/:secret', FileDownloadHandler); Route('file_download_with_name', '/fs/:docId/:name/:secret', FileDownloadHandler); Route('status', '/status', StatusHandler); @@ -89,4 +89,4 @@ apply.updateStatus = function updateStatus(args) { ); }; -global.Hydro.handler.misc = module.exports = apply; +global.Hydro.handler.misc = apply; diff --git a/hydro/handler/notfound.js b/hydro/handler/notfound.ts similarity index 60% rename from hydro/handler/notfound.js rename to hydro/handler/notfound.ts index d761366c2..ef86a26f9 100644 --- a/hydro/handler/notfound.js +++ b/hydro/handler/notfound.ts @@ -1,7 +1,7 @@ /* eslint-disable no-empty-function */ /* eslint-disable class-methods-use-this */ -const { Route, Handler } = require('../service/server'); -const { NotFoundError } = require('../error'); +import { NotFoundError } from '../error'; +import { Route, Handler } from '../service/server'; class NotFoundHandler extends Handler { async prepare() { @@ -13,8 +13,8 @@ class NotFoundHandler extends Handler { async post() { } } -async function apply() { +export async function apply() { Route('notfound', '*', NotFoundHandler); } -global.Hydro.handler.notfound = module.exports = apply; +global.Hydro.handler.notfound = apply; diff --git a/hydro/handler/problem.js b/hydro/handler/problem.ts similarity index 83% rename from hydro/handler/problem.js rename to hydro/handler/problem.ts index e49ae23d4..97e5bd7f1 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.ts @@ -1,58 +1,47 @@ -const paginate = require('../lib/paginate'); -const validator = require('../lib/validator'); -const file = require('../model/file'); -const problem = require('../model/problem'); -const record = require('../model/record'); -const user = require('../model/user'); -const solution = require('../model/solution'); -const system = require('../model/system'); -const { - PERM: { - PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM, - PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM, - PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION, - PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY, - PERM_REPLY_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SELF, - }, - PRIV: { - PRIV_USER_PROFILE, PRIV_JUDGE, - }, -} = require('../model/builtin'); -const bus = require('../service/bus'); -const { +import paginate from '../lib/paginate'; +import * as validator from '../lib/validator'; +import * as file from '../model/file'; +import * as problem from '../model/problem'; +import * as record from '../model/record'; +import * as user from '../model/user'; +import * as solution from '../model/solution'; +import * as system from '../model/system'; +import { PERM, PRIV } from '../model/builtin'; +import * as bus from '../service/bus'; +import { Route, Connection, Handler, ConnectionHandler, -} = require('../service/server'); -const { +} from '../service/server'; +import { NoProblemError, ProblemDataNotFoundError, BadRequestError, SolutionNotFoundError, -} = require('../error'); +} from '../error'; class ProblemHandler extends Handler { - async _prepare() { - this.checkPerm(PERM_VIEW_PROBLEM); + async __prepare() { + this.checkPerm(PERM.PERM_VIEW_PROBLEM); } async get({ domainId, page = 1, q = '' }) { this.response.template = 'problem_main.html'; - const query = {}; + const query: any = {}; let psdict = {}; - const path = [ + const path: any = [ ['Hydro', 'homepage'], ['problem_main', null], ]; if (q) { q = q.toLowerCase(); - const $regex = new RegExp(`\\A\\Q${q.replace(/\\E/gmi, /\\E\\E\\Q/gmi)}\\E`, 'gmi'); + const $regex = new RegExp(`\\A\\Q${q}\\E`, 'gmi'); query.title = { $regex }; path.push([q, null, null, true]); } - if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false; + 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, await system.get('PROBLEM_PER_PAGE'), ); - if (this.user.hasPriv(PRIV_USER_PROFILE)) { + if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { psdict = await problem.getListStatus( domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId), ); @@ -73,7 +62,7 @@ class ProblemHandler extends Handler { } async cleanup() { - if (this.response.template === 'problem_main.html' && this.preferJson) { + if (this.response.template === 'problem_main.html' && this.request.json) { const { path, page, pcount, ppcount, pdocs, psdict, category, } = this.response.body; @@ -98,7 +87,7 @@ class ProblemHandler extends Handler { class ProblemCategoryHandler extends ProblemHandler { async get({ domainId, page = 1, category }) { this.response.template = 'problem_main.html'; - const q = { $and: [] }; + const q: any = { $and: [] }; for (const name of category) { q.$and.push({ $or: [ @@ -108,13 +97,13 @@ class ProblemCategoryHandler extends ProblemHandler { }); } let psdict = {}; - if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + 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, await system.get('PROBLEM_PER_PAGE'), ); - if (this.user.hasPriv(PRIV_USER_PROFILE)) { + if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { psdict = await problem.getListStatus( domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId), ); @@ -132,7 +121,7 @@ class ProblemCategoryHandler extends ProblemHandler { class ProblemRandomHandler extends ProblemHandler { async get({ domainId, category }) { - const q = category[0] ? { $and: [] } : {}; + const q: any = category[0] ? { $and: [] } : {}; for (const name of category) { if (name) { q.$and.push({ @@ -143,7 +132,7 @@ class ProblemRandomHandler extends ProblemHandler { }); } } - if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; + 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 }; @@ -157,7 +146,7 @@ class ProblemDetailHandler extends ProblemHandler { if (pid) { this.pdoc = await problem.get(domainId, pid, this.user._id); if (this.pdoc.hidden && this.pdoc.owner !== this.user._id) { - this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); } if (this.pdoc) this.udoc = await user.getById(domainId, this.pdoc.owner); } @@ -168,7 +157,8 @@ class ProblemDetailHandler extends ProblemHandler { }; } - async get() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async get(args: any) { this.response.body.path = [ ['Hydro', 'homepage'], ['problem_main', 'problem_main'], @@ -260,8 +250,8 @@ class ProblemStatisticsHandler extends ProblemDetailHandler { class ProblemManageHandler extends ProblemDetailHandler { async prepare() { - if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_PROBLEM); - else this.checkPerm(PERM_EDIT_PROBLEM_SELF); + if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM); + else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF); } } @@ -335,8 +325,10 @@ class ProblemDataUploadHandler extends ProblemManageHandler { class ProblemDataDownloadHandler extends ProblemDetailHandler { async get({ pid }) { - if (this.user._id !== this.pdoc.owner) { - if (!this.user.hasPerm(PERM_READ_PROBLEM_DATA)) this.checkPriv(PRIV_JUDGE); + 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) throw new ProblemDataNotFoundError(pid); else if (typeof this.pdoc.data === 'string') [, this.response.redirect] = this.pdoc.data.split('from:'); @@ -347,7 +339,7 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler { class ProblemSolutionHandler extends ProblemDetailHandler { async get({ domainId, page = 1 }) { this.response.template = 'problem_solution.html'; - this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); + this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); const [psdocs, pcount, pscount] = await paginate( solution.getMulti(domainId, this.pdoc.docId), page, @@ -362,7 +354,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler { for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner); } } - const udict = await user.getList(uids); + const udict = await user.getList(domainId, uids); const pssdict = solution.getListStatus(domainId, docids, this.user._id); const path = [ ['problem_main', 'problem_main'], @@ -379,28 +371,31 @@ class ProblemSolutionHandler extends ProblemDetailHandler { } async postSubmit({ domainId, content }) { - this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION); + this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION); await solution.add(domainId, this.pdoc.docId, this.user._id, content); this.back(); } async postEditSolution({ domainId, content }) { - if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION); + if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION); + else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF); this.psdoc = await solution.edit(domainId, this.psdoc.docId, content); this.ctx.body.psdoc = this.psdoc; this.back(); } async postDeleteSolution({ domainId }) { - if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION); + if (this.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, this.psdoc.docId); this.back(); } async postReply({ domainId, psid, content }) { - this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION); + 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(); } async postEditReply({ @@ -408,14 +403,27 @@ class ProblemSolutionHandler extends ProblemDetailHandler { }) { 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.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); + if (!(psdoc.owner === this.user._id + && this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) { + 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(); } async postDeleteReply({ domainId, psid, psrid }) { 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.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); + if (!(psdoc.owner === this.user._id + && this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) { + 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(); } @@ -435,7 +443,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler { class ProblemSolutionRawHandler extends ProblemDetailHandler { async get({ domainId, psid }) { - this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); + this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); const psdoc = await solution.get(domainId, psid); this.response.type = 'text/markdown'; this.response.body = psdoc.content; @@ -443,10 +451,10 @@ class ProblemSolutionRawHandler extends ProblemDetailHandler { } class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { - async get({ domainId, psid }) { - this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); - const [psdoc, psrdoc] = await solution.getReply(domainId, psid); - if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); + async get({ domainId, psid, psrid }) { + 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; } @@ -455,7 +463,7 @@ class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { class ProblemCreateHandler extends Handler { async get() { this.response.template = 'problem_edit.html'; - this.checkPerm(PERM_CREATE_PROBLEM); + this.checkPerm(PERM.PERM_CREATE_PROBLEM); this.response.body = { path: [ ['Hydro', 'homepage'], @@ -477,12 +485,12 @@ class ProblemCreateHandler extends Handler { } } -async function apply() { +export async function apply() { Route('problem_main', '/p', ProblemHandler); 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_SUBMIT_PROBLEM); + 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); @@ -496,4 +504,4 @@ async function apply() { Connection('problem_pretest_conn', '/p/:pid/pretest-conn', ProblemPretestConnectionHandler); } -global.Hydro.handler.problem = module.exports = apply; +global.Hydro.handler.problem = apply; diff --git a/hydro/handler/record.js b/hydro/handler/record.ts similarity index 86% rename from hydro/handler/record.js rename to hydro/handler/record.ts index 02d70332c..0316bebdf 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.ts @@ -1,16 +1,16 @@ -const { ObjectID } = require('mongodb'); -const { PermissionError } = require('../error'); -const { PERM_READ_RECORD_CODE, PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../model/builtin').PERM; -const problem = require('../model/problem'); -const record = require('../model/record'); -const contest = require('../model/contest'); -const system = require('../model/system'); -const user = require('../model/user'); -const paginate = require('../lib/paginate'); -const bus = require('../service/bus'); -const { +import { ObjectID } from 'mongodb'; +import { PermissionError } from '../error'; +import { PERM } from '../model/builtin'; +import * as problem from '../model/problem'; +import * as record from '../model/record'; +import * as contest from '../model/contest'; +import * as system from '../model/system'; +import * as user from '../model/user'; +import paginate from '../lib/paginate'; +import * as bus from '../service/bus'; +import { Route, Handler, Connection, ConnectionHandler, -} = require('../service/server'); +} from '../service/server'; const RecordHandler = contest.ContestHandlerMixin(Handler); @@ -19,7 +19,7 @@ class RecordListHandler extends RecordHandler { domainId, page = 1, pid, tid, uidOrName, }) { this.response.template = 'record_main.html'; - const q = { tid, hidden: false }; + const q: any = { tid, hidden: false }; if (uidOrName) { q.$or = [ { unameLower: uidOrName.toLowerCase() }, @@ -61,7 +61,7 @@ class RecordDetailHandler extends RecordHandler { const tdoc = await contest.get(domainId, rdoc.tid, rdoc.ttype); if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); } - if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) { + if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) { rdoc.code = null; } if (rdoc.type !== 'run') rdoc.stdout = rdoc.stderr = ''; @@ -85,7 +85,7 @@ class RecordDetailHandler extends RecordHandler { class RecordRejudgeHandler extends Handler { async post({ domainId, rid }) { - this.checkPerm(PERM_REJUDGE); + this.checkPerm(PERM.PERM_REJUDGE); const rdoc = await record.get(domainId, rid); if (rdoc) await record.rejudge(domainId, rid); this.back(); @@ -131,7 +131,7 @@ class RecordMainConnectionHandler extends RecordConnectionHandler { user.getById(this.domainId, rdoc.uid), problem.get(this.domainId, rdoc.pid, null, false), ]); - if (pdoc && pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; + if (pdoc && pdoc.hidden && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); } } @@ -165,7 +165,7 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti } } -async function apply() { +export async function apply() { Route('record_main', '/record', RecordListHandler); Route('record_detail', '/record/:rid', RecordDetailHandler); Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler); @@ -173,4 +173,4 @@ async function apply() { Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler); } -global.Hydro.handler.record = module.exports = apply; +global.Hydro.handler.record = apply; diff --git a/hydro/handler/training.js b/hydro/handler/training.ts similarity index 85% rename from hydro/handler/training.js rename to hydro/handler/training.ts index 48677366a..e179e7119 100644 --- a/hydro/handler/training.js +++ b/hydro/handler/training.ts @@ -1,21 +1,14 @@ -const assert = require('assert'); -const { ValidationError, ProblemNotFoundError } = require('../error'); -const paginate = require('../lib/paginate'); -const { - PERM: { - PERM_VIEW_TRAINING, PERM_VIEW_PROBLEM_HIDDEN, PERM_CREATE_TRAINING, - PERM_EDIT_TRAINING, PERM_EDIT_TRAINING_SELF, - }, - PRIV: { - PRIV_USER_PROFILE, - }, -} = require('../model/builtin'); -const problem = require('../model/problem'); -const builtin = require('../model/builtin'); -const training = require('../model/training'); -const user = require('../model/user'); -const system = require('../model/system'); -const { Route, Handler } = require('../service/server'); +import assert from 'assert'; +import { ValidationError, ProblemNotFoundError } from '../error'; +import paginate from '../lib/paginate'; +import { PERM, PRIV } 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 * as system from '../model/system'; +import { Route, Handler } from '../service/server'; +import { ObjectID } from 'mongodb'; async function _parseDagJson(domainId, dag) { const parsed = []; @@ -65,12 +58,12 @@ class TrainingMainHandler extends Handler { page, await system.get('TRAINING_PER_PAGE'), ); - const tids = new Set(); + const tids: Set = new Set(); for (const tdoc of tdocs) tids.add(tdoc.docId); const tsdict = {}; let tdict = {}; - if (this.user.hasPriv(PRIV_USER_PROFILE)) { - const enrolledTids = new Set(); + if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { + const enrolledTids: Set = new Set(); const tsdocs = await training.getMultiStatus(domainId, { uid: this.user._id, $or: [{ docId: { $in: Array.from(tids) } }, { enroll: 1 }], @@ -84,7 +77,7 @@ class TrainingMainHandler extends Handler { tdict = await training.getList(domainId, Array.from(enrolledTids)); } } - for (const tdoc in tdocs) tdict[tdoc.docId] = tdoc; + for (const tdoc of tdocs) tdict[tdoc.docId] = tdoc; const path = [ ['Hydro', 'homepage'], ['training_main', null], @@ -102,7 +95,7 @@ class TrainingDetailHandler extends Handler { const pids = training.getPids(tdoc); const [owner, pdict] = await Promise.all([ user.getById(domainId, tdoc.owner), - problem.getList(domainId, pids, this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)), + 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(); @@ -123,7 +116,7 @@ class TrainingDetailHandler extends Handler { const totalCount = node.pids.length; const doneCount = Set.union(new Set(node.pids), donePids).size; const nsdoc = { - progress: totalCount ? parseInt(100 * (doneCount / totalCount), 10) : 100, + 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), @@ -149,7 +142,7 @@ class TrainingDetailHandler extends Handler { } async postEnroll({ domainId, tid }) { - this.checkPriv(PRIV_USER_PROFILE); + this.checkPriv(PRIV.PRIV_USER_PROFILE); const tdoc = await training.get(domainId, tid); await training.enroll(domainId, tdoc.docId, this.user._id); this.back(); @@ -186,8 +179,8 @@ class TrainingCreateHandler extends Handler { ); } } - for (const pdoc in pdocs) { - if (pdoc.hidden) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + 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 }; @@ -198,8 +191,8 @@ class TrainingCreateHandler extends Handler { class TrainingEditHandler extends Handler { async prepare({ domainId, tid }) { this.tdoc = await training.get(domainId, tid); - if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_TRAINING); - else this.checkPerm(PERM_EDIT_TRAINING_SELF); + if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_TRAINING); + else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF); } async get({ tid }) { @@ -238,8 +231,8 @@ class TrainingEditHandler extends Handler { ); } } - for (const pdoc in pdocs) { - if (pdoc.hidden) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + for (const pdoc of pdocs) { + if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); } await training.edit(domainId, tid, { title, content, dag, description, @@ -249,11 +242,11 @@ class TrainingEditHandler extends Handler { } } -async function apply() { - Route('training_main', '/training', TrainingMainHandler, PERM_VIEW_TRAINING); - Route('training_create', '/training/create', TrainingCreateHandler, PERM_CREATE_TRAINING); - Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM_VIEW_TRAINING); +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 = module.exports = apply; +global.Hydro.handler.training = apply; diff --git a/hydro/handler/user.js b/hydro/handler/user.ts similarity index 90% rename from hydro/handler/user.js rename to hydro/handler/user.ts index e066ec77a..5350b9527 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.ts @@ -1,27 +1,20 @@ -const superagent = require('superagent'); -const moment = require('moment-timezone'); -const { Route, Handler } = require('../service/server.js'); -const user = require('../model/user'); -const token = require('../model/token'); -const record = require('../model/record'); -const problem = require('../model/problem'); -const task = require('../model/task'); -const system = require('../model/system'); -const { sendMail } = require('../lib/mail'); -const misc = require('../lib/misc'); -const { +import superagent from 'superagent'; +import moment from 'moment-timezone'; +import { Route, Handler } 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 { sendMail } from '../lib/mail'; +import * as misc from '../lib/misc'; +import { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError, SystemError, BlacklistedError, UserFacingError, -} = require('../error'); -const { - PERM: { - PERM_VIEW_PROBLEM_HIDDEN, - }, - PRIV: { - PRIV_USER_PROFILE, PRIV_REGISTER_USER, PRIV_NONE, - }, -} = require('../model/builtin'); +} from '../error'; class UserLoginHandler extends Handler { async get() { @@ -41,7 +34,7 @@ class UserLoginHandler extends Handler { if (!udoc) throw new LoginError(uname); udoc.checkPassword(password); await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); - if (udoc.priv === PRIV_NONE) throw new BlacklistedError(uname); + 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; @@ -116,7 +109,7 @@ class UserLostPassHandler extends Handler { async post({ mail }) { if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail'); - const udoc = await user.getByEmail(mail); + const udoc = await user.getByEmail('system', mail); if (!udoc) throw new UserNotFoundError(mail); const [tid] = await token.add( token.TYPE_LOSTPASS, @@ -155,10 +148,14 @@ class UserDetailHandler extends Handler { const rdocs = await record.getByUid(domainId, uid); const pdict = await problem.getList( domainId, rdocs.map((rdoc) => rdoc.pid), - this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN), false, + this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false, ); // Remove sensitive data - if (!isSelfProfile && sdoc) sdoc.createIp = sdoc.updateIp = sdoc._id = ''; + if (!isSelfProfile && sdoc) { + sdoc.createIp = ''; + sdoc.updateIp = ''; + sdoc._id = ''; + } this.response.template = 'user_detail.html'; this.response.body = { isSelfProfile, udoc, sdoc, rdocs, pdict, @@ -300,7 +297,7 @@ class OauthCallbackHandler extends Handler { if (udoc) { this.session.uid = udoc._id; } else { - this.checkPriv(PRIV_REGISTER_USER); + this.checkPriv(PRIV.PRIV_REGISTER_USER); let username = ''; r.uname = r.uname || []; r.uname.push(String.random(16)); @@ -315,7 +312,7 @@ class OauthCallbackHandler extends Handler { const uid = await user.create({ mail: r.email, uname: username, password: String.random(32), regip: this.request.ip, }); - const $set = { + const $set: any = { oauth: args.type, }; if (r.bio) $set.bio = r.bio; @@ -326,18 +323,18 @@ class OauthCallbackHandler extends Handler { } } -async function apply() { +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_REGISTER_USER); - Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV_REGISTER_USER); - Route('user_logout', '/logout', UserLogoutHandler, PRIV_USER_PROFILE); + 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); - Route('user_delete', '/user/delete', UserDeleteHandler, PRIV_USER_PROFILE); + Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE); Route('user_detail', '/user/:uid', UserDetailHandler); } -global.Hydro.handler.user = module.exports = apply; +global.Hydro.handler.user = apply; diff --git a/hydro/handler/wiki.js b/hydro/handler/wiki.ts similarity index 80% rename from hydro/handler/wiki.js rename to hydro/handler/wiki.ts index 9c8fd1336..ff894afe4 100644 --- a/hydro/handler/wiki.js +++ b/hydro/handler/wiki.ts @@ -1,5 +1,5 @@ -const { Route, Handler } = require('../service/server'); -const { NotFoundError } = require('../error'); +import { NotFoundError } from '../error'; +import { Route, Handler } from '../service/server'; class WikiHandler extends Handler { async get({ category = 'wiki', page }) { @@ -18,9 +18,9 @@ class WikiHandler extends Handler { } } -async function apply() { +export async function apply() { Route('wiki', '/wiki/:page', WikiHandler); Route('wiki_with_category', '/wiki/:category/:page', WikiHandler); } -global.Hydro.handler.wiki = module.exports = apply; +global.Hydro.handler.wiki = apply; diff --git a/hydro/interface.js b/hydro/interface.js deleted file mode 100644 index 179c6eca1..000000000 --- a/hydro/interface.js +++ /dev/null @@ -1,77 +0,0 @@ -const { ObjectID } = require('mongodb'); - -exports.udoc = { - _id: 0, - mail: '', - mailLower: '', - uname: '', - unameLower: '', - salt: '', - hash: '', - hashType: 'hydro', - nAccept: 0, - nSubmit: 0, - nLike: 0, - bio: '', - gender: 0, - regat: new Date(), - regip: '0.0.0.0', - gravatar: '', - viewLang: 'zh_CN', - loginat: new Date(), - loginip: '0.0.0.0', - priv: '', -}; - -exports.pdoc = { - _id: new ObjectID(), - pid: 'P1000', - owner: 0, - title: 'No Title', - content: 'No Content', - nSubmit: 0, - nAccept: 0, - tag: [], - category: [], - data: null, - hidden: false, -}; - -/* -export interface TestCase { - time: number, - memory: number, - status: number, - message: string -} -export interface Rdoc { - _id: ObjectID, - pid: ObjectID, - 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 -} -export interface Bdoc { - _id: string, - expireAt: Date -} -export interface Tdoc { - _id: ObjectID - beginAt: Date - endAt: Date - attend: number - title: string - content: string - pids: ObjectID[] -} -*/ diff --git a/hydro/interface.ts b/hydro/interface.ts deleted file mode 100644 index bf358f985..000000000 --- a/hydro/interface.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ObjectID } from 'mongodb'; - -export interface Udoc { - _id: number - mail: string - mailLower: string - uname: string - unameLower: string - salt: string - hash: string - hashType: string - nAccept: number - nSubmit: number - nLike: number - perm: string - viewLang: string - codeLang: string - codeTemplate: string - gravatar: string - hasPerm: Function - checkPassword: Function -} -export interface Pdoc { - _id: ObjectID - pid: string - owner: number - title: string - content: string - nSubmit: number - nAccept: number - tag: string[] - category: string[], - data: ObjectID | null - hidden: boolean -} -export interface TestCase { - time: number, - memory: number, - status: number, - message: string -} -export interface Rdoc { - _id: ObjectID, - pid: ObjectID, - 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 -} -export interface Bdoc { - _id: string, - expireAt: Date -} -export interface Tdoc { - _id: ObjectID - beginAt: Date - endAt: Date - attend: number - title: string - content: string - pids: ObjectID[] -} \ No newline at end of file diff --git a/hydro/lib/download.js b/hydro/lib/download.js deleted file mode 100644 index a9f1b49ff..000000000 --- a/hydro/lib/download.js +++ /dev/null @@ -1,19 +0,0 @@ -const superagent = require('superagent'); -const fs = require('fs'); - -async function _download(url, path, retry) { - const w = fs.createWriteStream(path); - await superagent.get(url).retry(retry).pipe(w); - await new Promise((resolve, reject) => { - w.on('finish', resolve); - w.on('error', reject); - }); - return path; -} - -function download(url, path, retry = 3) { - if (path) return _download(url, path, retry); - return superagent.get(url).retry(retry); -} - -global.Hydro.lib.download = module.exports = download; diff --git a/hydro/lib/download.ts b/hydro/lib/download.ts new file mode 100644 index 000000000..e6aa19796 --- /dev/null +++ b/hydro/lib/download.ts @@ -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 | undefined | null, retry = 3) { + if (path) return _download(url, path, retry); + return superagent.get(url).retry(retry); +} + +global.Hydro.lib.download = download; diff --git a/hydro/lib/hash.hydro.js b/hydro/lib/hash.hydro.js deleted file mode 100644 index 309fcd3fd..000000000 --- a/hydro/lib/hash.hydro.js +++ /dev/null @@ -1,7 +0,0 @@ -const crypto = require('crypto'); - -function hash(password, salt) { - return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256').toString('hex').substr(0, 64); -} - -global.Hydro.lib['hash.hydro'] = module.exports = hash; diff --git a/hydro/lib/hash.hydro.ts b/hydro/lib/hash.hydro.ts new file mode 100644 index 000000000..ac610893a --- /dev/null +++ b/hydro/lib/hash.hydro.ts @@ -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; diff --git a/hydro/lib/hpm.js b/hydro/lib/hpm.ts similarity index 79% rename from hydro/lib/hpm.js rename to hydro/lib/hpm.ts index 4c1eae2f9..ed2d91275 100644 --- a/hydro/lib/hpm.js +++ b/hydro/lib/hpm.ts @@ -1,13 +1,13 @@ /* eslint-disable import/no-dynamic-require */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const download = require('./download'); -const { folderSize } = require('../utils'); +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import download from './download'; +import { folderSize } from '../utils'; -function root(name) { +function root(name: string) { return path.resolve(process.cwd(), name); } @@ -17,7 +17,8 @@ const moduleRoots = [ root(path.resolve(os.homedir(), '.hydro', 'module')), root('.'), ]; -let moduleRoot; + +let moduleRoot: string; for (const i of moduleRoots) { if (fs.existsSync(i) && fs.statSync(i).isDirectory()) { moduleRoot = i; @@ -25,7 +26,7 @@ for (const i of moduleRoots) { } } -async function getInstalled() { +export async function getInstalled() { const modules = []; const files = fs.readdirSync(`${os.tmpdir()}/hydro/tmp`); for (const file of files) { @@ -37,7 +38,7 @@ async function getInstalled() { return modules; } -async function getDetail() { +export async function getDetail() { const modules = []; const files = fs.readdirSync(`${os.tmpdir()}/hydro/tmp`); for (const file of files) { @@ -56,14 +57,14 @@ async function getDetail() { return modules; } -async function del(id) { +export async function del(id: string) { fs.unlinkSync(root(`${moduleRoot}/${id}.hydro`)); } -function install(url) { +export function install(url: string) { return download(url, root(`${moduleRoot}/${String.random(16)}.hydro`)); } -global.Hydro.lib.hpm = module.exports = { +global.Hydro.lib.hpm = { getInstalled, getDetail, del, install, }; diff --git a/hydro/lib/i18n.js b/hydro/lib/i18n.ts similarity index 79% rename from hydro/lib/i18n.js rename to hydro/lib/i18n.ts index c707f318d..da36b2eb5 100644 --- a/hydro/lib/i18n.js +++ b/hydro/lib/i18n.ts @@ -1,5 +1,13 @@ const locales = {}; +declare global { + interface String { + format: (...args: Array) => string; + rawformat: (object: any) => string; + translate: (language: string | undefined) => string; + } +} + String.prototype.format = function formatStr(...args) { let result = this; if (args.length > 0) { @@ -24,7 +32,7 @@ String.prototype.format = function formatStr(...args) { String.prototype.rawformat = function rawFormat(object) { const res = this.split('{@}'); - return [res[0], object, res[1]]; + return [res[0], object, res[1]].join(); }; String.prototype.translate = function translate(language = 'zh_CN') { @@ -34,7 +42,7 @@ String.prototype.translate = function translate(language = 'zh_CN') { return this; }; -function load(data) { +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]); @@ -42,4 +50,4 @@ function load(data) { } global.Hydro.locales = locales; -global.Hydro.lib.i18n = module.exports = load; +global.Hydro.lib.i18n = load; diff --git a/hydro/lib/jwt.js b/hydro/lib/jwt.ts similarity index 75% rename from hydro/lib/jwt.js rename to hydro/lib/jwt.ts index 6a90687b7..e41334841 100644 --- a/hydro/lib/jwt.js +++ b/hydro/lib/jwt.ts @@ -1,13 +1,13 @@ -function decodeBase64WithUriEncoding(encodedText) { +function decodeBase64WithUriEncoding(encodedText: string) { return Buffer.from(encodedText, 'base64').toString('utf8'); } -function unescapedString(escapedString) { +function unescapedString(escapedString: string) { escapedString += new Array(5 - (escapedString.length % 4)).join('='); return escapedString.replace(/-/g, '+').replace(/_/g, '/'); } -function decodeJWT(idToken) { +function decodeJWT(idToken: string) { const token = idToken.split('.'); if (token.length !== 3) { throw new Error('Invalid idToken'); @@ -27,9 +27,10 @@ function decodeJWT(idToken) { } } -function decode(idToken) { +// eslint-disable-next-line import/prefer-default-export +export function decode(idToken: string) { const decodedJWT = decodeJWT(idToken); return decodedJWT.payload; } -global.Hydro.lib.jwt = module.exports = { decode }; +global.Hydro.lib.jwt = { decode }; diff --git a/hydro/lib/logger.js b/hydro/lib/logger.ts similarity index 50% rename from hydro/lib/logger.js rename to hydro/lib/logger.ts index 628ca7683..cdfd582c7 100644 --- a/hydro/lib/logger.js +++ b/hydro/lib/logger.ts @@ -1,7 +1,7 @@ -const yaml = require('js-yaml'); +import yaml from 'js-yaml'; -function warp(func, type) { - return (...args) => { +function wrap(func: Function, type: string) { + return (...args: any) => { const time = new Date(); func(...args); if (global.Hydro.model.message) { @@ -21,13 +21,27 @@ function warp(func, type) { } 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 = warp(console.log, 'log'); - this.error = warp(console.error, 'error'); - this.info = warp(console.info, 'info'); - this.warn = warp(console.warn, 'warn'); - this.debug = warp(console.debug, 'debug'); + 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'); } } -global.Hydro.lib.logger = module.exports = new Logger(); +const exp = new Logger(); + +global.Hydro.lib.logger = exp; + +export default exp; diff --git a/hydro/lib/mail.js b/hydro/lib/mail.ts similarity index 82% rename from hydro/lib/mail.js rename to hydro/lib/mail.ts index 58eb7488a..da3b25170 100644 --- a/hydro/lib/mail.js +++ b/hydro/lib/mail.ts @@ -1,4 +1,5 @@ -const nodemailer = require('nodemailer'); +import nodemailer from 'nodemailer'; + const { SendMailError } = require('../error'); /** @@ -7,8 +8,9 @@ const { SendMailError } = require('../error'); * @param {string} text * @param {string} html */ -async function sendMail(to, subject, text, html) { - let t; +// eslint-disable-next-line import/prefer-default-export +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([ @@ -40,6 +42,6 @@ async function sendMail(to, subject, text, html) { return t; } -global.Hydro.lib.mail = module.exports = { +global.Hydro.lib.mail = { sendMail, }; diff --git a/hydro/lib/markdown.js b/hydro/lib/markdown.ts similarity index 81% rename from hydro/lib/markdown.js rename to hydro/lib/markdown.ts index 6ade6c702..7cb3e5c3d 100644 --- a/hydro/lib/markdown.js +++ b/hydro/lib/markdown.ts @@ -1,12 +1,10 @@ -const MarkdownIt = require('markdown-it'); - -const md = MarkdownIt(); -const Prism = require('prismjs'); +import MarkdownIt from 'markdown-it'; +import Prism from 'prismjs'; // For math: $a\times b\eq 10$ -const katex = require('@hydrooj/markdown-it-katex'); +import katex from '@hydrooj/markdown-it-katex'; // Specific image size: ![image](image.png =100x100) -const Imsize = require('markdown-it-imsize'); +import Imsize from 'markdown-it-imsize'; /* Footnote support. Here is a footnote reference,[^1] and another.[^longnote] @@ -17,17 +15,18 @@ Here is a footnote reference,[^1] and another.[^longnote] Subsequent paragraphs are indented to show that they belong to the previous footnote. */ -const Footnote = require('markdown-it-footnote'); +import Footnote from 'markdown-it-footnote'; // ==Highlight== -const Mark = require('markdown-it-mark'); +import Mark from 'markdown-it-mark'; // :::warn This page requires javascript. ::: // :::record-pass Accepted ::: -const Container = require('markdown-it-container'); -let Anchor = require('markdown-it-anchor'); -// For webpack -if (Anchor.default) Anchor = Anchor.default; +import Container from 'markdown-it-container'; -const TOC = require('markdown-it-table-of-contents'); +import TOC from 'markdown-it-table-of-contents'; + +import Anchor from 'markdown-it-anchor'; + +const md = MarkdownIt(); require('prismjs/components/index'); @@ -78,7 +77,9 @@ class Markdown extends MarkdownIt { } } -global.Hydro.lib.markdown = module.exports = { - unsafe: new Markdown(false), - safe: new Markdown(true), +export const safe = new Markdown(true); +export const unsafe = new Markdown(false); + +global.Hydro.lib.markdown = { + safe, unsafe, }; diff --git a/hydro/lib/md5.js b/hydro/lib/md5.ts similarity index 97% rename from hydro/lib/md5.js rename to hydro/lib/md5.ts index ce026606d..e03b8c8ab 100644 --- a/hydro/lib/md5.js +++ b/hydro/lib/md5.ts @@ -130,4 +130,8 @@ function rstr2hex(input) { return output; } -global.Hydro.lib.md5 = module.exports = (s) => rstr2hex(rstrMD5(unescape(encodeURIComponent(s)))); +export default function md5(s: string) { + return rstr2hex(rstrMD5(unescape(encodeURIComponent(s)))); +} + +global.Hydro.lib.md5 = md5; diff --git a/hydro/lib/misc.js b/hydro/lib/misc.ts similarity index 81% rename from hydro/lib/misc.js rename to hydro/lib/misc.ts index 337212a72..b1bcf6e42 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.ts @@ -1,17 +1,17 @@ -const { default: AnsiUp } = require('ansi_up'); -const md5 = require('./md5'); +import AnsiUp from 'ansi_up'; +import md5 from './md5'; const AU = new AnsiUp(); -function ansiToHtml(str) { +export function ansiToHtml(str) { return AU.ansi_to_html(str); } -function gravatar(email, s = 32) { +export function gravatar(email, s = 32) { return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${s}`; } -function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { +export function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { if (!dt) return 'DATETIME_SPAN_ERROR'; if (dt.generationTime) dt = new Date(dt.generationTime * 1000); else if (typeof dt === 'number' || typeof dt === 'string') dt = new Date(dt); @@ -22,7 +22,7 @@ function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { ); } -function* paginate(page, numPages) { +export function* paginate(page, numPages) { const radius = 2; let first; let last; if (page > 1) { @@ -45,7 +45,7 @@ function* paginate(page, numPages) { yield ['last', numPages]; } -function size(s, base = 1) { +export function size(s, base = 1) { s *= base; const unit = 1024; const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; @@ -56,13 +56,13 @@ function size(s, base = 1) { return '{0} {1}'.format(Math.round(s * unit), unitNames[unitNames.length - 1]); } -function _digit2(number) { +export function _digit2(number: number) { if (number < 10) return `0${number}`; return number.toString(); } -function formatSeconds(seconds = 0) { - seconds = parseInt(seconds, 10); +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)), @@ -70,6 +70,6 @@ function formatSeconds(seconds = 0) { ); } -global.Hydro.lib.misc = module.exports = { +global.Hydro.lib.misc = { gravatar, datetimeSpan, paginate, size, formatSeconds, ansiToHtml, }; diff --git a/hydro/lib/nav.js b/hydro/lib/nav.ts similarity index 56% rename from hydro/lib/nav.js rename to hydro/lib/nav.ts index b20a34d73..a82711247 100644 --- a/hydro/lib/nav.js +++ b/hydro/lib/nav.ts @@ -1,27 +1,34 @@ -const { isNull } = require('lodash'); -const { PRIV, PERM } = require('../model/builtin'); +import { isNull } from 'lodash'; +import { PERM, PRIV } from '../model/builtin'; global.Hydro.ui.nav = []; const trueChecker = () => true; -const Checker = (perm, priv, checker = trueChecker) => (handler) => ( +const Checker = (perm: string, priv: number, checker: Function = trueChecker) => (handler) => ( checker(handler) && (perm ? handler.user.hasPerm(perm) : true) && (priv ? handler.user.hasPriv(priv) : true) ); -const Item = (name, args, prefix, ...permPrivChecker) => { - let _priv; - let _perm; - let checker = trueChecker; +const Item = ( + name: string, args: any, prefix: string, + ...permPrivChecker: Array> +) => { + let _priv: number; + let _perm: string; + let checker: Function = trueChecker; for (const item of permPrivChecker) { - if (typeof item === 'object' && !isNull(item)) { - if (typeof item.call !== 'undefined') { + if (item instanceof Object && !isNull(item)) { + if (item instanceof Array) { + if (typeof item[0] === 'number') { + // @ts-ignore + _priv = item; + } else if (typeof item[0] === 'string') { + // @ts-ignore + _perm = item; + } + } else if (typeof item.call !== 'undefined') { checker = item; - } if (typeof item[0] === 'number') { - _priv = item; - } else if (typeof item[0] === 'string') { - _perm = item; } } else if (typeof item === 'number') { _priv = item; @@ -46,4 +53,5 @@ 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 = module.exports = Item; +global.Hydro.lib.nav = Item; +export default Item; diff --git a/hydro/lib/paginate.js b/hydro/lib/paginate.ts similarity index 50% rename from hydro/lib/paginate.js rename to hydro/lib/paginate.ts index d970ced74..cd38eae38 100644 --- a/hydro/lib/paginate.js +++ b/hydro/lib/paginate.ts @@ -1,12 +1,6 @@ -const { ValidationError } = require('../error'); +import { ValidationError } from '../error'; -/** - * @param {import('mongodb').Cursor} cursor - * @param {number} page - * @param {number} pageSize - * @returns {Promise} pageDocs numPages count - */ -async function paginate(cursor, page, pageSize) { +async function paginate(cursor: import('mongodb').Cursor, page: number, pageSize: number): Promise<[any[], number, number]> { if (page <= 0) throw new ValidationError('page'); const [count, pageDocs] = await Promise.all([ cursor.count(), @@ -16,4 +10,5 @@ async function paginate(cursor, page, pageSize) { return [pageDocs, numPages, count]; } -global.Hydro.lib.paginate = module.exports = paginate; +global.Hydro.lib.paginate = paginate; +export default paginate; diff --git a/hydro/lib/rank.js b/hydro/lib/rank.ts similarity index 82% rename from hydro/lib/rank.js rename to hydro/lib/rank.ts index 92ae1697e..53a8e450b 100644 --- a/hydro/lib/rank.js +++ b/hydro/lib/rank.ts @@ -10,4 +10,5 @@ function* ranked(diter, equ = (a, b) => a === b) { } } -global.Hydro.lib.rank = module.exports = ranked; +global.Hydro.lib.rank = ranked; +export default ranked; diff --git a/hydro/lib/rating.js b/hydro/lib/rating.ts similarity index 91% rename from hydro/lib/rating.js rename to hydro/lib/rating.ts index d18cea593..ea476a9e2 100644 --- a/hydro/lib/rating.js +++ b/hydro/lib/rating.ts @@ -1,5 +1,13 @@ class User { - constructor(rank, old, uid = 0) { + rank: any; + + old: any; + + seed: number; + + uid: number; + + constructor(rank: number, old: number, uid = 0) { this.rank = rank; this.old = old; this.seed = 1; @@ -8,6 +16,8 @@ class User { } class RatingCalculator { + users: any; + constructor(users) { this.users = []; for (const user of users) { @@ -80,9 +90,10 @@ class RatingCalculator { } } -function calculate(users) { +function calculate(users: any[]) { const calculator = new RatingCalculator(users); return calculator.calculate(); } -module.exports = calculate; +global.Hydro.lib.rating = calculate; +export default calculate; diff --git a/hydro/lib/readConfig.js b/hydro/lib/readConfig.ts similarity index 76% rename from hydro/lib/readConfig.js rename to hydro/lib/readConfig.ts index 2dcd95759..27fa6aada 100644 --- a/hydro/lib/readConfig.js +++ b/hydro/lib/readConfig.ts @@ -1,6 +1,6 @@ -const Zip = require('adm-zip'); +import Zip from 'adm-zip'; -async function readConfig(filePath) { +async function readConfig(filePath: string) { const data = new Zip(filePath); const entries = data.getEntries(); for (const entry of entries) { @@ -16,4 +16,5 @@ async function readConfig(filePath) { return ''; } -global.Hydro.lib.readConfig = module.exports = readConfig; +global.Hydro.lib.readConfig = readConfig; +export default readConfig; diff --git a/hydro/lib/sha1.js b/hydro/lib/sha1.js deleted file mode 100644 index 9731a07a5..000000000 --- a/hydro/lib/sha1.js +++ /dev/null @@ -1,10 +0,0 @@ -const crypto = require('crypto'); -/** - * @param {any} content - * @return {string} - */ -global.Hydro.lib.sha1 = module.exports = (content) => { - const hash = crypto.createHash('sha1'); - hash.update(content); - return hash.digest('hex'); -}; diff --git a/hydro/lib/sha1.ts b/hydro/lib/sha1.ts new file mode 100644 index 000000000..268b9050d --- /dev/null +++ b/hydro/lib/sha1.ts @@ -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; diff --git a/hydro/lib/sysinfo.js b/hydro/lib/sysinfo.ts similarity index 86% rename from hydro/lib/sysinfo.js rename to hydro/lib/sysinfo.ts index 9df703247..77d714649 100644 --- a/hydro/lib/sysinfo.js +++ b/hydro/lib/sysinfo.ts @@ -1,9 +1,11 @@ -const systeminformation = require('systeminformation'); -const { size } = require('./misc'); +import systeminformation from 'systeminformation'; +import { size } from './misc'; -const cache = {}; +const cache = { + mid: '', cpu: '', osinfo: '', flags: '', +}; -async function get() { +export async function get() { const [Cpu, Memory, OsInfo, CurrentLoad, CpuFlags, CpuTemp, Battery] = await Promise.all([ systeminformation.cpu(), systeminformation.mem(), @@ -31,7 +33,7 @@ async function get() { }; } -async function update() { +export async function update(): Promise<[string, any, any]> { const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([ systeminformation.mem(), systeminformation.currentLoad(), @@ -57,4 +59,4 @@ async function update() { ]; } -global.Hydro.lib.sysinfo = module.exports = { get, update }; +global.Hydro.lib.sysinfo = { get, update }; diff --git a/hydro/lib/template.js b/hydro/lib/template.ts similarity index 81% rename from hydro/lib/template.js rename to hydro/lib/template.ts index 9977097d0..300655fba 100644 --- a/hydro/lib/template.js +++ b/hydro/lib/template.ts @@ -1,7 +1,7 @@ -const nunjucks = require('nunjucks'); -const markdown = require('./markdown'); -const { STATUS, PERM, PRIV } = require('../model/builtin'); -const misc = require('./misc'); +import nunjucks from 'nunjucks'; +import * as markdown from './markdown'; +import { STATUS, PERM, PRIV } from '../model/builtin'; +import * as misc from './misc'; function Loader() { } Loader.prototype.getSource = function getSource(name) { @@ -28,7 +28,7 @@ class Nunjucks extends nunjucks.Environment { } const env = new Nunjucks(); -function render(name, state) { +export function render(name: string, state: any): Promise { return new Promise((resolve, reject) => { env.render(name, { page_name: name.split('.')[0], @@ -44,7 +44,7 @@ function render(name, state) { gravatar: misc.gravatar, model: global.Hydro.model, Context: global.Hydro.ui, - isIE: (str) => str.includes('MSIE') || str.includes('rv:11.0'), + isIE: (str: string) => str.includes('MSIE') || str.includes('rv:11.0'), }, (err, res) => { if (err) reject(err); else resolve(res); @@ -52,4 +52,4 @@ function render(name, state) { }); } -global.Hydro.lib.template = module.exports = { render }; +global.Hydro.lib.template = { render }; diff --git a/hydro/lib/testdata.convert.ini.js b/hydro/lib/testdata.convert.ini.ts similarity index 73% rename from hydro/lib/testdata.convert.ini.js rename to hydro/lib/testdata.convert.ini.ts index 11046ddc4..c88c909e7 100644 --- a/hydro/lib/testdata.convert.ini.js +++ b/hydro/lib/testdata.convert.ini.ts @@ -1,15 +1,15 @@ -const yaml = require('js-yaml'); +import yaml from 'js-yaml'; -function convert(ini) { +export default function convert(ini: string) { const f = ini.split('\n'); - const count = f[0]; + 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) }; - cur.time = `${time}s`; + 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`; - else cur.memory = '128m'; res.cases.push(cur); } if (res.cases.length > 0) { @@ -29,4 +29,4 @@ function convert(ini) { }); } -global.Hydro.lib['testdata.convert.ini'] = module.exports = convert; +global.Hydro.lib['testdata.convert.ini'] = convert; diff --git a/hydro/lib/useragent.js b/hydro/lib/useragent.js deleted file mode 100644 index 90d38f442..000000000 --- a/hydro/lib/useragent.js +++ /dev/null @@ -1,11 +0,0 @@ -const browser = require('detect-browser'); - -function parse(str) { - return browser.parseUserAgent(str); -} - -function icon(str) { - return str.split(' ')[0].toLowerCase(); -} - -global.Hydro.lib.useragent = module.exports = { parse, icon }; diff --git a/hydro/lib/useragent.ts b/hydro/lib/useragent.ts new file mode 100644 index 000000000..e414b5040 --- /dev/null +++ b/hydro/lib/useragent.ts @@ -0,0 +1,12 @@ +import 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 }; +export default { parse, icon }; diff --git a/hydro/lib/validator.js b/hydro/lib/validator.js deleted file mode 100644 index c4d233761..000000000 --- a/hydro/lib/validator.js +++ /dev/null @@ -1,91 +0,0 @@ -const { ValidationError } = require('../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; - -const isTitle = (s) => s && s.length < 64; -const checkTitle = (s) => { if (!isTitle(s)) throw new ValidationError('title'); else return s; }; -const isUid = (s) => RE_UID.test(s); -const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; }; -const isUname = (s) => RE_UNAME.test(s); -const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; }; -const isRole = (s) => RE_ROLE.test(s); -const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; }; -const isPassword = (s) => s.length >= 5; -const checkPassword = (s) => { if (!isPassword(s)) throw new ValidationError('password'); else return s; }; -const isEmail = (s) => RE_MAIL.test(s); -const checkEmail = (s) => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; }; -const isContent = (s) => s && s.length < 65536; -const checkContent = (s) => { if (!isContent(s)) throw new ValidationError('content'); else return s; }; -const isName = (s) => s && s.length < 256; -const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; }; -const isPid = (s) => RE_PID.test(s.toString()); -const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; }; -const isIntro = () => true; -const checkIntro = (s) => { if (!isIntro(s)) throw new ValidationError('intro'); else return s; }; -const isDescription = () => true; -const checkDescription = (s) => { if (!isDescription(s)) throw new ValidationError('description'); else return s; }; - -global.Hydro.lib.validator = module.exports = { - 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 constant.language.LANG_TEXTS - -def check_lang(i): - if not is_lang(i): - raise error.ValidationError('lang') - -*/ diff --git a/hydro/lib/validator.ts b/hydro/lib/validator.ts new file mode 100644 index 000000000..796e29459 --- /dev/null +++ b/hydro/lib/validator.ts @@ -0,0 +1,91 @@ +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; + +export const isTitle = (s) => s && s.length < 64; +export const checkTitle = (s) => { if (!isTitle(s)) 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 (!isPassword(s)) throw new ValidationError('password'); else return s; }; +export const isEmail = (s) => RE_MAIL.test(s); +export const checkEmail = (s) => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; }; +export const isContent = (s) => s && s.length < 65536; +export const checkContent = (s) => { if (!isContent(s)) 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 = () => true; +export const checkDescription = (s) => { if (!isDescription()) throw new ValidationError('description'); else return s; }; + +global.Hydro.lib.validator = { + 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') + +*/ diff --git a/hydro/loader.js b/hydro/loader.ts similarity index 83% rename from hydro/loader.js rename to hydro/loader.ts index 22a32e05c..ff6887b25 100644 --- a/hydro/loader.js +++ b/hydro/loader.ts @@ -1,8 +1,25 @@ /* eslint-disable import/no-dynamic-require */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ -const cluster = require('cluster'); -const { argv } = require('yargs'); + +import './declare'; +import cluster from 'cluster'; +import { argv } from 'yargs'; + +global.Hydro = { + stat: { reqCount: 0 }, + handler: {}, + service: {}, + model: {}, + script: {}, + lib: {}, + wiki: {}, + template: {}, + ui: {}, + error: {}, + locales: {}, +}; +global.onDestory = []; if (argv.debug) console.log(process.argv); @@ -14,25 +31,25 @@ async function terminate() { process.exit(0); } -async function fork(args = []) { +async function fork(args: string[] = []) { cluster.setupMaster({ args }); return cluster.fork(); } -async function entry(config) { +async function entry(config: any) { if (config.entry) { if (config.newProcess) { const p = await fork([`--entry=${config.entry}`]); await new Promise((resolve, reject) => { p.on('exit', resolve); - p.on('error', (err) => { + p.on('error', (err: Error) => { p.kill(); reject(err); }); }); } else { const loader = require(`./entry/${config.entry}`); - return await loader(entry); + return await loader.load(entry); } } return null; @@ -42,12 +59,12 @@ async function stopWorker() { cluster.disconnect(); } -async function startWorker(cnt) { +async function startWorker(cnt: number) { await fork(['--firstWorker']); for (let i = 1; i < cnt; i++) await fork(); } -async function executeCommand(input) { +async function executeCommand(input: string) { try { const t = eval(input.toString().trim()); if (t instanceof Promise) console.log(await t); @@ -57,13 +74,13 @@ async function executeCommand(input) { } } -async function messageHandler(worker, msg) { +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) { - if (i !== worker.id) cluster.workers[i].send(msg); + cluster.workers[i].send(msg); } } else { global.Hydro.service.bus.publish(msg.eventName, msg.payload, false); @@ -88,18 +105,6 @@ async function load() { 'js-yaml': require('js-yaml'), mongodb: require('mongodb'), }; - global.Hydro = { - stat: { reqCount: 0 }, - handler: {}, - service: {}, - model: {}, - script: {}, - lib: {}, - wiki: {}, - template: {}, - ui: {}, - }; - global.onDestory = []; Error.stackTraceLimit = 50; process.on('unhandledRejection', (e) => console.error(e)); process.on('SIGINT', terminate); @@ -108,7 +113,8 @@ async function load() { if (cluster.isMaster || argv.startAsMaster) { console.log(`Master ${process.pid} Starting`); process.stdin.setEncoding('utf8'); - process.stdin.on('data', (input) => { + 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) }); @@ -140,6 +146,7 @@ async function load() { 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`); diff --git a/hydro/model/blacklist.js b/hydro/model/blacklist.ts similarity index 65% rename from hydro/model/blacklist.js rename to hydro/model/blacklist.ts index ac46dfaca..42e5d8ec7 100644 --- a/hydro/model/blacklist.js +++ b/hydro/model/blacklist.ts @@ -1,24 +1,24 @@ -const db = require('../service/db'); +import * as db from '../service/db'; const coll = db.collection('blacklist'); -async function add(ip) { +export async function add(ip: string) { const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000); return coll.findOneAndUpdate({ _id: ip }, { $set: { expireAt } }, { upsert: true }); } -function get(ip) { +export function get(ip: string) { return coll.findOne({ _id: ip }); } -function del(ip) { +export function del(ip: string) { return coll.deleteOne({ _id: ip }); } -function ensureIndexes() { +export function ensureIndexes() { return coll.createIndex('expireAt', { expireAfterSeconds: 0 }); } -global.Hydro.model.blacklist = module.exports = { +global.Hydro.model.blacklist = { add, get, del, ensureIndexes, }; diff --git a/hydro/model/builtin.js b/hydro/model/builtin.ts similarity index 92% rename from hydro/model/builtin.js rename to hydro/model/builtin.ts index 2ade5d280..7c83b6f2e 100644 --- a/hydro/model/builtin.js +++ b/hydro/model/builtin.ts @@ -1,4 +1,4 @@ -const PERM = { +export const PERM = { PERM_NONE: '', // Domain Settings @@ -30,8 +30,10 @@ const PERM = { PERM_REPLY_PROBLEM_SOLUTION: 'L', PERM_EDIT_PROBLEM_SOLUTION_REPLY: 'M', PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 'N', + PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'x', PERM_DELETE_PROBLEM_SOLUTION_REPLY: 'O', PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 'P', + PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'y', // Discussion PERM_VIEW_DISCUSSION: 'Q', @@ -75,11 +77,17 @@ const PERM = { // Ranking PERM_VIEW_RANKING: 'w', + + // Placeholder + PERM_ALL: '', + PERM_BASIC: '', + PERM_DEFAULT: '', + PERM_ADMIN: '', }; -const Permission = (family, key, desc) => ({ family, key, desc }); +export const Permission = (family, key, desc) => ({ family, key, desc }); -const PERMS = [ +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'), @@ -142,10 +150,10 @@ const PERMS = [ ]; let PERM_ALL = ''; -for (const p in PERM) PERM_ALL += PERM[p]; +for (const p in PERM) if (PERM[p]) PERM_ALL += PERM[p]; PERM.PERM_ALL = PERM_ALL; -const PERMS_BY_FAMILY = {}; +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); @@ -208,7 +216,7 @@ PERM.PERM_DEFAULT = PERM.PERM_VIEW PERM.PERM_ADMIN = PERM.PERM_ALL; -const PRIV = { +export const PRIV = { PRIV_NONE: 0, PRIV_EDIT_SYSTEM: 1 << 0, // renamed from PRIV_SET_PRIV PRIV_SET_PERM: 1 << 1, @@ -231,18 +239,19 @@ const PRIV = { PRIV_DELETE_FILE: 1 << 18, PRIV_DELETE_FILE_SELF: 1 << 19, PRIV_ALL: -1, + + PRIV_DEFAULT: 0, }; -PRIV.PRIV_DETAULT = PRIV.PRIV_USER_PROFILE +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 - + PRIV.PRIV_USERFILE; + + PRIV.PRIV_DELETE_FILE_SELF; // [10, 1] means that people whose rank is less than 1% will get Level 10 -const LEVELS = [ +export const LEVELS = [ [10, 1], [9, 2], [8, 10], @@ -255,7 +264,7 @@ const LEVELS = [ [1, 100], ]; -const BUILTIN_USERS = [ +export const BUILTIN_USERS = [ { _id: 0, mail: 'Guest@hydro.local', @@ -288,13 +297,13 @@ const BUILTIN_USERS = [ }, ]; -const BUILTIN_ROLES = { +export const BUILTIN_ROLES = { guest: { perm: PERM.PERM_BASIC }, default: { perm: PERM.PERM_DEFAULT }, admin: { perm: PERM.PERM_ADMIN }, }; -const DEFAULT_NODES = { +export const DEFAULT_NODES = { 探索: [ { pic: 'qa', name: '问答' }, { pic: 'share', name: '分享' }, @@ -353,7 +362,7 @@ const DEFAULT_NODES = { ], }; -const CATEGORIES = { +export const CATEGORIES = { 动态规划: [ 'LCS', 'LIS', @@ -490,15 +499,15 @@ const CATEGORIES = { ], }; -const FOOTER_EXTRA_HTMLS = []; +export const FOOTER_EXTRA_HTMLS = []; -const VIEW_LANGS = [ +export const VIEW_LANGS = [ { code: 'zh_CN', name: '简体中文' }, { code: 'zh_TW', name: '正體中文' }, { code: 'en', name: 'English' }, ]; -const LANG_TEXTS = { +export const LANG_TEXTS = { c: 'C', cc: 'C++', cs: 'C#', @@ -514,7 +523,7 @@ const LANG_TEXTS = { rb: 'Ruby', }; -const LANG_HIGHLIGHT_ID = { +export const LANG_HIGHLIGHT_ID = { c: 'c', cc: 'cpp', cs: 'csharp', @@ -530,7 +539,7 @@ const LANG_HIGHLIGHT_ID = { rb: 'ruby', }; -const STATUS = { +export const STATUS = { STATUS_WAITING: 0, STATUS_ACCEPTED: 1, STATUS_WRONG_ANSWER: 2, @@ -548,7 +557,7 @@ const STATUS = { STATUS_IGNORED: 30, }; -const STATUS_TEXTS = { +export const STATUS_TEXTS = { [STATUS.STATUS_WAITING]: 'Waiting', [STATUS.STATUS_ACCEPTED]: 'Accepted', [STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer', @@ -566,7 +575,7 @@ const STATUS_TEXTS = { [STATUS.STATUS_IGNORED]: 'Ignored', }; -const STATUS_CODES = { +export const STATUS_CODES = { 0: 'pending', 1: 'pass', 2: 'fail', @@ -584,7 +593,22 @@ const STATUS_CODES = { 30: 'ignored', }; -global.Hydro.model.builtin = module.exports = { +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]: '?', +}; + +global.Hydro.model.builtin = { PERM, PERMS, PERMS_BY_FAMILY, @@ -601,4 +625,10 @@ global.Hydro.model.builtin = module.exports = { STATUS, STATUS_TEXTS, STATUS_CODES, + USER_GENDER_MALE, + USER_GENDER_FEMALE, + USER_GENDER_OTHER, + USER_GENDERS, + USER_GENDER_RANGE, + USER_GENDER_ICONS, }; diff --git a/hydro/model/contest.js b/hydro/model/contest.ts similarity index 53% rename from hydro/model/contest.js rename to hydro/model/contest.ts index cb08b30d4..fe5c8b44a 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.ts @@ -1,28 +1,51 @@ -const user = require('./user'); -const problem = require('./problem'); -const { +import { ObjectID } from 'mongodb'; +import * as user from './user'; +import * as problem from './problem'; +import { ValidationError, ContestNotFoundError, ContestAlreadyAttendedError, ContestNotAttendedError, ContestScoreboardHiddenError, -} = require('../error'); -const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('./builtin').PERM; -const validator = require('../lib/validator'); -const ranked = require('../lib/rank'); -const document = require('./document'); +} from '../error'; +import * as document from './document'; +import { PERM } from './builtin'; +import * as validator from '../lib/validator'; +import * as misc from '../lib/misc'; +import ranked from '../lib/rank'; + +interface Column { + type: string, + value: string, + raw?: any +} + +export interface Tdoc { + _id: ObjectID, + domainId: string, + docId: ObjectID, + docType: number, + beginAt: Date, + endAt: Date, + penaltySince?: Date, + attend: number, + title: string, + content: string, + rule: string, + pids: number[] +} const acm = { TEXT: 'ACM/ICPC', check: () => { }, showScoreboard: () => true, showRecord: (tdoc, now) => now > tdoc.endAt, - stat: (tdoc, journal) => { + stat: (tdoc, journal: any[]) => { const naccept = {}; const effective = {}; const detail = []; let accept = 0; let time = 0; - for (const j in journal) { + for (const j of journal) { if (tdoc.pids.includes(j.pid) - && !(effective.includes(j.pid) && effective[j.pid].accept)) { + && !(effective[j.pid] && effective[j.pid].accept)) { effective[j.pid] = j; } if (!j.accept) naccept[j.pid]++; @@ -32,15 +55,19 @@ const acm = { const penalty = 20 * 60 * naccept[jdoc.pid]; return real + penalty; } - for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); + for (const j in effective) { + detail.push({ + ...effective[j], naccept: naccept[effective[j].pid], time: _time(effective[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 = [ + scoreboard(isExport: boolean, _: (s: string) => string, tdoc, rankedTsdocs, udict, pdict) { + const columns: Column[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'solved_problems', value: _('Solved Problems') }, @@ -130,7 +157,7 @@ const oi = { stat: (tdoc, journal) => { const detail = {}; let score = 0; - for (const j in journal) { + for (const j of journal) { if (tdoc.pids.includes(j.pid)) { detail[j.pid] = j; score += j.score; @@ -145,7 +172,7 @@ const oi = { return now > tdoc.endAt; }, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ + const columns: Column[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'total_score', value: _('Total Score') }, @@ -186,30 +213,156 @@ const oi = { rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), }; -const RULES = { - acm, oi, +const homework = { + TEXT: 'Assignment', + check: () => { }, + 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: Column[] = [ + { 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 (const index in tdoc.pids) { + const pid = tdoc.pids[index]; + if (isExport) { + columns.push( + { + type: 'problem_score', + value: '#{0} {1}'.format(index + 1, pdict[pid].title), + }, + { + type: 'problem_original_score', + value: '#{0} {1}'.format(index + 1, _('Original Score')), + }, + { + type: 'problem_time', + value: '#{0} {1}'.format(index + 1, _('Time (Seconds)')), + }, + { + type: 'problem_time_str', + value: '#{0} {1}'.format(index + 1, _('Time')), + }, + ); + } else { + columns.push({ + type: 'problem_detail', + value: '#{0}'.format(index + 1), + raw: pdict[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: udict[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 rdoc = (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: rdoc, + }); + } + } + rows.push(row); + } + return rows; + }, + rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score), }; -/** - * @typedef {import('mongodb').ObjectID} ObjectID - * @typedef {import('../interface').Tdoc} Tdoc - */ +export const RULES = { + acm, oi, homework, +}; -/** - * @param {string} domainId - * @param {string} title - * @param {string} content - * @param {number} owner - * @param {string} rule - * @param {Date} beginAt - * @param {Date} endAt - * @param {ObjectID[]} pids - * @param {object} data - * @returns {ObjectID} tid - */ -function add(domainId, title, content, owner, rule, - beginAt = new Date(), endAt = new Date(), pids = [], rated = false, - data = {}, type = document.TYPE_CONTEST) { +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, +) { validator.checkTitle(title); validator.checkContent(content); if (!this.RULES[rule]) throw new ValidationError('rule'); @@ -223,14 +376,10 @@ function add(domainId, title, content, owner, rule, }); } -/** - * @param {string} domainId - * @param {ObjectID} tid - * @param {object} $set - * @param {number} type - * @returns {Promise} tdoc after modification - */ -async function edit(domainId, tid, $set, type = document.TYPE_CONTEST) { +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'); } @@ -240,13 +389,7 @@ async function edit(domainId, tid, $set, type = document.TYPE_CONTEST) { return await document.set(domainId, type, tid, $set); } -/** - * @param {string} domainId - * @param {ObjectID} tid - * @param {number} type - * @returns {Promise} - */ -async function get(domainId, tid, type = document.TYPE_CONTEST) { +export async function get(domainId: string, tid: ObjectID, type = document.TYPE_CONTEST) { let tdoc; if (type === -1) { tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); @@ -256,8 +399,8 @@ async function get(domainId, tid, type = document.TYPE_CONTEST) { return tdoc; } -async function updateStatus( - domainId, tid, uid, rid, pid, +export async function updateStatus( + domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number, accept = false, score = 0, type = document.TYPE_CONTEST, ) { await get(domainId, tid, type); @@ -267,18 +410,27 @@ async function updateStatus( if (!tsdoc.attend) throw new ContestNotAttendedError(tid, uid); } -function getStatus(domainId, tid, uid, type = document.TYPE_CONTEST) { +export function getStatus( + domainId: string, tid: ObjectID, uid: number, + type = document.TYPE_CONTEST, +) { return document.getStatus(domainId, type, tid, uid); } -async function getListStatus(domainId, uid, tids, type = document.TYPE_CONTEST) { +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] = await getStatus(domainId, tid, uid, type); + for (const tid of tids) r[tid.toHexString()] = await getStatus(domainId, tid, uid, type); return r; } -async function attend(domainId, tid, uid, type = document.TYPE_CONTEST) { +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) { @@ -288,49 +440,61 @@ async function attend(domainId, tid, uid, type = document.TYPE_CONTEST) { return {}; } -function getMultiStatus(domainId, query, docType = document.TYPE_CONTEST) { +export function getMultiStatus(domainId: string, query: any, docType = document.TYPE_CONTEST) { return document.getMultiStatus(domainId, docType, query); } -function isNew(tdoc, days = 1) { +export function isNew(tdoc: Tdoc, days = 1) { const now = new Date().getTime(); const readyAt = tdoc.beginAt.getTime(); return (now < readyAt - days * 24 * 3600 * 1000); } -function isUpcoming(tdoc, days = 1) { + +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); + return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt.getTime()); } -function isNotStarted(tdoc) { + +export function isNotStarted(tdoc: Tdoc) { return (new Date()) < tdoc.beginAt; } -function isOngoing(tdoc) { + +export function isOngoing(tdoc: Tdoc) { const now = new Date(); return (tdoc.beginAt <= now && now < tdoc.endAt); } -function isDone(tdoc) { + +export function isDone(tdoc: Tdoc) { return tdoc.endAt <= new Date(); } -const ContestHandlerMixin = (c) => class extends c { +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_VIEW_CONTEST_HIDDEN_SCOREBOARD); + return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } - canShowRecord(tdoc, allowPermOverride = true) { + canShowRecord(tdoc: Tdoc, allowPermOverride = true) { if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true; - if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; + if (allowPermOverride && this.canViewHiddenScoreboard()) return true; return false; } - canShowScoreboard(tdoc, allowPermOverride = true) { + canShowScoreboard(tdoc: Tdoc, allowPermOverride = true) { if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true; - if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; + if (allowPermOverride && this.canViewHiddenScoreboard()) return true; return false; } - async getScoreboard(domainId, tid, isExport = false, docType = document.TYPE_CONTEST) { + async getScoreboard( + domainId: string, tid: ObjectID, + isExport = false, docType = document.TYPE_CONTEST, + ) { const tdoc = await get(domainId, tid, docType); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType) @@ -346,29 +510,32 @@ const ContestHandlerMixin = (c) => class extends c { return [tdoc, rows, udict]; } - async verifyProblems(domainId, pids) { // eslint-disable-line class-methods-use-this + // eslint-disable-next-line class-methods-use-this + async verifyProblems(domainId: string, pids: number[]) { await problem.getList(domainId, pids, true); return pids; } }; -function setStatus(domainId, tid, uid, $set) { +export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) { return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); } -function count(domainId, query, type = document.TYPE_CONTEST) { +export function count(domainId: string, query: any, type = document.TYPE_CONTEST) { return document.count(domainId, type, query); } -function getMulti(domainId, query = {}, type = document.TYPE_CONTEST) { +export function getMulti(domainId: string, query = {}, type = document.TYPE_CONTEST) { return document.getMulti(domainId, type, query); } -function _getStatusJournal(tsdoc) { +export function _getStatusJournal(tsdoc) { return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); } -async function getAndListStatus(domainId, tid, docType = document.TYPE_CONTEST) { +export async function getAndListStatus( + domainId: string, tid: ObjectID, docType = document.TYPE_CONTEST, +) { // TODO(iceboy): projection, pagination. const tdoc = await get(domainId, tid, docType); const tsdocs = await document.getMultiStatus(domainId, docType, { docId: tid }) @@ -376,7 +543,7 @@ async function getAndListStatus(domainId, tid, docType = document.TYPE_CONTEST) return [tdoc, tsdocs]; } -async function recalcStatus(domainId, tid, type) { +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(), @@ -397,7 +564,23 @@ async function recalcStatus(domainId, tid, type) { return await Promise.all(tasks); } -global.Hydro.model.contest = module.exports = { +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, @@ -418,18 +601,7 @@ global.Hydro.model.contest = module.exports = { isNotStarted, isOngoing, isDone, - statusText: (tdoc) => ( - isNew(tdoc) - ? 'New' - : isUpcoming(tdoc) - ? 'Ready (☆▽☆)' - : isOngoing(tdoc) - ? 'Live...' - : 'Done'), - getStatusText: (tdoc) => ( - isNotStarted(tdoc) - ? 'not_started' - : isOngoing(tdoc) - ? 'ongoing' - : 'finished'), + isExtended, + statusText, + getStatusText, }; diff --git a/hydro/model/discussion.js b/hydro/model/discussion.ts similarity index 68% rename from hydro/model/discussion.js rename to hydro/model/discussion.ts index 57272f4bd..c85157dd6 100644 --- a/hydro/model/discussion.js +++ b/hydro/model/discussion.ts @@ -1,12 +1,12 @@ -const { ObjectID } = require('mongodb'); -const problem = require('./problem'); -const document = require('./document'); -const contest = require('./contest'); -const training = require('./training'); -const { PERM_VIEW_PROBLEM_HIDDEN } = require('./builtin').PERM; -const { DocumentNotFoundError } = require('../error'); +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 { PERM } from './builtin'; +import { DocumentNotFoundError } from '../error'; -const typeDisplay = { +export const typeDisplay = { [document.TYPE_PROBLEM]: 'problem', [document.TYPE_CONTEST]: 'contest', [document.TYPE_DISCUSSION_NODE]: 'node', @@ -14,7 +14,11 @@ const typeDisplay = { [document.TYPE_HOMEWORK]: 'homework', }; -function add(domainId, parentType, parentId, owner, title, content, ip = null, highlight = false) { +export function add( + domainId: string, parentType: number, parentId: ObjectID, + owner: number, title: string, content: string, + ip: string | null = null, highlight: boolean = false, +) { return document.add( domainId, content, owner, document.TYPE_DISCUSSION, null, parentType, parentId, @@ -29,15 +33,18 @@ function add(domainId, parentType, parentId, owner, title, content, ip = null, h ); } -function get(domainId, did) { +export function get(domainId: string, did: ObjectID) { return document.get(domainId, document.TYPE_DISCUSSION, did); } -function edit(domainId, did, title, content, highlight) { +export function edit( + domainId: string, did: ObjectID, + title: string, content: string, highlight: boolean, +) { return document.set(domainId, document.TYPE_DISCUSSION, did, { title, content, highlight }); } -function del(domainId, did) { +export function del(domainId: string, did: ObjectID) { return Promise.all([ document.deleteOne(domainId, document.TYPE_DISCUSSION, did), document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { @@ -47,15 +54,18 @@ function del(domainId, did) { ]); } -function count(domainId, query) { +export function count(domainId: string, query: any) { return document.count(domainId, document.TYPE_DISCUSSION, query); } -function getMulti(domainId, query) { +export function getMulti(domainId: string, query: any = {}) { return document.getMulti(domainId, document.TYPE_DISCUSSION, query).sort('updateAt', -1); } -async function addReply(domainId, did, owner, content, ip) { +export async function addReply( + domainId: string, did: ObjectID, owner: number, + content: string, ip: string, +) { const [drdoc] = await Promise.all([ document.add( domainId, content, owner, document.TYPE_DISCUSSION_REPLY, @@ -66,15 +76,15 @@ async function addReply(domainId, did, owner, content, ip) { return drdoc; } -function getReply(domainId, drid) { +export function getReply(domainId: string, drid: ObjectID) { return document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid); } -function editReply(domainId, drid, content) { +export async function editReply(domainId: string, drid: ObjectID, content: string) { return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content }); } -async function delReply(domainId, drid) { +export async function delReply(domainId: string, drid: ObjectID) { const drdoc = await getReply(domainId, drid); if (!drdoc) throw new DocumentNotFoundError(drid); return await Promise.all([ @@ -83,18 +93,21 @@ async function delReply(domainId, drid) { ]); } -function getMultiReply(domainId, did) { +export function getMultiReply(domainId: string, did: ObjectID) { return document.getMulti( domainId, document.TYPE_DISCUSSION_REPLY, { parentType: document.TYPE_DISCUSSION, parentId: did }, ).sort('_id', -1); } -function getListReply(domainId, did) { +export function getListReply(domainId: string, did: ObjectID) { return getMultiReply(domainId, did).toArray(); } -async function addTailReply(domainId, drid, owner, content, ip) { +export async function addTailReply( + domainId: string, drid: ObjectID, + owner: number, content: string, ip: string, +) { let drdoc = await document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid); const sid = new ObjectID(); [drdoc] = await Promise.all([ @@ -104,42 +117,42 @@ async function addTailReply(domainId, drid, owner, content, ip) { return [drdoc, sid]; } -function getTailReply(domainId, drid, drrid) { +export function getTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) { return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid); } -function editTailReply(domainId, drid, drrid, content) { +export function editTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) { return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content }); } -function delTailReply(domainId, drid, drrid) { +export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) { return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid); } -function setStar(domainId, did, uid, star) { +export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) { return document.setStatus(domainId, document.TYPE_DISCUSSION, did, uid, { star }); } -function getStatus(domainId, did, uid) { +export function getStatus(domainId: string, did: ObjectID, uid: number) { return document.getStatus(domainId, document.TYPE_DISCUSSION, did, uid); } -function addNode(domainId, _id, category, args) { +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, ); } -function getNode(domainId, _id) { +export function getNode(domainId: string, _id: string) { return document.get(domainId, document.TYPE_DISCUSSION_NODE, _id); } -async function getVnode(domainId, ddoc, handler) { +export async function getVnode(domainId: string, ddoc: any, handler: any) { if (ddoc.parentType === document.TYPE_PROBLEM) { - const pdoc = await problem.getById(domainId, ddoc.parentId); + const pdoc = await problem.get(domainId, ddoc.parentId); if (!pdoc) return null; - if (pdoc.hidden && handler) handler.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + if (pdoc.hidden && handler) handler.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); return { ...pdoc, type: ddoc.parentType, id: ddoc.parentId }; } if (ddoc.parentType === document.TYPE_CONTEST) { @@ -170,11 +183,11 @@ async function getVnode(domainId, ddoc, handler) { }; } -function getNodes(domainId) { +export function getNodes(domainId: string) { return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray(); } -async function getListVnodes(domainId, ddocs, handler) { +export async function getListVnodes(domainId: string, ddocs: any, handler: any) { const tasks = []; const res = {}; function task(ddoc) { @@ -188,7 +201,7 @@ async function getListVnodes(domainId, ddocs, handler) { return res; } -global.Hydro.model.discussion = module.exports = { +global.Hydro.model.discussion = { typeDisplay, add, get, diff --git a/hydro/model/document.js b/hydro/model/document.ts similarity index 68% rename from hydro/model/document.js rename to hydro/model/document.ts index d3cc7642c..40a9b5d0b 100644 --- a/hydro/model/document.js +++ b/hydro/model/document.ts @@ -1,28 +1,32 @@ -const assert = require('assert'); -const { ObjectID } = require('mongodb'); -const db = require('../service/db'); +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'); -const TYPE_DOMAIN_USER = 0; -const TYPE_PROBLEM = 10; -const TYPE_PROBLEM_SOLUTION = 11; -const TYPE_PROBLEM_LIST = 12; -const TYPE_DISCUSSION_NODE = 20; -const TYPE_DISCUSSION = 21; -const TYPE_DISCUSSION_REPLY = 22; -const TYPE_CONTEST = 30; -const TYPE_TRAINING = 40; -const TYPE_FILE = 50; -const TYPE_HOMEWORK = 60; +export const TYPE_DOMAIN_USER = 0; +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; -async function add( - domainId, content, owner, docType, docId = null, - parentType = null, parentId = null, args = {}, +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 = { + const doc: any = { _id, content, owner, @@ -40,11 +44,11 @@ async function add( return docId || res.insertedId; } -function get(domainId, docType, docId) { +export function get(domainId: string, docType: number, docId: DocID) { return coll.findOne({ domainId, docType, docId }); } -async function set(domainId, docType, docId, args) { +export async function set(domainId: string, docType: number, docId: DocID, args: any) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $set: args }, @@ -53,26 +57,29 @@ async function set(domainId, docType, docId, args) { return res.value; } -function deleteOne(domainId, docType, docId) { +export function deleteOne(domainId: string, docType: number, docId: DocID) { return Promise.all([ coll.deleteMany({ domainId, docType, docId }), coll.deleteOne({ domainId, docType, docId }), ]); } -function deleteMulti(domainId, docType, args = {}) { +export function deleteMulti(domainId: string, docType: number, args: any = {}) { return coll.deleteMany({ ...args, domainId, docType }); } -function deleteMultiStatus(domainId, docType, args = {}) { +export function deleteMultiStatus(domainId: string, docType: number, args: any = {}) { return collStatus.deleteMany({ ...args, domainId, docType }); } -function getMulti(domainId, docType, args = {}) { +export function getMulti(domainId: string, docType: number, args: any = {}) { return coll.find({ ...args, docType, domainId }); } -async function inc(domainId, docType, docId, key, value) { +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 } }, @@ -81,7 +88,10 @@ async function inc(domainId, docType, docId, key, value) { return res.value; } -async function incAndSet(domainId, docType, docId, key, value, args) { +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 }, @@ -90,11 +100,14 @@ async function incAndSet(domainId, docType, docId, key, value, args) { return res.value; } -function count(domainId, docType, query) { +export function count(domainId: string, docType: number, query: any) { return coll.find({ ...query, docType, domainId }).count(); } -async function push(domainId, docType, docId, key, content, owner, args) { +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 }, @@ -110,7 +123,10 @@ async function push(domainId, docType, docId, key, content, owner, args) { return [doc.value, _id]; } -async function pull(domainId, docType, docId, setKey, contents) { +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 } } }, @@ -119,7 +135,10 @@ async function pull(domainId, docType, docId, setKey, contents) { return res.value; } -async function deleteSub(domainId, docType, docId, key, subId) { +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 } } }, @@ -128,7 +147,10 @@ async function deleteSub(domainId, docType, docId, key, subId) { return res.value; } -async function getSub(domainId, docType, docId, key, subId) { +export async function getSub( + domainId: string, docType: number, docId: DocID, + key: string, subId: ObjectID, +) { const doc = await coll.findOne({ domainId, docType, @@ -142,7 +164,10 @@ async function getSub(domainId, docType, docId, key, subId) { return [doc, null]; } -async function setSub(domainId, docType, docId, key, subId, args) { +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]; @@ -160,7 +185,10 @@ async function setSub(domainId, docType, docId, key, subId, args) { return res.value; } -async function addToSet(domainId, docType, docId, setKey, content) { +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 } }, @@ -169,17 +197,19 @@ async function addToSet(domainId, docType, docId, setKey, content) { return res.value; } -function getStatus(domainId, docType, docId, uid) { +export function getStatus(domainId: string, docType: number, docId: DocID, uid: number) { return collStatus.findOne({ domainId, docType, docId, uid, }); } -function getMultiStatus(domainId, docType, args) { +export function getMultiStatus(domainId: string, docType: number, args: any) { return collStatus.find({ ...args, docType, domainId }); } -async function setStatus(domainId, docType, docId, uid, args) { +export async function setStatus( + domainId: string, docType: number, docId: DocID, uid: number, args: any, +) { const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid, @@ -190,14 +220,17 @@ async function setStatus(domainId, docType, docId, uid, args) { return res.value; } -function setMultiStatus(domainId, docType, query, args) { +export function setMultiStatus(domainId: string, docType: number, query: any, args: any) { return collStatus.updateMany( { domainId, docType, ...query }, { $set: args }, ); } -async function setIfNotStatus(domainId, docType, docId, uid, key, value, ifNot, 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 } }, @@ -208,9 +241,9 @@ async function setIfNotStatus(domainId, docType, docId, uid, key, value, ifNot, return res.value; } -async function cappedIncStatus( - domainId, docType, docId, uid, - key, value, minValue = -1, maxValue = 1, +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 }; @@ -224,7 +257,10 @@ async function cappedIncStatus( return res.value; } -async function incStatus(domainId, docType, docId, uid, key, 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, @@ -235,7 +271,10 @@ async function incStatus(domainId, docType, docId, uid, key, value) { return res.value; } -async function revPushStatus(domainId, docType, docId, uid, key, 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, @@ -246,7 +285,9 @@ async function revPushStatus(domainId, docType, docId, uid, key, value) { return res.value; } -async function revInitStatus(domainId, docType, docId, uid) { +export async function revInitStatus( + domainId: string, docType: number, docId: DocID, uid: number, +) { const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid, @@ -257,7 +298,10 @@ async function revInitStatus(domainId, docType, docId, uid) { return res.value; } -async function revSetStatus(domainId, docType, docId, uid, rev, args, returnDoc = true) { +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, }; @@ -270,7 +314,7 @@ async function revSetStatus(domainId, docType, docId, uid, rev, args, returnDoc return res; } -async function ensureIndexes() { +export async function ensureIndexes() { await coll.createIndex({ domainId: 1, docType: 1, docId: 1 }, { unique: true }); await coll.createIndex({ domainId: 1, docType: 1, owner: 1, docId: -1, @@ -332,7 +376,7 @@ async function ensureIndexes() { }, { sparse: true }); } -global.Hydro.model.document = module.exports = { +global.Hydro.model.document = { add, addToSet, cappedIncStatus, @@ -373,3 +417,5 @@ global.Hydro.model.document = module.exports = { TYPE_FILE, TYPE_TRAINING, }; + +export default global.Hydro.model.document; diff --git a/hydro/model/domain.js b/hydro/model/domain.ts similarity index 59% rename from hydro/model/domain.js rename to hydro/model/domain.ts index 5af5d4df5..9d78c7293 100644 --- a/hydro/model/domain.js +++ b/hydro/model/domain.ts @@ -1,21 +1,11 @@ -const builtin = require('./builtin'); -const document = require('./document'); -const db = require('../service/db'); -const validator = require('../lib/validator'); +import * as builtin from './builtin'; +import * as document from './document'; +import * as db from '../service/db'; +import * as validator from '../lib/validator'; const coll = db.collection('domain'); -/** - * @typedef {import('../interface').Pdoc} Pdoc - * @typedef {import('mongodb').ObjectID} ObjectID - * @typedef {import('mongodb').Cursor} Cursor - */ - -/** - * @param {string} domainId - * @param {number} owner - */ -function add(domainId, owner, name, isEnsure = false) { +export function add(domainId: string, owner: number, name: string, isEnsure = false) { const tasks = []; if (isEnsure) { tasks.push( @@ -43,46 +33,26 @@ function add(domainId, owner, name, isEnsure = false) { return Promise.all(tasks); } -/** - * @param {string} domainId - * @returns {Ddoc} - */ -async function get(domainId) { +export async function get(domainId: string) { return coll.findOne({ _id: domainId }); } -/** - * @param {object} query - * @param {object} sort - * @param {number} page - * @param {number} limit - * @returns {Ddoc[]} - */ -function getMany(query, sort, page, limit) { +export function getMany(query: any, sort: any, page: number, limit: number) { return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit) .toArray(); } -/** - * @param {object} query - * @returns {Cursor} - */ -function getMulti(query) { +export function getMulti(query: any) { return coll.find(query); } -/** - * @param {string} domainId - * @param {object} $set - * @returns {Ddoc} - */ -function edit(domainId, $set) { +export function edit(domainId: string, $set: any) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkContent($set.content); return coll.updateOne({ _id: domainId }, { $set }); } -async function inc(domainId, field, n) { +export async function inc(domainId: string, field: string, n: number) { const res = await coll.findOneAndUpdate( { _id: domainId }, { $inc: { [field]: n } }, @@ -91,14 +61,14 @@ async function inc(domainId, field, n) { return res.value; } -async function getList(domainIds) { +export async function getList(domainIds: string[]) { const r = {}; // eslint-disable-next-line no-await-in-loop for (const domainId of domainIds) r[domainId] = await get(domainId); return r; } -global.Hydro.model.domain = module.exports = { +global.Hydro.model.domain = { add, inc, get, diff --git a/hydro/model/file.js b/hydro/model/file.js deleted file mode 100644 index 04038f5b9..000000000 --- a/hydro/model/file.js +++ /dev/null @@ -1,83 +0,0 @@ -const fs = require('fs'); -const { ForbiddenError, NotFoundError } = require('../error'); -const db = require('../service/db'); -const gridfs = require('../service/gridfs'); -const hash = require('../lib/hash.hydro'); - -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); -} - -async function add(streamOrPath, filename, meta = {}) { - if (typeof streamOrPath === 'string') streamOrPath = fs.createReadStream(streamOrPath); - const w = gridfs.openUploadStream(filename); - await coll.insertOne({ ...meta, _id: w.id, secret: String.random(32) }); - await new Promise((resolve, reject) => { - w.on('error', reject); - w.on('finish', resolve); - streamOrPath.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].size } }); - return w.id; -} - -function del(_id) { - return Promise.all([ - coll.deleteOne({ _id }), - collFile.deleteOne({ _id }), - collChunk.deleteMany({ files_id: _id }), - ]); -} - -async function inc(_id) { - const doc = await coll.findOneAndUpdate( - { _id }, - { $inc: { count: 1 } }, - { returnOriginal: false }, - ); - return (doc.value || {}).count; -} - -async function dec(_id) { - const file = await coll.findOneAndUpdate( - { _id }, - { $inc: { count: -1 } }, - { returnOriginal: false }, - ); - if (!file.count) await del(_id); - return file.count; -} - -async function get(_id, secret) { - const file = await coll.findOne({ _id }); - if (!file) throw new NotFoundError(_id); - if (typeof secret !== 'undefined') { - const timestamp = _timestamp(); - if (!(hash(file.secret, timestamp.toString()) === secret)) { - if (!(hash(file.secret, timestamp.toString() - 1) === secret)) { - throw new ForbiddenError(); - } - } - } - return gridfs.openDownloadStream(_id); -} - -function getMeta(_id) { - return coll.findOne({ _id }); -} - -async function url(_id, name) { - 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}`; -} - -global.Hydro.model.file = module.exports = { - add, get, del, inc, dec, getMeta, url, -}; diff --git a/hydro/model/file.ts b/hydro/model/file.ts new file mode 100644 index 000000000..36a1b15b2 --- /dev/null +++ b/hydro/model/file.ts @@ -0,0 +1,104 @@ +import { ObjectID } from 'mongodb'; +import * as fs from 'fs'; +import { ForbiddenError, NotFoundError } from '../error'; +import * as db from '../service/db'; +import gridfs from '../service/gridfs'; +import hash from '../lib/hash.hydro'; + +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, 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) }); + 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].size } }); + return w.id; +} + +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) { + const doc = await coll.findOneAndUpdate( + { _id }, + { $inc: { count: 1 } }, + { returnOriginal: false }, + ); + return (doc.value || {}).count; +} + +export async function dec(_id: ObjectID) { + const file = await coll.findOneAndUpdate( + { _id }, + { $inc: { count: -1 } }, + { returnOriginal: false }, + ); + if (!file.value.count) await del(_id); + return file.value.count; +} + +export async function getWithSecret(_id: ObjectID, secret: string) { + 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(); + } + } + return gridfs.openDownloadStream(_id); +} + +export async function get(_id: ObjectID) { + const file = await coll.findOne({ _id }); + if (!file) throw new NotFoundError(_id); + return gridfs.openDownloadStream(_id); +} + +export function getMeta(_id: ObjectID) { + return coll.findOne({ _id }); +} + +export async function getMetaDict(ufids: ObjectID[]) { + 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); +} + +export 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}`; +} + +global.Hydro.model.file = { + add, get, del, inc, dec, getMeta, getWithSecret, getMulti, getMetaDict, url, +}; diff --git a/hydro/model/message.js b/hydro/model/message.ts similarity index 61% rename from hydro/model/message.js rename to hydro/model/message.ts index faddd2e57..93686d0ee 100644 --- a/hydro/model/message.js +++ b/hydro/model/message.ts @@ -1,9 +1,10 @@ -const { MessageNotFoundError } = require('../error'); -const db = require('../service/db.js'); +import { ObjectID, QuerySelector } from 'mongodb'; +import { MessageNotFoundError } from '../error'; +import * as db from '../service/db'; const coll = db.collection('message'); -async function send(from, to, content) { +export async function send(from: number, to: number, content: string) { const res = await coll.insertOne({ from, to, content, unread: true, }); @@ -12,42 +13,42 @@ async function send(from, to, content) { }; } -async function get(_id) { +export async function get(_id: ObjectID) { const doc = await coll.findOne({ _id }); if (!doc) throw new MessageNotFoundError(_id); return doc; } -function getByUser(uid) { +export function getByUser(uid: number) { return coll.find({ $or: [{ from: uid }, { to: uid }] }).sort('_id', 1).toArray(); } -function getMany(query, sort, page, limit) { +export function getMany(query: QuerySelector, sort, page: number, limit: number) { return coll.find(query).sort(sort) .skip((page - 1) * limit).limit(limit) .toArray(); } -function del(_id) { +export function del(_id: ObjectID) { return coll.deleteOne({ _id }); } -function count(query) { +export function count(query: QuerySelector) { return coll.find(query).count(); } -function getMulti(uid) { +export function getMulti(uid: number) { return coll.find({ $or: [{ from: uid }, { to: uid }] }); } -function ensureIndexes() { +export function ensureIndexes() { return Promise.all([ coll.createIndex({ to: 1, _id: -1 }), coll.createIndex({ from: 1, _id: -1 }), ]); } -global.Hydro.model.message = module.exports = { +global.Hydro.model.message = { count, get, getByUser, diff --git a/hydro/model/opcount.js b/hydro/model/opcount.ts similarity index 62% rename from hydro/model/opcount.js rename to hydro/model/opcount.ts index 796d6b135..6f496934f 100644 --- a/hydro/model/opcount.js +++ b/hydro/model/opcount.ts @@ -1,15 +1,9 @@ -const { OpcountExceededError } = require('../error'); -const db = require('../service/db'); +import { OpcountExceededError } from '../error'; +import * as db from '../service/db'; const coll = db.collection('opcount'); -/** - * @param {string} op - * @param {string} ident - * @param {number} period_secs - * @param {number} max_operations - */ -async function inc(op, ident, periodSecs, maxOperations) { +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); @@ -25,8 +19,8 @@ async function inc(op, ident, periodSecs, maxOperations) { } } -function ensureIndexes() { +export function ensureIndexes() { return coll.createIndex('expireAt', { expireAfterSeconds: 0 }); } -global.Hydro.model.opcount = module.exports = { inc, ensureIndexes }; +global.Hydro.model.opcount = { inc, ensureIndexes }; diff --git a/hydro/model/problem.js b/hydro/model/problem.js deleted file mode 100644 index 1fbc40a1f..000000000 --- a/hydro/model/problem.js +++ /dev/null @@ -1,230 +0,0 @@ -const { ObjectId } = require('mongodb'); -const { STATUS_ACCEPTED } = require('./builtin').STATUS; -const file = require('./file'); -const document = require('./document'); -const domain = require('./domain'); -const { ProblemNotFoundError } = require('../error'); -const readConfig = require('../lib/readConfig'); - -/** - * @typedef {import('../interface').Pdoc} Pdoc - * @typedef {import('mongodb').ObjectID} ObjectID - * @typedef {import('mongodb').Cursor} Cursor - */ - -class Problem { - constructor(pdoc) { - this.docType = document.TYPE_PROBLEM; - if (pdoc) { - this._id = pdoc._id; - this.docId = pdoc.docId; - this.pid = pdoc.pid; - this.title = pdoc.title; - this.content = pdoc.content; - this.owner = pdoc.owner; - this.config = pdoc.config; - this.data = pdoc.data; - this.nSubmit = pdoc.nSubmit; - this.nAccept = pdoc.nAccept; - this.difficulty = pdoc.difficulty || 0; - this.tag = pdoc.tag || []; - this.category = pdoc.category || []; - this.hidden = pdoc.hidden || false; - } else { - this._id = new ObjectId(); - this.docId = this._id; - this.pid = String.random(8); - this.title = '*'; - this.content = ''; - this.owner = 1; - this.config = ''; - this.data = null; - this.nSubmit = 0; - this.nAccept = 0; - this.difficulty = 0; - this.tag = []; - this.category = []; - this.hidden = true; - } - } -} - -/** - * @param {string} domainId - * @param {string} title - * @param {string} content - * @param {number} owner - * @param {object} args - * @returns {Promise} docId - */ -async function add(domainId, title, content, owner, { - pid = null, - data = null, - category = [], - tag = [], - hidden = false, -}) { - const d = await domain.inc(domainId, 'pidCounter', 1); - if (!pid) pid = d.pidCounter.toString(); - return await document.add( - domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null, - { - pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0, - }, - ); -} - -/** - * @param {string} domainId - * @param {string|number} pid - * @param {number} uid - * @returns {Promise} - */ -async function get(domainId, pid, uid = null, doThrow = true) { - if (!Number.isNaN(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) { - if (doThrow) throw new ProblemNotFoundError(domainId, pid); - return null; - } - if (uid) { - pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); - } - return new Problem(pdoc); -} - -/** - * @param {string} domainId - * @param {object} query - * @param {object} sort - * @param {number} page - * @param {number} limit - * @returns {Promise} - */ -async function getMany(domainId, query, sort, page, limit) { - const pdocs = await document.getMulti(domainId, query) - .sort(sort).skip((page - 1) * limit).limit(limit) - .toArray(); - for (let i = 0; i < pdocs.length; i++) { - pdocs[i] = new Problem(pdocs[i]); - } - return pdocs; -} - -/** - * @param {string} domainId - * @param {object} query - * @returns {Cursor} - */ -function getMulti(domainId, query) { - return document.getMulti(domainId, document.TYPE_PROBLEM, query); -} - -/** - * @param {string} domainId - * @param {object} query - * @returns {Cursor} - */ -function getMultiStatus(domainId, query) { - return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query); -} - -/** - * @param {string} domainId - * @param {ObjectID} _id - * @param {object} query - * @returns {Promise} - */ -async function edit(domainId, _id, $set) { - const pdoc = await document.set(domainId, document.TYPE_PROBLEM, _id, $set); - return new Problem(pdoc); -} - -function inc(domainId, _id, field, n) { - return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n); -} - -function count(domainId, query) { - return document.count(domainId, query); -} - -async function random(domainId, query) { - 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; -} - -async function getList(domainId, pids, getHidden = false, doThrow = true) { - pids = Array.from(new Set(pids)); - const r = {}; - const q = { $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] = r[pdoc.pid] = pdoc; - } - if (pdocs.length !== pids.length) { - for (const pid of pids) { - if (!r[pid]) { - if (doThrow) { - throw new ProblemNotFoundError(domainId, pid); - } else { - r[pid] = new Problem(); - } - } - } - } - return r; -} - -async function getListStatus(domainId, uid, pids) { - const psdocs = await getMultiStatus( - domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, - ).toArray(); - const r = {}; - for (const psdoc of psdocs) r[psdoc.docId] = psdoc; - return r; -} - -async function updateStatus(domainId, pid, uid, rid, status) { - try { - await document.setIfNotStatus(domainId, document.TYPE_PROBLEM, pid, uid, 'status', status, STATUS_ACCEPTED, { rid }); - } catch (e) { - return false; - } - return true; -} - -function setStar(domainId, pid, uid, star) { - return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star }); -} - -async function setTestdata(domainId, _id, filePath) { - const pdoc = await get(domainId, _id); - const config = await readConfig(filePath); - const id = await file.add(filePath, 'data.zip'); - if (pdoc.data && typeof pdoc.data === 'object') file.dec(pdoc.data); - return await edit(domainId, _id, { data: id, config }); -} - -global.Hydro.model.problem = module.exports = { - add, - inc, - get, - getMany, - edit, - count, - random, - getMulti, - getList, - getListStatus, - getMultiStatus, - setStar, - setTestdata, - updateStatus, -}; diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts new file mode 100644 index 000000000..7225e7c9f --- /dev/null +++ b/hydro/model/problem.ts @@ -0,0 +1,194 @@ +import { ObjectID, FilterQuery } from 'mongodb'; +import { STATUS } from './builtin'; +import * as file from './file'; +import * as document from './document'; +import * as domain from './domain'; +import { ProblemNotFoundError } from '../error'; +import readConfig from '../lib/readConfig'; + +export interface Pdoc { + _id: ObjectID + domainId: string + docId: number + pid: string + owner: number + title: string + content: string + nSubmit: number + nAccept: number + tag: string[] + category: string[], + data: ObjectID | null + hidden: boolean + config: string +} + +export const pdocHidden: Pdoc = { + _id: new ObjectID(), + domainId: 'system', + docId: -1, + pid: '', + owner: 1, + title: '*', + content: '', + nSubmit: 0, + nAccept: 0, + tag: [], + category: [], + data: null, + hidden: true, + config: '', +}; + +export async function add(domainId: string, title: string, content: string, owner: number, { + pid = null, + data = null, + category = [], + tag = [], + hidden = false, +}) { + const d = await domain.inc(domainId, 'pidCounter', 1); + if (!pid) pid = d.pidCounter.toString(); + return await document.add( + domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null, + { + pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0, + }, + ); +} + +export async function get( + domainId: string, pid: string | number, + uid: number = null, doThrow = true, +): Promise { + if (typeof pid !== 'number') { + if (!Number.isNaN(parseInt(pid, 10))) pid = parseInt(pid, 10); + else if (doThrow) throw new ProblemNotFoundError(domainId, pid); + else return null; + } + 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) { + if (doThrow) throw new ProblemNotFoundError(domainId, pid); + return null; + } + if (uid) { + pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); + } + return pdoc; +} + +export function getMany( + domainId: string, query: any, sort: any, page: number, limit: number, +): Promise { + return document.getMulti(domainId, query) + .sort(sort).skip((page - 1) * limit).limit(limit) + .toArray(); +} + +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 { + return document.set(domainId, document.TYPE_PROBLEM, _id, $set); +} + +export function inc(domainId: string, _id: number, field: string, n: number) { + return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n); +} + +export function count(domainId: string, query: FilterQuery) { + return document.count(domainId, document.TYPE_PROBLEM, query); +} + +export async function random(domainId: string, query: FilterQuery) { + 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: number[], getHidden = false, doThrow = true) { + pids = Array.from(new Set(pids)); + const r = {}; + 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; + r[pdoc.pid] = pdoc; + } + if (pdocs.length !== pids.length) { + for (const pid of pids) { + if (!r[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 = {}; + 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, +) { + try { + await document.setIfNotStatus( + domainId, document.TYPE_PROBLEM, pid, uid, + 'status', status, STATUS.STATUS_ACCEPTED, { rid }, + ); + } catch (e) { + return false; + } + return true; +} + +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 readConfig(filePath); + const id = await file.add(filePath, 'data.zip'); + if (pdoc.data && typeof pdoc.data === 'object') file.dec(pdoc.data); + return await edit(domainId, _id, { data: id, config }); +} + +global.Hydro.model.problem = { + add, + inc, + get, + getMany, + edit, + count, + random, + getMulti, + getList, + getListStatus, + getMultiStatus, + setStar, + setTestdata, + updateStatus, +}; diff --git a/hydro/model/record.js b/hydro/model/record.js deleted file mode 100644 index 926202e98..000000000 --- a/hydro/model/record.js +++ /dev/null @@ -1,161 +0,0 @@ -const _ = require('lodash'); -const { ObjectID } = require('mongodb'); -const { STATUS_WAITING } = require('./builtin').STATUS; -const task = require('./task'); -const problem = require('./problem'); -const { RecordNotFoundError } = require('../error'); -const db = require('../service/db'); - -const coll = db.collection('record'); - -/** - * @param {string} domainId - * @param {import('../interface').Record} data - */ -async function add(domainId, data, addTask = true) { - _.defaults(data, { - status: STATUS_WAITING, - domainId, - score: 0, - time: 0, - memory: 0, - hidden: false, - judgeTexts: [], - compilerTexts: [], - testCases: [], - judger: null, - judgeAt: null, - }); - const [pdoc, res] = await Promise.all([ - problem.get(domainId, data.pid, null, false), - coll.insertOne(data), - ]); - if (addTask) { - const t = { - type: data.type || 'judge', - event: data.type || 'judge', - rid: res.insertedId, - domainId, - pid: data.pid, - lang: data.lang, - code: data.code, - }; - if (t.type === 'judge') { - t.data = pdoc.data; - t.config = pdoc.config; - } else { - t.config = { - time: data.time, - memory: data.memory, - input: data.input, - }; - } - await task.add(t); - } - return res.insertedId; -} - -/** - * @param {string} domainId - * @param {ObjectID} rid - * @returns {import('../interface').Record} - */ -async function get(domainId, rid) { - const _id = new ObjectID(rid); - const rdoc = await coll.findOne({ domainId, _id }); - if (!rdoc) throw new RecordNotFoundError(rid); - return rdoc; -} - -function getMulti(domainId, query) { - return coll.find({ ...query, domainId }); -} - -async function update(domainId, rid, $set, $push, $unset) { - const _id = new ObjectID(rid); - const $update = {}; - 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; - const res = await coll.findOneAndUpdate({ domainId, _id }, $update, { returnOriginal: false }); - if (!res.value) throw new RecordNotFoundError(rid); - return res.value; -} - -function reset(domainId, rid) { - return update(domainId, rid, { - score: 0, - status: STATUS_WAITING, - time: 0, - memory: 0, - testCases: [], - judgeTexts: [], - compilerTexts: [], - judgeAt: null, - judger: null, - rejudged: true, - }); -} - -function count(domainId, query) { - return coll.find({ domainId, ...query }).count(); -} - -async function getList(domainId, rids, showHidden = false) { - const r = {}; - // eslint-disable-next-line no-await-in-loop - for (const rid of rids) r[rid] = await get(domainId, rid, showHidden); - return r; -} - -function getUserInProblemMulti(domainId, uid, pid) { - return coll.find({ domainId, uid, pid }); -} - -function getByUid(domainId, uid) { - return coll.find({ domainId, uid }).toArray(); -} - -async function judge(domainId, rid) { - const rdoc = await get(domainId, rid); - const pdoc = await problem.get(domainId, rdoc.pid); - await task.add({ - type: 'judge', - rid, - domainId, - config: pdoc.config || '', - pid: rdoc.pid, - data: pdoc.data, - lang: rdoc.lang, - code: rdoc.code, - }); -} - -async function rejudge(domainId, rid) { - await reset(domainId, rid); - const rdoc = await get(domainId, rid); - const pdoc = await problem.get(domainId, rdoc.pid); - await task.add({ - type: 'judge', - rid, - domainId, - pid: rdoc.pid, - data: pdoc.data, - lang: rdoc.lang, - code: rdoc.code, - }); -} - -global.Hydro.model.record = module.exports = { - add, - get, - getMulti, - update, - count, - reset, - getList, - getUserInProblemMulti, - getByUid, - judge, - rejudge, -}; diff --git a/hydro/model/record.ts b/hydro/model/record.ts new file mode 100644 index 000000000..89618ab41 --- /dev/null +++ b/hydro/model/record.ts @@ -0,0 +1,233 @@ +import _ from 'lodash'; +import yaml from 'js-yaml'; +import { ObjectID, FilterQuery } from 'mongodb'; +import { STATUS } from './builtin'; +import * as task from './task'; +import * as problem from './problem'; +import { RecordNotFoundError } from '../error'; +import * as db from '../service/db'; + +const coll = db.collection('record'); + +export interface TestCase { + time: number, + memory: number, + status: number, + message: string, +} + +export interface Rdoc { + _id: ObjectID, + domainId: string, + pid: ObjectID + 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 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 add(domainId: string, data: RdocBase, addTask = true) { + _.defaults(data, { + status: STATUS.STATUS_WAITING, + type: 'judge', + domainId, + score: 0, + time: 0, + memory: 0, + hidden: false, + judgeTexts: [], + compilerTexts: [], + testCases: [], + judger: null, + judgeAt: null, + }); + const [pdoc, res] = await Promise.all([ + problem.get(domainId, data.pid, null, false), + coll.insertOne(data), + ]); + if (addTask) { + const t: any = { + type: data.type || 'judge', + rid: res.insertedId, + domainId, + pid: data.pid, + lang: data.lang, + code: data.code, + }; + if (t.type === 'judge') { + t.data = pdoc.data; + t.config = pdoc.config; + } else { + t.config = yaml.safeDump({ + time: data.time, + memory: data.memory, + input: data.input, + }); + } + await task.add(t); + } + return res.insertedId; +} + +export async function get(domainId: string, _id: ObjectID) { + const rdoc = await coll.findOne({ domainId, _id }); + if (!rdoc) throw new RecordNotFoundError(_id); + return rdoc; +} + +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 = {}, +) { + 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; + const res = await coll.findOneAndUpdate({ domainId, _id }, $update, { returnOriginal: false }); + if (!res.value) throw new RecordNotFoundError(_id); + return res.value; +} + +export function reset(domainId: string, rid: ObjectID) { + return update(domainId, rid, { + score: 0, + status: STATUS.STATUS_WAITING, + time: 0, + memory: 0, + testCases: [], + judgeTexts: [], + compilerTexts: [], + judgeAt: null, + judger: null, + rejudged: true, + }); +} + +export function count(domainId: string, query: FilterQuery) { + return coll.find({ domainId, ...query }).count(); +} + +export async function getList(domainId: string, rids: ObjectID[], showHidden = false) { + const r = {}; + for (const rid of rids) { + // eslint-disable-next-line no-await-in-loop + const rdoc = await get(domainId, rid); + if (rdoc.hidden && !showHidden) r[rid.toHexString()] = null; + else r[rid.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) { + return coll.find({ domainId, uid }).toArray(); +} + +export async function judge(domainId: string, rid: ObjectID) { + const rdoc = await get(domainId, rid); + const pdoc = await problem.get(domainId, rdoc.pid); + await task.add({ + type: 'judge', + rid, + domainId, + config: pdoc.config || '', + pid: rdoc.pid, + data: pdoc.data, + lang: rdoc.lang, + code: rdoc.code, + }); +} + +export async function rejudge(domainId: string, rid: ObjectID) { + await reset(domainId, rid); + const rdoc = await get(domainId, rid); + const pdoc = await problem.get(domainId, rdoc.pid); + await task.add({ + type: 'judge', + rid, + domainId, + pid: rdoc.pid, + data: pdoc.data, + lang: rdoc.lang, + code: rdoc.code, + }); +} + +global.Hydro.model.record = { + add, + get, + getMulti, + update, + count, + reset, + getList, + getUserInProblemMulti, + getByUid, + judge, + rejudge, +}; diff --git a/hydro/model/setting.js b/hydro/model/setting.ts similarity index 87% rename from hydro/model/setting.js rename to hydro/model/setting.ts index ca395065f..9f5757b56 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.ts @@ -1,5 +1,5 @@ -const moment = require('moment-timezone'); -const builtin = require('./builtin'); +import moment from 'moment-timezone'; +import * as builtin from './builtin'; const countries = moment.tz.countries(); const tzs = new Set(); @@ -9,22 +9,22 @@ for (const country of countries) { } const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]); -const FLAG_HIDDEN = 1; -const FLAG_DISABLED = 2; -const FLAG_SECRET = 4; +export const FLAG_HIDDEN = 1; +export const FLAG_DISABLED = 2; +export const FLAG_SECRET = 4; -const PREFERENCE_SETTINGS = []; -const ACCOUNT_SETTINGS = []; -const DOMAIN_SETTINGS = []; -const DOMAIN_USER_SETTINGS = []; -const SYSTEM_SETTINGS = []; -const SETTINGS = []; -const SETTINGS_BY_KEY = {}; -const DOMAIN_USER_SETTINGS_BY_KEY = {}; -const DOMAIN_SETTINGS_BY_KEY = {}; -const SYSTEM_SETTINGS_BY_KEY = {}; +export const PREFERENCE_SETTINGS = []; +export const ACCOUNT_SETTINGS = []; +export const DOMAIN_SETTINGS = []; +export const DOMAIN_USER_SETTINGS = []; +export const SYSTEM_SETTINGS = []; +export const SETTINGS = []; +export const SETTINGS_BY_KEY = {}; +export const DOMAIN_USER_SETTINGS_BY_KEY = {}; +export const DOMAIN_SETTINGS_BY_KEY = {}; +export const SYSTEM_SETTINGS_BY_KEY = {}; -const Setting = ( +export const Setting = ( family, key, range = null, value = null, type = 'text', name = '', desc = '', flag = 0, @@ -32,33 +32,33 @@ const Setting = ( family, key, range, value, type, name, desc, flag, }); -const PreferenceSetting = (...settings) => { +export const PreferenceSetting = (...settings) => { for (const setting of settings) { PREFERENCE_SETTINGS.push(setting); SETTINGS.push(setting); SETTINGS_BY_KEY[setting.key] = setting; } }; -const AccountSetting = (...settings) => { +export const AccountSetting = (...settings) => { for (const setting of settings) { ACCOUNT_SETTINGS.push(setting); SETTINGS.push(setting); SETTINGS_BY_KEY[setting.key] = setting; } }; -const DomainUserSetting = (...settings) => { +export const DomainUserSetting = (...settings) => { for (const setting of settings) { DOMAIN_USER_SETTINGS.push(setting); DOMAIN_USER_SETTINGS_BY_KEY[setting.key] = setting; } }; -const DomainSetting = (...settings) => { +export const DomainSetting = (...settings) => { for (const setting of settings) { DOMAIN_SETTINGS.push(setting); DOMAIN_SETTINGS_BY_KEY[setting.key] = setting; } }; -const SystemSetting = (...settings) => { +export const SystemSetting = (...settings) => { for (const setting of settings) { SYSTEM_SETTINGS.push(setting); SYSTEM_SETTINGS_BY_KEY[setting.key] = setting; @@ -130,7 +130,7 @@ SystemSetting( Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'number', 'Reply per Page'), ); -global.Hydro.model.setting = module.exports = { +global.Hydro.model.setting = { Setting, PreferenceSetting, AccountSetting, diff --git a/hydro/model/solution.js b/hydro/model/solution.ts similarity index 64% rename from hydro/model/solution.js rename to hydro/model/solution.ts index 0ce92c4de..052f412f5 100644 --- a/hydro/model/solution.js +++ b/hydro/model/solution.ts @@ -1,67 +1,63 @@ -const document = require('./document'); -const { SolutionNotFoundError } = require('../error'); +import { ObjectID } from 'mongodb'; +import * as document from './document'; +import { SolutionNotFoundError } from '../error'; -/** - * @param {string} pid - * @param {number} owner - * @param {string} content - */ -function add(domainId, pid, owner, content) { +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 }, ); } -async function get(domainId, psid) { +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; } -function getMany(domainId, query, sort, page, limit) { +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(); } -function edit(domainId, psid, content) { +export function edit(domainId: string, psid: ObjectID, content: string) { return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content }); } -function del(domainId, psid) { +export function del(domainId: string, psid: ObjectID) { return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid); } -function count(domainId, query) { +export function count(domainId: string, query: any) { return document.count(domainId, document.TYPE_PROBLEM_SOLUTION, query); } -function getMulti(domainId, pid) { +export function getMulti(domainId: string, pid: number) { return document.getMulti( domainId, document.TYPE_PROBLEM_SOLUTION, { parentType: document.TYPE_PROBLEM, parentId: pid }, ).sort({ vote: -1 }); } -function reply(domainId, psid, owner, content) { +export function reply(domainId: string, psid: ObjectID, owner: number, content: string) { return document.push(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', content, owner); } -function getReply(domainId, psid, psrid) { +export function getReply(domainId: string, psid: ObjectID, psrid: ObjectID) { return document.getSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid); } -function editReply(domainId, psid, psrid, content) { +export function editReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) { return document.setSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid, { content }); } -function delReply(domainId, psid, psrid) { +export function delReply(domainId: string, psid: ObjectID, psrid: ObjectID) { return document.deleteSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid); } -async function vote(domainId, psid, uid, value) { +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; @@ -70,16 +66,16 @@ async function vote(domainId, psid, uid, value) { return [psdoc, pssdoc]; } -async function getListStatus(domainId, list, uid) { +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: list } }, + 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 = module.exports = { +global.Hydro.model.solution = { count, add, get, diff --git a/hydro/model/system.js b/hydro/model/system.ts similarity index 65% rename from hydro/model/system.js rename to hydro/model/system.ts index 12dde20b5..d2a97fa71 100644 --- a/hydro/model/system.js +++ b/hydro/model/system.ts @@ -1,14 +1,15 @@ -const db = require('../service/db'); +import { FindOneAndUpdateOption } from 'mongodb'; +import * as db from '../service/db'; const coll = db.collection('system'); -async function get(_id) { +export async function get(_id: string) { const doc = await coll.findOne({ _id }); if (doc) return doc.value; return null; } -async function getMany(keys) { +export async function getMany(keys: string[]) { const docs = await coll.find({ _id: { $in: keys } }).toArray(); const dict = {}; const res = []; @@ -21,24 +22,21 @@ async function getMany(keys) { return res; } -async function update(_id, operation, config) { +export async function update(_id: string, operation: any, config: FindOneAndUpdateOption) { await coll.findOneAndUpdate({ _id }, operation, config); return get(_id); } -async function set(_id, value) { +export async function set(_id: string, value: any) { await coll.findOneAndUpdate({ _id }, { $set: { value } }, { upsert: true }); return get(_id); } -/** - * Increments the counter. - * @returns {number} Integer value after increment. - */ -function inc(field) { + +export function inc(field: string) { return update(field, { $inc: { value: 1 } }, { upsert: true }); } -global.Hydro.model.system = module.exports = { +global.Hydro.model.system = { get, getMany, update, diff --git a/hydro/model/task.js b/hydro/model/task.ts similarity index 73% rename from hydro/model/task.js rename to hydro/model/task.ts index b7e79c284..0b204bb58 100644 --- a/hydro/model/task.js +++ b/hydro/model/task.ts @@ -1,9 +1,16 @@ -const moment = require('moment-timezone'); -const db = require('../service/db'); +import moment from 'moment-timezone'; +import { ObjectID } from 'mongodb'; +import * as db from '../service/db'; const coll = db.collection('task'); -async function add(task) { +export interface Task { + _id: ObjectID, + count: number, + executeAfter: number, +} + +export async function add(task: any) { const t = { ...task }; if (typeof t.executeAfter === 'object') t.executeAfter = t.executeAfter.getTime(); t.count = t.count || 1; @@ -12,19 +19,19 @@ async function add(task) { return res.insertedId; } -function get(_id) { +export function get(_id: ObjectID) { return coll.findOne({ _id }); } -function count(query) { +export function count(query: any) { return coll.find(query).count(); } -function del(_id) { +export function del(_id: ObjectID) { return coll.deleteOne({ _id }); } -async function getFirst(query) { +export async function getFirst(query: any) { const q = { ...query }; q.executeAfter = q.executeAfter || { $lt: new Date().getTime() }; const res = await coll.find(q).sort('_id', 1).limit(1).toArray(); @@ -40,7 +47,7 @@ async function getFirst(query) { return null; } -async function consume(query, cb) { +export async function consume(query, cb: Function) { let isRunning = false; const interval = setInterval(async () => { if (isRunning) return; @@ -57,6 +64,6 @@ async function consume(query, cb) { }, 100); } -global.Hydro.model.task = module.exports = { +global.Hydro.model.task = { add, get, del, count, getFirst, consume, }; diff --git a/hydro/model/token.js b/hydro/model/token.js deleted file mode 100644 index be86e18c0..000000000 --- a/hydro/model/token.js +++ /dev/null @@ -1,121 +0,0 @@ -const db = require('../service/db'); -const { ValidationError } = require('../error'); - -const coll = db.collection('token'); - -function ensureIndexes() { - return Promise.all([ - coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }), - coll.createIndex('expireAt', { expireAfterSeconds: 0 }), - ]); -} - -/** - * Add a token. - * @param {number} tokenType type of the token. - * @param {number} expireSeconds expire time, in seconds. - * @param {object} data extra data. - * @returns {Promise} token ID, token data - */ -async function add(tokenType, expireSeconds, data) { - 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]]; -} - -/** - * Get a token. - * @param {string} tokenId token ID. - * @param {number} tokenType type of the token. - * @returns {Promise} The token document, or null. - */ -async function get(tokenId, tokenType, doThrow = true) { - const res = await coll.findOne({ _id: tokenId, tokenType }); - if (!res && doThrow) throw new ValidationError('token'); - return res; -} - -/** - * Update a token. - * @param {string} tokenId token ID. - * @param {number} tokenType type of the token. - * @param {number} expireSeconds expire time, in seconds. - * @param {object} data extra data. - * @returns {Promise} The token document, or null. - */ -async function update(tokenId, tokenType, expireSeconds, data) { - 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; -} - -/** - * Delete a token. - * @param {string} tokenId token ID. - * @param {int} tokenType type of the token. - * @returns {Promise} true if deleted, or false. - */ -async function del(tokenId, tokenType) { - const result = await coll.deleteOne({ _id: tokenId, tokenType }); - return !!result.deletedCount; -} - -/** - * Update a token(or create one) - * @param {number} tokenType type of the token. - * @param {number} expireSeconds expire time, in seconds. - * @param {object} data extra data. - * @returns {Array} token ID, token data - */ -async function createOrUpdate(tokenType, expireSeconds, data) { - 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; -} - -global.Hydro.model.token = module.exports = { - TYPE_SESSION: 0, - TYPE_CSRF_TOKEN: 1, - TYPE_REGISTER: 2, - TYPE_CHANGEMAIL: 3, - TYPE_OAUTH: 4, - - ensureIndexes, - add, - createOrUpdate, - get, - update, - del, - delByUid(uid) { - return coll.deleteMany({ uid }); - }, - getMostRecentSessionByUid(uid) { - return coll.findOne({ uid, tokenType: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); - }, - getSessionListByUid(uid) { - return coll.find({ uid, tokenType: this.TYPE_SESSION }).sort('updateAt', -1).toArray(); - }, -}; diff --git a/hydro/model/token.ts b/hydro/model/token.ts new file mode 100644 index 000000000..2cec9fbb0 --- /dev/null +++ b/hydro/model/token.ts @@ -0,0 +1,103 @@ +import { ValidationError } from '../error'; +import * as db from '../service/db'; + +const coll = db.collection('token'); + +export const TYPE_SESSION = 0; +export const TYPE_CSRF_TOKEN = 1; +export const TYPE_REGISTRATION = 2; +export const TYPE_CHANGEMAIL = 3; +export const TYPE_OAUTH = 4; +export const TYPE_LOSTPASS = 5; + +export 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) { + 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, doThrow = true) { + const res = await coll.findOne({ _id: tokenId, tokenType }); + if (!res && doThrow) throw new ValidationError('token'); + return res; +} + +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: object) { + 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: this.TYPE_SESSION }).sort('updateAt', -1).toArray(); +} + +export function getMostRecentSessionByUid(uid: number) { + return coll.findOne({ uid, tokenType: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); +} + +export function delByUid(uid: number) { + return coll.deleteMany({ uid }); +} + +global.Hydro.model.token = { + TYPE_SESSION, + TYPE_CHANGEMAIL, + TYPE_CSRF_TOKEN, + TYPE_OAUTH, + TYPE_REGISTRATION, + + ensureIndexes, + add, + createOrUpdate, + get, + update, + del, + delByUid, + getMostRecentSessionByUid, + getSessionListByUid, +}; diff --git a/hydro/model/training.js b/hydro/model/training.js deleted file mode 100644 index a1157cf8e..000000000 --- a/hydro/model/training.js +++ /dev/null @@ -1,107 +0,0 @@ -const document = require('./document'); -const { TrainingNotFoundError, TrainingAlreadyEnrollError } = require('../error'); - -function getStatus(domainId, tid, uid) { - return document.getStatus(domainId, document.TYPE_TRAINING, tid, uid); -} - -function getMultiStatus(domainId, query) { - return document.getMultiStatus(domainId, document.TYPE_TRAINING, query); -} - -async function getListStatus(domainId, uid, tids) { - 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; -} - -async function enroll(domainId, tid, uid) { - 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); -} - -function setStatus(domainId, tid, uid, $set) { - return document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, $set); -} - -function add(domainId, title, content, owner, dag = [], description = '') { - return document.add(domainId, content, owner, document.TYPE_TRAINING, null, null, null, { - dag, - title, - description, - enroll: 0, - }); -} - -function edit(domainId, tid, $set) { - return document.set(domainId, document.TYPE_TRAINING, tid, $set); -} - -global.Hydro.model.training = module.exports = { - getPids(tdoc) { - const pids = new Set(); - for (const node of tdoc.dag) { - for (const pid of node.pids) pids.add(pid); - } - return Array.from(pids); - }, - isDone(node, doneNids, donePids) { - return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids)) - && Set.isSuperset(new Set(donePids), new Set(node.pids))); - }, - 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); - }, - 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); - }, - isInvalid(node, doneNids) { - return (!Set.isSuperset(new Set(doneNids), new Set(node.requireNids))); - }, - add, - edit, - count: (domainId, query) => document.count(domainId, document.TYPE_TRAINING, query), - async 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; - }, - async getList(domainId, tids) { - 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; - }, - getMulti: (domainId, query) => document.getMulti(domainId, document.TYPE_TRAINING, query), - getMultiStatus, - getStatus, - enroll, - setStatus, - getListStatus, -}; diff --git a/hydro/model/training.ts b/hydro/model/training.ts new file mode 100644 index 000000000..8080d361a --- /dev/null +++ b/hydro/model/training.ts @@ -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 = 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); + +global.Hydro.model.training = { + getPids, + isDone, + isProgress, + isOpen, + isInvalid, + add, + edit, + count, + get, + getList, + getMulti, + getMultiStatus, + getStatus, + enroll, + setStatus, + getListStatus, +}; diff --git a/hydro/model/user.js b/hydro/model/user.ts similarity index 71% rename from hydro/model/user.js rename to hydro/model/user.ts index 2ba29fa52..0fc04330c 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.ts @@ -1,15 +1,15 @@ -const { BUILTIN_ROLES, BUILTIN_USERS, PRIV } = require('./builtin'); -const document = require('./document'); -const system = require('./system'); -const token = require('./token'); -const setting = require('./setting'); -const { UserNotFoundError, UserAlreadyExistError, LoginError } = require('../error'); -const pwhash = require('../lib/hash.hydro'); -const db = require('../service/db'); +import * as document from './document'; +import * as system from './system'; +import * as token from './token'; +import * as setting from './setting'; +import { BUILTIN_ROLES, BUILTIN_USERS, PRIV } from './builtin'; +import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error'; +import pwhash from '../lib/hash.hydro'; +import * as db from '../service/db'; const coll = db.collection('user'); -function setPassword(uid, password) { +export function setPassword(uid: number, password: string) { const salt = String.random(); return coll.findOneAndUpdate( { _id: uid }, @@ -17,7 +17,33 @@ function setPassword(uid, password) { ); } -class USER { +export class User { + udoc: () => any; + + dudoc: () => any; + + _id: number; + + mail: string; + + uname: string; + + salt: () => string; + + hash: () => string; + + hashType: string; + + priv: number; + + regat: Date; + + loginat: Date; + + perm: string; + + role: string; + constructor(udoc, dudoc) { this.udoc = () => udoc; this.dudoc = () => dudoc; @@ -48,15 +74,15 @@ class USER { } } - hasPerm(p) { + hasPerm(p: string) { return this.perm.includes(p); } - hasPriv(p) { + hasPriv(p: number) { return this.priv & p; } - checkPassword(password) { + 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())) { @@ -67,7 +93,7 @@ class USER { } } -async function getInDomain(domainId, udoc) { +export async function getInDomain(domainId: string, udoc: User) { let dudoc = await document.getStatus(domainId, document.TYPE_DOMAIN_USER, 0, udoc._id); dudoc = dudoc || {}; if (udoc._id === 1) dudoc.role = 'guest'; @@ -77,7 +103,7 @@ async function getInDomain(domainId, udoc) { return dudoc; } -async function getById(domainId, _id, throwError = false) { +export async function getById(domainId: string, _id: number, throwError = false) { const udoc = _id === 0 || _id === 1 ? BUILTIN_USERS[_id] : await coll.findOne({ _id }); @@ -86,18 +112,18 @@ async function getById(domainId, _id, throwError = false) { else return null; } const dudoc = await getInDomain(domainId, udoc); - return new USER(udoc, dudoc); + return new User(udoc, dudoc); } -async function getList(domainId, uids) { - uids = new Set(uids); +export async function getList(domainId: string, uids: number[]) { + 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); + for (const uid of _uids) r[uid] = await getById(domainId, uid); return r; } -async function getByUname(domainId, uname, ignoreMissing = false) { +export async function getByUname(domainId: string, uname: string, ignoreMissing = false) { const unameLower = uname.trim().toLowerCase(); const udoc = (unameLower === 'guest') ? BUILTIN_USERS[0] @@ -109,10 +135,10 @@ async function getByUname(domainId, uname, ignoreMissing = false) { throw new UserNotFoundError(uname); } const dudoc = await getInDomain(domainId, udoc); - return new USER(udoc, dudoc); + return new User(udoc, dudoc); } -async function getByEmail(domainId, mail, ignoreMissing = false) { +export async function getByEmail(domainId: string, mail: string, ignoreMissing = false) { const mailLower = mail.trim().toLowerCase(); const udoc = (mailLower === 'guest@hydro.local') ? BUILTIN_USERS[0] @@ -124,21 +150,21 @@ async function getByEmail(domainId, mail, ignoreMissing = false) { throw new UserNotFoundError(mail); } const dudoc = await getInDomain(domainId, udoc); - return new USER(udoc, dudoc); + return new User(udoc, dudoc); } -function setById(uid, $set, $unset) { - const op = {}; +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); } -function setEmail(uid, mail) { +export function setEmail(uid: number, mail: string) { return setById(uid, { mail, mailLower: mail.trim().toLowerCase() }); } -async function changePassword(uid, currentPassword, newPassword) { +export async function changePassword(uid: number, currentPassword: string, newPassword: string) { const udoc = await getById('system', uid); udoc.checkPassword(currentPassword); const salt = String.random(); @@ -148,7 +174,7 @@ async function changePassword(uid, currentPassword, newPassword) { ); } -async function inc(_id, field, n = 1) { +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; @@ -156,22 +182,20 @@ async function inc(_id, field, n = 1) { return udoc; } -function setInDomain(domainId, uid, params) { +export function setInDomain(domainId: string, uid: number, params: any) { return document.setStatus(domainId, document.TYPE_DOMAIN_USER, 0, uid, params); } -async function incDomain(domainId, uid, field, n = 1) { +export async function incDomain(domainId: string, uid: number, field: string, n = 1) { + // @ts-ignore const dudoc = await getInDomain(domainId, { _id: uid }); dudoc[field] = dudoc[field] + n || n; await setInDomain(domainId, uid, { [field]: dudoc[field] }); return dudoc; } -/** - * @returns {Promise} uid - */ -async function create({ - uid, mail, uname, password, regip = '127.0.0.1', priv = PRIV.PRIV_DEFAULT, +export async function create({ + uid = null, mail, uname, password, regip = '127.0.0.1', priv = PRIV.PRIV_DEFAULT, }) { const salt = String.random(); if (!uid) uid = await system.inc('user'); @@ -198,30 +222,30 @@ async function create({ return uid; } -function getMulti(params) { +export function getMulti(params) { return coll.find(params); } -function setMultiInDomain(domainId, query, params) { +export function setMultiInDomain(domainId: string, query: any, params: any) { return document.setMultiStatus(domainId, document.TYPE_DOMAIN_USER, query, params); } -async function getPrefixList(prefix, limit = 50) { +export async function getPrefixList(prefix: string, limit = 50) { prefix = prefix.toLowerCase(); - const $regex = new RegExp(`\\A\\Q${prefix.replace(/\\E/gmi, /\\E\\E\\Q/gmi)}\\E`, 'gmi'); + const $regex = new RegExp(`\\A\\Q${prefix}\\E`, 'gmi'); const udocs = await coll.find({ unameLower: { $regex } }).limit(limit).toArray(); return udocs; } -function setPriv(uid, priv) { +export function setPriv(uid: number, priv: number) { return coll.findOneAndUpdate({ _id: uid }, { $set: priv }, { returnOriginal: false }); } -function setRole(domainId, uid, role) { +export function setRole(domainId: string, uid: number, role: string) { return document.setStatus(domainId, document.TYPE_DOMAIN_USER, 0, uid, { role }); } -function setRoles(domainId, roles) { +export function setRoles(domainId: string, roles: any) { const tasks = []; for (const role in roles) { tasks.push(document.set( @@ -231,7 +255,7 @@ function setRoles(domainId, roles) { return Promise.all(tasks); } -async function getRoles(domainId) { +export async function getRoles(domainId: string) { const docs = await document.getMulti(domainId, document.TYPE_DOMAIN_USER).sort('_id', 1).toArray(); const roles = []; for (const doc of docs) { @@ -240,40 +264,40 @@ async function getRoles(domainId) { return roles; } -function getRole(domainId, name) { +export function getRole(domainId: string, name: string) { return document.get(domainId, document.TYPE_DOMAIN_USER, name); } -function getMultiInDomain(domainId, query) { +export function getMultiInDomain(domainId: string, query: any = {}) { return document.getMultiStatus(domainId, document.TYPE_DOMAIN_USER, query); } -function addRole(domainId, name, permission) { +export function addRole(domainId: string, name: string, permission: string) { return document.add(domainId, permission, 1, document.TYPE_DOMAIN_USER, name); } -function deleteRoles(domainId, roles) { +export function deleteRoles(domainId: string, roles) { return Promise.all([ document.deleteMulti(domainId, document.TYPE_DOMAIN_USER, { docId: { $in: roles } }), document.deleteMultiStatus(domainId, document.TYPE_DOMAIN_USER, { role: { $in: roles } }), ]); } -function ban(uid) { +export function ban(uid: number) { return Promise.all([ setPriv(uid, PRIV.PRIV_NONE), token.delByUid(uid), ]); } -function ensureIndexes() { +export function ensureIndexes() { return Promise.all([ coll.createIndex('unameLower', { unique: true }), coll.createIndex('mailLower', { sparse: true }), ]); } -global.Hydro.model.user = module.exports = { +global.Hydro.model.user = { changePassword, create, getByEmail, diff --git a/hydro/options.js b/hydro/options.ts similarity index 55% rename from hydro/options.js rename to hydro/options.ts index d5970802b..90aabe80f 100644 --- a/hydro/options.js +++ b/hydro/options.ts @@ -1,20 +1,17 @@ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const { defaults } = require('lodash'); +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { defaults } from 'lodash'; -let options = { +let f = path.resolve(process.cwd(), 'config.json'); +if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json'); +if (!fs.existsSync(f)) f = path.resolve('/config/config.json'); +const t = JSON.parse(fs.readFileSync(f).toString()); + +export default defaults(t, { name: 'hydro', host: '127.0.0.1', port: 27017, username: '', password: '', -}; - -let f = path.resolve(process.cwd(), 'config.json'); -if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json'); -if (!fs.existsSync(f)) f = path.resolve('/config/config.json'); -const t = JSON.parse(fs.readFileSync(f)); -options = defaults(t, options); - -module.exports = options; +}); diff --git a/hydro/script/blacklist.js b/hydro/script/blacklist.js index 20b153f38..ff875ae38 100644 --- a/hydro/script/blacklist.js +++ b/hydro/script/blacklist.js @@ -7,7 +7,7 @@ const document = require('../model/document'); const discussion = require('../model/discussion'); const user = require('../model/user'); const blacklist = require('../model/blacklist'); -const db = require('../service/db.js'); +const db = require('../service/db'); async function _address(ip, bset, uset, dset, dryrun, report) { if (bset.has(ip)) diff --git a/hydro/script/install.js b/hydro/script/install.js index 6d1eab284..d3a94efab 100644 --- a/hydro/script/install.js +++ b/hydro/script/install.js @@ -1,14 +1,10 @@ const description = 'Install'; -const { defaults } = require('lodash'); -const db = require('../service/db'); const { PRIV_ALL } = require('../model/builtin').PRIV; const system = require('../model/system'); +const user = require('../model/user'); const domain = require('../model/domain'); const pwhash = require('../lib/hash.hydro'); -const { udoc } = require('../interface'); - -const collUser = db.collection('user'); async function run({ username, password } = {}) { const def = { @@ -38,7 +34,7 @@ async function run({ username, password } = {}) { changemail_token_expire_seconds: 3600 * 24, registration_token_expire_seconds: 600, }; - let tasks = []; + const tasks = []; for (const key in def) { tasks.push(system.get(key).then((value) => { if (!value) return system.set(key, def[key]); @@ -46,26 +42,29 @@ async function run({ username, password } = {}) { })); } await Promise.all(tasks); - tasks = [domain.add('system', 0, 'Hydro', true)]; + await domain.add('system', 0, 'Hydro', true); if (username && password) { - const salt = String.random(); - tasks.push( - collUser.updateOne({ _id: -1 }, { - $set: defaults({ - _id: -1, - mail: 'root@hydro', - mailLower: 'root@hydro', - uname: username, - unameLower: username.trim().toLowerCase(), - hash: pwhash(password, salt), - salt, - gravatar: 'root@hydro', - priv: PRIV_ALL, - }, udoc), - }, { upsert: true }), - ); + const udoc = await user.getById('system', -1, false); + if (!udoc) { + await user.create({ + uid: -1, + mail: 'root@hydro.local', + uname: username, + password, + regip: '127.0.0.1', + priv: PRIV_ALL, + }); + } else { + const salt = String.random(); + await user.setById(-1, { + uname: username, + unameLower: username.trim().toLowerCase(), + salt, + hash: pwhash.default(password, salt), + hashType: 'hydro', + }); + } } - await Promise.all(tasks); } global.Hydro.script.install = module.exports = { run, description }; diff --git a/hydro/service/bus.js b/hydro/service/bus.ts similarity index 75% rename from hydro/service/bus.js rename to hydro/service/bus.ts index 1e1784d78..45c7a6526 100644 --- a/hydro/service/bus.js +++ b/hydro/service/bus.ts @@ -1,9 +1,9 @@ -const { EventEmitter } = require('events'); -const cluster = require('cluster'); +import { EventEmitter } from 'events'; +import cluster from 'cluster'; const bus = new EventEmitter(); -function subscribe(events, handler, funcName) { +export function subscribe(events, handler, funcName) { if (!funcName) { for (const event of events) bus.on(event, handler); } else { @@ -14,7 +14,7 @@ function subscribe(events, handler, funcName) { } } -function unsubscribe(events, handler, funcName) { +export function unsubscribe(events, handler, funcName) { if (!funcName) { for (const event of events) bus.off(event, handler); } else { @@ -27,7 +27,7 @@ function unsubscribe(events, handler, funcName) { } } -function publish(event, payload, isMaster = true) { +export function publish(event, payload, isMaster = true) { // Process forked by pm2 would also have process.send if (isMaster && process.send && !cluster.isMaster) { process.send({ @@ -40,6 +40,8 @@ function publish(event, payload, isMaster = true) { } } -global.Hydro.service.bus = module.exports = { +global.Hydro.service.bus = { subscribe, unsubscribe, publish, }; + +export default global.Hydro.service.bus; diff --git a/hydro/service/db.js b/hydro/service/db.js deleted file mode 100644 index e6c4b533f..000000000 --- a/hydro/service/db.js +++ /dev/null @@ -1,22 +0,0 @@ -const Mongo = require('mongodb'); -const bus = require('./bus'); -const options = require('../options'); - -let mongourl = 'mongodb://'; -if (options.username) mongourl += `${options.username}:${options.password}@`; -mongourl += `${options.host}:${options.port}/${options.name}`; -/** - * @type {import('mongodb').Db} - */ -let db = null; -Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) - .then((Client) => { - db = Client.db(options.name); - db.Client = Client; - bus.publish('system_database_connected', null); - }); - -global.Hydro.service.db = module.exports = { - collection: (c) => db.collection(c), - dropDatabase: () => db.dropDatabase(), -}; diff --git a/hydro/service/db.ts b/hydro/service/db.ts new file mode 100644 index 000000000..a198001c4 --- /dev/null +++ b/hydro/service/db.ts @@ -0,0 +1,23 @@ +import mongodb from 'mongodb'; +import * as bus from './bus'; +import options from '../options'; + +let mongourl = 'mongodb://'; +if (options.username) mongourl += `${options.username}:${options.password}@`; +mongourl += `${options.host}:${options.port}/${options.name}`; + +export let db: mongodb.Db = null; + +mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) + .then((Client) => { + db = Client.db(options.name); + bus.publish('system_database_connected', null); + }); + +export function collection(c: string) { + return db.collection(c); +} + +global.Hydro.service.db = { collection }; + +export default { collection }; diff --git a/hydro/service/gridfs.js b/hydro/service/gridfs.js deleted file mode 100644 index a08b419f4..000000000 --- a/hydro/service/gridfs.js +++ /dev/null @@ -1,6 +0,0 @@ -const { GridFSBucket } = require('mongodb'); -const db = require('./db'); - -if (!db.s) db.s = {}; - -global.Hydro.service.gridfs = module.exports = new GridFSBucket(db); diff --git a/hydro/service/gridfs.ts b/hydro/service/gridfs.ts new file mode 100644 index 000000000..2582b312e --- /dev/null +++ b/hydro/service/gridfs.ts @@ -0,0 +1,8 @@ +import { GridFSBucket } from 'mongodb'; +import { db } from './db'; + +const exp = new GridFSBucket(db); + +global.Hydro.service.gridfs = exp; + +export default exp; diff --git a/hydro/service/monitor.js b/hydro/service/monitor.ts similarity index 80% rename from hydro/service/monitor.js rename to hydro/service/monitor.ts index a44e65783..b13199e30 100644 --- a/hydro/service/monitor.js +++ b/hydro/service/monitor.ts @@ -1,5 +1,5 @@ -const db = require('./db'); -const sysinfo = require('../lib/sysinfo'); +import * as db from './db'; +import * as sysinfo from '../lib/sysinfo'; const coll = db.collection('status'); @@ -23,4 +23,6 @@ async function postInit() { setInterval(update, 60 * 1000); } -global.Hydro.service.monitor = module.exports = { postInit }; +global.Hydro.service.monitor = { postInit }; + +export default { postInit }; diff --git a/hydro/service/server.js b/hydro/service/server.ts similarity index 78% rename from hydro/service/server.js rename to hydro/service/server.ts index 701d59548..a51be504d 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.ts @@ -1,24 +1,26 @@ -const assert = require('assert'); -const path = require('path'); -const os = require('os'); -const _ = require('lodash'); -const { ObjectID } = require('mongodb'); -const Koa = require('koa'); -const yaml = require('js-yaml'); -const morgan = require('koa-morgan'); -const Body = require('koa-body'); -const Router = require('koa-router'); -const cache = require('koa-static-cache'); -const sockjs = require('sockjs'); -const http = require('http'); -const validator = require('../lib/validator'); -const template = require('../lib/template'); -const misc = require('../lib/misc'); -const user = require('../model/user'); -const system = require('../model/system'); -const blacklist = require('../model/blacklist'); -const token = require('../model/token'); -const opcount = require('../model/opcount'); +import assert from 'assert'; +import path from 'path'; +import os from 'os'; +import http from 'http'; +import _ from 'lodash'; +import { ObjectID } from 'mongodb'; +import Koa from 'koa'; +import yaml from 'js-yaml'; +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 { render } from '../lib/template'; +import * as validator from '../lib/validator'; +import * as misc from '../lib/misc'; +import * as user from '../model/user'; +import * as system from '../model/system'; +import * as blacklist from '../model/blacklist'; +import * as token from '../model/token'; +import * as opcount from '../model/opcount'; + const { UserNotFoundError, BlacklistedError, PermissionError, UserFacingError, ValidationError, PrivilegeError, @@ -31,21 +33,21 @@ const app = new Koa(); let server; const router = new Router(); -const _validateObjectId = (id, key) => { +const _validateObjectId = (id: string | ObjectID, key: string) => { if (ObjectID.isValid(id)) return new ObjectID(id); throw new ValidationError(key); }; -const _bool = (val) => !!val; -const _splitAndTrim = (val) => { +const _bool = (val: any) => !!val; +const _splitAndTrim = (val: string) => { const t = val.split(',').map((i) => i.trim().split('+')); return _.flatten(t); }; -const _date = (val) => { +const _date = (val: string) => { const d = val.split('-'); assert(d.length === 3); return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; }; -const _time = (val) => { +const _time = (val: string) => { const t = val.split(':'); assert(t.length === 2); return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`; @@ -67,29 +69,32 @@ const validate = { tag: _splitAndTrim, beginAtDate: _date, beginAtTime: _time, - pid: (pid) => (Number.isSafeInteger(parseInt(pid, 10)) ? parseInt(pid, 10) : pid), + pid: (pid: string) => (Number.isSafeInteger(parseInt(pid, 10)) ? parseInt(pid, 10) : pid), content: validator.checkContent, title: validator.checkTitle, - uid: (uid) => parseInt(validator.checkUid(uid), 10), + uid: (uid: string) => parseInt(validator.checkUid(uid), 10), password: validator.checkPassword, mail: validator.checkEmail, uname: validator.checkUname, - page: (page) => { - if (Number.isSafeInteger(parseInt(page, 10))) page = parseInt(page, 10); + page: (_page: string) => { + let page: number; + if (Number.isSafeInteger(parseInt(_page, 10))) page = parseInt(_page, 10); if (page <= 0) throw new ValidationError('page'); return page; }, - duration: (duration) => { - if (!Number.isNaN(parseFloat(duration))) duration = parseFloat(duration); + duration: (_duration: string) => { + let duration: number; + if (!Number.isNaN(parseFloat(_duration))) duration = parseFloat(_duration); if (duration <= 0) throw new ValidationError('duration'); return duration; }, - pids: (pids) => { - const res = pids.split(',').map((i) => i.trim()); - for (const i in res) { - if (Number.isSafeInteger(parseInt(res[i], 10))) res[i] = parseInt(res[i], 10); + pids: (pids: string) => { + const src = pids.split(',').map((i) => i.trim()); + const d: number[] = []; + for (const i in src) { + if (Number.isSafeInteger(parseInt(src[i], 10))) d[i] = parseInt(src[i], 10); } - return res; + return d; }, role: validator.checkRole, penaltyRules: (penaltyRules) => { @@ -122,15 +127,16 @@ async function prepare() { } const HandlerMixin = (Class) => class extends Class { - async renderHTML(name, context) { + async renderHTML(name: string, context: any): Promise { if (enableLog) console.time(name); this.hasPerm = (perm) => this.user.hasPerm(perm); // FIXME fix this ugly hack this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) }; - const res = await template.render(name, Object.assign(context, { + const res = await render(name, Object.assign(context, { handler: this, + // @ts-ignore url: (...args) => this.url(...args), - _: (str) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''), + _: (str: string) => (str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''), user: this.user, })); if (enableLog) console.timeEnd(name); @@ -185,7 +191,7 @@ const HandlerMixin = (Class) => class extends Class { url(name, kwargs = {}) { let res = '#'; - const args = { ...kwargs }; + const args: any = { ...kwargs }; try { if (this.args.domainId !== 'system') { name += '_with_domainId'; @@ -203,11 +209,45 @@ const HandlerMixin = (Class) => class extends Class { } }; -const Handler = HandlerMixin(class { - /** - * @param {import('koa').Context} ctx - */ - constructor(ctx, args) { +export const Handler = HandlerMixin(class { + 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: any, + json: boolean, + }; + + response: { + body: any, + type: string, + status: number, + template: string | undefined, + redirect: string | undefined, + disposition: string | undefined, + attachment: (name: string) => void, + }; + + session: any; + + csrfToken: string; + + user: any; + + constructor(ctx: Koa.Context, args: any) { this.ctx = ctx; this.args = args; this.request = { @@ -222,6 +262,7 @@ const Handler = HandlerMixin(class { path: ctx.path, params: ctx.params, referer: ctx.request.headers.referer || '/', + json: (ctx.request.headers.accept || '').includes('application/json'), }; this.response = { body: {}, @@ -230,6 +271,7 @@ const Handler = HandlerMixin(class { template: null, redirect: null, attachment: (name) => ctx.attachment(name), + disposition: null, }; this.UIContext = { cdn_prefix: '/', @@ -238,17 +280,18 @@ const Handler = HandlerMixin(class { this.session = {}; } - async render(name, context) { + async render(name: string, context: any) { + // @ts-ignore this.response.body = await this.renderHTML(name, context); this.response.type = 'text/html'; } - back(body) { + back(body: any) { this.response.body = body || this.response.body || {}; this.response.redirect = this.request.headers.referer || '/'; } - binary(data, name) { + binary(data: any, name: string) { this.response.body = data; this.response.template = null; this.response.type = 'application/octet-stream'; @@ -275,7 +318,6 @@ const Handler = HandlerMixin(class { } async init({ domainId }) { - this.preferJson = (this.request.headers.accept || '').includes('application/json'); await Promise.all([ this.getSession(), this.getBdoc(), @@ -286,7 +328,7 @@ const Handler = HandlerMixin(class { ]); } - async checkCsrfToken(csrfToken) { + async checkCsrfToken(csrfToken: any) { const sdoc = await token.get(csrfToken, token.TYPE_CSRF_TOKEN, false); if (!sdoc || sdoc.uid !== this.user._id) throw new CsrfTokenError(csrfToken); } @@ -296,7 +338,7 @@ const Handler = HandlerMixin(class { await this.renderBody(); } catch (error) { this.response.status = error instanceof UserFacingError ? error.code : 500; - if (this.preferJson) this.response.body = { error }; + if (this.request.json) this.response.body = { error }; else await this.render(error instanceof UserFacingError ? 'error.html' : 'bsod.html', { error }); } await this.putResponse(); @@ -304,9 +346,9 @@ const Handler = HandlerMixin(class { } async renderBody() { - if (!this.response.redirect && !this.preferJson) { + if (!this.response.redirect && !this.request.json) { if (this.response.body || this.response.template) { - if (this.request.query.noTemplate || this.preferJson) return; + if (this.request.query.noTemplate || this.request.json) return; const templateName = this.request.query.template || this.response.template; if (templateName) { this.response.body = this.response.body || {}; @@ -318,7 +360,7 @@ const Handler = HandlerMixin(class { async putResponse() { if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); - if (this.response.redirect && !this.preferJson) { + 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); @@ -331,7 +373,7 @@ const Handler = HandlerMixin(class { this.ctx.response.body = this.response.body; this.ctx.response.status = this.response.status || 200; } - this.ctx.response.type = this.preferJson + this.ctx.response.type = this.request.json ? 'application/json' : this.response.type ? this.response.type @@ -367,7 +409,7 @@ const Handler = HandlerMixin(class { }, ); } - const cookie = { secure: await system.get('session.secure') }; + const cookie: any = { secure: await system.get('session.secure') }; if (this.session.save) { cookie.expires = this.session.expireAt; cookie.maxAge = expireSeconds; @@ -469,36 +511,47 @@ const Checker = (permPrivChecker) => { }; }; -function Route(name, route, RouteHandler, ...permPrivChecker) { +export function Route(name, route, RouteHandler, ...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)); } -const ConnectionHandler = HandlerMixin(class { - /** - * @param {import('sockjs').Connection} conn - */ - constructor(conn) { +export const ConnectionHandler = HandlerMixin(class { + conn: sockjs.Connection; + + request: { + params: any + headers: any + ip: string + } + + session: any + + user: any + + constructor(conn: sockjs.Connection) { this.conn = conn; this.request = { params: {}, headers: conn.headers, + ip: this.conn.remoteAddress, }; - const p = (conn.url.split('?')[1] || '').split('&'); + 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]); } - send(data) { + send(data: JSON) { this.conn.write(JSON.stringify(data)); } - close(code, reason) { - this.conn.close(code, reason); + close(code: number, reason: string) { + this.conn.close(code.toString(), reason); } - onerror(err) { + onerror(err: Error) { console.error(err); this.close(1001, err.toString()); } @@ -516,7 +569,16 @@ const ConnectionHandler = HandlerMixin(class { } }); -function Connection(name, prefix, RouteConnHandler, ...permPrivChecker) { +interface IConnectionHandler { + // @ts-ignore + new(conn: sockjs.Connection): ConnectionHandler; +} + +export function Connection( + name: string, prefix: string, + RouteConnHandler: IConnectionHandler, + ...permPrivChecker: Array +) { const sock = sockjs.createServer({ prefix }); const checker = Checker(permPrivChecker); sock.on('connection', async (conn) => { @@ -557,16 +619,16 @@ function Connection(name, prefix, RouteConnHandler, ...permPrivChecker) { sock.installHandlers(server); } -function Middleware(middleware) { +export function Middleware(middleware: Koa.Middleware) { app.use(middleware); } -function Validate(key, func) { +export function Validate(key: string, func: Function) { if (validate[key]) validate[key].push(func); else validate[key] = [func]; } -async function start() { +export async function start() { const [disableLog, port] = await system.getMany(['server.log', 'server.port']); if (!disableLog) { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); @@ -577,6 +639,8 @@ async function start() { console.log('Server listening at: %s', port); } -global.Hydro.service.server = module.exports = { +global.Hydro.service.server = { Handler, ConnectionHandler, Route, Connection, Middleware, Validate, prepare, start, }; + +export default global.Hydro.service.server; diff --git a/hydro/utils.js b/hydro/utils.ts similarity index 63% rename from hydro/utils.js rename to hydro/utils.ts index ccaad2614..f51c0a941 100644 --- a/hydro/utils.js +++ b/hydro/utils.ts @@ -1,12 +1,33 @@ -const fs = require('fs'); -const cluster = require('cluster'); -const path = require('path'); -const superagent = require('superagent'); -const proxy = require('superagent-proxy'); +import fs from 'fs'; +import cluster from 'cluster'; +import path from 'path'; +import * as superagent from 'superagent'; +import proxy from 'superagent-proxy'; + +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; + } + interface SetConstructor { + isSuperset: (set: Set, subset: Set) => boolean; + intersection: (setA: Set, setB: Set) => Set; + union: (setA: Set, setB: Set) => Set; + } +} proxy(superagent); if (!cluster.worker) { + // @ts-ignore cluster.worker = { id: 0 }; } @@ -22,14 +43,12 @@ 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; } + for (const i in a) { + if (a[i] !== b[i]) return true; + } return false; }; -/** - * @param {string} format %Y-%m-%d %H:%M:%S - * @returns {string} Formatted date string - */ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { let h = this.getHours(); if (h < 10) h = `0${h}`; @@ -46,17 +65,16 @@ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { .replace('%S', s); }; -/** - * @param {object} param0 - * @returns {Date} - */ -Date.prototype.delta = function delta({ - year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, -}) { - let ts = this.getTime(); - ts += second * 1000 + minute * 60000 + hour * 60 * 60000; - ts += day * 24 * 60 * 60000 + month * 30 * 24 * 60 * 60000 + year * 365 * 24 * 60 * 60000; - return new Date(ts); +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) { @@ -66,27 +84,22 @@ Set.isSuperset = function isSuperset(set, subset) { return true; }; -Set.union = function Union(setA, setB) { +Set.union = function Union(setA: Set, setB: Set) { const union = new Set(setA); for (const elem of setB) union.add(elem); return union; }; -Set.intersection = function Intersection(setA, setB) { +Set.intersection = function Intersection(setA: Set, setB: Set): Set { const intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) intersection.add(elem); } + // @ts-ignore return intersection; }; -/** - * calculate size of a file or directory synchronously - * @param {String} folderPath path of the file or folder - * @param {fsizeOptions} opts additional options - * @returns {Number} - */ -function folderSize(folderPath) { +export function folderSize(folderPath: string) { let size = 0; const _next = function a(p) { if (p) { @@ -108,36 +121,24 @@ function folderSize(folderPath) { return size; } -exports.folderSize = folderSize; - 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 }; -function parseTimeMS(str) { +export function parseTimeMS(str) { const match = TIME_RE.exec(str); - if (!match) throw new Error(str, 'error parsing time'); - return parseInt(parseFloat(match[1], 10) * TIME_UNITS[match[2]], 10); + if (!match) throw new Error(`${str} error parsing time`); + return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); } -exports.parseTimeMS = parseTimeMS; - -function parseMemoryMB(str) { +export function parseMemoryMB(str) { const match = MEMORY_RE.exec(str); - if (!match) throw new Error(str, 'error parsing memory'); - return parseInt(parseFloat(match[1], 10) * MEMORY_UNITS[match[2]], 10); + if (!match) throw new Error(`${str} error parsing memory`); + return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); } -exports.parseMemoryMB = parseMemoryMB; - -/** - * Checks if an object could be an instantiable class. - * @param {any} obj - * @param {boolean} strict - * @returns {boolean} - */ -function isClass(obj, strict = false) { +export function isClass(obj: any, strict = false) { if (typeof obj !== 'function') return false; const str = obj.toString(); if (obj.prototype === undefined) return false; @@ -152,5 +153,3 @@ function isClass(obj, strict = false) { } return false; } - -exports.isClass = isClass; diff --git a/package.json b/package.json index c66cd347a..d47933a92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.5.0", + "version": "2.6.0", "main": "hydro/loader.js", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", @@ -10,7 +10,6 @@ "adm-zip": "^0.4.16", "ansi_up": "^4.0.4", "detect-browser": "^5.1.1", - "eslint": "7.2.0", "js-yaml": "^3.13.1", "koa": "^2.13.0", "koa-body": "^4.2.0", @@ -30,26 +29,50 @@ "nodemailer": "^6.4.10", "nunjucks": "^3.2.1", "prismjs": "^1.20.0", - "snyk": "^1.354.0", "sockjs": "^0.3.20", "superagent": "^5.2.2", "superagent-proxy": "^2.0.0", "systeminformation": "^4.26.5", - "yargs": "^15.3.1" + "yargs": "^15.4.0" }, "devDependencies": { + "@types/adm-zip": "^0.4.33", + "@types/koa": "^2.11.3", + "@types/koa-morgan": "^1.0.4", + "@types/koa-router": "^7.4.1", + "@types/koa-static-cache": "^5.1.0", + "@types/lodash": "^4.14.157", + "@types/markdown-it": "^10.0.1", + "@types/mongodb": "^3.5.25", + "@types/node": "^14.0.18", + "@types/nodemailer": "^6.4.0", + "@types/nunjucks": "^3.1.3", + "@types/prismjs": "^1.16.1", + "@types/sockjs": "^0.3.32", + "@types/superagent": "^4.1.8", + "@types/superagent-proxy": "^2.0.0", + "@types/yargs": "^15.0.5", + "@typescript-eslint/eslint-plugin": "^3.6.0", + "@typescript-eslint/parser": "^3.6.0", + "eslint": "^7.4.0", "eslint-config-airbnb-base": "^14.2.0", + "eslint-import-resolver-typescript": "^2.0.0", "eslint-plugin-import": "^2.20.2", "friendly-errors-webpack-plugin": "^1.7.0", + "snyk": "^1.354.0", + "ts-node": "^8.10.2", + "tsc": "^1.20150623.0", + "typescript": "^3.9.6", "webpack": "^4.42.1" }, "scripts": { - "build": "node build/development.js", - "build:development": "node build/development.js", - "build:production": "node build/production.js", - "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production", - "start": "node hydro/loader.js", - "pack": "pkg .", + "tsc": "tsc", + "build": "tsc && cp -r hydro/script dist", + "pack:development": "node build/development.js", + "pack:production": "node build/production.js", + "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build && yarn pack:production", + "start": "ts-node hydro/loader.ts", + "pkg": "pkg .", "lint": "eslint build hydro module tool --fix", "snyk-protect": "snyk protect", "prepare": "yarn run snyk-protect" @@ -63,4 +86,4 @@ ] }, "snyk": true -} +} \ No newline at end of file diff --git a/templates/homework_main.html b/templates/homework_main.html index 50ec92ec7..ff17fa9a3 100644 --- a/templates/homework_main.html +++ b/templates/homework_main.html @@ -47,7 +47,7 @@ {{ _('Open Since') }}: {{ datetimeSpan(tdoc['beginAt'])|safe }}
  • - {% if model.contest.isHomeworkExtended(tdoc) or model.contest.isDone(tdoc) %} + {% if model.contest.isExtended(tdoc) or model.contest.isDone(tdoc) %} {{ _('Hard Deadline') }}: {{ datetimeSpan(tdoc['endAt'])|safe }} {% else %} {{ _('Deadline') }}: {{ datetimeSpan(tdoc['penaltySince'])|safe }} diff --git a/templates/main.html b/templates/main.html index e7d50b02b..3e05324c5 100644 --- a/templates/main.html +++ b/templates/main.html @@ -81,7 +81,7 @@ {{ _('Open Since') }}: {{ datetimeSpan(tdoc['beginAt'])|safe }}
  • - {% if model.contest.isHomeworkExtended(tdoc) or model.contest.isDone(tdoc) %} + {% if model.contest.isExtended(tdoc) or model.contest.isDone(tdoc) %} {{ _('Hard Deadline') }}: {{ datetimeSpan(tdoc['endAt'])|safe }} {% else %} {{ _('Deadline') }}: {{ datetimeSpan(tdoc['penaltySince'])|safe }} diff --git a/templates/partials/problem_sidebar_homework.html b/templates/partials/problem_sidebar_homework.html index 14d851659..7c53d6861 100644 --- a/templates/partials/problem_sidebar_homework.html +++ b/templates/partials/problem_sidebar_homework.html @@ -7,7 +7,7 @@ {% endif %}