diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2938e6ce1..e260acf27 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.13.4", + "version": "2.13.5", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index d165dcf88..3cd48a476 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -159,7 +159,7 @@ class HomeSecurityHandler extends Handler { if (udoc) throw new UserAlreadyExistError(email); const [code] = await token.add( token.TYPE_CHANGEMAIL, - await system.get('changemail_token_expire_seconds'), + await system.get('changemail_token_expire_seconds') as number, { uid: this.user._id, email }, ); const m = await this.renderHTML('user_changemail_mail.html', { diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index a18c01560..5cf626250 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -13,10 +13,10 @@ import { } from '../service/server'; import { Tdoc } from '../interface'; -async function _parseDagJson(domainId, dag) { +async function _parseDagJson(domainId: string, _dag: string): Promise { const parsed = []; try { - dag = JSON.parse(dag); + const dag = JSON.parse(_dag); assert(dag instanceof Array, 'dag must be an array'); const ids = new Set(dag.map((s) => s._id)); assert(dag.length === ids.size, '_id must be unique'); @@ -177,16 +177,6 @@ class TrainingCreateHandler extends Handler { const pids = training.getPids(dag); assert(pids.length, new ValidationError('dag')); const pdocs = await problem.getMulti(domainId, { docId: { $in: pids } }).sort('_id', 1).toArray(); - const existPids = pdocs.map((pdoc) => pdoc.docId); - const existPnames = pdocs.map((pdoc) => pdoc.pid); - if (pids.length !== existPids.length) { - for (const pid of pids) { - assert( - existPids.includes(pid) || existPnames.includes(pid), - new ProblemNotFoundError(pid), - ); - } - } for (const pdoc of pdocs) { if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); } @@ -242,11 +232,10 @@ class TrainingEditHandler extends Handler { ], }).sort('_id', 1).toArray(); const existPids = pdocs.map((pdoc) => pdoc.docId); - const existPnames = pdocs.map((pdoc) => pdoc.pid); if (pids.length !== existPids.length) { for (const pid in pids) { assert( - existPids.includes(pid) || existPnames.includes(pid), + existPids.includes(parseInt(pid, 10)), new ProblemNotFoundError(pid), ); } diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index bcb6b2fe2..2545975b8 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -62,7 +62,7 @@ class UserRegisterHandler extends Handler { this.limitRate('send_mail', 3600, 30); const t = await token.add( token.TYPE_REGISTRATION, - await system.get('registration_token_expire_seconds'), + await system.get('registration_token_expire_seconds') as number, { mail }, ); if (await system.get('smtp.user')) { @@ -118,7 +118,7 @@ class UserLostPassHandler extends Handler { if (!udoc) throw new UserNotFoundError(mail); const [tid] = await token.add( token.TYPE_LOSTPASS, - await system.get('lostpass_token_expire_seconds'), + await system.get('lostpass_token_expire_seconds') as number, { uid: udoc._id }, ); const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname }); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 64af21769..c34110df1 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -3,6 +3,15 @@ import { ObjectID, GridFSBucket } from 'mongodb'; import fs from 'fs'; import { Dictionary, NumericDictionary } from 'lodash'; +export type NumberKeys = { + [K in keyof O]: number extends O[K] ? K : never +}[keyof O] + +export interface System { + _id: string, + value: string | number, +} + export interface Setting { family: string, key: string, @@ -175,7 +184,7 @@ export interface PretestConfig { } export interface ContestInfo { - type: number, + type: 30 | 60, tid: ObjectID, } @@ -293,6 +302,8 @@ export interface Ddoc { ip: string, pin: boolean, highlight: boolean, + updateAt: Date, + nReply: number, } // Discussion reply @@ -379,6 +390,29 @@ export interface JudgeResultBody { export type PathComponent = [string, string, Dictionary?, boolean?]; +export interface Collections { + 'blacklist': Bdoc, + 'contest': Tdoc, + 'domain': DomainDoc, + 'domain.user': any, + 'record': Rdoc, + 'document': any, + 'problem': Pdoc, + 'user': Udoc, + 'check': any, + 'message': Mdoc, + 'token': TokenDoc, + 'file': Ufdoc, + 'status': any, + 'oauth': any, + 'system': System, + 'task': any, + 'opcount': any, + 'fs.chunks': any, + 'fs.files': any, + 'document.status': any, +} + export interface Model { blacklist: typeof import('./model/blacklist'), builtin: typeof import('./model/builtin'), diff --git a/packages/hydrooj/src/lib/mail.ts b/packages/hydrooj/src/lib/mail.ts index 7d5827106..76c9a2677 100644 --- a/packages/hydrooj/src/lib/mail.ts +++ b/packages/hydrooj/src/lib/mail.ts @@ -12,7 +12,7 @@ export async function sendMail(to: string, subject: string, text: string, html: system.get('smtp.user'), system.get('smtp.pass'), system.get('smtp.from'), - ]); + ]) as unknown as [string, number, boolean, string, string, string]; const transporter = nodemailer.createTransport({ host, port, secure, auth: { user, pass }, }); diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index fe255bad7..01b04d144 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,4 +1,4 @@ -import { ObjectID } from 'mongodb'; +import { ObjectID, FilterQuery } from 'mongodb'; import * as user from './user'; import * as problem from './problem'; import * as document from './document'; @@ -13,6 +13,8 @@ import { import * as misc from '../lib/misc'; import ranked from '../lib/rank'; +type Type = 30 | 60; + const acm: ContestRule = { TEXT: 'ACM/ICPC', check: () => { }, @@ -356,7 +358,7 @@ function _getStatusJournal(tsdoc) { 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, + rated = false, data: Partial = {}, type: Type = document.TYPE_CONTEST, ) { if (!this.RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); @@ -371,7 +373,7 @@ export function add( export async function edit( domainId: string, tid: ObjectID, - $set: any, type = document.TYPE_CONTEST, + $set: any, type: Type = document.TYPE_CONTEST, ) { if ($set.rule) { if (!this.RULES[$set.rule]) throw new ValidationError('rule'); @@ -383,7 +385,7 @@ export async function edit( } export async function get( - domainId: string, tid: ObjectID, type = document.TYPE_CONTEST, + domainId: string, tid: ObjectID, type: Type | -1 = document.TYPE_CONTEST, ) { let tdoc: Tdoc; if (type === -1) { @@ -396,7 +398,7 @@ export async function get( export async function updateStatus( domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number, - accept = false, score = 0, type = document.TYPE_CONTEST, + accept = false, score = 0, type: 30 | 60 = document.TYPE_CONTEST, ) { const tdoc = await get(domainId, tid, type); const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', { @@ -427,7 +429,7 @@ export async function getListStatus( export async function attend( domainId: string, tid: ObjectID, uid: number, - type = document.TYPE_CONTEST, + type: Type = document.TYPE_CONTEST, ) { try { await document.cappedIncStatus(domainId, type, tid, uid, 'attend', 1, 0, 1); @@ -491,7 +493,7 @@ export const ContestHandlerMixin = (c) => class extends c { async getScoreboard( domainId: string, tid: ObjectID, - isExport = false, docType = document.TYPE_CONTEST, + isExport = false, docType: 30 | 60 = document.TYPE_CONTEST, ): Promise<[Tdoc, ScoreboardNode[][], Udict]> { const tdoc = await get(domainId, tid, docType); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); @@ -522,16 +524,18 @@ export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: an return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); } -export function count(domainId: string, query: any, type = document.TYPE_CONTEST) { +export function count(domainId: string, query: any, type: Type = document.TYPE_CONTEST) { return document.count(domainId, type, query); } -export function getMulti(domainId: string, query = {}, type = document.TYPE_CONTEST) { - return document.getMulti(domainId, type, query); +export function getMulti( + domainId: string, query: FilterQuery = {}, type?: K, +) { + return document.getMulti(domainId, type || document.TYPE_CONTEST, query); } export async function getAndListStatus( - domainId: string, tid: ObjectID, docType = document.TYPE_CONTEST, + domainId: string, tid: ObjectID, docType: Type | -1 = document.TYPE_CONTEST, ): Promise<[Tdoc, any[]]> { // TODO(iceboy): projection, pagination. const tdoc = await get(domainId, tid, docType); @@ -540,7 +544,7 @@ export async function getAndListStatus( return [tdoc, tsdocs]; } -export async function recalcStatus(domainId: string, tid: ObjectID, type = document.TYPE_CONTEST) { +export async function recalcStatus(domainId: string, tid: ObjectID, type: Type = document.TYPE_CONTEST) { const [tdoc, tsdocs] = await Promise.all([ document.get(domainId, type, tid), document.getMultiStatus(domainId, type, { docId: tid }).toArray(), diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index c9a155280..bcc113e9c 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -1,7 +1,10 @@ import assert from 'assert'; -import { ObjectID } from 'mongodb'; +import { ObjectID, Cursor, FilterQuery } from 'mongodb'; import * as db from '../service/db'; import * as bus from '../service/bus'; +import { + Pdoc, Ddoc, Ufdoc, Drdoc, Tdoc, TrainingDoc, NumberKeys, +} from '../interface'; type DocID = ObjectID | string | number; @@ -19,15 +22,28 @@ export const TYPE_TRAINING = 40; export const TYPE_FILE = 50; export const TYPE_HOMEWORK = 60; -export async function add( +export interface DocumentType { + [TYPE_PROBLEM]: Pdoc, + [TYPE_PROBLEM_SOLUTION]: any, + [TYPE_PROBLEM_LIST]: any, + [TYPE_DISCUSSION_NODE]: any, + [TYPE_DISCUSSION]: Ddoc, + [TYPE_DISCUSSION_REPLY]: Drdoc, + [TYPE_CONTEST]: Tdoc, + [TYPE_TRAINING]: TrainingDoc, + [TYPE_FILE]: Ufdoc, + [TYPE_HOMEWORK]: Tdoc, +} + +export async function add( domainId: string, content: string, owner: number, - docType: number, docId: T, + docType: K, docId: DocumentType[K]['docId'], parentType?: number | null, parentId?: DocID, args?: any, ): Promise -export async function add( +export async function add( domainId: string, content: string, owner: number, - docType: number, docId: null, + docType: K, docId: null, parentType?: number, parentId?: DocID, args?: any, ): Promise @@ -57,11 +73,13 @@ export async function add( return docId || res.insertedId; } -export function get(domainId: string, docType: number, docId: DocID) { +export function get(domainId: string, docType: K, docId: DocumentType[K]['docId']) { return coll.findOne({ domainId, docType, docId }); } -export async function set(domainId: string, docType: number, docId: DocID, $set: any) { +export async function set( + domainId: string, docType: K, docId: DocumentType[K]['docId'], $set: Partial, +): Promise { await bus.parallel('document/set', domainId, docType, docId, $set); const res = await coll.findOneAndUpdate( { domainId, docType, docId }, @@ -71,28 +89,36 @@ export async function set(domainId: string, docType: number, docId: DocID, $set: return res.value; } -export function deleteOne(domainId: string, docType: number, docId: DocID) { +export function deleteOne( + domainId: string, docType: K, docId: DocumentType[K]['docId'], +) { return Promise.all([ collStatus.deleteMany({ domainId, docType, docId }), coll.deleteOne({ domainId, docType, docId }), ]); } -export function deleteMulti(domainId: string, docType: number, args: any = {}) { - return coll.deleteMany({ ...args, domainId, docType }); +export function deleteMulti( + domainId: string, docType: K, query?: FilterQuery, +) { + return coll.deleteMany({ ...query, domainId, docType }); } -export function deleteMultiStatus(domainId: string, docType: number, args: any = {}) { - return collStatus.deleteMany({ ...args, domainId, docType }); +export function deleteMultiStatus( + domainId: string, docType: K, query?: FilterQuery, +) { + return collStatus.deleteMany({ ...query, domainId, docType }); } -export function getMulti(domainId: string, docType: number, args: any = {}) { - return coll.find({ ...args, docType, domainId }); +export function getMulti( + domainId: string, docType: K, query?: FilterQuery, +): Cursor { + return coll.find({ ...query, docType, domainId }); } -export async function inc( - domainId: string, docType: number, docId: DocID, - key: string, value: number, +export async function inc( + domainId: string, docType: K, docId: DocumentType[K]['docId'], + key: NumberKeys, value: number, ) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, @@ -102,9 +128,11 @@ export async function inc( return res.value; } -export async function incAndSet( - domainId: string, docType: number, docId: DocID, - key: string, value: number, args: any, +type Q = NumberKeys + +export async function incAndSet( + domainId: string, docType: K, docId: DocumentType[K]['docId'], + key: NumberKeys, value: number, args: Partial, ) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, @@ -114,13 +142,15 @@ export async function incAndSet( return res.value; } -export function count(domainId: string, docType: number, query: any) { +export function count( + domainId: string, docType: K, query?: FilterQuery, +) { return coll.find({ ...query, docType, domainId }).count(); } -export async function push( - domainId: string, docType: number, docId: DocID, - key: string, content: string, owner: number, args: any = {}, +export async function push( + domainId: string, docType: K, docId: DocumentType[K]['docId'], + key: keyof DocumentType[K], content: string, owner: number, args: DocumentType[K][T][0] = {}, ) { const _id = new ObjectID(); const doc = await coll.findOneAndUpdate( @@ -137,10 +167,10 @@ export async function push( return [doc.value, _id]; } -export async function pull( - domainId: string, docType: number, docId: DocID, +export async function pull( + domainId: string, docType: K, docId: DocumentType[K]['docId'], setKey: string, contents: string[], -) { +): Promise { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, { $pull: { [setKey]: { $in: contents } } }, diff --git a/packages/hydrooj/src/model/domain.ts b/packages/hydrooj/src/model/domain.ts index 6f14b53d9..559b3a023 100644 --- a/packages/hydrooj/src/model/domain.ts +++ b/packages/hydrooj/src/model/domain.ts @@ -1,10 +1,9 @@ import { Dictionary } from 'lodash'; -import { Collection } from 'mongodb'; import { BUILTIN_ROLES, PRIV } from './builtin'; import { DomainDoc } from '../interface'; import * as db from '../service/db'; -const coll: Collection = db.collection('domain'); +const coll = db.collection('domain'); const collUser = db.collection('domain.user'); export const JOIN_METHOD_NONE = 0; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 571000d8d..de51ed9b4 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -6,7 +6,7 @@ import * as file from './file'; import * as document from './document'; import * as domain from './domain'; import { - Pdoc, Pdict, ProblemDataSource, ProblemStatusDoc, + Pdoc, Pdict, ProblemDataSource, ProblemStatusDoc, NumberKeys, } from '../interface'; import { ProblemNotFoundError } from '../error'; import * as testdataConfig from '../lib/testdataConfig'; @@ -63,7 +63,7 @@ export async function get( if (typeof pid !== 'number') { if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10); } - const pdoc = Number.isInteger(pid) + const pdoc = typeof pid === 'number' ? await document.get(domainId, document.TYPE_PROBLEM, pid) : (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0]; if (!pdoc) return null; @@ -85,7 +85,7 @@ export function edit(domainId: string, _id: number, $set: Partial): Promis return document.set(domainId, document.TYPE_PROBLEM, _id, $set); } -export function inc(domainId: string, _id: number, field: string, n: number): Promise { +export function inc(domainId: string, _id: number, field: NumberKeys, n: number): Promise { return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n); } diff --git a/packages/hydrooj/src/model/system.ts b/packages/hydrooj/src/model/system.ts index 14a47af16..3330de20f 100644 --- a/packages/hydrooj/src/model/system.ts +++ b/packages/hydrooj/src/model/system.ts @@ -2,11 +2,11 @@ import Lru from 'lru-cache'; import * as db from '../service/db'; const coll = db.collection('system'); -const cache = new Lru({ +const cache = new Lru({ maxAge: 5000, }); -export async function get(_id: string) { +export async function get(_id: string): Promise { const res = cache.get(_id); if (res !== undefined) return res; const doc = await coll.findOne({ _id }); @@ -51,11 +51,13 @@ export async function set(_id: string, value: any) { export async function inc(_id: string) { const res = await coll.findOneAndUpdate( { _id }, + // FIXME NumberKeys<> + // @ts-ignore { $inc: { value: 1 } }, { upsert: true, returnOriginal: false }, ); cache.set(_id, res.value.value); - return res.value.value; + return res.value.value as number; } global.Hydro.model.system = { diff --git a/packages/hydrooj/src/model/training.ts b/packages/hydrooj/src/model/training.ts index c01244576..0462f82d8 100644 --- a/packages/hydrooj/src/model/training.ts +++ b/packages/hydrooj/src/model/training.ts @@ -27,7 +27,7 @@ export async function enroll(domainId: string, tid: ObjectID, uid: number) { } catch (e) { throw new TrainingAlreadyEnrollError(tid, uid); } - return await document.inc(domainId, document.TYPE_TRAINING, tid, 'enroll', 1); + return await document.inc(domainId, document.TYPE_TRAINING, tid, 'attend', 1); } export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) { @@ -42,7 +42,7 @@ export function add( dag, title, description, - enroll: 0, + attend: 0, }); } diff --git a/packages/hydrooj/src/script/rating.ts b/packages/hydrooj/src/script/rating.ts index d24b238f6..503ee5d83 100644 --- a/packages/hydrooj/src/script/rating.ts +++ b/packages/hydrooj/src/script/rating.ts @@ -128,7 +128,7 @@ async function runInDomain(domainId: string, isSub: boolean, report: Function) { for (const dudoc of dudocs) { deltaudict[dudoc.uid] = dudoc.rpdelta || 0; } - const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray(); + const contests = await contest.getMulti(domainId, { rated: true }).sort('endAt', -1).toArray(); await report({ message: `Found ${contests.length} contests in ${domainId}` }); for (const i in contests) { const tdoc = contests[i]; diff --git a/packages/hydrooj/src/service/db.ts b/packages/hydrooj/src/service/db.ts index 4bb0ac593..d763b58a3 100644 --- a/packages/hydrooj/src/service/db.ts +++ b/packages/hydrooj/src/service/db.ts @@ -1,6 +1,7 @@ -import mongodb from 'mongodb'; +import mongodb, { Collection } from 'mongodb'; import * as bus from './bus'; import options from '../options'; +import { Collections } from '../interface'; const opts = options(); @@ -25,7 +26,7 @@ mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopolog }); }); -export function collection(c: string) { +export function collection(c: K): Collection { if (opts.prefix) return db.collection(`${opts.prefix}.${c}`); return db.collection(c); } diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 4b866d513..699034f8f 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -203,7 +203,7 @@ export function requireCsrfToken(target: any, funcName: string, obj: any) { } export async function prepare() { - app.keys = await system.get('session.keys'); + app.keys = await system.get('session.keys') as unknown as string[]; if (argv.public) { app.use(cache(argv.public, { maxAge: 0, @@ -402,7 +402,7 @@ export class Handler { } async init({ domainId }) { - const xff = await system.get('server.xff'); + const xff = await system.get('server.xff') as string; if (xff) this.request.ip = this.request.headers[xff.toLowerCase()]; [this.domain] = await Promise.all([ domain.get(domainId), @@ -498,8 +498,8 @@ export class Handler { async saveCookie() { const expireSeconds = this.session.save - ? await system.get('session.expire_seconds') - : await system.get('session.unsaved_expire_seconds'); + ? await system.get('session.expire_seconds') as number + : await system.get('session.unsaved_expire_seconds') as number; if (this.session._id) { await token.update( this.session._id, @@ -525,7 +525,7 @@ export class Handler { ); } const cookie: SetOption = { - secure: await system.get('session.secure'), + secure: !!await system.get('session.secure'), httpOnly: false, }; if (this.session.save) { diff --git a/packages/migrate-vijos/package.json b/packages/migrate-vijos/package.json index c5111fc12..f6237bba7 100644 --- a/packages/migrate-vijos/package.json +++ b/packages/migrate-vijos/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/migrate-vijos", - "version": "1.3.7", + "version": "1.3.8", "main": "package.json", "repository": "git@github.com:hydro-dev/Hydro.git", "author": "undefined ", @@ -14,7 +14,7 @@ "@types/mongodb": "^3.5.27" }, "peerDependencies": { - "hydrooj": "^2.13.0" + "hydrooj": "^2.13.5" }, "dependencies": { "js-yaml": "^3.14.0", diff --git a/packages/migrate-vijos/script.ts b/packages/migrate-vijos/script.ts index 7ce962f24..a17faa95d 100644 --- a/packages/migrate-vijos/script.ts +++ b/packages/migrate-vijos/script.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import { - Rdoc, TestCase, Mdoc, Drrdoc, + Rdoc, TestCase, Mdoc, Drrdoc, TrainingNode, } from 'hydrooj'; import fs from 'fs'; import os from 'os'; @@ -126,12 +126,12 @@ const tasks = { }, attend: 'attend', desc: 'description', - enroll: 'enroll', + enroll: 'attend', rated: 'rated', dag: { field: 'dag', processer: (dag) => { - const r = []; + const r: TrainingNode[] = []; for (const t of dag) { r.push({ _id: t._id,