patches from chongqing site

This commit is contained in:
undefined committed 2025-12-02 00:28:58 +08:00
1 parent b530a8dc00
commit 956631680b
16 files changed
+146 -85

No files matched your search

+3 -3
View File
@@ -6,9 +6,9 @@
"repository": "https://github.com/hydro-dev/Hydro",
"dependencies": {
"@antfu/eslint-config": "^6.2.0",
"@eslint-react/eslint-plugin": "^2.3.7",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"@eslint-react/eslint-plugin": "^2.3.9",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"eslint-plugin-de-morgan": "^2.0.0",
"eslint-plugin-github": "^6.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
+1 -1
View File
@@ -53,7 +53,7 @@
"mongodb-memory-server": "10.3.0",
"nyc": "^17.1.0",
"ora": "^9.0.0",
"oxlint": "^1.29.0",
"oxlint": "^1.30.0",
"package-json": "^10.0.1",
"semver": "^7.7.3",
"simple-git": "^3.30.0",
+2 -2
View File
@@ -36,13 +36,13 @@
"koa-proxies": "^0.12.4",
"koa-static-cache": "^5.1.4",
"lodash": "^4.17.21",
"lru-cache": "^11.2.2",
"lru-cache": "^11.2.4",
"mime-types": "^3.0.2",
"moment-timezone": "^0.6.0",
"mongodb": "^7.0.0",
"mongodb-uri": "^0.9.7",
"nanoid": "^5.1.6",
"nodemailer": "^7.0.10",
"nodemailer": "^7.0.11",
"notp": "^2.0.3",
"p-queue": "^9.0.1",
"sanitize-filename": "^1.6.3",
+6 -3
View File
@@ -485,8 +485,11 @@ export class ContestCodeHandler extends Handler {
async get(domainId: string, tid: ObjectId, all: boolean) {
await this.limitRate('contest_code', 60, 10);
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
if (!this.user.own(tdoc) && !this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
if (!this.user.own(tdoc)) {
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
}
if (!contest.isDone(tdoc)) throw new ContestNotEndedError(domainId, tid);
}
if (!contest.canShowRecord.call(this, tdoc as any, true)) {
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
@@ -754,7 +757,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler {
async postDone(domainId: string, tid: ObjectId, bid: ObjectId) {
const balloon = await contest.getBalloon(domainId, tid, bid);
if (!balloon) throw new ValidationError('balloon');
if (balloon.sent) throw new ValidationError('balloon', null, 'Balloon already sent');
if (balloon.sent) throw new ValidationError('Balloon already sent');
await contest.updateBalloon(domainId, tid, bid, { sent: this.user._id, sentAt: new Date() });
this.back();
}
+4 -6
View File
@@ -124,15 +124,13 @@ class HomeworkDetailHandler extends Handler {
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
tsdoc.startAt = new Date();
}
for (const pdetail of tsdoc.journal || []) {
const valid = (tsdoc.journal || []).filter((p) => this.tdoc.pids.includes(p.pid));
for (const pdetail of valid) {
psdict[pdetail.pid] = pdetail;
rdict[pdetail.rid] = { _id: pdetail.rid };
}
if (contest.canShowSelfRecord.call(this, this.tdoc) && tsdoc.journal) {
rdict = await record.getList(
domainId,
tsdoc.journal.map((pdetail) => pdetail.rid),
);
if (contest.canShowSelfRecord.call(this, this.tdoc) && valid.length) {
rdict = await record.getList(domainId, valid.map((pdetail) => pdetail.rid));
}
}
Object.assign(this.response.body, { pdict, psdict, rdict });
+1
View File
@@ -331,6 +331,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
if (rdoc.domainId !== this.args.domainId) return;
if (!this.pretest && typeof rdoc.input === 'string') return;
if (!this.all) {
if (!rdoc.contest && this.tid) return;
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
if (this.tid && rdoc.contest?.toString() !== '0'.repeat(24)) {
if (rdoc.uid !== this.user._id && !contest.canShowRecord.call(this, this.tdoc, true)) return;
+19 -7
View File
@@ -2,10 +2,10 @@
import { LRUCache } from 'lru-cache';
import moment from 'moment';
import {
_, avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync,
_, avatar, BadRequestError, ContestModel, ContestNotEndedError, Context, db, findFileSync,
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, PRIV, ProblemConfig, ProblemModel,
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
Time, Types, UserModel, Zip,
SystemModel, Time, Types, UserModel, Zip,
} from 'hydrooj';
import { ResolverInput } from './interface';
@@ -118,7 +118,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
exclude: t.unrank,
})),
submissions: submissions.map((i) => ({
team: i.uid.toString(),
team: udict[i.uid]?.seat || i.uid.toString(),
problem: i.pid.toString(),
verdict: STATUS_SHORT_TEXTS[i.status],
time: time(i.rid),
@@ -134,6 +134,11 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
async display({ tdoc }) {
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
try {
new URL(SystemModel.get('server.url')); // eslint-disable-line no-new
} catch (e) {
throw new BadRequestError('Server URL not set');
}
let token = 0;
const getFeed = (type: string, data: any) => ({
type, id: data.id, data, token: `t${token++}`,
@@ -146,7 +151,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
const teams = tsdocs.map((i) => {
const udoc = udict[i.uid];
return {
team_id: `team-${udoc._id}`,
team_id: udoc.seat || `team-${udoc._id}`,
name: udoc.uname,
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
organization: udoc.school || udoc.uname,
@@ -239,6 +244,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
width: 1920,
height: 1080,
}],
logo: [{
href: new URL(i.avatar, SystemModel.get('server.url')).toString(),
filename: 'logo.webp',
mime: 'image/webp',
width: 128,
height: 128,
}],
})),
...tdoc.pids.map((i, idx) => getFeed('problems', {
id: `${i}`,
@@ -265,7 +277,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
const judgeAt = moment().startOf('day').seconds(judgeDelta).format('HH:mm:ss.SSS');
result.push(getFeed('submissions', {
id: s.rid,
team_id: `team-${i.uid}`,
team_id: udict[i.uid]?.seat || `team-${i.uid}`,
problem_id: `${s.pid}`,
language_id: s.lang?.split('.')[0] || 'cpp',
files: [],
@@ -305,13 +317,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
]);
await Promise.all(teams.map(async (i) => {
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
await zip.add(`teams/${i.team_id}/photo.download.txt`, new Zip.TextReader(i.avatar));
await zip.add(`teams/${i.team_id}/photo.url`, new Zip.TextReader(`URL=${i.avatar}`));
}));
await Promise.all(organizations.map(async (i) => {
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
if (!avatarSrc) return;
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
await zip.add(`organizations/${orgId[i]}/photo.download.txt`, new Zip.TextReader(avatar(avatarSrc)));
await zip.add(`organizations/${orgId[i]}/photo.url`, new Zip.TextReader(`URL=${avatar(avatarSrc)}`));
}));
this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`);
},
+1 -1
View File
@@ -4,7 +4,7 @@
"repository": "https://github.com/hydro-dev/Hydro",
"dependencies": {
"@react-spring/web": "^10.0.3",
"lru-cache": "^11.2.2",
"lru-cache": "^11.2.4",
"react-use": "^17.6.0"
}
}
+96 -52
View File
@@ -1,10 +1,12 @@
import path from 'path';
import { LRUCache } from 'lru-cache';
import {
avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM,
RecordDoc, Schema, STATUS, Tdoc, Types, UserModel,
avatar, ContestModel, Context, fs, getAlphabeticId, Logger, ObjectId,
PERM, RecordDoc, Schema, STATUS, superagent, Tdoc, Types, UserModel,
} from 'hydrooj';
const logger = new Logger('scoreboard-xcpcio');
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
const indexJs = file.match(/index-([\w-]+)\.js"/)?.[1];
const indexCss = file.match(/index-([\w-]+)\.css"/)?.[1];
@@ -88,11 +90,28 @@ async function loadContestState(tdoc: Tdoc, realtime: boolean) {
}
export const name = 'scoreboard-xcpcio';
const PublishConfig = Schema.object({
domainId: Schema.string().required(),
contestId: Schema.string().required(),
publishToken: Schema.string().required(),
publishPath: Schema.string().required(),
publishEndpoint: Schema.string().default('https://scoreboard.hydrooj.com/_publish'),
medals: Schema.object({
gold: Schema.number().required(),
silver: Schema.number().required(),
bronze: Schema.number().required(),
}),
banner: Schema.string().default(''),
badge: Schema.boolean().default(false),
override: Schema.any().default({}).description('Scoreboard contest override'),
});
export const Config = Schema.object({
cacheTTL: Schema.number().default(0).description('Cache TTL in milliseconds'),
cacheSize: Schema.number().default(100).description('Cache size'),
asDefault: Schema.boolean().default(false).description('As default scoreboard'),
override: Schema.any().default({}).description('Scoreboard contest override'),
publish: Schema.array(PublishConfig).description('Scoreboard publish config'),
});
export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
@@ -138,6 +157,80 @@ export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
});
}
const getJson = async (tdoc, realtime: boolean, cfg: Partial<ReturnType<typeof PublishConfig>>) => {
const isLocked = ContestModel.isLocked(tdoc);
const cacheKey = `${tdoc.docId.toHexString()}/${(isLocked && realtime) ? 'realtime' : 'public'}`;
const state = lru.get(cacheKey) || await loadContestState(tdoc, realtime);
if (cfg.cacheTTL) lru.set(cacheKey, state);
const relatedGroups = state.teams.flatMap((i) => i.group);
return {
contest: {
contest_name: tdoc.title,
start_time: Math.floor(tdoc.beginAt.getTime() / 1000),
end_time: Math.floor(tdoc.endAt.getTime() / 1000),
frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0,
penalty: 1200,
problem_quantity: tdoc.pids.length,
problem_id: tdoc.pids.map((i, idx) => getAlphabeticId(idx)),
group: {
official: '正式队伍',
unofficial: '打星队伍',
...Object.fromEntries(cfg.groups?.filter((i) => relatedGroups.includes(i.name)).map((i) => [i.name, i.name]) || []),
},
...(cfg.badge ? { badge: 'Badge' } : {}),
organization: 'School',
status_time_display: {
correct: true,
incorrect: true,
pending: true,
},
medal: {
official: cfg.medals,
},
balloon_color: tdoc.balloon
? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({
color: '#000',
background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color,
}))
: [],
logo: {
preset: 'ICPC',
},
...(cfg.banner ? { banner: { url: 'banner.jpg' } } : {}),
options: {
submission_timestamp_unit: 'millisecond',
},
...(typeof cfg.override === 'object' ? cfg.override || {} : {}),
},
...state,
};
};
if (config.publish?.length && process.env.NODE_APP_INSTANCE === '0') {
const done = [];
const unlocked = [];
logger.debug('Will publish scoreboards', config.publish);
ctx.effect(() => ctx.setInterval(() => {
Promise.allSettled(config.publish.map(async (i) => {
const key = `${i.domainId}/${i.contestId}`;
if (unlocked.includes(key)) return;
const tdoc = await ContestModel.get(i.domainId, new ObjectId(i.contestId));
if (ContestModel.isDone(tdoc) && ContestModel.isLocked(tdoc) && done.includes(key)) return;
if (ContestModel.isDone(tdoc) && !ContestModel.isLocked(tdoc)) unlocked.push(key);
if (ContestModel.isDone(tdoc)) done.push(key);
const groups = await UserModel.listGroup(i.domainId);
const json = await getJson(tdoc, false, { ...i, groups });
logger.info(`Publishing scoreboard ${i.domainId}/${i.contestId} to ${i.publishEndpoint}`);
const res = await superagent.post(i.publishEndpoint).send({
path: i.publishPath,
token: i.publishToken,
json,
});
logger.info(`Published scoreboard ${i.domainId}/${i.contestId} to ${i.publishEndpoint}`, res.body);
})).catch(console.error);
}, 30000));
}
ctx.inject(['scoreboard'], ({ scoreboard }) => {
scoreboard.addView('xcpcio', 'XCPCIO', {
tdoc: 'tdoc',
@@ -155,56 +248,7 @@ export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
}) {
if (realtime && !this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (json || this.request.json) {
const isLocked = ContestModel.isLocked(tdoc);
const cacheKey = `${tdoc.docId.toHexString()}/${(isLocked && realtime) ? 'realtime' : 'public'}`;
const state = lru.get(cacheKey) || await loadContestState(tdoc, realtime);
if (config.cacheTTL) lru.set(cacheKey, state);
const relatedGroups = state.teams.flatMap((i) => i.group);
this.response.body = {
contest: {
contest_name: tdoc.title,
start_time: Math.floor(tdoc.beginAt.getTime() / 1000),
end_time: Math.floor(tdoc.endAt.getTime() / 1000),
frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0,
penalty: 1200,
problem_quantity: tdoc.pids.length,
problem_id: tdoc.pids.map((i, idx) => getAlphabeticId(idx)),
group: {
official: '正式队伍',
unofficial: '打星队伍',
...Object.fromEntries(groups.filter((i) => relatedGroups.includes(i.name)).map((i) => [i.name, i.name])),
},
...(badge ? { badge: 'Badge' } : {}),
organization: 'School',
status_time_display: {
correct: true,
incorrect: true,
pending: true,
},
medal: {
official: {
gold,
silver,
bronze,
},
},
balloon_color: tdoc.balloon
? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({
color: '#000',
background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color,
}))
: [],
logo: {
preset: 'ICPC',
},
...(banner ? { banner: { url: 'banner.jpg' } } : {}),
options: {
submission_timestamp_unit: 'millisecond',
},
...(typeof config.override === 'object' ? config.override || {} : {}),
},
...state,
};
this.response.body = await getJson(tdoc, realtime, { badge, banner, groups, medals: { gold, silver, bronze } });
} else {
this.response.template = 'xcpcio_board.html';
let query = '';
+3 -3
View File
@@ -1,16 +1,16 @@
{
"name": "@hydrooj/scoreboard-xcpcio",
"license": "AGPL-3.0-or-later",
"version": "0.0.5-beta.7",
"version": "0.0.5-beta.8",
"main": "index.ts",
"repository": "https://github.com/hydro-dev/Hydro.git",
"scripts": {
"postinstall": "node -r @hydrooj/register install.ts"
},
"dependencies": {
"@xcpcio/board-app": "^0.75.1"
"@xcpcio/board-app": "^0.76.0"
},
"devDependencies": {
"@xcpcio/types": "^0.75.1"
"@xcpcio/types": "^0.76.0"
}
}
@@ -12,6 +12,7 @@
window.DATA_REGION = "Hydro";
window.DEFAULT_LANG = "{{ 'zh-CN' if _('__id').startsWith('zh') else 'en' }}";
window.DATA_SOURCE = "{{ dataSource|safe }}";
{% if refreshInterval %}window.REFRESH_INTERVAL = {{ refreshInterval }};{% endif %}
if (document.documentElement.classList.contains("theme--dark")) document.documentElement.classList.toggle("dark", !0);
setInterval(() => {
const location = window.location.href;
@@ -30,7 +31,7 @@
</style>
<script type="module" crossorigin src="{{ '/' if process.env.DEV else UiContext.cdn_prefix }}assets/index-{{ js }}.js"></script>
<link rel="stylesheet" crossorigin href="{{ '/' if process.env.DEV else UiContext.cdn_prefix }}assets/index-{{ css }}.css">
{% if realtime and model.contest.isLocked(tdoc) and model.contest.isDone(tdoc) and handler.user.own(tdoc) %}
{% if tdoc and realtime and model.contest.isLocked(tdoc) and model.contest.isDone(tdoc) and handler.user.own(tdoc) %}
<div class="main">
<div class="row typo">
<blockquote class="note">
+1
View File
@@ -587,6 +587,7 @@ My Files: 我的文件
My Profile: 我的资料
My Recent Submissions: 我的最近递交记录
My Role: 我的角色
My Submissions: 我的递交
name: 名称
Name: 名称
Nav Logo Dark: 左上角小图标(暗)
+3 -3
View File
@@ -23,7 +23,7 @@
"@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@mantine/notifications": "^8.3.9",
"@sentry/browser": "^10.26.0",
"@sentry/browser": "^10.27.0",
"@sentry/webpack-plugin": "^4.6.1",
"@simplewebauthn/browser": "13.2.2",
"@svgr/webpack": "^8.1.0",
@@ -36,7 +36,7 @@
"@types/nunjucks": "^3.2.6",
"@types/pickadate": "^3.5.35",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/redux-logger": "^3.0.13",
"@types/uuid": "^11.0.0",
@@ -75,7 +75,7 @@
"monaco-editor": "0.52.2",
"monaco-editor-nls": "^3.1.0",
"monaco-editor-webpack-plugin": "^7.1.1",
"monaco-themes": "^0.4.7",
"monaco-themes": "0.4.7",
"monaco-yaml": "5.1.1",
"nanoid": "^5.1.6",
"normalize.css": "^8.0.1",
@@ -38,7 +38,7 @@ const page = new NamedPage('domain_user', () => {
options: UiContext.roles.filter((i) => !['default', 'guest'].includes(i)),
columns: -6,
},
...(UiContext.canForceJoin ? {
...((UiContext.canForceJoin && UiContext.domain._id !== 'system') ? {
join: {
type: 'checkbox',
label: i18n('Mark user as joined using admin privilege'),
+1
View File
@@ -25,6 +25,7 @@
<span style="color: #66d9ef">return</span> <span style="color: #ae81ff">0</span><span style="color: #f8f8f2">;</span>
<span style="color: #f8f8f2">}</span>
<span style="color: #75715e">/*</span>
<span style="color: #75715e">{{ handler.request.hostname }}{{ handler.request.originalPath|default(handler.request.path) }}</span>
<span style="color: #75715e">{{ error.message }}</span>
<span style="color: #75715e">{{ error.stack }}</span>
<span style="color: #75715e">*/</span>
+2 -2
View File
@@ -13,9 +13,9 @@
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/react": "^19.2.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.2.2",
"vite": "^7.2.4"
"vite": "^7.2.6"
}
}