diff --git a/build/development.js b/build/development.js deleted file mode 100644 index 43e47641c..000000000 --- a/build/development.js +++ /dev/null @@ -1,4 +0,0 @@ -global.DEBUG = true; -require('./index.js')('development').catch((e) => { - console.error(e); -}); diff --git a/build/hack.js b/build/hack.js index f2d8e27fd..499b2e206 100644 --- a/build/hack.js +++ b/build/hack.js @@ -25,16 +25,6 @@ module.exports = { console.error('Cannot hack sockjs'); } }, - yargs: () => { - const p = require.resolve('yargs'); - const file = fs.readFileSync(p).toString().split('\n'); - if (file[12].includes('const argv = yargs(processArgs, cwd, require)')) { - file[12] = 'const argv = yargs(processArgs, cwd)'; - fs.writeFileSync(p, file.join('\n')); - } else if (!file[12].includes('const argv = yargs(processArgs, cwd)')) { - console.error('Cannot hack yargs'); - } - }, saslprep: () => { const q = require.resolve('saslprep'); const mem = path.join(path.dirname(q), 'code-points.mem'); @@ -48,4 +38,14 @@ module.exports = { console.error('Cannot hack saslprep'); } }, + ftp: () => { + const p = require.resolve('ftp/lib/connection'); + const file = fs.readFileSync(p).toString().split('\n'); + if (file[51].includes('new Buffer')) { + file[51] = file[51].replace('new Buffer', 'Buffer.from'); + fs.writeFileSync(p, file.join('\n')); + } else if (!file[51].includes('Buffer.from')) { + console.error('Cannot hack ftp'); + } + }, }; diff --git a/build/index.js b/build/index.js index bb90f8660..b86c8a481 100644 --- a/build/index.js +++ b/build/index.js @@ -3,6 +3,7 @@ const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const yaml = require('js-yaml'); +const argv = require('yargs-parser')(process.argv.slice(2)); const { root, ignoreFailure, rmdir } = require('./utils'); const template = require('./template'); const hack = require('./hack'); @@ -84,4 +85,7 @@ async function build(type) { fs.writeFileSync(root('.build/install.js'), `global.Hydro="${d}"; ${installer}`); } -module.exports = build; +build(argv.development ? 'development' : 'production').catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/build/production.js b/build/production.js deleted file mode 100644 index ec842afda..000000000 --- a/build/production.js +++ /dev/null @@ -1,3 +0,0 @@ -require('./index.js')('production').catch((e) => { - console.error(e); -}); diff --git a/build/template.js b/build/template.js index 57ff42adc..a418e3792 100644 --- a/build/template.js +++ b/build/template.js @@ -24,27 +24,6 @@ const build = (dirOrObject) => { templates[i] = template; } } else templates = dirOrObject; - for (const i in templates) { - if (i.endsWith('.html') && !global.DEBUG && !templates[i].includes('No Compression')) { - templates[i] = templates[i] - .trim() - .replace(/ *\n */gmi, ' ') - .replace(/, /gmi, ',') - .replace(/%} {%/gmi, '%}{%') - .replace(/ %}/gmi, '%}') - .replace(/{% /gmi, '{%') - .replace(/> <') - .replace(/}} {{/gmi, '>{{') - .replace(/{{ /gmi, '{{') - .replace(/ }}/gmi, '}}') - .replace(/%} {%/gmi, '>{%') - .replace(/= /gmi, '=') - .replace(/ =/gmi, '=') - .trim(); - } - } return templates; }; diff --git a/build/webpack.js b/build/webpack.js index 5abf38f23..352a9081e 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -1,4 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ +const path = require('path'); const webpack = require('webpack'); const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); const { root } = require('./utils'); @@ -7,14 +8,31 @@ const build = async (type) => { const config = { mode: type, entry: { - app: root('dist/loader.js'), + app: root('hydro/loader.ts'), }, output: { filename: '[name].js', path: root('.build'), }, target: 'node', - module: {}, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'cache-loader', + options: { + cacheDirectory: path.resolve(__dirname, '..', '.cache', 'ts'), + }, + }, + 'ts-loader', + ], + }, + ], + }, + resolve: { extensions: ['.js', '.ts'] }, plugins: [ new webpack.ProgressPlugin(), new FriendlyErrorsPlugin({ diff --git a/hydro/handler/contest.ts b/hydro/handler/contest.ts index bb8ae02a7..2ca57b119 100644 --- a/hydro/handler/contest.ts +++ b/hydro/handler/contest.ts @@ -375,6 +375,7 @@ class ContestCreateHandler extends ContestHandler { } export async function apply() { + Route('contest_create', '/contest/create', ContestCreateHandler, PERM.PERM_CREATE_CONTEST); Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST); Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST); Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST); @@ -383,7 +384,6 @@ export async function apply() { 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 = apply; diff --git a/hydro/handler/discussion.ts b/hydro/handler/discussion.ts index 0872358c6..e4c47d145 100644 --- a/hydro/handler/discussion.ts +++ b/hydro/handler/discussion.ts @@ -1,4 +1,5 @@ import { ObjectID } from 'mongodb'; +import { isSafeInteger } from 'lodash'; import { DiscussionNotFoundError, DocumentNotFoundError } from '../error'; import paginate from '../lib/paginate'; import * as system from '../model/system'; @@ -87,17 +88,23 @@ class DiscussionMainHandler extends Handler { } class DiscussionNodeHandler extends DiscussionHandler { - async get({ - domainId, type, name, page = 1, - }) { - if (ObjectID.isValid(name)) name = new ObjectID(name); + @param('type', Types.String) + @param('name', Types.String) + @param('page', Types.UnsignedInt, true) + async get(domainId: string, type: string, _name: string, page = 1) { + let name: ObjectID | string | number; + if (ObjectID.isValid(_name)) name = new ObjectID(_name); + else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10); + else name = _name; const [ddocs, dpcount] = await paginate( discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }), page, await system.get('DISCUSSION_PER_PAGE'), ); + const uids = ddocs.map((ddoc) => ddoc.owner); + uids.push(this.vnode.owner); const [udict, vnodes] = await Promise.all([ - user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)), + user.getList(domainId, uids), discussion.getNodes(domainId), ]); const path = [ @@ -105,7 +112,7 @@ class DiscussionNodeHandler extends DiscussionHandler { ['discussion_main', 'discussion_main'], [this.vnode.title, null, true], ]; - const vndict = { [typeMapper[type]]: { [name]: this.vnode } }; + const vndict = { [typeMapper[type]]: { [name.toString()]: this.vnode } }; this.response.template = 'discussion_main_or_node.html'; this.response.body = { ddocs, @@ -138,11 +145,20 @@ class DiscussionCreateHandler extends DiscussionHandler { this.response.body = { path, vnode: this.vnode }; } - async post({ - domainId, type, name, title, content, highlight, - }) { + @param('type', Types.String) + @param('name', Types.String) + @param('title', Types.String, isTitle) + @param('content', Types.String, isContent) + @param('highlight', Types.Boolean, true) + async post( + domainId: string, type: string, _name: string, + title: string, content: string, highlight = false, + ) { this.limitRate('add_discussion', 3600, 30); - if (ObjectID.isValid(name)) name = new ObjectID(name); + let name: ObjectID | string | number; + if (ObjectID.isValid(_name)) name = new ObjectID(_name); + else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10); + else name = _name; if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION); const did = await discussion.add( domainId, typeMapper[type], name, this.user._id, diff --git a/hydro/handler/judge.ts b/hydro/handler/judge.ts index 71251b595..a66051135 100644 --- a/hydro/handler/judge.ts +++ b/hydro/handler/judge.ts @@ -1,4 +1,5 @@ import yaml from 'js-yaml'; +import { ObjectID } from 'mongodb'; import { parseTimeMS, parseMemoryMB } from '../utils'; import * as record from '../model/record'; import * as problem from '../model/problem'; @@ -8,7 +9,7 @@ import * as user from '../model/user'; import * as task from '../model/task'; import * as bus from '../service/bus'; import { - Route, Handler, Connection, ConnectionHandler, + Route, Handler, Connection, ConnectionHandler, Types, param, } from '../service/server'; async function _postJudge(rdoc) { @@ -36,6 +37,8 @@ async function _postJudge(rdoc) { } export async function next(body) { + if (body.rid) body.rid = new ObjectID(body.rid); + if (body.tid) body.tid = new ObjectID(body.tid); let rdoc = await record.get(body.domainId, body.rid); const $set: any = {}; const $push: any = {}; @@ -66,6 +69,8 @@ export async function next(body) { } export async function end(body) { + if (body.rid) body.rid = new ObjectID(body.rid); + if (body.tid) body.tid = new ObjectID(body.tid); let rdoc = await record.get(body.domainId, body.rid); const $set: any = {}; const $push: any = {}; @@ -92,9 +97,17 @@ export async function end(body) { } class PretestHandler extends Handler { - async post({ - domainId, pid, code, lang, time = '1s', memory = '256m', input = '', - }) { + @param('pid', Types.UnsignedInt, true) + @param('code', Types.String) + @param('lang', Types.String) + @param('time', Types.String, true) + @param('memory', Types.String, true) + @param('input', Types.String, true) + async post( + domainId: string, pid = 0, + code: string, lang: string, + time = '1s', memory = '256m', input = '', + ) { if (pid) { const pdoc = await problem.get(domainId, pid); if (pdoc.config) { @@ -104,7 +117,7 @@ class PretestHandler extends Handler { } } const rid = await record.add(domainId, { - pid: pid || String.random(16), + pid, uid: this.user._id, type: 'run', time: parseTimeMS(time), diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index 8aed68703..551cfbca0 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -403,10 +403,6 @@ class ProblemSolutionHandler extends ProblemDetailHandler { }; } - async post({ domainId, psid }) { - if (psid) this.psdoc = await solution.get(domainId, new ObjectID(psid)); - } - @param('content', Types.String, isContent) async postSubmit(domainId: string, content: string) { this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION); @@ -415,17 +411,21 @@ class ProblemSolutionHandler extends ProblemDetailHandler { } @param('content', Types.String, isContent) - async postEditSolution(domainId: string, content: string) { - if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION); + @param('psid', Types.ObjectID) + async postEditSolution(domainId: string, content: string, psid: ObjectID) { + let psdoc = await solution.get(domainId, psid); + if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION); else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF); - const psdoc = await solution.edit(domainId, this.psdoc.docId, content); + psdoc = await solution.edit(domainId, this.psdoc.docId, content); this.back({ psdoc }); } - async postDeleteSolution({ domainId }) { - if (this.psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION); + @param('psid', Types.ObjectID) + async postDeleteSolution(domainId: string, psid: ObjectID) { + const psdoc = await solution.get(domainId, psid); + if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION); else this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF); - await solution.del(domainId, this.psdoc.docId); + await solution.del(domainId, psdoc.docId); this.back(); } @@ -471,16 +471,16 @@ class ProblemSolutionHandler extends ProblemDetailHandler { this.back(); } - async postUpvote({ domainId }) { - const [psdoc, pssdoc] = await solution.vote(domainId, this.psdoc.docId, this.user._id, 1); - this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - this.back(); + @param('psid', Types.ObjectID) + async postUpvote(domainId: string, psid: ObjectID) { + const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, 1); + this.back({ vote: psdoc.vote, user_vote: pssdoc.vote }); } - async postDownvote({ domainId }) { - const [psdoc, pssdoc] = await solution.vote(domainId, this.psdoc.docId, this.user._id, -1); - this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - this.back(); + @param('psid', Types.ObjectID) + async postDownvote(domainId: string, psid: ObjectID) { + const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, -1); + this.back({ vote: psdoc.vote, user_vote: pssdoc.vote }); } } diff --git a/hydro/lib/template.ts b/hydro/lib/template.ts index 300655fba..4bbade300 100644 --- a/hydro/lib/template.ts +++ b/hydro/lib/template.ts @@ -1,16 +1,33 @@ +import fs from 'fs'; +import path from 'path'; 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) { - if (!global.Hydro.template[name]) throw new Error(`Cannot get template ${name}`); - return { - src: global.Hydro.template[name], - path: name, - }; -}; +class Loader extends nunjucks.Loader { + // eslint-disable-next-line class-methods-use-this + getSource(name: string) { + if (!process.env.debug) { + if (!global.Hydro.template[name]) throw new Error(`Cannot get template ${name}`); + return { + src: global.Hydro.template[name], + path: name, + noCache: false, + }; + } + let fullpath = null; + const base = path.join(process.cwd(), 'templates'); + const p = path.resolve(base, name); + if (fs.existsSync(p)) fullpath = p; + if (!fullpath) throw new Error(`Cannot get template ${name}`); + return { + src: fs.readFileSync(fullpath, 'utf-8'), + path: fullpath, + noCache: true, + }; + } +} class Nunjucks extends nunjucks.Environment { constructor() { diff --git a/hydro/loader.ts b/hydro/loader.ts index fbec6e861..4aa734de9 100644 --- a/hydro/loader.ts +++ b/hydro/loader.ts @@ -4,7 +4,9 @@ import './interface'; import cluster from 'cluster'; -import { argv } from 'yargs'; +import parse from 'yargs-parser'; + +const argv = parse(process.argv.slice(2)); global.Hydro = { stat: { reqCount: 0 }, @@ -25,7 +27,10 @@ global.Hydro = { }; global.onDestory = []; -if (argv.debug) console.log(process.argv); +if (argv.debug) { + console.log(process.argv); + process.env.debug = 'enable'; +} async function terminate() { for (const task of global.onDestory) { diff --git a/hydro/model/contest.ts b/hydro/model/contest.ts index 8a402bcca..ae9894377 100644 --- a/hydro/model/contest.ts +++ b/hydro/model/contest.ts @@ -51,7 +51,7 @@ const acm = { if (!j.accept) naccept[j.pid]++; } function _time(jdoc) { - const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); + const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000); const penalty = 20 * 60 * naccept[jdoc.pid]; return real + penalty; } @@ -73,23 +73,27 @@ const acm = { { type: 'solved_problems', value: _('Solved Problems') }, ]; if (isExport) { - columns.push({ type: 'total_time', value: _('Total Time (Seconds)') }); - columns.push({ type: 'total_time_str', value: _('Total Time') }); + columns.push( + { type: 'total_time', value: _('Total Time (Seconds)') }, + { type: 'total_time_str', value: _('Total Time') }, + ); } for (const i in tdoc.pids) { if (isExport) { - columns.push({ - type: 'problem_flag', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); - columns.push({ - type: 'problem_time', - value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), - }); - columns.push({ - type: 'problem_time_str', - value: '#{0} {1}'.format(i + 1, _('Time')), - }); + columns.push( + { + type: 'problem_flag', + value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), + }, + { + type: 'problem_time', + value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), + }, + { + type: 'problem_time_str', + value: '#{0} {1}'.format(i + 1, _('Time')), + }, + ); } else { columns.push({ type: 'problem_detail', diff --git a/hydro/model/discussion.ts b/hydro/model/discussion.ts index c37aadf28..4e23b7cd3 100644 --- a/hydro/model/discussion.ts +++ b/hydro/model/discussion.ts @@ -15,7 +15,7 @@ export const typeDisplay = { }; export function add( - domainId: string, parentType: number, parentId: ObjectID, + domainId: string, parentType: number, parentId: ObjectID | number | string, owner: number, title: string, content: string, ip: string | null = null, highlight: boolean = false, ) { diff --git a/hydro/model/task.ts b/hydro/model/task.ts index 8c8f5b79b..fa237f5ec 100644 --- a/hydro/model/task.ts +++ b/hydro/model/task.ts @@ -28,20 +28,19 @@ export function del(_id: ObjectID) { 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(); - if (res.length) { - await coll.deleteOne({ _id: res[0]._id }); - if (res[0].interval) { + const res = await coll.findOneAndDelete(q); + if (res.value) { + if (res.value.interval) { await coll.insertOne({ - ...res[0], executeAfter: moment().add(...res[0].interval).toDate(), + ...res.value, executeAfter: moment().add(...res.value.interval).toDate(), }); } - return res[0]; + return res.value; } return null; } -export async function consume(query, cb: Function) { +export async function consume(query: any, cb: Function) { let isRunning = false; const interval = setInterval(async () => { if (isRunning) return; diff --git a/package.json b/package.json index d0ddd6ea9..2d36f0e10 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "superagent": "^5.2.2", "superagent-proxy": "^2.0.0", "systeminformation": "^4.26.5", - "yargs": "^15.4.0" + "yargs-parser": "^18.1.3" }, "devDependencies": { "@types/adm-zip": "^0.4.33", @@ -45,7 +45,7 @@ "@types/lodash": "^4.14.157", "@types/markdown-it": "^10.0.1", "@types/mongodb": "^3.5.25", - "@types/node": "^14.0.21", + "@types/node": "^14.0.22", "@types/nodemailer": "^6.4.0", "@types/nunjucks": "^3.1.3", "@types/prismjs": "^1.16.1", @@ -55,21 +55,21 @@ "@types/yargs": "^15.0.5", "@typescript-eslint/eslint-plugin": "^3.6.0", "@typescript-eslint/parser": "^3.6.0", + "cache-loader": "^4.1.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", + "ts-loader": "^8.0.0", "typescript": "^3.9.6", "webpack": "^4.42.1" }, "scripts": { - "build": "tsc && node build/development.js", - "build:development": "tsc && node build/development.js", - "build:production": "tsc && node build/production.js", + "build": "node build --development", + "build:development": "node build --development", + "build:production": "node build --production", "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production", "start": "ts-node hydro/loader.ts", "pkg": "pkg .", @@ -86,4 +86,4 @@ ] }, "snyk": true -} +} \ No newline at end of file diff --git a/templates/components/comments_discussion.html b/templates/components/comments_discussion.html index 1c1fe87fa..fcefbe302 100644 --- a/templates/components/comments_discussion.html +++ b/templates/components/comments_discussion.html @@ -31,12 +31,12 @@ {% if handler.user.hasPerm(reply_post_perm) %} {% endif %} -{% if doc.owner == handler.state.user._id or handler.user.hasPerm(reply_edit_perm) %} +{% if doc.owner == handler.user._id or handler.user.hasPerm(reply_edit_perm) %} {% endif %} -{% if doc.owner == handler.state.user._id or handler.user.hasPerm(reply_delete_perm) %} +{% if doc.owner == handler.user._id or handler.user.hasPerm(reply_delete_perm) %} @@ -75,7 +75,7 @@