diff --git a/packages/better-search/model.ts b/packages/better-search/model.ts index 350211cb8..5aa802130 100644 --- a/packages/better-search/model.ts +++ b/packages/better-search/model.ts @@ -1,6 +1,4 @@ import nodejieba from 'nodejieba'; -import { Pdoc } from 'hydrooj'; -import * as document from 'hydrooj/dist/model/document'; import * as bus from 'hydrooj/dist/service/bus'; declare module 'hydrooj' { @@ -9,15 +7,11 @@ declare module 'hydrooj' { } } -bus.on('document/add', async (doc) => { - if (doc.type !== document.TYPE_PROBLEM) return; - const pdoc = doc as Pdoc; +const jiebaHook = async (pdoc) => { + if (!pdoc.title) return; const segments = nodejieba.cutForSearch(pdoc.title); pdoc.search = segments.join(' '); -}); +}; -bus.on('document/set', async (domainId, docType, docId, $set) => { - if (docType !== document.TYPE_PROBLEM || !$set.title) return; - const segments = nodejieba.cutForSearch($set.title); - $set.search = segments.join(' '); -}); +bus.on('problem/before-add', jiebaHook); +bus.on('problem/before-edit', jiebaHook); diff --git a/packages/better-search/package.json b/packages/better-search/package.json index 26289f553..b76318c68 100644 --- a/packages/better-search/package.json +++ b/packages/better-search/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/better-search", - "version": "1.0.2", + "version": "1.0.3", "description": "Enhance Problem Search", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", @@ -8,13 +8,5 @@ "license": "AGPL-3.0-only", "dependencies": { "nodejieba": "^2.5.1" - }, - "devDependencies": {}, - "peerDependencies": { - "hydrooj": "^2.13.2" - }, - "scripts": { - "build": "tsc", - "lint": "eslint . --ext ts --fix" } } \ No newline at end of file diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 21814e04c..a9dcd5a0e 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.4.15", + "version": "2.4.16", "os": [ "linux" ], diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index bbc4fc5b5..e06372b56 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -44,10 +44,10 @@ async function postInit() { if (system.get('hydrojudge.disable')) return; const info = await sysinfo.get(); - monitor.updateJudger(info); + monitor.updateJudge(info); setInterval(async () => { const [mid, inf] = await sysinfo.update(); - monitor.updateJudger({ mid, ...inf }); + monitor.updateJudge({ mid, ...inf }); }, 1200000); async function processData(folder: string) { diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts index 857e1972d..fb088dfe0 100644 --- a/packages/hydrooj/src/entry/common.ts +++ b/packages/hydrooj/src/entry/common.ts @@ -206,6 +206,10 @@ export async function service(pending: string[], fail: string[]) { } } } + for (const key in global.Hydro.service) { + const srv = global.Hydro.service[key]; + if (!srv.started && srv.start) await srv.start(); + } await bus.serial('app/load/service'); } diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index a68338965..9c23094a7 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -12,7 +12,7 @@ import { PERM, PRIV } from '../model/builtin'; import * as contest from '../model/contest'; import * as system from '../model/system'; import * as document from '../model/document'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import domain from '../model/domain'; import record from '../model/record'; import user from '../model/user'; diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 641f53b01..60714dc12 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -17,11 +17,11 @@ import { import { gravatar } from '../lib/misc'; import paginate from '../lib/paginate'; import { - Route, Handler, Types, param, + Route, Handler, Types, param, query, post, } from '../service/server'; class DomainRankHandler extends Handler { - @param('page', Types.PositiveInt, true) + @query('page', Types.PositiveInt, true) async get(domainId: string, page = 1) { const [dudocs, upcount, ucount] = await paginate( domain.getMultiUserInDomain(domainId).sort({ rp: -1 }), @@ -129,8 +129,8 @@ class DomainUserHandler extends ManageHandler { }; } - @param('uid', Types.Int) - @param('role', Types.String) + @post('uid', Types.Int) + @post('role', Types.String) async postSetUser(domainId: string, uid: number, role: string) { await domain.setUserRole(domainId, uid, role); this.back(); @@ -228,10 +228,10 @@ class DomainJoinApplicationsHandler extends ManageHandler { this.response.template = 'domain_join_applications.html'; } - @param('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE])) - @param('role', Types.String, true) - @param('expire', Types.Int, true) - @param('invitationCode', Types.String, true) + @post('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE])) + @post('role', Types.String, true) + @post('expire', Types.Int, true) + @post('invitationCode', Types.String, true) async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') { const r = await domain.getRoles(this.domain); const roles = r.map((rl) => rl._id); diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index c7fb50c00..6763fd8a3 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -18,7 +18,7 @@ import user from '../model/user'; import * as system from '../model/system'; import * as contest from '../model/contest'; import * as discussion from '../model/discussion'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import record from '../model/record'; import * as document from '../model/document'; import paginate from '../lib/paginate'; diff --git a/packages/hydrooj/src/handler/import.syzoj.ts b/packages/hydrooj/src/handler/import.syzoj.ts index e55122f2b..99742b0a0 100644 --- a/packages/hydrooj/src/handler/import.syzoj.ts +++ b/packages/hydrooj/src/handler/import.syzoj.ts @@ -10,7 +10,7 @@ import yaml from 'js-yaml'; import { ValidationError, RemoteOnlineJudgeError } from '../error'; import { Logger } from '../logger'; import type { ContentNode } from '../interface'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import { PERM } from '../model/builtin'; import { Route, Handler, Types, post, diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 139c21133..503533f30 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -4,7 +4,7 @@ import { sleep } from '../utils'; import { Logger } from '../logger'; import difficultyAlgorithm from '../lib/difficulty'; import record from '../model/record'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import * as builtin from '../model/builtin'; import * as contest from '../model/contest'; import domain from '../model/domain'; @@ -14,7 +14,7 @@ import { Route, Handler, Connection, ConnectionHandler, post, Types, } from '../service/server'; import storage from '../service/storage'; -import { updateJudger } from '../service/monitor'; +import { updateJudge } from '../service/monitor'; const logger = new Logger('judge'); @@ -145,7 +145,7 @@ class JudgeConnectionHandler extends ConnectionHandler { this.processing = null; await this.newTask(); } else if (msg.key === 'status') { - await updateJudger(msg.info); + await updateJudge(msg.info); } } diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index cdddb0cee..6c23807cc 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -3,6 +3,7 @@ import * as yaml from 'js-yaml'; import * as judge from './judge'; import { ValidationError } from '../error'; import * as check from '../check'; +import { Logger } from '../logger'; import * as setting from '../model/setting'; import * as system from '../model/system'; import user from '../model/user'; @@ -17,6 +18,8 @@ import { validate, isEmail, isUname, isPassword, } from '../lib/validator'; +const logger = new Logger('manage'); + function set(key: string, value: any) { if (setting.SYSTEM_SETTINGS_BY_KEY[key]) { const s = setting.SYSTEM_SETTINGS_BY_KEY[key]; @@ -125,11 +128,12 @@ class SystemScriptHandler extends SystemHandler { }) .catch((err: Error) => { const time = new Date().getTime() - start; + logger.error(err); judge.end({ domainId, rid, status: STATUS.STATUS_SYSTEM_ERROR, - message: `${err} \n${err.stack} `, + message: `${err.message} \n${err.stack} `, judger: 1, time, memory: 0, diff --git a/packages/hydrooj/src/handler/misc.ts b/packages/hydrooj/src/handler/misc.ts index 9dc24b734..af8c138b7 100644 --- a/packages/hydrooj/src/handler/misc.ts +++ b/packages/hydrooj/src/handler/misc.ts @@ -32,9 +32,9 @@ class StatusHandler extends Handler { class StatusUpdateHandler extends Handler { async post(args) { this.checkPriv(PRIV.PRIV_JUDGE); - args.type = 'judger'; + args.type = 'judge'; return coll.updateOne( - { mid: args.mid, type: 'judger' }, + { mid: args.mid, type: 'judge' }, { $set: args }, { upsert: true }, ); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 8f926362d..84fd75322 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -12,7 +12,7 @@ import paginate from '../lib/paginate'; import { isTitle, isContent, isPid } from '../lib/validator'; import difficultyAlgorithm from '../lib/difficulty'; import * as system from '../model/system'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import record from '../model/record'; import domain from '../model/domain'; import user from '../model/user'; @@ -62,18 +62,26 @@ export class ProblemMainHandler extends ProblemHandler { ['Hydro', 'homepage'], ['problem_main', null], ]; + const search = global.Hydro.lib.problemSearch; + let sort: number[]; if (q) { - query.$text = { $search: q }; path[1][1] = 'problem_main'; path.push([q, null, null, true]); + if (search) { + const result = await search(domainId, q); + query.docId = { $in: result }; + sort = result; + } else query.$text = { $search: q }; } if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false; await bus.serial('problem/list', query, this); - const [pdocs, ppcount, pcount] = await paginate( + // eslint-disable-next-line prefer-const + let [pdocs, ppcount, pcount] = await paginate( problem.getMulti(domainId, query).sort({ pid: 1, docId: 1 }), page, system.get('pagination.problem'), ); + if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(a.docId) - sort.indexOf(b.docId)); if (q && +q) { const pdoc = await problem.get(domainId, +q, this.user._id, problem.PROJECTION_LIST); if (pdoc) pdocs.unshift(pdoc); @@ -341,9 +349,7 @@ export class ProblemEditHandler extends ProblemManageHandler { }; let pdoc = await problem.get(domainId, pid); $update.difficulty = difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept); - await bus.serial('problem/before-edit', $update); pdoc = await problem.edit(domainId, pdoc.docId, $update); - await bus.emit('problem/edit', pdoc); this.response.redirect = this.url('problem_detail', { pid: newPid || pdoc.docId }); } } diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index de126787f..5465c8d08 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -3,7 +3,7 @@ import { PermissionError, RecordNotFoundError } from '../error'; import { Rdoc } from '../interface'; import { PERM, STATUS, PRIV } from '../model/builtin'; import * as system from '../model/system'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import record from '../model/record'; import * as contest from '../model/contest'; import user from '../model/user'; @@ -49,7 +49,7 @@ class RecordListHandler extends RecordHandler { user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), canViewProblem ? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false) - : Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.Pdoc.create(rdoc.pid, rdoc.pid)])]), + : Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.create(rdoc.pid, rdoc.pid)])]), ]); const path = [ ['Hydro', 'homepage'], @@ -88,11 +88,11 @@ class RecordDetailHandler extends RecordHandler { user.getById(domainId, rdoc.uid), ]); if (!(pdoc && this.user.hasPerm(PERM.PERM_VIEW_PROBLEM))) { - pdoc = problem.Pdoc.create(pdoc?.docId || 0, pdoc?.pid || '*'); + pdoc = problem.create(pdoc?.docId || 0, pdoc?.pid || '*'); } if (!rdoc.contest && pdoc.hidden && pdoc.owner !== this.user._id) { if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) { - pdoc = problem.Pdoc.create(pdoc.docId, pdoc.pid); + pdoc = problem.create(pdoc.docId, pdoc.pid); } } this.response.body = { diff --git a/packages/hydrooj/src/handler/remote.ts b/packages/hydrooj/src/handler/remote.ts index 35252a961..c147b4eae 100644 --- a/packages/hydrooj/src/handler/remote.ts +++ b/packages/hydrooj/src/handler/remote.ts @@ -6,7 +6,7 @@ import { Logger } from '../logger'; import { ProblemAdd } from '../lib/ui'; import { PERM } from '../model/builtin'; import token from '../model/token'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import * as system from '../model/system'; import { Handler, Types, Route, post, @@ -24,9 +24,10 @@ export class ProblemSendHandler extends Handler { @post('target', Types.String) @post('pids', Types.Array) - async postSend(domainId: string, target: any, _pids: (number | string)[]) { + async postSend(domainId: string, _target: string, _pids: (number | string)[]) { this.checkPerm(PERM.PERM_VIEW_PROBLEM); - target = target.split('@'); + const target = _target.split('@'); + if (target[1].endsWith('/')) target[1] = target[1].substr(0, target[1].length - 1); const getHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); const getData = this.user.hasPerm(PERM.PERM_READ_PROBLEM_DATA); const pdocs = await problem.getList(domainId, _pids, true, true, ['docId', 'owner', 'hidden']); diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 231192fc5..443cfa279 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -6,7 +6,7 @@ import { isTitle, isContent, isDescription } from '../lib/validator'; import paginate from '../lib/paginate'; import * as system from '../model/system'; import { PERM, PRIV } from '../model/builtin'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import * as builtin from '../model/builtin'; import * as training from '../model/training'; import user from '../model/user'; diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 8d2028171..d4267e638 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -11,7 +11,7 @@ import user from '../model/user'; import oauth from '../model/oauth'; import token from '../model/token'; import record from '../model/record'; -import * as problem from '../model/problem'; +import problem, { Pdoc } from '../model/problem'; import task from '../model/task'; import * as system from '../model/system'; import { PERM, PRIV } from '../model/builtin'; @@ -183,7 +183,7 @@ class UserDetailHandler extends Handler { 1, 100, ) - : [[], 0, 0] as [problem.Pdoc[], number, number], + : [[], 0, 0] as [Pdoc[], number, number], ]); const pdict = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM) ? await problem.getList( @@ -191,7 +191,7 @@ class UserDetailHandler extends Handler { this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false, ) : Object.fromEntries(rdocs.map( - (rdoc) => [rdoc.pid, problem.Pdoc.create(undefined, rdoc.pid.toString())], + (rdoc) => [rdoc.pid, problem.create(undefined, rdoc.pid.toString())], )); // Remove sensitive data if (!isSelfProfile && sdoc) { diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 7392356c2..763b468ec 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -458,6 +458,13 @@ export interface HydroFileSystem { export type PathComponent = [string, string, Dictionary?, boolean?]; +export interface BaseService { + started: boolean; + error?: Error | string; + start: Function; + stop?: Function; +} + export interface Collections { 'blacklist': Bdoc, 'contest': Tdoc, @@ -483,25 +490,25 @@ export interface Collections { } export interface Model { - blacklist: typeof import('./model/blacklist'), + blacklist: typeof import('./model/blacklist').default, builtin: typeof import('./model/builtin'), contest: typeof import('./model/contest'), discussion: typeof import('./model/discussion'), document: typeof import('./model/document'), - domain: typeof import('./model/domain'), - message: typeof import('./model/message'), + domain: typeof import('./model/domain').default, + message: typeof import('./model/message').default, opcount: typeof import('./model/opcount'), - problem: typeof import('./model/problem'), - record: typeof import('./model/record'), + problem: typeof import('./model/problem').default, + record: typeof import('./model/record').default, setting: typeof import('./model/setting'), - solution: typeof import('./model/solution'), + solution: typeof import('./model/solution').default, system: typeof import('./model/system'), - task: typeof import('./model/task'), + task: typeof import('./model/task').default, oplog: typeof import('./model/oplog'), - token: typeof import('./model/token'), + token: typeof import('./model/token').default, training: typeof import('./model/training'), - user: typeof import('./model/user'), - oauth: typeof import('./model/oauth'), + user: typeof import('./model/user').default, + oauth: typeof import('./model/oauth').default, } export interface Service { diff --git a/packages/hydrooj/src/model/blacklist.ts b/packages/hydrooj/src/model/blacklist.ts index a1e7bf32a..d6bb0cd93 100644 --- a/packages/hydrooj/src/model/blacklist.ts +++ b/packages/hydrooj/src/model/blacklist.ts @@ -32,5 +32,5 @@ async function ensureIndexes() { } bus.once('app/started', ensureIndexes); -export = BlackListModel; +export default BlackListModel; global.Hydro.model.blacklist = BlackListModel; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 185c9d404..85da111a7 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,6 +1,6 @@ import { ObjectID, FilterQuery } from 'mongodb'; import user from './user'; -import * as problem from './problem'; +import problem from './problem'; import * as document from './document'; import { PERM } from './builtin'; import { diff --git a/packages/hydrooj/src/model/discussion.ts b/packages/hydrooj/src/model/discussion.ts index 643232cdd..8ce83d393 100644 --- a/packages/hydrooj/src/model/discussion.ts +++ b/packages/hydrooj/src/model/discussion.ts @@ -1,5 +1,5 @@ import { FilterQuery, ObjectID } from 'mongodb'; -import * as problem from './problem'; +import problem from './problem'; import * as contest from './contest'; import * as training from './training'; import * as document from './document'; diff --git a/packages/hydrooj/src/model/domain.ts b/packages/hydrooj/src/model/domain.ts index cf84581cf..f246dd2df 100644 --- a/packages/hydrooj/src/model/domain.ts +++ b/packages/hydrooj/src/model/domain.ts @@ -235,5 +235,5 @@ class DomainModel { } } -export = DomainModel; +export default DomainModel; global.Hydro.model.domain = DomainModel; diff --git a/packages/hydrooj/src/model/message.ts b/packages/hydrooj/src/model/message.ts index 76df8fc42..8794fede2 100644 --- a/packages/hydrooj/src/model/message.ts +++ b/packages/hydrooj/src/model/message.ts @@ -76,5 +76,5 @@ function ensureIndexes() { } bus.once('app/started', ensureIndexes); -export = MessageModel; +export default MessageModel; global.Hydro.model.message = MessageModel; diff --git a/packages/hydrooj/src/model/oauth.ts b/packages/hydrooj/src/model/oauth.ts index 216f13fef..167dce7e5 100644 --- a/packages/hydrooj/src/model/oauth.ts +++ b/packages/hydrooj/src/model/oauth.ts @@ -25,5 +25,5 @@ class OauthModel { } } -export = OauthModel; +export default OauthModel; global.Hydro.model.oauth = OauthModel; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 1e268c7f3..651024478 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -13,245 +13,231 @@ import storage from '../service/storage'; import * as bus from '../service/bus'; export interface Pdoc extends Document { } -export namespace Pdoc { - export type Field = keyof Pdoc; - export const fields: Field[] = []; - type Getter = (docId?: number, pid?: string) => Partial; - const getters: Getter[] = []; - export function extend(getter: Getter) { +export type Field = keyof Pdoc; +const fields: Field[] = []; +export type Getter = (docId?: number, pid?: string) => Partial; +const getters: Getter[] = []; + +export class ProblemModel { + static PROJECTION_LIST: Field[] = [ + '_id', 'domainId', 'docType', 'docId', 'pid', + 'owner', 'title', 'nSubmit', 'nAccept', 'difficulty', + 'tag', 'hidden', + ]; + + static PROJECTION_PUBLIC: Field[] = [ + ...ProblemModel.PROJECTION_LIST, + 'content', 'html', 'data', 'config', 'acMsg', + 'additional_file', + ]; + + static extend(getter: Getter) { getters.push(getter); fields.push(...Object.keys(getter(0, '0')) as any); } - extend((docId, pid) => ({ - _id: new ObjectID(), - domainId: 'system', - docType: document.TYPE_PROBLEM, - docId: docId || -1, - pid: pid || docId.toString(), - owner: 1, - title: '*', - content: '', - html: false, - nSubmit: 0, - nAccept: 0, - tag: [], - data: [], - additional_file: [], - hidden: true, - config: {}, - acMsg: '', - difficulty: 0, - })); - - export function create(docId?: number, pid?: string) { + static create(docId?: number, pid?: string) { const result = {} as Pdoc; for (const getter of getters) { Object.assign(result, getter(docId, pid)); } return result; } -} -export const PROJECTION_LIST: Pdoc.Field[] = [ - '_id', 'domainId', 'docType', 'docId', 'pid', - 'owner', 'title', 'nSubmit', 'nAccept', 'difficulty', - 'tag', 'hidden', -]; - -export const PROJECTION_PUBLIC: Pdoc.Field[] = [ - ...PROJECTION_LIST, - 'content', 'html', 'data', 'config', 'acMsg', - 'additional_file', -]; - -export async function add( - domainId: string, pid: string = null, title: string, content: Content, owner: number, - tag: string[] = [], hidden = false, -) { - const pidCounter = await domain.inc(domainId, 'pidCounter', 1); - const args: Partial = { - title, tag, hidden, nSubmit: 0, nAccept: 0, - }; - if (pid) args.pid = pid; - return await document.add(domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, args); -} - -export async function get( - domainId: string, pid: string | number, - uid: number = null, projection: Projection = PROJECTION_PUBLIC, -): Promise { - if (typeof pid !== 'number') { - if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10); + static async add( + domainId: string, pid: string = null, title: string, content: Content, owner: number, + tag: string[] = [], hidden = false, + ) { + const pidCounter = await domain.inc(domainId, 'pidCounter', 1); + const args: Partial = { + title, tag, hidden, nSubmit: 0, nAccept: 0, + }; + if (pid) args.pid = pid; + await bus.serial('problem/before-add', args); + const result = await document.add(domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, args); + await bus.emit('problem/add', args, result); + return result; } - const pdoc = typeof pid === 'number' - ? await document.get(domainId, document.TYPE_PROBLEM, pid, projection) - : (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0]; - if (!pdoc) return null; - if (uid) { - pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); + + static async get( + domainId: string, pid: string | number, + uid: number = null, projection: Projection = ProblemModel.PROJECTION_PUBLIC, + ): Promise { + if (typeof pid !== 'number') { + if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10); + } + const pdoc = typeof pid === 'number' + ? await document.get(domainId, document.TYPE_PROBLEM, pid, projection) + : (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0]; + if (!pdoc) return null; + if (uid) { + pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); + } + return pdoc; } - return pdoc; -} -export function getMulti(domainId: string, query: FilterQuery, projection = PROJECTION_LIST) { - return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection); -} - -export function getMultiStatus(domainId: string, query: FilterQuery) { - return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query); -} - -export function edit(domainId: string, _id: number, $set: Partial): Promise { - const delpid = $set.pid === ''; - if (delpid) delete $set.pid; - return document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined); -} - -export function push>(domainId: string, _id: number, key: ArrayKeys, value: Pdoc[T][0]) { - return document.push(domainId, document.TYPE_PROBLEM, _id, key, value); -} - -export function pull>(domainId: string, pid: number, key: ArrayKeys, values: Pdoc[T][0][]) { - return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values); -} - -export function inc(domainId: string, _id: number, field: NumberKeys, n: number): Promise { - 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 function del(domainId: string, docId: number) { - return Promise.all([ - document.deleteOne(domainId, document.TYPE_PROBLEM, docId), - document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }), - storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))), - bus.parallel('problem/delete', domainId, docId), - ]); -} - -export async function addTestdata(domainId: string, pid: number, name: string, f: Readable | Buffer | string) { - if (!name) throw new ValidationError('name'); - const [[, fileinfo]] = await Promise.all([ - document.getSub(domainId, document.TYPE_PROBLEM, pid, 'data', name), - storage.put(`problem/${domainId}/${pid}/testdata/${name}`, f), - ]); - const meta = await storage.getMeta(`problem/${domainId}/${pid}/testdata/${name}`); - if (!meta) throw new Error('Upload failed'); - const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) }; - if (!fileinfo) await push(domainId, pid, 'data', { _id: name, ...payload }); - else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'data', name, payload); -} - -export async function delTestdata(domainId: string, pid: number, name: string | string[]) { - const names = (name instanceof Array) ? name : [name]; - await storage.del(names.map((t) => `problem/${domainId}/${pid}/testdata/${t}`)); - await pull(domainId, pid, 'data', names); -} - -export async function addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string) { - const [[, fileinfo]] = await Promise.all([ - document.getSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name), - storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f), - ]); - const meta = await storage.getMeta(`problem/${domainId}/${pid}/additional_file/${name}`); - const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) }; - if (!fileinfo) await push(domainId, pid, 'additional_file', { _id: name, ...payload }); - else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name, payload); -} - -export async function delAdditionalFile(domainId: string, pid: number, name: string | string[]) { - const names = (name instanceof Array) ? name : [name]; - await storage.del(names.map((t) => `problem/${domainId}/${pid}/additional_file/${t}`)); - await pull(domainId, pid, 'additional_file', names); -} - -export async function random(domainId: string, query: FilterQuery): Promise { - const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query); - const pcount = await cursor.count(); - if (pcount) { - const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray(); - return pdoc[0].pid; - } return null; -} - -export async function getList( - domainId: string, pids: Array, - getHidden = false, doThrow = true, projection = PROJECTION_PUBLIC, -): Promise { - pids = Array.from(new Set(pids)).map((i) => (+i ? +i : i)); - const r = {}; - const l = {}; - const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; - if (!getHidden) q.hidden = false; - const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q) - .project(buildProjection(projection)).toArray(); - for (const pdoc of pdocs) { - r[pdoc.docId] = pdoc; - l[pdoc.pid] = pdoc; + static getMulti(domainId: string, query: FilterQuery, projection = ProblemModel.PROJECTION_LIST) { + return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection); } - // TODO enhance - if (pdocs.length !== pids.length) { - for (const pid of pids) { - if (!(r[pid] || l[pid])) { - if (doThrow) throw new ProblemNotFoundError(domainId, pid); - else r[pid] = Pdoc.create(undefined, pid.toString()); + + static getMultiStatus(domainId: string, query: FilterQuery) { + return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query); + } + + static async edit(domainId: string, _id: number, $set: Partial): Promise { + const delpid = $set.pid === ''; + if (delpid) delete $set.pid; + await bus.serial('problem/before-edit', $set); + const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined); + await bus.emit('problem/edit', result); + return result; + } + + static push>(domainId: string, _id: number, key: ArrayKeys, value: Pdoc[T][0]) { + return document.push(domainId, document.TYPE_PROBLEM, _id, key, value); + } + + static pull>(domainId: string, pid: number, key: ArrayKeys, values: Pdoc[T][0][]) { + return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values); + } + + static inc(domainId: string, _id: number, field: NumberKeys, n: number): Promise { + return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n); + } + + static count(domainId: string, query: FilterQuery) { + return document.count(domainId, document.TYPE_PROBLEM, query); + } + + static async del(domainId: string, docId: number) { + await bus.serial('problem/before-del', domainId, docId); + const res = await Promise.all([ + document.deleteOne(domainId, document.TYPE_PROBLEM, docId), + document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }), + storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))), + bus.parallel('problem/delete', domainId, docId), + ]); + await bus.emit('problem/del', domainId, docId); + return res; + } + + static async addTestdata(domainId: string, pid: number, name: string, f: Readable | Buffer | string) { + if (!name) throw new ValidationError('name'); + const [[, fileinfo]] = await Promise.all([ + document.getSub(domainId, document.TYPE_PROBLEM, pid, 'data', name), + storage.put(`problem/${domainId}/${pid}/testdata/${name}`, f), + ]); + const meta = await storage.getMeta(`problem/${domainId}/${pid}/testdata/${name}`); + if (!meta) throw new Error('Upload failed'); + const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) }; + if (!fileinfo) await ProblemModel.push(domainId, pid, 'data', { _id: name, ...payload }); + else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'data', name, payload); + } + + static async delTestdata(domainId: string, pid: number, name: string | string[]) { + const names = (name instanceof Array) ? name : [name]; + await storage.del(names.map((t) => `problem/${domainId}/${pid}/testdata/${t}`)); + await ProblemModel.pull(domainId, pid, 'data', names); + } + + static async addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string) { + const [[, fileinfo]] = await Promise.all([ + document.getSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name), + storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f), + ]); + const meta = await storage.getMeta(`problem/${domainId}/${pid}/additional_file/${name}`); + const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) }; + if (!fileinfo) await ProblemModel.push(domainId, pid, 'additional_file', { _id: name, ...payload }); + else await document.setSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name, payload); + } + + static async delAdditionalFile(domainId: string, pid: number, name: string | string[]) { + const names = (name instanceof Array) ? name : [name]; + await storage.del(names.map((t) => `problem/${domainId}/${pid}/additional_file/${t}`)); + await ProblemModel.pull(domainId, pid, 'additional_file', names); + } + + static async random(domainId: string, query: FilterQuery): Promise { + 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; + } + + static async getList( + domainId: string, pids: Array, + getHidden = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, + ): Promise { + pids = Array.from(new Set(pids)).map((i) => (+i ? +i : i)); + const r = {}; + const l = {}; + const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }; + if (!getHidden) q.hidden = false; + const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q) + .project(buildProjection(projection)).toArray(); + for (const pdoc of pdocs) { + r[pdoc.docId] = pdoc; + l[pdoc.pid] = pdoc; + } + // TODO enhance + if (pdocs.length !== pids.length) { + for (const pid of pids) { + if (!(r[pid] || l[pid])) { + if (doThrow) throw new ProblemNotFoundError(domainId, pid); + else r[pid] = ProblemModel.create(undefined, pid.toString()); + } } } + return Object.assign(r, l); + } + + static async getListStatus(domainId: string, uid: number, pids: number[]) { + const psdocs = await ProblemModel.getMultiStatus( + domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, + ).toArray(); + const r: Dictionary = {}; + for (const psdoc of psdocs) r[psdoc.docId] = psdoc; + return r; + } + + static async updateStatus( + domainId: string, pid: number, uid: number, + rid: ObjectID, status: number, + ) { + const res = await document.setIfNotStatus( + domainId, document.TYPE_PROBLEM, pid, uid, + 'status', status, STATUS.STATUS_ACCEPTED, { rid }, + ); + return !!res; + } + + static setStar(domainId: string, pid: number, uid: number, star: boolean) { + return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star }); } - return Object.assign(r, l); } -export async function getListStatus(domainId: string, uid: number, pids: number[]) { - const psdocs = await getMultiStatus( - domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, - ).toArray(); - const r: Dictionary = {}; - for (const psdoc of psdocs) r[psdoc.docId] = psdoc; - return r; -} +ProblemModel.extend((docId, pid) => ({ + _id: new ObjectID(), + domainId: 'system', + docType: document.TYPE_PROBLEM, + docId: docId || -1, + pid: pid || docId.toString(), + owner: 1, + title: '*', + content: '', + html: false, + nSubmit: 0, + nAccept: 0, + tag: [], + data: [], + additional_file: [], + hidden: true, + config: {}, + acMsg: '', + difficulty: 0, +})); -export async function updateStatus( - domainId: string, pid: number, uid: number, - rid: ObjectID, status: number, -) { - const res = await document.setIfNotStatus( - domainId, document.TYPE_PROBLEM, pid, uid, - 'status', status, STATUS.STATUS_ACCEPTED, { rid }, - ); - return !!res; -} - -export function setStar(domainId: string, pid: number, uid: number, star: boolean) { - return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star }); -} - -global.Hydro.model.problem = { - Pdoc, - - PROJECTION_LIST, - PROJECTION_PUBLIC, - - add, - inc, - get, - del, - edit, - count, - push, - pull, - random, - addTestdata, - delTestdata, - addAdditionalFile, - delAdditionalFile, - getMulti, - getList, - getListStatus, - getMultiStatus, - setStar, - updateStatus, -}; +global.Hydro.model.problem = ProblemModel; +export default ProblemModel; diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index 35a6d6c0e..81f1aad13 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -5,7 +5,7 @@ import { Dictionary } from 'lodash'; import moment from 'moment'; import { STATUS } from './builtin'; import task from './task'; -import * as problem from './problem'; +import problem from './problem'; import { Rdoc, ContestInfo, ProblemConfig } from '../interface'; import { ArgMethod, Time } from '../utils'; import * as bus from '../service/bus'; @@ -166,5 +166,5 @@ class RecordModel { } bus.on('problem/delete', (domainId, docId) => RecordModel.coll.deleteMany({ domainId, pid: docId })); -export = RecordModel; +export default RecordModel; global.Hydro.model.record = RecordModel; diff --git a/packages/hydrooj/src/model/solution.ts b/packages/hydrooj/src/model/solution.ts index a4fc516c6..c29178c6e 100644 --- a/packages/hydrooj/src/model/solution.ts +++ b/packages/hydrooj/src/model/solution.ts @@ -92,5 +92,5 @@ bus.on('problem/delete', async (domainId, docId) => { ]); }); -export = SolutionModel; +export default SolutionModel; global.Hydro.model.solution = SolutionModel; diff --git a/packages/hydrooj/src/model/task.ts b/packages/hydrooj/src/model/task.ts index eecefeed6..a47402880 100644 --- a/packages/hydrooj/src/model/task.ts +++ b/packages/hydrooj/src/model/task.ts @@ -97,5 +97,5 @@ class TaskModel { }; } -export = TaskModel; +export default TaskModel; global.Hydro.model.task = TaskModel; diff --git a/packages/hydrooj/src/model/token.ts b/packages/hydrooj/src/model/token.ts index 7689b9d61..9de14eea1 100644 --- a/packages/hydrooj/src/model/token.ts +++ b/packages/hydrooj/src/model/token.ts @@ -101,5 +101,5 @@ function ensureIndexes() { } bus.once('app/started', ensureIndexes); -export = TokenModel; +export default TokenModel; global.Hydro.model.token = TokenModel; diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 593200963..407a2cc72 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -271,5 +271,5 @@ function ensureIndexes() { } bus.once('app/started', ensureIndexes); -export = UserModel; +export default UserModel; global.Hydro.model.user = UserModel; diff --git a/packages/hydrooj/src/script/rating.ts b/packages/hydrooj/src/script/rating.ts index 8c3f46c0e..4127b9a7e 100644 --- a/packages/hydrooj/src/script/rating.ts +++ b/packages/hydrooj/src/script/rating.ts @@ -4,7 +4,7 @@ import { ObjectID, FilterQuery } from 'mongodb'; import { Tdoc, Pdoc, Udoc } from '../interface'; import domain from '../model/domain'; import * as contest from '../model/contest'; -import * as problem from '../model/problem'; +import problem from '../model/problem'; import record from '../model/record'; import { STATUS } from '../model/builtin'; import rating from '../lib/rating'; diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index c5f28f3f8..d96d2c2df 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -45,7 +45,7 @@ export interface EventMap extends Record { 'system/setting': (args: Record) => VoidReturn - 'monitor/update': (type: 'server' | 'judger', $set: any) => VoidReturn + 'monitor/update': (type: 'server' | 'judge', $set: any) => VoidReturn 'user/message': (uid: number, mdoc: Mdoc, udoc: User) => void 'user/get': (udoc: User) => void @@ -76,10 +76,13 @@ export interface EventMap extends Record { ip: string | null, highlight: boolean, pin: boolean ) => VoidReturn + 'problem/before-add': (doc: Partial) => VoidReturn + 'problem/add': (doc: Partial, docId: number) => VoidReturn 'problem/before-edit': (doc: Partial) => VoidReturn 'problem/edit': (doc: Pdoc) => VoidReturn + 'problem/before-del': (domainId: string, docId: number) => VoidReturn + 'problem/del': (domainId: string, docId: number) => VoidReturn 'problem/list': (query: FilterQuery, handler: any) => VoidReturn - 'problem/setting': (update: Partial, handler: any) => VoidReturn 'problem/get': (doc: Pdoc, handler: any) => VoidReturn 'problem/delete': (domainId: string, docId: number) => VoidReturn diff --git a/packages/hydrooj/src/service/db.ts b/packages/hydrooj/src/service/db.ts index f8782e715..fb4fd2689 100644 --- a/packages/hydrooj/src/service/db.ts +++ b/packages/hydrooj/src/service/db.ts @@ -1,6 +1,6 @@ import { Collection, Db, MongoClient } from 'mongodb'; import * as bus from './bus'; -import { Collections } from '../interface'; +import { Collections, BaseService } from '../interface'; interface MongoConfig { protocol?: string, @@ -13,11 +13,12 @@ interface MongoConfig { prefix?: string, } -class MongoService { +class MongoService implements BaseService { public client: MongoClient; public client2: MongoClient; public db: Db; public db2: Db; + public started = false; private opts: MongoConfig; static buildUrl(opts: MongoConfig) { @@ -35,7 +36,8 @@ class MongoService { this.db = this.client.db(opts.name); this.client2 = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); this.db2 = this.client2.db(opts.name); - bus.parallel('database/connect', this.db); + await bus.parallel('database/connect', this.db); + this.started = true; } public collection(c: K): Collection { diff --git a/packages/hydrooj/src/service/monitor.ts b/packages/hydrooj/src/service/monitor.ts index f892f210c..9aca31fe5 100644 --- a/packages/hydrooj/src/service/monitor.ts +++ b/packages/hydrooj/src/service/monitor.ts @@ -70,11 +70,11 @@ export async function update() { global.Hydro.stat.reqCount = 0; } -export async function updateJudger(args) { +export async function updateJudge(args) { const $set = { ...args, updateAt: new Date() }; - await bus.serial('monitor/update', 'judger', $set); + await bus.serial('monitor/update', 'judge', $set); return await coll.updateOne( - { mid: args.mid, type: 'judger' }, + { mid: args.mid, type: 'judge' }, { $set }, { upsert: true }, ); @@ -93,4 +93,4 @@ if (cluster.isMaster) { }); } -global.Hydro.service.monitor = { feedback, update, updateJudger }; +global.Hydro.service.monitor = { feedback, update, updateJudge }; diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 9a6e70250..0a7ff8bac 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -210,6 +210,7 @@ type DescriptorBuilder = & ((name: string, ...args: Array) => MethodDecorator); export const get: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args)); +export const query: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'get', ...args)); export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args)); export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args)); export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args)); @@ -319,6 +320,7 @@ export class HandlerCommon { url(name: string, kwargs: any = {}) { let res = '#'; const args: any = {}; + // eslint-disable-next-line @typescript-eslint/no-shadow const query: any = {}; for (const key in kwargs) { if (kwargs[key] instanceof ObjectID) args[key] = kwargs[key].toHexString(); @@ -803,12 +805,16 @@ export function Connection( sock.installHandlers(server); } +let started = false; + // TODO use postInit? export function start() { + if (started) return; const port = system.get('server.port'); app.use(router.routes()).use(router.allowedMethods()); server.listen(argv.port || port); logger.success('Server listening at: %d', argv.port || port); + started = true; } global.Hydro.service.server = { @@ -818,6 +824,7 @@ global.Hydro.service.server = { router, UiContextBase, get, + query, post, route, param, diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 4db735d32..72d55cec3 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -11,7 +11,7 @@ import { streamToBuffer } from './utils'; import gridfs from './service/gridfs'; import storage from './service/storage'; import db from './service/db'; -import * as problem from './model/problem'; +import problem, { Field, Pdoc } from './model/problem'; import domain from './model/domain'; import * as document from './model/document'; import * as system from './model/system'; @@ -25,13 +25,13 @@ export async function iterateAllDomain(cb: (ddoc: DomainDoc, current?: number, t for (const i in ddocs) await cb(ddocs[i], +i, ddocs.length); } -interface PartialPdoc extends problem.Pdoc { +interface PartialPdoc extends Pdoc { [key: string]: any, } export async function iterateAllProblemInDomain( domainId: string, - fields: (problem.Pdoc.Field | string)[], + fields: (Field | string)[], cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise, ) { if (!fields.includes('domainId')) fields.push('domainId'); @@ -48,7 +48,7 @@ export async function iterateAllProblemInDomain( } export async function iterateAllProblem( - fields: (problem.Pdoc.Field | string)[], + fields: (Field | string)[], cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise, ) { await iterateAllDomain(async (d) => { @@ -171,7 +171,7 @@ const scripts: UpgradeScript[] = [ logger.info('%s/%s', pdoc.domainId, pdoc.docId); const [data, additional_file] = await Promise.all([ storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`), - storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_files/`), + storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`), ]); await problem.edit(pdoc.domainId, pdoc.docId, { data, additional_file }); if (!pdoc.config) return; @@ -187,7 +187,7 @@ const scripts: UpgradeScript[] = [ logger.info('%s/%s', pdoc.domainId, pdoc.docId); const [data, additional_file] = await Promise.all([ storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`), - storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_files/`), + storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/additional_file/`), ]) as any; for (let i = 0; i < data.length; i++) { data[i]._id = data[i].name; diff --git a/packages/import-qduoj/handler.ts b/packages/import-qduoj/handler.ts index bbff3094c..2c440e530 100644 --- a/packages/import-qduoj/handler.ts +++ b/packages/import-qduoj/handler.ts @@ -8,7 +8,7 @@ import type { ContentNode, ProblemConfig } from 'hydrooj'; import { Route, Handler } from 'hydrooj/dist/service/server'; import { BadRequestError, ValidationError } from 'hydrooj/dist/error'; import { ProblemAdd } from 'hydrooj/dist/lib/ui'; -import * as problem from 'hydrooj/dist/model/problem'; +import problem from 'hydrooj/dist/model/problem'; import { PERM } from 'hydrooj/dist/model/builtin'; fs.ensureDirSync('/tmp/hydro/import-qduoj'); diff --git a/packages/sonic/model.ts b/packages/sonic/model.ts new file mode 100644 index 000000000..c23455344 --- /dev/null +++ b/packages/sonic/model.ts @@ -0,0 +1,27 @@ +import * as bus from 'hydrooj/dist/service/bus'; +import * as system from 'hydrooj/dist/model/system'; +import sonic from './service'; + +bus.on('problem/add', async (doc, docId) => { + await sonic.push('problem', `${doc.domainId}@title`, docId.toString(), doc.title); + await sonic.push('problem', `${doc.domainId}@content`, docId.toString(), doc.content.toString()); +}); + +bus.on('problem/edit', async (pdoc) => { + await sonic.flusho('problem', `${pdoc.domainId}@title`, pdoc.docId.toString()); + await sonic.flusho('problem', `${pdoc.domainId}@content`, pdoc.docId.toString()); + await sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title); + await sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString()); +}); + +bus.on('problem/del', async (domainId, docId) => { + await sonic.flusho('problem', `${domainId}@title`, docId.toString()); + await sonic.flusho('problem', `${domainId}@content`, docId.toString()); +}); + +global.Hydro.lib.problemSearch = async (domainId: string, query: string) => { + const c = system.get('pagination.problem'); + const docIds = await sonic.query('problem', `${domainId}@title`, query, { limit: c }); + docIds.push(...await sonic.query('problem', `${domainId}@content`, query, { limit: c - docIds.length })); + return docIds.map(Number); +}; diff --git a/packages/sonic/package.json b/packages/sonic/package.json new file mode 100644 index 000000000..593164582 --- /dev/null +++ b/packages/sonic/package.json @@ -0,0 +1,13 @@ +{ + "name": "@hydrooj/sonic", + "version": "1.0.0", + "description": "Sonic search service", + "main": "service.js", + "typings": "service.d.ts", + "repository": "https://github.com/hydro-dev/Hydro.git", + "author": "undefined ", + "license": "AGPL-3.0-only", + "dependencies": { + "sonic-channel": "^1.2.6" + } +} \ No newline at end of file diff --git a/packages/sonic/script.ts b/packages/sonic/script.ts new file mode 100644 index 000000000..0ddd4ebe4 --- /dev/null +++ b/packages/sonic/script.ts @@ -0,0 +1,25 @@ +import { iterateAllProblem, iterateAllProblemInDomain } from 'hydrooj/dist/upgrade'; +import sonic from './service'; + +export const description = 'Sonic problem search re-index'; + +export async function run({ domainId }) { + if (domainId) await sonic.flushb('problem', domainId); + else await sonic.flushc('problem'); + const cb = async (pdoc) => { + await sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title); + await sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString()); + }; + if (domainId) await iterateAllProblemInDomain(domainId, ['title', 'content'], cb); + else await iterateAllProblem(['title', 'content'], cb); + return true; +} + +export const validate = { + $or: [ + { domainId: 'string' }, + { domainId: 'undefined' }, + ], +}; + +global.Hydro.script.ensureSonicSearch = { run, description, validate }; diff --git a/packages/sonic/service.ts b/packages/sonic/service.ts new file mode 100644 index 000000000..bce3d6a10 --- /dev/null +++ b/packages/sonic/service.ts @@ -0,0 +1,117 @@ +import { BaseService } from 'hydrooj'; +import * as system from 'hydrooj/dist/model/system'; +import { + Search, Ingest, QueryOptions, SuggestOptions, PushOptions, +} from 'sonic-channel'; +import { Logger } from 'hydrooj/dist/logger'; + +const logger = new Logger('sonic'); + +declare module 'hydrooj/dist/interface' { + interface SystemKeys { + 'sonic.host': string; + 'sonic.port': number; + 'sonic.auth': string; + } +} + +function getHandler(type: string, that: any) { + return { + connected() { + that.started = true; + logger.info(`Sonic Channel succeeded to connect to host (${type}).`); + }, + disconnected() { + that.started = false; + logger.error(`Sonic Channel is now disconnected (${type}).`); + }, + timeout() { + logger.error(`Sonic Channel connection timed out (${type}).`); + }, + retrying() { + logger.error(`Trying to reconnect to Sonic Channel (${type})...`); + }, + error(error) { + that.error = error; + logger.error(`Sonic Channel failed to connect to host (${type}).`, error); + }, + }; +} + +class SonicService implements BaseService { + public search: Search; + public ingest: Ingest; + public started = false; + public error = ''; + + async start() { + const [host, port, auth] = system.getMany(['sonic.host', 'sonic.port', 'sonic.auth']); + const cfg = { + host: host || '127.0.0.1', + port: port || 1491, + auth: auth || 'sonic-passwd', + }; + this.search = new Search(cfg); + this.ingest = new Ingest(cfg); + await this.connect(); + } + + async connect() { + try { + this.search.connect(getHandler('search', this)); + this.ingest.connect(getHandler('ingest', this)); + } catch (e) { + logger.warn('Sonic init fail. will retry later.'); + this.error = e.toString(); + setTimeout(() => this.start(), 10000); + } + } + + async query(collection: string, bucket: string, terms: string, options?: QueryOptions) { + return await this.search.query(collection, bucket, terms, options); + } + + async suggest(collection: string, bucket: string, word: string, options?: SuggestOptions) { + return await this.search.suggest(collection, bucket, word, options); + } + + async push(collection: string, bucket: string, object: string, text: string, options?: PushOptions) { + return await this.ingest.push(collection, bucket, object, text, options); + } + + async pop(collection: string, bucket: string, object: string, text: string) { + return await this.ingest.pop(collection, bucket, object, text); + } + + async count(collection: string, bucket?: string, object?: string) { + return await this.ingest.count(collection, bucket, object); + } + + async flusho(collection: string, bucket: string, object: string) { + return await this.ingest.flusho(collection, bucket, object); + } + + async flushb(collection: string, bucket: string) { + return await this.ingest.flushb(collection, bucket); + } + + async flushc(collection: string) { + return await this.ingest.flushc(collection); + } + + async stop() { + await Promise.all([ + this.search.close(), + this.ingest.close(), + ]); + } +} + +const service = new SonicService(); +global.Hydro.service.sonic = service; +export = service; +declare module 'hydrooj/dist/interface' { + interface Service { + sonic: typeof service + } +} diff --git a/packages/sonic/setting.yaml b/packages/sonic/setting.yaml new file mode 100644 index 000000000..d9ae6043e --- /dev/null +++ b/packages/sonic/setting.yaml @@ -0,0 +1,7 @@ +host: + value: localhost +port: + type: number + value: 1491 +auth: + type: text \ No newline at end of file diff --git a/packages/syzoj-ng-api/handlers/problem.ts b/packages/syzoj-ng-api/handlers/problem.ts index 566739f04..df20ed381 100644 --- a/packages/syzoj-ng-api/handlers/problem.ts +++ b/packages/syzoj-ng-api/handlers/problem.ts @@ -1,6 +1,6 @@ import { FilterQuery } from 'mongodb'; import { Pdoc } from 'hydrooj/dist/interface'; -import * as problem from 'hydrooj/dist/model/problem'; +import problem from 'hydrooj/dist/model/problem'; import record from 'hydrooj/dist/model/record'; import user from 'hydrooj/dist/model/user'; import { PERM, PRIV } from 'hydrooj/dist/model/builtin';