mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
feat: search with sonic
This commit is contained in:
1 parent
5d01c81607
commit
43c4bfd8e7
43 files changed
+512
-317
No files matched your search
@@ -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);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "2.4.15",
|
||||
"version": "2.4.16",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -458,6 +458,13 @@ export interface HydroFileSystem {
|
||||
|
||||
export type PathComponent = [string, string, Dictionary<any>?, 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 {
|
||||
|
||||
@@ -32,5 +32,5 @@ async function ensureIndexes() {
|
||||
}
|
||||
|
||||
bus.once('app/started', ensureIndexes);
|
||||
export = BlackListModel;
|
||||
export default BlackListModel;
|
||||
global.Hydro.model.blacklist = BlackListModel;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -235,5 +235,5 @@ class DomainModel {
|
||||
}
|
||||
}
|
||||
|
||||
export = DomainModel;
|
||||
export default DomainModel;
|
||||
global.Hydro.model.domain = DomainModel;
|
||||
@@ -76,5 +76,5 @@ function ensureIndexes() {
|
||||
}
|
||||
|
||||
bus.once('app/started', ensureIndexes);
|
||||
export = MessageModel;
|
||||
export default MessageModel;
|
||||
global.Hydro.model.message = MessageModel;
|
||||
@@ -25,5 +25,5 @@ class OauthModel {
|
||||
}
|
||||
}
|
||||
|
||||
export = OauthModel;
|
||||
export default OauthModel;
|
||||
global.Hydro.model.oauth = OauthModel;
|
||||
@@ -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<Pdoc>;
|
||||
const getters: Getter[] = [];
|
||||
export function extend(getter: Getter) {
|
||||
export type Field = keyof Pdoc;
|
||||
const fields: Field[] = [];
|
||||
export type Getter = (docId?: number, pid?: string) => Partial<Pdoc>;
|
||||
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<Pdoc> = {
|
||||
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<Pdoc> = PROJECTION_PUBLIC,
|
||||
): Promise<Pdoc> {
|
||||
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<Pdoc> = {
|
||||
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<Pdoc> = ProblemModel.PROJECTION_PUBLIC,
|
||||
): Promise<Pdoc> {
|
||||
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<Pdoc>, projection = PROJECTION_LIST) {
|
||||
return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection);
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, query: FilterQuery<Pdoc>) {
|
||||
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
|
||||
}
|
||||
|
||||
export function edit(domainId: string, _id: number, $set: Partial<Pdoc>): Promise<Pdoc> {
|
||||
const delpid = $set.pid === '';
|
||||
if (delpid) delete $set.pid;
|
||||
return document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined);
|
||||
}
|
||||
|
||||
export function push<T extends ArrayKeys<Pdoc>>(domainId: string, _id: number, key: ArrayKeys<Pdoc>, value: Pdoc[T][0]) {
|
||||
return document.push(domainId, document.TYPE_PROBLEM, _id, key, value);
|
||||
}
|
||||
|
||||
export function pull<T extends ArrayKeys<Pdoc>>(domainId: string, pid: number, key: ArrayKeys<Pdoc>, values: Pdoc[T][0][]) {
|
||||
return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values);
|
||||
}
|
||||
|
||||
export function inc(domainId: string, _id: number, field: NumberKeys<Pdoc>, n: number): Promise<Pdoc> {
|
||||
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: FilterQuery<Pdoc>) {
|
||||
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<Pdoc>): Promise<string | null> {
|
||||
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<number | string>,
|
||||
getHidden = false, doThrow = true, projection = PROJECTION_PUBLIC,
|
||||
): Promise<Pdict> {
|
||||
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<Pdoc>, 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<Pdoc>) {
|
||||
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
|
||||
}
|
||||
|
||||
static async edit(domainId: string, _id: number, $set: Partial<Pdoc>): Promise<Pdoc> {
|
||||
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<T extends ArrayKeys<Pdoc>>(domainId: string, _id: number, key: ArrayKeys<Pdoc>, value: Pdoc[T][0]) {
|
||||
return document.push(domainId, document.TYPE_PROBLEM, _id, key, value);
|
||||
}
|
||||
|
||||
static pull<T extends ArrayKeys<Pdoc>>(domainId: string, pid: number, key: ArrayKeys<Pdoc>, values: Pdoc[T][0][]) {
|
||||
return document.deleteSub(domainId, document.TYPE_PROBLEM, pid, key, values);
|
||||
}
|
||||
|
||||
static inc(domainId: string, _id: number, field: NumberKeys<Pdoc>, n: number): Promise<Pdoc> {
|
||||
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
|
||||
}
|
||||
|
||||
static count(domainId: string, query: FilterQuery<Pdoc>) {
|
||||
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<Pdoc>): Promise<string | null> {
|
||||
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<number | string>,
|
||||
getHidden = false, doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
|
||||
): Promise<Pdict> {
|
||||
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<ProblemStatusDoc> = {};
|
||||
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<ProblemStatusDoc> = {};
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -92,5 +92,5 @@ bus.on('problem/delete', async (domainId, docId) => {
|
||||
]);
|
||||
});
|
||||
|
||||
export = SolutionModel;
|
||||
export default SolutionModel;
|
||||
global.Hydro.model.solution = SolutionModel;
|
||||
@@ -97,5 +97,5 @@ class TaskModel {
|
||||
};
|
||||
}
|
||||
|
||||
export = TaskModel;
|
||||
export default TaskModel;
|
||||
global.Hydro.model.task = TaskModel;
|
||||
@@ -101,5 +101,5 @@ function ensureIndexes() {
|
||||
}
|
||||
|
||||
bus.once('app/started', ensureIndexes);
|
||||
export = TokenModel;
|
||||
export default TokenModel;
|
||||
global.Hydro.model.token = TokenModel;
|
||||
@@ -271,5 +271,5 @@ function ensureIndexes() {
|
||||
}
|
||||
|
||||
bus.once('app/started', ensureIndexes);
|
||||
export = UserModel;
|
||||
export default UserModel;
|
||||
global.Hydro.model.user = UserModel;
|
||||
@@ -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';
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface EventMap extends Record<string, any> {
|
||||
|
||||
'system/setting': (args: Record<string, any>) => 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<string, any> {
|
||||
ip: string | null, highlight: boolean, pin: boolean
|
||||
) => VoidReturn
|
||||
|
||||
'problem/before-add': (doc: Partial<Pdoc>) => VoidReturn
|
||||
'problem/add': (doc: Partial<Pdoc>, docId: number) => VoidReturn
|
||||
'problem/before-edit': (doc: Partial<Pdoc>) => 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<Pdoc>, handler: any) => VoidReturn
|
||||
'problem/setting': (update: Partial<Pdoc>, handler: any) => VoidReturn
|
||||
'problem/get': (doc: Pdoc, handler: any) => VoidReturn
|
||||
'problem/delete': (domainId: string, docId: number) => VoidReturn
|
||||
|
||||
|
||||
@@ -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<K extends keyof Collections>(c: K): Collection<Collections[K]> {
|
||||
|
||||
@@ -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 };
|
||||
@@ -210,6 +210,7 @@ type DescriptorBuilder =
|
||||
& ((name: string, ...args: Array<Type | boolean | Validator | Converter>) => 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,
|
||||
|
||||
@@ -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<any>,
|
||||
) {
|
||||
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<any>,
|
||||
) {
|
||||
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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"sonic-channel": "^1.2.6"
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
host:
|
||||
value: localhost
|
||||
port:
|
||||
type: number
|
||||
value: 1491
|
||||
auth:
|
||||
type: text
|
||||
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user