Merge branch 'master' into contest-access-lock

This commit is contained in:
undefined committed 2022-12-12 02:48:02 +08:00
commit 73dc91c342
146 files changed
+751 -757

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
dist
*.d.ts
node_modules
public
public/**/*.js
+21 -3
View File
@@ -3,7 +3,7 @@ extends:
- airbnb-base
- airbnb-typescript/base
env:
jquery: true
es6: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly
@@ -14,8 +14,26 @@ plugins:
- simple-import-sort
- eslint-plugin-import
ignorePatterns:
- public/
- '*.spec.ts'
- public/**/*.js
- packages/ui-default
overrides:
- files:
- '**/public/**/*.ts'
- '**/public/**/*.page.js'
rules:
'@typescript-eslint/indent':
- warn
- 2
env:
browser: true
es6: true
jquery: true
globals:
UiContext: true
UserContext: true
externalModules: true
LOCALES: true
LANGS: true
rules:
'@typescript-eslint/no-shadow': 1
+1
View File
@@ -27,6 +27,7 @@ globalConfig.json
/config.yaml
/config.json
yarn.lock
pnpm-lock.yaml
tsconfig.json
tsconfig.*.json
+7
View File
@@ -1,5 +1,10 @@
const fs = require('fs');
const path = require('path');
const child = require('child_process');
if (fs.existsSync('plugins/patch-package/package.json') && fs.existsSync('node_modules/patch-package/package.json')) {
child.execSync('npx patch-package --patch-dir=plugins/patch-package/patches', { stdio: 'inherit' });
}
const dir = path.dirname(path.dirname(require.resolve('@types/node/package.json')));
const types = fs.readdirSync(dir).filter((i) => !['sharedworker', 'serviceworker'].includes(i));
@@ -51,6 +56,8 @@ const configFlat = (name) => ({
if (!fs.existsSync(path.resolve(process.cwd(), 'plugins'))) {
fs.mkdirSync(path.resolve(process.cwd(), 'plugins'));
// Write an empty file to make eslint happy
fs.writeFileSync(path.resolve(process.cwd(), 'plugins/eslint.ts'), '');
}
const modules = [
+1 -5
View File
@@ -336,11 +336,7 @@ connect-timeout = 10`);
})})`),
[`mongo 127.0.0.1:27017/hydro ${tmpFile}`, { retry: true }],
() => writeFileSync(`${process.env.HOME}/.hydro/config.json`, JSON.stringify({
host: '127.0.0.1',
port: 27017,
name: 'hydro',
username: 'hydro',
password,
uri: `mongodb://hydro:${password}@127.0.0.1:27017/hydro`,
})),
'pm2 stop mongod',
'pm2 del mongod',
+29 -13
View File
@@ -17,12 +17,12 @@
"build:ui:production:webpack": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node packages/ui-default/build --production",
"test": "mocha",
"benchmark": "cross-env BENCHMARK=true mocha",
"lint": "eslint packages --ext ts --fix",
"lint:ci": "eslint packages --ext ts",
"lint": "eslint packages plugins --ext ts --fix",
"lint:ci": "eslint packages plugins --ext ts",
"lint:ui": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --fix",
"lint:ui:ci": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx .",
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation node_modules/hydrooj/bin/hydrooj --debug --template",
"start": "node_modules/.bin/hydrooj",
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation packages/hydrooj/bin/hydrooj --debug --template",
"start": "packages/hydrooj/bin/hydrooj",
"postinstall": "node build/prepare.js"
},
"version": "1.0.0",
@@ -34,14 +34,18 @@
"@types/node": "^18.11.9",
"@types/semver": "^7.3.13",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"autocannon": "^7.10.0",
"cac": "^6.7.14",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3",
"css-loader": "^6.7.2",
"esbuild": "0.15.2",
"eslint": "^8.28.0",
"esbuild-loader": "^2.20.0",
"eslint": "^8.29.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-import-resolver-typescript": "^3.5.2",
@@ -51,17 +55,29 @@
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "8.0.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"fs-extra": "^10.1.0",
"globby": "13.1.2",
"latest-version": "7.0.0",
"mocha": "^10.1.0",
"mini-css-extract-plugin": "^2.7.2",
"mocha": "^10.2.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"mongodb": "^3.7.3",
"mongodb-memory-server": "^8.10.1",
"mongodb-memory-server": "^8.10.2",
"nyc": "^15.1.0",
"ora": "^6.1.2",
"postcss-loader": "^7.0.1",
"semver": "^7.3.8",
"supertest": "^6.3.1",
"typescript": "4.9.3"
},
"packageManager": "yarn@3.2.3"
"style-loader": "^3.3.1",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.0",
"supertest": "^6.3.3",
"ts-loader": "^9.4.2",
"typescript": "4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^5.0.0",
"webpackbar": "^5.0.2"
}
}
+3 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js",
"version": "3.1.9",
"version": "3.1.11",
"main": "package.json",
"author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git",
@@ -9,21 +9,17 @@
"@hydrooj/utils": "workspace:*",
"cac": "^6.7.14",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mongodb": "^3.7.3",
"p-queue": "^7.3.0",
"schemastery": "^3.5.4",
"schemastery": "^3.6.1",
"shell-quote": "^1.7.4",
"superagent": "^8.0.4",
"superagent": "^8.0.6",
"ws": "^8.11.0"
},
"preferUnplugged": true,
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.190",
"@types/shell-quote": "^1.7.1",
"@types/ws": "^8.5.3"
}
+3 -5
View File
@@ -1,11 +1,9 @@
/* eslint-disable no-await-in-loop */
import path from 'path';
import fs from 'fs-extra';
import yaml from 'js-yaml';
import { max, sum } from 'lodash';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import { changeErrorType } from '@hydrooj/utils/lib/utils';
import { changeErrorType, yaml } from '@hydrooj/utils/lib/utils';
import { ProblemConfigFile } from 'hydrooj';
import { getConfig } from './config';
import { FormatError, SystemError } from './error';
@@ -16,11 +14,11 @@ function isValidConfig(config) {
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
const time = sum(config.subtasks.map((subtask) => sum(subtask.cases.map((c) => c.time))));
const time = Math.sum(...config.subtasks.flatMap((subtask) => subtask.cases.map((c) => c.time)));
if (time > (getConfig('total_time_limit') || 60) * 1000) {
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
}
const memMax = max(config.subtasks.map((subtask) => max(subtask.cases.map((c) => c.memory))));
const memMax = Math.max(...config.subtasks.flatMap((subtask) => subtask.cases.map((c) => c.memory)));
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
throw new FormatError('You did not specify a checker.');
+1 -1
View File
@@ -210,7 +210,7 @@ const checkers: Record<string, Checker> = new Proxy({
message: stderr,
};
}
if (status === STATUS.STATUS_RUNTIME_ERROR) {
if (status === STATUS.STATUS_RUNTIME_ERROR && !stderr?.trim()) {
return {
status: STATUS.STATUS_WRONG_ANSWER,
score: 0,
+1 -1
View File
@@ -2,8 +2,8 @@ import os from 'os';
import path from 'path';
import cac from 'cac';
import fs from 'fs-extra';
import yaml from 'js-yaml';
import Schema from 'schemastery';
import { yaml } from '@hydrooj/utils';
const argv = cac().parse();
+1 -1
View File
@@ -145,7 +145,7 @@ export default class Hydro {
send(rid: string | ObjectID, key: 'next' | 'end', data: Partial<JudgeResultBody>) {
data.rid = new ObjectID(rid);
data.key = key;
if (data.case) data.case.message = removeNixPath(data.case.message || '');
if (data.case && typeof data.case.message === 'string') data.case.message = removeNixPath(data.case.message);
if (typeof data.message === 'string') data.message = removeNixPath(data.message);
if (typeof data.compilerText === 'string') data.compilerText = removeNixPath(data.compilerText);
this.ws.send(JSON.stringify(data));
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'assert';
import { readFile } from 'fs-extra';
import yaml from 'js-yaml';
import { yaml } from '@hydrooj/utils';
import { STATUS } from '@hydrooj/utils/lib/status';
import { FormatError } from '../error';
import { Context } from './interface';
+1
View File
@@ -98,6 +98,7 @@ function proc(params: Parameter): Cmd {
strictMemoryLimit: getConfig('strict_memory'),
// stackLimit: memory * 1024 * 1024,
procLimit: params.processLimit || getConfig('processLimit'),
copyOutMax: Math.floor(1024 * 1024 * size * 3),
copyIn,
copyOut,
copyOutCached,
+2 -3
View File
@@ -1,6 +1,5 @@
import path from 'path';
import fs from 'fs-extra';
import { noop } from 'lodash';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { STATUS } from '@hydrooj/utils/lib/status';
import type {
@@ -40,7 +39,7 @@ export class JudgeTask {
folder: string;
config: ParsedConfig;
meta: JudgeMeta;
files?:Record<string, string>;
files?: Record<string, string>;
next: (data: Partial<JudgeResultBody>) => void;
end: (data: Partial<JudgeResultBody>) => void;
env: Record<string, string>;
@@ -103,7 +102,7 @@ export class JudgeTask {
} finally {
Lock.release(`${host}/${this.source}/${this.rid}`);
// eslint-disable-next-line no-await-in-loop
for (const clean of this.clean) await clean()?.catch(noop);
for (const clean of this.clean) await clean()?.catch(() => null);
tmpfs.umount(this.tmpdir);
fs.removeSync(this.tmpdir);
}
+13 -13
View File
@@ -12,12 +12,12 @@
},
"preferUnplugged": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.216.0",
"@aws-sdk/lib-storage": "^3.216.0",
"@aws-sdk/middleware-endpoint": "^3.215.0",
"@aws-sdk/s3-presigned-post": "^3.216.0",
"@aws-sdk/s3-request-presigner": "^3.216.0",
"@graphql-tools/schema": "^9.0.10",
"@aws-sdk/client-s3": "^3.226.0",
"@aws-sdk/lib-storage": "^3.226.0",
"@aws-sdk/middleware-endpoint": "^3.226.0",
"@aws-sdk/s3-presigned-post": "^3.226.0",
"@aws-sdk/s3-request-presigner": "^3.226.0",
"@graphql-tools/schema": "^9.0.12",
"@hydrooj/utils": "workspace:*",
"adm-zip": "0.5.5",
"cac": "^6.7.14",
@@ -30,16 +30,16 @@
"graphql-scalars": "1.20.1",
"isbinaryfile": "^5.0.0",
"js-yaml": "^4.1.0",
"koa": "^2.13.4",
"koa": "^2.14.1",
"koa-body": "^6.0.1",
"koa-compress": "^5.1.0",
"koa-proxies": "^0.12.2",
"koa-proxies": "^0.12.3",
"koa-router": "^12.0.0",
"koa-static-cache": "^5.1.4",
"lodash": "^4.17.21",
"lru-cache": "7.14.1",
"mime-types": "^2.1.35",
"moment-timezone": "^0.5.39",
"moment-timezone": "^0.5.40",
"mongodb": "^3.7.3",
"nanoid": "^4.0.0",
"nodemailer": "^6.8.0",
@@ -48,11 +48,11 @@
"path-to-regexp": "^6.2.1",
"require-resolve-hook": "^1.1.0",
"saslprep": "^1.0.3",
"schemastery": "^3.5.4",
"schemastery": "^3.6.1",
"semver": "^7.3.8",
"serialize-javascript": "^6.0.0",
"superagent": "^8.0.4",
"tar": "^6.1.12",
"superagent": "^8.0.6",
"tar": "^6.1.13",
"thirty-two": "^1.0.2",
"ws": "^8.11.0"
},
@@ -64,7 +64,7 @@
"@types/koa-compress": "^4.0.3",
"@types/koa-router": "^7.4.4",
"@types/koa-static-cache": "^5.1.1",
"@types/lodash": "^4.14.190",
"@types/lodash": "^4.14.191",
"@types/mime-types": "^2.1.1",
"@types/mongodb": "^3.6.20",
"@types/nodemailer": "^6.4.6",
+5
View File
@@ -41,6 +41,11 @@ langs:
time_limit_rate: Time limit rate
target: Compiler output file
display: Display name
bash:
display: Bash
code_file: foo.sh
execute: /bin/bash foo.sh
highlight: bash
c:
compile: /usr/bin/gcc -Wall --std=c99 -o foo foo.c -lm
highlight: c
+13 -8
View File
@@ -2,7 +2,7 @@ import AdmZip from 'adm-zip';
import { readFile, statSync } from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
import {
escapeRegExp, flattenDeep, intersection, isSafeInteger, pick,
escapeRegExp, flattenDeep, intersection, isSafeInteger, pick, uniqBy,
} from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
@@ -35,6 +35,7 @@ import * as bus from '../service/bus';
import {
Handler, param, post, query, route, Types,
} from '../service/server';
import { buildProjection } from '../utils';
import { registerResolver, registerValue } from './api';
import { ContestDetailBaseHandler } from './contest';
@@ -992,18 +993,22 @@ export class ProblemCreateHandler extends Handler {
export class ProblemPrefixListHandler extends Handler {
@param('prefix', Types.Name)
async get(domainId: string, prefix: string) {
const pdocs = await problem.getPrefixList(domainId, prefix);
if (!Number.isNaN(+prefix) && !pdocs.filter((i) => i.docId === +prefix)) {
const pdoc = await problem.get(domainId, +prefix, ['domainId', 'docId', 'pid', 'title']);
if (pdoc) pdocs.unshift(pdoc);
}
const projection = ['domainId', 'docId', 'pid', 'title'] as const;
const [pdocs, pdoc, apdoc] = await Promise.all([
problem.getPrefixList(domainId, prefix),
problem.get(domainId, Number.isSafeInteger(+prefix) ? +prefix : prefix, projection),
/^P\d+$/.test(prefix) ? problem.get(domainId, +prefix.substring(1), projection) : Promise.resolve(null),
]);
if (apdoc) pdocs.unshift(apdoc);
if (pdoc) pdocs.unshift(pdoc);
if (pdocs.length < 20) {
const search = global.Hydro.lib.problemSearch || defaultSearch;
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();
const docs = await problem.getMulti(domainId, { docId: { $in: result.hits.map((i) => +i.split('/')[1]) } })
.project(buildProjection(projection)).toArray();
pdocs.push(...docs);
}
this.response.body = pdocs;
this.response.body = uniqBy(pdocs, 'docId');
}
}
+5 -1
View File
@@ -12,6 +12,7 @@ import { sendMail } from '../lib/mail';
import { isEmail, isPassword } from '../lib/validator';
import BlackListModel from '../model/blacklist';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as ContestModel from '../model/contest';
import domain from '../model/domain';
import oauth from '../model/oauth';
import * as oplog from '../model/oplog';
@@ -356,9 +357,12 @@ class UserDetailHandler extends Handler {
}
}
const tags = Object.entries(acInfo).sort((a, b) => b[1] - a[1]).slice(0, 20);
const tsdocs = await ContestModel.getMultiStatus(domainId, { uid, attend: { $exists: true } }).project({ docId: 1 }).toArray();
const tdocs = await ContestModel.getMulti(domainId, { docId: { $in: tsdocs.map((i) => i.docId) } })
.project({ docId: 1, title: 1, rule: 1 }).sort({ _id: -1 }).toArray();
this.response.template = 'user_detail.html';
this.response.body = {
isSelfProfile, udoc, sdoc, pdocs, tags,
isSelfProfile, udoc, sdoc, pdocs, tags, tdocs,
};
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION)) {
const psdocs = await SolutionModel.getByUser(domainId, uid).limit(10).toArray();
+3
View File
@@ -69,6 +69,9 @@ export async function add(
payload.domainId!, payload.content!, payload.owner!, document.TYPE_DISCUSSION,
null, payload.parentType, payload.parentId, omit(payload, ['domainId', 'content', 'owner', 'parentType', 'parentId']),
);
await coll.insertOne({
domainId, docId: res, content, uid: owner, ip, time: new Date(),
});
payload.docId = res;
await bus.parallel('discussion/add', payload);
return payload.docId;
+1 -2
View File
@@ -330,8 +330,7 @@ export class ProblemModel {
}
static async getPrefixList(domainId: string, prefix: string) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const $regex = new RegExp(`^${escapeRegExp(prefix.toLowerCase())}`, 'i');
const filter = { $or: [{ pid: { $regex } }, { title: { $regex } }] };
return await document.getMulti(domainId, document.TYPE_PROBLEM, filter, ['domainId', 'docId', 'pid', 'title'])
.limit(20).toArray();
+1 -1
View File
@@ -60,7 +60,7 @@ export class StorageModel {
if (target.includes('..') || target.includes('//')) throw new Error('Invalid path');
if (target.length && !target.endsWith('/')) target += '/';
const results = await StorageModel.coll.find({
path: { $regex: new RegExp(`^${escapeRegExp(target)}${recursive ? '' : '[^/]+$'}`, 'i') },
path: { $regex: `^${escapeRegExp(target)}${recursive ? '' : '[^/]+$'}` },
autoDelete: null,
}).toArray();
return results.map((i) => ({
+1 -2
View File
@@ -367,8 +367,7 @@ class UserModel {
@ArgMethod
static async getPrefixList(domainId: string, prefix: string, limit: number = 50) {
prefix = prefix.toLowerCase();
const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi');
const $regex = `^${escapeRegExp(prefix.toLowerCase())}`;
const udocs = await coll.find({ unameLower: { $regex } })
.limit(limit).project({ _id: 1 }).toArray();
const users = [];
+6 -13
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-constant-condition */
/* eslint-disable no-await-in-loop */
import { ObjectID } from 'mongodb';
import Schema from 'schemastery';
@@ -29,10 +28,8 @@ export async function udoc(report) {
},
];
let bulk = db.collection('domain.user').initializeUnorderedBulkOp();
const cursor = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
while (true) {
const adoc = await cursor.next() as any;
if (!adoc) break;
const cursor = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
for await (const adoc of cursor) {
bulk.find({
domainId: adoc._id.domainId,
uid: adoc._id.uid,
@@ -61,10 +58,8 @@ export async function psdoc(report) {
},
},
];
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
while (true) {
const adoc = await data.next() as any;
if (!adoc) break;
const data = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
for await (const adoc of data) {
await document.setStatus(adoc._id.domainId, document.TYPE_PROBLEM, adoc._id.pid, adoc._id.uid, { nSubmit: adoc.nSubmit });
}
}
@@ -117,11 +112,9 @@ export async function pdoc(report) {
pipeline[2].$group[`s${i}`] = { $sum: `$s${i}` };
}
let bulk = db.collection('document').initializeUnorderedBulkOp();
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
const data = db.collection('record').aggregate<any>(pipeline, { allowDiskUse: true });
let cnt = 0;
while (true) {
const adoc = await data.next() as any;
if (!adoc) break;
for await (const adoc of data) {
const $set = {
nSubmit: adoc.nSubmit,
nAccept: adoc.nAccept,
+15 -17
View File
@@ -3,6 +3,7 @@
import { NumericDictionary, unionWith } from 'lodash';
import { FilterQuery } from 'mongodb';
import Schema from 'schemastery';
import { Counter } from '@hydrooj/utils';
import { Tdoc, Udoc } from '../interface';
import difficultyAlgorithm from '../lib/difficulty';
import rating from '../lib/rating';
@@ -100,21 +101,15 @@ export const RpTypes: Record<string, RpDef> = {
global.Hydro.model.rp = RpTypes;
export async function calcLevel(domainId: string, report: Function) {
const filter = { rp: { $gt: 0 } };
const ducnt = await domain.getMultiUserInDomain(domainId, filter).count();
await domain.setMultiUserInDomain(domainId, {}, { level: 0, rank: null });
if (!ducnt) return;
let last = { rp: null };
let rank = 0;
let count = 0;
const coll = db.collection('domain.user');
const filter = { rp: { $gt: 0 }, uid: { $nin: [0, 1], $gt: -1000 } };
const ducur = domain.getMultiUserInDomain(domainId, filter).project({ rp: 1 }).sort({ rp: -1 });
let bulk = coll.initializeUnorderedBulkOp();
// eslint-disable-next-line no-constant-condition
while (true) {
const dudoc = await ducur.next();
if (!dudoc) break;
if ([0, 1].includes(dudoc.uid)) continue;
for await (const dudoc of ducur) {
count++;
if (!dudoc.rp) dudoc.rp = null;
if (dudoc.rp !== last.rp) rank = count;
@@ -122,6 +117,7 @@ export async function calcLevel(domainId: string, report: Function) {
last = dudoc;
if (count % 100 === 0) report({ message: `#${count}: Rank ${rank}` });
}
if (!count) return;
await bulk.execute();
const levels = global.Hydro.model.builtin.LEVELS;
bulk = coll.initializeUnorderedBulkOp();
@@ -136,28 +132,30 @@ export async function calcLevel(domainId: string, report: Function) {
await bulk.execute();
}
async function runInDomain(id: string, report: Function) {
const info = await domain.get(id);
const domainIds = [id, ...(info.union || [])];
async function runInDomain(domainId: string, report: Function) {
const info = await domain.get(domainId);
const domainIds = [domainId, ...(info.union || [])];
const results: Record<keyof typeof RpTypes, ND> = {};
const udict = new Proxy({}, { get: (self, key) => self[key] || 0 });
const udict = Counter();
for (const type in RpTypes) {
results[type] = new Proxy({}, { get: (self, key) => self[key] || RpTypes[type].base });
await RpTypes[type].run(domainIds, results[type], report);
const bulk = db.collection('domain.user').initializeUnorderedBulkOp();
for (const uid in results[type]) {
const udoc = await UserModel.getById(id, +uid);
const udoc = await UserModel.getById(domainId, +uid);
if (!udoc?.hasPriv(PRIV.PRIV_USER_PROFILE)) continue;
await domain.updateUserInDomain(id, +uid, { $set: { [`rpInfo.${type}`]: results[type][uid] } });
bulk.find({ domainId, uid: +uid }).updateOne({ $set: { [`rpInfo.${type}`]: results[type][uid] } });
udict[+uid] += results[type][uid];
}
if (bulk.length) await bulk.execute();
}
await domain.setMultiUserInDomain(id, {}, { rp: 0 });
await domain.setMultiUserInDomain(domainId, {}, { rp: 0 });
const bulk = db.collection('domain.user').initializeUnorderedBulkOp();
for (const uid in udict) {
bulk.find({ domainId: id, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
bulk.find({ domainId, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
}
if (bulk.length) await bulk.execute();
await calcLevel(id, report);
await calcLevel(domainId, report);
}
export async function run({ domainId }, report: Function) {
+8 -2
View File
@@ -17,6 +17,7 @@ interface MongoConfig {
port?: string,
name?: string,
url?: string,
uri?: string,
prefix?: string,
}
@@ -29,7 +30,7 @@ class MongoService {
let mongourl = `${opts.protocol || 'mongodb'}://`;
if (opts.username) mongourl += `${opts.username}:${encodeURIComponent(opts.password)}@`;
mongourl += `${opts.host}:${opts.port}/${opts.name}`;
if (opts.url) mongourl = opts.url;
if (opts.url || opts.uri) mongourl = opts.url || opts.uri;
return mongourl;
}
@@ -42,7 +43,12 @@ class MongoService {
mongourl = mongod.getUri();
}
this.opts = opts;
this.client = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
this.client = await MongoClient.connect(mongourl, {
useNewUrlParser: true,
useUnifiedTopology: true,
readPreference: 'nearest',
writeConcern: 'majority',
});
this.db = this.client.db(opts.name || 'hydro');
await bus.parallel('database/connect', this.db);
}
+1 -1
View File
@@ -10,7 +10,7 @@ export type NestKeys<O, T = any, S = any[]> = O extends object ? O extends S ? n
export type Value<O, V = ''> = {
[K in keyof O]: V
};
export type Projection<O> = (string & keyof O)[];
export type Projection<O> = readonly (string & keyof O)[];
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Filter<T, U> = T extends U ? T : never;
export type MaybeArray<T> = T | T[];
+1 -1
View File
@@ -1,4 +1,4 @@
export function buildProjection(fields: Array<string | number>): Record<string, 1> {
export function buildProjection(fields: readonly (string | number)[]): Record<string, 1> {
const o = {};
for (const k of fields) o[k] = 1;
return o;
+76 -77
View File
@@ -1,82 +1,81 @@
(() => {
const { AutoloadPage } = window.Hydro;
const { $ } = window.node_modules;
import { $ } from '@hydrooj/ui-default';
let loaded = false;
async function load() {
if (loaded) return Promise.resolve();
return new Promise((resolve, reject) => {
const scriptElement = document.createElement('script');
scriptElement.src = UiContext.onlyofficeApi;
scriptElement.async = true;
document.head.appendChild(scriptElement);
scriptElement.onload = resolve;
scriptElement.onerror = reject;
loaded = true;
});
}
const { AutoloadPage } = window.Hydro;
const loader = mode => async (element) => {
const id = `${mode}-${Math.random().toString()}`;
$(element).attr('id', id);
const url = $(element).text();
const t = new URL(url, window.location.href).pathname.split('.');
const n = new URL(url, window.location.href).pathname.split('/');
const lang = UserContext.viewLang.includes('_') ? UserContext.viewLang.split('_')[0] : UserContext.viewLang;
// eslint-disable-next-line no-undef
window.editor = new DocsAPI.DocEditor(id, {
document: {
fileType: t[t.length - 1],
key: Math.random().toString(16),
title: decodeURIComponent(n[n.length - 1]),
url: new URL(url, window.location.href),
permissions: {
comment: false,
copy: true,
download: true,
edit: false,
fillForms: false,
modifyContentControl: false,
modifyFilter: false,
print: true,
protect: false,
review: false,
},
},
editorConfig: {
lang,
mode: 'view',
user: {
group: "Hydro",
id: UserContext._id.toString(),
name: UserContext.uname,
},
customization: {
chat: false,
comments: false,
help: false,
hideRulers: true,
plugins: false,
}
},
documentType: mode,
height: mode === 'slide' ? '560px' : '900px',
});
}
let loaded = false;
async function load() {
if (loaded) return Promise.resolve();
return new Promise((resolve, reject) => {
const scriptElement = document.createElement('script');
scriptElement.src = UiContext.onlyofficeApi;
scriptElement.async = true;
document.head.appendChild(scriptElement);
scriptElement.onload = resolve;
scriptElement.onerror = reject;
loaded = true;
});
}
const getEles = types => {
const eles = [];
for (const type of types) eles.push(...$('div[data-' + type + ']').get());
return eles;
}
const loader = (mode) => async (element) => {
const id = `${mode}-${Math.random().toString()}`;
$(element).attr('id', id);
const url = $(element).text();
const t = new URL(url, window.location.href).pathname.split('.');
const n = new URL(url, window.location.href).pathname.split('/');
const lang = UserContext.viewLang.includes('_') ? UserContext.viewLang.split('_')[0] : UserContext.viewLang;
// eslint-disable-next-line no-undef
window.editor = new DocsAPI.DocEditor(id, {
document: {
fileType: t[t.length - 1],
key: Math.random().toString(16),
title: decodeURIComponent(n[n.length - 1]),
url: new URL(url, window.location.href),
permissions: {
comment: false,
copy: true,
download: true,
edit: false,
fillForms: false,
modifyContentControl: false,
modifyFilter: false,
print: true,
protect: false,
review: false,
},
},
editorConfig: {
lang,
mode: 'view',
user: {
group: 'Hydro',
id: UserContext._id.toString(),
name: UserContext.uname,
},
customization: {
chat: false,
comments: false,
help: false,
hideRulers: true,
plugins: false,
},
},
documentType: mode,
height: mode === 'slide' ? '560px' : '900px',
});
};
const page = new AutoloadPage('onlyoffice', async () => {
let all = getEles(['doc', 'docx', 'cell', 'xls', 'xlsx', 'slide', 'ppt', 'pptx']);
if (all.length) await load();
getEles(['doc', 'docx']).forEach(loader('word'));
getEles(['cell', 'xls', 'xlsx']).forEach(loader('cell'))
getEles(['slide', 'ppt', 'pptx']).forEach(loader('slide'));
});
const getEles = (types) => {
const eles = [];
for (const type of types) eles.push(...$(`div[data-${type}]`).get());
return eles;
};
window.Hydro.extraPages.push(page);
})();
const page = new AutoloadPage('onlyoffice', async () => {
const all = getEles(['doc', 'docx', 'cell', 'xls', 'xlsx', 'slide', 'ppt', 'pptx']);
if (all.length) await load();
getEles(['doc', 'docx']).forEach(loader('word'));
getEles(['cell', 'xls', 'xlsx']).forEach(loader('cell'));
getEles(['slide', 'ppt', 'pptx']).forEach(loader('slide'));
});
window.Hydro.extraPages.push(page);
+62
View File
@@ -0,0 +1,62 @@
export * from './utils';
export { default as Notification } from './components/notification';
export * from './components/dialog';
export * as bus from './bus';
export { default as loadMonaco } from './components/monaco/loader';
export * as redux from 'react-redux';
export * from './components/zipDownloader';
export { default as $ } from 'jquery';
export { default as _ } from 'lodash';
export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom/client';
export * from './misc/Page';
export default async function load(name: string) {
if (window.node_modules[name]) return window.node_modules[name];
if (name === 'echarts') return import('echarts');
if (name === 'moment') return import('moment');
throw new Error(`Module ${name} not found`);
}
import AutoComplete from './components/autocomplete';
import CustomSelectAutoComplete from './components/autocomplete/CustomSelectAutoComplete';
import DomainSelectAutoComplete from './components/autocomplete/DomainSelectAutoComplete';
import ProblemSelectAutoComplete from './components/autocomplete/ProblemSelectAutoComplete';
import UserSelectAutoComplete from './components/autocomplete/UserSelectAutoComplete';
export {
load, AutoComplete, UserSelectAutoComplete, ProblemSelectAutoComplete, DomainSelectAutoComplete, CustomSelectAutoComplete,
};
export const { UserContext, UiContext } = window;
export function addPage(page: import('./misc/Page').Page | (() => Promise<void> | void)) {
window.Hydro.extraPages.push(page);
}
declare global {
interface Window {
LANGS: Record<string, any>;
}
let UserContext: Record<string, any>;
let UiContext: Record<string, any>;
}
// Below are old version api compat
/* eslint-disable import/order */
import $ from 'jquery';
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as redux from 'react-redux';
const modules = {
_, $, React, redux, ReactDOM, load,
};
declare global {
interface Window {
node_modules: typeof modules;
}
}
Object.assign(window, { node_modules: modules, $, jQuery: $ });
@@ -1,4 +1,3 @@
/* eslint-disable */
import path from 'path';
export default function root(fn = '.') {
+2 -1
View File
@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
import type * as monaco from 'monaco-editor';
// eslint-disable-next-line @typescript-eslint/naming-convention
const _hooks: Record<keyof any, Array<(...args: any[]) => any>> = {};
@@ -11,7 +12,7 @@ export type Disposable = () => void;
export type VoidReturn = Promise<any> | any;
export interface EventMap extends Record<string, any> {
'scratchpadEditorCreate': (editor, monaco) => any;
'scratchpadEditorCreate': (editor: monaco.editor.IStandaloneCodeEditor, monaco: typeof import('monaco-editor')) => any;
}
function getHooks<K extends keyof EventMap>(name: K) {
@@ -23,7 +23,7 @@ function monitorResource(resource) {
}
}
export default class DOMAttachedObject {
class DOMAttachedObject {
static uniqueIdCounter = 0;
static DOMAttachKey: string;
static DOMAttachSelector: string;
@@ -127,3 +127,5 @@ export default class DOMAttachedObject {
if (monitorDetach) monitorResource(this);
}
}
export default DOMAttachedObject as typeof DOMAttachedObject & DOMAttachedObject;
@@ -1,7 +1,7 @@
import type { DomainDoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoCompleteProps<DomainDoc>>((props, ref) => (
@@ -1,8 +1,7 @@
import type { ProblemDoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import api, { gql } from 'vj/utils/api';
import request from 'vj/utils/request';
import { api, gql, request } from 'vj/utils';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
@@ -1,7 +1,7 @@
import type { Udoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import api, { gql } from 'vj/utils/api';
import { api, gql } from 'vj/utils';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
// eslint-disable-next-line prefer-arrow-callback
@@ -1,6 +1,5 @@
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
import { InfoDialog } from './dialog';
function isSupported() {
@@ -1,7 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
import tpl from 'vj/utils/tpl';
import { tpl } from 'vj/utils';
export default class Calendar {
constructor(events) {
@@ -1,9 +1,7 @@
import Clipboard from 'clipboard';
import Notification from 'vj/components/notification';
import { AutoloadPage } from 'vj/misc/Page';
import base64 from 'vj/utils/base64';
import i18n from 'vj/utils/i18n';
import substitute from 'vj/utils/substitute';
import { base64, i18n, substitute } from 'vj/utils';
export default new AutoloadPage('clipboard', () => {
$('[data-copy]').get().forEach((el) => {
@@ -1,9 +1,7 @@
import $ from 'jquery';
import Notification from 'vj/components/notification';
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { delay, i18n, request } from 'vj/utils';
const contestPage = new AutoloadPage('contestPage', () => {
$('[data-contest-code]').on('click', (ev) => {
@@ -1,7 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import zIndexManager from 'vj/utils/zIndexManager';
import { zIndexManager } from 'vj/utils';
export interface DialogOptions {
classes: string;
@@ -1,6 +1,5 @@
import $ from 'jquery';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
import DomDialog, { DialogOptions } from './DomDialog';
export class Dialog {
@@ -3,7 +3,7 @@ import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import TextareaHandler from 'vj/components/editor/textareaHandler';
import Notification from 'vj/components/notification';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
let initialized = false;
const $template = $('.dczcomments__box').eq(0).clone();
@@ -4,11 +4,9 @@ import $ from 'jquery';
import { ConfirmDialog } from 'vj/components/dialog';
import CommentBox from 'vj/components/discussion/CommentBox';
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { slideDown, slideUp } from 'vj/utils/slide';
import tpl from 'vj/utils/tpl';
import {
delay, i18n, request, slideDown, slideUp, tpl,
} from 'vj/utils';
const $replyTemplate = $('.commentbox-container').eq(0).clone();
@@ -5,9 +5,7 @@ import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import TimeAgo from 'timeago-react';
import { InfoDialog } from 'vj/components/dialog';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import { i18n, request, tpl } from 'vj/utils';
async function historyDialog(payload, time, uid) {
const ts = new Date(time).getTime();
@@ -6,7 +6,7 @@ import { chunk } from 'lodash';
import * as React from 'react';
import ReactDOM from 'react-dom/client';
import { AutoloadPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
function renderReactions(reactions, self, rootEle) {
let html = '';
@@ -2,8 +2,7 @@ import _ from 'lodash';
import Drop from 'tether-drop';
import responsiveCutoff from 'vj/breakpoints.json';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import { isBelow } from 'vj/utils/mediaQuery';
import zIndexManager from 'vj/utils/zIndexManager';
import { mediaQuery, zIndexManager } from 'vj/utils';
export default class Dropdown extends DOMAttachedObject {
static DOMAttachKey = 'vjDropdownInstance';
@@ -12,7 +11,7 @@ export default class Dropdown extends DOMAttachedObject {
constructor($dom, options = {}) {
if ($dom.attr('data-dropdown-trigger-desktop-only') !== undefined) {
if (isBelow(responsiveCutoff.mobile)) {
if (mediaQuery.isBelow(responsiveCutoff.mobile)) {
super(null);
return;
}
@@ -1,5 +1,5 @@
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import { delay } from 'vj/utils';
import CmEditor from '.';
function runSubstitute($container: JQuery<Document | HTMLElement>) {
@@ -3,8 +3,7 @@ import _ from 'lodash';
import { nanoid } from 'nanoid';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import Notification from 'vj/components/notification';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { i18n, request } from 'vj/utils';
export const config = {
toolbar: [
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import { delay } from 'vj/utils';
const formPage = new AutoloadPage('formPage', () => {
$(document).on('vjFormDisableUpdate', 'input, select, textarea', (ev) => {
@@ -1,7 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
const highlighterPage = new AutoloadPage('highlighterPage', () => {
import('./prismjs').then(({ default: prismjs }) => {
@@ -11,7 +11,7 @@ import $ from 'jquery';
import components from 'prismjs/components';
import getLoader from 'prismjs/dependencies';
import Notification from 'vj/components/notification/index';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import languageMeta from './meta';
const files = require.context('prismjs/components/', true, /prism-[a-z0-9-]+\.js/);
+1 -2
View File
@@ -1,7 +1,6 @@
import $ from 'jquery';
import { InfoDialog } from 'vj/components/dialog';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
export default function createHint(message: string, element?: any) {
if (i18n(message) === message || !element) return;
@@ -1,8 +1,6 @@
import $ from 'jquery';
import { NamedPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import { i18n, request, tpl } from 'vj/utils';
export default new NamedPage('homepage', () => {
function getHitokoto($containers) {
@@ -1,7 +1,7 @@
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom/client';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
export default function LanguageSelect({
fieldSelector, firstLoadMain, firstLoadSub, availableLangs, mainLangs,
@@ -1,8 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import Tooltip from 'vj/components/tooltip/Tooltip';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import { delay, i18n } from 'vj/utils';
const MARKER_ID = `marker_${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`;
const MARKER_OFFSET = 20;
@@ -1,7 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import delay from 'vj/utils/delay';
import { delay } from 'vj/utils';
import Marker from './Marker';
export default class MarkerReactive extends DOMAttachedObject {
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
export default new AutoloadPage('media', async () => {
async function parseMedia($dom = $(document.body)) {
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import tpl from 'vj/utils/tpl';
import { tpl } from 'vj/utils';
const menuHeadingPage = new AutoloadPage('menuHeadingPage', null, () => {
$('[data-heading-extract-to]').get().forEach((container) => {
@@ -1,7 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import { slideDown } from 'vj/utils/slide';
import { delay, slideDown } from 'vj/utils';
function expandMenu($menu) {
slideDown($menu, 500, { opacity: 0 }, { opacity: 1 });
@@ -4,8 +4,7 @@ import { InfoDialog } from 'vj/components/dialog';
import VjNotification from 'vj/components/notification/index';
import { FLAG_ALERT } from 'vj/constant/message';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
const onmessage = (msg) => {
console.log('Received message', msg);
@@ -6,8 +6,7 @@ import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import TimeAgo from 'timeago-react';
import i18n from 'vj/utils/i18n';
import { parse as parseMongoId } from 'vj/utils/mongoId';
import { i18n, mongoId } from 'vj/utils';
import Message from './MessageComponent';
const mapStateToProps = (state) => ({
@@ -70,7 +69,7 @@ export default connect(mapStateToProps)(class MessagePadDialogueContentContainer
renderInner() {
if (this.props.activeId === null) return [];
const sorted = this.props.item.messages
.sort((msg1, msg2) => parseMongoId(msg1._id).timestamp - parseMongoId(msg2._id).timestamp);
.sort((msg1, msg2) => mongoId(msg1._id).timestamp - mongoId(msg2._id).timestamp);
return sorted.map((msg) => (
<Message
key={msg._id}
@@ -82,8 +81,8 @@ export default connect(mapStateToProps)(class MessagePadDialogueContentContainer
}
>
<div>{this.renderContent(msg)}</div>
<time data-tooltip={moment(parseMongoId(msg._id).timestamp * 1000).format('YYYY-MM-DD HH:mm:ss')}>
<TimeAgo datetime={parseMongoId(msg._id).timestamp * 1000} locale={i18n('timeago_locale')} />
<time data-tooltip={moment(mongoId(msg._id).timestamp * 1000).format('YYYY-MM-DD HH:mm:ss')}>
<TimeAgo datetime={mongoId(msg._id).timestamp * 1000} locale={i18n('timeago_locale')} />
</time>
</Message>
));
@@ -4,7 +4,7 @@ import $ from 'jquery';
import _ from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import ListItem from './DialogueListItemComponent';
const mapStateToProps = (state) => ({
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'vj/components/react/IconComponent';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
const mapStateToProps = (state) => ({
activeId: state.activeId,
@@ -2,8 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'vj/components/react/IconComponent';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { i18n, request } from 'vj/utils';
import MessagePadDialogueContent from './MessagePadDialogueContentContainer';
import MessagePadDialogueList from './MessagePadDialogueListContainer';
import MessagePadInput from './MessagePadInputContainer';
@@ -6,8 +6,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickInput';
import list from 'monaco-themes/themes/themelist.json';
import { nanoid } from 'nanoid';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { i18n, request } from 'vj/utils';
export default monaco;
export const customOptions: monaco.editor.IStandaloneDiffEditorConstructionOptions = JSON.parse(localStorage.getItem('editor.config') || '{}');
@@ -1,7 +1,7 @@
import keyword from 'emojis-keywords';
import list from 'emojis-list';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import api, { gql } from 'vj/utils/api';
import { api, gql } from 'vj/utils';
const qqEmojies = [
'weixiao',
@@ -1,4 +1,4 @@
import loadExternalModule from 'vj/utils/loadModule';
import { loadExternalModule } from 'vj/utils';
let loaded;
const loaders = {
@@ -3,9 +3,7 @@ import Slideout from 'slideout';
import Notification from 'vj/components/notification';
import selectUser from 'vj/components/selectUser';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import { i18n, request, tpl } from 'vj/utils';
function handleNavLogoutClick(ev) {
const $logoutLink = $(ev.currentTarget);
@@ -1,7 +1,6 @@
import { Intent, Position, Toaster } from '@blueprintjs/core';
import $ from 'jquery';
import tpl from 'vj/utils/tpl';
import zIndexManager from 'vj/utils/zIndexManager';
import { tpl, zIndexManager } from 'vj/utils/base';
const ToasterContainer = document.createElement('div');
ToasterContainer.style.position = 'fixed';
@@ -1,6 +1,6 @@
import Notification from 'vj/components/notification/index';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
export default new AutoloadPage('notificationPage', (pagename) => {
const message = i18n(`Hint::Page::${pagename}`);
@@ -4,10 +4,9 @@ import { nanoid } from 'nanoid';
import { ActionDialog, InfoDialog } from 'vj/components/dialog/index';
import Notification from 'vj/components/notification';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import pjax from 'vj/utils/pjax';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import {
i18n, pjax, request, tpl,
} from 'vj/utils';
async function startEdit(filename, value, fileCategory = 'file') {
const { default: Editor } = await import('vj/components/editor/index');
@@ -1,8 +1,7 @@
import $ from 'jquery';
import { InfoDialog } from 'vj/components/dialog';
import { NamedPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
export default new NamedPage(['problem_create', 'problem_edit'], () => {
$('input[name="pid"]').on('blur', () => {
@@ -1,6 +1,6 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
import type { RootState } from './reducer/index';
@@ -3,7 +3,7 @@ import {
} from '@blueprintjs/core';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
import { FormItem } from './BasicForm';
@@ -3,7 +3,7 @@ import {
} from '@blueprintjs/core';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import { FormItem, ManagedSelect, SingleFileSelect } from './BasicForm';
import type { RootState } from './reducer/index';
@@ -4,7 +4,7 @@ import type { SubtaskConfig } from 'hydrooj/src/interface';
import { isEqual, pick } from 'lodash';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
import { FormItem } from './BasicForm';
import { RootState } from './reducer';
@@ -4,7 +4,7 @@ import type { SubtaskConfig } from 'hydrooj/src/interface';
import { isEqual } from 'lodash';
import React, { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
import type { RootState } from './reducer/index';
@@ -1,7 +1,7 @@
import $ from 'jquery';
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import delay from 'vj/utils/delay';
import { delay } from 'vj/utils';
const ANIMATION_DURATION = 4000;
@@ -2,7 +2,7 @@ import AnsiUp from 'ansi_up';
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'vj/components/react/IconComponent';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import DataInput from './DataInputComponent';
import Panel from './PanelComponent';
@@ -2,7 +2,7 @@ import classNames from 'classnames';
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'vj/components/react/IconComponent';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
import Panel from './PanelComponent';
import ScratchpadRecordsRow from './ScratchpadRecordsRowContainer';
@@ -5,10 +5,9 @@ import React from 'react';
import { connect } from 'react-redux';
import TimeAgo from 'timeago-react';
import * as recordEnum from 'vj/constant/record';
import emulateAnchorClick from 'vj/utils/emulateAnchorClick';
import i18n from 'vj/utils/i18n';
import { parse as parseMongoId } from 'vj/utils/mongoId';
import substitute from 'vj/utils/substitute';
import {
emulateAnchorClick, i18n, mongoId, substitute,
} from 'vj/utils';
const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status];
@@ -58,7 +57,7 @@ export default connect(mapStateToProps, null, mergeProps)(class ScratchpadRecord
render() {
const { data } = this.props;
const submitAt = parseMongoId(data._id).timestamp * 1000;
const submitAt = mongoId(data._id).timestamp * 1000;
// Is pretest
return data.contest?.toString() === '000000000000000000000000' ? null : (
<tr onClick={(ev) => this.handleRowClick(ev, data._id)}>
@@ -4,9 +4,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'vj/components/react/IconComponent';
import getAvailableLangs from 'vj/utils/availableLangs';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import { getAvailableLangs, i18n, request } from 'vj/utils';
import Toolbar, {
ToolbarButtonComponent as ToolbarButton,
ToolbarItemComponent as ToolbarItem,
@@ -1,5 +1,5 @@
import Notification from 'vj/components/notification';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
export default function reducer(state = {
main: {
+1 -2
View File
@@ -1,7 +1,6 @@
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
import { ActionDialog } from 'vj/components/dialog';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { i18n, tpl } from 'vj/utils';
import createHint from './hint';
let hintInserted = false;
@@ -3,9 +3,7 @@ import responsiveCutoff from 'vj/breakpoints.json';
import DomDialog from 'vj/components/dialog/DomDialog';
import { InfoDialog } from 'vj/components/dialog/index';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import { isBelow } from 'vj/utils/mediaQuery';
import tpl from 'vj/utils/tpl';
import { i18n, mediaQuery, tpl } from 'vj/utils';
const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
const signInDialog = DomDialog.getOrConstruct($('.dialog--signin'), {
@@ -17,7 +15,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
if ($('[name="nav_login"]').length > 0) {
// nav
$('[name="nav_login"]').on('click', (ev) => {
if (isBelow(responsiveCutoff.mobile)) return;
if (mediaQuery.isBelow(responsiveCutoff.mobile)) return;
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
signInDialog.show();
ev.preventDefault();
@@ -32,7 +30,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
}
window.showSignInDialog = () => {
if (isBelow(responsiveCutoff.mobile)) {
if (mediaQuery.isBelow(responsiveCutoff.mobile)) {
if ($('[name="nav_login"]').length > 0) {
window.location.href = $('[name="nav_login"]').attr('href');
return;
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
function setStarButtonState($starButton, star) {
if (star) {
@@ -1,7 +1,7 @@
import $ from 'jquery';
import * as timeago from 'timeago.js';
import { AutoloadPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import { i18n } from 'vj/utils';
try {
const locales = require.context('timeago.js/lib/lang', false, /\.js$/);
+3 -4
View File
@@ -1,9 +1,8 @@
import { Dialog } from 'vj/components/dialog/index';
import Notification from 'vj/components/notification';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import pjax from 'vj/utils/pjax';
import request from 'vj/utils/request';
import {
delay, i18n, pjax, request,
} from 'vj/utils';
function onBeforeUnload(e) {
e.returnValue = '';
@@ -1,7 +1,7 @@
import $ from 'jquery';
import Rotator from 'vj/components/rotator';
import { AutoloadPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
function setVoteState($container, value, status) {
const $num = $container.find('.vote-number');
@@ -2,11 +2,9 @@ import { dump } from 'js-yaml';
import PQueue from 'p-queue';
import streamsaver from 'streamsaver';
import Notification from 'vj/components/notification';
import api, { gql } from 'vj/utils/api';
import i18n from 'vj/utils/i18n';
import pipeStream from 'vj/utils/pipeStream';
import request from 'vj/utils/request';
import { createZipStream } from 'vj/utils/zip';
import {
api, createZipStream, gql, i18n, pipeStream, request,
} from 'vj/utils';
let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext;
function onBeforeUnload(e) {
@@ -29,8 +27,8 @@ export default async function download(filename, targets) {
await waitForWritableStream;
const fileStream = streamsaver.createWriteStream(filename);
const queue = new PQueue({ concurrency: 5 });
const abortCallbackReceiver = {};
function stopDownload() { abortCallbackReceiver.abort(); }
const abortCallbackReceiver: any = {};
function stopDownload() { abortCallbackReceiver.abort?.(); }
let i = 0;
async function downloadFile(target) {
try {
+4 -3
View File
@@ -46,10 +46,11 @@ document.addEventListener('DOMContentLoaded', async () => {
// eslint-disable-next-line camelcase
try { __webpack_public_path__ = UiContext.cdn_prefix; } catch (e) { }
const [data] = await Promise.all([
(await fetch(`/constant/${UiContext.constantVersion}`, { cache: 'force-cache' })).json(),
await import('./modules'),
const [data, HydroExports] = await Promise.all([
fetch(`/constant/${UiContext.constantVersion}`, { cache: 'force-cache' }).then((r) => r.json()),
import('./api'),
]);
Object.assign(window, { HydroExports });
eval(data[0]); // eslint-disable-line no-eval
data.shift();
window.Hydro.preload = data;
+2 -3
View File
@@ -4,16 +4,15 @@ import $ from 'jquery';
import _ from 'lodash';
import Notification from 'vj/components/notification';
import PageLoader from 'vj/misc/PageLoader';
import delay from 'vj/utils/delay';
import { delay } from 'vj/utils';
declare global {
interface Window {
UserContext: any;
UiContext: any;
Hydro: any;
// eslint-disable-next-line camelcase
node_modules: any;
externalModules: Record<string, string>;
captureException?: (e: Error) => void;
}
}
+16 -1
View File
@@ -51,7 +51,22 @@ export async function buildUI() {
target: [
'chrome60',
],
plugins: global.Hydro.ui.esbuildPlugins || [],
plugins: [
...(global.Hydro.ui.esbuildPlugins || []),
{
name: 'federation',
setup(b) {
b.onResolve({ filter: /^@hydrooj\/ui-default/ }, () => ({
path: 'api',
namespace: 'ui-default',
}));
b.onLoad({ filter: /.*/, namespace: 'ui-default' }, () => ({
contents: 'module.exports = window.HydroExports;',
loader: 'tsx',
}));
},
},
],
minify: !process.env.DEV,
});
if (build.errors.length) console.error(build.errors);
+1
View File
@@ -83,6 +83,7 @@ API: API
Argument {0} is invalid.: 非法的参数 {0} 。
Arguments: 参数
Assign: 分配给
Assignment: 作业
At least 4 characters: 至少 4 个字符
Attend Contest: 参加比赛
Attend contests: 参加比赛
-37
View File
@@ -1,37 +0,0 @@
import './utils/delay';
import './utils/emulateAnchorClick';
import './utils/i18n';
import './utils/loadReactRedux';
import './utils/mediaQuery';
import './utils/mongoId';
import './utils/pipeStream';
import './utils/pjax';
import './utils/request';
import './utils/slide';
import './utils/substitute';
import './utils/tpl';
import './utils/zIndexManager';
import './utils/zip';
import './components/autocomplete';
import './components/dialog';
import './components/notification';
import './components/monaco/loader';
import $ from 'jquery';
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as redux from 'react-redux';
const modules = {
_, $, React, redux, ReactDOM,
};
export default async function load(name) {
if (modules[name]) return modules[name];
if (name === 'echarts') return import('echarts');
if (name === 'moment') return import('moment');
throw new Error(`Module ${name} not found`);
}
window.node_modules = { ...modules, load };
window.$ = $;
window.jQuery = $;
+12 -27
View File
@@ -4,14 +4,15 @@
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "index.ts",
"types": "api.ts",
"repository": "https://github.com/hydro-dev/Hydro.git",
"preferUnplugged": true,
"scripts": {
"lint": "eslint"
},
"devDependencies": {
"@blueprintjs/core": "4.12.0",
"@blueprintjs/select": "4.8.11",
"@blueprintjs/core": "4.13.0",
"@blueprintjs/select": "4.8.12",
"@fontsource/dm-mono": "^4.5.10",
"@fontsource/fira-code": "^4.5.12",
"@fontsource/inconsolata": "^4.5.9",
@@ -29,29 +30,25 @@
"@types/markdown-it": "^12.2.3",
"@types/pickadate": "^3.5.32",
"@types/qrcode": "^1.5.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/redux-logger": "^3.0.9",
"@types/serviceworker": "^0.0.57",
"@types/sharedworker": "^0.0.86",
"@types/serviceworker": "^0.0.58",
"@types/sharedworker": "^0.0.87",
"@types/webpack-env": "^1.18.0",
"@vscode/codicons": "^0.0.32",
"autoprefixer": "^10.4.13",
"browser-update": "^3.3.44",
"chalk": "^5.1.2",
"chalk": "^5.2.0",
"classnames": "^2.3.2",
"clean-webpack-plugin": "^4.0.0",
"clipboard": "^2.0.11",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.2",
"diff": "^5.1.0",
"diff-dom": "^4.2.8",
"echarts": "^5.4.0",
"echarts": "^5.4.1",
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^2.20.0",
"fancy-log": "^2.0.0",
"flatpickr": "^4.6.13",
"friendly-errors-webpack-plugin": "^1.7.0",
"graphiql": "2.2.0",
"gulp": "^4.0.2",
"gulp-iconfont": "^11.0.1",
@@ -63,11 +60,9 @@
"jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2",
"mini-css-extract-plugin": "^2.7.0",
"moment": "^2.29.4",
"monaco-editor": "0.33.0",
"monaco-editor-nls": "^2.0.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"monaco-themes": "^0.4.3",
"monaco-yaml": "^4.0.2",
"nanoid": "^4.0.0",
@@ -75,8 +70,7 @@
"nprogress": "^0.2.0",
"p-queue": "^7.3.0",
"pickadate": "^3.6.4",
"plugin-error": "^2.0.0",
"postcss-loader": "^7.0.1",
"plugin-error": "^2.0.1",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"prop-types": "^15.8.1",
@@ -97,23 +91,14 @@
"rupture": "^0.7.1",
"slideout": "^1.0.1",
"sticky-kit": "^1.1.3",
"style-loader": "^3.3.1",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.0",
"tether": "1.4.7",
"tether-drop": "^1.4.2",
"through2": "^4.0.2",
"timeago-react": "^3.0.5",
"timeago.js": "^4.0.2",
"ts-loader": "^9.4.1",
"vditor": "^3.8.18",
"vinyl-buffer": "^1.0.1",
"web-streams-polyfill": "^3.2.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^5.0.0",
"webpackbar": "^5.0.2"
"web-streams-polyfill": "^3.2.1"
},
"dependencies": {
"ansi_up": "^5.1.0",
@@ -121,11 +106,11 @@
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"katex": "^0.16.3",
"katex": "^0.16.4",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.5",
"markdown-it-attrs": "^4.1.4",
"markdown-it-attrs": "^4.1.6",
"markdown-it-footnote": "^3.0.3",
"markdown-it-imsize": "^2.0.1",
"markdown-it-mark": "^3.0.1",
+1 -1
View File
@@ -2,7 +2,7 @@ import { pick } from 'lodash';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import { request } from 'vj/utils';
const defaultQuery = `\
query Example(
+1 -1
View File
@@ -1,7 +1,7 @@
import { formatSeconds } from '@hydrooj/utils/lib/common';
import NProgress from 'nprogress';
import { NamedPage } from 'vj/misc/Page';
import tpl from 'vj/utils/tpl';
import { tpl } from 'vj/utils';
const contestTimer = $(tpl`<pre class="contest-timer" style="display:none"></pre>`);
contestTimer.appendTo(document.body);
@@ -2,9 +2,7 @@ import $ from 'jquery';
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
import { ConfirmDialog } from 'vj/components/dialog';
import { NamedPage } from 'vj/misc/Page';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import { i18n, request, tpl } from 'vj/utils';
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
@@ -4,10 +4,9 @@ import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoCom
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
import Notification from 'vj/components/notification';
import { NamedPage } from 'vj/misc/Page';
import api, { gql } from 'vj/utils/api';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import {
api, delay, gql, i18n, tpl,
} from 'vj/utils';
function update(name: string, uids: number[]) {
return api(gql`
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more