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

+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">