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
+1343
-665
No files matched your search
@@ -67,6 +67,8 @@ jobs:
|
||||
else
|
||||
parallel --tty -j+0 yarn ::: lint:ci build test
|
||||
fi
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
- name: Publish
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: node -r @hydrooj/register build/publish.ts
|
||||
|
||||
@@ -111,6 +111,34 @@ Hydro 用户群:1085853538
|
||||
<details>
|
||||
<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
|
||||
- core: 将 API 模块移入 service
|
||||
- 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 许可证和下述附加条款章节的前提下免费使用这些代码:
|
||||
|
||||
+6
-21
@@ -72,7 +72,7 @@ for (const name of ['plugins', 'modules']) {
|
||||
|
||||
const modules = [
|
||||
'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());
|
||||
|
||||
const UIConfig = {
|
||||
@@ -80,22 +80,17 @@ const UIConfig = {
|
||||
'packages/ui-default/public',
|
||||
'**/node_modules',
|
||||
],
|
||||
include: ['ts', 'tsx']
|
||||
.flatMap((ext) => ['plugins', 'modules']
|
||||
include: ['ts', 'tsx', 'vue', 'json']
|
||||
.flatMap((ext) => ['plugins']
|
||||
.flatMap((name) => [`${name}/**/public/**/*.${ext}`, `${name}/**/frontend/**/*.${ext}`])
|
||||
.concat(`packages/ui-default/**/*.${ext}`)),
|
||||
compilerOptions: {
|
||||
experimentalDecorators: true,
|
||||
esModuleInterop: true,
|
||||
resolveJsonModule: true,
|
||||
jsx: 'react',
|
||||
module: 'commonjs',
|
||||
...compilerOptionsBase,
|
||||
module: 'ESNext',
|
||||
skipLibCheck: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
target: 'es2020',
|
||||
baseUrl: '.',
|
||||
outDir: path.join(baseOutDir, 'ui'),
|
||||
moduleResolution: 'node',
|
||||
paths: {
|
||||
'vj/*': [
|
||||
'./packages/ui-default/*',
|
||||
@@ -120,17 +115,7 @@ const pluginsConfig = {
|
||||
],
|
||||
exclude,
|
||||
compilerOptions: {
|
||||
target: 'es2020',
|
||||
module: 'commonjs',
|
||||
esModuleInterop: true,
|
||||
moduleResolution: 'node',
|
||||
jsx: 'react',
|
||||
sourceMap: false,
|
||||
composite: true,
|
||||
strictBindCallApply: true,
|
||||
resolveJsonModule: true,
|
||||
experimentalDecorators: true,
|
||||
incremental: true,
|
||||
...compilerOptionsBase,
|
||||
rootDir: '.',
|
||||
baseUrl: '.',
|
||||
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",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"license": "MIT",
|
||||
"main": "react.yaml",
|
||||
"dependencies": {
|
||||
"@stylistic/eslint-plugin": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@stylistic/eslint-plugin": "^2.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"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-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.0"
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">= 8"
|
||||
File renamed without changes.
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@hydrooj/framework",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"main": "index.ts",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"cordis": "*"
|
||||
},
|
||||
@@ -15,18 +16,18 @@
|
||||
"koa-router": "^12.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"path-to-regexp": "^6.2.2",
|
||||
"path-to-regexp": "^7.0.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"saslprep": "^1.0.3",
|
||||
"shorty.js": "^1.0.1",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-compress": "^4.0.6",
|
||||
"@types/koa-router": "^7.4.8",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/saslprep": "^1.0.3",
|
||||
"@types/ws": "^8.5.10"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Koa from 'koa';
|
||||
import Body from 'koa-body';
|
||||
import Compress from 'koa-compress';
|
||||
import { Shorty } from 'shorty.js';
|
||||
import WebSocket from 'ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import {
|
||||
Counter, errorMessage, isClass, Logger, parseMemoryMB,
|
||||
} from '@hydrooj/utils/lib/utils';
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import { Router } from './router';
|
||||
import serializer from './serializer';
|
||||
|
||||
export { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||
export function encodeRFC5987ValueChars(str: string) {
|
||||
return (
|
||||
@@ -91,7 +93,7 @@ export const koa = new Koa<Koa.DefaultState, KoaContext>({
|
||||
});
|
||||
export const router = new Router();
|
||||
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) => {
|
||||
if (error.code !== 'EPIPE' && error.code !== 'ECONNRESET' && !error.message.includes('Parse Error')) {
|
||||
logger.error('Koa app-level error', { error });
|
||||
@@ -322,6 +324,7 @@ export class WebService extends Service {
|
||||
HandlerCommon = HandlerCommon;
|
||||
Handler = Handler;
|
||||
ConnectionHandler = ConnectionHandler;
|
||||
handlerCtxBase = this.ctx;
|
||||
|
||||
constructor(ctx: Context, public config: WebServiceConfig) {
|
||||
super(ctx, 'server', true);
|
||||
@@ -420,6 +423,9 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
||||
}
|
||||
socket.close();
|
||||
});
|
||||
this.ctx.inject(['server'], (c) => {
|
||||
this.handlerCtxBase = c;
|
||||
});
|
||||
}
|
||||
|
||||
async listen() {
|
||||
@@ -438,7 +444,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
||||
private async handleHttp(ctx: KoaContext, HandlerClass, checker) {
|
||||
const { args } = ctx.HydroContext;
|
||||
Object.assign(args, ctx.params);
|
||||
const h = new HandlerClass(ctx, this.ctx);
|
||||
const h = new HandlerClass(ctx, this.handlerCtxBase);
|
||||
ctx.handler = h;
|
||||
const method = ctx.method.toLowerCase();
|
||||
try {
|
||||
@@ -511,7 +517,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
||||
|
||||
private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer) {
|
||||
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);
|
||||
ctx.handler = h;
|
||||
h.conn = conn;
|
||||
|
||||
@@ -50,6 +50,12 @@ require.extensions['.js'] = function loader(module, filename) {
|
||||
}
|
||||
try {
|
||||
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);
|
||||
} catch (e) { // ESM
|
||||
return module._compile(transform(filename), filename);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/register",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
+4
-4
@@ -49,14 +49,14 @@
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.8",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globby": "^14.0.1",
|
||||
"globby": "^14.0.2",
|
||||
"inspectpack": "^4.7.1",
|
||||
"mongodb-memory-server": "^9.3.0",
|
||||
"nyc": "^15.1.0",
|
||||
"mongodb-memory-server": "^9.4.0",
|
||||
"nyc": "^17.0.0",
|
||||
"ora": "^8.0.1",
|
||||
"package-json": "^10.0.0",
|
||||
"semver": "^7.6.2",
|
||||
"simple-git": "^3.24.0",
|
||||
"simple-git": "^3.25.0",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "5.3.3",
|
||||
"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) {
|
||||
// Auto authorize user 2 as super admin
|
||||
ctx.on('handler/after/UserRegisterWithCode#post', async (that) => {
|
||||
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",
|
||||
"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",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.5",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.13.1"
|
||||
"@elastic/elasticsearch": "^8.14.0"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "3.3.13",
|
||||
"version": "3.3.15",
|
||||
"main": "./src/index.ts",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
"dependencies": {
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"adm-zip": "0.5.5",
|
||||
"adm-zip": "0.5.14",
|
||||
"cac": "^6.7.14",
|
||||
"p-queue": "^8.0.1",
|
||||
"schemastery": "^3.14.6",
|
||||
"semver": "^7.6.2",
|
||||
"shell-quote": "^1.8.1",
|
||||
"superagent": "^9.0.2",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"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 */
|
||||
import path from 'path';
|
||||
import { fs } from '@hydrooj/utils';
|
||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
import {
|
||||
JudgeHandler, JudgeResultBody, ObjectId, RecordModel, SettingModel,
|
||||
StorageModel, SystemModel, TaskModel,
|
||||
db, JudgeHandler, JudgeResultBody, ObjectId, RecordModel,
|
||||
SettingModel, StorageModel, SystemModel, TaskModel,
|
||||
} from 'hydrooj';
|
||||
import { langs } from 'hydrooj/src/model/setting';
|
||||
import { compilerVersions } from '../compiler';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, SystemError } from '../error';
|
||||
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));
|
||||
});
|
||||
await fs.ensureDir(getConfig('tmp_dir'));
|
||||
const info = await sysinfo.get();
|
||||
const handle = async (t) => {
|
||||
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
||||
if (!rdoc) {
|
||||
@@ -92,9 +96,19 @@ export async function postInit(ctx) {
|
||||
};
|
||||
const parallelism = Math.max(getConfig('parallelism'), 2);
|
||||
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', () => {
|
||||
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
|
||||
collectCompilerInfo();
|
||||
});
|
||||
collectCompilerInfo();
|
||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||
TaskModel.consume({ type: 'generate' }, handle);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { fs, pipeRequest } from '@hydrooj/utils';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
import type { JudgeResultBody } from 'hydrooj';
|
||||
import { compilerVersions } from '../compiler';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, SystemError } from '../error';
|
||||
import { Session } from '../interface';
|
||||
@@ -173,13 +174,21 @@ export default class Hydro implements Session {
|
||||
const content = Object.keys(config).length
|
||||
? JSON.stringify({ key: 'config', ...config })
|
||||
: '{"key":"ping"}';
|
||||
let compilers = {};
|
||||
let compilerVersionCallback = () => { };
|
||||
this.ws.on('message', (data) => {
|
||||
if (data.toString() === 'ping') {
|
||||
this.ws.send('pong');
|
||||
return;
|
||||
}
|
||||
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)));
|
||||
});
|
||||
this.ws.on('close', (data, reason) => {
|
||||
@@ -196,10 +205,13 @@ export default class Hydro implements Session {
|
||||
this.ws.send('{"key":"start"}');
|
||||
if (!this.config.noStatus) {
|
||||
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 () => {
|
||||
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);
|
||||
}
|
||||
resolve(null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "4.12.3",
|
||||
"version": "4.13.1",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/plugin-api",
|
||||
"module": "src/plugin-api",
|
||||
@@ -12,38 +12,37 @@
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.591.0",
|
||||
"@aws-sdk/lib-storage": "3.591.0",
|
||||
"@aws-sdk/s3-presigned-post": "3.591.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.591.0",
|
||||
"@aws-sdk/client-s3": "3.609.0",
|
||||
"@aws-sdk/lib-storage": "3.609.0",
|
||||
"@aws-sdk/s3-presigned-post": "3.609.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.609.0",
|
||||
"@graphql-tools/schema": "^10.0.4",
|
||||
"@hydrooj/framework": "workspace:^",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@simplewebauthn/server": "7.4.0",
|
||||
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
||||
"adm-zip": "0.5.5",
|
||||
"adm-zip": "0.5.14",
|
||||
"cac": "^6.7.14",
|
||||
"cordis": "3.16.0",
|
||||
"detect-browser": "^5.3.0",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"emojis-list": "2.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"koa-proxies": "^0.12.4",
|
||||
"koa-static-cache": "^5.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^10.2.2",
|
||||
"lru-cache": "^10.3.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"mongodb": "^5.9.2",
|
||||
"mongodb-uri": "^0.9.7",
|
||||
"nanoid": "^5.0.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"nodemailer": "^6.9.14",
|
||||
"notp": "^2.0.3",
|
||||
"p-queue": "^8.0.1",
|
||||
"path-to-regexp": "^6.2.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"saslprep": "^1.0.3",
|
||||
"schemastery": "^3.14.6",
|
||||
@@ -51,15 +50,14 @@
|
||||
"shorty.js": "^1.0.1",
|
||||
"superagent": "^9.0.2",
|
||||
"tar": "6.2.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"ws": "^8.17.0"
|
||||
"thirty-two": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/koa-static-cache": "^5.1.4",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mongodb-uri": "^0.9.4",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
|
||||
@@ -49,11 +49,13 @@ langs:
|
||||
code_file: foo.sh
|
||||
execute: /bin/bash foo.sh
|
||||
highlight: bash
|
||||
version: /bin/bash --version
|
||||
c:
|
||||
compile: /usr/bin/gcc -Wall --std=c99 -o foo foo.c -lm
|
||||
highlight: c
|
||||
monaco: c
|
||||
display: C
|
||||
version: /usr/bin/gcc --version
|
||||
cc:
|
||||
compile: /usr/bin/g++ -Wall -std=c++14 -o foo foo.cc -lm -I/include
|
||||
code_file: foo.cc
|
||||
@@ -62,6 +64,7 @@ langs:
|
||||
display: C++
|
||||
address_space_limit: true
|
||||
process_limit: 1
|
||||
version: /usr/bin/g++ --version
|
||||
cc.cc98:
|
||||
compile: /usr/bin/g++ -Wall -std=c++98 -o foo foo.cc -lm -I/include
|
||||
display: C++98
|
||||
@@ -95,6 +98,7 @@ langs:
|
||||
compile: /usr/bin/fpc -O2 -o/w/foo foo.pas
|
||||
highlight: pascal
|
||||
display: Pascal
|
||||
version: /usr/bin/fpc -iW
|
||||
java:
|
||||
compile: /usr/bin/bash -c "javac -d /w -encoding utf8 ./Main.java && jar cvf Main.jar *.class >/dev/null"
|
||||
code_file: Main.java
|
||||
@@ -105,6 +109,7 @@ langs:
|
||||
monaco: java
|
||||
display: Java
|
||||
compile_time_limit: 30000
|
||||
version: /usr/bin/javac -version
|
||||
kt:
|
||||
compile: echo "Invalid language selection"
|
||||
code_file: foo.kt
|
||||
@@ -119,6 +124,7 @@ langs:
|
||||
execute: /usr/bin/java -jar foo.jar
|
||||
display: Kotlin/JVM
|
||||
compile_time_limit: 30000
|
||||
version: /usr/bin/kotlinc -version
|
||||
py:
|
||||
compile: /usr/bin/python -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
|
||||
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)"
|
||||
execute: /usr/bin/python3 foo
|
||||
display: Python 3
|
||||
version: /usr/bin/python3 --version
|
||||
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"
|
||||
execute: /bin/bash -c 'mv foo foo.py && /usr/bin/pypy3 -B foo.py'
|
||||
display: PyPy3
|
||||
version: /usr/bin/pypy3 --version
|
||||
php:
|
||||
execute: /usr/bin/php foo.php
|
||||
display: PHP
|
||||
version: /usr/bin/php -v
|
||||
rs:
|
||||
compile: /usr/bin/rustc -O -o /w/foo /w/foo.rs
|
||||
highlight: rust
|
||||
display: Rust
|
||||
version: /usr/bin/rustc --version
|
||||
hs:
|
||||
compile: /usr/bin/ghc -O -outputdir /tmp -o foo foo.hs
|
||||
highlight: haskell
|
||||
display: Haskell
|
||||
version: /usr/bin/ghc --version
|
||||
js:
|
||||
execute: /usr/bin/node /w/foo.js
|
||||
highlight: javascript
|
||||
display: NodeJS
|
||||
version: /usr/bin/node --version
|
||||
go:
|
||||
compile: /usr/bin/go build -o foo foo.go
|
||||
display: Golang
|
||||
version: /usr/bin/go version
|
||||
rb:
|
||||
execute: /usr/bin/ruby foo.rb
|
||||
highlight: ruby
|
||||
display: Ruby
|
||||
version: /usr/bin/ruby --version
|
||||
cs:
|
||||
compile: /usr/bin/mcs -optimize+ -out:/w/foo /w/foo.cs
|
||||
execute: /usr/bin/mono foo
|
||||
highlight: csharp
|
||||
monaco: csharp
|
||||
display: 'C#'
|
||||
version: /usr/bin/mcs --version
|
||||
r:
|
||||
display: 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 path from 'path';
|
||||
import cac from 'cac';
|
||||
import fs from 'fs-extra';
|
||||
import type { Context } from 'koa';
|
||||
import { MongoClient, WriteConcern } from 'mongodb';
|
||||
import mongoUri from 'mongodb-uri';
|
||||
import { Context as PluginContext } from '../context';
|
||||
import { readline } from '../ui';
|
||||
|
||||
const listenPort = cac().parse().options.port || 8888;
|
||||
let resolve;
|
||||
|
||||
async function get(ctx: Context) {
|
||||
ctx.body = `<!DOCTYPE html>
|
||||
<html data-page="setup" data-layout="immersive" class="layout--immersive page--setup nojs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="stylesheet" media="all" href="/default.theme.css">
|
||||
<title>Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slideout-panel" id="panel">
|
||||
<div class="main">
|
||||
<div class="row"><div class="columns">
|
||||
<div class="immersive--content immersive--center">
|
||||
<h1>Setup</h1>
|
||||
<form method="POST">
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
MongoDB Connection URI
|
||||
<input name="url" type="text" value="mongodb://user:pass@127.0.0.1:27017/hydro" autofocus>
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<div class="text-center">
|
||||
<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';
|
||||
export async function load() {
|
||||
console.error('Config file not found.');
|
||||
if (!process.stdin.isTTY) {
|
||||
console.error('Please fill in ~/.hydro/config.json with MongoDB connection string in the following format:');
|
||||
console.error('{"url":"mongodb://localhost:27017/hydro"}');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function post(ctx: Context) {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
console.error('Please type your MongoDB connection string below:');
|
||||
console.error('Example: mongodb://localhost:27017/hydro');
|
||||
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, {
|
||||
const uri = await readline();
|
||||
if (!uri) throw new Error('no url found');
|
||||
const url = mongoUri.parse(uri);
|
||||
const Database = await MongoClient.connect(uri, {
|
||||
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 },
|
||||
),
|
||||
]);
|
||||
await coll.updateOne({ _id: '_test' }, { $set: { value: 1 } }, { upsert: true });
|
||||
await coll.deleteOne({ _id: '_test' });
|
||||
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?.();
|
||||
fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({ url: uri }));
|
||||
break;
|
||||
} catch (e) {
|
||||
ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (!process.env.CI && !config) {
|
||||
logger.info('Starting setup');
|
||||
await require('./setup').load();
|
||||
await require('./setup').load(ctx);
|
||||
}
|
||||
const pending = global.addons;
|
||||
const fail = [];
|
||||
@@ -65,10 +65,10 @@ export async function apply(ctx: Context) {
|
||||
for (const h of handlers.filter((i) => i.endsWith('.ts'))) {
|
||||
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 addon(pending, fail, ctx);
|
||||
await ctx.lifecycle.flush();
|
||||
for (const i in global.Hydro.handler) await global.Hydro.handler[i]();
|
||||
const scriptDir = path.resolve(__dirname, '..', 'script');
|
||||
for (const h of await fs.readdir(scriptDir)) {
|
||||
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.parallel('app/started');
|
||||
if (process.env.NODE_APP_INSTANCE === '0') {
|
||||
const { default: scripts, init } = require('../upgrade');
|
||||
let dbVer = (await modelSystem.get('db.ver')) ?? 0;
|
||||
const isFresh = !dbVer;
|
||||
const expected = scripts.length;
|
||||
if (isFresh) {
|
||||
await init();
|
||||
await modelSystem.set('db.ver', expected);
|
||||
} else {
|
||||
while (dbVer < expected) {
|
||||
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);
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.inject(['migration'], async (c) => {
|
||||
c.migration.registerChannel('hydrooj', require('../upgrade').coreScripts);
|
||||
try {
|
||||
await c.migration.doUpgrade();
|
||||
resolve(null);
|
||||
} catch (e) {
|
||||
logger.error('Upgrade failed: %O', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const f of global.addons) {
|
||||
const dir = path.join(f, 'public');
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '../error';
|
||||
import type { DomainDoc } from '../interface';
|
||||
import avatar from '../lib/avatar';
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
|
||||
import * as discussion from '../model/discussion';
|
||||
import domain from '../model/domain';
|
||||
@@ -24,10 +23,10 @@ import { log2 } from '../utils';
|
||||
class DomainRankHandler extends Handler {
|
||||
@query('page', Types.PositiveInt, true)
|
||||
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 }),
|
||||
page,
|
||||
100,
|
||||
'ranking',
|
||||
);
|
||||
const udict = await user.getList(domainId, dudocs.map((dudoc) => dudoc.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 DomainModel from '../model/domain';
|
||||
import RecordModel from '../model/record';
|
||||
import * as SettingModel from '../model/setting';
|
||||
import UserModel from '../model/user';
|
||||
import db from '../service/db';
|
||||
import { Handler } from '../service/server';
|
||||
@@ -25,11 +26,47 @@ async function getStatus() {
|
||||
class StatusHandler extends Handler {
|
||||
async get() {
|
||||
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) {
|
||||
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' : ''}`;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AttestationFormat } from '@simplewebauthn/server/dist/helpers/decodeAttestationObject';
|
||||
import { AuthenticationExtensionsAuthenticatorOutputs } from '@simplewebauthn/server/dist/helpers/decodeAuthenticatorExtensions';
|
||||
import type { AttestationFormat } from '@simplewebauthn/server/dist/helpers/decodeAttestationObject';
|
||||
import type { AuthenticationExtensionsAuthenticatorOutputs } from '@simplewebauthn/server/dist/helpers/decodeAuthenticatorExtensions';
|
||||
import { CredentialDeviceType } from '@simplewebauthn/typescript-types';
|
||||
import type fs from 'fs';
|
||||
import type { Dictionary, NumericDictionary } from 'lodash';
|
||||
@@ -644,6 +644,8 @@ export interface FileNode {
|
||||
size?: number;
|
||||
/** AutoDelete */
|
||||
autoDelete?: Date;
|
||||
/** fileId if linked to an existing file */
|
||||
link?: string;
|
||||
owner?: number;
|
||||
operator?: 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 interface Lib extends Record<string, any> {
|
||||
difficulty: typeof import('./lib/difficulty');
|
||||
difficulty: typeof import('./lib/difficulty').default;
|
||||
buildContent: typeof import('./lib/content').buildContent;
|
||||
mail: typeof import('./lib/mail');
|
||||
rank: typeof import('./lib/rank');
|
||||
rating: typeof import('./lib/rating');
|
||||
rating: typeof import('./lib/rating').default;
|
||||
testdataConfig: typeof import('./lib/testdataConfig');
|
||||
problemSearch: ProblemSearch;
|
||||
}
|
||||
@@ -807,8 +808,6 @@ export interface ModuleInterfaces {
|
||||
export interface HydroGlobal {
|
||||
version: Record<string, string>;
|
||||
model: Model;
|
||||
/** @deprecated */
|
||||
handler: Record<string, Function>;
|
||||
script: Record<string, Script>;
|
||||
service: HydroService;
|
||||
lib: Lib;
|
||||
|
||||
@@ -42,5 +42,5 @@ function difficultyAlgorithm(nSubmit: number, nAccept: number) {
|
||||
return Math.max(ans, 1);
|
||||
}
|
||||
|
||||
export = difficultyAlgorithm;
|
||||
export default 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;
|
||||
@@ -1,7 +1,6 @@
|
||||
import './i18n';
|
||||
import './mail';
|
||||
import './hash.hydro';
|
||||
import './rank';
|
||||
import './ui';
|
||||
import './testdataConfig';
|
||||
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[];
|
||||
}
|
||||
|
||||
export = calculate;
|
||||
export default calculate;
|
||||
|
||||
global.Hydro.lib.rating = calculate;
|
||||
@@ -5,7 +5,7 @@ import Schema from 'schemastery';
|
||||
import superagent from 'superagent';
|
||||
import { Context } from './context';
|
||||
export { ObjectId, Filter } from 'mongodb';
|
||||
export { WebSocket, WebSocketServer } from 'ws';
|
||||
export { WebSocket, WebSocketServer } from '@hydrooj/framework';
|
||||
export * from './utils';
|
||||
export * from './interface';
|
||||
export * from './typeutils';
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
||||
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
||||
} from '../interface';
|
||||
import ranked from '../lib/rank';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import type { Handler } from '../service/server';
|
||||
@@ -224,7 +223,7 @@ const acm = buildContestRule({
|
||||
return row;
|
||||
},
|
||||
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 udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||
// Find first accept
|
||||
@@ -257,7 +256,7 @@ const acm = buildContestRule({
|
||||
return [rows, udict];
|
||||
},
|
||||
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) {
|
||||
if (isDone(tdoc)) return rdoc;
|
||||
@@ -394,7 +393,7 @@ const oi = buildContestRule({
|
||||
return row;
|
||||
},
|
||||
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 udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||
const psdict = {};
|
||||
@@ -431,7 +430,7 @@ const oi = buildContestRule({
|
||||
return [rows, udict];
|
||||
},
|
||||
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) {
|
||||
if (isDone(tdoc)) return rdoc;
|
||||
@@ -744,7 +743,7 @@ const homework = buildContestRule({
|
||||
return row;
|
||||
},
|
||||
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 udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName);
|
||||
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
|
||||
@@ -757,7 +756,7 @@ const homework = buildContestRule({
|
||||
return [rows, udict];
|
||||
},
|
||||
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[] = [
|
||||
...ProblemModel.PROJECTION_CONTEST_LIST,
|
||||
'content', 'html', 'data', 'config', 'additional_file',
|
||||
'reference',
|
||||
'reference', 'maintainer',
|
||||
];
|
||||
|
||||
static PROJECTION_PUBLIC: Field[] = [
|
||||
...ProblemModel.PROJECTION_LIST,
|
||||
'content', 'html', 'data', 'config', 'additional_file',
|
||||
'reference',
|
||||
'reference', 'maintainer',
|
||||
];
|
||||
|
||||
static default = {
|
||||
@@ -262,7 +262,8 @@ export class ProblemModel {
|
||||
const res = await Promise.all([
|
||||
document.deleteOne(domainId, document.TYPE_PROBLEM, docId),
|
||||
document.deleteMultiStatus(domainId, document.TYPE_PROBLEM, { docId }),
|
||||
storage.list(`problem/${domainId}/${docId}/`).then((items) => storage.del(items.map((item) => item.prefix + item.name))),
|
||||
storage.list(`problem/${domainId}/${docId}/`)
|
||||
.then((items) => storage.del(items.map((item) => `problem/${domainId}/${docId}/${item.name}`))),
|
||||
bus.parallel('problem/delete', domainId, docId),
|
||||
]);
|
||||
await bus.emit('problem/del', domainId, docId);
|
||||
@@ -363,7 +364,7 @@ export class ProblemModel {
|
||||
domainId: string, pids: number[], canViewHidden: number | boolean = false,
|
||||
doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, indexByDocIdOnly = false,
|
||||
): Promise<ProblemDict> {
|
||||
if (!pids?.length) return [];
|
||||
if (!pids?.length) return {};
|
||||
const r: Record<number, ProblemDoc> = {};
|
||||
const l: Record<string, ProblemDoc> = {};
|
||||
const q: any = { docId: { $in: pids } };
|
||||
|
||||
@@ -280,22 +280,23 @@ export function apply(ctx: Context) {
|
||||
}, { upsert: true });
|
||||
}
|
||||
});
|
||||
ctx.on('ready', async () => {
|
||||
await db.ensureIndexes(
|
||||
ctx.on('ready', () => Promise.all([
|
||||
db.ensureIndexes(
|
||||
RecordModel.coll,
|
||||
{ key: { domainId: 1, pid: 1 }, name: 'delete' },
|
||||
{ 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, pid: 1, _id: -1 }, name: 'withProblem' },
|
||||
{ key: { domainId: 1, contest: 1, pid: 1, uid: 1, _id: -1 }, name: 'withUserAndProblem' },
|
||||
{ key: { domainId: 1, contest: 1, status: 1, _id: -1 }, name: 'withStatus' },
|
||||
);
|
||||
await db.ensureIndexes(
|
||||
),
|
||||
db.ensureIndexes(
|
||||
RecordModel.collStat,
|
||||
{ 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, memory: 1 }, name: 'memory' },
|
||||
{ key: { domainId: 1, pid: 1, uid: 1, length: 1 }, name: 'length' },
|
||||
);
|
||||
});
|
||||
),
|
||||
]) as any);
|
||||
}
|
||||
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.discussion', 50, 'number', 'pagination.discussion', 'Discussions 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.training', 10, 'number', 'pagination.training', 'Trainings per page'),
|
||||
Setting('setting_basic', 'pagination.reply', 50, 'number', 'pagination.reply', 'Replies per page'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { extname } from 'path';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { escapeRegExp, omit } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Readable } from 'stream';
|
||||
@@ -39,7 +39,7 @@ export class StorageModel {
|
||||
{ $set: { lastUsage: new Date() } },
|
||||
{ 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) {
|
||||
@@ -51,6 +51,20 @@ export class StorageModel {
|
||||
|
||||
static async del(path: string[], operator = 1) {
|
||||
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();
|
||||
await StorageModel.coll.updateMany(
|
||||
{ path: { $in: path }, autoDelete: null },
|
||||
@@ -66,7 +80,7 @@ export class StorageModel {
|
||||
autoDelete: null,
|
||||
}).toArray();
|
||||
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
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
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({
|
||||
_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 { }
|
||||
}
|
||||
|
||||
function handleMailLower(mail: string) {
|
||||
export function handleMailLower(mail: string) {
|
||||
const [n, d] = mail.trim().toLowerCase().split('@');
|
||||
const [name] = n.split('+');
|
||||
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 rating } from './lib/rating';
|
||||
export { default as avatar } from './lib/avatar';
|
||||
export { default as rank } from './lib/rank';
|
||||
export { default as paginate } from './lib/paginate';
|
||||
/** @deprecated use Handler.paginate instead */
|
||||
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 * from '@hydrooj/framework/decorators';
|
||||
export * from '@hydrooj/framework/validator';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
Collection, Db, IndexDescription, MongoClient,
|
||||
Collection, Db, FindCursor, IndexDescription, MongoClient,
|
||||
} from 'mongodb';
|
||||
import mongoUri from 'mongodb-uri';
|
||||
import { Time } from '@hydrooj/utils';
|
||||
import { ValidationError } from '../error';
|
||||
import { Logger } from '../logger';
|
||||
import { load } from '../options';
|
||||
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) {
|
||||
await this.start();
|
||||
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 DomainModel from '../../model/domain';
|
||||
import * as system from '../../model/system';
|
||||
@@ -24,5 +24,12 @@ export default async (ctx: KoaContext, next) => {
|
||||
ctx.domainId = inferDomain?._id || domainId;
|
||||
ctx.domainInfo = inferDomain || absoluteDomain;
|
||||
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 { NotFoundError } from '../../error';
|
||||
import { PERM } from '../../model/builtin';
|
||||
import UserModel from '../../model/user';
|
||||
|
||||
@@ -15,9 +14,5 @@ export default async (ctx: KoaContext, next) => {
|
||||
}
|
||||
if (user._id === 0) delete user.viewLang;
|
||||
ctx.HydroContext.user = await user.private();
|
||||
if (!domain) {
|
||||
ctx.pendingError = new NotFoundError(args.domainId);
|
||||
args.domainId = 'system';
|
||||
}
|
||||
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 { PermissionError, PrivilegeError } from '../error';
|
||||
import type { DomainDoc } from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import { Logger } from '../logger';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import * as opcount from '../model/opcount';
|
||||
import * as OplogModel from '../model/oplog';
|
||||
import * as system from '../model/system';
|
||||
import { builtinConfig } from '../settings';
|
||||
import db from './db';
|
||||
import baseLayer from './layers/base';
|
||||
import domainLayer from './layers/domain';
|
||||
import userLayer from './layers/user';
|
||||
@@ -144,8 +144,18 @@ export async function apply(ctx: Context) {
|
||||
});
|
||||
if (process.env.HYDRO_CLI) return;
|
||||
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', {
|
||||
target: builtinConfig.file.endPoint,
|
||||
target: endpoint,
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace('/fs', ''),
|
||||
});
|
||||
@@ -209,7 +219,7 @@ export async function apply(ctx: Context) {
|
||||
? (!host.includes(this.request.host))
|
||||
: this.request.host !== host)
|
||||
)) 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 (withDomainId) res = `/d/${withDomainId}${res}`;
|
||||
} catch (e) {
|
||||
@@ -228,7 +238,7 @@ export async function apply(ctx: Context) {
|
||||
return res;
|
||||
},
|
||||
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[]) {
|
||||
if (!this.user.hasPerm(...args)) {
|
||||
@@ -255,7 +265,7 @@ export async function apply(ctx: Context) {
|
||||
},
|
||||
renderTitle(str: string) {
|
||||
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}`;
|
||||
},
|
||||
});
|
||||
@@ -276,6 +286,7 @@ export async function apply(ctx: Context) {
|
||||
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.context.pendingError) throw h.context.pendingError;
|
||||
});
|
||||
|
||||
on('app/listen', () => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { dirname, resolve } from 'path';
|
||||
import { PassThrough, Readable } from 'stream';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand,
|
||||
GetObjectCommand, HeadObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client,
|
||||
DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand,
|
||||
HeadObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { Upload } from '@aws-sdk/lib-storage';
|
||||
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
||||
@@ -243,16 +243,6 @@ class RemoteStorageService {
|
||||
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() {
|
||||
return {
|
||||
type: 'S3',
|
||||
@@ -332,14 +322,6 @@ class LocalStorageService {
|
||||
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() {
|
||||
return {
|
||||
type: 'Local',
|
||||
|
||||
@@ -22,7 +22,6 @@ process.on('SIGTERM', terminate);
|
||||
|
||||
const shell = new Logger('shell');
|
||||
async function executeCommand(input: string) {
|
||||
input = input.trim();
|
||||
// Clear the stack
|
||||
setImmediate(async () => {
|
||||
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.setRawMode?.(false);
|
||||
process.stdin.on('data', (buf) => {
|
||||
const input = buf.toString();
|
||||
executeCommand(input);
|
||||
const input = buf.toString().trim();
|
||||
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,22 +21,24 @@ import StorageModel from './model/storage';
|
||||
import * as system from './model/system';
|
||||
import TaskModel from './model/task';
|
||||
import * as training from './model/training';
|
||||
import user from './model/user';
|
||||
import user, { handleMailLower } from './model/user';
|
||||
import {
|
||||
iterateAllContest, iterateAllDomain, iterateAllProblem, iterateAllUser,
|
||||
} from './pipelineUtils';
|
||||
import db from './service/db';
|
||||
import { MigrationScript } from './service/migration';
|
||||
import { setBuiltinConfig } from './settings';
|
||||
import welcome from './welcome';
|
||||
|
||||
const logger = new Logger('upgrade');
|
||||
type UpgradeScript = void | (() => Promise<boolean | void>);
|
||||
const unsupportedUpgrade = async function _26_27() {
|
||||
throw new Error('This upgrade was no longer supported in hydrooj@4. \
|
||||
Please use hydrooj@3 to perform these upgrades before upgrading to v4');
|
||||
};
|
||||
|
||||
export async function init() {
|
||||
export const coreScripts: MigrationScript[] = [
|
||||
// Mark as used
|
||||
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);
|
||||
}
|
||||
@@ -47,11 +49,7 @@ export async function init() {
|
||||
if (!ddoc) await domain.add('system', 1, 'Hydro', 'Welcome to Hydro!');
|
||||
await welcome();
|
||||
return true;
|
||||
}
|
||||
|
||||
const scripts: UpgradeScript[] = [
|
||||
// Mark as used
|
||||
null,
|
||||
},
|
||||
// Init
|
||||
...new Array(26).fill(unsupportedUpgrade),
|
||||
null,
|
||||
@@ -675,6 +673,16 @@ const scripts: UpgradeScript[] = [
|
||||
if (bulk.batches.length) await bulk.execute();
|
||||
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",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"prom-client": "^15.1.2"
|
||||
"prom-client": "^15.1.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import esbuild from 'esbuild';
|
||||
import {
|
||||
Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1,
|
||||
size, Types, UiContextBase,
|
||||
size, SystemModel, Types, UiContextBase,
|
||||
} from 'hydrooj';
|
||||
import { debounce } from 'lodash';
|
||||
import { tmpdir } from 'os';
|
||||
@@ -46,6 +46,7 @@ const build = async (contents: string) => {
|
||||
format: 'iife' as 'iife',
|
||||
bundle: true,
|
||||
outdir: tmp,
|
||||
sourcemap: SystemModel.get('ui-default.nosourcemap') ? false : 'external',
|
||||
splitting: false,
|
||||
write: false,
|
||||
target: ['chrome65'],
|
||||
@@ -103,11 +104,11 @@ export async function buildUI() {
|
||||
}
|
||||
const entry = await build([
|
||||
`window.lazyloadMetadata = ${JSON.stringify(hashes)};`,
|
||||
`window.LANGS=${JSON.stringify(SettingModel.langs)};`,
|
||||
...entryPoints.map((i) => `import '${relative(tmp, i).replace(/\\/g, '\\\\')}';`),
|
||||
].join('\n'));
|
||||
const pages = entry.outputFiles.map((i) => i.text);
|
||||
const str = `window.LANGS=${JSON.stringify(SettingModel.langs)};${pages.join('\n')}`;
|
||||
addFile('entry.js', str);
|
||||
const pages = entry.outputFiles.filter((i) => i.path.endsWith('.js')).map((i) => i.text);
|
||||
addFile('entry.js', `window._hydroLoad=()=>{ ${pages.join('\n')} };`);
|
||||
UiContextBase.constantVersion = hashes['entry.js'];
|
||||
for (const key in vfs) {
|
||||
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
|
||||
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) {
|
||||
const max = state.posMax;
|
||||
@@ -109,8 +110,8 @@ function block(state, start, end, silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = function plugin(md) {
|
||||
const options = { throwOnError: false, strict: 'ignore' };
|
||||
export default function plugin(md) {
|
||||
const options = { throwOnError: false, strict: 'ignore', displayMode: false };
|
||||
const render = (latex: string, displayMode = false) => {
|
||||
options.displayMode = displayMode;
|
||||
try {
|
||||
@@ -136,4 +137,4 @@ module.exports = function plugin(md) {
|
||||
});
|
||||
md.renderer.rules.math_inline = inlineRenderer;
|
||||
md.renderer.rules.math_block = blockRenderer;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable prefer-destructuring */
|
||||
|
||||
import { randomUUID } from 'crypto';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
|
||||
|
||||
@@ -110,7 +110,7 @@ export function Media(md: MarkdownIt) {
|
||||
if (service === 'pdf') {
|
||||
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
|
||||
return `\
|
||||
<object classid="clsid:${randomUUID().toUpperCase()}">
|
||||
<object classid="clsid:${uuid().toUpperCase()}">
|
||||
<param name="SRC" value="${src}" >
|
||||
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
|
||||
<noembed></noembed>
|
||||
|
||||
@@ -23,7 +23,7 @@ const MergeCells = require('markdown-it-merge-cells');
|
||||
// @[youtube](https://youtube.com/watch?v=xxx)
|
||||
// @[pdf](https://foo.com/bar.pdf)
|
||||
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');
|
||||
|
||||
class Markdown extends MarkdownIt {
|
||||
|
||||
@@ -2,17 +2,42 @@
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
import { CleanWebpackPlugin } from 'clean-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 fs from 'fs';
|
||||
import { DuplicatesPlugin } from 'inspectpack/plugin';
|
||||
import ExtractCssPlugin from 'mini-css-extract-plugin';
|
||||
import packageJson from 'package-json';
|
||||
import { dirname } from 'path';
|
||||
import { gt } from 'semver';
|
||||
import webpack from 'webpack';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
|
||||
import WebpackBar from 'webpackbar';
|
||||
import { version } from '../../package.json';
|
||||
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() {
|
||||
return {
|
||||
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
|
||||
devtool: env.production ? 'source-map' : false,
|
||||
entry: {
|
||||
hydro: './entry.js',
|
||||
polyfill: './polyfill.ts',
|
||||
[`hydro-${version}`]: './entry.js',
|
||||
'default.theme': './theme/default.js',
|
||||
'service-worker': './service-worker.ts',
|
||||
'messages-shared-worker': './components/message/worker.ts',
|
||||
},
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
@@ -78,6 +101,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
output: {
|
||||
path: root('public'),
|
||||
publicPath: '/', // overwrite in entry.js
|
||||
workerPublicPath: '/',
|
||||
hashFunction: 'sha1',
|
||||
hashDigest: 'hex',
|
||||
hashDigestLength: 10,
|
||||
@@ -233,18 +257,30 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
filename: '[name].css?[fullhash:6]',
|
||||
}),
|
||||
new WebpackManifestPlugin({}),
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }),
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$)/ }),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: root('static') },
|
||||
{ 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'))}/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: `${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({
|
||||
'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',
|
||||
}),
|
||||
] : []),
|
||||
new webpack.NormalModuleReplacementPlugin(/core-js\/stable/, root('__core-js.js')),
|
||||
...env.measure ? [
|
||||
new BundleAnalyzerPlugin({ analyzerPort: 'auto' }),
|
||||
new DuplicatesPlugin(),
|
||||
|
||||
@@ -19,7 +19,7 @@ const argv = cac().parse();
|
||||
async function runWebpack({
|
||||
watch, production, measure, dev, https,
|
||||
}) {
|
||||
const compiler = webpack(webpackConfig({ watch, production, measure }));
|
||||
const compiler = webpack(await webpackConfig({ watch, production, measure }));
|
||||
if (dev) {
|
||||
const server = new WebpackDevServer({
|
||||
port: https ? 8001 : 8000,
|
||||
@@ -49,7 +49,10 @@ async function runWebpack({
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (watch) compiler.watch({}, compilerCallback);
|
||||
@@ -121,22 +124,9 @@ async function main() {
|
||||
if (argv.options.gulp) await runGulp();
|
||||
else {
|
||||
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')) {
|
||||
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)));
|
||||
}
|
||||
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';
|
||||
options: CustomSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
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';
|
||||
options: FileSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
|
||||
@@ -11,10 +11,7 @@ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||
<AutoComplete<DefaultProps>
|
||||
ref={ref as any}
|
||||
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name
|
||||
? i.name.toLowerCase().match(query.toLowerCase())
|
||||
: i.toString().toLowerCase().match(query.toLowerCase())
|
||||
))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
||||
itemText={(item) => `${item.name || item}`}
|
||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||
renderItem={(item) => `${item.name || item}`}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
defaultItems?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
classes?: string;
|
||||
listStyle?: any;
|
||||
allowEmptyQuery?: boolean;
|
||||
freeSolo?: boolean;
|
||||
@@ -18,17 +19,17 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
text?: () => string;
|
||||
}
|
||||
|
||||
export default class AutoComplete extends DOMAttachedObject {
|
||||
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||
ref = null;
|
||||
container = document.createElement('div');
|
||||
options: AutoCompleteOptions;
|
||||
options: AutoCompleteOptions & Options;
|
||||
component = ReactDOM.createRoot(this.container);
|
||||
changeListener = [
|
||||
(val) => this.$dom.val(val),
|
||||
];
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
constructor($dom, options = {} as Options) {
|
||||
super($dom);
|
||||
this.options = {
|
||||
items: async () => [],
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
|
||||
.drop.nav__dropdown
|
||||
max-height: calc(100% - 60px)
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
|
||||
|
||||
.drop.nav__dropdown.drop-open
|
||||
overflow-x: auto
|
||||
|
||||
.dropdown
|
||||
.menu
|
||||
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
|
||||
color: grey !important
|
||||
background-color: grey !important
|
||||
+94
-100
@@ -1,36 +1,11 @@
|
||||
import $ from 'jquery';
|
||||
import type { editor } from 'monaco-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { i18n, request } from 'vj/utils';
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
import { i18n } from 'vj/utils';
|
||||
import uploadFiles from '../upload';
|
||||
|
||||
interface MonacoOptions {
|
||||
language?: string;
|
||||
@@ -43,23 +18,23 @@ interface MonacoOptions {
|
||||
hide?: string[];
|
||||
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
||||
}
|
||||
interface VditorOptions {
|
||||
theme?: 'classic' | 'dark'
|
||||
}
|
||||
type Options = MonacoOptions & VditorOptions;
|
||||
type Options = MonacoOptions;
|
||||
|
||||
export default class Editor extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjEditorInstance';
|
||||
model: editor.IModel;
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
vditor: import('vditor').default;
|
||||
markdownEditor: any;
|
||||
valueCache?: string;
|
||||
setMarkdownEditorValue?: (v: string) => void;
|
||||
reactRoot?: ReactDOM.Root;
|
||||
isValid: boolean;
|
||||
|
||||
constructor($dom, public options: Options = {}) {
|
||||
super($dom);
|
||||
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
||||
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
||||
else this.initVditor();
|
||||
else this.initMarkdownEditor();
|
||||
}
|
||||
|
||||
async initMonaco() {
|
||||
@@ -169,87 +144,102 @@ export default class Editor extends DOMAttachedObject {
|
||||
window.editor = this.editor;
|
||||
}
|
||||
|
||||
async initVditor() {
|
||||
async initMarkdownEditor() {
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
const isProblemEdit = pagename === 'problem_edit';
|
||||
const { default: Vditor } = await import('vditor');
|
||||
const that = this;
|
||||
const { $dom } = this;
|
||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||
const origin = $dom.get(0);
|
||||
const ele = document.createElement('div');
|
||||
const value = $dom.val();
|
||||
const { onChange } = this.options;
|
||||
await new Promise((resolve) => {
|
||||
this.vditor = new Vditor(ele, {
|
||||
...config,
|
||||
upload: {
|
||||
multiple: false,
|
||||
handler: (files) => {
|
||||
let wrapper = ['', ''];
|
||||
const { MdEditor } = await import('./mdeditor');
|
||||
|
||||
const renderCallback = (ref) => {
|
||||
this.markdownEditor = ref;
|
||||
};
|
||||
|
||||
function EditorComponent() {
|
||||
const [val, setVal] = React.useState(value);
|
||||
that.setMarkdownEditorValue = setVal;
|
||||
return <MdEditor
|
||||
className='textbox'
|
||||
autoFocus={hasFocus}
|
||||
codeTheme='github'
|
||||
codeStyleReverse={false}
|
||||
ref={renderCallback}
|
||||
modelValue={val}
|
||||
noMermaid
|
||||
noPrettier
|
||||
autoDetectCode
|
||||
toolbarsExclude={[
|
||||
// 'bold',
|
||||
// 'underline',
|
||||
// 'italic',
|
||||
// '-',
|
||||
// 'strikeThrough',
|
||||
// 'title',
|
||||
// 'sub',
|
||||
// 'sup',
|
||||
// 'quote',
|
||||
// 'unorderedList',
|
||||
// 'orderedList',
|
||||
// 'task',
|
||||
// '-',
|
||||
// 'codeRow',
|
||||
// 'code',
|
||||
// 'link',
|
||||
// 'image',
|
||||
// 'table',
|
||||
'mermaid',
|
||||
// 'katex',
|
||||
// '-',
|
||||
// 'revoke',
|
||||
// 'next',
|
||||
'save',
|
||||
// '=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
// 'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
// 'catalog',
|
||||
'github',
|
||||
]}
|
||||
onChange={(v) => {
|
||||
that.valueCache = v;
|
||||
setVal(v);
|
||||
$dom.val(v);
|
||||
$dom.text(v);
|
||||
onChange?.(v);
|
||||
}}
|
||||
onUploadImg={async (files, callback) => {
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
wrapper = [''];
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
wrapper = ['[file](', ')'];
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
const data = new FormData();
|
||||
data.append('filename', filename);
|
||||
data.append('file', files[0]);
|
||||
data.append('operation', 'upload_file');
|
||||
if (isProblemEdit) data.append('type', 'additional_file');
|
||||
let progress = 0;
|
||||
request.postFile(isProblemEdit ? './files' : '/file', data, {
|
||||
xhr: () => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (!e.lengthComputable) return;
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
if (percentComplete === progress) return;
|
||||
progress = percentComplete;
|
||||
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
|
||||
this.vditor.vditor.tip.hide();
|
||||
})
|
||||
.catch((e: { message: string; }) => {
|
||||
console.error(e);
|
||||
return `${i18n('Upload Failed')}: ${e.message}`;
|
||||
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.options,
|
||||
after: () => {
|
||||
const pos = $(ele).find('button[data-mode="sv"]').get();
|
||||
const button = $('<button data-mode="monaco">Monaco Editor</button>');
|
||||
button.on('click', (e) => {
|
||||
Notification.info('You can select this in PreferenceSettings');
|
||||
e.preventDefault();
|
||||
});
|
||||
button.insertAfter(pos);
|
||||
resolve(null);
|
||||
},
|
||||
input(v) {
|
||||
$dom.val(v);
|
||||
$dom.text(v);
|
||||
if (onChange) onChange(v);
|
||||
},
|
||||
value,
|
||||
cache: { id: Math.random().toString() },
|
||||
});
|
||||
});
|
||||
$(ele).addClass('textbox');
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
this.reactRoot = ReactDOM.createRoot(ele);
|
||||
this.reactRoot.render(<EditorComponent />);
|
||||
$dom.hide();
|
||||
origin.parentElement.appendChild(ele);
|
||||
this.isValid = true;
|
||||
@@ -258,7 +248,7 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
destroy() {
|
||||
this.detach();
|
||||
if (this.vditor?.destroy) this.vditor.destroy();
|
||||
if (this.reactRoot) this.reactRoot.unmount();
|
||||
else if (this.editor?.dispose) this.editor.dispose();
|
||||
}
|
||||
|
||||
@@ -268,13 +258,17 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
value(val?: string) {
|
||||
this.ensureValid();
|
||||
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
|
||||
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
if (typeof val === 'string') {
|
||||
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() {
|
||||
this.ensureValid();
|
||||
this.vditor?.focus();
|
||||
if (!this.editor || !this.model) return;
|
||||
this.editor.focus();
|
||||
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 ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import { InfoDialog } from 'vj/components/dialog';
|
||||
import VjNotification from 'vj/components/notification/index';
|
||||
import {
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
} from 'vj/constant/message';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { i18n, tpl } from 'vj/utils';
|
||||
import Sock from '../socket';
|
||||
|
||||
let previous: VjNotification;
|
||||
const onmessage = (msg) => {
|
||||
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
|
||||
|
||||
const initWorkerMode = (endpoint) => {
|
||||
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();
|
||||
window.addEventListener('beforeunload', () => {
|
||||
worker.port.postMessage({ type: 'unload' });
|
||||
@@ -128,12 +128,8 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||
isMaster = true;
|
||||
localStorage.setItem('page.master', selfId);
|
||||
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||
const sock = new ReconnectingWebsocket(endpoint);
|
||||
sock.onopen = () => console.log('Connected');
|
||||
sock.onerror = console.error;
|
||||
sock.onclose = (...args) => console.log('Closed', ...args);
|
||||
const sock = new Sock(endpoint);
|
||||
sock.onmessage = async (message) => {
|
||||
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
|
||||
const payload = JSON.parse(message.data);
|
||||
masterChannel.postMessage({ type: 'message', payload });
|
||||
};
|
||||
|
||||
@@ -31,12 +31,17 @@ function broadcastMsg(message: any) {
|
||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
if (cookie !== lcookie) conn?.close();
|
||||
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;
|
||||
console.log('Init connection for', path);
|
||||
conn?.close();
|
||||
conn = new ReconnectingWebsocket(path);
|
||||
conn = new ReconnectingWebsocket(url.toString());
|
||||
ports.push(port);
|
||||
conn.onopen = () => conn.send(cookie);
|
||||
conn.onopen = () => {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
};
|
||||
conn.onerror = () => broadcastMsg({ type: 'error' });
|
||||
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
||||
conn.onmessage = (message) => {
|
||||
@@ -46,14 +51,9 @@ function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(message.data);
|
||||
if (payload.event === 'auth') {
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
}
|
||||
} else {
|
||||
broadcastMsg({ type: 'message', payload });
|
||||
let acked = false;
|
||||
|
||||
@@ -87,8 +87,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||
disposable = [];
|
||||
containerElement: HTMLElement;
|
||||
private __preventUpdate = false;
|
||||
private __preventFormat = false;
|
||||
__preventUpdate = false;
|
||||
__preventFormat = false;
|
||||
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
model: editor.ITextModel;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`;
|
||||
if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`;
|
||||
|
||||
// TODO switch to indexeddb
|
||||
export default function reducer(state = {
|
||||
lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang,
|
||||
code: localStorage.getItem(cacheKey) || UiContext.codeTemplate,
|
||||
|
||||
@@ -54,6 +54,10 @@ export default class Sock {
|
||||
onclose: (code: number, reason: string) => void;
|
||||
onopen: (sock: ReconnectingWebSocket) => void;
|
||||
|
||||
on(event: 'message' | 'close' | 'open', callback: (...args: any[]) => void) {
|
||||
this[`on${event}`] = callback;
|
||||
}
|
||||
|
||||
send(data) {
|
||||
this.sock.send(data);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
const TAB_TRANSITION_DURATION = 300;
|
||||
@@ -144,5 +143,3 @@ export default class Tab extends DOMAttachedObject {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(Tab, DOMAttachedObject);
|
||||
@@ -73,6 +73,26 @@
|
||||
background: $secondary-color
|
||||
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
|
||||
position: relative
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ interface UploadOptions {
|
||||
pjax?: boolean;
|
||||
sidebar?: boolean;
|
||||
singleFileUploadCallback?: (file: File) => any;
|
||||
filenameCallback?: (file: File) => string;
|
||||
}
|
||||
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
||||
const dialog = new Dialog({
|
||||
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
|
||||
if (Number.isNaN(+i)) continue;
|
||||
const file = files[i];
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('filename', options.filenameCallback?.(file) || file.name);
|
||||
data.append('file', file);
|
||||
if (options.type) data.append('type', options.type);
|
||||
data.append('operation', 'upload_file');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './polyfill';
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import $ from 'jquery';
|
||||
|
||||
@@ -29,17 +31,23 @@ console.log(
|
||||
window.UiContext = JSON.parse(window.UiContext);
|
||||
window.UserContext = JSON.parse(window.UserContext);
|
||||
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({
|
||||
dsn: UiContext.sentry_dsn || 'https://04d1d77b095231d55e1202bdc912ff16@sentry.hydro.ac/3',
|
||||
release: process.env.VERSION,
|
||||
dsn: UiContext.sentry_dsn || 'https://2f95d53751e08c74c1af1c4b93ccaff7@sentry.hydro.ac/2',
|
||||
release: `hydro-web@${process.env.VERSION}`,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration(),
|
||||
Sentry.browserApiErrorsIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
networkRequestHeaders: ['Content-Type'],
|
||||
networkResponseHeaders: ['Content-Type', 'Location'],
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
tracePropagationTargets: ['localhost', /^\//, window.location.host],
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysSessionSampleRate: 0.03,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
@@ -57,16 +65,10 @@ const PageLoader = '<div class="page-loader nojs--hide" style="display:none;"><d
|
||||
$('body').prepend(PageLoader);
|
||||
$('.page-loader').fadeIn(500);
|
||||
|
||||
const prefetch = Promise.all([
|
||||
fetch(`/constant/${UiContext.constantVersion}.js`).then((r) => r.text()),
|
||||
import('./api'),
|
||||
]);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
Object.assign(window.UiContext, JSON.parse(window.UiContextNew));
|
||||
Object.assign(window.UserContext, JSON.parse(window.UserContextNew));
|
||||
const [data, HydroExports] = await prefetch;
|
||||
Object.assign(window, { HydroExports });
|
||||
eval(data); // eslint-disable-line no-eval
|
||||
await HydroExports.initPageLoader();
|
||||
window.HydroExports = await import('./api');
|
||||
await window._hydroLoad();
|
||||
await window.HydroExports.initPageLoader();
|
||||
}, false);
|
||||
@@ -10,13 +10,6 @@ class WikiHelpHandler extends Handler {
|
||||
noCheckPermView = true;
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.50.2",
|
||||
"version": "4.51.4",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
@@ -14,13 +14,13 @@
|
||||
"cli": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blueprintjs/core": "^5.10.3",
|
||||
"@blueprintjs/core": "^5.10.5",
|
||||
"@codingame/monaco-editor-wrapper": "^6.6.10",
|
||||
"@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-hant": "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/fira-code": "^5.0.18",
|
||||
"@fontsource/inconsolata": "^5.0.18",
|
||||
@@ -30,12 +30,13 @@
|
||||
"@fontsource/source-code-pro": "^5.0.18",
|
||||
"@fontsource/ubuntu-mono": "^5.0.20",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@sentry/browser": "^8.8.0",
|
||||
"@sentry/webpack-plugin": "^2.18.0",
|
||||
"@sentry/browser": "^8.15.0",
|
||||
"@sentry/webpack-plugin": "^2.20.1",
|
||||
"@simplewebauthn/browser": "7.4.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/gulp-if": "^0.0.34",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/jquery.transit": "^0.9.33",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
@@ -45,25 +46,28 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/vscode": "^1.89.0",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"ajv": "^8.16.0",
|
||||
"allotment": "^1.20.2",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browser-update": "^3.3.51",
|
||||
"browser-update": "^3.3.53",
|
||||
"chalk": "^5.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"core-js": "^3.37.1",
|
||||
"core-js-compat": "^3.37.1",
|
||||
"css-loader": "^6.11.0",
|
||||
"diff": "^5.2.0",
|
||||
"diff-dom": "^5.1.3",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts": "^5.5.1",
|
||||
"emojis-keywords": "2.0.0",
|
||||
"emojis-list": "2.1.0",
|
||||
"esbuild-loader": "^4.1.0",
|
||||
"esbuild-loader": "^4.2.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"graphiql": "^2.4.7",
|
||||
@@ -72,17 +76,19 @@
|
||||
"gulp-if": "^3.0.0",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-svgmin": "^4.1.0",
|
||||
"idb": "^8.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-scroll-lock": "^3.1.3",
|
||||
"jquery.easing": "^1.4.1",
|
||||
"jquery.transit": "^0.9.12",
|
||||
"matchmedia-polyfill": "^0.3.2",
|
||||
"md-editor-rt": "^4.17.3",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0",
|
||||
"monaco-languageclient": "^8.4.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"monaco-yaml": "^5.1.1",
|
||||
"monaco-yaml": "5.1.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -95,7 +101,6 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"qface": "^1.4.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"queue-microtask": "^1.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
@@ -129,7 +134,7 @@
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@^5.3.0",
|
||||
"vscode-languageclient": "^9.0.1",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
@@ -142,10 +147,10 @@
|
||||
"fs-extra": "^11.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"katex": "^0.16.10",
|
||||
"katex": "^0.16.11",
|
||||
"lodash": "^4.17.21",
|
||||
"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-footnote": "^4.0.0",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
@@ -155,6 +160,7 @@
|
||||
"nunjucks": "^3.2.4",
|
||||
"schemastery-jsonschema": "^1.1.0",
|
||||
"streamsaver": "^2.0.6",
|
||||
"uuid": "^10.0.0",
|
||||
"xss": "^1.0.15"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import $ from 'jquery';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
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 key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`;
|
||||
const read = () => JSON.parse(localStorage.getItem(key) || '[]');
|
||||
const write = (data) => localStorage.setItem(key, JSON.stringify(data));
|
||||
const id = `${tdoc.domainId}/${tdoc.docId}`;
|
||||
const read = async () => (await db.get('scoreboard-star', id))?.value || [];
|
||||
const write = (data) => db.put('scoreboard-star', { id, data });
|
||||
|
||||
$(`.star.user--${UserContext._id}`).closest('tr').addClass('star-highlight');
|
||||
read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
|
||||
$('.star').on('click', (e) => {
|
||||
const star = read();
|
||||
(await read()).forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
|
||||
$('.star').on('click', async (e) => {
|
||||
const star = await read();
|
||||
const $target = $(e.currentTarget);
|
||||
const uid = $target.data('uid');
|
||||
if (star.includes(uid)) {
|
||||
@@ -24,14 +26,14 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
|
||||
write(star);
|
||||
});
|
||||
|
||||
$('.select.filter').on('change', (e) => {
|
||||
const val = $(e.target).val();
|
||||
async function update() {
|
||||
const val = $($('.select.filter')).val();
|
||||
if (val === 'all') {
|
||||
$('.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') {
|
||||
$('.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') {
|
||||
$('.data-table tbody tr').show();
|
||||
$('.rank--unrank').closest('tr').hide();
|
||||
@@ -41,13 +43,17 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
|
||||
if (!uids?.length) return;
|
||||
uids.forEach((uid) => $(`.user--${uid}`).closest('tr').show());
|
||||
}
|
||||
});
|
||||
}
|
||||
$('.select.filter').on('change', update);
|
||||
|
||||
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
|
||||
const endAt = new Date(UiContext.tdoc.endAt).getTime();
|
||||
function updateScoreboard() {
|
||||
async function updateScoreboard() {
|
||||
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);
|
||||
|
||||
+33
-23
@@ -9,15 +9,14 @@ import { NamedPage } from 'vj/misc/Page';
|
||||
import {
|
||||
delay, i18n, loadReactRedux, pjax, request, tpl,
|
||||
} from 'vj/utils';
|
||||
import { openDB } from 'vj/utils/db';
|
||||
|
||||
class ProblemPageExtender {
|
||||
constructor() {
|
||||
this.isExtended = false;
|
||||
this.inProgress = false;
|
||||
this.$content = $('.problem-content-container');
|
||||
this.$contentBound = this.$content.closest('.section');
|
||||
this.$scratchpadContainer = $('.scratchpad-container');
|
||||
}
|
||||
isExtended = false;
|
||||
inProgress = false;
|
||||
$content = $('.problem-content-container');
|
||||
$contentBound = this.$content.closest('.section');
|
||||
$scratchpadContainer = $('.scratchpad-container');
|
||||
|
||||
async extend() {
|
||||
if (this.inProgress) return;
|
||||
@@ -28,6 +27,7 @@ class ProblemPageExtender {
|
||||
.get(0)
|
||||
.getBoundingClientRect();
|
||||
|
||||
// @ts-ignore
|
||||
this.$content.transition({ opacity: 0 }, { duration: 100 });
|
||||
await delay(100);
|
||||
|
||||
@@ -41,6 +41,7 @@ class ProblemPageExtender {
|
||||
})
|
||||
.show()
|
||||
.transition({
|
||||
// @ts-ignore
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
@@ -77,6 +78,7 @@ class ProblemPageExtender {
|
||||
|
||||
await this.$scratchpadContainer
|
||||
.transition({
|
||||
// @ts-ignore
|
||||
left: bound.left,
|
||||
top: bound.top,
|
||||
width: bound.width,
|
||||
@@ -88,6 +90,7 @@ class ProblemPageExtender {
|
||||
.promise();
|
||||
|
||||
this.$scratchpadContainer.hide();
|
||||
// @ts-ignore
|
||||
this.$content.transition({ opacity: 1 }, { duration: 100 });
|
||||
window.document.body.style.overflow = 'scroll';
|
||||
|
||||
@@ -112,6 +115,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
}
|
||||
|
||||
async function scratchpadFadeIn() {
|
||||
// @ts-ignore
|
||||
await $('#scratchpad')
|
||||
.transition(
|
||||
{ opacity: 1 },
|
||||
@@ -121,6 +125,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
}
|
||||
|
||||
async function scratchpadFadeOut() {
|
||||
// @ts-ignore
|
||||
await $('#scratchpad')
|
||||
.transition(
|
||||
{ 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 { Provider, store } = await loadReactRedux(ScratchpadReducer);
|
||||
|
||||
// @ts-ignore
|
||||
window.store = store;
|
||||
const sock = new WebSocket(UiContext.ws_prefix + UiContext.pretestConnUrl);
|
||||
sock.onmessage = (message) => {
|
||||
@@ -237,22 +243,25 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
[, setUpdate] = React.useState(0);
|
||||
return <div className="contest-problems" style={{ margin: '1em' }}>
|
||||
{pids.map((i) => <a href={`#p${i}`} className={ans[i] ? 'pass ' : ''}>
|
||||
<span class="id">{i}</span>
|
||||
{ans[i] && <span class="icon icon-check"></span>}
|
||||
<span className="id">{i}</span>
|
||||
{ans[i] && <span className="icon icon-check"></span>}
|
||||
</a>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function saveAns() {
|
||||
localStorage.setItem(`${cacheKey}#objective`, JSON.stringify(ans));
|
||||
setUpdate?.((i) => i + 1);
|
||||
const db = await openDB;
|
||||
async function saveAns() {
|
||||
await db.put('solutions', {
|
||||
id: `${cacheKey}#objective`,
|
||||
value: JSON.stringify(ans),
|
||||
});
|
||||
}
|
||||
function clearAns() {
|
||||
localStorage.removeItem(`${cacheKey}#objective`);
|
||||
async function clearAns() {
|
||||
await db.delete('solutions', `${cacheKey}#objective`);
|
||||
window.location.reload();
|
||||
}
|
||||
function loadAns() {
|
||||
const saved = localStorage.getItem(`${cacheKey}#objective`);
|
||||
async function loadAns() {
|
||||
const saved = await db.get('solutions', `${cacheKey}#objective`);
|
||||
if (!saved) return;
|
||||
const isValidOption = (v) => v.length === 1 && v.charCodeAt(0) >= 65 && v.charCodeAt(0) <= 90;
|
||||
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) {
|
||||
if (isValidOption(v)) $(`.objective_${id} input[value="${v}"]`).prop('checked', true);
|
||||
}
|
||||
} else {
|
||||
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val);
|
||||
} else if (val) {
|
||||
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val.toString());
|
||||
if (isValidOption(val)) $(`.objective_${id}.radiobox [value="${val}"]`).prop('checked', true);
|
||||
}
|
||||
}
|
||||
setUpdate((v) => v + 1);
|
||||
}
|
||||
|
||||
if (cnt) {
|
||||
loadAns();
|
||||
await loadAns();
|
||||
$('.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" 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;
|
||||
saveAns();
|
||||
});
|
||||
$('input.objective-input[type=checkbox]').on('input', (e) => {
|
||||
$('input.objective-input[type=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
ans[e.target.name] ||= [];
|
||||
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 {
|
||||
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({
|
||||
$body: tpl.typoMsg('All changes will be lost. Are you sure to clear all answers?'),
|
||||
}).open();
|
||||
if (result === 'yes') clearAns();
|
||||
if (result === 'yes') await clearAns();
|
||||
});
|
||||
}
|
||||
const ele = document.createElement('div');
|
||||
@@ -154,7 +154,7 @@ async function handleSection(ev, sidebar, type) {
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
export default new NamedPage(['problem_create', 'problem_edit'], () => {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -240,14 +240,6 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
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 $field = $('textarea[data-markdown-upload]');
|
||||
let content = $field.val();
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
.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
|
||||
height: 270px
|
||||
overflow-y: auto
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'core-js/stable';
|
||||
import 'matchmedia-polyfill';
|
||||
|
||||
import browserUpdate from 'browser-update';
|
||||
import queueMicrotask from 'queue-microtask';
|
||||
|
||||
window.queueMicrotask = queueMicrotask;
|
||||
browserUpdate({
|
||||
required: {
|
||||
e: -10, f: -10, o: -3, s: -1, c: -10,
|
||||
@@ -13,11 +12,23 @@ browserUpdate({
|
||||
});
|
||||
|
||||
// monaco-editor requires this polyfill
|
||||
if (window.MediaQueryList) {
|
||||
if (window.MediaQueryList && !MediaQueryList.prototype.addEventListener) {
|
||||
MediaQueryList.prototype.addEventListener = (k, 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)) {
|
||||
const localMatchMedia = window.matchMedia;
|
||||
const hasMediaQueries = localMatchMedia('only all').matches;
|
||||
|
||||
@@ -89,7 +89,7 @@ self.addEventListener('notificationclick', (event) => {
|
||||
});
|
||||
|
||||
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) {
|
||||
if (!path.split('?')[0].split('/').pop()) return false;
|
||||
|
||||
@@ -43,8 +43,6 @@ nav_logo_dark:
|
||||
default: /components/navigation/nav-logo-small_dark.png
|
||||
preferredEditorType:
|
||||
type:
|
||||
wysiwyg: WYSIWYG
|
||||
ir: Instant Rendering
|
||||
sv: Split View
|
||||
monaco: Monaco Editor
|
||||
family: markdown
|
||||
|
||||
@@ -46,33 +46,19 @@
|
||||
<script>
|
||||
var _htmlNode = document.documentElement;
|
||||
_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 UserContext = '{{ UserContext|json|jsesc|safe }}';
|
||||
</script>
|
||||
{% endif %}
|
||||
{% set UiContext = Object.create(UiContext) %}
|
||||
{% set UserContext = Object.create(UserContext) %}
|
||||
{% if process.env.DEV %}
|
||||
<script type="text/javascript" src="/resource/hash/lang-{{ _('__id') }}.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
||||
<script type="text/javascript" src="/hydro.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
||||
{% else %}
|
||||
</head>
|
||||
<body>
|
||||
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
||||
<script type="text/javascript" src="/constant/{{ UiContext.constantVersion }}.js"></script>
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
{% block script %}{% endblock %}
|
||||
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
<div class="row">
|
||||
<div class="medium-12 columns">
|
||||
<div class="section">
|
||||
<div class="section__body no-padding">
|
||||
<div data-fragment-id="status">
|
||||
<div class="section__tab-container">
|
||||
<ul class="section__tabs">
|
||||
<li class="section__tab-item">
|
||||
<h1 class="section__tab-title">{{ _('Servers') }}</h1>
|
||||
<div class="section__tab-main">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--id">
|
||||
@@ -38,6 +41,33 @@
|
||||
</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>
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
<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 %}
|
||||
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数和当前所用编译器版本见状态页。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
|
||||
@@ -196,7 +196,7 @@ $hover-background-color = #424242
|
||||
background-color: #171717
|
||||
color: #999
|
||||
|
||||
pre:not(.vditor-sv), code
|
||||
pre, code
|
||||
background-color: #171717
|
||||
|
||||
.scratchpad__toolbar
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'vditor/dist/index.css';
|
||||
import 'allotment/dist/style.css';
|
||||
import 'pickadate/lib/themes/classic.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 };
|
||||
|
||||
export function tpl(node: React.ReactNode, reactive?: boolean);
|
||||
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]);
|
||||
export function tpl<T extends boolean = false>(node: React.ReactNode, reactive?: T): T extends true ? HTMLDivElement : string;
|
||||
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]): string;
|
||||
export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitutions: Substitution[] | boolean[]) {
|
||||
if (React.isValidElement(pieces)) {
|
||||
if (substitutions[0]) {
|
||||
@@ -49,7 +49,7 @@ export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitut
|
||||
for (let i = 0; i < substitutions.length; ++i) {
|
||||
const subst = substitutions[i];
|
||||
let substHtml: string;
|
||||
if (typeof subst === 'object' && subst.templateRaw) {
|
||||
if (subst && typeof subst === 'object' && subst.templateRaw) {
|
||||
substHtml = subst.html;
|
||||
} else substHtml = _.escape(String(subst));
|
||||
result += substHtml + pieces[i + 1];
|
||||
@@ -82,7 +82,8 @@ export const zIndexManager = {
|
||||
};
|
||||
|
||||
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) => {
|
||||
$
|
||||
.ajax({
|
||||
@@ -96,9 +97,12 @@ export const request = {
|
||||
if (textStatus === 'abort') {
|
||||
const err = new Error(i18n('Aborted')) as any;
|
||||
err.aborted = true;
|
||||
err.isUserFacingError = true;
|
||||
reject(err);
|
||||
} 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) {
|
||||
const { error } = jqXHR.responseJSON;
|
||||
if (error.params) {
|
||||
@@ -117,6 +121,9 @@ export const request = {
|
||||
}
|
||||
})
|
||||
.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';
|
||||
|
||||
String.random = function random(digit = 32, dict = defaultDict) {
|
||||
export function randomstring(digit = 32, dict = defaultDict) {
|
||||
let str = '';
|
||||
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
|
||||
return str;
|
||||
};
|
||||
}
|
||||
try {
|
||||
String.random = randomstring;
|
||||
} catch (e) { } // Cannot add property random, object is not extensible
|
||||
|
||||
String.prototype.format ||= function formatStr(...args) {
|
||||
let result = this;
|
||||
@@ -236,7 +239,7 @@ export function sortFiles(files: Record<string, any>[] | string[], key = '_id')
|
||||
|
||||
interface MatchRule {
|
||||
regex: RegExp;
|
||||
output: ((a: RegExpExecArray) => string)[];
|
||||
output: (a: RegExpExecArray) => string[];
|
||||
id: (a: RegExpExecArray) => number;
|
||||
subtask: (a: RegExpExecArray) => number;
|
||||
preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum';
|
||||
@@ -244,47 +247,35 @@ interface MatchRule {
|
||||
|
||||
const SubtaskMatcher: MatchRule[] = [
|
||||
{
|
||||
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|txt|in\.txt)$/,
|
||||
output: [
|
||||
(a) => `${a[1]}.out`,
|
||||
(a) => `${a[1]}.out.txt`,
|
||||
(a) => `${a[1]}.ans`,
|
||||
(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),
|
||||
],
|
||||
regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/,
|
||||
output: (a) => ['out', 'ans']
|
||||
.flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`])
|
||||
.flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')])
|
||||
.concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null),
|
||||
id: (a) => +a[4],
|
||||
subtask: (a) => +(a[3] || 1),
|
||||
preferredScorerType: (a) => (a[3] ? 'min' : 'sum'),
|
||||
},
|
||||
{
|
||||
regex: /^([^\d]*)\.in(\d+)$/,
|
||||
output: [
|
||||
(a) => `${a[1]}.ou${a[2]}`,
|
||||
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
||||
(a) => `${a[1]}.out${a[2]}`,
|
||||
(a) => `${a[1]}.out${a[2]}`.replace(/input/g, 'output'),
|
||||
],
|
||||
regex: /^([^\d]*)\.(in|IN)(\d+)$/,
|
||||
output: (a) => [
|
||||
`${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`,
|
||||
`${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`,
|
||||
].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]),
|
||||
id: (a) => +a[2],
|
||||
subtask: () => 1,
|
||||
preferredScorerType: () => 'sum',
|
||||
},
|
||||
{
|
||||
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.in$/,
|
||||
output: [
|
||||
(a) => `${a[1]}${a[2]}${a[3]}${a[4]}.out`,
|
||||
(a) => `${a[1]}${a[2]}${a[3]}${a[4]}.ans`,
|
||||
],
|
||||
regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/,
|
||||
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`),
|
||||
id: (a) => +a[4],
|
||||
subtask: (a) => +a[2],
|
||||
preferredScorerType: () => 'min',
|
||||
},
|
||||
{
|
||||
regex: /^(([0-9]+)[-_](?:.*))\.in$/,
|
||||
output: [
|
||||
(a) => `${a[1]}.out`,
|
||||
(a) => `${a[1]}.ans`,
|
||||
],
|
||||
regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/,
|
||||
output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`),
|
||||
id: (a) => +a[2],
|
||||
subtask: () => 1,
|
||||
preferredScorerType: () => 'sum',
|
||||
@@ -329,12 +320,12 @@ export function readSubtasksFromFiles(files: string[], config) {
|
||||
const sid = rule.subtask(data);
|
||||
const c = { input: file, output: '', id: rule.id(data) };
|
||||
const type = rule.preferredScorerType(data);
|
||||
for (const func of rule.output) {
|
||||
if (config.noOutputFile) c.output = '/dev/null';
|
||||
else c.output = func(data);
|
||||
if (c.output === file) continue;
|
||||
if (c.output === '/dev/null' || files.includes(c.output)) {
|
||||
const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i);
|
||||
for (const output of outputs) {
|
||||
if (output === file) continue;
|
||||
if (output === '/dev/null' || files.includes(output)) {
|
||||
match = true;
|
||||
c.output = output;
|
||||
if (!subtask[sid]) {
|
||||
subtask[sid] = {
|
||||
time: config.time,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface LangConfig {
|
||||
comment?: string | [string, string];
|
||||
compile_time_limit?: number;
|
||||
compile_memory_limit?: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export function parseLang(config: string): Record<string, LangConfig> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/utils",
|
||||
"version": "1.4.24",
|
||||
"version": "1.4.25",
|
||||
"description": "hydrooj utils",
|
||||
"main": "lib/utils.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
@@ -16,7 +16,7 @@
|
||||
"mongodb": "^5.9.2",
|
||||
"reggol": "^1.6.3",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"systeminformation": "^5.22.10"
|
||||
"systeminformation": "^5.22.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4"
|
||||
|
||||
Reference in new issue
Block a user