mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
patches from chongqing site
This commit is contained in:
16 files changed
+145
-84
No files matched your search
@@ -6,9 +6,9 @@
|
|||||||
"repository": "https://github.com/hydro-dev/Hydro",
|
"repository": "https://github.com/hydro-dev/Hydro",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antfu/eslint-config": "^6.2.0",
|
"@antfu/eslint-config": "^6.2.0",
|
||||||
"@eslint-react/eslint-plugin": "^2.3.7",
|
"@eslint-react/eslint-plugin": "^2.3.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||||
"@typescript-eslint/parser": "^8.47.0",
|
"@typescript-eslint/parser": "^8.48.0",
|
||||||
"eslint-plugin-de-morgan": "^2.0.0",
|
"eslint-plugin-de-morgan": "^2.0.0",
|
||||||
"eslint-plugin-github": "^6.0.0",
|
"eslint-plugin-github": "^6.0.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@
|
|||||||
"mongodb-memory-server": "10.3.0",
|
"mongodb-memory-server": "10.3.0",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"ora": "^9.0.0",
|
"ora": "^9.0.0",
|
||||||
"oxlint": "^1.29.0",
|
"oxlint": "^1.30.0",
|
||||||
"package-json": "^10.0.1",
|
"package-json": "^10.0.1",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"simple-git": "^3.30.0",
|
"simple-git": "^3.30.0",
|
||||||
|
|||||||
@@ -36,13 +36,13 @@
|
|||||||
"koa-proxies": "^0.12.4",
|
"koa-proxies": "^0.12.4",
|
||||||
"koa-static-cache": "^5.1.4",
|
"koa-static-cache": "^5.1.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^11.2.2",
|
"lru-cache": "^11.2.4",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"mongodb": "^7.0.0",
|
"mongodb": "^7.0.0",
|
||||||
"mongodb-uri": "^0.9.7",
|
"mongodb-uri": "^0.9.7",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.11",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"p-queue": "^9.0.1",
|
"p-queue": "^9.0.1",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
|
|||||||
@@ -485,9 +485,12 @@ export class ContestCodeHandler extends Handler {
|
|||||||
async get(domainId: string, tid: ObjectId, all: boolean) {
|
async get(domainId: string, tid: ObjectId, all: boolean) {
|
||||||
await this.limitRate('contest_code', 60, 10);
|
await this.limitRate('contest_code', 60, 10);
|
||||||
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
|
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
|
||||||
if (!this.user.own(tdoc) && !this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
|
if (!this.user.own(tdoc)) {
|
||||||
|
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
|
||||||
this.checkPerm(PERM.PERM_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)) {
|
if (!contest.canShowRecord.call(this, tdoc as any, true)) {
|
||||||
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
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) {
|
async postDone(domainId: string, tid: ObjectId, bid: ObjectId) {
|
||||||
const balloon = await contest.getBalloon(domainId, tid, bid);
|
const balloon = await contest.getBalloon(domainId, tid, bid);
|
||||||
if (!balloon) throw new ValidationError('balloon');
|
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() });
|
await contest.updateBalloon(domainId, tid, bid, { sent: this.user._id, sentAt: new Date() });
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,15 +124,13 @@ class HomeworkDetailHandler extends Handler {
|
|||||||
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
|
||||||
tsdoc.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;
|
psdict[pdetail.pid] = pdetail;
|
||||||
rdict[pdetail.rid] = { _id: pdetail.rid };
|
rdict[pdetail.rid] = { _id: pdetail.rid };
|
||||||
}
|
}
|
||||||
if (contest.canShowSelfRecord.call(this, this.tdoc) && tsdoc.journal) {
|
if (contest.canShowSelfRecord.call(this, this.tdoc) && valid.length) {
|
||||||
rdict = await record.getList(
|
rdict = await record.getList(domainId, valid.map((pdetail) => pdetail.rid));
|
||||||
domainId,
|
|
||||||
tsdoc.journal.map((pdetail) => pdetail.rid),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(this.response.body, { pdict, psdict, rdict });
|
Object.assign(this.response.body, { pdict, psdict, rdict });
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
|
|||||||
if (rdoc.domainId !== this.args.domainId) return;
|
if (rdoc.domainId !== this.args.domainId) return;
|
||||||
if (!this.pretest && typeof rdoc.input === 'string') return;
|
if (!this.pretest && typeof rdoc.input === 'string') return;
|
||||||
if (!this.all) {
|
if (!this.all) {
|
||||||
|
if (!rdoc.contest && this.tid) return;
|
||||||
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
|
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
|
||||||
if (this.tid && rdoc.contest?.toString() !== '0'.repeat(24)) {
|
if (this.tid && rdoc.contest?.toString() !== '0'.repeat(24)) {
|
||||||
if (rdoc.uid !== this.user._id && !contest.canShowRecord.call(this, this.tdoc, true)) return;
|
if (rdoc.uid !== this.user._id && !contest.canShowRecord.call(this, this.tdoc, true)) return;
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
_, avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync,
|
_, avatar, BadRequestError, ContestModel, ContestNotEndedError, Context, db, findFileSync,
|
||||||
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, PRIV, ProblemConfig, ProblemModel,
|
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, PRIV, ProblemConfig, ProblemModel,
|
||||||
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
|
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
|
||||||
Time, Types, UserModel, Zip,
|
SystemModel, Time, Types, UserModel, Zip,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { ResolverInput } from './interface';
|
import { ResolverInput } from './interface';
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
|||||||
exclude: t.unrank,
|
exclude: t.unrank,
|
||||||
})),
|
})),
|
||||||
submissions: submissions.map((i) => ({
|
submissions: submissions.map((i) => ({
|
||||||
team: i.uid.toString(),
|
team: udict[i.uid]?.seat || i.uid.toString(),
|
||||||
problem: i.pid.toString(),
|
problem: i.pid.toString(),
|
||||||
verdict: STATUS_SHORT_TEXTS[i.status],
|
verdict: STATUS_SHORT_TEXTS[i.status],
|
||||||
time: time(i.rid),
|
time: time(i.rid),
|
||||||
@@ -134,6 +134,11 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
|||||||
async display({ tdoc }) {
|
async display({ tdoc }) {
|
||||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||||
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
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;
|
let token = 0;
|
||||||
const getFeed = (type: string, data: any) => ({
|
const getFeed = (type: string, data: any) => ({
|
||||||
type, id: data.id, data, token: `t${token++}`,
|
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 teams = tsdocs.map((i) => {
|
||||||
const udoc = udict[i.uid];
|
const udoc = udict[i.uid];
|
||||||
return {
|
return {
|
||||||
team_id: `team-${udoc._id}`,
|
team_id: udoc.seat || `team-${udoc._id}`,
|
||||||
name: udoc.uname,
|
name: udoc.uname,
|
||||||
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
|
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
|
||||||
organization: udoc.school || udoc.uname,
|
organization: udoc.school || udoc.uname,
|
||||||
@@ -239,6 +244,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
|||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
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', {
|
...tdoc.pids.map((i, idx) => getFeed('problems', {
|
||||||
id: `${i}`,
|
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');
|
const judgeAt = moment().startOf('day').seconds(judgeDelta).format('HH:mm:ss.SSS');
|
||||||
result.push(getFeed('submissions', {
|
result.push(getFeed('submissions', {
|
||||||
id: s.rid,
|
id: s.rid,
|
||||||
team_id: `team-${i.uid}`,
|
team_id: udict[i.uid]?.seat || `team-${i.uid}`,
|
||||||
problem_id: `${s.pid}`,
|
problem_id: `${s.pid}`,
|
||||||
language_id: s.lang?.split('.')[0] || 'cpp',
|
language_id: s.lang?.split('.')[0] || 'cpp',
|
||||||
files: [],
|
files: [],
|
||||||
@@ -305,13 +317,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
|||||||
]);
|
]);
|
||||||
await Promise.all(teams.map(async (i) => {
|
await Promise.all(teams.map(async (i) => {
|
||||||
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
|
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) => {
|
await Promise.all(organizations.map(async (i) => {
|
||||||
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
|
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
|
||||||
if (!avatarSrc) return;
|
if (!avatarSrc) return;
|
||||||
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
|
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`);
|
this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"repository": "https://github.com/hydro-dev/Hydro",
|
"repository": "https://github.com/hydro-dev/Hydro",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/web": "^10.0.3",
|
"@react-spring/web": "^10.0.3",
|
||||||
"lru-cache": "^11.2.2",
|
"lru-cache": "^11.2.4",
|
||||||
"react-use": "^17.6.0"
|
"react-use": "^17.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import {
|
import {
|
||||||
avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM,
|
avatar, ContestModel, Context, fs, getAlphabeticId, Logger, ObjectId,
|
||||||
RecordDoc, Schema, STATUS, Tdoc, Types, UserModel,
|
PERM, RecordDoc, Schema, STATUS, superagent, Tdoc, Types, UserModel,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
|
|
||||||
|
const logger = new Logger('scoreboard-xcpcio');
|
||||||
|
|
||||||
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
|
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
|
||||||
const indexJs = file.match(/index-([\w-]+)\.js"/)?.[1];
|
const indexJs = file.match(/index-([\w-]+)\.js"/)?.[1];
|
||||||
const indexCss = file.match(/index-([\w-]+)\.css"/)?.[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';
|
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({
|
export const Config = Schema.object({
|
||||||
cacheTTL: Schema.number().default(0).description('Cache TTL in milliseconds'),
|
cacheTTL: Schema.number().default(0).description('Cache TTL in milliseconds'),
|
||||||
cacheSize: Schema.number().default(100).description('Cache size'),
|
cacheSize: Schema.number().default(100).description('Cache size'),
|
||||||
asDefault: Schema.boolean().default(false).description('As default scoreboard'),
|
asDefault: Schema.boolean().default(false).description('As default scoreboard'),
|
||||||
override: Schema.any().default({}).description('Scoreboard contest override'),
|
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>) {
|
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 }) => {
|
ctx.inject(['scoreboard'], ({ scoreboard }) => {
|
||||||
scoreboard.addView('xcpcio', 'XCPCIO', {
|
scoreboard.addView('xcpcio', 'XCPCIO', {
|
||||||
tdoc: 'tdoc',
|
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 (realtime && !this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||||
if (json || this.request.json) {
|
if (json || this.request.json) {
|
||||||
const isLocked = ContestModel.isLocked(tdoc);
|
this.response.body = await getJson(tdoc, realtime, { badge, banner, groups, medals: { gold, silver, bronze } });
|
||||||
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,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
this.response.template = 'xcpcio_board.html';
|
this.response.template = 'xcpcio_board.html';
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/scoreboard-xcpcio",
|
"name": "@hydrooj/scoreboard-xcpcio",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"version": "0.0.5-beta.7",
|
"version": "0.0.5-beta.8",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node -r @hydrooj/register install.ts"
|
"postinstall": "node -r @hydrooj/register install.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xcpcio/board-app": "^0.75.1"
|
"@xcpcio/board-app": "^0.76.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@xcpcio/types": "^0.75.1"
|
"@xcpcio/types": "^0.76.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
window.DATA_REGION = "Hydro";
|
window.DATA_REGION = "Hydro";
|
||||||
window.DEFAULT_LANG = "{{ 'zh-CN' if _('__id').startsWith('zh') else 'en' }}";
|
window.DEFAULT_LANG = "{{ 'zh-CN' if _('__id').startsWith('zh') else 'en' }}";
|
||||||
window.DATA_SOURCE = "{{ dataSource|safe }}";
|
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);
|
if (document.documentElement.classList.contains("theme--dark")) document.documentElement.classList.toggle("dark", !0);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const location = window.location.href;
|
const location = window.location.href;
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="{{ '/' if process.env.DEV else UiContext.cdn_prefix }}assets/index-{{ js }}.js"></script>
|
<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">
|
<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="main">
|
||||||
<div class="row typo">
|
<div class="row typo">
|
||||||
<blockquote class="note">
|
<blockquote class="note">
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ My Files: 我的文件
|
|||||||
My Profile: 我的资料
|
My Profile: 我的资料
|
||||||
My Recent Submissions: 我的最近递交记录
|
My Recent Submissions: 我的最近递交记录
|
||||||
My Role: 我的角色
|
My Role: 我的角色
|
||||||
|
My Submissions: 我的递交
|
||||||
name: 名称
|
name: 名称
|
||||||
Name: 名称
|
Name: 名称
|
||||||
Nav Logo Dark: 左上角小图标(暗)
|
Nav Logo Dark: 左上角小图标(暗)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"@mantine/core": "^8.3.9",
|
"@mantine/core": "^8.3.9",
|
||||||
"@mantine/hooks": "^8.3.9",
|
"@mantine/hooks": "^8.3.9",
|
||||||
"@mantine/notifications": "^8.3.9",
|
"@mantine/notifications": "^8.3.9",
|
||||||
"@sentry/browser": "^10.26.0",
|
"@sentry/browser": "^10.27.0",
|
||||||
"@sentry/webpack-plugin": "^4.6.1",
|
"@sentry/webpack-plugin": "^4.6.1",
|
||||||
"@simplewebauthn/browser": "13.2.2",
|
"@simplewebauthn/browser": "13.2.2",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@types/pickadate": "^3.5.35",
|
"@types/pickadate": "^3.5.35",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/redux-logger": "^3.0.13",
|
"@types/redux-logger": "^3.0.13",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"monaco-editor": "0.52.2",
|
"monaco-editor": "0.52.2",
|
||||||
"monaco-editor-nls": "^3.1.0",
|
"monaco-editor-nls": "^3.1.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.1.1",
|
"monaco-editor-webpack-plugin": "^7.1.1",
|
||||||
"monaco-themes": "^0.4.7",
|
"monaco-themes": "0.4.7",
|
||||||
"monaco-yaml": "5.1.1",
|
"monaco-yaml": "5.1.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const page = new NamedPage('domain_user', () => {
|
|||||||
options: UiContext.roles.filter((i) => !['default', 'guest'].includes(i)),
|
options: UiContext.roles.filter((i) => !['default', 'guest'].includes(i)),
|
||||||
columns: -6,
|
columns: -6,
|
||||||
},
|
},
|
||||||
...(UiContext.canForceJoin ? {
|
...((UiContext.canForceJoin && UiContext.domain._id !== 'system') ? {
|
||||||
join: {
|
join: {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: i18n('Mark user as joined using admin privilege'),
|
label: i18n('Mark user as joined using admin privilege'),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<span style="color: #66d9ef">return</span> <span style="color: #ae81ff">0</span><span style="color: #f8f8f2">;</span>
|
<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: #f8f8f2">}</span>
|
||||||
<span style="color: #75715e">/*</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.message }}</span>
|
||||||
<span style="color: #75715e">{{ error.stack }}</span>
|
<span style="color: #75715e">{{ error.stack }}</span>
|
||||||
<span style="color: #75715e">*/</span>
|
<span style="color: #75715e">*/</span>
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user