mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge branch 'master' into monaco
This commit is contained in:
93 files changed
+1411
-733
No files matched your search
@@ -67,6 +67,8 @@ jobs:
|
|||||||
else
|
else
|
||||||
parallel --tty -j+0 yarn ::: lint:ci build test
|
parallel --tty -j+0 yarn ::: lint:ci build test
|
||||||
fi
|
fi
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
run: node -r @hydrooj/register build/publish.ts
|
run: node -r @hydrooj/register build/publish.ts
|
||||||
|
|||||||
@@ -111,6 +111,34 @@ Hydro 用户群:1085853538
|
|||||||
<details>
|
<details>
|
||||||
<summary><h2>更新日志(点击展开)</h2></summary>
|
<summary><h2>更新日志(点击展开)</h2></summary>
|
||||||
|
|
||||||
|
### Hydro 4.13.0 / UI 4.51.0
|
||||||
|
- ui: 添加 sentry
|
||||||
|
- core&ui: 支持显示当前编译器版本
|
||||||
|
- core: 数据库索引优化
|
||||||
|
- core: 文件复制使用软链接
|
||||||
|
- a11y: 添加性能测试工具
|
||||||
|
- utils: 支持识别大写文件扩展名
|
||||||
|
- ui: 优化下拉菜单样式
|
||||||
|
- core: 优化页面标题
|
||||||
|
- register: 支持从文件读取原 sourcemap
|
||||||
|
- ui: 修复站内消息推送
|
||||||
|
- ui: ranking 页面添加说明
|
||||||
|
- ui: 添加生成测试数据提示
|
||||||
|
- framework: 从 core 解离
|
||||||
|
- core: loader: 支持多 profile 切换
|
||||||
|
- ui: 修复比赛计分板选手组显示
|
||||||
|
- core: 重设比赛分数时自动重算分数
|
||||||
|
|
||||||
|
本版本同时引入下述插件 API 修改:
|
||||||
|
|
||||||
|
- 移除了 app/load/${category} 钩子
|
||||||
|
- 移除了 ctx.app (请使用 ctx.root)
|
||||||
|
- 移除了 ctx.options (请使用 ctx.root.config)
|
||||||
|
- serializer: 移除 showDisplayName 参数
|
||||||
|
- 移除了 loader.addScript, loader.addon 函数
|
||||||
|
- 移除了 Hydro.module.render (请使用 ctx.server.registerRenderer)
|
||||||
|
- 移除了 lib/paginate 和 lib/rank (请使用 db.paginate 和 db.ranked)
|
||||||
|
|
||||||
### Hydro 4.12.3 / UI 4.50.2
|
### Hydro 4.12.3 / UI 4.50.2
|
||||||
- core: 将 API 模块移入 service
|
- core: 将 API 模块移入 service
|
||||||
- core: 比赛时提交被 hack 不触发整体重测
|
- core: 比赛时提交被 hack 不触发整体重测
|
||||||
@@ -634,7 +662,8 @@ Hydro 用户群:1085853538
|
|||||||
|
|
||||||
## 开源许可
|
## 开源许可
|
||||||
|
|
||||||
本项目中的 examples/ install/ packages/ui-default/ 下的内容仅采用 AGPL-3.0 进行授权。
|
本项目中 framework/ examples/ install/ 下的内容采用 MIT 协议授权,您可自由使用。
|
||||||
|
本项目中 packages/ui-default/ 下的内容仅采用 AGPL-3.0 进行授权。
|
||||||
项目其余部分使用双重许可:
|
项目其余部分使用双重许可:
|
||||||
|
|
||||||
1. 您可以在遵守 AGPL-3.0 许可证和下述附加条款章节的前提下免费使用这些代码:
|
1. 您可以在遵守 AGPL-3.0 许可证和下述附加条款章节的前提下免费使用这些代码:
|
||||||
|
|||||||
+6
-21
@@ -72,7 +72,7 @@ for (const name of ['plugins', 'modules']) {
|
|||||||
|
|
||||||
const modules = [
|
const modules = [
|
||||||
'packages/hydrooj',
|
'packages/hydrooj',
|
||||||
...['packages', 'framework', 'modules'].flatMap((i) => fs.readdirSync(path.resolve(process.cwd(), i)).map((j) => `${i}/${j}`)),
|
...['packages', 'framework'].flatMap((i) => fs.readdirSync(path.resolve(process.cwd(), i)).map((j) => `${i}/${j}`)),
|
||||||
].filter((i) => !i.includes('/.') && !i.includes('ui-default')).filter((i) => fs.statSync(path.resolve(process.cwd(), i)).isDirectory());
|
].filter((i) => !i.includes('/.') && !i.includes('ui-default')).filter((i) => fs.statSync(path.resolve(process.cwd(), i)).isDirectory());
|
||||||
|
|
||||||
const UIConfig = {
|
const UIConfig = {
|
||||||
@@ -80,22 +80,17 @@ const UIConfig = {
|
|||||||
'packages/ui-default/public',
|
'packages/ui-default/public',
|
||||||
'**/node_modules',
|
'**/node_modules',
|
||||||
],
|
],
|
||||||
include: ['ts', 'tsx']
|
include: ['ts', 'tsx', 'vue', 'json']
|
||||||
.flatMap((ext) => ['plugins', 'modules']
|
.flatMap((ext) => ['plugins']
|
||||||
.flatMap((name) => [`${name}/**/public/**/*.${ext}`, `${name}/**/frontend/**/*.${ext}`])
|
.flatMap((name) => [`${name}/**/public/**/*.${ext}`, `${name}/**/frontend/**/*.${ext}`])
|
||||||
.concat(`packages/ui-default/**/*.${ext}`)),
|
.concat(`packages/ui-default/**/*.${ext}`)),
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
experimentalDecorators: true,
|
...compilerOptionsBase,
|
||||||
esModuleInterop: true,
|
module: 'ESNext',
|
||||||
resolveJsonModule: true,
|
|
||||||
jsx: 'react',
|
|
||||||
module: 'commonjs',
|
|
||||||
skipLibCheck: true,
|
skipLibCheck: true,
|
||||||
allowSyntheticDefaultImports: true,
|
allowSyntheticDefaultImports: true,
|
||||||
target: 'es2020',
|
|
||||||
baseUrl: '.',
|
baseUrl: '.',
|
||||||
outDir: path.join(baseOutDir, 'ui'),
|
outDir: path.join(baseOutDir, 'ui'),
|
||||||
moduleResolution: 'node',
|
|
||||||
paths: {
|
paths: {
|
||||||
'vj/*': [
|
'vj/*': [
|
||||||
'./packages/ui-default/*',
|
'./packages/ui-default/*',
|
||||||
@@ -120,17 +115,7 @@ const pluginsConfig = {
|
|||||||
],
|
],
|
||||||
exclude,
|
exclude,
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
target: 'es2020',
|
...compilerOptionsBase,
|
||||||
module: 'commonjs',
|
|
||||||
esModuleInterop: true,
|
|
||||||
moduleResolution: 'node',
|
|
||||||
jsx: 'react',
|
|
||||||
sourceMap: false,
|
|
||||||
composite: true,
|
|
||||||
strictBindCallApply: true,
|
|
||||||
resolveJsonModule: true,
|
|
||||||
experimentalDecorators: true,
|
|
||||||
incremental: true,
|
|
||||||
rootDir: '.',
|
rootDir: '.',
|
||||||
baseUrl: '.',
|
baseUrl: '.',
|
||||||
outDir: path.join(baseOutDir, 'plugins'),
|
outDir: path.join(baseOutDir, 'plugins'),
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
移除了 app/listen, app/load/${category} 钩子
|
|
||||||
移除了 ctx.app (请使用 ctx.root)
|
|
||||||
移除了 ctx.options (请使用 ctx.root.config)
|
|
||||||
serializer: 移除 showDisplayName 参数
|
|
||||||
移除了 loader.addScript, loader.addon 函数
|
|
||||||
移除了 Hydro.module.render (请使用 ctx.server.registerRenderer)
|
|
||||||
File renamed without changes.
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/eslint-config",
|
"name": "@hydrooj/eslint-config",
|
||||||
"version": "1.0.10",
|
"version": "1.0.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "react.yaml",
|
"main": "react.yaml",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stylistic/eslint-plugin": "^2.1.0",
|
"@stylistic/eslint-plugin": "^2.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||||
"@typescript-eslint/parser": "^7.12.0",
|
"@typescript-eslint/parser": "^7.15.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^5.0.1",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||||
"eslint-plugin-react": "^7.34.2",
|
"eslint-plugin-react": "^7.34.3",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.0"
|
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">= 8"
|
"eslint": ">= 8"
|
||||||
File renamed without changes.
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/framework",
|
"name": "@hydrooj/framework",
|
||||||
"version": "0.0.2",
|
"version": "0.0.4",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"cordis": "*"
|
"cordis": "*"
|
||||||
},
|
},
|
||||||
@@ -15,18 +16,18 @@
|
|||||||
"koa-router": "^12.0.1",
|
"koa-router": "^12.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"path-to-regexp": "^6.2.2",
|
"path-to-regexp": "^7.0.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"saslprep": "^1.0.3",
|
"saslprep": "^1.0.3",
|
||||||
"shorty.js": "^1.0.1",
|
"shorty.js": "^1.0.1",
|
||||||
"ws": "^8.17.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@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.4",
|
"@types/lodash": "^4.17.6",
|
||||||
"@types/saslprep": "^1.0.3",
|
"@types/saslprep": "^1.0.3",
|
||||||
"@types/ws": "^8.5.10"
|
"@types/ws": "^8.5.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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 { Shorty } from 'shorty.js';
|
import { Shorty } from 'shorty.js';
|
||||||
import WebSocket from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import {
|
import {
|
||||||
Counter, errorMessage, isClass, Logger, parseMemoryMB,
|
Counter, errorMessage, isClass, Logger, parseMemoryMB,
|
||||||
} from '@hydrooj/utils/lib/utils';
|
} from '@hydrooj/utils/lib/utils';
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
import { Router } from './router';
|
import { Router } from './router';
|
||||||
import serializer from './serializer';
|
import serializer from './serializer';
|
||||||
|
|
||||||
|
export { WebSocket, WebSocketServer } from 'ws';
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||||
export function encodeRFC5987ValueChars(str: string) {
|
export function encodeRFC5987ValueChars(str: string) {
|
||||||
return (
|
return (
|
||||||
@@ -91,7 +93,7 @@ export const koa = new Koa<Koa.DefaultState, KoaContext>({
|
|||||||
});
|
});
|
||||||
export const router = new Router();
|
export const router = new Router();
|
||||||
export const httpServer = http.createServer(koa.callback());
|
export const httpServer = http.createServer(koa.callback());
|
||||||
export const wsServer = new WebSocket.Server({ server: httpServer });
|
export const wsServer = new WebSocketServer({ server: httpServer });
|
||||||
koa.on('error', (error) => {
|
koa.on('error', (error) => {
|
||||||
if (error.code !== 'EPIPE' && error.code !== 'ECONNRESET' && !error.message.includes('Parse Error')) {
|
if (error.code !== 'EPIPE' && error.code !== 'ECONNRESET' && !error.message.includes('Parse Error')) {
|
||||||
logger.error('Koa app-level error', { error });
|
logger.error('Koa app-level error', { error });
|
||||||
@@ -322,6 +324,7 @@ export class WebService extends Service {
|
|||||||
HandlerCommon = HandlerCommon;
|
HandlerCommon = HandlerCommon;
|
||||||
Handler = Handler;
|
Handler = Handler;
|
||||||
ConnectionHandler = ConnectionHandler;
|
ConnectionHandler = ConnectionHandler;
|
||||||
|
handlerCtxBase = this.ctx;
|
||||||
|
|
||||||
constructor(ctx: Context, public config: WebServiceConfig) {
|
constructor(ctx: Context, public config: WebServiceConfig) {
|
||||||
super(ctx, 'server', true);
|
super(ctx, 'server', true);
|
||||||
@@ -420,6 +423,9 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
}
|
}
|
||||||
socket.close();
|
socket.close();
|
||||||
});
|
});
|
||||||
|
this.ctx.inject(['server'], (c) => {
|
||||||
|
this.handlerCtxBase = c;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listen() {
|
async listen() {
|
||||||
@@ -438,7 +444,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
private async handleHttp(ctx: KoaContext, HandlerClass, checker) {
|
private async handleHttp(ctx: KoaContext, HandlerClass, checker) {
|
||||||
const { args } = ctx.HydroContext;
|
const { args } = ctx.HydroContext;
|
||||||
Object.assign(args, ctx.params);
|
Object.assign(args, ctx.params);
|
||||||
const h = new HandlerClass(ctx, this.ctx);
|
const h = new HandlerClass(ctx, this.handlerCtxBase);
|
||||||
ctx.handler = h;
|
ctx.handler = h;
|
||||||
const method = ctx.method.toLowerCase();
|
const method = ctx.method.toLowerCase();
|
||||||
try {
|
try {
|
||||||
@@ -511,7 +517,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
|
|
||||||
private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer) {
|
private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer) {
|
||||||
const { args } = ctx.HydroContext;
|
const { args } = ctx.HydroContext;
|
||||||
const h = new HandlerClass(ctx, this.ctx);
|
const h = new HandlerClass(ctx, this.handlerCtxBase);
|
||||||
await this.ctx.parallel('connection/create', h);
|
await this.ctx.parallel('connection/create', h);
|
||||||
ctx.handler = h;
|
ctx.handler = h;
|
||||||
h.conn = conn;
|
h.conn = conn;
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ require.extensions['.js'] = function loader(module, filename) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filename, 'utf-8');
|
const content = fs.readFileSync(filename, 'utf-8');
|
||||||
|
const lastLine = content.trim().split('\n').pop();
|
||||||
|
if (lastLine.startsWith('//# sourceMappingURL=data:application/json;base64,')) {
|
||||||
|
const info = lastLine.split('//# sourceMappingURL=data:application/json;base64,')[1];
|
||||||
|
const payload = JSON.parse(Buffer.from(info, 'base64').toString());
|
||||||
|
map[filename] = payload;
|
||||||
|
}
|
||||||
return module._compile(content, filename);
|
return module._compile(content, filename);
|
||||||
} catch (e) { // ESM
|
} catch (e) { // ESM
|
||||||
return module._compile(transform(filename), filename);
|
return module._compile(transform(filename), filename);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/register",
|
"name": "@hydrooj/register",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
+4
-4
@@ -49,14 +49,14 @@
|
|||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-import-resolver-webpack": "^0.13.8",
|
"eslint-import-resolver-webpack": "^0.13.8",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"globby": "^14.0.1",
|
"globby": "^14.0.2",
|
||||||
"inspectpack": "^4.7.1",
|
"inspectpack": "^4.7.1",
|
||||||
"mongodb-memory-server": "^9.3.0",
|
"mongodb-memory-server": "^9.4.0",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^17.0.0",
|
||||||
"ora": "^8.0.1",
|
"ora": "^8.0.1",
|
||||||
"package-json": "^10.0.0",
|
"package-json": "^10.0.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"simple-git": "^3.24.0",
|
"simple-git": "^3.25.0",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"wtfnode": "^0.9.2"
|
"wtfnode": "^0.9.2"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Context, UserModel } from 'hydrooj';
|
import { Context, Schema, UserModel } from 'hydrooj';
|
||||||
|
import { startPerformanceTest } from './performance-test';
|
||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
// Auto authorize user 2 as super admin
|
// Auto authorize user 2 as super admin
|
||||||
ctx.on('handler/after/UserRegisterWithCode#post', async (that) => {
|
ctx.on('handler/after/UserRegisterWithCode#post', async (that) => {
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/a11y",
|
"name": "@hydrooj/a11y",
|
||||||
"version": "0.0.1"
|
"version": "0.0.2"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import {
|
||||||
|
ProblemModel, RecordModel, sleep, STATUS, yaml,
|
||||||
|
} from 'hydrooj';
|
||||||
|
|
||||||
|
// These tests are from https://loj.ac/d/425
|
||||||
|
const head = '#include<cstdio>\nusing namespace std;\n';
|
||||||
|
const TESTS = {
|
||||||
|
' 1 ': `
|
||||||
|
int main(){
|
||||||
|
int a=1000000000,b=1;
|
||||||
|
while(a)b<<=1,a--;
|
||||||
|
printf("%d\\n",b);`,
|
||||||
|
|
||||||
|
' 2 ': `
|
||||||
|
const int MX=50000000;
|
||||||
|
int p[MX],m[MX],pc;
|
||||||
|
int main(){
|
||||||
|
for(int i=2;i<MX;i++){
|
||||||
|
if(!m[i])p[++pc]=m[i]=i;
|
||||||
|
static int k;
|
||||||
|
for(int j=1;j<=pc&&p[j]<=m[i]&&(k=p[j]*i)<MX;j++)m[k]=p[j];
|
||||||
|
}
|
||||||
|
int ans=0;
|
||||||
|
for(int i=1;i<=pc;i++)ans^=p[i];
|
||||||
|
printf("%d\\n",ans);`,
|
||||||
|
|
||||||
|
' 3 ': `
|
||||||
|
const int MX=1000;
|
||||||
|
int G[MX][MX];
|
||||||
|
int sed=0;
|
||||||
|
inline int rand(){return sed=(sed*sed*73+sed*233+19260817)&0x0000ffff;}
|
||||||
|
int main(){
|
||||||
|
for(int i=0;i<MX;i++)
|
||||||
|
for(int j=0;j<MX;j++)
|
||||||
|
G[i][j]=rand();
|
||||||
|
for(int i=0;i<MX;i++)
|
||||||
|
for(int j=0;j<MX;j++)
|
||||||
|
for(int k=0;k<MX;k++)
|
||||||
|
if(G[j][k]>G[j][i]+G[i][k])G[j][k]=G[j][i]+G[i][k];
|
||||||
|
int ans=0;
|
||||||
|
for(int i=0;i<MX;i++)
|
||||||
|
for(int j=0;j<MX;j++)
|
||||||
|
ans^=G[i][j];
|
||||||
|
printf("%d\\n",ans);`,
|
||||||
|
|
||||||
|
' 4 ': `
|
||||||
|
#include<algorithm>
|
||||||
|
#include<set>
|
||||||
|
const int MX=1000000;
|
||||||
|
int sed=0;
|
||||||
|
inline int rand(){return sed=(sed*sed*73+sed*233+19260817);}
|
||||||
|
int main(){
|
||||||
|
set<int>S;
|
||||||
|
for(int i=0;i<MX;i++)S.insert(rand());
|
||||||
|
int ans=0;
|
||||||
|
for(set<int>::iterator it=S.begin();it!=S.end();it++)ans^=*it;
|
||||||
|
printf("%d\\n",ans);`,
|
||||||
|
|
||||||
|
// skipping test 5 by default as it contains a lot of ram
|
||||||
|
' 5 ': `
|
||||||
|
const int MX=20000000;
|
||||||
|
int *it[MX];
|
||||||
|
int main(){
|
||||||
|
for(int i=0;i<MX;i++)it[i]=new int;
|
||||||
|
for(int i=0;i<MX;i++)*it[i]=i;
|
||||||
|
int ans=0;
|
||||||
|
for(int i=0;i<MX;i++)ans^=*it[i];
|
||||||
|
printf("%d\\n",ans);`,
|
||||||
|
|
||||||
|
'6-1': `
|
||||||
|
const int MX=1<<25;
|
||||||
|
int a[MX];
|
||||||
|
inline unsigned int rand(){static unsigned int sed=0;return (sed=(sed*233+19260421))&(MX-1);}
|
||||||
|
int main(){
|
||||||
|
for(int i=0;i<MX;i++)a[rand()]=i;`,
|
||||||
|
|
||||||
|
'6-2': `
|
||||||
|
const int MX=1<<25;
|
||||||
|
int a[MX];
|
||||||
|
inline unsigned int rand(){static unsigned int sed=0;return (sed=(sed*(MX+1)+1025))&(MX-1);}
|
||||||
|
int main(){
|
||||||
|
for(int i=0;i<MX;i++)a[rand()]=i;`,
|
||||||
|
|
||||||
|
'6-3': `
|
||||||
|
const int MX=1<<25;
|
||||||
|
int a[MX];
|
||||||
|
inline unsigned int rand(){static unsigned int sed=0;return (sed=(sed*(MX+1)+1))&(MX-1);}
|
||||||
|
int main(){
|
||||||
|
for(int i=0;i<MX;i++)a[rand()]=i;`,
|
||||||
|
|
||||||
|
'7-1': `
|
||||||
|
typedef unsigned long long ull;
|
||||||
|
#define P 1000000007
|
||||||
|
const int MX=100000000;
|
||||||
|
int main(){
|
||||||
|
ull ans=1;
|
||||||
|
for(int i=1;i<MX;i++)ans=ans*i%P;
|
||||||
|
printf("%llu\\n",ans);`,
|
||||||
|
|
||||||
|
'7-2': `
|
||||||
|
typedef unsigned long long ull;
|
||||||
|
int P=1000000007;
|
||||||
|
const int MX=100000000;
|
||||||
|
int main(){
|
||||||
|
ull ans=1;
|
||||||
|
for(int i=1;i<MX;i++)ans=ans*i%P;
|
||||||
|
printf("%llu\\n",ans);`,
|
||||||
|
|
||||||
|
' 8 ': `
|
||||||
|
const int MX=20000000;
|
||||||
|
int main(){
|
||||||
|
double ans=0.61234567898765,t=1,s=0;
|
||||||
|
for(int i=1;i<MX;i++)s+=(t*=ans);
|
||||||
|
printf("%f\\n",s);`,
|
||||||
|
|
||||||
|
'9-1': `
|
||||||
|
typedef unsigned int uint;
|
||||||
|
const int MX=1<<10;
|
||||||
|
uint a[MX][MX],b[MX][MX];
|
||||||
|
inline uint rand(){static unsigned int sed=0;return (sed=(sed*233+19260421))&(MX-1);}
|
||||||
|
int main(){
|
||||||
|
register int i,j,k;
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
a[i][j]=rand();
|
||||||
|
#define A(t) (b[i][k+t]+=a[i][j]*a[j][k+t])
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
for(k=0;k<MX;k++)
|
||||||
|
A(0);
|
||||||
|
#undef A
|
||||||
|
uint s;
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
s+=a[i][j];
|
||||||
|
printf("%u\\n",s);`,
|
||||||
|
|
||||||
|
'9-2': `
|
||||||
|
typedef unsigned int uint;
|
||||||
|
const int MX=1<<10;
|
||||||
|
uint a[MX][MX],b[MX][MX];
|
||||||
|
inline uint rand(){static unsigned int sed=0;return (sed=(sed*233+19260421))&(MX-1);}
|
||||||
|
int main(){
|
||||||
|
register int i,j,k;
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
a[i][j]=rand();
|
||||||
|
#define A(t) (b[i][k+t]+=a[i][j]*a[j][k+t])
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
for(k=0;k<MX;k+=8)
|
||||||
|
A(0),A(1),A(2),A(3),A(4),A(5),A(6),A(7);
|
||||||
|
#undef A
|
||||||
|
uint s;
|
||||||
|
for(i=0;i<MX;i++)
|
||||||
|
for(j=0;j<MX;j++)
|
||||||
|
s+=a[i][j];
|
||||||
|
printf("%u\\n",s);`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function startPerformanceTest(args: { enable5: boolean }, report) {
|
||||||
|
const docId = (await ProblemModel.get('system', 'PTEST'))?.docId
|
||||||
|
|| await ProblemModel.add('system', 'PTEST', 'Performance Test', 'test only', 1, [], { hidden: true });
|
||||||
|
await ProblemModel.addTestdata('system', docId, '1.in', Buffer.from('1'));
|
||||||
|
await ProblemModel.addTestdata('system', docId, '1.out', Buffer.from(''));
|
||||||
|
await ProblemModel.addTestdata('system', docId, 'config.yaml', Buffer.from(yaml.dump({
|
||||||
|
time: '3s',
|
||||||
|
memory: args.enable5 ? '2g' : '512m',
|
||||||
|
cases: new Array(20).fill({
|
||||||
|
input: '1.in',
|
||||||
|
output: '1.out',
|
||||||
|
}),
|
||||||
|
})));
|
||||||
|
report({ message: 'Running tests...' });
|
||||||
|
const results = {};
|
||||||
|
await Promise.all(Object.keys(TESTS).map(async (key) => {
|
||||||
|
if (key === ' 5 ' && !args.enable5) return;
|
||||||
|
const id = await RecordModel.add('system', docId, 1, 'cc.cc14o2', `// TEST ${key}\n${head}${TESTS[key]}\nreturn 0;}`, true);
|
||||||
|
while ([
|
||||||
|
STATUS.STATUS_WAITING, STATUS.STATUS_JUDGING, STATUS.STATUS_FETCHED, STATUS.STATUS_COMPILING,
|
||||||
|
].includes((await RecordModel.get('system', id))?.status)) {
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
const result = await RecordModel.get('system', id);
|
||||||
|
if (result.status !== STATUS.STATUS_ACCEPTED && result.status !== STATUS.STATUS_WRONG_ANSWER) {
|
||||||
|
report({ message: `Test ${key} failed (${id}) ${result.status}` });
|
||||||
|
} else {
|
||||||
|
results[key] = result.testCases.map((i) => i.time);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const formatL = (t: number, width = 4) => Math.floor(t).toString().padEnd(width);
|
||||||
|
const formatR = (t: number, width = 4) => Math.floor(t).toString().padStart(width);
|
||||||
|
for (const key of Object.keys(TESTS)) {
|
||||||
|
const timeArr = results[key];
|
||||||
|
if (!timeArr) continue;
|
||||||
|
const avgTime = Math.sum(...timeArr) / 20;
|
||||||
|
await report({
|
||||||
|
message: [
|
||||||
|
`-------Test ${key}-------`,
|
||||||
|
` Avg: ${formatL(avgTime)} D: ${formatR(Math.sum(...timeArr.map((i) => (i - avgTime) ** 2)) / 20, 5)} `,
|
||||||
|
` Max: ${formatL(Math.max(...timeArr))} Min: ${formatR(Math.min(...timeArr))} `,
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/elastic-search",
|
"name": "@hydrooj/elastic-search",
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro",
|
"repository": "https://github.com/hydro-dev/Hydro",
|
||||||
"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.13.1"
|
"@elastic/elasticsearch": "^8.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "3.3.13",
|
"version": "3.3.15",
|
||||||
"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/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.14",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"p-queue": "^8.0.1",
|
"p-queue": "^8.0.1",
|
||||||
"schemastery": "^3.14.6",
|
"schemastery": "^3.14.6",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.1",
|
||||||
"superagent": "^9.0.2",
|
"superagent": "^9.0.2",
|
||||||
"ws": "^8.17.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||||
|
import { runQueued } from './sandbox';
|
||||||
|
|
||||||
|
export async function compilerVersions(langs: Record<string, LangConfig>) {
|
||||||
|
const result = {};
|
||||||
|
for (const lang in langs) {
|
||||||
|
const version = langs[lang].version;
|
||||||
|
if (!version) continue;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const res = await runQueued(version, {
|
||||||
|
copyIn: {},
|
||||||
|
time: 10000,
|
||||||
|
memory: 256 * 1024 * 1024,
|
||||||
|
}, 5);
|
||||||
|
result[lang] = `${res.stdout}\n${res.stderr}`.trim();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fs } from '@hydrooj/utils';
|
import { fs } from '@hydrooj/utils';
|
||||||
|
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||||
import {
|
import {
|
||||||
JudgeHandler, JudgeResultBody, ObjectId, RecordModel, SettingModel,
|
db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel,
|
||||||
StorageModel, SystemModel, TaskModel,
|
SettingModel, StorageModel, SystemModel, TaskModel,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
|
import { langs } from 'hydrooj/src/model/setting';
|
||||||
|
import { compilerVersions } from '../compiler';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { FormatError, SystemError } from '../error';
|
import { FormatError, SystemError } from '../error';
|
||||||
import { Context } from '../judge/interface';
|
import { Context } from '../judge/interface';
|
||||||
@@ -82,6 +85,7 @@ export async function postInit(ctx) {
|
|||||||
c.check.addChecker('Judge', (_ctx, log, warn, error) => versionCheck(warn, error));
|
c.check.addChecker('Judge', (_ctx, log, warn, error) => versionCheck(warn, error));
|
||||||
});
|
});
|
||||||
await fs.ensureDir(getConfig('tmp_dir'));
|
await fs.ensureDir(getConfig('tmp_dir'));
|
||||||
|
const info = await sysinfo.get();
|
||||||
const handle = async (t) => {
|
const handle = async (t) => {
|
||||||
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
||||||
if (!rdoc) {
|
if (!rdoc) {
|
||||||
@@ -92,9 +96,19 @@ export async function postInit(ctx) {
|
|||||||
};
|
};
|
||||||
const parallelism = Math.max(getConfig('parallelism'), 2);
|
const parallelism = Math.max(getConfig('parallelism'), 2);
|
||||||
const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
|
const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
|
||||||
|
function collectCompilerInfo() {
|
||||||
|
const coll = db.collection('status');
|
||||||
|
compilerVersions(langs).then((compilers) => coll.updateOne(
|
||||||
|
{ mid: info.mid, type: 'server' },
|
||||||
|
{ $set: { compilers } },
|
||||||
|
{ upsert: true },
|
||||||
|
)).catch((e) => logger.error(e));
|
||||||
|
}
|
||||||
ctx.on('system/setting', () => {
|
ctx.on('system/setting', () => {
|
||||||
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
|
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
|
||||||
|
collectCompilerInfo();
|
||||||
});
|
});
|
||||||
|
collectCompilerInfo();
|
||||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||||
TaskModel.consume({ type: 'generate' }, handle);
|
TaskModel.consume({ type: 'generate' }, handle);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { fs, pipeRequest } from '@hydrooj/utils';
|
|||||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||||
import type { JudgeResultBody } from 'hydrooj';
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
|
import { compilerVersions } from '../compiler';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { FormatError, SystemError } from '../error';
|
import { FormatError, SystemError } from '../error';
|
||||||
import { Session } from '../interface';
|
import { Session } from '../interface';
|
||||||
@@ -173,13 +174,21 @@ export default class Hydro implements Session {
|
|||||||
const content = Object.keys(config).length
|
const content = Object.keys(config).length
|
||||||
? JSON.stringify({ key: 'config', ...config })
|
? JSON.stringify({ key: 'config', ...config })
|
||||||
: '{"key":"ping"}';
|
: '{"key":"ping"}';
|
||||||
|
let compilers = {};
|
||||||
|
let compilerVersionCallback = () => { };
|
||||||
this.ws.on('message', (data) => {
|
this.ws.on('message', (data) => {
|
||||||
if (data.toString() === 'ping') {
|
if (data.toString() === 'ping') {
|
||||||
this.ws.send('pong');
|
this.ws.send('pong');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const request = JSON.parse(data.toString());
|
const request = JSON.parse(data.toString());
|
||||||
if (request.language) this.language = request.language;
|
if (request.language) {
|
||||||
|
this.language = request.language;
|
||||||
|
compilerVersions(this.language).then((res) => {
|
||||||
|
compilers = res;
|
||||||
|
compilerVersionCallback();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (request.task) queue.add(() => new JudgeTask(this, request.task).handle().catch((e) => log.error(e)));
|
if (request.task) queue.add(() => new JudgeTask(this, request.task).handle().catch((e) => log.error(e)));
|
||||||
});
|
});
|
||||||
this.ws.on('close', (data, reason) => {
|
this.ws.on('close', (data, reason) => {
|
||||||
@@ -196,10 +205,13 @@ export default class Hydro implements Session {
|
|||||||
this.ws.send('{"key":"start"}');
|
this.ws.send('{"key":"start"}');
|
||||||
if (!this.config.noStatus) {
|
if (!this.config.noStatus) {
|
||||||
const info = await sysinfo.get();
|
const info = await sysinfo.get();
|
||||||
this.ws.send(JSON.stringify({ key: 'status', info }));
|
this.ws.send(JSON.stringify({ key: 'status', info: { ...info } }));
|
||||||
|
compilerVersionCallback = () => {
|
||||||
|
this.ws.send(JSON.stringify({ key: 'status', info: { ...info, compilers } }));
|
||||||
|
};
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const [mid, inf] = await sysinfo.update();
|
const [mid, inf] = await sysinfo.update();
|
||||||
this.ws.send(JSON.stringify({ key: 'status', info: { mid, ...inf } }));
|
this.ws.send(JSON.stringify({ key: 'status', info: { mid, ...inf, compilers } }));
|
||||||
}, 1200000);
|
}, 1200000);
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.12.3",
|
"version": "4.13.1",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
@@ -12,38 +12,37 @@
|
|||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.591.0",
|
"@aws-sdk/client-s3": "3.609.0",
|
||||||
"@aws-sdk/lib-storage": "3.591.0",
|
"@aws-sdk/lib-storage": "3.609.0",
|
||||||
"@aws-sdk/s3-presigned-post": "3.591.0",
|
"@aws-sdk/s3-presigned-post": "3.609.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.591.0",
|
"@aws-sdk/s3-request-presigner": "3.609.0",
|
||||||
"@graphql-tools/schema": "^10.0.4",
|
"@graphql-tools/schema": "^10.0.4",
|
||||||
"@hydrooj/framework": "workspace:^",
|
"@hydrooj/framework": "workspace:^",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"@simplewebauthn/server": "7.4.0",
|
"@simplewebauthn/server": "7.4.0",
|
||||||
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.14",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"cordis": "3.16.0",
|
"cordis": "3.16.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.9.0",
|
||||||
"graphql-scalars": "^1.23.0",
|
"graphql-scalars": "^1.23.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"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.2.2",
|
"lru-cache": "^10.3.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"mongodb": "^5.9.2",
|
"mongodb": "^5.9.2",
|
||||||
"mongodb-uri": "^0.9.7",
|
"mongodb-uri": "^0.9.7",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nodemailer": "^6.9.13",
|
"nodemailer": "^6.9.14",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"p-queue": "^8.0.1",
|
"p-queue": "^8.0.1",
|
||||||
"path-to-regexp": "^6.2.2",
|
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"saslprep": "^1.0.3",
|
"saslprep": "^1.0.3",
|
||||||
"schemastery": "^3.14.6",
|
"schemastery": "^3.14.6",
|
||||||
@@ -51,15 +50,14 @@
|
|||||||
"shorty.js": "^1.0.1",
|
"shorty.js": "^1.0.1",
|
||||||
"superagent": "^9.0.2",
|
"superagent": "^9.0.2",
|
||||||
"tar": "6.2.1",
|
"tar": "6.2.1",
|
||||||
"thirty-two": "^1.0.2",
|
"thirty-two": "^1.0.2"
|
||||||
"ws": "^8.17.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.4.34",
|
"@types/adm-zip": "^0.4.34",
|
||||||
"@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.4",
|
"@types/lodash": "^4.17.6",
|
||||||
"@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.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ langs:
|
|||||||
code_file: foo.sh
|
code_file: foo.sh
|
||||||
execute: /bin/bash foo.sh
|
execute: /bin/bash foo.sh
|
||||||
highlight: bash
|
highlight: bash
|
||||||
|
version: /bin/bash --version
|
||||||
c:
|
c:
|
||||||
compile: /usr/bin/gcc -Wall --std=c99 -o foo foo.c -lm
|
compile: /usr/bin/gcc -Wall --std=c99 -o foo foo.c -lm
|
||||||
highlight: c
|
highlight: c
|
||||||
monaco: c
|
monaco: c
|
||||||
display: C
|
display: C
|
||||||
|
version: /usr/bin/gcc --version
|
||||||
cc:
|
cc:
|
||||||
compile: /usr/bin/g++ -Wall -std=c++14 -o foo foo.cc -lm -I/include
|
compile: /usr/bin/g++ -Wall -std=c++14 -o foo foo.cc -lm -I/include
|
||||||
code_file: foo.cc
|
code_file: foo.cc
|
||||||
@@ -62,6 +64,7 @@ langs:
|
|||||||
display: C++
|
display: C++
|
||||||
address_space_limit: true
|
address_space_limit: true
|
||||||
process_limit: 1
|
process_limit: 1
|
||||||
|
version: /usr/bin/g++ --version
|
||||||
cc.cc98:
|
cc.cc98:
|
||||||
compile: /usr/bin/g++ -Wall -std=c++98 -o foo foo.cc -lm -I/include
|
compile: /usr/bin/g++ -Wall -std=c++98 -o foo foo.cc -lm -I/include
|
||||||
display: C++98
|
display: C++98
|
||||||
@@ -95,6 +98,7 @@ langs:
|
|||||||
compile: /usr/bin/fpc -O2 -o/w/foo foo.pas
|
compile: /usr/bin/fpc -O2 -o/w/foo foo.pas
|
||||||
highlight: pascal
|
highlight: pascal
|
||||||
display: Pascal
|
display: Pascal
|
||||||
|
version: /usr/bin/fpc -iW
|
||||||
java:
|
java:
|
||||||
compile: /usr/bin/bash -c "javac -d /w -encoding utf8 ./Main.java && jar cvf Main.jar *.class >/dev/null"
|
compile: /usr/bin/bash -c "javac -d /w -encoding utf8 ./Main.java && jar cvf Main.jar *.class >/dev/null"
|
||||||
code_file: Main.java
|
code_file: Main.java
|
||||||
@@ -105,6 +109,7 @@ langs:
|
|||||||
monaco: java
|
monaco: java
|
||||||
display: Java
|
display: Java
|
||||||
compile_time_limit: 30000
|
compile_time_limit: 30000
|
||||||
|
version: /usr/bin/javac -version
|
||||||
kt:
|
kt:
|
||||||
compile: echo "Invalid language selection"
|
compile: echo "Invalid language selection"
|
||||||
code_file: foo.kt
|
code_file: foo.kt
|
||||||
@@ -119,6 +124,7 @@ langs:
|
|||||||
execute: /usr/bin/java -jar foo.jar
|
execute: /usr/bin/java -jar foo.jar
|
||||||
display: Kotlin/JVM
|
display: Kotlin/JVM
|
||||||
compile_time_limit: 30000
|
compile_time_limit: 30000
|
||||||
|
version: /usr/bin/kotlinc -version
|
||||||
py:
|
py:
|
||||||
compile: /usr/bin/python -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
|
compile: /usr/bin/python -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
|
||||||
code_file: foo.py
|
code_file: foo.py
|
||||||
@@ -132,38 +138,47 @@ langs:
|
|||||||
compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
|
compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
|
||||||
execute: /usr/bin/python3 foo
|
execute: /usr/bin/python3 foo
|
||||||
display: Python 3
|
display: Python 3
|
||||||
|
version: /usr/bin/python3 --version
|
||||||
py.pypy3:
|
py.pypy3:
|
||||||
compile: /bin/bash -c "/usr/bin/pypy3 -c \"import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)\" && mv foo.py foo"
|
compile: /bin/bash -c "/usr/bin/pypy3 -c \"import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)\" && mv foo.py foo"
|
||||||
execute: /bin/bash -c 'mv foo foo.py && /usr/bin/pypy3 -B foo.py'
|
execute: /bin/bash -c 'mv foo foo.py && /usr/bin/pypy3 -B foo.py'
|
||||||
display: PyPy3
|
display: PyPy3
|
||||||
|
version: /usr/bin/pypy3 --version
|
||||||
php:
|
php:
|
||||||
execute: /usr/bin/php foo.php
|
execute: /usr/bin/php foo.php
|
||||||
display: PHP
|
display: PHP
|
||||||
|
version: /usr/bin/php -v
|
||||||
rs:
|
rs:
|
||||||
compile: /usr/bin/rustc -O -o /w/foo /w/foo.rs
|
compile: /usr/bin/rustc -O -o /w/foo /w/foo.rs
|
||||||
highlight: rust
|
highlight: rust
|
||||||
display: Rust
|
display: Rust
|
||||||
|
version: /usr/bin/rustc --version
|
||||||
hs:
|
hs:
|
||||||
compile: /usr/bin/ghc -O -outputdir /tmp -o foo foo.hs
|
compile: /usr/bin/ghc -O -outputdir /tmp -o foo foo.hs
|
||||||
highlight: haskell
|
highlight: haskell
|
||||||
display: Haskell
|
display: Haskell
|
||||||
|
version: /usr/bin/ghc --version
|
||||||
js:
|
js:
|
||||||
execute: /usr/bin/node /w/foo.js
|
execute: /usr/bin/node /w/foo.js
|
||||||
highlight: javascript
|
highlight: javascript
|
||||||
display: NodeJS
|
display: NodeJS
|
||||||
|
version: /usr/bin/node --version
|
||||||
go:
|
go:
|
||||||
compile: /usr/bin/go build -o foo foo.go
|
compile: /usr/bin/go build -o foo foo.go
|
||||||
display: Golang
|
display: Golang
|
||||||
|
version: /usr/bin/go version
|
||||||
rb:
|
rb:
|
||||||
execute: /usr/bin/ruby foo.rb
|
execute: /usr/bin/ruby foo.rb
|
||||||
highlight: ruby
|
highlight: ruby
|
||||||
display: Ruby
|
display: Ruby
|
||||||
|
version: /usr/bin/ruby --version
|
||||||
cs:
|
cs:
|
||||||
compile: /usr/bin/mcs -optimize+ -out:/w/foo /w/foo.cs
|
compile: /usr/bin/mcs -optimize+ -out:/w/foo /w/foo.cs
|
||||||
execute: /usr/bin/mono foo
|
execute: /usr/bin/mono foo
|
||||||
highlight: csharp
|
highlight: csharp
|
||||||
monaco: csharp
|
monaco: csharp
|
||||||
display: 'C#'
|
display: 'C#'
|
||||||
|
version: /usr/bin/mcs --version
|
||||||
r:
|
r:
|
||||||
display: R
|
display: R
|
||||||
execute: /bin/sh -c "cat /dev/stdin >stdin && /usr/bin/Rscript --vanilla foo.r"
|
execute: /bin/sh -c "cat /dev/stdin >stdin && /usr/bin/Rscript --vanilla foo.r"
|
||||||
|
|||||||
@@ -1,102 +1,39 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import cac from 'cac';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { Context } from 'koa';
|
|
||||||
import { MongoClient, WriteConcern } from 'mongodb';
|
import { MongoClient, WriteConcern } from 'mongodb';
|
||||||
import mongoUri from 'mongodb-uri';
|
import mongoUri from 'mongodb-uri';
|
||||||
import { Context as PluginContext } from '../context';
|
import { readline } from '../ui';
|
||||||
|
|
||||||
const listenPort = cac().parse().options.port || 8888;
|
export async function load() {
|
||||||
let resolve;
|
console.error('Config file not found.');
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
async function get(ctx: Context) {
|
console.error('Please fill in ~/.hydro/config.json with MongoDB connection string in the following format:');
|
||||||
ctx.body = `<!DOCTYPE html>
|
console.error('{"url":"mongodb://localhost:27017/hydro"}');
|
||||||
<html data-page="setup" data-layout="immersive" class="layout--immersive page--setup nojs">
|
process.exit(1);
|
||||||
<head>
|
}
|
||||||
<meta charset="UTF-8">
|
// eslint-disable-next-line no-constant-condition
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
while (true) {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
console.error('Please type your MongoDB connection string below:');
|
||||||
<link rel="stylesheet" media="all" href="/default.theme.css">
|
console.error('Example: mongodb://localhost:27017/hydro');
|
||||||
<title>Setup</title>
|
try {
|
||||||
</head>
|
const uri = await readline();
|
||||||
<body>
|
if (!uri) throw new Error('no url found');
|
||||||
<div class="slideout-panel" id="panel">
|
const url = mongoUri.parse(uri);
|
||||||
<div class="main">
|
const Database = await MongoClient.connect(uri, {
|
||||||
<div class="row"><div class="columns">
|
readPreference: 'nearest',
|
||||||
<div class="immersive--content immersive--center">
|
writeConcern: new WriteConcern('majority'),
|
||||||
<h1>Setup</h1>
|
});
|
||||||
<form method="POST">
|
const db = Database.db(url.database);
|
||||||
<div class="row"><div class="columns">
|
const coll = db.collection<any>('system');
|
||||||
<label class="inverse material textbox">
|
await coll.updateOne({ _id: '_test' }, { $set: { value: 1 } }, { upsert: true });
|
||||||
MongoDB Connection URI
|
await coll.deleteOne({ _id: '_test' });
|
||||||
<input name="url" type="text" value="mongodb://user:pass@127.0.0.1:27017/hydro" autofocus>
|
fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
|
||||||
</label>
|
fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({ url: uri }));
|
||||||
</div></div>
|
break;
|
||||||
<div class="row"><div class="columns">
|
} catch (e) {
|
||||||
<div class="text-center">
|
console.error(e);
|
||||||
<input type="submit" value="Confirm" class="inverse expanded rounded primary button">
|
}
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
ctx.response.type = 'text/html';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function post(ctx: Context) {
|
|
||||||
try {
|
|
||||||
if (!ctx.request.body?.url) throw new Error('no url found');
|
|
||||||
const url = mongoUri.parse(ctx.request.body.url);
|
|
||||||
const Database = await MongoClient.connect(ctx.request.body.url, {
|
|
||||||
readPreference: 'nearest',
|
|
||||||
writeConcern: new WriteConcern('majority'),
|
|
||||||
});
|
|
||||||
const db = Database.db(url.database);
|
|
||||||
const coll = db.collection<any>('system');
|
|
||||||
await Promise.all([
|
|
||||||
coll.updateOne(
|
|
||||||
{ _id: 'server.url' },
|
|
||||||
{ $set: { value: ctx.request.href } },
|
|
||||||
{ upsert: true },
|
|
||||||
),
|
|
||||||
coll.updateOne(
|
|
||||||
{ _id: 'server.port' },
|
|
||||||
{ $set: { value: parseInt(listenPort as string, 10) } },
|
|
||||||
{ upsert: true },
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
|
|
||||||
fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({ url: ctx.request.body.url }));
|
|
||||||
ctx.body = '<h1>Done! Hydro is now starting.</h1>';
|
|
||||||
resolve?.();
|
|
||||||
} catch (e) {
|
|
||||||
ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function load(ctx: PluginContext) {
|
|
||||||
let dispose;
|
|
||||||
ctx.plugin(require('@hydrooj/framework'), {
|
|
||||||
keys: [Math.random()],
|
|
||||||
proxy: false,
|
|
||||||
upload: false,
|
|
||||||
port: listenPort,
|
|
||||||
});
|
|
||||||
ctx.inject(['server'], ({ server }) => {
|
|
||||||
dispose = server.addServerLayer('setup', (c) => {
|
|
||||||
if (c.request.method.toLowerCase() === 'post') return post(c);
|
|
||||||
return get(c);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return new Promise((r) => {
|
|
||||||
resolve = () => {
|
|
||||||
dispose?.();
|
|
||||||
r(null);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ export async function apply(ctx: Context) {
|
|||||||
const config = load();
|
const config = load();
|
||||||
if (!process.env.CI && !config) {
|
if (!process.env.CI && !config) {
|
||||||
logger.info('Starting setup');
|
logger.info('Starting setup');
|
||||||
await require('./setup').load();
|
await require('./setup').load(ctx);
|
||||||
}
|
}
|
||||||
const pending = global.addons;
|
const pending = global.addons;
|
||||||
const fail = [];
|
const fail = [];
|
||||||
@@ -65,10 +65,10 @@ export async function apply(ctx: Context) {
|
|||||||
for (const h of handlers.filter((i) => i.endsWith('.ts'))) {
|
for (const h of handlers.filter((i) => i.endsWith('.ts'))) {
|
||||||
ctx.loader.reloadPlugin(ctx, path.resolve(handlerDir, h), {}, `hydrooj/handler/${h.split('.')[0]}`);
|
ctx.loader.reloadPlugin(ctx, path.resolve(handlerDir, h), {}, `hydrooj/handler/${h.split('.')[0]}`);
|
||||||
}
|
}
|
||||||
|
ctx.plugin(require('../service/migration').default);
|
||||||
await handler(pending, fail, ctx);
|
await handler(pending, fail, ctx);
|
||||||
await addon(pending, fail, ctx);
|
await addon(pending, fail, ctx);
|
||||||
await ctx.lifecycle.flush();
|
await ctx.lifecycle.flush();
|
||||||
for (const i in global.Hydro.handler) await global.Hydro.handler[i]();
|
|
||||||
const scriptDir = path.resolve(__dirname, '..', 'script');
|
const scriptDir = path.resolve(__dirname, '..', 'script');
|
||||||
for (const h of await fs.readdir(scriptDir)) {
|
for (const h of await fs.readdir(scriptDir)) {
|
||||||
ctx.loader.reloadPlugin(ctx, path.resolve(scriptDir, h), {}, `hydrooj/script/${h.split('.')[0]}`);
|
ctx.loader.reloadPlugin(ctx, path.resolve(scriptDir, h), {}, `hydrooj/script/${h.split('.')[0]}`);
|
||||||
@@ -78,27 +78,18 @@ export async function apply(ctx: Context) {
|
|||||||
await ctx.lifecycle.flush();
|
await ctx.lifecycle.flush();
|
||||||
await ctx.parallel('app/started');
|
await ctx.parallel('app/started');
|
||||||
if (process.env.NODE_APP_INSTANCE === '0') {
|
if (process.env.NODE_APP_INSTANCE === '0') {
|
||||||
const { default: scripts, init } = require('../upgrade');
|
await new Promise((resolve, reject) => {
|
||||||
let dbVer = (await modelSystem.get('db.ver')) ?? 0;
|
ctx.inject(['migration'], async (c) => {
|
||||||
const isFresh = !dbVer;
|
c.migration.registerChannel('hydrooj', require('../upgrade').coreScripts);
|
||||||
const expected = scripts.length;
|
try {
|
||||||
if (isFresh) {
|
await c.migration.doUpgrade();
|
||||||
await init();
|
resolve(null);
|
||||||
await modelSystem.set('db.ver', expected);
|
} catch (e) {
|
||||||
} else {
|
logger.error('Upgrade failed: %O', e);
|
||||||
while (dbVer < expected) {
|
reject(e);
|
||||||
const func = scripts[dbVer];
|
|
||||||
if (typeof func !== 'function') {
|
|
||||||
dbVer++;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
logger.info('Upgrading database: from %d to %d', dbVer, expected);
|
});
|
||||||
const result = await func();
|
});
|
||||||
if (!result) break;
|
|
||||||
dbVer++;
|
|
||||||
await modelSystem.set('db.ver', dbVer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const f of global.addons) {
|
for (const f of global.addons) {
|
||||||
const dir = path.join(f, 'public');
|
const dir = path.join(f, 'public');
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from '../error';
|
} from '../error';
|
||||||
import type { DomainDoc } from '../interface';
|
import type { DomainDoc } from '../interface';
|
||||||
import avatar from '../lib/avatar';
|
import avatar from '../lib/avatar';
|
||||||
import paginate from '../lib/paginate';
|
|
||||||
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
|
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
|
||||||
import * as discussion from '../model/discussion';
|
import * as discussion from '../model/discussion';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
@@ -24,10 +23,10 @@ import { log2 } from '../utils';
|
|||||||
class DomainRankHandler extends Handler {
|
class DomainRankHandler extends Handler {
|
||||||
@query('page', Types.PositiveInt, true)
|
@query('page', Types.PositiveInt, true)
|
||||||
async get(domainId: string, page = 1) {
|
async get(domainId: string, page = 1) {
|
||||||
const [dudocs, upcount, ucount] = await paginate(
|
const [dudocs, upcount, ucount] = await this.paginate(
|
||||||
domain.getMultiUserInDomain(domainId, { uid: { $gt: 1 }, rp: { $gt: 0 } }).sort({ rp: -1 }),
|
domain.getMultiUserInDomain(domainId, { uid: { $gt: 1 }, rp: { $gt: 0 } }).sort({ rp: -1 }),
|
||||||
page,
|
page,
|
||||||
100,
|
'ranking',
|
||||||
);
|
);
|
||||||
const udict = await user.getList(domainId, dudocs.map((dudoc) => dudoc.uid));
|
const udict = await user.getList(domainId, dudocs.map((dudoc) => dudoc.uid));
|
||||||
const udocs = dudocs.map((i) => udict[i.uid]);
|
const udocs = dudocs.map((i) => udict[i.uid]);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PRIV } from '../model/builtin';
|
|||||||
import * as DocumentModel from '../model/document';
|
import * as DocumentModel from '../model/document';
|
||||||
import DomainModel from '../model/domain';
|
import DomainModel from '../model/domain';
|
||||||
import RecordModel from '../model/record';
|
import RecordModel from '../model/record';
|
||||||
|
import * as SettingModel from '../model/setting';
|
||||||
import UserModel from '../model/user';
|
import UserModel from '../model/user';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import { Handler } from '../service/server';
|
import { Handler } from '../service/server';
|
||||||
@@ -25,11 +26,47 @@ async function getStatus() {
|
|||||||
class StatusHandler extends Handler {
|
class StatusHandler extends Handler {
|
||||||
async get() {
|
async get() {
|
||||||
const stats = await getStatus();
|
const stats = await getStatus();
|
||||||
|
const compilers = {};
|
||||||
|
const warn = {};
|
||||||
|
const result: Array<{ key: string[], message: string }> = [];
|
||||||
|
// For each language, select the most common compiler message version,
|
||||||
|
// then merge languages with the same message.
|
||||||
for (const stat of stats) {
|
for (const stat of stats) {
|
||||||
if (!stat.battery.hasBattery) stat.battery = 'No battery';
|
if (!stat.battery?.hasBattery) stat.battery = 'No battery';
|
||||||
else stat.battery = `${stat.battery.type} ${stat.battery.model} ${stat.battery.percent}%${stat.battery.isCharging ? ' Charging' : ''}`;
|
else stat.battery = `${stat.battery.type} ${stat.battery.model} ${stat.battery.percent}%${stat.battery.isCharging ? ' Charging' : ''}`;
|
||||||
|
if (stat.compilers) {
|
||||||
|
for (const key in stat.compilers) {
|
||||||
|
if (!compilers[key]) compilers[key] ||= [];
|
||||||
|
const related = compilers[key].find((i) => i.message === stat.compilers[key]);
|
||||||
|
if (related) related.related.push(stat._id);
|
||||||
|
else {
|
||||||
|
compilers[key].push({
|
||||||
|
related: [stat._id],
|
||||||
|
message: stat.compilers[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.response.body = { stats };
|
for (const key in compilers) {
|
||||||
|
compilers[key].sort((a, b) => b.related.length - a.related.length);
|
||||||
|
const message = compilers[key][0].message;
|
||||||
|
for (let i = 1; i < compilers[key].length; i++) {
|
||||||
|
for (const id of compilers[key][i].related) {
|
||||||
|
warn[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const t = result.find((i) => i.message === message);
|
||||||
|
if (t) t.key.push(key);
|
||||||
|
else result.push({ key: [key], message });
|
||||||
|
}
|
||||||
|
const LANGS = SettingModel.langs;
|
||||||
|
const languages = {};
|
||||||
|
for (const key in LANGS) {
|
||||||
|
if (LANGS[key].hidden) continue;
|
||||||
|
languages[`${LANGS[key].display}(${key})`] = LANGS[key].compile || LANGS[key].execute;
|
||||||
|
}
|
||||||
|
this.response.body = { stats, languages, compilers: result };
|
||||||
this.response.template = 'status.html';
|
this.response.template = 'status.html';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AttestationFormat } from '@simplewebauthn/server/dist/helpers/decodeAttestationObject';
|
import type { AttestationFormat } from '@simplewebauthn/server/dist/helpers/decodeAttestationObject';
|
||||||
import { AuthenticationExtensionsAuthenticatorOutputs } from '@simplewebauthn/server/dist/helpers/decodeAuthenticatorExtensions';
|
import type { AuthenticationExtensionsAuthenticatorOutputs } from '@simplewebauthn/server/dist/helpers/decodeAuthenticatorExtensions';
|
||||||
import { CredentialDeviceType } from '@simplewebauthn/typescript-types';
|
import { CredentialDeviceType } from '@simplewebauthn/typescript-types';
|
||||||
import type fs from 'fs';
|
import type fs from 'fs';
|
||||||
import type { Dictionary, NumericDictionary } from 'lodash';
|
import type { Dictionary, NumericDictionary } from 'lodash';
|
||||||
@@ -644,6 +644,8 @@ export interface FileNode {
|
|||||||
size?: number;
|
size?: number;
|
||||||
/** AutoDelete */
|
/** AutoDelete */
|
||||||
autoDelete?: Date;
|
autoDelete?: Date;
|
||||||
|
/** fileId if linked to an existing file */
|
||||||
|
link?: string;
|
||||||
owner?: number;
|
owner?: number;
|
||||||
operator?: number[];
|
operator?: number[];
|
||||||
meta?: Record<string, string | number>;
|
meta?: Record<string, string | number>;
|
||||||
@@ -777,11 +779,10 @@ export interface ProblemSearchOptions {
|
|||||||
export type ProblemSearch = (domainId: string, q: string, options?: ProblemSearchOptions) => Promise<ProblemSearchResponse>;
|
export type ProblemSearch = (domainId: string, q: string, options?: ProblemSearchOptions) => Promise<ProblemSearchResponse>;
|
||||||
|
|
||||||
export interface Lib extends Record<string, any> {
|
export interface Lib extends Record<string, any> {
|
||||||
difficulty: typeof import('./lib/difficulty');
|
difficulty: typeof import('./lib/difficulty').default;
|
||||||
buildContent: typeof import('./lib/content').buildContent;
|
buildContent: typeof import('./lib/content').buildContent;
|
||||||
mail: typeof import('./lib/mail');
|
mail: typeof import('./lib/mail');
|
||||||
rank: typeof import('./lib/rank');
|
rating: typeof import('./lib/rating').default;
|
||||||
rating: typeof import('./lib/rating');
|
|
||||||
testdataConfig: typeof import('./lib/testdataConfig');
|
testdataConfig: typeof import('./lib/testdataConfig');
|
||||||
problemSearch: ProblemSearch;
|
problemSearch: ProblemSearch;
|
||||||
}
|
}
|
||||||
@@ -807,8 +808,6 @@ export interface ModuleInterfaces {
|
|||||||
export interface HydroGlobal {
|
export interface HydroGlobal {
|
||||||
version: Record<string, string>;
|
version: Record<string, string>;
|
||||||
model: Model;
|
model: Model;
|
||||||
/** @deprecated */
|
|
||||||
handler: Record<string, Function>;
|
|
||||||
script: Record<string, Script>;
|
script: Record<string, Script>;
|
||||||
service: HydroService;
|
service: HydroService;
|
||||||
lib: Lib;
|
lib: Lib;
|
||||||
|
|||||||
@@ -42,5 +42,5 @@ function difficultyAlgorithm(nSubmit: number, nAccept: number) {
|
|||||||
return Math.max(ans, 1);
|
return Math.max(ans, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export = difficultyAlgorithm;
|
export default difficultyAlgorithm;
|
||||||
global.Hydro.lib.difficulty = difficultyAlgorithm;
|
global.Hydro.lib.difficulty = difficultyAlgorithm;
|
||||||
@@ -9,5 +9,5 @@ function hash(password: string, salt: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export = hash;
|
export default hash;
|
||||||
global.Hydro.module.hash.hydro = hash;
|
global.Hydro.module.hash.hydro = hash;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import './i18n';
|
import './i18n';
|
||||||
import './mail';
|
import './mail';
|
||||||
import './hash.hydro';
|
import './hash.hydro';
|
||||||
import './rank';
|
|
||||||
import './ui';
|
import './ui';
|
||||||
import './testdataConfig';
|
import './testdataConfig';
|
||||||
import './difficulty';
|
import './difficulty';
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { FindCursor } from 'mongodb';
|
|
||||||
import { ValidationError } from '../error';
|
|
||||||
import db from '../service/db';
|
|
||||||
|
|
||||||
async function paginate<T>(
|
|
||||||
cursor: FindCursor<T>, page: number, pageSize: number,
|
|
||||||
): Promise<[docs: T[], numPages: number, count: number]> {
|
|
||||||
if (page <= 0) throw new ValidationError('page');
|
|
||||||
let filter = {};
|
|
||||||
for (const key of Object.getOwnPropertySymbols(cursor)) {
|
|
||||||
if (key.toString() !== 'Symbol(filter)') continue;
|
|
||||||
filter = cursor[key];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const coll = db.collection(cursor.namespace.collection as any);
|
|
||||||
const [count, pageDocs] = await Promise.all([
|
|
||||||
Object.keys(filter).length ? coll.count(filter) : coll.countDocuments(filter),
|
|
||||||
cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
|
|
||||||
]);
|
|
||||||
const numPages = Math.floor((count + pageSize - 1) / pageSize);
|
|
||||||
return [pageDocs, numPages, count];
|
|
||||||
}
|
|
||||||
|
|
||||||
export = paginate;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { FindCursor } from 'mongodb';
|
|
||||||
|
|
||||||
type ranked = <T extends Record<string, any>>(cursor: FindCursor<T>, equ: ((a: T, b: T) => boolean)) => Promise<[number, T][]>;
|
|
||||||
|
|
||||||
const ranked: ranked = async (cursor, equ) => {
|
|
||||||
let last = null;
|
|
||||||
let r = 0;
|
|
||||||
let count = 0;
|
|
||||||
const results = [];
|
|
||||||
const docs = await cursor.toArray();
|
|
||||||
for (const doc of docs) {
|
|
||||||
if (doc.unrank) {
|
|
||||||
results.push([0, doc]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
if (!last || !equ(last, doc)) r = count;
|
|
||||||
last = doc;
|
|
||||||
results.push([r, doc]);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
global.Hydro.lib.rank = ranked;
|
|
||||||
export = ranked;
|
|
||||||
@@ -118,6 +118,6 @@ function calculate(users: RatingInputUser[]): RatingOutputUser[] {
|
|||||||
return calculator.calculate() as RatingOutputUser[];
|
return calculator.calculate() as RatingOutputUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export = calculate;
|
export default calculate;
|
||||||
|
|
||||||
global.Hydro.lib.rating = calculate;
|
global.Hydro.lib.rating = calculate;
|
||||||
@@ -5,7 +5,7 @@ import Schema from 'schemastery';
|
|||||||
import superagent from 'superagent';
|
import superagent from 'superagent';
|
||||||
import { Context } from './context';
|
import { Context } from './context';
|
||||||
export { ObjectId, Filter } from 'mongodb';
|
export { ObjectId, Filter } from 'mongodb';
|
||||||
export { WebSocket, WebSocketServer } from 'ws';
|
export { WebSocket, WebSocketServer } from '@hydrooj/framework';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './interface';
|
export * from './interface';
|
||||||
export * from './typeutils';
|
export * from './typeutils';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
||||||
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import ranked from '../lib/rank';
|
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import type { Handler } from '../service/server';
|
import type { Handler } from '../service/server';
|
||||||
@@ -224,7 +223,7 @@ const acm = buildContestRule({
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||||
// Find first accept
|
// Find first accept
|
||||||
@@ -257,7 +256,7 @@ const acm = buildContestRule({
|
|||||||
return [rows, udict];
|
return [rows, udict];
|
||||||
},
|
},
|
||||||
async ranked(tdoc, cursor) {
|
async ranked(tdoc, cursor) {
|
||||||
return await ranked(cursor, (a, b) => a.accept === b.accept && a.time === b.time);
|
return await db.ranked(cursor, (a, b) => a.accept === b.accept && a.time === b.time);
|
||||||
},
|
},
|
||||||
applyProjection(tdoc, rdoc) {
|
applyProjection(tdoc, rdoc) {
|
||||||
if (isDone(tdoc)) return rdoc;
|
if (isDone(tdoc)) return rdoc;
|
||||||
@@ -394,7 +393,7 @@ const oi = buildContestRule({
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => a.score === b.score);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||||
const psdict = {};
|
const psdict = {};
|
||||||
@@ -431,7 +430,7 @@ const oi = buildContestRule({
|
|||||||
return [rows, udict];
|
return [rows, udict];
|
||||||
},
|
},
|
||||||
async ranked(tdoc, cursor) {
|
async ranked(tdoc, cursor) {
|
||||||
return await ranked(cursor, (a, b) => a.score === b.score);
|
return await db.ranked(cursor, (a, b) => a.score === b.score);
|
||||||
},
|
},
|
||||||
applyProjection(tdoc, rdoc) {
|
applyProjection(tdoc, rdoc) {
|
||||||
if (isDone(tdoc)) return rdoc;
|
if (isDone(tdoc)) return rdoc;
|
||||||
@@ -744,7 +743,7 @@ const homework = buildContestRule({
|
|||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => a.score === b.score);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||||
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
|
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
|
||||||
@@ -757,7 +756,7 @@ const homework = buildContestRule({
|
|||||||
return [rows, udict];
|
return [rows, udict];
|
||||||
},
|
},
|
||||||
async ranked(tdoc, cursor) {
|
async ranked(tdoc, cursor) {
|
||||||
return await ranked(cursor, (a, b) => a.score === b.score);
|
return await db.ranked(cursor, (a, b) => a.score === b.score);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ export class ProblemModel {
|
|||||||
static PROJECTION_CONTEST_DETAIL: Field[] = [
|
static PROJECTION_CONTEST_DETAIL: Field[] = [
|
||||||
...ProblemModel.PROJECTION_CONTEST_LIST,
|
...ProblemModel.PROJECTION_CONTEST_LIST,
|
||||||
'content', 'html', 'data', 'config', 'additional_file',
|
'content', 'html', 'data', 'config', 'additional_file',
|
||||||
'reference',
|
'reference', 'maintainer',
|
||||||
];
|
];
|
||||||
|
|
||||||
static PROJECTION_PUBLIC: Field[] = [
|
static PROJECTION_PUBLIC: Field[] = [
|
||||||
...ProblemModel.PROJECTION_LIST,
|
...ProblemModel.PROJECTION_LIST,
|
||||||
'content', 'html', 'data', 'config', 'additional_file',
|
'content', 'html', 'data', 'config', 'additional_file',
|
||||||
'reference',
|
'reference', 'maintainer',
|
||||||
];
|
];
|
||||||
|
|
||||||
static default = {
|
static default = {
|
||||||
@@ -262,7 +262,8 @@ export class ProblemModel {
|
|||||||
const res = await Promise.all([
|
const res = await Promise.all([
|
||||||
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
|
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
|
||||||
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
|
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
|
||||||
storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))),
|
storage.list(`problem/${domainId}/${docId}/`)
|
||||||
|
.then((items) => storage.del(items.map((item) => `problem/${domainId}/${docId}/${item.name}`))),
|
||||||
bus.parallel('problem/delete', domainId, docId),
|
bus.parallel('problem/delete', domainId, docId),
|
||||||
]);
|
]);
|
||||||
await bus.emit('problem/del', domainId, docId);
|
await bus.emit('problem/del', domainId, docId);
|
||||||
@@ -363,7 +364,7 @@ export class ProblemModel {
|
|||||||
domainId: string, pids: number[], canViewHidden: number | boolean = false,
|
domainId: string, pids: number[], canViewHidden: number | boolean = false,
|
||||||
doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, indexByDocIdOnly = false,
|
doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, indexByDocIdOnly = false,
|
||||||
): Promise<ProblemDict> {
|
): Promise<ProblemDict> {
|
||||||
if (!pids?.length) return [];
|
if (!pids?.length) return {};
|
||||||
const r: Record<number, ProblemDoc> = {};
|
const r: Record<number, ProblemDoc> = {};
|
||||||
const l: Record<string, ProblemDoc> = {};
|
const l: Record<string, ProblemDoc> = {};
|
||||||
const q: any = { docId: { $in: pids } };
|
const q: any = { docId: { $in: pids } };
|
||||||
|
|||||||
@@ -280,22 +280,23 @@ export function apply(ctx: Context) {
|
|||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ctx.on('ready', async () => {
|
ctx.on('ready', () => Promise.all([
|
||||||
await db.ensureIndexes(
|
db.ensureIndexes(
|
||||||
RecordModel.coll,
|
RecordModel.coll,
|
||||||
|
{ key: { domainId: 1, pid: 1 }, name: 'delete' },
|
||||||
{ key: { domainId: 1, contest: 1, _id: -1 }, name: 'basic' },
|
{ key: { domainId: 1, contest: 1, _id: -1 }, name: 'basic' },
|
||||||
{ key: { domainId: 1, contest: 1, uid: 1, _id: -1 }, name: 'withUser' },
|
{ key: { domainId: 1, contest: 1, uid: 1, _id: -1 }, name: 'withUser' },
|
||||||
{ key: { domainId: 1, contest: 1, pid: 1, _id: -1 }, name: 'withProblem' },
|
{ key: { domainId: 1, contest: 1, pid: 1, _id: -1 }, name: 'withProblem' },
|
||||||
{ key: { domainId: 1, contest: 1, pid: 1, uid: 1, _id: -1 }, name: 'withUserAndProblem' },
|
{ key: { domainId: 1, contest: 1, pid: 1, uid: 1, _id: -1 }, name: 'withUserAndProblem' },
|
||||||
{ key: { domainId: 1, contest: 1, status: 1, _id: -1 }, name: 'withStatus' },
|
{ key: { domainId: 1, contest: 1, status: 1, _id: -1 }, name: 'withStatus' },
|
||||||
);
|
),
|
||||||
await db.ensureIndexes(
|
db.ensureIndexes(
|
||||||
RecordModel.collStat,
|
RecordModel.collStat,
|
||||||
{ key: { domainId: 1, pid: 1, uid: 1, _id: -1 }, name: 'basic' },
|
{ key: { domainId: 1, pid: 1, uid: 1, _id: -1 }, name: 'basic' },
|
||||||
{ key: { domainId: 1, pid: 1, uid: 1, time: 1 }, name: 'time' },
|
{ key: { domainId: 1, pid: 1, uid: 1, time: 1 }, name: 'time' },
|
||||||
{ key: { domainId: 1, pid: 1, uid: 1, memory: 1 }, name: 'memory' },
|
{ key: { domainId: 1, pid: 1, uid: 1, memory: 1 }, name: 'memory' },
|
||||||
{ key: { domainId: 1, pid: 1, uid: 1, length: 1 }, name: 'length' },
|
{ key: { domainId: 1, pid: 1, uid: 1, length: 1 }, name: 'length' },
|
||||||
);
|
),
|
||||||
});
|
]) as any);
|
||||||
}
|
}
|
||||||
global.Hydro.model.record = RecordModel;
|
global.Hydro.model.record = RecordModel;
|
||||||
@@ -274,6 +274,7 @@ SystemSetting(
|
|||||||
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests per page'),
|
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests per page'),
|
||||||
Setting('setting_basic', 'pagination.discussion', 50, 'number', 'pagination.discussion', 'Discussions per page'),
|
Setting('setting_basic', 'pagination.discussion', 50, 'number', 'pagination.discussion', 'Discussions per page'),
|
||||||
Setting('setting_basic', 'pagination.record', 100, 'number', 'pagination.record', 'Records per page'),
|
Setting('setting_basic', 'pagination.record', 100, 'number', 'pagination.record', 'Records per page'),
|
||||||
|
Setting('setting_basic', 'pagination.ranking', 100, 'number', 'pagination.ranking', 'Users per page'),
|
||||||
Setting('setting_basic', 'pagination.solution', 20, 'number', 'pagination.solution', 'Solutions per page'),
|
Setting('setting_basic', 'pagination.solution', 20, 'number', 'pagination.solution', 'Solutions per page'),
|
||||||
Setting('setting_basic', 'pagination.training', 10, 'number', 'pagination.training', 'Trainings per page'),
|
Setting('setting_basic', 'pagination.training', 10, 'number', 'pagination.training', 'Trainings per page'),
|
||||||
Setting('setting_basic', 'pagination.reply', 50, 'number', 'pagination.reply', 'Replies per page'),
|
Setting('setting_basic', 'pagination.reply', 50, 'number', 'pagination.reply', 'Replies per page'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { escapeRegExp } from 'lodash';
|
import { escapeRegExp, omit } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
@@ -39,7 +39,7 @@ export class StorageModel {
|
|||||||
{ $set: { lastUsage: new Date() } },
|
{ $set: { lastUsage: new Date() } },
|
||||||
{ returnDocument: 'after' },
|
{ returnDocument: 'after' },
|
||||||
);
|
);
|
||||||
return await storage.get(value?._id || path, savePath);
|
return await storage.get(value?.link || value?._id || path, savePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async rename(path: string, newPath: string, operator = 1) {
|
static async rename(path: string, newPath: string, operator = 1) {
|
||||||
@@ -51,6 +51,20 @@ export class StorageModel {
|
|||||||
|
|
||||||
static async del(path: string[], operator = 1) {
|
static async del(path: string[], operator = 1) {
|
||||||
if (!path.length) return;
|
if (!path.length) return;
|
||||||
|
const affected = await StorageModel.coll.find({ path: { $in: path } }).toArray();
|
||||||
|
if (!affected.length) return;
|
||||||
|
const linked = await StorageModel.coll.find({ link: { $in: affected.map((i) => i._id) }, path: { $nin: path } }).toArray();
|
||||||
|
const processedIds = [];
|
||||||
|
for (const i of linked || []) {
|
||||||
|
if (processedIds.includes(i.link)) continue;
|
||||||
|
const current = affected.find((t) => t._id === t.link); // to be deleted
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await Promise.all([
|
||||||
|
StorageModel.coll.updateOne({ _id: current._id }, { $set: omit(i, ['_id']) }),
|
||||||
|
StorageModel.coll.updateOne({ _id: i._id }, { $set: omit(current, ['_id']), $unset: { link: '' } }),
|
||||||
|
]);
|
||||||
|
processedIds.push(i.link);
|
||||||
|
}
|
||||||
const autoDelete = moment().add(7, 'day').toDate();
|
const autoDelete = moment().add(7, 'day').toDate();
|
||||||
await StorageModel.coll.updateMany(
|
await StorageModel.coll.updateMany(
|
||||||
{ path: { $in: path }, autoDelete: null },
|
{ path: { $in: path }, autoDelete: null },
|
||||||
@@ -66,7 +80,7 @@ export class StorageModel {
|
|||||||
autoDelete: null,
|
autoDelete: null,
|
||||||
}).toArray();
|
}).toArray();
|
||||||
return results.map((i) => ({
|
return results.map((i) => ({
|
||||||
...i, name: i.path.split(target)[1], prefix: target,
|
...i, name: i.path.split(target)[1],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +120,10 @@ export class StorageModel {
|
|||||||
// Make sure id is not used
|
// Make sure id is not used
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
while (await StorageModel.coll.findOne({ _id })) _id = StorageModel.generateId(extname(dst));
|
while (await StorageModel.coll.findOne({ _id })) _id = StorageModel.generateId(extname(dst));
|
||||||
const result = await storage.copy(value._id, _id);
|
|
||||||
const { metaData, size, etag } = await storage.getMeta(_id);
|
|
||||||
await StorageModel.coll.insertOne({
|
await StorageModel.coll.insertOne({
|
||||||
_id, meta: metaData, path: dst, size, etag, lastModified: new Date(), owner: value.owner || 1,
|
...value, _id, path: dst, link: value._id, lastModified: new Date(), owner: value.owner || 1,
|
||||||
});
|
});
|
||||||
return result;
|
return _id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ declare module '@hydrooj/framework' {
|
|||||||
interface UserModel extends User { }
|
interface UserModel extends User { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMailLower(mail: string) {
|
export function handleMailLower(mail: string) {
|
||||||
const [n, d] = mail.trim().toLowerCase().split('@');
|
const [n, d] = mail.trim().toLowerCase().split('@');
|
||||||
const [name] = n.split('+');
|
const [name] = n.split('+');
|
||||||
return `${name.replace(/\./g, '')}@${d === 'googlemail.com' ? 'gmail.com' : d}`;
|
return `${name.replace(/\./g, '')}@${d === 'googlemail.com' ? 'gmail.com' : d}`;
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ export { buildContent } from './lib/content';
|
|||||||
export { default as mime } from './lib/mime';
|
export { default as mime } from './lib/mime';
|
||||||
export { default as rating } from './lib/rating';
|
export { default as rating } from './lib/rating';
|
||||||
export { default as avatar } from './lib/avatar';
|
export { default as avatar } from './lib/avatar';
|
||||||
export { default as rank } from './lib/rank';
|
/** @deprecated use Handler.paginate instead */
|
||||||
export { default as paginate } from './lib/paginate';
|
export const paginate = db.paginate.bind(db);
|
||||||
|
/** @deprecated use db.ranked instead */
|
||||||
|
export const rank = db.ranked.bind(db);
|
||||||
export { UiContextBase } from './service/layers/base';
|
export { UiContextBase } from './service/layers/base';
|
||||||
export * from '@hydrooj/framework/decorators';
|
export * from '@hydrooj/framework/decorators';
|
||||||
export * from '@hydrooj/framework/validator';
|
export * from '@hydrooj/framework/validator';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import {
|
import {
|
||||||
Collection, Db, IndexDescription, MongoClient,
|
Collection, Db, FindCursor, IndexDescription, MongoClient,
|
||||||
} from 'mongodb';
|
} from 'mongodb';
|
||||||
import mongoUri from 'mongodb-uri';
|
import mongoUri from 'mongodb-uri';
|
||||||
import { Time } from '@hydrooj/utils';
|
import { Time } from '@hydrooj/utils';
|
||||||
|
import { ValidationError } from '../error';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { load } from '../options';
|
import { load } from '../options';
|
||||||
import * as bus from './bus';
|
import * as bus from './bus';
|
||||||
@@ -102,6 +103,44 @@ class MongoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async paginate<T>(
|
||||||
|
cursor: FindCursor<T>, page: number, pageSize: number,
|
||||||
|
): Promise<[docs: T[], numPages: number, count: number]> {
|
||||||
|
if (page <= 0) throw new ValidationError('page');
|
||||||
|
let filter = {};
|
||||||
|
for (const key of Object.getOwnPropertySymbols(cursor)) {
|
||||||
|
if (key.toString() !== 'Symbol(filter)') continue;
|
||||||
|
filter = cursor[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const coll = this.db.collection(cursor.namespace.collection as any);
|
||||||
|
const [count, pageDocs] = await Promise.all([
|
||||||
|
Object.keys(filter).length ? coll.count(filter) : coll.countDocuments(filter),
|
||||||
|
cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
|
||||||
|
]);
|
||||||
|
const numPages = Math.floor((count + pageSize - 1) / pageSize);
|
||||||
|
return [pageDocs, numPages, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
async ranked<T extends Record<string, any>>(cursor: FindCursor<T>, equ: (a: T, b: T) => boolean): Promise<[number, T][]> {
|
||||||
|
let last = null;
|
||||||
|
let r = 0;
|
||||||
|
let count = 0;
|
||||||
|
const results = [];
|
||||||
|
const docs = await cursor.toArray();
|
||||||
|
for (const doc of docs) {
|
||||||
|
if ((doc as any).unrank) {
|
||||||
|
results.push([0, doc]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
if (!last || !equ(last, doc)) r = count;
|
||||||
|
last = doc;
|
||||||
|
results.push([r, doc]);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
public async apply(ctx) {
|
public async apply(ctx) {
|
||||||
await this.start();
|
await this.start();
|
||||||
ctx.on('dispose', () => this.client.close());
|
ctx.on('dispose', () => this.client.close());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KoaContext } from '@hydrooj/framework';
|
import { KoaContext, NotFoundError } from '@hydrooj/framework';
|
||||||
import BlackListModel from '../../model/blacklist';
|
import BlackListModel from '../../model/blacklist';
|
||||||
import DomainModel from '../../model/domain';
|
import DomainModel from '../../model/domain';
|
||||||
import * as system from '../../model/system';
|
import * as system from '../../model/system';
|
||||||
@@ -24,5 +24,12 @@ export default async (ctx: KoaContext, next) => {
|
|||||||
ctx.domainId = inferDomain?._id || domainId;
|
ctx.domainId = inferDomain?._id || domainId;
|
||||||
ctx.domainInfo = inferDomain || absoluteDomain;
|
ctx.domainInfo = inferDomain || absoluteDomain;
|
||||||
if (ctx.domainInfo && ctx.domainId !== ctx.domainInfo._id) ctx.redirect(ctx.originalPath.replace(/^\/d\/[^/]+\//, `/d/${ctx.domainInfo._id}/`));
|
if (ctx.domainInfo && ctx.domainId !== ctx.domainInfo._id) ctx.redirect(ctx.originalPath.replace(/^\/d\/[^/]+\//, `/d/${ctx.domainInfo._id}/`));
|
||||||
else await next();
|
else {
|
||||||
|
if (!ctx.domainInfo) {
|
||||||
|
ctx.pendingError = new NotFoundError(ctx.domainId);
|
||||||
|
ctx.domainId = 'system';
|
||||||
|
ctx.domainInfo = await DomainModel.get('system');
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { KoaContext } from '@hydrooj/framework';
|
import type { KoaContext } from '@hydrooj/framework';
|
||||||
import { NotFoundError } from '../../error';
|
|
||||||
import { PERM } from '../../model/builtin';
|
import { PERM } from '../../model/builtin';
|
||||||
import UserModel from '../../model/user';
|
import UserModel from '../../model/user';
|
||||||
|
|
||||||
@@ -15,9 +14,5 @@ export default async (ctx: KoaContext, next) => {
|
|||||||
}
|
}
|
||||||
if (user._id === 0) delete user.viewLang;
|
if (user._id === 0) delete user.viewLang;
|
||||||
ctx.HydroContext.user = await user.private();
|
ctx.HydroContext.user = await user.private();
|
||||||
if (!domain) {
|
|
||||||
ctx.pendingError = new NotFoundError(args.domainId);
|
|
||||||
args.domainId = 'system';
|
|
||||||
}
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { Logger } from '@hydrooj/utils';
|
||||||
|
import { Context, Service } from '../context';
|
||||||
|
import * as system from '../model/system';
|
||||||
|
|
||||||
|
export type MigrationScript = null | (() => Promise<boolean | void>);
|
||||||
|
const logger = new Logger('migration');
|
||||||
|
|
||||||
|
declare module '../context' {
|
||||||
|
interface Context {
|
||||||
|
migration: MigrationService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MigrationService extends Service {
|
||||||
|
private channels: Record<string, MigrationScript[]> = {};
|
||||||
|
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
super(ctx, 'migration', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerChannel(name: string, s: MigrationScript[]) {
|
||||||
|
this.channels[name] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpgrade() {
|
||||||
|
for (const channel in this.channels) {
|
||||||
|
const name = `db.ver${channel === 'hydrooj' ? '' : `-${channel}`}`;
|
||||||
|
let dbVer = system.get(name) ?? 0;
|
||||||
|
const scripts = this.channels[channel];
|
||||||
|
const isFresh = !dbVer;
|
||||||
|
const expected = scripts.length;
|
||||||
|
if (isFresh) {
|
||||||
|
await scripts[0]?.();
|
||||||
|
await system.set(name, expected);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (dbVer < expected) {
|
||||||
|
const func = scripts[dbVer];
|
||||||
|
if (typeof func !== 'function') {
|
||||||
|
dbVer++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.info('Upgrading database [%s]: from %d to %d', channel, dbVer, expected);
|
||||||
|
const result = await func();
|
||||||
|
if (!result) break;
|
||||||
|
dbVer++;
|
||||||
|
await system.set(name, dbVer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,13 @@ import { errorMessage, Time } from '@hydrooj/utils';
|
|||||||
import { Context } from '../context';
|
import { Context } from '../context';
|
||||||
import { PermissionError, PrivilegeError } from '../error';
|
import { PermissionError, PrivilegeError } from '../error';
|
||||||
import type { DomainDoc } from '../interface';
|
import type { DomainDoc } from '../interface';
|
||||||
import paginate from '../lib/paginate';
|
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { PERM, PRIV } from '../model/builtin';
|
import { PERM, PRIV } from '../model/builtin';
|
||||||
import * as opcount from '../model/opcount';
|
import * as opcount from '../model/opcount';
|
||||||
import * as OplogModel from '../model/oplog';
|
import * as OplogModel from '../model/oplog';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import { builtinConfig } from '../settings';
|
import { builtinConfig } from '../settings';
|
||||||
|
import db from './db';
|
||||||
import baseLayer from './layers/base';
|
import baseLayer from './layers/base';
|
||||||
import domainLayer from './layers/domain';
|
import domainLayer from './layers/domain';
|
||||||
import userLayer from './layers/user';
|
import userLayer from './layers/user';
|
||||||
@@ -144,8 +144,18 @@ export async function apply(ctx: Context) {
|
|||||||
});
|
});
|
||||||
if (process.env.HYDRO_CLI) return;
|
if (process.env.HYDRO_CLI) return;
|
||||||
ctx.inject(['server'], ({ server, on }) => {
|
ctx.inject(['server'], ({ server, on }) => {
|
||||||
|
let endpoint = builtinConfig.file.endPoint;
|
||||||
|
if (builtinConfig.file.type === 's3' && !builtinConfig.file.pathStyle) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(builtinConfig.file.endPoint);
|
||||||
|
parsed.hostname = `${builtinConfig.file.bucket}.${parsed.hostname}`;
|
||||||
|
endpoint = parsed.toString();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse file endpoint');
|
||||||
|
}
|
||||||
|
}
|
||||||
const proxyMiddleware = proxy('/fs', {
|
const proxyMiddleware = proxy('/fs', {
|
||||||
target: builtinConfig.file.endPoint,
|
target: endpoint,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (p) => p.replace('/fs', ''),
|
rewrite: (p) => p.replace('/fs', ''),
|
||||||
});
|
});
|
||||||
@@ -209,7 +219,7 @@ export async function apply(ctx: Context) {
|
|||||||
? (!host.includes(this.request.host))
|
? (!host.includes(this.request.host))
|
||||||
: this.request.host !== host)
|
: this.request.host !== host)
|
||||||
)) withDomainId ||= domainId;
|
)) withDomainId ||= domainId;
|
||||||
res = this.context.router.url.call(this.context.router, name, args, { query }).toString();
|
res = ctx.server.router.url.call(ctx.server.router, name, args, { query }).toString();
|
||||||
if (anchor) res = `${res}#${anchor}`;
|
if (anchor) res = `${res}#${anchor}`;
|
||||||
if (withDomainId) res = `/d/${withDomainId}${res}`;
|
if (withDomainId) res = `/d/${withDomainId}${res}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -228,7 +238,7 @@ export async function apply(ctx: Context) {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
paginate<T>(cursor: FindCursor<T>, page: number, key: string) {
|
paginate<T>(cursor: FindCursor<T>, page: number, key: string) {
|
||||||
return paginate(cursor, page, this.ctx.setting.get(`pagination.${key}`));
|
return db.paginate(cursor, page, this.ctx.setting.get(`pagination.${key}`));
|
||||||
},
|
},
|
||||||
checkPerm(...args: bigint[]) {
|
checkPerm(...args: bigint[]) {
|
||||||
if (!this.user.hasPerm(...args)) {
|
if (!this.user.hasPerm(...args)) {
|
||||||
@@ -255,7 +265,7 @@ export async function apply(ctx: Context) {
|
|||||||
},
|
},
|
||||||
renderTitle(str: string) {
|
renderTitle(str: string) {
|
||||||
const name = this.ctx.setting.get('server.name');
|
const name = this.ctx.setting.get('server.name');
|
||||||
if (this.UiContext.extraTitleContent) return `${this.translate(str)} - ${this.UiContext.extraTitleContent} - ${name}`;
|
if (this.UiContext.extraTitleContent) return `${this.UiContext.extraTitleContent} - ${this.translate(str)} - ${name}`;
|
||||||
return `${this.translate(str)} - ${name}`;
|
return `${this.translate(str)} - ${name}`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -276,6 +286,7 @@ export async function apply(ctx: Context) {
|
|||||||
text: global.Hydro.module.oauth[key].text,
|
text: global.Hydro.module.oauth[key].text,
|
||||||
}));
|
}));
|
||||||
if (!h.noCheckPermView && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW);
|
if (!h.noCheckPermView && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW);
|
||||||
|
if (h.context.pendingError) throw h.context.pendingError;
|
||||||
});
|
});
|
||||||
|
|
||||||
on('app/listen', () => {
|
on('app/listen', () => {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { dirname, resolve } from 'path';
|
|||||||
import { PassThrough, Readable } from 'stream';
|
import { PassThrough, Readable } from 'stream';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import {
|
import {
|
||||||
CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand,
|
DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand,
|
||||||
GetObjectCommand, HeadObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client,
|
HeadObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { Upload } from '@aws-sdk/lib-storage';
|
import { Upload } from '@aws-sdk/lib-storage';
|
||||||
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
||||||
@@ -243,16 +243,6 @@ class RemoteStorageService {
|
|||||||
return { url, fields };
|
return { url, fields };
|
||||||
}
|
}
|
||||||
|
|
||||||
async copy(src: string, target: string) {
|
|
||||||
src = convertPath(src);
|
|
||||||
target = convertPath(target);
|
|
||||||
return await this.client.send(new CopyObjectCommand({
|
|
||||||
Bucket: this.bucket,
|
|
||||||
Key: target,
|
|
||||||
CopySource: src,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
return {
|
return {
|
||||||
type: 'S3',
|
type: 'S3',
|
||||||
@@ -332,14 +322,6 @@ class LocalStorageService {
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
async copy(src: string, target: string) {
|
|
||||||
src = resolve(this.dir, convertPath(src));
|
|
||||||
target = resolve(this.dir, convertPath(target));
|
|
||||||
await ensureDir(dirname(target));
|
|
||||||
await copyFile(src, target);
|
|
||||||
return { etag: target, lastModified: new Date() };
|
|
||||||
}
|
|
||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
return {
|
return {
|
||||||
type: 'Local',
|
type: 'Local',
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ process.on('SIGTERM', terminate);
|
|||||||
|
|
||||||
const shell = new Logger('shell');
|
const shell = new Logger('shell');
|
||||||
async function executeCommand(input: string) {
|
async function executeCommand(input: string) {
|
||||||
input = input.trim();
|
|
||||||
// Clear the stack
|
// Clear the stack
|
||||||
setImmediate(async () => {
|
setImmediate(async () => {
|
||||||
if (input === 'exit' || input === 'quit' || input === 'shutdown') {
|
if (input === 'exit' || input === 'quit' || input === 'shutdown') {
|
||||||
@@ -39,9 +38,20 @@ async function executeCommand(input: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let readlineCallback;
|
||||||
|
|
||||||
process.stdin.setEncoding('utf-8');
|
process.stdin.setEncoding('utf-8');
|
||||||
process.stdin.setRawMode?.(false);
|
process.stdin.setRawMode?.(false);
|
||||||
process.stdin.on('data', (buf) => {
|
process.stdin.on('data', (buf) => {
|
||||||
const input = buf.toString();
|
const input = buf.toString().trim();
|
||||||
executeCommand(input);
|
if (readlineCallback) {
|
||||||
|
readlineCallback(input);
|
||||||
|
readlineCallback = null;
|
||||||
|
} else executeCommand(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useReadline = (callback: (string) => any) => {
|
||||||
|
if (readlineCallback) throw new Error('Already waiting for input.');
|
||||||
|
readlineCallback = callback;
|
||||||
|
};
|
||||||
|
export const readline = () => new Promise<string>((resolve) => { useReadline(resolve); });
|
||||||
@@ -21,37 +21,35 @@ import StorageModel from './model/storage';
|
|||||||
import * as system from './model/system';
|
import * as system from './model/system';
|
||||||
import TaskModel from './model/task';
|
import TaskModel from './model/task';
|
||||||
import * as training from './model/training';
|
import * as training from './model/training';
|
||||||
import user from './model/user';
|
import user, { handleMailLower } from './model/user';
|
||||||
import {
|
import {
|
||||||
iterateAllContest, iterateAllDomain, iterateAllProblem, iterateAllUser,
|
iterateAllContest, iterateAllDomain, iterateAllProblem, iterateAllUser,
|
||||||
} from './pipelineUtils';
|
} from './pipelineUtils';
|
||||||
import db from './service/db';
|
import db from './service/db';
|
||||||
|
import { MigrationScript } from './service/migration';
|
||||||
import { setBuiltinConfig } from './settings';
|
import { setBuiltinConfig } from './settings';
|
||||||
import welcome from './welcome';
|
import welcome from './welcome';
|
||||||
|
|
||||||
const logger = new Logger('upgrade');
|
const logger = new Logger('upgrade');
|
||||||
type UpgradeScript = void | (() => Promise<boolean | void>);
|
|
||||||
const unsupportedUpgrade = async function _26_27() {
|
const unsupportedUpgrade = async function _26_27() {
|
||||||
throw new Error('This upgrade was no longer supported in hydrooj@4. \
|
throw new Error('This upgrade was no longer supported in hydrooj@4. \
|
||||||
Please use hydrooj@3 to perform these upgrades before upgrading to v4');
|
Please use hydrooj@3 to perform these upgrades before upgrading to v4');
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function init() {
|
export const coreScripts: MigrationScript[] = [
|
||||||
if (!await user.getById('system', 0)) {
|
|
||||||
await user.create('Guest@hydro.local', 'Guest', String.random(32), 0, '127.0.0.1', PRIV.PRIV_REGISTER_USER);
|
|
||||||
}
|
|
||||||
if (!await user.getById('system', 1)) {
|
|
||||||
await user.create('Hydro@hydro.local', 'Hydro', String.random(32), 1, '127.0.0.1', PRIV.PRIV_USER_PROFILE);
|
|
||||||
}
|
|
||||||
const ddoc = await domain.get('system');
|
|
||||||
if (!ddoc) await domain.add('system', 1, 'Hydro', 'Welcome to Hydro!');
|
|
||||||
await welcome();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scripts: UpgradeScript[] = [
|
|
||||||
// Mark as used
|
// Mark as used
|
||||||
null,
|
async function init() {
|
||||||
|
if (!await user.getById('system', 0)) {
|
||||||
|
await user.create('Guest@hydro.local', 'Guest', String.random(32), 0, '127.0.0.1', PRIV.PRIV_REGISTER_USER);
|
||||||
|
}
|
||||||
|
if (!await user.getById('system', 1)) {
|
||||||
|
await user.create('Hydro@hydro.local', 'Hydro', String.random(32), 1, '127.0.0.1', PRIV.PRIV_USER_PROFILE);
|
||||||
|
}
|
||||||
|
const ddoc = await domain.get('system');
|
||||||
|
if (!ddoc) await domain.add('system', 1, 'Hydro', 'Welcome to Hydro!');
|
||||||
|
await welcome();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
// Init
|
// Init
|
||||||
...new Array(26).fill(unsupportedUpgrade),
|
...new Array(26).fill(unsupportedUpgrade),
|
||||||
null,
|
null,
|
||||||
@@ -675,6 +673,16 @@ const scripts: UpgradeScript[] = [
|
|||||||
if (bulk.batches.length) await bulk.execute();
|
if (bulk.batches.length) await bulk.execute();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
async function _89_90() {
|
||||||
|
return await iterateAllUser(async (udoc) => {
|
||||||
|
const wanted = handleMailLower(udoc.mail);
|
||||||
|
if (wanted !== udoc.mailLower) {
|
||||||
|
if (await user.getByEmail('system', wanted)) {
|
||||||
|
console.warn('Email conflict when trying to rename %s to %s', udoc.mailLower, wanted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await user.coll.updateOne({ _id: udoc._id }, { $set: { mailLower: wanted } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default scripts;
|
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
"version": "0.1.9",
|
"version": "0.1.9",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prom-client": "^15.1.2"
|
"prom-client": "^15.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import {
|
import {
|
||||||
Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1,
|
Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1,
|
||||||
size, Types, UiContextBase,
|
size, SystemModel, Types, UiContextBase,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
@@ -46,6 +46,7 @@ const build = async (contents: string) => {
|
|||||||
format: 'iife' as 'iife',
|
format: 'iife' as 'iife',
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outdir: tmp,
|
outdir: tmp,
|
||||||
|
sourcemap: SystemModel.get('ui-default.nosourcemap') ? false : 'external',
|
||||||
splitting: false,
|
splitting: false,
|
||||||
write: false,
|
write: false,
|
||||||
target: ['chrome65'],
|
target: ['chrome65'],
|
||||||
@@ -103,11 +104,11 @@ export async function buildUI() {
|
|||||||
}
|
}
|
||||||
const entry = await build([
|
const entry = await build([
|
||||||
`window.lazyloadMetadata = ${JSON.stringify(hashes)};`,
|
`window.lazyloadMetadata = ${JSON.stringify(hashes)};`,
|
||||||
|
`window.LANGS=${JSON.stringify(SettingModel.langs)};`,
|
||||||
...entryPoints.map((i) => `import '${relative(tmp, i).replace(/\\/g, '\\\\')}';`),
|
...entryPoints.map((i) => `import '${relative(tmp, i).replace(/\\/g, '\\\\')}';`),
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
const pages = entry.outputFiles.map((i) => i.text);
|
const pages = entry.outputFiles.filter((i) => i.path.endsWith('.js')).map((i) => i.text);
|
||||||
const str = `window.LANGS=${JSON.stringify(SettingModel.langs)};${pages.join('\n')}`;
|
addFile('entry.js', `window._hydroLoad=()=>{ ${pages.join('\n')} };`);
|
||||||
addFile('entry.js', str);
|
|
||||||
UiContextBase.constantVersion = hashes['entry.js'];
|
UiContextBase.constantVersion = hashes['entry.js'];
|
||||||
for (const key in vfs) {
|
for (const key in vfs) {
|
||||||
if (newFiles.includes(key)) continue;
|
if (newFiles.includes(key)) continue;
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
// Process 
|
||||||
|
// ^^^^^^^^ this size specification
|
||||||
|
function parseNextNumber(str, pos, max) {
|
||||||
|
const start = pos;
|
||||||
|
const result = {
|
||||||
|
ok: false,
|
||||||
|
pos,
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
let code = str.charCodeAt(pos);
|
||||||
|
while (pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || code === 0x25 /* % */) {
|
||||||
|
code = str.charCodeAt(++pos);
|
||||||
|
}
|
||||||
|
result.ok = true;
|
||||||
|
result.pos = pos;
|
||||||
|
result.value = str.slice(start, pos);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImageSize(str, pos, max) {
|
||||||
|
const result = {
|
||||||
|
ok: false,
|
||||||
|
pos: 0,
|
||||||
|
width: '',
|
||||||
|
height: '',
|
||||||
|
};
|
||||||
|
if (pos >= max) { return result; }
|
||||||
|
let code = str.charCodeAt(pos);
|
||||||
|
if (code !== 0x3d /* = */) { return result; }
|
||||||
|
pos++;
|
||||||
|
code = str.charCodeAt(pos);
|
||||||
|
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// parse width
|
||||||
|
const resultW = parseNextNumber(str, pos, max);
|
||||||
|
pos = resultW.pos;
|
||||||
|
// next charactor must be 'x'
|
||||||
|
code = str.charCodeAt(pos);
|
||||||
|
if (code !== 0x78 /* x */) { return result; }
|
||||||
|
pos++;
|
||||||
|
// parse height
|
||||||
|
const resultH = parseNextNumber(str, pos, max);
|
||||||
|
pos = resultH.pos;
|
||||||
|
result.width = resultW.value;
|
||||||
|
result.height = resultH.value;
|
||||||
|
result.pos = pos;
|
||||||
|
result.ok = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function plugin(md) {
|
||||||
|
md.inline.ruler.before('emphasis', 'image', (state, silent) => {
|
||||||
|
let attrs;
|
||||||
|
let code;
|
||||||
|
let label;
|
||||||
|
let pos;
|
||||||
|
let ref;
|
||||||
|
let res;
|
||||||
|
let title;
|
||||||
|
let width = '';
|
||||||
|
let height = '';
|
||||||
|
let token;
|
||||||
|
let tokens;
|
||||||
|
let start;
|
||||||
|
let href = '';
|
||||||
|
const oldPos = state.pos;
|
||||||
|
const max = state.posMax;
|
||||||
|
|
||||||
|
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false; }
|
||||||
|
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; }
|
||||||
|
|
||||||
|
const labelStart = state.pos + 2;
|
||||||
|
const labelEnd = md.helpers.parseLinkLabel(state, state.pos + 1, false);
|
||||||
|
|
||||||
|
// parser failed to find ']', so it's not a valid link
|
||||||
|
if (labelEnd < 0) { return false; }
|
||||||
|
|
||||||
|
pos = labelEnd + 1;
|
||||||
|
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
|
||||||
|
//
|
||||||
|
// Inline link
|
||||||
|
//
|
||||||
|
|
||||||
|
// [link]( <href> "title" )
|
||||||
|
// ^^ skipping these spaces
|
||||||
|
pos++;
|
||||||
|
for (; pos < max; pos++) {
|
||||||
|
code = state.src.charCodeAt(pos);
|
||||||
|
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||||
|
}
|
||||||
|
if (pos >= max) { return false; }
|
||||||
|
|
||||||
|
// [link]( <href> "title" )
|
||||||
|
// ^^^^^^ parsing link destination
|
||||||
|
start = pos;
|
||||||
|
res = md.helpers.parseLinkDestination(state.src, pos, state.posMax);
|
||||||
|
if (res.ok) {
|
||||||
|
href = state.md.normalizeLink(res.str);
|
||||||
|
if (state.md.validateLink(href)) {
|
||||||
|
pos = res.pos;
|
||||||
|
} else {
|
||||||
|
href = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [link]( <href> "title" )
|
||||||
|
// ^^ skipping these spaces
|
||||||
|
start = pos;
|
||||||
|
for (; pos < max; pos++) {
|
||||||
|
code = state.src.charCodeAt(pos);
|
||||||
|
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// [link]( <href> "title" )
|
||||||
|
// ^^^^^^^ parsing link title
|
||||||
|
res = md.helpers.parseLinkTitle(state.src, pos, state.posMax);
|
||||||
|
if (pos < max && start !== pos && res.ok) {
|
||||||
|
title = res.str;
|
||||||
|
pos = res.pos;
|
||||||
|
|
||||||
|
// [link]( <href> "title" )
|
||||||
|
// ^^ skipping these spaces
|
||||||
|
for (; pos < max; pos++) {
|
||||||
|
code = state.src.charCodeAt(pos);
|
||||||
|
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// [link]( <href> "title" =WxH )
|
||||||
|
// ^^^^ parsing image size
|
||||||
|
if (pos - 1 >= 0) {
|
||||||
|
code = state.src.charCodeAt(pos - 1);
|
||||||
|
|
||||||
|
// there must be at least one white spaces
|
||||||
|
// between previous field and the size
|
||||||
|
if (code === 0x20) {
|
||||||
|
res = parseImageSize(state.src, pos, state.posMax);
|
||||||
|
if (res.ok) {
|
||||||
|
width = res.width;
|
||||||
|
height = res.height;
|
||||||
|
pos = res.pos;
|
||||||
|
|
||||||
|
// [link]( <href> "title" =WxH )
|
||||||
|
// ^^ skipping these spaces
|
||||||
|
for (; pos < max; pos++) {
|
||||||
|
code = state.src.charCodeAt(pos);
|
||||||
|
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
|
||||||
|
state.pos = oldPos;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// Link reference
|
||||||
|
//
|
||||||
|
if (typeof state.env.references === 'undefined') { return false; }
|
||||||
|
|
||||||
|
// [foo] [bar]
|
||||||
|
// ^^ optional whitespace (can include newlines)
|
||||||
|
for (; pos < max; pos++) {
|
||||||
|
code = state.src.charCodeAt(pos);
|
||||||
|
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
|
||||||
|
start = pos + 1;
|
||||||
|
pos = md.helpers.parseLinkLabel(state, pos);
|
||||||
|
if (pos >= 0) {
|
||||||
|
label = state.src.slice(start, pos++);
|
||||||
|
} else {
|
||||||
|
pos = labelEnd + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pos = labelEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// covers label === '' and label === undefined
|
||||||
|
// (collapsed reference link and shortcut reference link respectively)
|
||||||
|
label ||= state.src.slice(labelStart, labelEnd);
|
||||||
|
|
||||||
|
ref = state.env.references[md.utils.normalizeReference(label)];
|
||||||
|
if (!ref) {
|
||||||
|
state.pos = oldPos;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
href = ref.href;
|
||||||
|
title = ref.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// We found the end of the link, and know for a fact it's a valid link;
|
||||||
|
// so all that's left to do is to call tokenizer.
|
||||||
|
//
|
||||||
|
if (!silent) {
|
||||||
|
state.pos = labelStart;
|
||||||
|
state.posMax = labelEnd;
|
||||||
|
|
||||||
|
const newState = new state.md.inline.State(
|
||||||
|
state.src.slice(labelStart, labelEnd),
|
||||||
|
state.md,
|
||||||
|
state.env,
|
||||||
|
tokens = [],
|
||||||
|
);
|
||||||
|
newState.md.inline.tokenize(newState);
|
||||||
|
token = state.push('image', 'img', 0);
|
||||||
|
token.attrs = attrs = [['src', href],
|
||||||
|
['alt', '']];
|
||||||
|
token.children = tokens;
|
||||||
|
if (title) {
|
||||||
|
attrs.push(['title', title]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width !== '') {
|
||||||
|
attrs.push(['width', width]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height !== '') {
|
||||||
|
attrs.push(['height', height]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pos = pos;
|
||||||
|
state.posMax = max;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
+6
-5
@@ -1,7 +1,8 @@
|
|||||||
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
|
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
|
||||||
const katex = require('katex');
|
import { Logger } from 'hydrooj';
|
||||||
|
import katex from 'katex';
|
||||||
|
|
||||||
const logger = new global.Hydro.Logger('katex');
|
const logger = new Logger('katex');
|
||||||
|
|
||||||
function isValidDelim(state, pos) {
|
function isValidDelim(state, pos) {
|
||||||
const max = state.posMax;
|
const max = state.posMax;
|
||||||
@@ -109,8 +110,8 @@ function block(state, start, end, silent) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function plugin(md) {
|
export default function plugin(md) {
|
||||||
const options = { throwOnError: false, strict: 'ignore' };
|
const options = { throwOnError: false, strict: 'ignore', displayMode: false };
|
||||||
const render = (latex: string, displayMode = false) => {
|
const render = (latex: string, displayMode = false) => {
|
||||||
options.displayMode = displayMode;
|
options.displayMode = displayMode;
|
||||||
try {
|
try {
|
||||||
@@ -136,4 +137,4 @@ module.exports = function plugin(md) {
|
|||||||
});
|
});
|
||||||
md.renderer.rules.math_inline = inlineRenderer;
|
md.renderer.rules.math_inline = inlineRenderer;
|
||||||
md.renderer.rules.math_block = blockRenderer;
|
md.renderer.rules.math_block = blockRenderer;
|
||||||
};
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
|
||||||
import { randomUUID } from 'crypto';
|
import type MarkdownIt from 'markdown-it';
|
||||||
import MarkdownIt from 'markdown-it';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
|
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export function Media(md: MarkdownIt) {
|
|||||||
if (service === 'pdf') {
|
if (service === 'pdf') {
|
||||||
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
|
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
|
||||||
return `\
|
return `\
|
||||||
<object classid="clsid:${randomUUID().toUpperCase()}">
|
<object classid="clsid:${uuid().toUpperCase()}">
|
||||||
<param name="SRC" value="${src}" >
|
<param name="SRC" value="${src}" >
|
||||||
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
|
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
|
||||||
<noembed></noembed>
|
<noembed></noembed>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const MergeCells = require('markdown-it-merge-cells');
|
|||||||
// @[youtube](https://youtube.com/watch?v=xxx)
|
// @[youtube](https://youtube.com/watch?v=xxx)
|
||||||
// @[pdf](https://foo.com/bar.pdf)
|
// @[pdf](https://foo.com/bar.pdf)
|
||||||
const { Media } = require('./markdown-it-media');
|
const { Media } = require('./markdown-it-media');
|
||||||
const Katex = require('./markdown-it-katex');
|
const { default: Katex } = require('./markdown-it-katex');
|
||||||
const { xssProtector } = require('./markdown-it-xss');
|
const { xssProtector } = require('./markdown-it-xss');
|
||||||
|
|
||||||
class Markdown extends MarkdownIt {
|
class Markdown extends MarkdownIt {
|
||||||
|
|||||||
@@ -2,17 +2,42 @@
|
|||||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||||
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
||||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||||
|
import { version as coreJsVersion } from 'core-js/package.json';
|
||||||
|
import compat from 'core-js-compat';
|
||||||
import { EsbuildPlugin } from 'esbuild-loader';
|
import { EsbuildPlugin } from 'esbuild-loader';
|
||||||
|
import fs from 'fs';
|
||||||
import { DuplicatesPlugin } from 'inspectpack/plugin';
|
import { DuplicatesPlugin } from 'inspectpack/plugin';
|
||||||
import ExtractCssPlugin from 'mini-css-extract-plugin';
|
import ExtractCssPlugin from 'mini-css-extract-plugin';
|
||||||
|
import packageJson from 'package-json';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
import { gt } from 'semver';
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
|
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
|
||||||
import WebpackBar from 'webpackbar';
|
import WebpackBar from 'webpackbar';
|
||||||
|
import { version } from '../../package.json';
|
||||||
import root from '../utils/root';
|
import root from '../utils/root';
|
||||||
|
|
||||||
export default function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
|
const {
|
||||||
|
list,
|
||||||
|
targets,
|
||||||
|
} = compat({
|
||||||
|
targets: '> 1%, chrome 70, firefox 60, safari 16, ios_saf 16, not ie 11, not op_mini all',
|
||||||
|
modules: [
|
||||||
|
'core-js/stable',
|
||||||
|
],
|
||||||
|
exclude: [],
|
||||||
|
version: coreJsVersion,
|
||||||
|
inverse: false,
|
||||||
|
});
|
||||||
|
fs.writeFileSync(root('__core-js.js'), `${list.map((i) => `import 'core-js/modules/${i}';`).join('\n')}\n`);
|
||||||
|
|
||||||
|
export default async function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
|
||||||
|
if (env.production) console.log(targets);
|
||||||
|
let isNew = false;
|
||||||
|
const { version: latest } = await packageJson('@hydrooj/ui-default', { version: 'latest' });
|
||||||
|
if (typeof version === 'string' && gt(version, latest)) isNew = true;
|
||||||
|
|
||||||
function cssLoader() {
|
function cssLoader() {
|
||||||
return {
|
return {
|
||||||
loader: 'css-loader',
|
loader: 'css-loader',
|
||||||
@@ -61,11 +86,9 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
|||||||
// sentry requires source-map while keep it simple in dev mode
|
// sentry requires source-map while keep it simple in dev mode
|
||||||
devtool: env.production ? 'source-map' : false,
|
devtool: env.production ? 'source-map' : false,
|
||||||
entry: {
|
entry: {
|
||||||
hydro: './entry.js',
|
[`hydro-${version}`]: './entry.js',
|
||||||
polyfill: './polyfill.ts',
|
|
||||||
'default.theme': './theme/default.js',
|
'default.theme': './theme/default.js',
|
||||||
'service-worker': './service-worker.ts',
|
'service-worker': './service-worker.ts',
|
||||||
'messages-shared-worker': './components/message/worker.ts',
|
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
type: 'filesystem',
|
type: 'filesystem',
|
||||||
@@ -78,6 +101,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
|||||||
output: {
|
output: {
|
||||||
path: root('public'),
|
path: root('public'),
|
||||||
publicPath: '/', // overwrite in entry.js
|
publicPath: '/', // overwrite in entry.js
|
||||||
|
workerPublicPath: '/',
|
||||||
hashFunction: 'sha1',
|
hashFunction: 'sha1',
|
||||||
hashDigest: 'hex',
|
hashDigest: 'hex',
|
||||||
hashDigestLength: 10,
|
hashDigestLength: 10,
|
||||||
@@ -233,18 +257,30 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
|||||||
filename: '[name].css?[fullhash:6]',
|
filename: '[name].css?[fullhash:6]',
|
||||||
}),
|
}),
|
||||||
new WebpackManifestPlugin({}),
|
new WebpackManifestPlugin({}),
|
||||||
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }),
|
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$)/ }),
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: root('static') },
|
{ from: root('static') },
|
||||||
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
|
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
|
||||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
|
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
|
||||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
|
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
|
||||||
{ from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' },
|
|
||||||
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
|
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
|
||||||
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
sentryWebpackPlugin({
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
org: 'hydro-dev',
|
||||||
|
project: 'hydro-web',
|
||||||
|
url: 'https://sentry.hydro.ac',
|
||||||
|
sourcemaps: {
|
||||||
|
rewriteSources: (source) => source.replace('@hydrooj/ui-default/../../node_modules/', ''),
|
||||||
|
},
|
||||||
|
release: (process.env.CI && isNew) ? {
|
||||||
|
name: `hydro-web@${version}`,
|
||||||
|
uploadLegacySourcemaps: root('public'),
|
||||||
|
} : {},
|
||||||
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.VERSION': JSON.stringify(require('@hydrooj/ui-default/package.json').version),
|
'process.env.VERSION': JSON.stringify(require('@hydrooj/ui-default/package.json').version),
|
||||||
}),
|
}),
|
||||||
@@ -265,6 +301,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
|||||||
url: 'https://sentry.hydro.ac',
|
url: 'https://sentry.hydro.ac',
|
||||||
}),
|
}),
|
||||||
] : []),
|
] : []),
|
||||||
|
new webpack.NormalModuleReplacementPlugin(/core-js\/stable/, root('__core-js.js')),
|
||||||
...env.measure ? [
|
...env.measure ? [
|
||||||
new BundleAnalyzerPlugin({ analyzerPort: 'auto' }),
|
new BundleAnalyzerPlugin({ analyzerPort: 'auto' }),
|
||||||
new DuplicatesPlugin(),
|
new DuplicatesPlugin(),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const argv = cac().parse();
|
|||||||
async function runWebpack({
|
async function runWebpack({
|
||||||
watch, production, measure, dev, https,
|
watch, production, measure, dev, https,
|
||||||
}) {
|
}) {
|
||||||
const compiler = webpack(webpackConfig({ watch, production, measure }));
|
const compiler = webpack(await webpackConfig({ watch, production, measure }));
|
||||||
if (dev) {
|
if (dev) {
|
||||||
const server = new WebpackDevServer({
|
const server = new WebpackDevServer({
|
||||||
port: https ? 8001 : 8000,
|
port: https ? 8001 : 8000,
|
||||||
@@ -49,7 +49,10 @@ async function runWebpack({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (argv.options.detail) console.log(stats.toString());
|
if (argv.options.detail) console.log(stats.toString());
|
||||||
if (!watch && (!stats || stats.hasErrors())) process.exitCode = 1;
|
if (!watch && (!stats || stats.hasErrors())) {
|
||||||
|
if (!argv.options.detail) console.log(stats.toString());
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
resolve(stats);
|
resolve(stats);
|
||||||
}
|
}
|
||||||
if (watch) compiler.watch({}, compilerCallback);
|
if (watch) compiler.watch({}, compilerCallback);
|
||||||
@@ -121,22 +124,9 @@ async function main() {
|
|||||||
if (argv.options.gulp) await runGulp();
|
if (argv.options.gulp) await runGulp();
|
||||||
else {
|
else {
|
||||||
await runWebpack(argv.options as any);
|
await runWebpack(argv.options as any);
|
||||||
if (fs.existsSync('public/hydro.js')) {
|
|
||||||
fs.copyFileSync('public/hydro.js', `public/hydro-${pkg.version}.js`);
|
|
||||||
}
|
|
||||||
if (fs.existsSync('public/polyfill.js')) {
|
|
||||||
fs.copyFileSync('public/polyfill.js', `public/polyfill-${pkg.version}.js`);
|
|
||||||
}
|
|
||||||
if (fs.existsSync('public/theme.css')) {
|
if (fs.existsSync('public/theme.css')) {
|
||||||
fs.copyFileSync('public/theme.css', `public/theme-${pkg.version}.css`);
|
fs.copyFileSync('public/theme.css', `public/theme-${pkg.version}.css`);
|
||||||
}
|
}
|
||||||
if (argv.options.production) {
|
|
||||||
for (const f of ['echarts', 'graphviz', 'mermaid', 'mathjax']) {
|
|
||||||
fs.removeSync(`public/vditor/dist/js/${f}`);
|
|
||||||
}
|
|
||||||
const files = fs.readdirSync('public');
|
|
||||||
files.filter((i) => /(^[in]\..+|worker)\.js\.map$/.test(i)).forEach((i) => fs.removeSync(`public/${i}`));
|
|
||||||
}
|
|
||||||
await Promise.all(globbySync('public/**/*.map').map((i) => fs.remove(i)));
|
await Promise.all(globbySync('public/**/*.map').map((i) => fs.remove(i)));
|
||||||
}
|
}
|
||||||
process.chdir(dir);
|
process.chdir(dir);
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
|
||||||
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
||||||
options: CustomSelectOptions & AutoCompleteOptions;
|
|
||||||
|
|
||||||
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
|
||||||
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||||
options: FileSelectOptions & AutoCompleteOptions;
|
|
||||||
|
|
||||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
|||||||
<AutoComplete<DefaultProps>
|
<AutoComplete<DefaultProps>
|
||||||
ref={ref as any}
|
ref={ref as any}
|
||||||
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||||
queryItems={(query) => props.data.filter((i) => (i.name
|
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
||||||
? i.name.toLowerCase().match(query.toLowerCase())
|
|
||||||
: i.toString().toLowerCase().match(query.toLowerCase())
|
|
||||||
))}
|
|
||||||
itemText={(item) => `${item.name || item}`}
|
itemText={(item) => `${item.name || item}`}
|
||||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||||
renderItem={(item) => `${item.name || item}`}
|
renderItem={(item) => `${item.name || item}`}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
|||||||
defaultItems?: string;
|
defaultItems?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
|
classes?: string;
|
||||||
listStyle?: any;
|
listStyle?: any;
|
||||||
allowEmptyQuery?: boolean;
|
allowEmptyQuery?: boolean;
|
||||||
freeSolo?: boolean;
|
freeSolo?: boolean;
|
||||||
@@ -18,17 +19,17 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
|||||||
text?: () => string;
|
text?: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AutoComplete extends DOMAttachedObject {
|
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
|
||||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||||
ref = null;
|
ref = null;
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
options: AutoCompleteOptions;
|
options: AutoCompleteOptions & Options;
|
||||||
component = ReactDOM.createRoot(this.container);
|
component = ReactDOM.createRoot(this.container);
|
||||||
changeListener = [
|
changeListener = [
|
||||||
(val) => this.$dom.val(val),
|
(val) => this.$dom.val(val),
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor($dom, options = {}) {
|
constructor($dom, options = {} as Options) {
|
||||||
super($dom);
|
super($dom);
|
||||||
this.options = {
|
this.options = {
|
||||||
items: async () => [],
|
items: async () => [],
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
.drop.nav__dropdown
|
.drop.nav__dropdown
|
||||||
max-height: calc(100% - 60px)
|
max-height: calc(100% - 60px)
|
||||||
|
overflow-x: hidden
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
|
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
|
||||||
|
|
||||||
|
.drop.nav__dropdown.drop-open
|
||||||
|
overflow-x: auto
|
||||||
|
|
||||||
.dropdown
|
.dropdown
|
||||||
.menu
|
.menu
|
||||||
min-width: rem(150px)
|
min-width: rem(150px)
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
.vditor
|
|
||||||
margin-bottom: rem(10px)
|
|
||||||
|
|
||||||
.vditor-preview__action
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.vditor ol
|
|
||||||
list-style: decimal
|
|
||||||
|
|
||||||
.decoration-hide
|
.decoration-hide
|
||||||
color: grey !important
|
color: grey !important
|
||||||
background-color: grey !important
|
background-color: grey !important
|
||||||
+103
-109
@@ -1,36 +1,11 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import type { editor } from 'monaco-editor';
|
import type { editor } from 'monaco-editor';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import Notification from 'vj/components/notification';
|
import { i18n } from 'vj/utils';
|
||||||
import { i18n, request } from 'vj/utils';
|
import uploadFiles from '../upload';
|
||||||
|
|
||||||
export const config = {
|
|
||||||
toolbar: [
|
|
||||||
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
|
|
||||||
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
|
|
||||||
'quote', 'line', 'code', 'inline-code', 'table', '|',
|
|
||||||
'upload', 'edit-mode', 'fullscreen', 'export',
|
|
||||||
],
|
|
||||||
mode: UserContext.preferredEditorType || 'ir',
|
|
||||||
toolbarConfig: {
|
|
||||||
pin: true,
|
|
||||||
},
|
|
||||||
cdn: `${UiContext.cdn_prefix}vditor`,
|
|
||||||
counter: {
|
|
||||||
enable: true,
|
|
||||||
max: 65536,
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
math: {
|
|
||||||
inlineDigit: true,
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
path: `${UiContext.cdn_prefix}vditor/dist/css/content-theme`,
|
|
||||||
current: 'light',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MonacoOptions {
|
interface MonacoOptions {
|
||||||
language?: string;
|
language?: string;
|
||||||
@@ -43,23 +18,23 @@ interface MonacoOptions {
|
|||||||
hide?: string[];
|
hide?: string[];
|
||||||
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
||||||
}
|
}
|
||||||
interface VditorOptions {
|
type Options = MonacoOptions;
|
||||||
theme?: 'classic' | 'dark'
|
|
||||||
}
|
|
||||||
type Options = MonacoOptions & VditorOptions;
|
|
||||||
|
|
||||||
export default class Editor extends DOMAttachedObject {
|
export default class Editor extends DOMAttachedObject {
|
||||||
static DOMAttachKey = 'vjEditorInstance';
|
static DOMAttachKey = 'vjEditorInstance';
|
||||||
model: editor.IModel;
|
model: editor.IModel;
|
||||||
editor: editor.IStandaloneCodeEditor;
|
editor: editor.IStandaloneCodeEditor;
|
||||||
vditor: import('vditor').default;
|
markdownEditor: any;
|
||||||
|
valueCache?: string;
|
||||||
|
setMarkdownEditorValue?: (v: string) => void;
|
||||||
|
reactRoot?: ReactDOM.Root;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
|
|
||||||
constructor($dom, public options: Options = {}) {
|
constructor($dom, public options: Options = {}) {
|
||||||
super($dom);
|
super($dom);
|
||||||
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
||||||
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
||||||
else this.initVditor();
|
else this.initMarkdownEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initMonaco() {
|
async initMonaco() {
|
||||||
@@ -169,87 +144,102 @@ export default class Editor extends DOMAttachedObject {
|
|||||||
window.editor = this.editor;
|
window.editor = this.editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initVditor() {
|
async initMarkdownEditor() {
|
||||||
const pagename = document.documentElement.getAttribute('data-page');
|
const pagename = document.documentElement.getAttribute('data-page');
|
||||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||||
const isProblemEdit = pagename === 'problem_edit';
|
const isProblemEdit = pagename === 'problem_edit';
|
||||||
const { default: Vditor } = await import('vditor');
|
const that = this;
|
||||||
const { $dom } = this;
|
const { $dom } = this;
|
||||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||||
const origin = $dom.get(0);
|
const origin = $dom.get(0);
|
||||||
const ele = document.createElement('div');
|
const ele = document.createElement('div');
|
||||||
const value = $dom.val();
|
const value = $dom.val();
|
||||||
const { onChange } = this.options;
|
const { onChange } = this.options;
|
||||||
await new Promise((resolve) => {
|
const { MdEditor } = await import('./mdeditor');
|
||||||
this.vditor = new Vditor(ele, {
|
|
||||||
...config,
|
const renderCallback = (ref) => {
|
||||||
upload: {
|
this.markdownEditor = ref;
|
||||||
multiple: false,
|
};
|
||||||
handler: (files) => {
|
|
||||||
let wrapper = ['', ''];
|
function EditorComponent() {
|
||||||
let ext: string;
|
const [val, setVal] = React.useState(value);
|
||||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
that.setMarkdownEditorValue = setVal;
|
||||||
if (matches) {
|
return <MdEditor
|
||||||
wrapper = [''];
|
className='textbox'
|
||||||
[, ext] = matches;
|
autoFocus={hasFocus}
|
||||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
codeTheme='github'
|
||||||
wrapper = ['[file](', ')'];
|
codeStyleReverse={false}
|
||||||
ext = 'zip';
|
ref={renderCallback}
|
||||||
}
|
modelValue={val}
|
||||||
if (!ext) return i18n('No Supported file type.');
|
noMermaid
|
||||||
const filename = `${nanoid()}.${ext}`;
|
noPrettier
|
||||||
const data = new FormData();
|
autoDetectCode
|
||||||
data.append('filename', filename);
|
toolbarsExclude={[
|
||||||
data.append('file', files[0]);
|
// 'bold',
|
||||||
data.append('operation', 'upload_file');
|
// 'underline',
|
||||||
if (isProblemEdit) data.append('type', 'additional_file');
|
// 'italic',
|
||||||
let progress = 0;
|
// '-',
|
||||||
request.postFile(isProblemEdit ? './files' : '/file', data, {
|
// 'strikeThrough',
|
||||||
xhr: () => {
|
// 'title',
|
||||||
const xhr = new XMLHttpRequest();
|
// 'sub',
|
||||||
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
|
// 'sup',
|
||||||
xhr.upload.addEventListener('progress', (e) => {
|
// 'quote',
|
||||||
if (!e.lengthComputable) return;
|
// 'unorderedList',
|
||||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
// 'orderedList',
|
||||||
if (percentComplete === progress) return;
|
// 'task',
|
||||||
progress = percentComplete;
|
// '-',
|
||||||
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
|
// 'codeRow',
|
||||||
}, false);
|
// 'code',
|
||||||
return xhr;
|
// 'link',
|
||||||
},
|
// 'image',
|
||||||
})
|
// 'table',
|
||||||
.then(() => {
|
'mermaid',
|
||||||
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
|
// 'katex',
|
||||||
this.vditor.vditor.tip.hide();
|
// '-',
|
||||||
})
|
// 'revoke',
|
||||||
.catch((e: { message: string; }) => {
|
// 'next',
|
||||||
console.error(e);
|
'save',
|
||||||
return `${i18n('Upload Failed')}: ${e.message}`;
|
// '=',
|
||||||
});
|
'pageFullscreen',
|
||||||
return null;
|
'fullscreen',
|
||||||
},
|
// 'preview',
|
||||||
},
|
'previewOnly',
|
||||||
...this.options,
|
'htmlPreview',
|
||||||
after: () => {
|
// 'catalog',
|
||||||
const pos = $(ele).find('button[data-mode="sv"]').get();
|
'github',
|
||||||
const button = $('<button data-mode="monaco">Monaco Editor</button>');
|
]}
|
||||||
button.on('click', (e) => {
|
onChange={(v) => {
|
||||||
Notification.info('You can select this in PreferenceSettings');
|
that.valueCache = v;
|
||||||
e.preventDefault();
|
setVal(v);
|
||||||
});
|
|
||||||
button.insertAfter(pos);
|
|
||||||
resolve(null);
|
|
||||||
},
|
|
||||||
input(v) {
|
|
||||||
$dom.val(v);
|
$dom.val(v);
|
||||||
$dom.text(v);
|
$dom.text(v);
|
||||||
if (onChange) onChange(v);
|
onChange?.(v);
|
||||||
},
|
}}
|
||||||
value,
|
onUploadImg={async (files, callback) => {
|
||||||
cache: { id: Math.random().toString() },
|
let ext: string;
|
||||||
});
|
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||||
});
|
if (matches) {
|
||||||
$(ele).addClass('textbox');
|
[, ext] = matches;
|
||||||
|
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||||
|
ext = 'zip';
|
||||||
|
}
|
||||||
|
if (!ext) return i18n('No Supported file type.');
|
||||||
|
const filename = `${nanoid()}.${ext}`;
|
||||||
|
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
|
||||||
|
type: isProblemEdit ? 'additional_file' : undefined,
|
||||||
|
filenameCallback: () => filename,
|
||||||
|
}).then(() => {
|
||||||
|
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
|
||||||
|
}).catch(() => {
|
||||||
|
callback([]);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reactRoot = ReactDOM.createRoot(ele);
|
||||||
|
this.reactRoot.render(<EditorComponent />);
|
||||||
$dom.hide();
|
$dom.hide();
|
||||||
origin.parentElement.appendChild(ele);
|
origin.parentElement.appendChild(ele);
|
||||||
this.isValid = true;
|
this.isValid = true;
|
||||||
@@ -258,7 +248,7 @@ export default class Editor extends DOMAttachedObject {
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.detach();
|
this.detach();
|
||||||
if (this.vditor?.destroy) this.vditor.destroy();
|
if (this.reactRoot) this.reactRoot.unmount();
|
||||||
else if (this.editor?.dispose) this.editor.dispose();
|
else if (this.editor?.dispose) this.editor.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +258,17 @@ export default class Editor extends DOMAttachedObject {
|
|||||||
|
|
||||||
value(val?: string) {
|
value(val?: string) {
|
||||||
this.ensureValid();
|
this.ensureValid();
|
||||||
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
|
if (typeof val === 'string') {
|
||||||
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
|
if (this.editor) return this.editor.setValue(val);
|
||||||
|
this.setMarkdownEditorValue?.(val);
|
||||||
|
this.markdownEditor?.resetHistory?.();
|
||||||
|
}
|
||||||
|
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||||
|
return this.valueCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.ensureValid();
|
this.ensureValid();
|
||||||
this.vditor?.focus();
|
|
||||||
if (!this.editor || !this.model) return;
|
if (!this.editor || !this.model) return;
|
||||||
this.editor.focus();
|
this.editor.focus();
|
||||||
const range = this.model.getFullModelRange();
|
const range = this.model.getFullModelRange();
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'md-editor-rt/lib/style.css';
|
||||||
|
|
||||||
|
import Anchor from 'markdown-it-anchor';
|
||||||
|
import Footnote from 'markdown-it-footnote';
|
||||||
|
import Mark from 'markdown-it-mark';
|
||||||
|
import MergeCells from 'markdown-it-merge-cells';
|
||||||
|
import TOC from 'markdown-it-table-of-contents';
|
||||||
|
import { config, MdEditor } from 'md-editor-rt';
|
||||||
|
import Imsize from '../../backendlib/markdown-it-imsize';
|
||||||
|
import { Media } from '../../backendlib/markdown-it-media';
|
||||||
|
import { xssProtector } from '../../backendlib/markdown-it-xss';
|
||||||
|
|
||||||
|
const pagename = document.documentElement.getAttribute('data-page');
|
||||||
|
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||||
|
|
||||||
|
config({
|
||||||
|
markdownItConfig(mdit) {
|
||||||
|
mdit.options.html = true;
|
||||||
|
mdit.options.linkify = true;
|
||||||
|
mdit.linkify.tlds('.py', false);
|
||||||
|
mdit.linkify.tlds('.zip', false);
|
||||||
|
mdit.linkify.tlds('.mov', false);
|
||||||
|
mdit.use(Media);
|
||||||
|
mdit.use(Footnote);
|
||||||
|
mdit.use(Mark);
|
||||||
|
mdit.use(Imsize);
|
||||||
|
mdit.use(Anchor);
|
||||||
|
mdit.use(TOC);
|
||||||
|
mdit.use(MergeCells);
|
||||||
|
mdit.use(xssProtector);
|
||||||
|
if (isProblemPage) {
|
||||||
|
mdit.core.ruler.before('normalize', 'xss', (state) => {
|
||||||
|
state.src = state.src.replace(/file:\/\//g, pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { MdEditor };
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
|
||||||
import { InfoDialog } from 'vj/components/dialog';
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
import VjNotification from 'vj/components/notification/index';
|
import VjNotification from 'vj/components/notification/index';
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
} from 'vj/constant/message';
|
} from 'vj/constant/message';
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
import { i18n, tpl } from 'vj/utils';
|
import { i18n, tpl } from 'vj/utils';
|
||||||
|
import Sock from '../socket';
|
||||||
|
|
||||||
let previous: VjNotification;
|
let previous: VjNotification;
|
||||||
const onmessage = (msg) => {
|
const onmessage = (msg) => {
|
||||||
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
|
|||||||
|
|
||||||
const initWorkerMode = (endpoint) => {
|
const initWorkerMode = (endpoint) => {
|
||||||
console.log('Messages: using SharedWorker');
|
console.log('Messages: using SharedWorker');
|
||||||
const worker = new SharedWorker('/messages-shared-worker.js', { name: 'HydroMessagesWorker' });
|
const worker = new SharedWorker(new URL('./worker.ts', import.meta.url), { name: 'HydroMessagesWorker' });
|
||||||
worker.port.start();
|
worker.port.start();
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
worker.port.postMessage({ type: 'unload' });
|
worker.port.postMessage({ type: 'unload' });
|
||||||
@@ -128,12 +128,8 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
|||||||
isMaster = true;
|
isMaster = true;
|
||||||
localStorage.setItem('page.master', selfId);
|
localStorage.setItem('page.master', selfId);
|
||||||
const masterChannel = new BroadcastChannel('hydro-messages');
|
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||||
const sock = new ReconnectingWebsocket(endpoint);
|
const sock = new Sock(endpoint);
|
||||||
sock.onopen = () => console.log('Connected');
|
|
||||||
sock.onerror = console.error;
|
|
||||||
sock.onclose = (...args) => console.log('Closed', ...args);
|
|
||||||
sock.onmessage = async (message) => {
|
sock.onmessage = async (message) => {
|
||||||
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
|
|
||||||
const payload = JSON.parse(message.data);
|
const payload = JSON.parse(message.data);
|
||||||
masterChannel.postMessage({ type: 'message', payload });
|
masterChannel.postMessage({ type: 'message', payload });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,12 +31,17 @@ function broadcastMsg(message: any) {
|
|||||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||||
if (cookie !== lcookie) conn?.close();
|
if (cookie !== lcookie) conn?.close();
|
||||||
else if (conn && conn.readyState === conn.OPEN) return;
|
else if (conn && conn.readyState === conn.OPEN) return;
|
||||||
|
const url = new URL(path, location.origin);
|
||||||
|
if (cookie) url.searchParams.set('sid', cookie.split('sid=')[1].split(';')[0]);
|
||||||
lcookie = cookie;
|
lcookie = cookie;
|
||||||
console.log('Init connection for', path);
|
console.log('Init connection for', path);
|
||||||
conn?.close();
|
conn?.close();
|
||||||
conn = new ReconnectingWebsocket(path);
|
conn = new ReconnectingWebsocket(url.toString());
|
||||||
ports.push(port);
|
ports.push(port);
|
||||||
conn.onopen = () => conn.send(cookie);
|
conn.onopen = () => {
|
||||||
|
console.log('Connected to', path);
|
||||||
|
broadcastMsg({ type: 'open' });
|
||||||
|
};
|
||||||
conn.onerror = () => broadcastMsg({ type: 'error' });
|
conn.onerror = () => broadcastMsg({ type: 'error' });
|
||||||
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
||||||
conn.onmessage = (message) => {
|
conn.onmessage = (message) => {
|
||||||
@@ -46,14 +51,9 @@ function initConn(path: string, port: MessagePort, cookie: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = JSON.parse(message.data);
|
const payload = JSON.parse(message.data);
|
||||||
if (payload.event === 'auth') {
|
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
broadcastMsg({ type: 'close', error: payload.error });
|
||||||
broadcastMsg({ type: 'close', error: payload.error });
|
conn.close();
|
||||||
conn.close();
|
|
||||||
} else {
|
|
||||||
console.log('Connected to', path);
|
|
||||||
broadcastMsg({ type: 'open' });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
broadcastMsg({ type: 'message', payload });
|
broadcastMsg({ type: 'message', payload });
|
||||||
let acked = false;
|
let acked = false;
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||||
disposable = [];
|
disposable = [];
|
||||||
containerElement: HTMLElement;
|
containerElement: HTMLElement;
|
||||||
private __preventUpdate = false;
|
__preventUpdate = false;
|
||||||
private __preventFormat = false;
|
__preventFormat = false;
|
||||||
|
|
||||||
editor: editor.IStandaloneCodeEditor;
|
editor: editor.IStandaloneCodeEditor;
|
||||||
model: editor.ITextModel;
|
model: editor.ITextModel;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`;
|
let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`;
|
||||||
if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`;
|
if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`;
|
||||||
|
|
||||||
|
// TODO switch to indexeddb
|
||||||
export default function reducer(state = {
|
export default function reducer(state = {
|
||||||
lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang,
|
lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang,
|
||||||
code: localStorage.getItem(cacheKey) || UiContext.codeTemplate,
|
code: localStorage.getItem(cacheKey) || UiContext.codeTemplate,
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export default class Sock {
|
|||||||
onclose: (code: number, reason: string) => void;
|
onclose: (code: number, reason: string) => void;
|
||||||
onopen: (sock: ReconnectingWebSocket) => void;
|
onopen: (sock: ReconnectingWebSocket) => void;
|
||||||
|
|
||||||
|
on(event: 'message' | 'close' | 'open', callback: (...args: any[]) => void) {
|
||||||
|
this[`on${event}`] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
send(data) {
|
send(data) {
|
||||||
this.sock.send(data);
|
this.sock.send(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'lodash';
|
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
|
||||||
const TAB_TRANSITION_DURATION = 300;
|
const TAB_TRANSITION_DURATION = 300;
|
||||||
@@ -144,5 +143,3 @@ export default class Tab extends DOMAttachedObject {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_.assign(Tab, DOMAttachedObject);
|
|
||||||
@@ -73,6 +73,26 @@
|
|||||||
background: $secondary-color
|
background: $secondary-color
|
||||||
content: ''
|
content: ''
|
||||||
|
|
||||||
|
.section__tab-container:not(.immersive)
|
||||||
|
.section__tab-header
|
||||||
|
padding-top: 0 !important
|
||||||
|
|
||||||
|
.section__tab-header-item
|
||||||
|
border-top: 3px solid transparent;
|
||||||
|
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||||
|
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
margin-right: 0
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color: #5f9fd6;
|
||||||
|
background: #f8f8f8;
|
||||||
|
|
||||||
|
&.tab--active
|
||||||
|
border-color: #ed5f82;
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border-color: #ed5f82;
|
||||||
|
|
||||||
.section__tab-content
|
.section__tab-content
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface UploadOptions {
|
|||||||
pjax?: boolean;
|
pjax?: boolean;
|
||||||
sidebar?: boolean;
|
sidebar?: boolean;
|
||||||
singleFileUploadCallback?: (file: File) => any;
|
singleFileUploadCallback?: (file: File) => any;
|
||||||
|
filenameCallback?: (file: File) => string;
|
||||||
}
|
}
|
||||||
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
||||||
const dialog = new Dialog({
|
const dialog = new Dialog({
|
||||||
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
|
|||||||
if (Number.isNaN(+i)) continue;
|
if (Number.isNaN(+i)) continue;
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('filename', file.name);
|
data.append('filename', options.filenameCallback?.(file) || file.name);
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
if (options.type) data.append('type', options.type);
|
if (options.type) data.append('type', options.type);
|
||||||
data.append('operation', 'upload_file');
|
data.append('operation', 'upload_file');
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './polyfill';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
@@ -29,17 +31,23 @@ console.log(
|
|||||||
window.UiContext = JSON.parse(window.UiContext);
|
window.UiContext = JSON.parse(window.UiContext);
|
||||||
window.UserContext = JSON.parse(window.UserContext);
|
window.UserContext = JSON.parse(window.UserContext);
|
||||||
if (process.env.NODE_ENV === 'production' && !UiContext.sentry_disable) {
|
if (process.env.NODE_ENV === 'production' && !UiContext.sentry_disable) {
|
||||||
window.captureException = (e) => Sentry.captureException(e);
|
window.captureException = (e) => {
|
||||||
|
if (!e.isUserFacingError) Sentry.captureException(e);
|
||||||
|
};
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: UiContext.sentry_dsn || 'https://04d1d77b095231d55e1202bdc912ff16@sentry.hydro.ac/3',
|
dsn: UiContext.sentry_dsn || 'https://2f95d53751e08c74c1af1c4b93ccaff7@sentry.hydro.ac/2',
|
||||||
release: process.env.VERSION,
|
release: `hydro-web@${process.env.VERSION}`,
|
||||||
integrations: [
|
integrations: [
|
||||||
Sentry.browserTracingIntegration(),
|
Sentry.browserTracingIntegration(),
|
||||||
Sentry.replayIntegration(),
|
Sentry.browserApiErrorsIntegration(),
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
networkRequestHeaders: ['Content-Type'],
|
||||||
|
networkResponseHeaders: ['Content-Type', 'Location'],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
tracesSampleRate: 0.1,
|
tracesSampleRate: 0.1,
|
||||||
tracePropagationTargets: ['localhost', /^\//, window.location.host],
|
tracePropagationTargets: ['localhost', /^\//, window.location.host],
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.03,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -57,16 +65,10 @@ const PageLoader = '<div class="page-loader nojs--hide" style="display:none;"><d
|
|||||||
$('body').prepend(PageLoader);
|
$('body').prepend(PageLoader);
|
||||||
$('.page-loader').fadeIn(500);
|
$('.page-loader').fadeIn(500);
|
||||||
|
|
||||||
const prefetch = Promise.all([
|
|
||||||
fetch(`/constant/${UiContext.constantVersion}.js`).then((r) => r.text()),
|
|
||||||
import('./api'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
Object.assign(window.UiContext, JSON.parse(window.UiContextNew));
|
Object.assign(window.UiContext, JSON.parse(window.UiContextNew));
|
||||||
Object.assign(window.UserContext, JSON.parse(window.UserContextNew));
|
Object.assign(window.UserContext, JSON.parse(window.UserContextNew));
|
||||||
const [data, HydroExports] = await prefetch;
|
window.HydroExports = await import('./api');
|
||||||
Object.assign(window, { HydroExports });
|
await window._hydroLoad();
|
||||||
eval(data); // eslint-disable-line no-eval
|
await window.HydroExports.initPageLoader();
|
||||||
await HydroExports.initPageLoader();
|
|
||||||
}, false);
|
}, false);
|
||||||
@@ -10,13 +10,6 @@ class WikiHelpHandler extends Handler {
|
|||||||
noCheckPermView = true;
|
noCheckPermView = true;
|
||||||
|
|
||||||
async get() {
|
async get() {
|
||||||
const LANGS = SettingModel.langs;
|
|
||||||
const languages = {};
|
|
||||||
for (const key in LANGS) {
|
|
||||||
if (LANGS[key].hidden) continue;
|
|
||||||
languages[`${LANGS[key].display}(${key})`] = LANGS[key].compile || LANGS[key].execute;
|
|
||||||
}
|
|
||||||
this.response.body = { languages };
|
|
||||||
this.response.template = 'wiki_help.html';
|
this.response.template = 'wiki_help.html';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.50.2",
|
"version": "4.51.4",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
"cli": false
|
"cli": false
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/core": "^5.10.3",
|
"@blueprintjs/core": "^5.10.5",
|
||||||
"@codingame/monaco-editor-wrapper": "^6.6.10",
|
"@codingame/monaco-editor-wrapper": "^6.6.10",
|
||||||
"@codingame/monaco-vscode-language-pack-ko": "5.3.0",
|
"@codingame/monaco-vscode-language-pack-ko": "5.3.0",
|
||||||
"@codingame/monaco-vscode-language-pack-zh-hans": "5.3.0",
|
"@codingame/monaco-vscode-language-pack-zh-hans": "5.3.0",
|
||||||
"@codingame/monaco-vscode-language-pack-zh-hant": "5.3.0",
|
"@codingame/monaco-vscode-language-pack-zh-hant": "5.3.0",
|
||||||
"@codingame/monaco-vscode-typescript-language-features-default-extension": "5.3.0",
|
"@codingame/monaco-vscode-typescript-language-features-default-extension": "5.3.0",
|
||||||
"@cordisjs/core": "^3.16.2",
|
"@cordisjs/core": "3.17.3",
|
||||||
"@fontsource/dm-mono": "^5.0.20",
|
"@fontsource/dm-mono": "^5.0.20",
|
||||||
"@fontsource/fira-code": "^5.0.18",
|
"@fontsource/fira-code": "^5.0.18",
|
||||||
"@fontsource/inconsolata": "^5.0.18",
|
"@fontsource/inconsolata": "^5.0.18",
|
||||||
@@ -30,12 +30,13 @@
|
|||||||
"@fontsource/source-code-pro": "^5.0.18",
|
"@fontsource/source-code-pro": "^5.0.18",
|
||||||
"@fontsource/ubuntu-mono": "^5.0.20",
|
"@fontsource/ubuntu-mono": "^5.0.20",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"@sentry/browser": "^8.8.0",
|
"@sentry/browser": "^8.15.0",
|
||||||
"@sentry/webpack-plugin": "^2.18.0",
|
"@sentry/webpack-plugin": "^2.20.1",
|
||||||
"@simplewebauthn/browser": "7.4.0",
|
"@simplewebauthn/browser": "7.4.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/gulp-if": "^0.0.34",
|
"@types/gulp-if": "^0.0.34",
|
||||||
"@types/jquery": "^3.5.30",
|
"@types/jquery": "^3.5.30",
|
||||||
|
"@types/jquery.transit": "^0.9.33",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
@@ -45,25 +46,28 @@
|
|||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/redux-logger": "^3.0.13",
|
"@types/redux-logger": "^3.0.13",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/vscode": "^1.89.0",
|
"@types/vscode": "^1.89.0",
|
||||||
"@types/webpack-env": "^1.18.5",
|
"@types/webpack-env": "^1.18.5",
|
||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
"ajv": "^8.16.0",
|
"ajv": "^8.16.0",
|
||||||
"allotment": "^1.20.2",
|
"allotment": "^1.20.2",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"browser-update": "^3.3.51",
|
"browser-update": "^3.3.53",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"core-js": "^3.37.1",
|
||||||
|
"core-js-compat": "^3.37.1",
|
||||||
"css-loader": "^6.11.0",
|
"css-loader": "^6.11.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
"diff-dom": "^5.1.3",
|
"diff-dom": "^5.1.3",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.1",
|
||||||
"emojis-keywords": "2.0.0",
|
"emojis-keywords": "2.0.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"esbuild-loader": "^4.1.0",
|
"esbuild-loader": "^4.2.0",
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"graphiql": "^2.4.7",
|
"graphiql": "^2.4.7",
|
||||||
@@ -72,17 +76,19 @@
|
|||||||
"gulp-if": "^3.0.0",
|
"gulp-if": "^3.0.0",
|
||||||
"gulp-plumber": "^1.2.1",
|
"gulp-plumber": "^1.2.1",
|
||||||
"gulp-svgmin": "^4.1.0",
|
"gulp-svgmin": "^4.1.0",
|
||||||
|
"idb": "^8.0.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-scroll-lock": "^3.1.3",
|
"jquery-scroll-lock": "^3.1.3",
|
||||||
"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": "^4.17.3",
|
||||||
"mini-css-extract-plugin": "^2.9.0",
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0",
|
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0",
|
||||||
"monaco-languageclient": "^8.4.0",
|
"monaco-languageclient": "^8.4.0",
|
||||||
"monaco-themes": "^0.4.4",
|
"monaco-themes": "^0.4.4",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "5.1.1",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
@@ -95,7 +101,6 @@
|
|||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qface": "^1.4.1",
|
"qface": "^1.4.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"queue-microtask": "^1.2.3",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
@@ -129,7 +134,7 @@
|
|||||||
"vscode": "npm:@codingame/monaco-vscode-api@^5.3.0",
|
"vscode": "npm:@codingame/monaco-vscode-api@^5.3.0",
|
||||||
"vscode-languageclient": "^9.0.1",
|
"vscode-languageclient": "^9.0.1",
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.92.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-dev-server": "^5.0.4",
|
"webpack-dev-server": "^5.0.4",
|
||||||
"webpack-manifest-plugin": "^5.0.0",
|
"webpack-manifest-plugin": "^5.0.0",
|
||||||
@@ -142,10 +147,10 @@
|
|||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.11",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-anchor": "^8.6.7",
|
"markdown-it-anchor": "^9.0.1",
|
||||||
"markdown-it-attrs": "^4.1.6",
|
"markdown-it-attrs": "^4.1.6",
|
||||||
"markdown-it-footnote": "^4.0.0",
|
"markdown-it-footnote": "^4.0.0",
|
||||||
"markdown-it-imsize": "^2.0.1",
|
"markdown-it-imsize": "^2.0.1",
|
||||||
@@ -155,6 +160,7 @@
|
|||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"schemastery-jsonschema": "^1.1.0",
|
"schemastery-jsonschema": "^1.1.0",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"xss": "^1.0.15"
|
"xss": "^1.0.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import { pjax } from 'vj/utils';
|
import { pjax } from 'vj/utils';
|
||||||
|
import { openDB } from 'vj/utils/db';
|
||||||
|
|
||||||
const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () => {
|
const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], async () => {
|
||||||
|
const db = await openDB;
|
||||||
const { tdoc } = UiContext;
|
const { tdoc } = UiContext;
|
||||||
const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`;
|
const id = `${tdoc.domainId}/${tdoc.docId}`;
|
||||||
const read = () => JSON.parse(localStorage.getItem(key) || '[]');
|
const read = async () => (await db.get('scoreboard-star', id))?.value || [];
|
||||||
const write = (data) => localStorage.setItem(key, JSON.stringify(data));
|
const write = (data) => db.put('scoreboard-star', { id, data });
|
||||||
|
|
||||||
$(`.star.user--${UserContext._id}`).closest('tr').addClass('star-highlight');
|
$(`.star.user--${UserContext._id}`).closest('tr').addClass('star-highlight');
|
||||||
read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
|
(await read()).forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
|
||||||
$('.star').on('click', (e) => {
|
$('.star').on('click', async (e) => {
|
||||||
const star = read();
|
const star = await read();
|
||||||
const $target = $(e.currentTarget);
|
const $target = $(e.currentTarget);
|
||||||
const uid = $target.data('uid');
|
const uid = $target.data('uid');
|
||||||
if (star.includes(uid)) {
|
if (star.includes(uid)) {
|
||||||
@@ -24,14 +26,14 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
|
|||||||
write(star);
|
write(star);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.select.filter').on('change', (e) => {
|
async function update() {
|
||||||
const val = $(e.target).val();
|
const val = $($('.select.filter')).val();
|
||||||
if (val === 'all') {
|
if (val === 'all') {
|
||||||
$('.data-table tbody tr').show();
|
$('.data-table tbody tr').show();
|
||||||
read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight'));
|
(await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight'));
|
||||||
} else if (val === 'star') {
|
} else if (val === 'star') {
|
||||||
$('.data-table tbody tr').hide();
|
$('.data-table tbody tr').hide();
|
||||||
read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight'));
|
(await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight'));
|
||||||
} else if (val === 'rank') {
|
} else if (val === 'rank') {
|
||||||
$('.data-table tbody tr').show();
|
$('.data-table tbody tr').show();
|
||||||
$('.rank--unrank').closest('tr').hide();
|
$('.rank--unrank').closest('tr').hide();
|
||||||
@@ -41,13 +43,17 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
|
|||||||
if (!uids?.length) return;
|
if (!uids?.length) return;
|
||||||
uids.forEach((uid) => $(`.user--${uid}`).closest('tr').show());
|
uids.forEach((uid) => $(`.user--${uid}`).closest('tr').show());
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
$('.select.filter').on('change', update);
|
||||||
|
|
||||||
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
|
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
|
||||||
const endAt = new Date(UiContext.tdoc.endAt).getTime();
|
const endAt = new Date(UiContext.tdoc.endAt).getTime();
|
||||||
function updateScoreboard() {
|
async function updateScoreboard() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (beginAt <= now && now <= endAt) pjax.request({ url: UiContext.scoreboardUrl || '', push: false });
|
if (beginAt <= now && now <= endAt) {
|
||||||
|
await pjax.request({ url: UiContext.scoreboardUrl || '', push: false });
|
||||||
|
await update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(updateScoreboard, 180000);
|
setInterval(updateScoreboard, 180000);
|
||||||
|
|||||||
+33
-23
@@ -9,15 +9,14 @@ import { NamedPage } from 'vj/misc/Page';
|
|||||||
import {
|
import {
|
||||||
delay, i18n, loadReactRedux, pjax, request, tpl,
|
delay, i18n, loadReactRedux, pjax, request, tpl,
|
||||||
} from 'vj/utils';
|
} from 'vj/utils';
|
||||||
|
import { openDB } from 'vj/utils/db';
|
||||||
|
|
||||||
class ProblemPageExtender {
|
class ProblemPageExtender {
|
||||||
constructor() {
|
isExtended = false;
|
||||||
this.isExtended = false;
|
inProgress = false;
|
||||||
this.inProgress = false;
|
$content = $('.problem-content-container');
|
||||||
this.$content = $('.problem-content-container');
|
$contentBound = this.$content.closest('.section');
|
||||||
this.$contentBound = this.$content.closest('.section');
|
$scratchpadContainer = $('.scratchpad-container');
|
||||||
this.$scratchpadContainer = $('.scratchpad-container');
|
|
||||||
}
|
|
||||||
|
|
||||||
async extend() {
|
async extend() {
|
||||||
if (this.inProgress) return;
|
if (this.inProgress) return;
|
||||||
@@ -28,6 +27,7 @@ class ProblemPageExtender {
|
|||||||
.get(0)
|
.get(0)
|
||||||
.getBoundingClientRect();
|
.getBoundingClientRect();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
this.$content.transition({ opacity: 0 }, { duration: 100 });
|
this.$content.transition({ opacity: 0 }, { duration: 100 });
|
||||||
await delay(100);
|
await delay(100);
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ class ProblemPageExtender {
|
|||||||
})
|
})
|
||||||
.show()
|
.show()
|
||||||
.transition({
|
.transition({
|
||||||
|
// @ts-ignore
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -77,6 +78,7 @@ class ProblemPageExtender {
|
|||||||
|
|
||||||
await this.$scratchpadContainer
|
await this.$scratchpadContainer
|
||||||
.transition({
|
.transition({
|
||||||
|
// @ts-ignore
|
||||||
left: bound.left,
|
left: bound.left,
|
||||||
top: bound.top,
|
top: bound.top,
|
||||||
width: bound.width,
|
width: bound.width,
|
||||||
@@ -88,6 +90,7 @@ class ProblemPageExtender {
|
|||||||
.promise();
|
.promise();
|
||||||
|
|
||||||
this.$scratchpadContainer.hide();
|
this.$scratchpadContainer.hide();
|
||||||
|
// @ts-ignore
|
||||||
this.$content.transition({ opacity: 1 }, { duration: 100 });
|
this.$content.transition({ opacity: 1 }, { duration: 100 });
|
||||||
window.document.body.style.overflow = 'scroll';
|
window.document.body.style.overflow = 'scroll';
|
||||||
|
|
||||||
@@ -112,6 +115,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function scratchpadFadeIn() {
|
async function scratchpadFadeIn() {
|
||||||
|
// @ts-ignore
|
||||||
await $('#scratchpad')
|
await $('#scratchpad')
|
||||||
.transition(
|
.transition(
|
||||||
{ opacity: 1 },
|
{ opacity: 1 },
|
||||||
@@ -121,6 +125,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function scratchpadFadeOut() {
|
async function scratchpadFadeOut() {
|
||||||
|
// @ts-ignore
|
||||||
await $('#scratchpad')
|
await $('#scratchpad')
|
||||||
.transition(
|
.transition(
|
||||||
{ opacity: 0 },
|
{ opacity: 0 },
|
||||||
@@ -138,6 +143,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
|
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
|
||||||
const { Provider, store } = await loadReactRedux(ScratchpadReducer);
|
const { Provider, store } = await loadReactRedux(ScratchpadReducer);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
window.store = store;
|
window.store = store;
|
||||||
const sock = new WebSocket(UiContext.ws_prefix + UiContext.pretestConnUrl);
|
const sock = new WebSocket(UiContext.ws_prefix + UiContext.pretestConnUrl);
|
||||||
sock.onmessage = (message) => {
|
sock.onmessage = (message) => {
|
||||||
@@ -237,22 +243,25 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
[, setUpdate] = React.useState(0);
|
[, setUpdate] = React.useState(0);
|
||||||
return <div className="contest-problems" style={{ margin: '1em' }}>
|
return <div className="contest-problems" style={{ margin: '1em' }}>
|
||||||
{pids.map((i) => <a href={`#p${i}`} className={ans[i] ? 'pass ' : ''}>
|
{pids.map((i) => <a href={`#p${i}`} className={ans[i] ? 'pass ' : ''}>
|
||||||
<span class="id">{i}</span>
|
<span className="id">{i}</span>
|
||||||
{ans[i] && <span class="icon icon-check"></span>}
|
{ans[i] && <span className="icon icon-check"></span>}
|
||||||
</a>)}
|
</a>)}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAns() {
|
const db = await openDB;
|
||||||
localStorage.setItem(`${cacheKey}#objective`, JSON.stringify(ans));
|
async function saveAns() {
|
||||||
setUpdate?.((i) => i + 1);
|
await db.put('solutions', {
|
||||||
|
id: `${cacheKey}#objective`,
|
||||||
|
value: JSON.stringify(ans),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function clearAns() {
|
async function clearAns() {
|
||||||
localStorage.removeItem(`${cacheKey}#objective`);
|
await db.delete('solutions', `${cacheKey}#objective`);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
function loadAns() {
|
async function loadAns() {
|
||||||
const saved = localStorage.getItem(`${cacheKey}#objective`);
|
const saved = await db.get('solutions', `${cacheKey}#objective`);
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
const isValidOption = (v) => v.length === 1 && v.charCodeAt(0) >= 65 && v.charCodeAt(0) <= 90;
|
const isValidOption = (v) => v.length === 1 && v.charCodeAt(0) >= 65 && v.charCodeAt(0) <= 90;
|
||||||
Object.assign(ans, JSON.parse(saved));
|
Object.assign(ans, JSON.parse(saved));
|
||||||
@@ -261,27 +270,28 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
for (const v of val) {
|
for (const v of val) {
|
||||||
if (isValidOption(v)) $(`.objective_${id} input[value="${v}"]`).prop('checked', true);
|
if (isValidOption(v)) $(`.objective_${id} input[value="${v}"]`).prop('checked', true);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (val) {
|
||||||
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val);
|
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val.toString());
|
||||||
if (isValidOption(val)) $(`.objective_${id}.radiobox [value="${val}"]`).prop('checked', true);
|
if (isValidOption(val)) $(`.objective_${id}.radiobox [value="${val}"]`).prop('checked', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setUpdate((v) => v + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cnt) {
|
if (cnt) {
|
||||||
loadAns();
|
await loadAns();
|
||||||
$('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length
|
$('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length
|
||||||
? `<input type="submit" disabled class="button rounded primary disabled" value="${i18n('Login to Submit')}" />`
|
? `<input type="submit" disabled class="button rounded primary disabled" value="${i18n('Login to Submit')}" />`
|
||||||
: `<input type="submit" class="button rounded primary" value="${i18n('Submit')}" />`);
|
: `<input type="submit" class="button rounded primary" value="${i18n('Submit')}" />`);
|
||||||
$('.objective-input[type!=checkbox]').on('input', (e) => {
|
$('.objective-input[type!=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
|
||||||
ans[e.target.name] = e.target.value;
|
ans[e.target.name] = e.target.value;
|
||||||
saveAns();
|
saveAns();
|
||||||
});
|
});
|
||||||
$('input.objective-input[type=checkbox]').on('input', (e) => {
|
$('input.objective-input[type=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
ans[e.target.name] ||= [];
|
ans[e.target.name] ||= [];
|
||||||
ans[e.target.name].push(e.target.value);
|
ans[e.target.name].push(e.target.value);
|
||||||
ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0));
|
ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a: string, b: string) => a.charCodeAt(0) - b.charCodeAt(0));
|
||||||
} else {
|
} else {
|
||||||
ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value);
|
ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value);
|
||||||
}
|
}
|
||||||
@@ -310,7 +320,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
|||||||
const result = await new ConfirmDialog({
|
const result = await new ConfirmDialog({
|
||||||
$body: tpl.typoMsg('All changes will be lost. Are you sure to clear all answers?'),
|
$body: tpl.typoMsg('All changes will be lost. Are you sure to clear all answers?'),
|
||||||
}).open();
|
}).open();
|
||||||
if (result === 'yes') clearAns();
|
if (result === 'yes') await clearAns();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const ele = document.createElement('div');
|
const ele = document.createElement('div');
|
||||||
@@ -154,7 +154,7 @@ async function handleSection(ev, sidebar, type) {
|
|||||||
$section.removeClass('animating');
|
$section.removeClass('animating');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
export default new NamedPage(['problem_create', 'problem_edit'], () => {
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
$(document).on('click', '[name="operation"]', (ev) => {
|
$(document).on('click', '[name="operation"]', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@@ -240,14 +240,6 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
|||||||
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
$('img').each(function () {
|
|
||||||
if (this.src.startsWith('file://')) {
|
|
||||||
$(this).attr('src', $(this).attr('src').replace('file://', (pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/')));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const $main = $('textarea[data-editor]');
|
const $main = $('textarea[data-editor]');
|
||||||
const $field = $('textarea[data-markdown-upload]');
|
const $field = $('textarea[data-markdown-upload]');
|
||||||
let content = $field.val();
|
let content = $field.val();
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
.page--problem_edit,.page--problem_create
|
.page--problem_edit,.page--problem_create
|
||||||
.section__tab-header-item
|
|
||||||
border-top: 3px solid transparent;
|
|
||||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
|
||||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
||||||
margin-right: 0
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: #5f9fd6;
|
|
||||||
background: #f8f8f8;
|
|
||||||
|
|
||||||
&.tab--active
|
|
||||||
border-color: #ed5f82;
|
|
||||||
|
|
||||||
.problem-sidebar-additional_file
|
.problem-sidebar-additional_file
|
||||||
height: 270px
|
height: 270px
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import 'core-js/stable';
|
||||||
import 'matchmedia-polyfill';
|
import 'matchmedia-polyfill';
|
||||||
|
|
||||||
import browserUpdate from 'browser-update';
|
import browserUpdate from 'browser-update';
|
||||||
import queueMicrotask from 'queue-microtask';
|
|
||||||
|
|
||||||
window.queueMicrotask = queueMicrotask;
|
|
||||||
browserUpdate({
|
browserUpdate({
|
||||||
required: {
|
required: {
|
||||||
e: -10, f: -10, o: -3, s: -1, c: -10,
|
e: -10, f: -10, o: -3, s: -1, c: -10,
|
||||||
@@ -13,11 +12,23 @@ browserUpdate({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// monaco-editor requires this polyfill
|
// monaco-editor requires this polyfill
|
||||||
if (window.MediaQueryList) {
|
if (window.MediaQueryList && !MediaQueryList.prototype.addEventListener) {
|
||||||
MediaQueryList.prototype.addEventListener = (k, listener) => {
|
MediaQueryList.prototype.addEventListener = (k, listener) => {
|
||||||
MediaQueryList.prototype.addListener(listener);
|
MediaQueryList.prototype.addListener(listener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (typeof window['WeakRef'] === 'undefined') {
|
||||||
|
// @ts-ignore
|
||||||
|
window.WeakRef = (function (wm) {
|
||||||
|
function WeakRef(target) {
|
||||||
|
wm.set(this, target);
|
||||||
|
}
|
||||||
|
WeakRef.prototype.deref = function () {
|
||||||
|
return wm.get(this);
|
||||||
|
};
|
||||||
|
return WeakRef;
|
||||||
|
}(new WeakMap()));
|
||||||
|
}
|
||||||
if (!(window.matchMedia('all').addListener || window.matchMedia('all').addEventListener)) {
|
if (!(window.matchMedia('all').addListener || window.matchMedia('all').addEventListener)) {
|
||||||
const localMatchMedia = window.matchMedia;
|
const localMatchMedia = window.matchMedia;
|
||||||
const hasMediaQueries = localMatchMedia('only all').matches;
|
const hasMediaQueries = localMatchMedia('only all').matches;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ self.addEventListener('notificationclick', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PRECACHE = `precache-${process.env.VERSION}`;
|
const PRECACHE = `precache-${process.env.VERSION}`;
|
||||||
const DO_NOT_PRECACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco'];
|
const DO_NOT_PRECACHE = ['.worker.js', 'fonts', 'i.monaco'];
|
||||||
|
|
||||||
function shouldCachePath(path: string) {
|
function shouldCachePath(path: string) {
|
||||||
if (!path.split('?')[0].split('/').pop()) return false;
|
if (!path.split('?')[0].split('/').pop()) return false;
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ nav_logo_dark:
|
|||||||
default: /components/navigation/nav-logo-small_dark.png
|
default: /components/navigation/nav-logo-small_dark.png
|
||||||
preferredEditorType:
|
preferredEditorType:
|
||||||
type:
|
type:
|
||||||
wysiwyg: WYSIWYG
|
|
||||||
ir: Instant Rendering
|
|
||||||
sv: Split View
|
sv: Split View
|
||||||
monaco: Monaco Editor
|
monaco: Monaco Editor
|
||||||
family: markdown
|
family: markdown
|
||||||
|
|||||||
@@ -46,32 +46,18 @@
|
|||||||
<script>
|
<script>
|
||||||
var _htmlNode = document.documentElement;
|
var _htmlNode = document.documentElement;
|
||||||
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
|
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
|
||||||
try {
|
|
||||||
import('data:text/javascript,');Object.fromEntries([]);window.matchMedia('all').addEventListener('change',()=>{});
|
|
||||||
} catch(e) {
|
|
||||||
const s = document.createElement('script');
|
|
||||||
s.async = false;
|
|
||||||
s.src = "{{ '/polyfill.js' if process.env.DEV else (UiContext.cdn_prefix + 'polyfill-' + global.Hydro.version['ui-default'] + '.js') }}";
|
|
||||||
document.head.appendChild(s);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
|
||||||
<script>
|
|
||||||
var UiContext = '{{ UiContext|json|jsesc|safe }}';
|
var UiContext = '{{ UiContext|json|jsesc|safe }}';
|
||||||
var UserContext = '{{ UserContext|json|jsesc|safe }}';
|
var UserContext = '{{ UserContext|json|jsesc|safe }}';
|
||||||
</script>
|
</script>
|
||||||
{% set UiContext = Object.create(UiContext) %}
|
{% endif %}
|
||||||
{% set UserContext = Object.create(UserContext) %}
|
{% set UiContext = Object.create(UiContext) %}
|
||||||
{% if process.env.DEV %}
|
{% set UserContext = Object.create(UserContext) %}
|
||||||
<script type="text/javascript" src="/resource/hash/lang-{{ _('__id') }}.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
</head>
|
||||||
<script type="text/javascript" src="/hydro.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
<body>
|
||||||
{% else %}
|
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
||||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
|
<script type="text/javascript" src="/constant/{{ UiContext.constantVersion }}.js"></script>
|
||||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
|
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
|
||||||
{% endif %}
|
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
{% block script %}{% endblock %}
|
{% block script %}{% endblock %}
|
||||||
|
|||||||
@@ -3,41 +3,71 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__body no-padding">
|
<div class="section__tab-container">
|
||||||
<div data-fragment-id="status">
|
<ul class="section__tabs">
|
||||||
<table class="data-table">
|
<li class="section__tab-item">
|
||||||
<colgroup>
|
<h1 class="section__tab-title">{{ _('Servers') }}</h1>
|
||||||
<col class="col--id">
|
<div class="section__tab-main">
|
||||||
<col class="col--status">
|
<table class="data-table">
|
||||||
<col class="col--os">
|
<colgroup>
|
||||||
<col class="col--cpu">
|
<col class="col--id">
|
||||||
<col class="col--memory">
|
<col class="col--status">
|
||||||
<col class="col--request">
|
<col class="col--os">
|
||||||
</colgroup>
|
<col class="col--cpu">
|
||||||
<thead>
|
<col class="col--memory">
|
||||||
<tr>
|
<col class="col--request">
|
||||||
<th class="col--id">{{ _('ID') }}</th>
|
</colgroup>
|
||||||
<th class="col--status">{{ _('Current Status') }}</th>
|
<thead>
|
||||||
<th class="col--os">{{ _('OS Info') }}</th>
|
<tr>
|
||||||
<th class="col--cpu">{{ _('CPU Info') }}</th>
|
<th class="col--id">{{ _('ID') }}</th>
|
||||||
<th class="col--memory">{{ _('Memory Info') }}</th>
|
<th class="col--status">{{ _('Current Status') }}</th>
|
||||||
<th class="col--request">{{ _('Handled requests') }}</th>
|
<th class="col--os">{{ _('OS Info') }}</th>
|
||||||
</tr>
|
<th class="col--cpu">{{ _('CPU Info') }}</th>
|
||||||
</thead>
|
<th class="col--memory">{{ _('Memory Info') }}</th>
|
||||||
<tbody>
|
<th class="col--request">{{ _('Handled requests') }}</th>
|
||||||
{%- for doc in stats -%}
|
</tr>
|
||||||
<tr>
|
</thead>
|
||||||
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
|
<tbody>
|
||||||
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
|
{%- for doc in stats -%}
|
||||||
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
|
<tr>
|
||||||
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
|
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
|
||||||
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
|
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
|
||||||
<td class="col--request">{{ doc.reqCount }}</td>
|
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
|
||||||
</tr>
|
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
|
||||||
{%- endfor -%}
|
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
|
||||||
</tbody>
|
<td class="col--request">{{ doc.reqCount }}</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% if compilers|length %}
|
||||||
|
<li class="section__tab-item">
|
||||||
|
<h1 class="section__tab-title">{{ _('Compiler Versions') }}</h1>
|
||||||
|
<div class="section__tab-main">
|
||||||
|
<div class="section__body typo">
|
||||||
|
{% for line in compilers %}
|
||||||
|
<p>{{ line.key|join(', ') }}</p>
|
||||||
|
<pre><code>{{ line.message }}</code></pre>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="section__tab-item">
|
||||||
|
<h1 class="section__tab-title">{{ _('Compile Commands') }}</h1>
|
||||||
|
<div class="section__tab-main">
|
||||||
|
<div class="section__body typo">
|
||||||
|
<ul>
|
||||||
|
{%- for k, v in languages -%}
|
||||||
|
<li>{{ k }} <pre>{{ v }}</pre></li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,14 +17,7 @@
|
|||||||
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
|
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数如下:</p>
|
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数和当前所用编译器版本见状态页。</p>
|
||||||
{% if languages %}
|
|
||||||
<blockquote><ul>
|
|
||||||
{%- for k, v in languages -%}
|
|
||||||
<li>{{ k }} <pre>{{ v }}</pre></li>
|
|
||||||
{%- endfor -%}
|
|
||||||
</ul></blockquote>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ $hover-background-color = #424242
|
|||||||
background-color: #171717
|
background-color: #171717
|
||||||
color: #999
|
color: #999
|
||||||
|
|
||||||
pre:not(.vditor-sv), code
|
pre, code
|
||||||
background-color: #171717
|
background-color: #171717
|
||||||
|
|
||||||
.scratchpad__toolbar
|
.scratchpad__toolbar
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'normalize.css/normalize.css';
|
import 'normalize.css/normalize.css';
|
||||||
import 'vditor/dist/index.css';
|
|
||||||
import 'allotment/dist/style.css';
|
import 'allotment/dist/style.css';
|
||||||
import 'pickadate/lib/themes/classic.css';
|
import 'pickadate/lib/themes/classic.css';
|
||||||
import 'pickadate/lib/themes/classic.date.css';
|
import 'pickadate/lib/themes/classic.date.css';
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export function secureRandomString(digit = 32, dict = defaultDict) {
|
|||||||
|
|
||||||
type Substitution = string | number | { templateRaw: true, html: string };
|
type Substitution = string | number | { templateRaw: true, html: string };
|
||||||
|
|
||||||
export function tpl(node: React.ReactNode, reactive?: boolean);
|
export function tpl<T extends boolean = false>(node: React.ReactNode, reactive?: T): T extends true ? HTMLDivElement : string;
|
||||||
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]);
|
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]): string;
|
||||||
export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitutions: Substitution[] | boolean[]) {
|
export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitutions: Substitution[] | boolean[]) {
|
||||||
if (React.isValidElement(pieces)) {
|
if (React.isValidElement(pieces)) {
|
||||||
if (substitutions[0]) {
|
if (substitutions[0]) {
|
||||||
@@ -49,7 +49,7 @@ export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitut
|
|||||||
for (let i = 0; i < substitutions.length; ++i) {
|
for (let i = 0; i < substitutions.length; ++i) {
|
||||||
const subst = substitutions[i];
|
const subst = substitutions[i];
|
||||||
let substHtml: string;
|
let substHtml: string;
|
||||||
if (typeof subst === 'object' && subst.templateRaw) {
|
if (subst && typeof subst === 'object' && subst.templateRaw) {
|
||||||
substHtml = subst.html;
|
substHtml = subst.html;
|
||||||
} else substHtml = _.escape(String(subst));
|
} else substHtml = _.escape(String(subst));
|
||||||
result += substHtml + pieces[i + 1];
|
result += substHtml + pieces[i + 1];
|
||||||
@@ -82,7 +82,8 @@ export const zIndexManager = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const request = {
|
export const request = {
|
||||||
async ajax(options: Record<string, any>) {
|
ajax(options: Record<string, any>) {
|
||||||
|
const stack = new Error().stack;
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
$
|
$
|
||||||
.ajax({
|
.ajax({
|
||||||
@@ -96,9 +97,12 @@ export const request = {
|
|||||||
if (textStatus === 'abort') {
|
if (textStatus === 'abort') {
|
||||||
const err = new Error(i18n('Aborted')) as any;
|
const err = new Error(i18n('Aborted')) as any;
|
||||||
err.aborted = true;
|
err.aborted = true;
|
||||||
|
err.isUserFacingError = true;
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (jqXHR.readyState === 0) {
|
} else if (jqXHR.readyState === 0) {
|
||||||
reject(new Error(i18n('Network error')));
|
const err = new Error(i18n('Network error')) as any;
|
||||||
|
err.isUserFacingError = true;
|
||||||
|
reject(err);
|
||||||
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
|
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
|
||||||
const { error } = jqXHR.responseJSON;
|
const { error } = jqXHR.responseJSON;
|
||||||
if (error.params) {
|
if (error.params) {
|
||||||
@@ -117,6 +121,9 @@ export const request = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.done(resolve);
|
.done(resolve);
|
||||||
|
}).catch((e: Error) => {
|
||||||
|
e.stack = stack;
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { openDB as _open } from 'idb';
|
||||||
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
|
import { tpl } from 'vj/utils';
|
||||||
|
|
||||||
|
export const openDB = _open('hydro', 1, {
|
||||||
|
upgrade(db, oldVersion) {
|
||||||
|
if (oldVersion < 1) {
|
||||||
|
const solutionStore = db.createObjectStore('solutions', { keyPath: 'id' });
|
||||||
|
db.createObjectStore('scoreboard-star', { keyPath: 'id' });
|
||||||
|
const keys = Object.keys(localStorage);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key.endsWith('#objective')) {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value) solutionStore.put({ id: key, value: JSON.parse(value) });
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
if (key.startsWith('scoreboard-star/')) localStorage.removeItem(key);
|
||||||
|
if (/^0\.\d+$/.test(key)) localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blocked(currentVersion, blockedVersion) {
|
||||||
|
console.error('IDB Blocked by version', blockedVersion, 'want', currentVersion);
|
||||||
|
new InfoDialog({
|
||||||
|
$body: tpl.typoMsg('Some other opened tabs locked the database. Please close them.'),
|
||||||
|
}).open();
|
||||||
|
},
|
||||||
|
blocking(currentVersion, blockedVersion) {
|
||||||
|
console.error('IDB Blocking version', blockedVersion);
|
||||||
|
new InfoDialog({
|
||||||
|
$body: tpl.typoMsg('Please close or refresh this tab to perform the upgrade.'),
|
||||||
|
}).open();
|
||||||
|
},
|
||||||
|
terminated() {
|
||||||
|
console.error('IDB Terminated');
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -22,11 +22,14 @@ declare global {
|
|||||||
|
|
||||||
const defaultDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
const defaultDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
||||||
|
|
||||||
String.random = function random(digit = 32, dict = defaultDict) {
|
export function randomstring(digit = 32, dict = defaultDict) {
|
||||||
let str = '';
|
let str = '';
|
||||||
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
|
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
|
||||||
return str;
|
return str;
|
||||||
};
|
}
|
||||||
|
try {
|
||||||
|
String.random = randomstring;
|
||||||
|
} catch (e) { } // Cannot add property random, object is not extensible
|
||||||
|
|
||||||
String.prototype.format ||= function formatStr(...args) {
|
String.prototype.format ||= function formatStr(...args) {
|
||||||
let result = this;
|
let result = this;
|
||||||
@@ -236,7 +239,7 @@ export function sortFiles(files: Record<string, any>[] | string[], key = '_id')
|
|||||||
|
|
||||||
interface MatchRule {
|
interface MatchRule {
|
||||||
regex: RegExp;
|
regex: RegExp;
|
||||||
output: ((a: RegExpExecArray) => string)[];
|
output: (a: RegExpExecArray) => string[];
|
||||||
id: (a: RegExpExecArray) => number;
|
id: (a: RegExpExecArray) => number;
|
||||||
subtask: (a: RegExpExecArray) => number;
|
subtask: (a: RegExpExecArray) => number;
|
||||||
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
|
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
|
||||||
@@ -244,47 +247,35 @@ interface MatchRule {
|
|||||||
|
|
||||||
const SubtaskMatcher: MatchRule[] = [
|
const SubtaskMatcher: MatchRule[] = [
|
||||||
{
|
{
|
||||||
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|txt|in\.txt)$/,
|
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/,
|
||||||
output: [
|
output: (a) => ['out', 'ans']
|
||||||
(a) => `${a[1]}.out`,
|
.flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`])
|
||||||
(a) => `${a[1]}.out.txt`,
|
.flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')])
|
||||||
(a) => `${a[1]}.ans`,
|
.concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
|
||||||
(a) => `${a[1]}.ans.txt`,
|
|
||||||
(a) => `${a[1]}.out`.replace(/input/g, 'output'),
|
|
||||||
(a) => (a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
|
|
||||||
],
|
|
||||||
id: (a) => +a[4],
|
id: (a) => +a[4],
|
||||||
subtask: (a) => +(a[3] || 1),
|
subtask: (a) => +(a[3] || 1),
|
||||||
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
|
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
regex: /^([^\d]*)\.in(\d+)$/,
|
regex: /^([^\d]*)\.(in|IN)(\d+)$/,
|
||||||
output: [
|
output: (a) => [
|
||||||
(a) => `${a[1]}.ou${a[2]}`,
|
`${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`,
|
||||||
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
`${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`,
|
||||||
(a) => `${a[1]}.out${a[2]}`,
|
].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]),
|
||||||
(a) => `${a[1]}.out${a[2]}`.replace(/input/g, 'output'),
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
id: (a) => +a[2],
|
||||||
subtask: () => 1,
|
subtask: () => 1,
|
||||||
preferredScorerType: () => 'sum',
|
preferredScorerType: () => 'sum',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.in$/,
|
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/,
|
||||||
output: [
|
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`),
|
||||||
(a) => `${a[1]}${a[2]}${a[3]}${a[4]}.out`,
|
|
||||||
(a) => `${a[1]}${a[2]}${a[3]}${a[4]}.ans`,
|
|
||||||
],
|
|
||||||
id: (a) => +a[4],
|
id: (a) => +a[4],
|
||||||
subtask: (a) => +a[2],
|
subtask: (a) => +a[2],
|
||||||
preferredScorerType: () => 'min',
|
preferredScorerType: () => 'min',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
regex: /^(([0-9]+)[-_](?:.*))\.in$/,
|
regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/,
|
||||||
output: [
|
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`),
|
||||||
(a) => `${a[1]}.out`,
|
|
||||||
(a) => `${a[1]}.ans`,
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
id: (a) => +a[2],
|
||||||
subtask: () => 1,
|
subtask: () => 1,
|
||||||
preferredScorerType: () => 'sum',
|
preferredScorerType: () => 'sum',
|
||||||
@@ -329,12 +320,12 @@ export function readSubtasksFromFiles(files: string[], config) {
|
|||||||
const sid = rule.subtask(data);
|
const sid = rule.subtask(data);
|
||||||
const c = { input: file, output: '', id: rule.id(data) };
|
const c = { input: file, output: '', id: rule.id(data) };
|
||||||
const type = rule.preferredScorerType(data);
|
const type = rule.preferredScorerType(data);
|
||||||
for (const func of rule.output) {
|
const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i);
|
||||||
if (config.noOutputFile) c.output = '/dev/null';
|
for (const output of outputs) {
|
||||||
else c.output = func(data);
|
if (output === file) continue;
|
||||||
if (c.output === file) continue;
|
if (output === '/dev/null' || files.includes(output)) {
|
||||||
if (c.output === '/dev/null' || files.includes(c.output)) {
|
|
||||||
match = true;
|
match = true;
|
||||||
|
c.output = output;
|
||||||
if (!subtask[sid]) {
|
if (!subtask[sid]) {
|
||||||
subtask[sid] = {
|
subtask[sid] = {
|
||||||
time: config.time,
|
time: config.time,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface LangConfig {
|
|||||||
comment?: string | [string, string];
|
comment?: string | [string, string];
|
||||||
compile_time_limit?: number;
|
compile_time_limit?: number;
|
||||||
compile_memory_limit?: number;
|
compile_memory_limit?: number;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseLang(config: string): Record<string, LangConfig> {
|
export function parseLang(config: string): Record<string, LangConfig> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/utils",
|
"name": "@hydrooj/utils",
|
||||||
"version": "1.4.24",
|
"version": "1.4.25",
|
||||||
"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",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"mongodb": "^5.9.2",
|
"mongodb": "^5.9.2",
|
||||||
"reggol": "^1.6.3",
|
"reggol": "^1.6.3",
|
||||||
"search-query-parser": "^1.6.0",
|
"search-query-parser": "^1.6.0",
|
||||||
"systeminformation": "^5.22.10"
|
"systeminformation": "^5.22.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^11.0.4"
|
"@types/fs-extra": "^11.0.4"
|
||||||
|
|||||||
Reference in new issue
Block a user