From 57881792f3a6675bf84eb36680999e8a5fdd1e94 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 24 Jul 2020 17:22:59 +0800 Subject: [PATCH] update interface: ProblemConfig --- .eslintrc.js | 1 + hydro/handler/contest.ts | 12 ++---- hydro/handler/homework.ts | 7 ++-- hydro/handler/judge.ts | 43 ++----------------- hydro/handler/manage.ts | 13 ++---- hydro/handler/problem.ts | 37 ++++++++++------- hydro/handler/record.ts | 13 +++--- hydro/interface.ts | 83 +++++++++++++++++++++++++++++++++---- hydro/lib/readConfig.ts | 21 ---------- hydro/lib/testdataConfig.ts | 60 +++++++++++++++++++++++++++ hydro/loader.ts | 7 +++- hydro/model/contest.ts | 20 +++------ hydro/model/problem.ts | 6 +-- hydro/model/record.ts | 46 ++++++++++++++------ package.json | 2 +- 15 files changed, 226 insertions(+), 145 deletions(-) delete mode 100644 hydro/lib/readConfig.ts create mode 100644 hydro/lib/testdataConfig.ts diff --git a/.eslintrc.js b/.eslintrc.js index 59dfb06e8..a8fc2a8e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,7 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], rules: { + camelcase: 'off', '@typescript-eslint/no-unused-vars': 'error', 'global-require': 'off', 'guard-for-in': 'off', diff --git a/hydro/handler/contest.ts b/hydro/handler/contest.ts index 6771bafa8..c47ac5cac 100644 --- a/hydro/handler/contest.ts +++ b/hydro/handler/contest.ts @@ -304,14 +304,10 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { @param('code', Types.String) async post(domainId: string, tid: ObjectID, lang: string, code: string) { await this.limitRate('add_record', 60, 100); - const rid = await record.add(domainId, { - pid: this.pdoc.docId, - uid: this.user._id, - tid: this.tdoc.docId, - ttype: document.TYPE_CONTEST, - lang, - code, - }, true); + const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, { + type: document.TYPE_CONTEST, + tid, + }); const [rdoc] = await Promise.all([ record.get(domainId, rid), contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId), diff --git a/hydro/handler/homework.ts b/hydro/handler/homework.ts index 14d6549ad..41c38d2b7 100644 --- a/hydro/handler/homework.ts +++ b/hydro/handler/homework.ts @@ -181,9 +181,10 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid); - const rid = await record.add(domainId, { - pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK, - }, true); + const rid = await record.add(domainId, pid, this.user._id, lang, code, true, { + type: document.TYPE_HOMEWORK, + tid, + }); const [rdoc] = await Promise.all([ record.get(domainId, rid), contest.updateStatus(domainId, tid, this.user._id, diff --git a/hydro/handler/judge.ts b/hydro/handler/judge.ts index a9c86f655..d065e9b2c 100644 --- a/hydro/handler/judge.ts +++ b/hydro/handler/judge.ts @@ -16,11 +16,11 @@ import { async function _postJudge(rdoc: Rdoc) { const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED; const tasks = []; - if (rdoc.tid) { + if (rdoc.contest) { tasks.push( contest.updateStatus( - rdoc.domainId, rdoc.tid, rdoc.uid, - rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.ttype, + rdoc.domainId, rdoc.contest.tid, rdoc.uid, + rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.contest.type, ), ); } @@ -93,42 +93,6 @@ export async function end(body: JudgeResultBody) { bus.publish('record_change', { rdoc }); // trigger a full update } -class PretestHandler extends Handler { - @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) { - const config: any = yaml.safeLoad(pdoc.config); - if (config.time) time = config.time; - if (config.memory) memory = config.memory; - } - } - const rid = await record.add(domainId, { - pid, - uid: this.user._id, - type: 'run', - time: parseTimeMS(time), - memory: parseMemoryMB(memory), - input, - lang, - code, - hidden: true, - }, true); - this.response.body = { rid }; - this.response.redirect = this.url('record_detail', { rid }); - } -} - class JudgeHandler extends Handler { async get({ check = false }) { if (check) return; @@ -174,7 +138,6 @@ class JudgeConnectionHandler extends ConnectionHandler { } export async function apply() { - Route('pretest', '/pretest', PretestHandler); Route('judge', '/judge', JudgeHandler, builtin.PRIV.PRIV_JUDGE); Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE); } diff --git a/hydro/handler/manage.ts b/hydro/handler/manage.ts index 031ba3847..dc9c806fb 100644 --- a/hydro/handler/manage.ts +++ b/hydro/handler/manage.ts @@ -89,18 +89,11 @@ class SystemScriptHandler extends SystemHandler { @param('id', Types.String) @param('args', Types.String, true) - async post(domainId: string, id: string, args = '{}') { + async post(domainId: string, id: string, raw = '{}') { if (!global.Hydro.script[id]) throw new ValidationError('id'); - args = JSON.parse(args); + const args = JSON.parse(raw); validate(global.Hydro.script[id].validate, args); - const rid = await record.add(domainId, { - pid: -1, - uid: this.user._id, - lang: '-', - code: id, - status: STATUS.STATUS_JUDGING, - hidden: true, - }, false); + const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: 'raw' }); judge.next({ domainId, rid, message: `Running script: ${id}` }); async function report(data) { judge.next({ domainId, rid, ...data }); diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index b895fb09e..be92992aa 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -24,7 +24,9 @@ import { ValidationError, PermissionError, } from '../error'; -import { Pdoc, User, Rdoc } from '../interface'; +import { + Pdoc, User, Rdoc, ProblemConfig, +} from '../interface'; const parseCategory = (value: string) => { if (!value) return []; @@ -179,12 +181,9 @@ class ProblemDetailHandler extends ProblemHandler { if (this.pdoc.hidden && this.pdoc.owner !== this.user._id) { this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); } - const config: any = this.pdoc.config ? yaml.safeLoad(this.pdoc.config) : {}; this.udoc = await user.getById(domainId, this.pdoc.owner); this.response.body = { pdoc: this.pdoc, - time: config.time, - memory: config.memory, udoc: this.udoc, title: this.pdoc.title, path: [ @@ -255,14 +254,12 @@ class ProblemSubmitHandler extends ProblemDetailHandler { @param('lang', Types.String) @param('code', Types.String) async post(domainId: string, lang: string, code: string) { - const rid = await record.add(domainId, { - uid: this.user._id, lang, code, pid: this.pdoc.docId, - }, true); + const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true); const [rdoc] = await Promise.all([ record.get(domainId, rid), domain.incUserInDomain(domainId, this.user._id, 'nSubmit'), ]); - bus.publish('record_change', rdoc); + bus.publish('record_change', { rdoc }); this.response.body = { rid }; this.response.redirect = this.url('record_detail', { rid }); } @@ -274,11 +271,18 @@ class ProblemPretestHandler extends ProblemDetailHandler { @param('input', Types.String, true) async post(domainId: string, lang: string, code: string, input: string = '') { this.limitRate('add_record', 3600, 100); - const rid = await record.add(domainId, { - uid: this.user._id, lang, code, pid: this.pdoc.docId, input, - }, true); + // TODO parseConfig + const rid = await record.add( + domainId, this.pdoc.docId, this.user._id, + lang, code, true, + { + input, + time: 1000, + memory: 256, + }, + ); const rdoc = await record.get(domainId, rid); - bus.publish('record_change', rdoc); + bus.publish('record_change', { rdoc }); this.response.body = { rid }; } } @@ -311,7 +315,8 @@ class ProblemPretestConnectionHandler extends ConnectionHandler { rdoc.testCases = rdoc.testCases.map((c) => ({ status: c.status, })); - if (rdoc.tid) return; + // TODO handle update + if (rdoc.contest) return; this.send({ rdoc }); } @@ -354,9 +359,11 @@ class ProblemSettingsHandler extends ProblemManageHandler { @param('pid', Types.String, null, parsePid) @param('yaml', Types.String) - async postConfig(domainId: string, pid: string | number, yaml: string) { + async postConfig(domainId: string, pid: string | number, cfg: string) { const pdoc = await problem.get(domainId, pid); - await problem.edit(domainId, pdoc.docId, { config: yaml }); + // TODO validate + const config = yaml.safeLoad(cfg); + await problem.edit(domainId, pdoc.docId, { config }); this.back(); } diff --git a/hydro/handler/record.ts b/hydro/handler/record.ts index f7b8e32e4..c42ac9ada 100644 --- a/hydro/handler/record.ts +++ b/hydro/handler/record.ts @@ -63,14 +63,13 @@ class RecordDetailHandler extends RecordHandler { async get(domainId: string, rid: ObjectID) { this.response.template = 'record_detail.html'; const rdoc = await record.get(domainId, rid); - if (rdoc.tid) { - const tdoc = await contest.get(domainId, rdoc.tid, rdoc.ttype); + if (rdoc.contest) { + const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type); if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); } if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) { rdoc.code = null; } - if (rdoc.type !== 'run') rdoc.stdout = rdoc.stderr = ''; const [pdoc, udoc] = await Promise.all([ problem.get(domainId, rdoc.pid, null), user.getById(domainId, rdoc.uid), @@ -83,8 +82,6 @@ class RecordDetailHandler extends RecordHandler { udoc, rdoc, pdoc, - // TODO protect - showStatus: true, }; } } @@ -134,7 +131,7 @@ class RecordMainConnectionHandler extends RecordConnectionHandler { async onRecordChange(data) { const rdoc: Rdoc = data.value.rdoc; - if (rdoc.tid && rdoc.tid.toString() !== this.tid) return; + if (rdoc.contest && rdoc.contest.tid.toString() !== this.tid) return; // eslint-disable-next-line prefer-const let [udoc, pdoc] = await Promise.all([ user.getById(this.domainId, rdoc.uid), @@ -149,8 +146,8 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti @param('rid', Types.ObjectID) async prepare(domainId: string, rid: ObjectID) { const rdoc = await record.get(domainId, rid); - if (rdoc.tid) { - const tdoc = await contest.get(domainId, rdoc.tid, -1); + if (rdoc.contest) { + const tdoc = await contest.get(domainId, rdoc.contest.tid, -1); if (!this.canShowRecord(tdoc)) { this.close(); return; diff --git a/hydro/interface.ts b/hydro/interface.ts index 18356183e..92126b27d 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -62,6 +62,66 @@ export interface ProblemDataSource { // ObjectID for built-in files, ProblemDataSource for other source (RemoteJudge, etc.) export type ProblemData = ObjectID | ProblemDataSource; +export interface TestCaseConfig { + input: string, + output: string, + time?: number, + memory?: number, +} + +export enum LocalProblemType { + Default = 'default', + SubmitAnswer = 'submit_answer', + Interactive = 'interactive', +} + +export enum RemoteProblemType { + RemoteJudge = 'remotejudge', +} + +export enum RemoteServerType { + vj4 = 'vj4', + syzoj = 'syzoj', +} + +export enum SubtaskType { + min = 'min', + max = 'max', + sum = 'sum', +} + +export interface SubtaskConfig { + time?: number, + memory?: number, + score?: number, + type?: SubtaskType, + cases?: TestCaseConfig[], +} + +export interface LocalProblemConfig { + type?: LocalProblemType, + score?: number, + time?: number, + memory?: number, + checker_type?: string, + checker?: string, + interactor?: string, + user_extra_files?: string[], + judge_extra_files?: string[], + cases?: TestCaseConfig[], + subtasks?: SubtaskConfig[], +} + +export interface RemoteProblemConfig { + type?: RemoteProblemType, + server_type?: RemoteServerType, + url?: string, + // TODO handle username&password storage + pid?: string, +} + +export type ProblemConfig = LocalProblemConfig | RemoteProblemConfig; + export interface Pdoc { _id: ObjectID, domainId: string, @@ -77,7 +137,7 @@ export interface Pdoc { category: string[], data?: ProblemData, hidden: boolean, - config: string, + config: ProblemConfig, acMsg?: string, } @@ -90,6 +150,17 @@ export interface TestCase { message: string, } +export interface PretestConfig { + time?: number, + memory?: number, + input: string, +} + +export interface ContestInfo { + type: number, + tid: ObjectID, +} + export interface Rdoc { _id: ObjectID, domainId: string, @@ -107,13 +178,9 @@ export interface Rdoc { judger: string, judgeAt: Date, status: number, - type: string, hidden: boolean, - input?: string, - stdout?: string, - stderr?: string, - tid?: ObjectID, - ttype?: number, + config?: PretestConfig, + contest?: ContestInfo, } export interface ScoreboardNode { @@ -279,7 +346,7 @@ declare global { paginate: typeof import('./lib/paginate').default, rank: typeof import('./lib/rank').default, rating: typeof import('./lib/rating').default, - readConfig: typeof import('./lib/readConfig').default, + testdataConfig: typeof import('./lib/testdataConfig'), sha1: typeof import('./lib/sha1').default, sysinfo: typeof import('./lib/sysinfo'), template: typeof import('./lib/template'), diff --git a/hydro/lib/readConfig.ts b/hydro/lib/readConfig.ts deleted file mode 100644 index 654b1959a..000000000 --- a/hydro/lib/readConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Zip from 'adm-zip'; - -async function readConfig(filePath: string) { - const data = new Zip(filePath); - const entries = data.getEntries(); - for (const entry of entries) { - if (entry.name.toLowerCase() === 'config.yaml') { - return entry.getData().toString(); - } - if (entry.name.toLowerCase() === 'config.ini') { - const ini = entry.getData().toString(); - const conv = global.Hydro.lib['testdata.convert.ini']; - return conv(ini); - } - } - return ''; -} - -global.Hydro.lib.readConfig = readConfig; - -export default readConfig; diff --git a/hydro/lib/testdataConfig.ts b/hydro/lib/testdataConfig.ts new file mode 100644 index 000000000..5ab43945b --- /dev/null +++ b/hydro/lib/testdataConfig.ts @@ -0,0 +1,60 @@ +import Zip from 'adm-zip'; +import { safeLoad } from 'js-yaml'; +import { LocalProblemConfig } from '../interface'; + +export async function readConfig(filePath: string) { + const data = new Zip(filePath); + const entries = data.getEntries(); + for (const entry of entries) { + if (entry.name.toLowerCase() === 'config.yaml') { + return entry.getData().toString(); + } + if (entry.name.toLowerCase() === 'config.ini') { + const ini = entry.getData().toString(); + const conv = global.Hydro.lib['testdata.convert.ini']; + return conv(ini); + } + } + return ''; +} + +interface ParseResult { + count: number, + memoryMax: number, + memoryMin: number, + timeMax: number, + timeMin: number, +} + +export async function parseConfig(config: string | LocalProblemConfig) { + const result: ParseResult = { + count: 0, + memoryMin: Number.MAX_SAFE_INTEGER, + memoryMax: 0, + timeMin: Number.MAX_SAFE_INTEGER, + timeMax: 0, + }; + let cfg: LocalProblemConfig = {}; + if (typeof config === 'string') { + // TODO should validate here? + cfg = safeLoad(config) as LocalProblemConfig; + } else cfg = config; + if (cfg.cases) { + for (const c of cfg.cases) { + result.memoryMax = Math.max(result.memoryMax, c.memory); + result.memoryMin = Math.min(result.memoryMin, c.memory); + result.timeMax = Math.max(result.timeMax, c.time); + result.timeMin = Math.min(result.timeMin, c.time); + result.count++; + } + } else if (cfg.subtasks) { + for (const subtask of cfg.subtasks) { + result.memoryMax = Math.max(result.memoryMax, subtask.memory); + result.memoryMin = Math.min(result.memoryMin, subtask.memory); + result.timeMax = Math.max(result.timeMax, subtask.time); + result.timeMin = Math.min(result.timeMin, subtask.time); + } + } +} + +global.Hydro.lib.testdataConfig = { readConfig, parseConfig }; diff --git a/hydro/loader.ts b/hydro/loader.ts index 2c884cf0e..425985c8f 100644 --- a/hydro/loader.ts +++ b/hydro/loader.ts @@ -10,6 +10,8 @@ import fs from 'fs-extra'; import { argv } from 'yargs'; import AdmZip from 'adm-zip'; +export * from './interface'; + global.Hydro = { stat: { reqCount: 0 }, handler: {}, @@ -65,7 +67,10 @@ async function entry(config: EntryConfig) { const sargv = [`--entry=${config.entry}`]; const p = await fork(sargv); await new Promise((resolve, reject) => { - p.on('exit', resolve); + p.on('exit', (code, signal) => { + if (code === 0) resolve(); + else reject(signal); + }); p.on('error', (err: Error) => { p.kill(); reject(err); diff --git a/hydro/model/contest.ts b/hydro/model/contest.ts index 756364a00..fe255bad7 100644 --- a/hydro/model/contest.ts +++ b/hydro/model/contest.ts @@ -1,17 +1,15 @@ import { ObjectID } from 'mongodb'; import * as user from './user'; import * as problem from './problem'; +import * as document from './document'; +import { PERM } from './builtin'; import { ValidationError, ContestNotFoundError, ContestAlreadyAttendedError, ContestNotAttendedError, ContestScoreboardHiddenError, } from '../error'; -import * as document from './document'; -import { PERM } from './builtin'; import { - Tdoc, ContestRule, ContestRules, - Udict, ScoreboardNode, + ContestRule, ContestRules, ScoreboardNode, Tdoc, Udict, } from '../interface'; -import * as validator from '../lib/validator'; import * as misc from '../lib/misc'; import ranked from '../lib/rank'; @@ -21,7 +19,7 @@ const acm: ContestRule = { statusSort: { accept: -1, time: 1 }, showScoreboard: () => true, showRecord: (tdoc, now) => now > tdoc.endAt, - stat: (tdoc, journal: any[]) => { + stat: (tdoc, journal) => { const naccept = {}; const effective = {}; const detail = []; @@ -156,12 +154,8 @@ const oi: ContestRule = { } return { score, detail }; }, - showScoreboard(tdoc, now) { - return now > tdoc.endAt; - }, - showRecord(tdoc, now) { - return now > tdoc.endAt; - }, + showScoreboard: (tdoc, now) => now > tdoc.endAt, + showRecord: (tdoc, now) => now > tdoc.endAt, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, @@ -364,8 +358,6 @@ export function add( 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'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); Object.assign(data, { diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index ec2c65e7b..bdf87a963 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -5,7 +5,7 @@ import * as document from './document'; import * as domain from './domain'; import { Pdoc, Pdict, ProblemDataSource } from '../interface'; import { ProblemNotFoundError } from '../error'; -import readConfig from '../lib/readConfig'; +import * as testdataConfig from '../lib/testdataConfig'; export const pdocHidden: Pdoc = { _id: new ObjectID(), @@ -22,7 +22,7 @@ export const pdocHidden: Pdoc = { category: [], data: null, hidden: true, - config: '', + config: {}, acMsg: '', }; @@ -151,7 +151,7 @@ export function setStar(domainId: string, pid: number, uid: number, star: boolea export async function setTestdata(domainId: string, _id: number, filePath: string) { const pdoc = await get(domainId, _id); - const config = await readConfig(filePath); + const config = await testdataConfig.readConfig(filePath); const id = await file.add(filePath, 'data.zip', 1); if (pdoc.data instanceof ObjectID) file.del(pdoc.data); return await edit(domainId, _id, { data: id, config }); diff --git a/hydro/model/record.ts b/hydro/model/record.ts index 1c4b5fcec..6fa770c04 100644 --- a/hydro/model/record.ts +++ b/hydro/model/record.ts @@ -4,7 +4,9 @@ import { ObjectID } from 'mongodb'; import { STATUS } from './builtin'; import * as task from './task'; import * as problem from './problem'; -import { Rdoc, TestCase } from '../interface'; +import { + Rdoc, TestCase, PretestConfig, ContestInfo, +} from '../interface'; import { RecordNotFoundError } from '../error'; import * as db from '../service/db'; @@ -46,28 +48,50 @@ export interface JudgeTask { type?: string, } -export async function add(domainId: string, data: RdocBase, addTask: boolean): Promise { - _.defaults(data, { +export async function add( + domainId: string, pid: number, uid: number, + lang: string, code: string, addTask: boolean, contest?: ContestInfo, +): Promise +export async function add( + domainId: string, pid: number, uid: number, + lang: string, code: string, addTask: boolean, config: PretestConfig, +): Promise +export async function add( + domainId: string, pid: number, uid: number, + lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | PretestConfig, +) { + const data: Rdoc = { status: STATUS.STATUS_WAITING, - type: 'judge', + _id: new ObjectID(), + uid, + code, + lang, + pid, domainId, score: 0, time: 0, memory: 0, - hidden: false, + hidden: !!contestOrConfig, judgeTexts: [], compilerTexts: [], testCases: [], judger: null, judgeAt: null, - }); + rejudged: false, + }; + if ((contestOrConfig as ContestInfo).type) { + // is contest + data.contest = contestOrConfig as ContestInfo; + } else { + data.config = contestOrConfig as PretestConfig; + } const [pdoc, res] = await Promise.all([ - problem.get(domainId, data.pid, null), + problem.get(domainId, pid, null), coll.insertOne(data), ]); if (addTask) { const t: any = { - type: data.type || 'judge', + type: 'judge', rid: res.insertedId, domainId, pid: data.pid, @@ -78,11 +102,7 @@ export async function add(domainId: string, data: RdocBase, addTask: boolean): P t.data = pdoc.data; t.config = pdoc.config; } else { - t.config = yaml.safeDump({ - time: data.time, - memory: data.memory, - input: data.input, - }); + t.config = yaml.safeDump(contestOrConfig); } await task.add(t); } diff --git a/package.json b/package.json index ee96733a3..5a9c737e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.9.13", + "version": "2.10.0", "main": "dist/loader.js", "bin": "bin/hydrooj.js", "repository": "https://github.com/hydro-dev/Hydro.git",