Merge branch 'master' into events

This commit is contained in:
undefined committed 2025-06-09 23:22:58 +08:00
commit 067ca3c6bb
57 files changed
+381 -229

No files matched your search

+22 -9
View File
@@ -32,7 +32,7 @@ Hydro 设计了一套模块化的插件系统,可以方便地扩展系统功
使用插件系统,可以在修改功能后,仍然保证系统的可升级性。 使用插件系统,可以在修改功能后,仍然保证系统的可升级性。
Hydro 的所有历史版本均可平滑升级到最新版本。 Hydro 的所有历史版本均可平滑升级到最新版本。
插件使用和开发指南,请前往文档 [插件](https://docs.hydro.ac/plugins/) 和 [开发](https://docs.hydro.ac/dev/typescript/) 章节。 插件使用和开发指南,请前往文档 [插件](https://docs.hydro.ac/docs/Hydro/plugins) 和 [开发](https://docs.hydro.ac/docs/Hydro/dev/typescript) 章节。
### 跨平台兼容,数据一键备份/导入 ### 跨平台兼容,数据一键备份/导入
@@ -51,7 +51,7 @@ Hydro 提供了单系统多空间支持,可以方便地为不同的班级/年
### 粒度精细的权限系统,灵活调节 ### 粒度精细的权限系统,灵活调节
Hydro 的权限可以按比赛/作业分配给对应的用户,也可以将用户分组(班级),按组分配权限。 Hydro 的权限可以按比赛/作业分配给对应的用户,也可以将用户分组(班级),按组分配权限。
有关权限节点,可以查看 [介绍](https://docs.hydro.ac/docs/) 下方截图。 有关权限节点,可以查看 [介绍](https://docs.hydro.ac/docs/Hydro) 下方截图。
### 规模化支持,上千用户无压力,伸缩组秒级自动扩展 ### 规模化支持,上千用户无压力,伸缩组秒级自动扩展
@@ -111,7 +111,9 @@ Hydro 用户群:1085853538
<details> <details>
<summary><h2>更新日志(点击展开)</h2></summary> <summary><h2>更新日志(点击展开)</h2></summary>
## Hydro 5.0.0-beta.0 / UI 4.58.0-beta.0 ## Hydro 5.0.0-beta.3 / UI 4.58.0-beta.3
### New Features
- judge: 添加 checker 编译缓存 - judge: 添加 checker 编译缓存
- ui: 优化题目详情页面 OGP 信息 - ui: 优化题目详情页面 OGP 信息
@@ -127,21 +129,32 @@ Hydro 用户群:1085853538
- core: 分离 HMR 和 Watcher 组件 - core: 分离 HMR 和 Watcher 组件
- core: i18n: 添加 interface 选项 - core: i18n: 添加 interface 选项
- judge: 添加 kattis checker 支持 - judge: 添加 kattis checker 支持
- migrate: 修复 hustoj 自动运行
- core: Settings: 支持使用 Schemastery - core: Settings: 支持使用 Schemastery
- ui: 更新系统设置页面样式 - ui: 更新系统设置页面样式
- import-qduoj: 修复 spj=null
- core: 修复文件复制
- core: 支持存储并显示提交记录重测历史 - core: 支持存储并显示提交记录重测历史
- core: 新的加域逻辑 - core: 新的加域逻辑
- ui: UserSelect.Multi: 支持批量粘贴用户名 - ui: UserSelect.Multi: 支持批量粘贴用户名
- ui: 修复在线 IDE 右键菜单
- ui: 修复未登录用户查看题目文件页
- core: oauth: 支持绑定/解绑三方平台账户 (#971) - core: oauth: 支持绑定/解绑三方平台账户 (#971)
- ui: 修复暗色模式表格边框 (#968)
- core: 优化 icpc 题目包导入 (#966) - core: 优化 icpc 题目包导入 (#966)
- judge: 弃用 diff (#965) - judge: 弃用 diff (#965)
- core: 支持设置加域时自动加入小组 - core: 支持设置加域时自动加入小组
- core: 添加 `--disable-worker` 启动选项
- ui: 支持使用自定义字体
- framework: 添加 Subscription API
- core: 添加 fixStorage 脚本
### Bug Fixes
- migrate: 修复 hustoj 自动运行
- import-qduoj: 修复 spj=null
- core: 修复文件复制
- ui: 修复在线 IDE 右键菜单
- ui: 修复未登录用户查看题目文件页
- ui: 修复暗色模式表格边框 (#968)
- ui: 修复题解回复编辑权限
- ui: 修复高亮行号显示
- core: 修复 `User.serialize()` 参数
- ui: 修复更换邮箱功能
### Breaking API Changes ### Breaking API Changes
+6 -6
View File
@@ -4,21 +4,21 @@
"license": "MIT", "license": "MIT",
"main": "base.mjs", "main": "base.mjs",
"dependencies": { "dependencies": {
"@eslint-react/eslint-plugin": "^1.49.0", "@eslint-react/eslint-plugin": "^1.51.2",
"@eslint/compat": "^1.2.9", "@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@stylistic/eslint-plugin": "^4.2.0", "@stylistic/eslint-plugin": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.33.1",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-plugin-de-morgan": "^1.2.1", "eslint-plugin-de-morgan": "^1.3.0",
"eslint-plugin-github": "^6.0.0", "eslint-plugin-github": "^6.0.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.1.0" "globals": "^16.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": ">= 9" "eslint": ">= 9"
+1 -1
View File
@@ -150,7 +150,7 @@ export class ApiService extends Service {
await emitHook?.(`api/before/${callOrName}`, args); await emitHook?.(`api/before/${callOrName}`, args);
} }
let result = await func(context, args as any, sendPayload); let result = await func(context, args as any, sendPayload);
if (result && 'next' in result) { if (result && typeof result === 'object' && 'next' in result) {
const it = result as AsyncGenerator<any, any, never>; const it = result as AsyncGenerator<any, any, never>;
while (true) { while (true) {
const value = await it.next(); // eslint-disable-line no-await-in-loop const value = await it.next(); // eslint-disable-line no-await-in-loop
+7 -2
View File
@@ -1,12 +1,17 @@
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import type { Next } from 'koa'; import type { Next } from 'koa';
import { pick } from 'lodash';
import { import {
HydroRequest, HydroResponse, KoaContext, serializer, HydroRequest, HydroResponse, KoaContext, serializer,
} from '@hydrooj/framework'; } from '@hydrooj/framework';
import { errorMessage } from '@hydrooj/utils/lib/utils'; import { errorMessage } from '@hydrooj/utils/lib/utils';
import { SystemError, UserFacingError } from './error'; import { SystemError, UserFacingError } from './error';
const pick = <T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> => {
const result: Partial<Pick<T, K>> = {};
for (const key of keys) result[key] = obj[key];
return result as Pick<T, K>;
};
export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => { export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
// Base Layer // Base Layer
const request: HydroRequest = { const request: HydroRequest = {
@@ -112,7 +117,7 @@ export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
ctx.response.status = 302; ctx.response.status = 302;
ctx.redirect(response.redirect); ctx.redirect(response.redirect);
} else if (response.body) { } else if (response.body) {
ctx.body = response.body instanceof Blob ? Buffer.from(await response.body.arrayBuffer()) : response.body; ctx.body = response.body;
ctx.response.status = response.status || 200; ctx.response.status = response.status || 200;
ctx.response.type = response.type ctx.response.type = response.type
|| (request.json || (request.json
+3 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/framework", "name": "@hydrooj/framework",
"version": "0.2.0-beta.2", "version": "0.2.0-beta.3",
"main": "index.ts", "main": "index.ts",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@@ -10,15 +10,14 @@
"@hydrooj/utils": "workspace:^", "@hydrooj/utils": "workspace:^",
"emoji-regex": "^10.4.0", "emoji-regex": "^10.4.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"koa": "^2.16.1", "koa": "^3.0.0",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
"koa-compress": "^5.1.1", "koa-compress": "^5.1.1",
"koa-router": "^13.0.1", "koa-router": "^13.0.1",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.48",
"path-to-regexp": "^8.2.0", "path-to-regexp": "^8.2.0",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"saslprep": "^1.0.3", "saslprep": "^1.0.3",
"schemastery": "^3.16.1",
"shorty.js": "^1.0.1", "shorty.js": "^1.0.1",
"ws": "^8.18.2" "ws": "^8.18.2"
}, },
@@ -27,7 +26,6 @@
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-compress": "^4.0.6", "@types/koa-compress": "^4.0.6",
"@types/koa-router": "^7.4.8", "@types/koa-router": "^7.4.8",
"@types/lodash": "^4.17.16",
"@types/saslprep": "^1.0.3", "@types/saslprep": "^1.0.3",
"@types/ws": "^8.18.1" "@types/ws": "^8.18.1"
} }
+14 -13
View File
@@ -8,6 +8,7 @@ import fs from 'fs-extra';
import Koa from 'koa'; import Koa from 'koa';
import Body from 'koa-body'; import Body from 'koa-body';
import Compress from 'koa-compress'; import Compress from 'koa-compress';
import Schema from 'schemastery';
import { Shorty } from 'shorty.js'; import { Shorty } from 'shorty.js';
import { WebSocket, WebSocketServer } from 'ws'; import { WebSocket, WebSocketServer } from 'ws';
import { import {
@@ -324,19 +325,19 @@ function executeMiddlewareStack(context: any, middlewares: { name: string, func:
return dispatch(0); return dispatch(0);
} }
export interface WebServiceConfig {
keys: string[];
proxy: boolean;
cors?: string;
upload?: string;
port: number;
host?: string;
xff?: string;
xhost?: string;
enableSSE?: boolean;
}
export class WebService<C extends CordisContext = CordisContext> extends Service<never, C> { export class WebService<C extends CordisContext = CordisContext> extends Service<never, C> {
static Config = Schema.object({
keys: Schema.array(Schema.string()),
proxy: Schema.boolean(),
cors: Schema.string(),
upload: Schema.string(),
port: Schema.number(),
host: Schema.string(),
xff: Schema.string(),
xhost: Schema.string(),
enableSSE: Schema.boolean(),
});
private registry: Record<string, any> = Object.create(null); private registry: Record<string, any> = Object.create(null);
private registrationCount = Counter(); private registrationCount = Counter();
private serverLayers = []; private serverLayers = [];
@@ -352,7 +353,7 @@ export class WebService<C extends CordisContext = CordisContext> extends Service
Handler = Handler; Handler = Handler;
ConnectionHandler = ConnectionHandler; ConnectionHandler = ConnectionHandler;
constructor(ctx: C, public config: WebServiceConfig) { constructor(ctx: C, public config: ReturnType<typeof WebService.Config>) {
super(ctx, 'server'); super(ctx, 'server');
ctx.mixin('server', ['Route', 'Connection', 'withHandlerClass']); ctx.mixin('server', ['Route', 'Connection', 'withHandlerClass']);
this.server.keys = this.config.keys; this.server.keys = this.config.keys;
+8 -7
View File
@@ -1,7 +1,5 @@
import assert from 'assert'; import assert from 'assert';
import emojiRegex from 'emoji-regex'; import emojiRegex from 'emoji-regex';
import { isSafeInteger } from 'lodash';
import moment from 'moment-timezone';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import saslprep from 'saslprep'; import saslprep from 'saslprep';
import Schema from 'schemastery'; import Schema from 'schemastery';
@@ -107,9 +105,9 @@ export const Types = {
ShortString: basicString(/^.{1,255}$/), ShortString: basicString(/^.{1,255}$/),
String: basicString(), String: basicString(),
Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && isSafeInteger(+v)], Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && isSafeInteger(+v)], UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && isSafeInteger(+v)], PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
Float: [(v) => +v, (v) => Number.isFinite(+v)], Float: [(v) => +v, (v) => Number.isFinite(+v)],
ObjectId: [() => { throw new Error('mongodb package not found'); }, () => true], ObjectId: [() => { throw new Error('mongodb package not found'); }, () => true],
@@ -118,13 +116,15 @@ export const Types = {
(v) => { (v) => {
const d = v.split('-'); const d = v.split('-');
assert(d.length === 3); assert(d.length === 3);
assert(d[0].length === 4);
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
}, },
(v) => { (v) => {
const d = v.toString().split('-'); const d = v.toString().split('-');
if (d.length !== 3) return false; if (d.length !== 3) return false;
if (d[0].length !== 4) return false;
const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
return moment(st).isValid(); return Number.isFinite(new Date(st).getTime());
}, },
], ],
Time: [ Time: [
@@ -136,7 +136,8 @@ export const Types = {
(v) => { (v) => {
const t = v.toString().split(':'); const t = v.toString().split(':');
if (t.length !== 2) return false; if (t.length !== 2) return false;
return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid(); const d = new Date(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`);
return Number.isFinite(d.getTime());
}, },
], ],
Range: (range) => [ Range: (range) => [
+29 -10
View File
@@ -7,15 +7,21 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
import net from 'net'; import net from 'net';
import os, { cpus } from 'os'; import os, { cpus } from 'os';
import { createInterface } from 'readline/promises'; import { createInterface } from 'readline/promises';
import { supportsHyperlink } from 'supports-hyperlinks'; import supportsHyperlink from 'supports-hyperlinks';
const isSupported = supportsHyperlink(process.stdout); const isSupported = supportsHyperlink.stdout;
const OSC = '\u001B]'; const OSC = '\u001B]';
const BEL = '\u0007'; const BEL = '\u0007';
const SEP = ';'; const SEP = ';';
export const link = isSupported ? (text: string, url: string) => [ export const link = isSupported ? (text: string, url: string) => [
OSC, '8', SEP, SEP, url, BEL, text, OSC, '8', SEP, SEP, BEL, OSC, '8', SEP, SEP, url, BEL, text, OSC, '8', SEP, SEP, BEL,
].join('') : (text: string, url: string) => `${text} < ${url} > `; ].join('') : (text: string, url: string) => `${text} < ${url} > `;
const freemem = os.freemem();
const smallMemory = (freemem < 1024 * 1024 * 1024);
const nixInstall = (...packages: string[]) => (smallMemory
? packages.map((t) => `nix-env -iA ${t.includes('.') ? t : `nixpkgs.${t}`}`).join(' && ')
: `nix-env -iA ${packages.map((t) => (t.includes('.') ? t : `nixpkgs.${t}`)).join(' ')}`);
const warnings: [string, ...any[]][] = []; const warnings: [string, ...any[]][] = [];
@@ -46,6 +52,7 @@ const locales = {
'error.unsupportedArch': '不支持的架构 %s ,请尝试手动安装。', 'error.unsupportedArch': '不支持的架构 %s ,请尝试手动安装。',
'error.osreleaseNotFound': '无法获取系统版本信息(/etc/os-release 文件未找到),请尝试手动安装。', 'error.osreleaseNotFound': '无法获取系统版本信息(/etc/os-release 文件未找到),请尝试手动安装。',
'error.unsupportedOS': '不支持的操作系统 %s ,请尝试手动安装,', 'error.unsupportedOS': '不支持的操作系统 %s ,请尝试手动安装,',
'error.centos': 'CentOS 及其变种系统因系统内核过低,无法安装 Hydro,强烈建议使用其他系统。若确有需求,请升级 Linux 内核至 4.4+ 后再手动安装 Hydro。',
'install.preparing': '正在初始化安装...', 'install.preparing': '正在初始化安装...',
'install.mongodb': '正在安装 mongodb...', 'install.mongodb': '正在安装 mongodb...',
'install.createDatabaseUser': '正在创建数据库用户...', 'install.createDatabaseUser': '正在创建数据库用户...',
@@ -85,6 +92,8 @@ then restart and run the script again.`,
'error.unsupportedArch': 'Unsupported architecture %s, please try to install manually.', 'error.unsupportedArch': 'Unsupported architecture %s, please try to install manually.',
'error.osreleaseNotFound': 'Unable to get system version information (/etc/os-release file not found), please try to install manually.', 'error.osreleaseNotFound': 'Unable to get system version information (/etc/os-release file not found), please try to install manually.',
'error.unsupportedOS': 'Unsupported operating system %s, please try to install manually.', 'error.unsupportedOS': 'Unsupported operating system %s, please try to install manually.',
'error.centos': `CentOS and its derivatives are not supported due to low system kernel versions.
It is strongly recommended to use other systems. If you really need it, please upgrade the Linux kernel to 4.4+ and manually install Hydro.`,
'install.preparing': 'Initializing installation...', 'install.preparing': 'Initializing installation...',
'install.mongodb': 'Installing mongodb...', 'install.mongodb': 'Installing mongodb...',
'install.createDatabaseUser': 'Creating database user...', 'install.createDatabaseUser': 'Creating database user...',
@@ -347,6 +356,18 @@ const Steps = () => [
} }
} }
}, },
async () => {
if (process.env.IGNORE_CENTOS) return;
const res = exec('yum -h');
if (res.code) return;
if (!process.argv.includes('--unsupported-centos')) {
log.warn('error.centos');
process.exit(1);
} else {
log.warn('warn.centos');
warnings.push(['warn.centos']);
}
},
async () => { async () => {
if (!avx && !installAsJudge) { if (!avx && !installAsJudge) {
log.warn('note.avx'); log.warn('note.avx');
@@ -397,9 +418,7 @@ ${nixConfBase}`);
exec('nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs', { stdio: 'inherit' }); exec('nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs', { stdio: 'inherit' });
exec('nix-channel --update', { stdio: 'inherit' }); exec('nix-channel --update', { stdio: 'inherit' });
}, },
`nix-env -iA ${[ nixInstall('pm2', 'yarn', 'esbuild', 'bash', 'unzip', 'zip', 'diffutils', 'patch', 'screen', 'gawk'),
'pm2', 'yarn', 'esbuild', 'bash', 'unzip', 'zip', 'diffutils', 'patch', 'screen', 'gawk',
].map((i) => `nixpkgs.${i}`).join(' ')}`,
'yarn config set disable-self-update-check true', 'yarn config set disable-self-update-check true',
async () => { async () => {
const rl = createInterface(process.stdin, process.stdout); const rl = createInterface(process.stdin, process.stdout);
@@ -450,20 +469,20 @@ ${nixConfBase}`);
"openssl-1.1.1z" "openssl-1.1.1z"
]; ];
}`), }`),
`nix-env -iA hydro.mongodb${avx ? 7 : 4}${CN ? '-cn' : ''} nixpkgs.mongosh nixpkgs.mongodb-tools`, nixInstall(`hydro.mongodb${avx ? 7 : 4}${CN ? '-cn' : ''}`, 'mongosh', 'mongodb-tools'),
], ],
}, },
{ {
init: 'install.compiler', init: 'install.compiler',
operations: [ operations: [
'nix-env -iA nixpkgs.gcc nixpkgs.python3', nixInstall('gcc', 'python3'),
], ],
}, },
{ {
init: 'install.sandbox', init: 'install.sandbox',
skip: () => !exec('hydro-sandbox --help').code, skip: () => !exec('hydro-sandbox --help').code,
operations: [ operations: [
'nix-env -iA nixpkgs.go-judge', nixInstall('go-judge'),
'ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox', 'ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox',
], ],
}, },
@@ -472,7 +491,7 @@ ${nixConfBase}`);
skip: () => installAsJudge || noCaddy || existsSync(`${process.env.HOME}/.hydro/Caddyfile`), skip: () => installAsJudge || noCaddy || existsSync(`${process.env.HOME}/.hydro/Caddyfile`),
hidden: installAsJudge, hidden: installAsJudge,
operations: [ operations: [
'nix-env -iA nixpkgs.caddy', nixInstall('caddy'),
() => writeFileSync(`${process.env.HOME}/.hydro/Caddyfile`, Caddyfile), () => writeFileSync(`${process.env.HOME}/.hydro/Caddyfile`, Caddyfile),
], ],
}, },
@@ -660,7 +679,7 @@ ${nixConfBase}`);
{ {
init: 'install.postinstall', init: 'install.postinstall',
operations: [ operations: [
'echo "layout=1" >/etc/HYDRO_INSTALLER', 'echo "layout=2" >/etc/HYDRO_INSTALLER',
'echo "vm.swappiness = 1" >>/etc/sysctl.conf', 'echo "vm.swappiness = 1" >>/etc/sysctl.conf',
'sysctl -p', 'sysctl -p',
// dont retry this as it usually fails // dont retry this as it usually fails
+8 -8
View File
@@ -43,22 +43,22 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"esbuild": "0.25.2", "esbuild": "0.25.2",
"eslint": "^9.26.0", "eslint": "^9.28.0",
"eslint-import-resolver-typescript": "^4.3.4", "eslint-import-resolver-typescript": "^4.4.3",
"eslint-import-resolver-webpack": "^0.13.10", "eslint-import-resolver-webpack": "^0.13.10",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"globals": "^16.1.0", "globals": "^16.2.0",
"globby": "~14.0.2", "globby": "~14.0.2",
"inspectpack": "^4.7.1", "inspectpack": "^4.7.1",
"mongodb-memory-server": "10.1.4", "mongodb-memory-server": "10.1.4",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"ora": "^8.2.0", "ora": "^8.2.0",
"oxlint": "^0.16.9", "oxlint": "^0.16.12",
"package-json": "^10.0.1", "package-json": "^10.0.1",
"semver": "^7.7.1", "semver": "^7.7.2",
"simple-git": "^3.27.0", "simple-git": "^3.28.0",
"supertest": "^7.1.0", "supertest": "^7.1.1",
"supports-hyperlinks": "^3.2.0", "supports-hyperlinks": "^4.1.0",
"typescript": "5.8.3", "typescript": "5.8.3",
"wtfnode": "^0.10.0" "wtfnode": "^0.10.0"
}, },
+15
View File
@@ -1,3 +1,5 @@
import { execSync } from 'child_process';
import fs from 'fs';
import { Context, Schema, UserModel } from 'hydrooj'; import { Context, Schema, UserModel } from 'hydrooj';
import { startPerformanceTest } from './performance-test'; import { startPerformanceTest } from './performance-test';
@@ -7,4 +9,17 @@ export async function apply(ctx: Context) {
if (that.session.uid === 2) await UserModel.setSuperAdmin(2); if (that.session.uid === 2) await UserModel.setSuperAdmin(2);
}); });
ctx.addScript('performance-test', 'test', Schema.object({ enable5: Schema.boolean().default(false) }), startPerformanceTest); ctx.addScript('performance-test', 'test', Schema.object({ enable5: Schema.boolean().default(false) }), startPerformanceTest);
const installerFileExists = fs.existsSync('/etc/HYDRO_INSTALLER');
if (!installerFileExists) return;
const content = fs.readFileSync('/etc/HYDRO_INSTALLER', 'utf-8');
const val: Record<string, string> = {};
for (const line of content.split('\n')) {
const [key, value] = line.split('=').map((i) => i?.trim());
if (key && value) val[key] = value;
}
if (val.layout === '1') {
execSync('nix-env -iA nixpkgs.gawk');
execSync('pm2 restart hydro-sandbox');
fs.writeFileSync('/etc/HYDRO_INSTALLER', content.replace('layout=1', 'layout=2'));
}
} }
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "@hydrooj/a11y", "name": "@hydrooj/a11y",
"version": "0.0.2" "version": "0.0.3"
} }
+1 -1
View File
@@ -11,6 +11,6 @@
}, },
"dependencies": { "dependencies": {
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"semver": "^7.7.1" "semver": "^7.7.2"
} }
} }
+15 -3
View File
@@ -13,7 +13,7 @@ const locales = {
}; };
function MonacoContainer({ function MonacoContainer({
config, setValue, setError, monaco, schema, editorCallback, size, config, setValue, setError, monaco, schema, editorCallback, size, dark,
}) { }) {
const [editor, setEditor] = React.useState<any>(null); const [editor, setEditor] = React.useState<any>(null);
const [model, setModel] = React.useState<any>(null); const [model, setModel] = React.useState<any>(null);
@@ -69,7 +69,7 @@ function MonacoContainer({
const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml')); const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
setModel(model); setModel(model);
const e = monaco.editor.create(element, { const e = monaco.editor.create(element, {
theme: 'vs-light', theme: dark ? 'vs-dark' : 'vs-light',
lineNumbers: 'off', lineNumbers: 'off',
glyphMargin: true, glyphMargin: true,
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On }, lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
@@ -149,15 +149,27 @@ export default function ConfigEditor({
setValue(yaml.load(v)); setValue(yaml.load(v));
}, [stringConfig]); }, [stringConfig]);
// FIXME: Otherwise first form change will be ignored
React.useEffect(() => {
setTimeout(() => {
updateFromMonaco(`${stringConfig}\n\ndummy: 1`);
setTimeout(() => {
setStringConfig(stringConfig);
setValue(yaml.load(stringConfig));
}, 300);
}, 300);
}, []);
const [size, setSize] = React.useState([50, 50]); const [size, setSize] = React.useState([50, 50]);
return (<div className="fullscreen-content" style={{ backgroundColor: 'white', zIndex: 10 }}> return (<div className="fullscreen-content" style={{ zIndex: 10 }}>
<Allotment onChange={setSize}> <Allotment onChange={setSize}>
<Allotment.Pane> <Allotment.Pane>
<MonacoContainer <MonacoContainer
editorCallback={registerAction} schema={schema} monaco={monaco} editorCallback={registerAction} schema={schema} monaco={monaco}
config={stringConfig} setValue={updateFromMonaco} setError={setInfo} config={stringConfig} setValue={updateFromMonaco} setError={setInfo}
size={size[0]} size={size[0]}
dark={document.documentElement.className.includes('theme--dark')}
/> />
<pre className="help-text">{info}</pre> <pre className="help-text">{info}</pre>
<button onClick={() => onSave(stringConfig)} className="rounded primary button">{i18n('Save All Changes')}</button> <button onClick={() => onSave(stringConfig)} className="rounded primary button">{i18n('Save All Changes')}</button>
+6 -6
View File
@@ -1,22 +1,22 @@
{ {
"name": "@hydrooj/components", "name": "@hydrooj/components",
"version": "1.0.0-beta.2", "version": "1.0.0-beta.3",
"main": "./frontend/index.ts", "main": "./frontend/index.ts",
"dependencies": { "dependencies": {
"allotment": "^1.20.3", "allotment": "^1.20.3",
"diff": "^7.0.0", "diff": "^8.0.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"schemastery-react": "^0.1.2" "schemastery-react": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^7.0.2", "@types/diff": "^8.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.17",
"sass": "^1.87.0" "sass": "^1.89.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
+1 -1
View File
@@ -6,7 +6,7 @@
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "^8.18.2" "@elastic/elasticsearch": "^9.0.2"
}, },
"engines": { "engines": {
"hydrooj": ">=5.0.0-alpha.1" "hydrooj": ">=5.0.0-alpha.1"
+1 -1
View File
@@ -6,7 +6,7 @@
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"maxmind": "^4.3.25" "maxmind": "^4.3.27"
}, },
"preferUnplugged": true "preferUnplugged": true
} }
+4 -4
View File
@@ -1,19 +1,19 @@
{ {
"name": "@hydrooj/hydrojudge", "name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js", "bin": "bin/hydrojudge.js",
"version": "3.5.0-beta.2", "version": "3.5.0-beta.3",
"main": "./src/index.ts", "main": "./src/index.ts",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"dependencies": { "dependencies": {
"@hydrooj/common": "workspace:^", "@hydrooj/common": "workspace:^",
"@hydrooj/utils": "workspace:^", "@hydrooj/utils": "workspace:^",
"@zip.js/zip.js": "^2.7.60", "@zip.js/zip.js": "^2.7.62",
"cac": "^6.7.14", "cac": "^6.7.14",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"schemastery": "^3.16.1", "schemastery": "^3.16.1",
"semver": "^7.7.1", "semver": "^7.7.2",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.3",
"superagent": "^10.2.1", "superagent": "^10.2.1",
"ws": "^8.18.2" "ws": "^8.18.2"
}, },
+1
View File
@@ -93,6 +93,7 @@ const getDefaultChecker = (strict: boolean) => async (config) => {
...config.copyIn, ...config.copyIn,
'compare.sh': { content: compareSh }, 'compare.sh': { content: compareSh },
}, },
processLimit: 32,
}); });
let status: number; let status: number;
let message: any = ''; let message: any = '';
+2 -5
View File
@@ -27,11 +27,8 @@ export namespace Lock {
} }
} }
export function compilerText(stdout: string, stderr: string) { export function compilerText(...messages: string[]) {
const ret = []; return messages.filter((i) => !EMPTY_STR.test(i)).map((i) => i.substring(0, 1024 * 1024)).join('\n');
if (!EMPTY_STR.test(stdout)) ret.push(stdout.substring(0, 1024 * 1024));
if (!EMPTY_STR.test(stderr)) ret.push(stderr.substring(0, 1024 * 1024));
return ret.join('\n');
} }
function restrictFile(p: string) { function restrictFile(p: string) {
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "5.0.0-beta.2", "version": "5.0.0-beta.3",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/plugin-api", "main": "src/plugin-api",
"module": "src/plugin-api", "module": "src/plugin-api",
@@ -23,10 +23,10 @@
"@hydrooj/utils": "workspace:^", "@hydrooj/utils": "workspace:^",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
"@undefined-moe/require-resolve-hook": "^1.1.0", "@undefined-moe/require-resolve-hook": "^1.1.0",
"@zip.js/zip.js": "^2.7.60", "@zip.js/zip.js": "^2.7.62",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"cac": "^6.7.14", "cac": "^6.7.14",
"cordis": "4.0.0-beta.2", "cordis": "4.0.0-beta.5",
"csv-stringify": "^6.5.2", "csv-stringify": "^6.5.2",
"emoji-regex": "^10.4.0", "emoji-regex": "^10.4.0",
"emojis-list": "2.1.0", "emojis-list": "2.1.0",
@@ -35,10 +35,10 @@
"koa-proxies": "^0.12.4", "koa-proxies": "^0.12.4",
"koa-static-cache": "^5.1.4", "koa-static-cache": "^5.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^10.4.3", "lru-cache": "^11.1.0",
"mime-types": "^3.0.1", "mime-types": "^3.0.1",
"moment-timezone": "^0.5.48", "moment-timezone": "^0.5.48",
"mongodb": "^6.16.0", "mongodb": "^6.17.0",
"mongodb-uri": "^0.9.7", "mongodb-uri": "^0.9.7",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
@@ -47,7 +47,7 @@
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"saslprep": "^1.0.3", "saslprep": "^1.0.3",
"schemastery": "^3.16.1", "schemastery": "^3.16.1",
"semver": "^7.7.1", "semver": "^7.7.2",
"shorty.js": "^1.0.1", "shorty.js": "^1.0.1",
"superagent": "^10.2.1", "superagent": "^10.2.1",
"tar": "6.2.1", "tar": "6.2.1",
@@ -60,7 +60,7 @@
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/koa-static-cache": "^5.1.4", "@types/koa-static-cache": "^5.1.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.17",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/mongodb-uri": "^0.9.4", "@types/mongodb-uri": "^0.9.4",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
+12 -1
View File
@@ -71,6 +71,17 @@ export class Context extends cordis.Context {
super(); super();
this.plugin(ApiMixin); this.plugin(ApiMixin);
this.plugin(TimerService); this.plugin(TimerService);
this.plugin(LoggerService); this.plugin(LoggerService, {
console: {
showDiff: false,
showTime: 'dd hh:mm:ss',
label: {
align: 'right',
width: 9,
margin: 1,
},
levels: { default: process.env.DEV ? 3 : 2 },
},
});
} }
} }
+4 -4
View File
@@ -6,7 +6,7 @@ import { ObjectId } from 'mongodb';
import { Context } from '../context'; import { Context } from '../context';
import { load as loadOptions } from '../options'; import { load as loadOptions } from '../options';
import { MongoService } from '../service/db'; import { MongoService } from '../service/db';
import { ConfigService } from '../settings'; import { SettingService } from '../settings';
import { import {
addon, builtinModel, model, service, addon, builtinModel, model, service,
} from './common'; } from './common';
@@ -110,10 +110,10 @@ export async function load(ctx: Context) {
const pending = global.addons; const pending = global.addons;
const fail = []; const fail = [];
await ctx.plugin(MongoService, loadOptions() || {}); await ctx.plugin(MongoService, loadOptions() || {});
await ctx.plugin(ConfigService); await ctx.plugin(SettingService);
await require('../model/system').runConfig(); await require('../model/system').runConfig();
ctx = await new Promise((resolve) => { ctx = await new Promise((resolve) => {
ctx.inject(['loader', 'config', 'db'], (c) => { ctx.inject(['loader', 'setting', 'db'], (c) => {
resolve(c); resolve(c);
}); });
}); });
@@ -127,7 +127,7 @@ export async function load(ctx: Context) {
await builtinModel(ctx); await builtinModel(ctx);
await model(pending, fail, ctx); await model(pending, fail, ctx);
ctx = await new Promise((resolve) => { ctx = await new Promise((resolve) => {
ctx.inject(['server', 'setting', 'worker'], (c) => { ctx.inject(['server', 'worker'], (c) => {
resolve(c); resolve(c);
}); });
}); });
+3 -3
View File
@@ -7,7 +7,7 @@ import { Context } from '../context';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { load } from '../options'; import { load } from '../options';
import { MongoService } from '../service/db'; import { MongoService } from '../service/db';
import { ConfigService } from '../settings'; import { SettingService } from '../settings';
import { import {
addon, builtinModel, locale, model, service, addon, builtinModel, locale, model, service,
} from './common'; } from './common';
@@ -30,11 +30,11 @@ export async function apply(ctx: Context) {
const fail = []; const fail = [];
await locale(pending, fail); await locale(pending, fail);
await ctx.plugin(MongoService, load() || {}); await ctx.plugin(MongoService, load() || {});
await ctx.plugin(ConfigService); await ctx.plugin(SettingService);
const modelSystem = require('../model/system'); const modelSystem = require('../model/system');
await modelSystem.runConfig(); await modelSystem.runConfig();
ctx = await new Promise((resolve) => { ctx = await new Promise((resolve) => {
ctx.inject(['loader', 'config', 'db'], (c) => { ctx.inject(['loader', 'setting', 'db'], (c) => {
resolve(c); resolve(c);
}); });
}); });
+2 -2
View File
@@ -576,7 +576,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler {
const bdocs = await contest.getMultiBalloon(domainId, tid, { const bdocs = await contest.getMultiBalloon(domainId, tid, {
...todo ? { sent: { $exists: false } } : {}, ...todo ? { sent: { $exists: false } } : {},
...(!this.tdoc.lockAt || this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)) ...(!this.tdoc.lockAt || this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD))
? {} : { _id: { $lt: this.tdoc.lockAt } }, ? {} : { _id: { $lt: Time.getObjectID(this.tdoc.lockAt) } },
}).sort({ _id: -1 }).toArray(); }).sort({ _id: -1 }).toArray();
const uids = bdocs.map((i) => i.uid).concat(bdocs.filter((i) => i.sent).map((i) => i.sent)); const uids = bdocs.map((i) => i.uid).concat(bdocs.filter((i) => i.sent).map((i) => i.sent));
this.response.body = { this.response.body = {
@@ -743,7 +743,7 @@ export async function apply(ctx: Context) {
await Promise.all(tasks); await Promise.all(tasks);
}); });
ctx.plugin(ScoreboardService); ctx.plugin(ScoreboardService);
ctx.inject(['scoreboard'], ({ Route, scoreboard }) => { await ctx.inject(['scoreboard'], ({ Route, scoreboard }) => {
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD); Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
Route('contest_scoreboard_view', '/contest/:tid/scoreboard/:view', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD); Route('contest_scoreboard_view', '/contest/:tid/scoreboard/:view', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
scoreboard.addView('default', 'Default', { tdoc: 'tdoc', groups: 'groups', realtime: Types.Boolean }, { scoreboard.addView('default', 'Default', { tdoc: 'tdoc', groups: 'groups', realtime: Types.Boolean }, {
+2 -1
View File
@@ -214,6 +214,7 @@ class DomainPermissionHandler extends ManageHandler {
async post({ domainId }) { async post({ domainId }) {
const roles = {}; const roles = {};
for (const role in this.request.body) { for (const role in this.request.body) {
if (role === 'root') continue; // root role is not editable
const perms = this.request.body[role] instanceof Array const perms = this.request.body[role] instanceof Array
? this.request.body[role] ? this.request.body[role]
: [this.request.body[role]]; : [this.request.body[role]];
@@ -460,7 +461,7 @@ export async function apply(ctx: Context) {
ctx.Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler); ctx.Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler);
ctx.Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE); ctx.Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
ctx.Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE); ctx.Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
ctx.inject(['api'], ({ api }) => { await ctx.inject(['api'], ({ api }) => {
api.provide(DomainApi); api.provide(DomainApi);
}); });
} }
+1 -1
View File
@@ -314,7 +314,7 @@ export async function apply(ctx) {
ctx.Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler); ctx.Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler);
ctx.Route('homework_files', '/homework/:tid/file', HomeworkFilesHandler, PERM.PERM_VIEW_HOMEWORK); ctx.Route('homework_files', '/homework/:tid/file', HomeworkFilesHandler, PERM.PERM_VIEW_HOMEWORK);
ctx.Route('homework_file_download', '/homework/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_HOMEWORK); ctx.Route('homework_file_download', '/homework/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_HOMEWORK);
ctx.inject(['scoreboard'], ({ Route }) => { await ctx.inject(['scoreboard'], ({ Route }) => {
Route('homework_scoreboard', '/homework/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD); Route('homework_scoreboard', '/homework/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
Route('homework_scoreboard_view', '/homework/:tid/scoreboard/:view', ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD); Route('homework_scoreboard_view', '/homework/:tid/scoreboard/:view', ContestScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
}); });
+10 -13
View File
@@ -182,7 +182,7 @@ class SystemConfigHandler extends SystemHandler {
@requireSudo @requireSudo
async get() { async get() {
this.response.template = 'manage_config.html'; this.response.template = 'manage_config.html';
let value; let value = this.ctx.setting.configSource;
const processNode = (node: any, schema: Schema<any, any>, parent?: any, accessKey?: string) => { const processNode = (node: any, schema: Schema<any, any>, parent?: any, accessKey?: string) => {
if (['union', 'intersect'].includes(schema.type)) { if (['union', 'intersect'].includes(schema.type)) {
@@ -198,23 +198,20 @@ class SystemConfigHandler extends SystemHandler {
}; };
try { try {
try { const temp = yaml.load(this.ctx.setting.configSource);
value = Schema.intersect(this.ctx.config.settings)(yaml.load(this.ctx.config.configSource)); for (const schema of this.ctx.setting.settings) processNode(temp, schema);
} catch (e) { value = yaml.dump(temp);
value = yaml.load(this.ctx.config.configSource);
}
for (const schema of this.ctx.config.settings) processNode(value, schema);
} catch (e) { } } catch (e) { }
this.response.body = { this.response.body = {
schema: Schema.intersect(this.ctx.config.settings).toJSON(), schema: Schema.intersect(this.ctx.setting.settings).toJSON(),
value: yaml.dump(value), value,
}; };
} }
@requireSudo @requireSudo
@param('value', Types.String) @param('value', Types.String)
async post({ }, value: string) { async post({ }, value: string) {
const oldConfig = yaml.load(this.ctx.config.configSource); const oldConfig = yaml.load(this.ctx.setting.configSource);
let config; let config;
const processNode = (node: any, old: any, schema: Schema<any, any>, parent?: any, accessKey?: string) => { const processNode = (node: any, old: any, schema: Schema<any, any>, parent?: any, accessKey?: string) => {
if (['union', 'intersect'].includes(schema.type)) { if (['union', 'intersect'].includes(schema.type)) {
@@ -231,11 +228,11 @@ class SystemConfigHandler extends SystemHandler {
try { try {
config = yaml.load(value); config = yaml.load(value);
for (const schema of this.ctx.config.settings) processNode(config, oldConfig, schema, null, ''); for (const schema of this.ctx.setting.settings) processNode(config, oldConfig, schema, null, '');
} catch (e) { } catch (e) {
throw new ValidationError('value', '', e.message); throw new ValidationError('value', '', e.message);
} }
await this.ctx.config.saveConfig(config); await this.ctx.setting.saveConfig(config);
} }
} }
@@ -357,7 +354,7 @@ class SystemUserPrivHandler extends SystemHandler {
} }
} }
export const inject = ['config', 'check']; export const inject = ['setting', 'check'];
export async function apply(ctx) { export async function apply(ctx) {
ctx.Route('manage', '/manage', SystemMainHandler); ctx.Route('manage', '/manage', SystemMainHandler);
ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler); ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
+3 -3
View File
@@ -384,10 +384,10 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
if (this.psdoc?.rid) { if (this.psdoc?.rid) {
this.response.body.rdoc = await record.get(this.args.domainId, this.psdoc.rid); this.response.body.rdoc = await record.get(this.args.domainId, this.psdoc.rid);
} }
[this.response.body.ctdocs, this.response.body.htdocs] = await Promise.all([ [this.response.body.ctdocs, this.response.body.htdocs] = (await Promise.all([
contest.getRelated(this.args.domainId, this.pdoc.docId), contest.getRelated(this.args.domainId, this.pdoc.docId),
contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'), contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'),
]); ])).map((tdocs) => tdocs.filter((tdoc) => !tdoc.assign?.length || Set.intersection(tdoc.assign, this.user.group).size));
} }
} }
@@ -1070,7 +1070,7 @@ export async function apply(ctx: Context) {
ctx.Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM); ctx.Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM);
ctx.Route('problem_statistics', '/p/:pid/stat', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM); ctx.Route('problem_statistics', '/p/:pid/stat', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM);
ctx.Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM); ctx.Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
ctx.inject(['api'], ({ api }) => { await ctx.inject(['api'], ({ api }) => {
api.provide(ProblemApi); api.provide(ProblemApi);
}); });
} }
+1 -1
View File
@@ -625,7 +625,7 @@ export async function apply(ctx: Context) {
throw new NotFoundError(); throw new NotFoundError();
}, },
}); });
ctx.inject(['api'], ({ api }) => { await ctx.inject(['api'], ({ api }) => {
api.provide(UserApi); api.provide(UserApi);
}); });
} }
+2 -1
View File
@@ -54,7 +54,8 @@ inject('Nav', 'record_main', {
query: (handler) => (handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) query: (handler) => (handler.user.hasPriv(PRIV.PRIV_USER_PROFILE)
? ({ uidOrName: handler.user._id }) ? ({ uidOrName: handler.user._id })
: ({})), : ({})),
}); }, (handler) => (handler.user.hasPriv(PRIV.PRIV_USER_PROFILE)
? true : handler.user.hasPerm(PERM.PERM_VIEW_RECORD)));
inject('Nav', 'ranking', { prefix: 'ranking' }, PERM.PERM_VIEW_RANKING); inject('Nav', 'ranking', { prefix: 'ranking' }, PERM.PERM_VIEW_RANKING);
inject('Nav', 'domain_dashboard', { prefix: 'domain' }, PERM.PERM_EDIT_DOMAIN); inject('Nav', 'domain_dashboard', { prefix: 'domain' }, PERM.PERM_EDIT_DOMAIN);
inject('Nav', 'manage_dashboard', { prefix: 'manage' }, PRIV.PRIV_EDIT_SYSTEM); inject('Nav', 'manage_dashboard', { prefix: 'manage' }, PRIV.PRIV_EDIT_SYSTEM);
+3 -3
View File
@@ -50,7 +50,7 @@ export class Loader extends Service {
public cache: Record<string, string> = Object.create(null); public cache: Record<string, string> = Object.create(null);
// public warnings: Record<string, string> = Object.create(null); // public warnings: Record<string, string> = Object.create(null);
static inject = ['config', 'timer', 'i18n', 'logger']; static inject = ['setting', 'timer', 'i18n', 'logger'];
constructor(ctx: Context) { constructor(ctx: Context) {
super(ctx, 'loader'); super(ctx, 'loader');
@@ -100,8 +100,8 @@ export class Loader extends Service {
const schemaRequest = configScope ? Schema.object({ const schemaRequest = configScope ? Schema.object({
[configScope]: schema, [configScope]: schema,
}) : schema; }) : schema;
await this.ctx.config._tryMigrateConfig(schemaRequest); await this.ctx.setting._tryMigrateConfig(schemaRequest);
const res = this.ctx.config.requestConfig(schemaRequest); const res = this.ctx.setting.requestConfig(schemaRequest);
return configScope ? res[configScope] : res; return configScope ? res[configScope] : res;
} }
+6 -39
View File
@@ -1,14 +1,13 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import fs from 'fs'; import fs from 'fs';
import * as cordis from 'cordis';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import Schema from 'schemastery'; import Schema from 'schemastery';
import { LangConfig, parseLang } from '@hydrooj/common'; import { LangConfig, parseLang } from '@hydrooj/common';
import { findFileSync, retry } from '@hydrooj/utils'; import { findFileSync, retry } from '@hydrooj/utils';
import { Context, Service } from '../context'; import { Context } from '../context';
import { Setting as _Setting } from '../interface'; import { Setting as _Setting } from '../interface';
import { Logger } from '../logger'; import { Logger } from '../logger';
import * as builtin from './builtin'; import * as builtin from './builtin';
@@ -27,11 +26,6 @@ for (const country of countries) {
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][]; const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
const langRange: Dictionary<string> = {}; const langRange: Dictionary<string> = {};
for (const lang in global.Hydro.locales) {
if (!global.Hydro.locales[lang].__interface) continue;
langRange[lang] = global.Hydro.locales[lang].__langname;
}
export const FLAG_HIDDEN = 1; export const FLAG_HIDDEN = 1;
export const FLAG_DISABLED = 2; export const FLAG_DISABLED = 2;
export const FLAG_SECRET = 4; export const FLAG_SECRET = 4;
@@ -89,7 +83,7 @@ function schemaToSettings(schema: Schema<any>) {
let flag = (s.meta?.hidden ? FLAG_HIDDEN : 0) let flag = (s.meta?.hidden ? FLAG_HIDDEN : 0)
| (s.meta?.disabled ? FLAG_DISABLED : 0); | (s.meta?.disabled ? FLAG_DISABLED : 0);
const type = s.type === 'number' ? 'number' const type = s.type === 'number' ? 'number'
: s.type === 'boolean' ? 'checkbox' : s.type === 'boolean' ? 'boolean'
: s.meta?.role === 'markdown' ? 'markdown' : s.meta?.role === 'markdown' ? 'markdown'
: s.meta?.role === 'textarea' ? 'textarea' : 'text'; : s.meta?.role === 'textarea' ? 'textarea' : 'text';
if (s.meta?.role === 'password') flag |= FLAG_SECRET; if (s.meta?.role === 'password') flag |= FLAG_SECRET;
@@ -362,39 +356,13 @@ SystemSetting(
export const langs: Record<string, LangConfig> = {}; export const langs: Record<string, LangConfig> = {};
declare module 'cordis' {
interface Context {
setting: SettingService;
}
}
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
function method(this: cordis.Service, ...args: Parameters<F>) {
this.ctx.effect(() => {
const res = origFunc(...args);
return () => (disposeFunc ? disposeFunc(res) : res());
});
};
export class SettingService extends Service {
PreferenceSetting = T(PreferenceSetting);
AccountSetting = T(AccountSetting);
DomainSetting = T(DomainSetting);
DomainUserSetting = T(DomainUserSetting);
SystemSetting = T(SystemSetting);
constructor(ctx: Context) {
super(ctx, 'setting');
}
get(key: string) {
return (this.ctx ? this.ctx.domain?.config?.[key.replace(/\./g, '$')] : null) ?? global.Hydro.model.system.get(key);
}
}
export const inject = ['db']; export const inject = ['db'];
export async function apply(ctx: Context) { export async function apply(ctx: Context) {
ctx.plugin(SettingService);
logger.info('Ensuring settings'); logger.info('Ensuring settings');
for (const lang in global.Hydro.locales) {
if (!global.Hydro.locales[lang].__interface) continue;
langRange[lang] = global.Hydro.locales[lang].__langname;
}
const system = global.Hydro.model.system; const system = global.Hydro.model.system;
for (const setting of SYSTEM_SETTINGS) { for (const setting of SYSTEM_SETTINGS) {
if (!setting.value) continue; if (!setting.value) continue;
@@ -424,7 +392,6 @@ global.Hydro.model.setting = {
apply, apply,
inject, inject,
SettingService,
Setting, Setting,
PreferenceSetting, PreferenceSetting,
AccountSetting, AccountSetting,
+2 -1
View File
@@ -159,7 +159,7 @@ export class User {
return user; return user;
} }
serialize(_, h) { serialize(h) {
if (!this._isPrivate) { if (!this._isPrivate) {
const fields = ['_id', 'uname', 'mail', 'perm', 'role', 'priv', 'regat', 'loginat', 'tfa', 'authn']; const fields = ['_id', 'uname', 'mail', 'perm', 'role', 'priv', 'regat', 'loginat', 'tfa', 'authn'];
if (h?.user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)) fields.push('displayName'); if (h?.user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)) fields.push('displayName');
@@ -189,6 +189,7 @@ async function initAndCache(udoc: Udoc, dudoc, scope: bigint = PERM.PERM_ALL) {
class UserModel { class UserModel {
static coll = coll; static coll = coll;
static collGroup = collGroup;
static User = User; static User = User;
static cache = cache; static cache = cache;
static defaultUser: Udoc = { static defaultUser: Udoc = {
+1
View File
@@ -2,6 +2,7 @@ import pwsh from './lib/hash.hydro';
import db from './service/db'; import db from './service/db';
export { nanoid } from 'nanoid'; export { nanoid } from 'nanoid';
export { default as moment, isMoment } from 'moment-timezone';
export { export {
WebService, Router, HandlerCommon, httpServer, WebService, Router, HandlerCommon, httpServer,
+53
View File
@@ -0,0 +1,53 @@
/* eslint-disable no-await-in-loop */
import { resolve } from 'path';
import {
existsSync, mkdir, readdir, rename, rmdir,
} from 'fs-extra';
import Schema from 'schemastery';
import { Context } from '../context';
export const apply = (ctx: Context) => ctx.addScript(
'fixStorage', 'Rename all files to lowercase',
Schema.object({}),
async () => {
const config = await ctx.get('storage').status();
console.log(config);
if (config.type !== 'Local') throw new Error('Only local storage is supported');
const dir = (config as any).dir;
if (!dir) throw new Error('Storage dir not found');
const files = await readdir(dir);
for (const file of files) {
let inner = await readdir(resolve(dir, file));
if (file.toLowerCase() !== file && !existsSync(resolve(dir, file.toLowerCase()))) {
await mkdir(resolve(dir, file.toLowerCase()));
console.log('+: ', resolve(dir, file.toLowerCase()));
}
for (const f of inner) {
if (resolve(dir, file, f) === resolve(dir, file.toLowerCase(), f.toLowerCase())) continue;
await rename(resolve(dir, file, f), resolve(dir, file.toLowerCase(), f.toLowerCase()));
console.log('R: ', resolve(dir, file, f), '->', resolve(dir, file.toLowerCase(), f.toLowerCase()));
}
inner = await readdir(resolve(dir, file));
if (!inner.length) {
await rmdir(resolve(dir, file));
console.log('-: ', resolve(dir, file));
}
}
const coll = ctx.get('db').collection('storage');
while (true) {
let hasItem = false;
for await (const t of coll.find({ $or: [{ _id: /[A-Z]/ }, { link: /[A-Z]/ }] })) {
hasItem = true;
await coll.insertOne({
...t,
_id: t._id.toLowerCase(),
...(t.link ? { link: t.link.toLowerCase() } : {}),
});
await coll.deleteOne({ _id: t._id });
console.log('Q: ', t._id, '->', t._id.toLowerCase());
}
if (!hasItem) break;
}
return true;
},
);
+1 -1
View File
@@ -91,7 +91,7 @@ export async function apply(ctx: Context) {
xhost: system.get('server.xhost'), xhost: system.get('server.xhost'),
}); });
if (process.env.HYDRO_CLI) return; if (process.env.HYDRO_CLI) return;
ctx.inject(['server', 'oauth', 'setting'], (childContext) => { await ctx.inject(['server', 'oauth', 'setting'], (childContext) => {
const { server, on, oauth } = childContext; const { server, on, oauth } = childContext;
server.addHandlerLayer('init', async (c, next) => { server.addHandlerLayer('init', async (c, next) => {
const init = Date.now(); const init = Date.now();
+2 -1
View File
@@ -387,6 +387,7 @@ class LocalStorageService {
status: !this.error, status: !this.error,
error: this.error, error: this.error,
bucket: 'Hydro', bucket: 'Hydro',
dir: this.config.path,
}; };
} }
} }
@@ -400,7 +401,7 @@ export async function apply(ctx: Context, config: ReturnType<typeof FileSetting>
service = new LocalStorageService(config); service = new LocalStorageService(config);
} }
await service.start(); await service.start();
ctx.inject(['server'], ({ server }) => { await ctx.inject(['server'], ({ server }) => {
let endpoint = config.endPoint; let endpoint = config.endPoint;
if (config.type === 's3' && !config.pathStyle) { if (config.type === 's3' && !config.pathStyle) {
try { try {
+31 -9
View File
@@ -2,18 +2,29 @@ import yaml from 'js-yaml';
import Schema from 'schemastery'; import Schema from 'schemastery';
import { Context, Service } from './context'; import { Context, Service } from './context';
import { Logger } from './logger'; import { Logger } from './logger';
import {
AccountSetting, DomainSetting, DomainUserSetting, PreferenceSetting, SystemSetting,
} from './model/setting';
const logger = new Logger('settings'); const logger = new Logger('settings');
declare module './context' { declare module 'cordis' {
interface Context { interface Context {
config: ConfigService; setting: SettingService;
} }
} }
export class ConfigService extends Service { const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
function method(this: Service, ...args: Parameters<F>) {
this.ctx.effect(() => {
const res = origFunc(...args);
return () => (disposeFunc ? disposeFunc(res) : res());
});
};
export class SettingService extends Service {
static inject = ['db']; static inject = ['db'];
static name = 'config'; static name = 'setting';
static blacklist = ['__proto__', 'prototype', 'constructor']; static blacklist = ['__proto__', 'prototype', 'constructor'];
settings: Schema[] = []; settings: Schema[] = [];
@@ -23,8 +34,14 @@ export class ConfigService extends Service {
private initialValues = {}; private initialValues = {};
private _lastMigrate = Promise.resolve(); private _lastMigrate = Promise.resolve();
PreferenceSetting = T(PreferenceSetting);
AccountSetting = T(AccountSetting);
DomainSetting = T(DomainSetting);
DomainUserSetting = T(DomainUserSetting);
SystemSetting = T(SystemSetting);
constructor(ctx: Context) { constructor(ctx: Context) {
super(ctx, 'config'); super(ctx, 'setting');
} }
async [Context.init]() { async [Context.init]() {
@@ -54,7 +71,7 @@ export class ConfigService extends Service {
applyDelta(source: any, key: string, value: any) { applyDelta(source: any, key: string, value: any) {
const path = key.split('.'); const path = key.split('.');
if (path.some((i) => ConfigService.blacklist.includes(i))) return false; if (path.some((i) => SettingService.blacklist.includes(i))) return false;
const t = path.pop(); const t = path.pop();
const root = JSON.parse(JSON.stringify(source)); const root = JSON.parse(JSON.stringify(source));
let cursor = root; let cursor = root;
@@ -111,9 +128,9 @@ export class ConfigService extends Service {
await this._lastMigrate; await this._lastMigrate;
} }
get(key: string) { _get(key: string) {
const parts = key.split('.'); const parts = key.split('.');
if (parts.some((p) => ConfigService.blacklist.includes(p.toString()))) throw new Error('Invalid path'); if (parts.some((p) => SettingService.blacklist.includes(p.toString()))) throw new Error('Invalid path');
let currentValue = this.applied; let currentValue = this.applied;
for (const p of parts) { for (const p of parts) {
if (!currentValue) return undefined; if (!currentValue) return undefined;
@@ -122,6 +139,11 @@ export class ConfigService extends Service {
return currentValue; return currentValue;
} }
get(key: string) {
return (this.ctx ? this.ctx.domain?.config?.[key.replace(/\./g, '$')] : null)
?? (this._get(key) || global.Hydro?.model?.system?.get?.(key));
}
async setConfig(key: string, value: any) { async setConfig(key: string, value: any) {
const newConfig = this.applyDelta(this.systemConfig, key, value); const newConfig = this.applyDelta(this.systemConfig, key, value);
await this.saveConfig(newConfig); await this.saveConfig(newConfig);
@@ -150,7 +172,7 @@ export class ConfigService extends Service {
}); });
const that = this; const that = this;
const getAccess = (path: (string | symbol)[]) => { const getAccess = (path: (string | symbol)[]) => {
if (path.some((p) => ConfigService.blacklist.includes(p.toString()))) throw new Error('Invalid path'); if (path.some((p) => SettingService.blacklist.includes(p.toString()))) throw new Error('Invalid path');
let currentValue = curValue; let currentValue = curValue;
for (const p of path) { for (const p of path) {
currentValue = currentValue[p]; currentValue = currentValue[p];
+1 -1
View File
@@ -290,7 +290,7 @@ export const coreScripts: MigrationScript[] = [
'file.pathStyle', 'file.endPointForUser', 'file.endPointForJudge', 'file.pathStyle', 'file.endPointForUser', 'file.endPointForJudge',
] as any[]) as any; ] as any[]) as any;
if ((endPoint && accessKey) || process.env.MINIO_ACCESS_KEY) { if ((endPoint && accessKey) || process.env.MINIO_ACCESS_KEY) {
await app.get('config').setConfig('file', { await app.get('setting').setConfig('file', {
type: 's3', type: 's3',
endPoint: process.env.MINIO_ACCESS_KEY ? 'http://127.0.0.1:9000/' : endPoint, endPoint: process.env.MINIO_ACCESS_KEY ? 'http://127.0.0.1:9000/' : endPoint,
accessKey: process.env.MINIO_ACCESS_KEY || accessKey, accessKey: process.env.MINIO_ACCESS_KEY || accessKey,
+4 -1
View File
@@ -26,7 +26,10 @@ const ProblemSchema = Schema.object({
})), })),
hint: StringValue, hint: StringValue,
source: Schema.union([StringValue, Schema.string()]), source: Schema.union([StringValue, Schema.string()]),
display_id: Schema.string(), display_id: Schema.transform(
Schema.union([Schema.number(), Schema.string()]),
(value) => (typeof value === 'number' ? `P${value.toString()}` : value),
),
time_limit: Schema.union([Schema.number(), Schema.string()]).required(), time_limit: Schema.union([Schema.number(), Schema.string()]).required(),
memory_limit: Schema.union([Schema.number(), Schema.string()]).required(), memory_limit: Schema.union([Schema.number(), Schema.string()]).required(),
spj: Schema.union([ spj: Schema.union([
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/import-qduoj", "name": "@hydrooj/import-qduoj",
"version": "1.6.0-beta.1", "version": "1.6.0-beta.2",
"description": "Import QDUOJ problem export", "description": "Import QDUOJ problem export",
"main": "index.ts", "main": "index.ts",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "@hydrooj/migrate", "name": "@hydrooj/migrate",
"version": "0.5.0-beta.1", "version": "0.5.0-beta.2",
"main": "index.ts", "main": "index.ts",
"repository": "git@github.com:hydro-dev/Hydro.git", "repository": "git@github.com:hydro-dev/Hydro.git",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"mariadb": "^3.4.2", "mariadb": "^3.4.2",
"mongodb": "^6.16.0", "mongodb": "^6.17.0",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
+3 -2
View File
@@ -115,7 +115,7 @@ export async function run({
const precheck = await UserModel.getMulti({ unameLower: { $in: udocs.map((u) => u.username.toLowerCase()) } }).toArray(); const precheck = await UserModel.getMulti({ unameLower: { $in: udocs.map((u) => u.username.toLowerCase()) } }).toArray();
if (precheck.length) throw new Error(`Conflict username: ${precheck.map((u) => u.unameLower).join(', ')}`); if (precheck.length) throw new Error(`Conflict username: ${precheck.map((u) => u.unameLower).join(', ')}`);
for (const udoc of udocs) { for (const udoc of udocs) {
if (randomMail === 'always') delete udoc.email; if (randomMail === 'always' || !udoc.email.includes('@')) delete udoc.email;
let current = await UserModel.getByEmail(domainId, udoc.email || `${udoc.username}@syzoj.local`); let current = await UserModel.getByEmail(domainId, udoc.email || `${udoc.username}@syzoj.local`);
current ||= await UserModel.getByUname(domainId, udoc.username); current ||= await UserModel.getByUname(domainId, udoc.username);
if (current && randomMail === 'needed') { if (current && randomMail === 'needed') {
@@ -147,6 +147,7 @@ export async function run({
displayName: udoc.nickname || '', displayName: udoc.nickname || '',
nSubmit: udoc.submit_num, nSubmit: udoc.submit_num,
nAccept: udoc.ac_num, nAccept: udoc.ac_num,
join: true,
}); });
} }
} }
@@ -359,7 +360,7 @@ export async function run({
} }
} }
} }
if (rdoc.type) { if (rdoc.type && rdoc.type_info && tidMap[rdoc.type_info]) {
data.contest = new ObjectId(tidMap[rdoc.type_info]); data.contest = new ObjectId(tidMap[rdoc.type_info]);
await ContestModel.attend(domainId, data.contest, uidMap[rdoc.user_id]).catch(noop); await ContestModel.attend(domainId, data.contest, uidMap[rdoc.user_id]).catch(noop);
} }
+1 -1
View File
@@ -118,7 +118,7 @@ export function apply(ctx: Context) {
let oid = 1; let oid = 1;
for (const i of organizations) orgId[i] = `org-${oid++}`; for (const i of organizations) orgId[i] = `org-${oid++}`;
const duration = moment(tdoc.endAt).diff(tdoc.beginAt, 'seconds'); const duration = moment(tdoc.endAt).diff(tdoc.beginAt, 'seconds');
const lockDuration = tdoc.lockAt ? moment(tdoc.lockAt).diff(tdoc.beginAt, 'seconds') : null; const lockDuration = tdoc.lockAt ? moment(tdoc.endAt).diff(tdoc.lockAt, 'seconds') : null;
const eventfeed: Record<string, any>[] = [ const eventfeed: Record<string, any>[] = [
getFeed('contest', { getFeed('contest', {
id: tdoc._id.toHexString(), id: tdoc._id.toHexString(),
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "@hydrooj/onsite-toolkit", "name": "@hydrooj/onsite-toolkit",
"version": "0.1.0-beta.1", "version": "0.1.0-beta.2",
"dependencies": { "dependencies": {
"@react-spring/web": "^9.7.5", "@react-spring/web": "^10.0.1",
"react-use": "^17.6.0" "react-use": "^17.6.0"
} }
} }
@@ -7,10 +7,10 @@ const highlighterPage = new AutoloadPage('highlighterPage', () => {
function runHighlight($container) { function runHighlight($container) {
$container.find('pre code').get().forEach((code) => { $container.find('pre code').get().forEach((code) => {
const language = ($(code).attr('class') || '').trim(); const language = ($(code).attr('class') || '').trim();
if (language.includes('line-numbers')) $(code).parent().addClass('line-numbers');
const m = language.match(/language-([a-z0-9]+)(\|[\d,-]+)/); const m = language.match(/language-([a-z0-9]+)(\|[\d,-]+)/);
if (m?.[2]) { if (m?.[2]) {
$(code).parent().attr('data-line', m[2].substring(1)); $(code).parent().attr('data-line', m[2].substring(1));
if (language.includes('line-numbers')) $(code).parent().addClass('line-numbers');
$(code).attr('class', `language-${m[1]}`); $(code).attr('class', `language-${m[1]}`);
} }
}); });
@@ -64,10 +64,7 @@ let scratchpad: ScratchpadService;
export default function ScratchpadContainer() { export default function ScratchpadContainer() {
const store = useStore(); const store = useStore();
if (!scratchpad) { scratchpad ||= new ScratchpadService(store);
scratchpad = new ScratchpadService(store);
ctx.provide('scratchpad', scratchpad);
}
const [, updateState] = React.useState<any>(); const [, updateState] = React.useState<any>();
const forceUpdate = React.useCallback(() => updateState({}), []); const forceUpdate = React.useCallback(() => updateState({}), []);
React.useEffect(() => { React.useEffect(() => {
+1 -1
View File
@@ -76,7 +76,7 @@ class MarkdownHandler extends Handler {
class SystemConfigSchemaHandler extends Handler { class SystemConfigSchemaHandler extends Handler {
async get() { async get() {
const schema = convert(Schema.intersect(this.ctx.config.settings) as any, true); const schema = convert(Schema.intersect(this.ctx.setting.settings) as any, true);
this.response.body = schema; this.response.body = schema;
} }
} }
+1
View File
@@ -4,6 +4,7 @@ __langname: 简体中文
'{0} comments': '{0} 条评论' '{0} comments': '{0} 条评论'
'{0} discussions': '{0} 条讨论' '{0} discussions': '{0} 条讨论'
'{0} is a moderator of this domain.': '{}是这个域的管理者之一。' '{0} is a moderator of this domain.': '{}是这个域的管理者之一。'
'{0} is a superuser.': '{0}是超级管理员。'
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。' '{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
'{0} problems': '{0} 道题' '{0} problems': '{0} 道题'
'{0} sections': '{0} 小节' '{0} sections': '{0} 小节'
+22 -23
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.58.0-beta.2", "version": "4.58.0-beta.3",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "index.ts", "main": "index.ts",
@@ -12,17 +12,17 @@
}, },
"devDependencies": { "devDependencies": {
"@blueprintjs/core": "^5.19.0", "@blueprintjs/core": "^5.19.0",
"@fontsource/dm-mono": "^5.2.5", "@fontsource/dm-mono": "^5.2.6",
"@fontsource/fira-code": "^5.2.6", "@fontsource/fira-code": "^5.2.6",
"@fontsource/inconsolata": "^5.2.5", "@fontsource/inconsolata": "^5.2.6",
"@fontsource/jetbrains-mono": "^5.2.5", "@fontsource/jetbrains-mono": "^5.2.6",
"@fontsource/pt-mono": "^5.2.5", "@fontsource/pt-mono": "^5.2.6",
"@fontsource/roboto-mono": "^5.2.5", "@fontsource/roboto-mono": "^5.2.6",
"@fontsource/source-code-pro": "^5.2.5", "@fontsource/source-code-pro": "^5.2.6",
"@fontsource/ubuntu-mono": "^5.2.5", "@fontsource/ubuntu-mono": "^5.2.6",
"@hydrooj/utils": "workspace:^", "@hydrooj/utils": "workspace:^",
"@sentry/browser": "^9.17.0", "@sentry/browser": "^9.27.0",
"@sentry/webpack-plugin": "^3.4.0", "@sentry/webpack-plugin": "^3.5.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",
@@ -34,8 +34,8 @@
"@types/nunjucks": "^3.2.6", "@types/nunjucks": "^3.2.6",
"@types/pickadate": "^3.5.35", "@types/pickadate": "^3.5.35",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react": "^19.1.3", "@types/react": "^18.3.23",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^18.3.7",
"@types/redux-logger": "^3.0.13", "@types/redux-logger": "^3.0.13",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@types/webpack-env": "^1.18.8", "@types/webpack-env": "^1.18.8",
@@ -49,12 +49,12 @@
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"copy-webpack-plugin": "^13.0.0", "copy-webpack-plugin": "^13.0.0",
"cordis": "4.0.0-beta.2", "cordis": "4.0.0-beta.5",
"core-js": "^3.42.0", "core-js": "^3.43.0",
"core-js-compat": "^3.42.0", "core-js-compat": "^3.43.0",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"diff": "^7.0.0", "diff": "^8.0.2",
"diff-dom": "^5.1.4", "diff-dom": "^5.2.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"emojis-keywords": "2.0.0", "emojis-keywords": "2.0.0",
"emojis-list": "2.1.0", "emojis-list": "2.1.0",
@@ -66,7 +66,7 @@
"jquery.easing": "^1.4.1", "jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12", "jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2", "matchmedia-polyfill": "^0.3.2",
"md-editor-rt": "^5.5.1", "md-editor-rt": "^5.6.1",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"monaco-editor": "0.52.2", "monaco-editor": "0.52.2",
@@ -98,10 +98,9 @@
"redux-promise-middleware": "^6.2.0", "redux-promise-middleware": "^6.2.0",
"redux-thunk": "^3.1.0", "redux-thunk": "^3.1.0",
"rupture": "^0.7.1", "rupture": "^0.7.1",
"sass": "^1.87.0", "sass": "^1.89.1",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"schemastery": "^3.16.1", "schemastery": "^3.16.1",
"schemastery-react": "^0.1.2",
"shorty.js": "^1.0.1", "shorty.js": "^1.0.1",
"slideout": "^1.0.1", "slideout": "^1.0.1",
"sticky-kit": "^1.1.3", "sticky-kit": "^1.1.3",
@@ -115,15 +114,15 @@
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"web-streams-polyfill": "^4.1.0", "web-streams-polyfill": "^4.1.0",
"webpack": "^5.99.8", "webpack": "^5.99.9",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "^5.2.1", "webpack-dev-server": "^5.2.2",
"webpack-manifest-plugin": "^5.0.1", "webpack-manifest-plugin": "^5.0.1",
"webpackbar": "^7.0.0" "webpackbar": "^7.0.0"
}, },
"dependencies": { "dependencies": {
"@hydrooj/common": "workspace:^", "@hydrooj/common": "workspace:^",
"ansi_up": "^6.0.5", "ansi_up": "^6.0.6",
"cssfilter": "^0.0.11", "cssfilter": "^0.0.11",
"esbuild": "0.25.2", "esbuild": "0.25.2",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
@@ -7,6 +7,10 @@ import { NamedPage } from 'vj/misc/Page';
import { i18n, request } from 'vj/utils'; import { i18n, request } from 'vj/utils';
const page = new NamedPage('manage_config', async () => { const page = new NamedPage('manage_config', async () => {
if (document.documentElement.className.includes('theme--dark')) {
document.documentElement.className += ' dark';
}
const [{ ConfigEditor, ComponentsProvider }, { load }] = await Promise.all([ const [{ ConfigEditor, ComponentsProvider }, { load }] = await Promise.all([
import('@hydrooj/components'), import('@hydrooj/components'),
import('vj/components/monaco/loader'), import('vj/components/monaco/loader'),
@@ -35,6 +39,7 @@ const page = new NamedPage('manage_config', async () => {
onSave={(value) => { onSave={(value) => {
request.post('', { value }).then(() => { request.post('', { value }).then(() => {
Notification.success(i18n('Changes saved successfully')); Notification.success(i18n('Changes saved successfully'));
window.location.reload();
}).catch((e) => { }).catch((e) => {
Notification.error(i18n('Failed to save changes:'), e.message); Notification.error(i18n('Failed to save changes:'), e.message);
}); });
+1
View File
@@ -95,6 +95,7 @@ function shouldCachePath(path: string) {
if (!path.split('?')[0].split('/').pop()) return false; if (!path.split('?')[0].split('/').pop()) return false;
if (!path.split('?')[0].split('/').pop().includes('.')) return false; if (!path.split('?')[0].split('/').pop().includes('.')) return false;
if (process.env.NODE_ENV !== 'production' && (path.includes('.hot-update.') || path.includes('?version='))) return false; if (process.env.NODE_ENV !== 'production' && (path.includes('.hot-update.') || path.includes('?version='))) return false;
if (path.includes('?v=')) return false;
return true; return true;
} }
function shouldCache(request: Request) { function shouldCache(request: Request) {
@@ -142,12 +142,12 @@
{% if handler.user.hasPerm(reply_post_perm) %} {% if handler.user.hasPerm(reply_post_perm) %}
<a href="javascript:;" data-tooltip="{{ _('Reply') }}" data-op="reply" data-type="reply"><span class="icon icon-reply"></span></a> <a href="javascript:;" data-tooltip="{{ _('Reply') }}" data-op="reply" data-type="reply"><span class="icon icon-reply"></span></a>
{% endif %} {% endif %}
{% if (handler.user.hasPerm(reply_edit_self_perm) and handler.user.own(doc)) or handler.user.hasPerm(reply_edit_perm) %} {% if (handler.user.hasPerm(reply_edit_self_perm) and handler.user.own(rdoc)) or handler.user.hasPerm(reply_edit_perm) %}
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" data-op="edit" data-type="reply" <a href="javascript:;" data-tooltip="{{ _('Edit') }}" data-op="edit" data-type="reply"
data-form="{{ {operation: reply_edit_op, psid: doc._id, psrid: rdoc._id}|json }}" data-form="{{ {operation: reply_edit_op, psid: doc._id, psrid: rdoc._id}|json }}"
><span class="icon icon-edit"></span></a> ><span class="icon icon-edit"></span></a>
{% endif %} {% endif %}
{% if (handler.user.hasPerm(reply_delete_self_perm) and handler.user.own(doc)) or handler.user.hasPerm(reply_delete_perm) %} {% if (handler.user.hasPerm(reply_delete_self_perm) and handler.user.own(rdoc)) or handler.user.hasPerm(reply_delete_perm) %}
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" data-op="delete" data-type="reply" <a href="javascript:;" data-tooltip="{{ _('Delete') }}" data-op="delete" data-type="reply"
data-form="{{ {operation: reply_delete_op, psid: doc._id, psrid: rdoc._id}|json }}" data-form="{{ {operation: reply_delete_op, psid: doc._id, psrid: rdoc._id}|json }}"
><span class="icon icon-delete"></span></a> ><span class="icon icon-delete"></span></a>
@@ -25,7 +25,7 @@
<li class="footer__category__item"><a href="https://github.com/hydro-dev/Hydro" target="_blank">{{ _('Open Source') }}</a></li> <li class="footer__category__item"><a href="https://github.com/hydro-dev/Hydro" target="_blank">{{ _('Open Source') }}</a></li>
</ul></div> </ul></div>
</div> </div>
<div class="medium-3 large-2 columns footer__category expandable end"> <div class="medium-3 large-2 columns footer__category expandable">
<h1> <h1>
{{ _('Support') }} {{ _('Support') }}
<span class="expand-icon"> <span class="expand-icon">
@@ -34,7 +34,21 @@
</h1> </h1>
<div class="footer__category__expander"><ul class="footer__category__list"> <div class="footer__category__expander"><ul class="footer__category__list">
<li class="footer__category__item"><a href="{{ url('wiki_help') }}">{{ _('Help') }}</a></li> <li class="footer__category__item"><a href="{{ url('wiki_help') }}">{{ _('Help') }}</a></li>
<li class="footer__category__item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('QQ Group') }}</a></li> <li class="footer__category__item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Contact Us') }}</a></li>
</ul></div>
</div>
<div class="medium-3 large-2 columns footer__category expandable end">
<h1>
{{ _('About') }}
<span class="expand-icon">
<span class="icon icon-expand_more"></span>
</span>
</h1>
<div class="footer__category__expander"><ul class="footer__category__list">
<li class="footer__category__item"><a href="{{ url('wiki_about') }}">{{ _('About') }}</a></li>
<li class="footer__category__item"><a href="{{ url('wiki_about', anchor='privacy') }}">{{ _('Privacy') }}</a></li>
<li class="footer__category__item"><a href="{{ url('wiki_about', anchor='tos') }}">{{ _('Terms of Service') }}</a></li>
<li class="footer__category__item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Copyright Complaint') }}</a></li>
</ul></div> </ul></div>
</div> </div>
</div> </div>
@@ -42,11 +56,13 @@
<div class="footer__extra-link clearfix"> <div class="footer__extra-link clearfix">
<div class="footer__extra-left"> <div class="footer__extra-left">
<ol class="clearfix"> <ol class="clearfix">
{% if not show_topics|default(true) %}
<li class="footer__extra-link-item"><a href="{{ url('wiki_about') }}">{{ _('About') }}</a></li> <li class="footer__extra-link-item"><a href="{{ url('wiki_about') }}">{{ _('About') }}</a></li>
<li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Contact Us') }}</a></li> <li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Contact Us') }}</a></li>
<li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='privacy') }}">{{ _('Privacy') }}</a></li> <li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='privacy') }}">{{ _('Privacy') }}</a></li>
<li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='tos') }}">{{ _('Terms of Service') }}</a></li> <li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='tos') }}">{{ _('Terms of Service') }}</a></li>
<li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Copyright Complaint') }}</a></li> <li class="footer__extra-link-item"><a href="{{ url('wiki_about', anchor='contact') }}">{{ _('Copyright Complaint') }}</a></li>
{% endif %}
<li class="footer__extra-link-item nojs--hide" data-dropdown-target="#menu-footer-lang"> <li class="footer__extra-link-item nojs--hide" data-dropdown-target="#menu-footer-lang">
<span><span class="icon icon-global"></span> Language <span class="icon icon-expand_less"></span></span> <span><span class="icon icon-global"></span> Language <span class="icon icon-expand_less"></span></span>
<ol class="dropdown-target menu" id="menu-footer-lang"> <ol class="dropdown-target menu" id="menu-footer-lang">
+4 -4
View File
@@ -8,14 +8,14 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.5.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.1.3", "@types/react": "^18.3.23",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.9.0", "@vitejs/plugin-react-swc": "^3.10.1",
"vite": "^6.3.5" "vite": "^6.3.5"
} }
} }
+14 -4
View File
@@ -5,8 +5,7 @@ import { Duplex, PassThrough, Writable } from 'stream';
import { inspect } from 'util'; import { inspect } from 'util';
import type { Entry, ZipReader } from '@zip.js/zip.js'; import type { Entry, ZipReader } from '@zip.js/zip.js';
import fs from 'fs-extra'; import fs from 'fs-extra';
import moment, { isMoment, Moment } from 'moment-timezone'; import type { Moment } from 'moment';
import { ObjectId } from 'mongodb';
import { Exporter, Factory, Logger as Reggol } from 'reggol'; import { Exporter, Factory, Logger as Reggol } from 'reggol';
import type * as superagent from 'superagent'; import type * as superagent from 'superagent';
export * as yaml from 'js-yaml'; export * as yaml from 'js-yaml';
@@ -25,8 +24,8 @@ factory.addExporter(new Exporter.Console({
margin: 1, margin: 1,
}, },
timestamp: Date.now(), timestamp: Date.now(),
levels: { default: process.env.DEV ? 3 : 2 },
})); }));
factory.exporters.get(1).levels = { default: process.env.DEV ? 3 : 2 };
function createLogger(name: string) { function createLogger(name: string) {
return factory.createLogger(name); return factory.createLogger(name);
@@ -34,7 +33,6 @@ function createLogger(name: string) {
export type Logger = Reggol & { new(name: string): Reggol & Logger }; export type Logger = Reggol & { new(name: string): Reggol & Logger };
export const Logger = createLogger as any as Logger; export const Logger = createLogger as any as Logger;
export { moment };
const encrypt = (algorithm, content) => crypto.createHash(algorithm).update(content).digest('hex'); const encrypt = (algorithm, content) => crypto.createHash(algorithm).update(content).digest('hex');
export const sha1 = (content: string) => encrypt('sha1', content); export const sha1 = (content: string) => encrypt('sha1', content);
@@ -146,6 +144,18 @@ export namespace Time {
} }
export function getObjectID(timestamp: string | Date | Moment, allZero = true) { export function getObjectID(timestamp: string | Date | Moment, allZero = true) {
let isMoment: (x: any) => x is Moment;
let ObjectId: typeof import('bson').ObjectId; // eslint-disable-line
try {
({ ObjectId } = require('bson'));
} catch (e) {
throw new Error('No bson module found');
}
try {
({ isMoment } = require('moment'));
} catch (e) {
throw new Error('No moment module found');
}
let _timestamp: number; let _timestamp: number;
if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime(); if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime();
else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime(); else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime();
+6 -4
View File
@@ -1,22 +1,24 @@
{ {
"name": "@hydrooj/utils", "name": "@hydrooj/utils",
"version": "1.4.34-beta.2", "version": "1.4.34-beta.3",
"description": "hydrooj utils", "description": "hydrooj utils",
"main": "lib/utils.ts", "main": "lib/utils.ts",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"preferUnplugged": true, "preferUnplugged": true,
"optionalPeerDependencies": {
"bson": "^6.17.0",
"moment-timezone": "^0.5.48"
},
"dependencies": { "dependencies": {
"@hydrooj/register": "workspace:^", "@hydrooj/register": "workspace:^",
"cac": "^6.7.14", "cac": "^6.7.14",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"moment-timezone": "^0.5.48",
"mongodb": "^6.16.0",
"reggol": "^2.1.0", "reggol": "^2.1.0",
"search-query-parser": "^1.6.0", "search-query-parser": "^1.6.0",
"systeminformation": "^5.25.11" "systeminformation": "^5.27.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4" "@types/fs-extra": "^11.0.4"