mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui&elastic: support elastic search and allow search result pagination
This commit is contained in:
1 parent
ea6529b523
commit
18fc66ecb7
33 files changed
+411
-244
No files matched your search
@@ -1,5 +1,6 @@
|
||||
root: true
|
||||
extends:
|
||||
- airbnb
|
||||
- airbnb-typescript/base
|
||||
env:
|
||||
jquery: true
|
||||
@@ -16,6 +17,8 @@ ignorePatterns:
|
||||
- public/
|
||||
- '*.spec.ts'
|
||||
rules:
|
||||
function-call-argument-newline: 0
|
||||
react/function-component-definition: 0
|
||||
'@typescript-eslint/dot-notation': 0
|
||||
'@typescript-eslint/no-implied-eval': 0
|
||||
'@typescript-eslint/no-throw-literal': 0
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@hydrooj/elastic-search",
|
||||
"version": "1.0.0",
|
||||
"main": "package.json",
|
||||
"repository": "https://github.com/hydro-dev/Hydro",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { omit } from 'lodash';
|
||||
import DomainModel from 'hydrooj/src/model/domain';
|
||||
import ProblemModel from 'hydrooj/src/model/problem';
|
||||
import * as system from 'hydrooj/src/model/system';
|
||||
import { iterateAllProblem, iterateAllProblemInDomain } from 'hydrooj/src/pipelineUtils';
|
||||
import * as bus from 'hydrooj/src/service/bus';
|
||||
|
||||
const client = new Client({ node: system.get('elasticsearch.url') || 'http://192.168.1.82:9200' });
|
||||
|
||||
const indexOmit = ['_id', 'docType', 'data', 'additional_file', 'config', 'stats', 'assign'];
|
||||
|
||||
bus.on('problem/add', async (doc, docId) => {
|
||||
await client.index({
|
||||
index: 'problem',
|
||||
id: `${doc.domainId}/${docId}`,
|
||||
document: omit(doc, indexOmit),
|
||||
});
|
||||
});
|
||||
bus.on('problem/edit', async (pdoc) => {
|
||||
await client.index({
|
||||
index: 'problem',
|
||||
id: `${pdoc.domainId}/${pdoc.docId}`,
|
||||
document: omit(pdoc, indexOmit),
|
||||
});
|
||||
});
|
||||
bus.on('problem/del', async (domainId, docId) => {
|
||||
await client.delete({
|
||||
index: 'problem',
|
||||
id: `${domainId}/${docId}`,
|
||||
});
|
||||
});
|
||||
|
||||
global.Hydro.lib.problemSearch = async (domainId, q, opts) => {
|
||||
const size = opts?.limit || system.get('pagination.problem');
|
||||
const from = opts?.skip || 0;
|
||||
const union = await DomainModel.getUnion(domainId);
|
||||
const domainIds = [domainId, ...(union?.union || [])];
|
||||
const res = await client.search({
|
||||
index: 'problem',
|
||||
size,
|
||||
from,
|
||||
query: {
|
||||
simple_query_string: {
|
||||
query: q,
|
||||
fields: ['tag^5', 'title^3', 'content'],
|
||||
},
|
||||
},
|
||||
post_filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: domainIds.map((i) => ({ match: { domainId: i } })),
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
countRelation: typeof res.hits.total === 'number' ? 'eq' : res.hits.total.relation,
|
||||
total: typeof res.hits.total === 'number' ? res.hits.total : res.hits.total.value,
|
||||
hits: res.hits.hits.map((i) => i._id),
|
||||
};
|
||||
};
|
||||
|
||||
export const description = 'Elastic problem search re-index';
|
||||
|
||||
export async function run({ domainId }, report) {
|
||||
try {
|
||||
if (domainId) await client.deleteByQuery({ index: 'problem', query: { match: { domainId } } });
|
||||
else await client.deleteByQuery({ index: 'problem', query: { match_all: {} } });
|
||||
} catch (e) {
|
||||
if (!e.message.includes('index_not_found_exception')) throw e;
|
||||
}
|
||||
let i = 0;
|
||||
const cb = async (pdoc) => {
|
||||
i++;
|
||||
if (!(i % 1000)) report({ message: `${i} problems indexed` });
|
||||
await client.index({
|
||||
index: 'problem',
|
||||
id: `${pdoc.domainId}/${pdoc.docId}`,
|
||||
document: omit(pdoc, indexOmit),
|
||||
});
|
||||
};
|
||||
if (domainId) await iterateAllProblemInDomain(domainId, ProblemModel.PROJECTION_PUBLIC, cb);
|
||||
else await iterateAllProblem(ProblemModel.PROJECTION_PUBLIC, cb);
|
||||
await client.indices.refresh({ index: 'problem' });
|
||||
return true;
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
$or: [
|
||||
{ domainId: 'string' },
|
||||
{ domainId: 'undefined' },
|
||||
],
|
||||
};
|
||||
|
||||
global.Hydro.script.ensureElasticSearch = { run, description, validate };
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as vj4 } from './vj4';
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default as hydro, default } from './hydro';
|
||||
@@ -68,6 +68,7 @@ export class Logger {
|
||||
public stream: NodeJS.WritableStream = process.stderr;
|
||||
|
||||
constructor(public name: string, private showDiff = false) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
if (name in instances) return instances[name];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import child from 'child_process';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import arg from '@hydrooj/utils/lib/arg';
|
||||
|
||||
const argv = arg();
|
||||
|
||||
const exec = (...args) => {
|
||||
console.log('Executing: ', args[0], args[1].join(' '));
|
||||
const res = child.spawnSync(...args);
|
||||
if (res.error) throw res.error;
|
||||
if (res.status) throw new Error(`Error: Exited with code ${res.status}`);
|
||||
return res;
|
||||
};
|
||||
|
||||
function buildUrl(opts) {
|
||||
let mongourl = `${opts.protocol || 'mongodb'}://`;
|
||||
if (opts.username) mongourl += `${opts.username}:${opts.password}@`;
|
||||
mongourl += `${opts.host}:${opts.port}/${opts.name}`;
|
||||
if (opts.url) mongourl = opts.url;
|
||||
return mongourl;
|
||||
}
|
||||
|
||||
const hydroPath = path.resolve(os.homedir(), '.hydro');
|
||||
fs.ensureDirSync(hydroPath);
|
||||
const addonPath = path.resolve(hydroPath, 'addon.json');
|
||||
if (!fs.existsSync(addonPath)) fs.writeFileSync(addonPath, '[]');
|
||||
let addons = JSON.parse(fs.readFileSync(addonPath).toString());
|
||||
|
||||
if (argv._[0] === 'db') {
|
||||
const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8');
|
||||
const url = buildUrl(JSON.parse(dbConfig));
|
||||
child.spawn('mongo', [url], { stdio: 'inherit' });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (argv._[0] === 'backup') {
|
||||
const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8');
|
||||
const url = buildUrl(JSON.parse(dbConfig));
|
||||
const dir = `${os.tmpdir()}/${Math.random().toString(36).substring(2)}`;
|
||||
exec('mongodump', [url, `--out=${dir}/dump`], { stdio: 'inherit' });
|
||||
const env = `${os.homedir()}/.hydro/env`;
|
||||
if (fs.existsSync(env)) fs.copySync(env, `${dir}/env`);
|
||||
const target = `${process.cwd()}/backup-${new Date().toISOString().replace(':', '-').split(':')[0]}.zip`;
|
||||
exec('zip', ['-r', target, 'dump'], { cwd: dir, stdio: 'inherit' });
|
||||
if (!argv.dbOnly) {
|
||||
exec('zip', ['-r', target, 'file'], { cwd: '/data', stdio: 'inherit' });
|
||||
}
|
||||
exec('rm', ['-rf', dir]);
|
||||
console.log(`Database backup saved at ${target}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (argv._[0] === 'restore') {
|
||||
const dbConfig = fs.readFileSync(path.resolve(hydroPath, 'config.json'), 'utf-8');
|
||||
const url = buildUrl(JSON.parse(dbConfig));
|
||||
const dir = `${os.tmpdir()}/${Math.random().toString(36).substring(2)}`;
|
||||
if (!fs.existsSync(argv._[1])) {
|
||||
console.error('Cannot find file');
|
||||
process.exit(1);
|
||||
}
|
||||
exec('unzip', [argv._[1], '-d', dir], { stdio: 'inherit' });
|
||||
exec('mongorestore', [`--uri=${url}`, `--dir=${dir}/dump/${JSON.parse(dbConfig).name}`, '--drop'], { stdio: 'inherit' });
|
||||
if (fs.existsSync(`${dir}/file`)) {
|
||||
exec('rm', ['-rf', '/data/file/*'], { stdio: 'inherit' });
|
||||
exec('bash', ['-c', `mv ${dir}/file/* /data/file`], { stdio: 'inherit' });
|
||||
}
|
||||
if (fs.existsSync(`${dir}/env`)) {
|
||||
fs.copySync(`${dir}/env`, `${os.homedir()}/.hydro/env`, { overwrite: true });
|
||||
}
|
||||
fs.removeSync(dir);
|
||||
console.log('Successfully restored.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!addons.includes('@hydrooj/ui-default')) {
|
||||
try {
|
||||
const ui = argv.ui as string || '@hydrooj/ui-default';
|
||||
require.resolve(ui);
|
||||
addons.push(ui);
|
||||
} catch (e) {
|
||||
console.error('Please also install @hydrooj/ui-default');
|
||||
}
|
||||
}
|
||||
|
||||
if (argv._[0] && argv._[0] !== 'cli') {
|
||||
const operation = argv._[0];
|
||||
const arg1 = argv._[1];
|
||||
const arg2 = argv._[2];
|
||||
if (operation === 'addon') {
|
||||
if (arg1 === 'create') {
|
||||
fs.mkdirSync('/root/addon');
|
||||
child.execSync('yarn init -y', { cwd: '/root/addon' });
|
||||
fs.mkdirSync('/root/addon/templates');
|
||||
fs.mkdirSync('/root/addon/locales');
|
||||
fs.mkdirSync('/root/addon/public');
|
||||
addons.push('/root/addon');
|
||||
} else if (arg1 === 'add') {
|
||||
for (let i = 0; i < addons.length; i++) {
|
||||
if (addons[i] === arg2) {
|
||||
addons.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
addons.push(arg2);
|
||||
} else if (arg1 === 'remove') {
|
||||
for (let i = 0; i < addons.length; i++) {
|
||||
if (addons[i] === arg2) {
|
||||
addons.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
addons = Array.from(new Set(addons));
|
||||
console.log('Current Addons: ', addons);
|
||||
fs.writeFileSync(addonPath, JSON.stringify(addons, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
console.error('Unknown command: ', argv._[0]);
|
||||
} else {
|
||||
const hydro = require('../src/loader');
|
||||
addons = Array.from(new Set(addons));
|
||||
for (const addon of addons) hydro.addon(addon);
|
||||
(argv._[0] === 'cli' ? hydro.loadCli : hydro.load)().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.9.7",
|
||||
"version": "3.10.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -7,9 +7,7 @@ import options from '../options';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import {
|
||||
builtinLib, builtinModel, builtinScript,
|
||||
lib, model, script,
|
||||
service,
|
||||
lib, model, script, service,
|
||||
} from './common';
|
||||
|
||||
const argv = cac().parse();
|
||||
@@ -92,14 +90,14 @@ export async function load() {
|
||||
await db.start(opts);
|
||||
const storage = require('../service/storage');
|
||||
await storage.start();
|
||||
for (const i of builtinLib) require(`../lib/${i}`);
|
||||
require('../lib/index');
|
||||
await lib(pending, fail);
|
||||
const systemModel = require('../model/system');
|
||||
await systemModel.runConfig();
|
||||
await service(pending, fail);
|
||||
for (const i of builtinModel) require(`../model/${i}`);
|
||||
require('../model/index');
|
||||
await model(pending, fail);
|
||||
for (const i of builtinScript) require(`../script/${i}`);
|
||||
require('../script/index');
|
||||
await script(pending, fail, []);
|
||||
await bus.parallel('app/started');
|
||||
await cli();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-eval */
|
||||
import os from 'os';
|
||||
@@ -10,31 +11,6 @@ import * as bus from '../service/bus';
|
||||
|
||||
const logger = new Logger('common');
|
||||
|
||||
export const builtinLib = [
|
||||
'jwt', 'download', 'i18n', 'mail', 'useragent',
|
||||
'crypto', 'misc', 'paginate', 'hash.hydro', 'rank',
|
||||
'validator', 'ui', 'testdataConfig', 'difficulty', 'content',
|
||||
'avatar',
|
||||
];
|
||||
|
||||
export const builtinModel = [
|
||||
'builtin', 'document', 'domain', 'blacklist', 'opcount',
|
||||
'setting', 'token', 'user', 'storage', 'problem',
|
||||
'record', 'contest', 'message', 'solution', 'training',
|
||||
'discussion', 'system', 'oplog', 'blog',
|
||||
];
|
||||
|
||||
export const builtinHandler = [
|
||||
'home', 'problem', 'record', 'judge', 'user',
|
||||
'contest', 'training', 'discussion', 'manage', 'import',
|
||||
'misc', 'homework', 'domain', 'status', 'api', 'blog',
|
||||
];
|
||||
|
||||
export const builtinScript = [
|
||||
'rating', 'problemStat', 'blacklist', 'deleteUser', 'storageUsage',
|
||||
'checkUpdate',
|
||||
];
|
||||
|
||||
function getFiles(folder: string, base = ''): string[] {
|
||||
const files = [];
|
||||
const f = fs.readdirSync(folder);
|
||||
@@ -53,7 +29,7 @@ export async function handler(pending: string[], fail: string[]) {
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
logger.info('Handler init: %s', i);
|
||||
eval('require')(p);
|
||||
require(p);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
logger.error('Handler Load Fail: %s', i);
|
||||
@@ -136,7 +112,7 @@ export async function template(pending: string[], fail: string[]) {
|
||||
try {
|
||||
const files = getFiles(p);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.tsx')) global.Hydro.ui.template[file] = eval('require')(path.resolve(p, file));
|
||||
if (file.endsWith('.tsx')) global.Hydro.ui.template[file] = require(path.resolve(p, file));
|
||||
global.Hydro.ui.template[file] = await fs.readFile(path.resolve(p, file), 'utf-8');
|
||||
}
|
||||
logger.info('Template init: %s', i);
|
||||
@@ -157,7 +133,7 @@ export async function model(pending: string[], fail: string[]) {
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
logger.info('Model init: %s', i);
|
||||
eval('require')(p);
|
||||
require(p);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
logger.error('Model Load Fail: %s', i);
|
||||
@@ -175,7 +151,7 @@ export async function lib(pending: string[], fail: string[]) {
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
logger.info('Lib init: %s', i);
|
||||
eval('require')(p);
|
||||
require(p);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
logger.error('Lib Load Fail: %s', i);
|
||||
@@ -193,7 +169,7 @@ export async function service(pending: string[], fail: string[]) {
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
logger.info('Service init: %s', i);
|
||||
eval('require')(p);
|
||||
require(p);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
logger.error('Service Load Fail: %s', i);
|
||||
@@ -215,7 +191,7 @@ export async function script(pending: string[], fail: string[], active: string[]
|
||||
if (await fs.pathExists(p) && !fail.includes(i)) {
|
||||
try {
|
||||
logger.info('Script init: %s', i);
|
||||
eval('require')(p);
|
||||
require(p);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
logger.error('Script Load Fail: %s', i);
|
||||
|
||||
@@ -9,8 +9,7 @@ import options from '../options';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import {
|
||||
builtinHandler, builtinLib, builtinModel,
|
||||
builtinScript, handler, lib, locale, model, script, service, setting, template,
|
||||
handler, lib, locale, model, script, service, setting, template,
|
||||
} from './common';
|
||||
|
||||
const logger = new Logger('worker');
|
||||
@@ -45,15 +44,7 @@ export async function load() {
|
||||
const storage = require('../service/storage');
|
||||
await storage.start();
|
||||
if (detail) logger.info('finish: storage.connect');
|
||||
for (const i of builtinLib) {
|
||||
let t;
|
||||
try {
|
||||
t = require.resolve(`../lib/${i}`);
|
||||
} catch (e) {
|
||||
t = require.resolve(`@hydrooj/utils/lib/${i}`);
|
||||
}
|
||||
require(t);
|
||||
}
|
||||
require('../lib/index');
|
||||
if (detail) logger.info('finish: lib.builtin');
|
||||
await lib(pending, fail);
|
||||
if (detail) logger.info('finish: lib.extra');
|
||||
@@ -64,9 +55,9 @@ export async function load() {
|
||||
if (detail) logger.info('finish: server');
|
||||
await service(pending, fail);
|
||||
if (detail) logger.info('finish: service.extra');
|
||||
for (const i of builtinModel) require(`../model/${i}`);
|
||||
require('../model/index');
|
||||
if (detail) logger.info('finish: model.builtin');
|
||||
for (const i of builtinHandler) require(`../handler/${i}`);
|
||||
require('../handler/index');
|
||||
if (detail) logger.info('finish: handler.builtin');
|
||||
await model(pending, fail);
|
||||
if (detail) logger.info('finish: model.extra');
|
||||
@@ -79,7 +70,7 @@ export async function load() {
|
||||
if (detail) logger.info('finish: handler.apply');
|
||||
const notfound = require('../handler/notfound');
|
||||
await notfound.apply();
|
||||
for (const i of builtinScript) require(`../script/${i}`);
|
||||
require('../script/index');
|
||||
if (detail) logger.info('finish: script.builtin');
|
||||
await script(pending, fail, active);
|
||||
if (detail) logger.info('finish: script.extra');
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import './home';
|
||||
import './problem';
|
||||
import './record';
|
||||
import './judge';
|
||||
import './user';
|
||||
import './contest';
|
||||
import './training';
|
||||
import './discussion';
|
||||
import './manage';
|
||||
import './import';
|
||||
import './misc';
|
||||
import './homework';
|
||||
import './domain';
|
||||
import './status';
|
||||
import './api';
|
||||
import './blog';
|
||||
@@ -114,15 +114,15 @@ export class ProblemHandler extends Handler {
|
||||
async cleanup() {
|
||||
if (this.response.template === 'problem_main.html' && this.request.json) {
|
||||
const {
|
||||
page, pcount, ppcount, pdocs, psdict, category,
|
||||
page, pcount, pcountRelation, ppcount, pdocs, psdict, category, qs,
|
||||
} = this.response.body;
|
||||
this.response.body = {
|
||||
title: this.renderTitle(this.translate('problem_main')),
|
||||
fragments: (await Promise.all([
|
||||
this.renderHTML('partials/problem_list.html', {
|
||||
page, ppcount, pcount, pdocs, psdict,
|
||||
page, ppcount, pcount, pdocs, psdict, qs,
|
||||
}),
|
||||
this.renderHTML('partials/problem_stat.html', { pcount }),
|
||||
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
|
||||
this.renderHTML('partials/problem_lucky.html', { category }),
|
||||
])).map((i) => ({ html: i })),
|
||||
raw: {
|
||||
@@ -145,21 +145,25 @@ export class ProblemMainHandler extends ProblemHandler {
|
||||
const search = global.Hydro.lib.problemSearch;
|
||||
let sort: string[];
|
||||
let fail = false;
|
||||
let pcountRelation = 'eq';
|
||||
if (category.length) query.$and = category.map((tag) => ({ tag }));
|
||||
if (q) category.push(q);
|
||||
if (category.length) this.extraTitleContent = category.join(',');
|
||||
let total = 0;
|
||||
if (q) {
|
||||
if (search) {
|
||||
const result = await search(domainId, q);
|
||||
if (!result.length) fail = true;
|
||||
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
|
||||
total = result.total;
|
||||
pcountRelation = result.countRelation;
|
||||
if (!result.hits.length) fail = true;
|
||||
if (!query.$and) query.$and = [];
|
||||
query.$and.push({
|
||||
$or: result.map((i) => {
|
||||
$or: result.hits.map((i) => {
|
||||
const [did, docId] = i.split('/');
|
||||
return { domainId: did, docId: +docId };
|
||||
}),
|
||||
});
|
||||
sort = result;
|
||||
sort = result.hits;
|
||||
} else query.$text = { $search: q };
|
||||
}
|
||||
await bus.serial('problem/list', query, this);
|
||||
@@ -168,11 +172,15 @@ export class ProblemMainHandler extends ProblemHandler {
|
||||
? [[], 0, 0]
|
||||
: await problem.list(
|
||||
domainId, query,
|
||||
page, system.get('pagination.problem'),
|
||||
sort.length ? 1 : page, system.get('pagination.problem'),
|
||||
undefined, this.user._id,
|
||||
);
|
||||
if (total) {
|
||||
pcount = total;
|
||||
ppcount = Math.ceil(total / system.get('pagination.problem'));
|
||||
}
|
||||
if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(`${a.domainId}/${a.docId}`) - sort.indexOf(`${b.domainId}/${b.docId}`));
|
||||
if (q) {
|
||||
if (q && page === 1) {
|
||||
const pdoc = await problem.get(domainId, +q || q, problem.PROJECTION_LIST);
|
||||
if (pdoc && problem.canViewBy(pdoc, this.user)) {
|
||||
const count = pdocs.length;
|
||||
@@ -192,7 +200,14 @@ export class ProblemMainHandler extends ProblemHandler {
|
||||
);
|
||||
}
|
||||
this.response.body = {
|
||||
page, pcount, ppcount, pdocs, psdict, category: category.join(','),
|
||||
page,
|
||||
pcount,
|
||||
ppcount,
|
||||
pcountRelation,
|
||||
pdocs,
|
||||
psdict,
|
||||
category: category.join(','),
|
||||
qs: q ? `q=${encodeURIComponent(q)}` : '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -829,8 +844,8 @@ export class ProblemPrefixListHandler extends Handler {
|
||||
const search = global.Hydro.lib.problemSearch;
|
||||
if (pdocs.length < 20) {
|
||||
if (search) {
|
||||
const result = await search(domainId, prefix, 20 - pdocs.length);
|
||||
const docs = await problem.getMulti(domainId, { docId: { $in: result } }).toArray();
|
||||
const result = await search(domainId, prefix, { limit: 20 - pdocs.length });
|
||||
const docs = await problem.getMulti(domainId, { docId: { $in: result.hits.map((i) => +i.split('/')[1]) } }).toArray();
|
||||
pdocs.push(...docs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,13 @@ export interface GDoc {
|
||||
uids: number[];
|
||||
}
|
||||
|
||||
export interface UserPreferenceDoc {
|
||||
_id: ObjectID;
|
||||
filename: string;
|
||||
uid: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type ownerInfo = { owner: number, maintainer?: number[] };
|
||||
|
||||
export type User = import('./model/user').User;
|
||||
@@ -579,6 +586,7 @@ export interface Collections {
|
||||
'document.status': any;
|
||||
'problem': ProblemDoc;
|
||||
'user': Udoc;
|
||||
'user.preference': UserPreferenceDoc;
|
||||
'vuser': VUdoc;
|
||||
'user.group': GDoc;
|
||||
'check': any;
|
||||
@@ -631,11 +639,23 @@ export interface Service {
|
||||
storage: typeof import('./service/storage'),
|
||||
}
|
||||
|
||||
interface GeoIP {
|
||||
export interface GeoIP {
|
||||
provider: string,
|
||||
lookup: (ip: string, locale?: string) => any,
|
||||
}
|
||||
|
||||
export interface ProblemSearchResponse {
|
||||
hits: string[];
|
||||
total: number;
|
||||
countRelation: 'eq' | 'gte';
|
||||
}
|
||||
export interface ProblemSearchOptions {
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
export type ProblemSearch = (domainId: string, q: string, options?: ProblemSearchOptions) => Promise<ProblemSearchResponse>;
|
||||
|
||||
export interface Lib extends Record<string, any> {
|
||||
download: typeof import('./lib/download'),
|
||||
difficulty: typeof import('./lib/difficulty'),
|
||||
@@ -655,6 +675,7 @@ export interface Lib extends Record<string, any> {
|
||||
validator: typeof import('./lib/validator'),
|
||||
template?: any,
|
||||
geoip?: GeoIP,
|
||||
problemSearch: ProblemSearch;
|
||||
}
|
||||
|
||||
export interface UI {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import './jwt';
|
||||
import './download';
|
||||
import './i18n';
|
||||
import './mail';
|
||||
import './useragent';
|
||||
import './crypto';
|
||||
import './misc';
|
||||
import './paginate';
|
||||
import './hash.hydro';
|
||||
import './rank';
|
||||
import './validator';
|
||||
import './ui';
|
||||
import './testdataConfig';
|
||||
import './difficulty';
|
||||
import './content';
|
||||
import './avatar';
|
||||
@@ -39,6 +39,7 @@ export class Logger {
|
||||
}
|
||||
|
||||
constructor(public name: string) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
if (name in instances) return instances[name];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
|
||||
@@ -125,7 +125,9 @@ const acm = buildContestRule({
|
||||
const data = await document.collStatus.aggregate([
|
||||
{
|
||||
$match: {
|
||||
domainId: tdoc.domainId, docType: document.TYPE_CONTEST, docId: tdoc.docId,
|
||||
domainId: tdoc.domainId,
|
||||
docType: document.TYPE_CONTEST,
|
||||
docId: tdoc.docId,
|
||||
accept: { $gte: 1 },
|
||||
detail: { $elemMatch: { status: STATUS.STATUS_ACCEPTED } },
|
||||
},
|
||||
@@ -230,9 +232,10 @@ const oi = buildContestRule({
|
||||
const psdict = {};
|
||||
const first = {};
|
||||
for (const pid of tdoc.pids) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define, no-await-in-loop
|
||||
const [data] = await getMultiStatus(tdoc.domainId, {
|
||||
docType: document.TYPE_CONTEST, docId: tdoc.docId,
|
||||
docType: document.TYPE_CONTEST,
|
||||
docId: tdoc.docId,
|
||||
[`detail.${pid}.status`]: STATUS.STATUS_ACCEPTED,
|
||||
}).sort({ [`detail.${pid}.rid`]: 1 }).limit(1).toArray();
|
||||
first[pid] = data ? data.detail[pid].rid.generationTime : new ObjectID().generationTime;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import './builtin';
|
||||
import './document';
|
||||
import './domain';
|
||||
import './blacklist';
|
||||
import './opcount';
|
||||
import './setting';
|
||||
import './token';
|
||||
import './user';
|
||||
import './storage';
|
||||
import './problem';
|
||||
import './record';
|
||||
import './contest';
|
||||
import './message';
|
||||
import './solution';
|
||||
import './training';
|
||||
import './discussion';
|
||||
import './system';
|
||||
import './oplog';
|
||||
import './blog';
|
||||
@@ -72,7 +72,7 @@ class WorkerService implements BaseService {
|
||||
const start = Date.now();
|
||||
await Promise.race([
|
||||
this.handlers[doc.subType](doc),
|
||||
new Promise((resolve) => setTimeout(resolve, 300000)),
|
||||
sleep(300000),
|
||||
]);
|
||||
const spent = Date.now() - start;
|
||||
if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import './rating';
|
||||
import './problemStat';
|
||||
import './blacklist';
|
||||
import './deleteUser';
|
||||
import './storageUsage';
|
||||
import './checkUpdate';
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Collection, Db, MongoClient } from 'mongodb';
|
||||
import { BaseService, Collections } from '../../interface';
|
||||
import * as bus from '../bus';
|
||||
|
||||
interface MongoConfig {
|
||||
protocol?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
host?: string,
|
||||
port?: string,
|
||||
name?: string,
|
||||
url?: string,
|
||||
prefix?: string,
|
||||
}
|
||||
|
||||
class MongoService implements BaseService {
|
||||
public client: MongoClient;
|
||||
public db: Db;
|
||||
public started = false;
|
||||
private opts: MongoConfig;
|
||||
|
||||
async start(opts: MongoConfig) {
|
||||
this.opts = opts;
|
||||
this.client = await MongoClient.connect(global.__MONGO_URI__, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||
this.db = this.client.db(global.__MONGO_DB_NAME_);
|
||||
await bus.parallel('database/connect', this.db);
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
public collection<K extends keyof Collections>(c: K): Collection<Collections[K]> {
|
||||
if (this.opts.prefix) return this.db.collection(`${this.opts.prefix}.${c}`);
|
||||
return this.db.collection(c);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.client.close();
|
||||
await this.client2.close();
|
||||
}
|
||||
}
|
||||
|
||||
const service = new MongoService();
|
||||
global.Hydro.service.db = service as any;
|
||||
export = service;
|
||||
@@ -1 +0,0 @@
|
||||
# Login-With-Osu
|
||||
@@ -1,73 +0,0 @@
|
||||
import 'hydrooj';
|
||||
|
||||
import * as superagent from 'superagent';
|
||||
|
||||
declare module 'hydrooj' {
|
||||
interface SystemKeys {
|
||||
'login-with-osu.id': string,
|
||||
'login-with-osu.secret': string,
|
||||
}
|
||||
interface Lib {
|
||||
oauth_osu: typeof import('./lib'),
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URL = 'https://osu.ppy.sh/';
|
||||
|
||||
async function get() {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const [[appid, url], [state]] = await Promise.all([
|
||||
system.getMany([
|
||||
'login-with-osu.id',
|
||||
'server.url',
|
||||
]),
|
||||
token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }),
|
||||
]);
|
||||
this.response.redirect = `${BASE_URL}oauth/authorize?client_id=${appid}&state=${state}&redirect_uri=${url}oauth/osu/callback&response_type=code`;
|
||||
}
|
||||
|
||||
async function callback({ state, code }) {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const { UserFacingError } = global.Hydro.error;
|
||||
const [[appid, secret, url], s] = await Promise.all([
|
||||
system.getMany([
|
||||
'login-with-osu.id',
|
||||
'login-with-osu.secret',
|
||||
'server.url',
|
||||
]),
|
||||
token.get(state, token.TYPE_OAUTH),
|
||||
]);
|
||||
const res = await superagent.post(`${BASE_URL}oauth/token`)
|
||||
.send({
|
||||
client_id: appid,
|
||||
client_secret: secret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: `${url}oauth/github/callback`,
|
||||
})
|
||||
.set('accept', 'application/json');
|
||||
if (res.body.error) {
|
||||
throw new UserFacingError(
|
||||
res.body.error, res.body.error_description, res.body.error_uri,
|
||||
);
|
||||
}
|
||||
const t = res.body.access_token;
|
||||
const userInfo = await superagent.get(`${BASE_URL}api/v2/me`)
|
||||
.set('User-Agent', 'Hydro-OAuth')
|
||||
.set('Authorization', `Bearer ${t}`);
|
||||
const ret = {
|
||||
_id: `${userInfo.body.id}@osu.local`,
|
||||
email: `${userInfo.body.id}@osu.local`,
|
||||
bio: '',
|
||||
uname: [userInfo.body.username],
|
||||
};
|
||||
this.response.redirect = s.redirect;
|
||||
await token.del(s._id, token.TYPE_OAUTH);
|
||||
return ret;
|
||||
}
|
||||
|
||||
global.Hydro.lib.oauth_osu = {
|
||||
text: 'Login with Osu',
|
||||
callback,
|
||||
get,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
Login with Osu: 使用 Osu 登录
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "@hydrooj/login-with-osu",
|
||||
"version": "0.1.0",
|
||||
"main": "package.json",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"preferUnplugged": true,
|
||||
"devDependencies": {
|
||||
"@types/superagent": "^4.1.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hydrooj": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"superagent": "^7.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
id:
|
||||
type: text
|
||||
category: system
|
||||
desc: Osu OAuth AppID
|
||||
name: id
|
||||
secret:
|
||||
type: text
|
||||
category: system
|
||||
desc: Osu OAuth Secret
|
||||
name: secret
|
||||
proxy:
|
||||
type: text
|
||||
category: system
|
||||
desc: Osu OAuth Proxy
|
||||
name: proxy
|
||||
@@ -56,7 +56,7 @@ const nameMap = {
|
||||
export async function run({
|
||||
host = 'localhost', port = 3306, name = 'jol',
|
||||
username, password, domainId, contestType = 'oi',
|
||||
dataDir,
|
||||
dataDir, rerun = false,
|
||||
}, report: Function) {
|
||||
const src = mysql.createConnection({
|
||||
host,
|
||||
@@ -65,7 +65,9 @@ export async function run({
|
||||
password,
|
||||
database: name,
|
||||
});
|
||||
await new Promise((resolve, reject) => src.connect((err) => (err ? reject(err) : resolve(null))));
|
||||
await new Promise((resolve, reject) => {
|
||||
src.connect((err) => (err ? reject(err) : resolve(null)));
|
||||
});
|
||||
const query = (q: string | mysql.Query) => new Promise<[values: any[], fields: mysql.FieldInfo[]]>((res, rej) => {
|
||||
src.query(q, (err, val, fields) => {
|
||||
if (err) rej(err);
|
||||
@@ -151,23 +153,29 @@ export async function run({
|
||||
for (let pageId = 0; pageId < pageCount; pageId++) {
|
||||
const [pdocs] = await query(`SELECT * FROM \`problem\` LIMIT ${pageId * step}, ${step}`);
|
||||
for (const pdoc of pdocs) {
|
||||
const pid = await problem.add(
|
||||
domainId, `P${pdoc.problem_id}`,
|
||||
pdoc.title, buildContent({
|
||||
description: pdoc.description,
|
||||
input: pdoc.input,
|
||||
output: pdoc.output,
|
||||
samples: [[pdoc.sample_input.trim(), pdoc.sample_output.trim()]],
|
||||
hint: pdoc.hint,
|
||||
source: pdoc.source,
|
||||
}, 'html'),
|
||||
1, pdoc.source.split(' ').map((i) => i.trim()).filter((i) => i), pdoc.defunct === 'Y',
|
||||
);
|
||||
pidMap[pdoc.problem_id] = pid;
|
||||
if (rerun) {
|
||||
const opdoc = await problem.get(domainId, `P${pdoc.problem_id}`);
|
||||
if (opdoc) pidMap[pdoc.problem_id] = opdoc.docId;
|
||||
}
|
||||
if (!pidMap[pdoc.problem_id]) {
|
||||
const pid = await problem.add(
|
||||
domainId, `P${pdoc.problem_id}`,
|
||||
pdoc.title, buildContent({
|
||||
description: pdoc.description,
|
||||
input: pdoc.input,
|
||||
output: pdoc.output,
|
||||
samples: [[pdoc.sample_input.trim(), pdoc.sample_output.trim()]],
|
||||
hint: pdoc.hint,
|
||||
source: pdoc.source,
|
||||
}, 'html'),
|
||||
1, pdoc.source.split(' ').map((i) => i.trim()).filter((i) => i), pdoc.defunct === 'Y',
|
||||
);
|
||||
pidMap[pdoc.problem_id] = pid;
|
||||
}
|
||||
const [cdoc] = await query(`SELECT * FROM \`privilege\` WHERE \`rightstr\` = 'p${pdoc.problem_id}'`);
|
||||
const maintainer = [];
|
||||
for (let i = 1; i < cdoc.length; i++) maintainer.push(uidMap[cdoc[i].user_id]);
|
||||
await problem.edit(domainId, pid, {
|
||||
await problem.edit(domainId, pidMap[pdoc.problem_id], {
|
||||
nAccept: 0,
|
||||
nSubmit: pdoc.submit,
|
||||
config: `time: ${pdoc.time_limit}s\nmemory: ${pdoc.memory_limit}m`,
|
||||
@@ -264,6 +272,7 @@ export async function run({
|
||||
|
||||
src.end();
|
||||
|
||||
if (!dataDir) return true;
|
||||
if (dataDir.endsWith('/')) dataDir = dataDir.slice(0, -1);
|
||||
const files = await fs.readdir(dataDir, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
|
||||
@@ -46,8 +46,13 @@ bus.on('problem/del', async (domainId, docId) => {
|
||||
await Promise.all(tasks);
|
||||
});
|
||||
|
||||
global.Hydro.lib.problemSearch = async (domainId: string, query: string, limit = system.get('pagination.problem')) => {
|
||||
global.Hydro.lib.problemSearch = async (domainId, query, opts) => {
|
||||
const limit = opts?.limit || system.get('pagination.problem');
|
||||
const ids = await sonic.query('problem', `${domainId}@title`, query, { limit });
|
||||
if (limit - ids.length > 0) ids.push(...await sonic.query('problem', `${domainId}@content`, query, { limit: limit - ids.length }));
|
||||
return ids;
|
||||
return {
|
||||
countRelation: ids.length >= limit ? 'gte' : 'eq',
|
||||
total: ids.length,
|
||||
hits: ids,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/sonic",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Sonic search service",
|
||||
"main": "service.js",
|
||||
"typings": "service.d.ts",
|
||||
|
||||
@@ -527,6 +527,7 @@ Or, with automatically filled invitation code: 或者,这是可以自动填写
|
||||
Ordered List: 有序列表
|
||||
Original Score: 原始分数
|
||||
OS Info: 系统信息
|
||||
Over {0} problems: 超过 {0} 道题目
|
||||
Owner: 所有者
|
||||
page.problem_detail.sidebar.show_category: 点击显示
|
||||
page.training_detail.invalid_when_not_enrolled: 未参加训练计划时您不能查看题目详情。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.34.12",
|
||||
"version": "4.34.13",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% if not pdocs.length %}
|
||||
{{ nothing.render('Sorry, there is no problem in the problem set') }}
|
||||
{% else %}
|
||||
{{ paginator.render(page, ppcount, position='top') }}
|
||||
{{ paginator.render(page, ppcount, position='top', add_qs=qs) }}
|
||||
<table class="data-table hide-problem-tag">
|
||||
<colgroup>
|
||||
<col class="col--checkbox display-mode-hide">
|
||||
@@ -73,6 +73,6 @@
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ paginator.render(page, ppcount) }}
|
||||
{{ paginator.render(page, ppcount, add_qs=qs) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,3 +1,3 @@
|
||||
<div data-fragment-id="problem_stat">
|
||||
<p>{{ _('{0} problems').format(pcount) }}</p>
|
||||
<p>{{ _('{0} problems' if pcountRelation == 'eq' else 'Over {0} problems').format(pcount) }}</p>
|
||||
</div>
|
||||
@@ -252,7 +252,8 @@ export default class UOJProvider implements IBasicProvider {
|
||||
}
|
||||
}
|
||||
if (document.querySelector('tbody').innerHTML.includes('Judging')) continue;
|
||||
const score = +summary.children[3]?.children[0].innerHTML || 0;
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
const score = +summary.children[3]?.children[0]?.innerHTML || 0;
|
||||
const status = score === 100 ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER;
|
||||
return await end({
|
||||
status,
|
||||
|
||||
Reference in new issue
Block a user