Skip to content

Commit 9566316

Browse files
committed
patches from chongqing site
1 parent b530a8d commit 9566316

16 files changed

Lines changed: 146 additions & 85 deletions

File tree

framework/eslint-config/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
"repository": "https://github.com/hydro-dev/Hydro",
77
"dependencies": {
88
"@antfu/eslint-config": "^6.2.0",
9-
"@eslint-react/eslint-plugin": "^2.3.7",
10-
"@typescript-eslint/eslint-plugin": "^8.47.0",
11-
"@typescript-eslint/parser": "^8.47.0",
9+
"@eslint-react/eslint-plugin": "^2.3.9",
10+
"@typescript-eslint/eslint-plugin": "^8.48.0",
11+
"@typescript-eslint/parser": "^8.48.0",
1212
"eslint-plugin-de-morgan": "^2.0.0",
1313
"eslint-plugin-github": "^6.0.0",
1414
"eslint-plugin-react-hooks": "^7.0.1",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"mongodb-memory-server": "10.3.0",
5454
"nyc": "^17.1.0",
5555
"ora": "^9.0.0",
56-
"oxlint": "^1.29.0",
56+
"oxlint": "^1.30.0",
5757
"package-json": "^10.0.1",
5858
"semver": "^7.7.3",
5959
"simple-git": "^3.30.0",

packages/hydrooj/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@
3636
"koa-proxies": "^0.12.4",
3737
"koa-static-cache": "^5.1.4",
3838
"lodash": "^4.17.21",
39-
"lru-cache": "^11.2.2",
39+
"lru-cache": "^11.2.4",
4040
"mime-types": "^3.0.2",
4141
"moment-timezone": "^0.6.0",
4242
"mongodb": "^7.0.0",
4343
"mongodb-uri": "^0.9.7",
4444
"nanoid": "^5.1.6",
45-
"nodemailer": "^7.0.10",
45+
"nodemailer": "^7.0.11",
4646
"notp": "^2.0.3",
4747
"p-queue": "^9.0.1",
4848
"sanitize-filename": "^1.6.3",

packages/hydrooj/src/handler/contest.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,11 @@ export class ContestCodeHandler extends Handler {
485485
async get(domainId: string, tid: ObjectId, all: boolean) {
486486
await this.limitRate('contest_code', 60, 10);
487487
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
488-
if (!this.user.own(tdoc) && !this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
489-
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
488+
if (!this.user.own(tdoc)) {
489+
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
490+
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
491+
}
492+
if (!contest.isDone(tdoc)) throw new ContestNotEndedError(domainId, tid);
490493
}
491494
if (!contest.canShowRecord.call(this, tdoc as any, true)) {
492495
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
@@ -754,7 +757,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler {
754757
async postDone(domainId: string, tid: ObjectId, bid: ObjectId) {
755758
const balloon = await contest.getBalloon(domainId, tid, bid);
756759
if (!balloon) throw new ValidationError('balloon');
757-
if (balloon.sent) throw new ValidationError('balloon', null, 'Balloon already sent');
760+
if (balloon.sent) throw new ValidationError('Balloon already sent');
758761
await contest.updateBalloon(domainId, tid, bid, { sent: this.user._id, sentAt: new Date() });
759762
this.back();
760763
}

packages/hydrooj/src/handler/homework.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,13 @@ class HomeworkDetailHandler extends Handler {
124124
await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() });
125125
tsdoc.startAt = new Date();
126126
}
127-
for (const pdetail of tsdoc.journal || []) {
127+
const valid = (tsdoc.journal || []).filter((p) => this.tdoc.pids.includes(p.pid));
128+
for (const pdetail of valid) {
128129
psdict[pdetail.pid] = pdetail;
129130
rdict[pdetail.rid] = { _id: pdetail.rid };
130131
}
131-
if (contest.canShowSelfRecord.call(this, this.tdoc) && tsdoc.journal) {
132-
rdict = await record.getList(
133-
domainId,
134-
tsdoc.journal.map((pdetail) => pdetail.rid),
135-
);
132+
if (contest.canShowSelfRecord.call(this, this.tdoc) && valid.length) {
133+
rdict = await record.getList(domainId, valid.map((pdetail) => pdetail.rid));
136134
}
137135
}
138136
Object.assign(this.response.body, { pdict, psdict, rdict });

packages/hydrooj/src/handler/record.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
331331
if (rdoc.domainId !== this.args.domainId) return;
332332
if (!this.pretest && typeof rdoc.input === 'string') return;
333333
if (!this.all) {
334+
if (!rdoc.contest && this.tid) return;
334335
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
335336
if (this.tid && rdoc.contest?.toString() !== '0'.repeat(24)) {
336337
if (rdoc.uid !== this.user._id && !contest.canShowRecord.call(this, this.tdoc, true)) return;

packages/onsite-toolkit/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import { LRUCache } from 'lru-cache';
33
import moment from 'moment';
44
import {
5-
_, avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync,
5+
_, avatar, BadRequestError, ContestModel, ContestNotEndedError, Context, db, findFileSync,
66
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, PRIV, ProblemConfig, ProblemModel,
77
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
8-
Time, Types, UserModel, Zip,
8+
SystemModel, Time, Types, UserModel, Zip,
99
} from 'hydrooj';
1010
import { ResolverInput } from './interface';
1111

@@ -118,7 +118,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
118118
exclude: t.unrank,
119119
})),
120120
submissions: submissions.map((i) => ({
121-
team: i.uid.toString(),
121+
team: udict[i.uid]?.seat || i.uid.toString(),
122122
problem: i.pid.toString(),
123123
verdict: STATUS_SHORT_TEXTS[i.status],
124124
time: time(i.rid),
@@ -134,6 +134,11 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
134134
async display({ tdoc }) {
135135
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
136136
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
137+
try {
138+
new URL(SystemModel.get('server.url')); // eslint-disable-line no-new
139+
} catch (e) {
140+
throw new BadRequestError('Server URL not set');
141+
}
137142
let token = 0;
138143
const getFeed = (type: string, data: any) => ({
139144
type, id: data.id, data, token: `t${token++}`,
@@ -146,7 +151,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
146151
const teams = tsdocs.map((i) => {
147152
const udoc = udict[i.uid];
148153
return {
149-
team_id: `team-${udoc._id}`,
154+
team_id: udoc.seat || `team-${udoc._id}`,
150155
name: udoc.uname,
151156
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
152157
organization: udoc.school || udoc.uname,
@@ -239,6 +244,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
239244
width: 1920,
240245
height: 1080,
241246
}],
247+
logo: [{
248+
href: new URL(i.avatar, SystemModel.get('server.url')).toString(),
249+
filename: 'logo.webp',
250+
mime: 'image/webp',
251+
width: 128,
252+
height: 128,
253+
}],
242254
})),
243255
...tdoc.pids.map((i, idx) => getFeed('problems', {
244256
id: `${i}`,
@@ -265,7 +277,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
265277
const judgeAt = moment().startOf('day').seconds(judgeDelta).format('HH:mm:ss.SSS');
266278
result.push(getFeed('submissions', {
267279
id: s.rid,
268-
team_id: `team-${i.uid}`,
280+
team_id: udict[i.uid]?.seat || `team-${i.uid}`,
269281
problem_id: `${s.pid}`,
270282
language_id: s.lang?.split('.')[0] || 'cpp',
271283
files: [],
@@ -305,13 +317,13 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
305317
]);
306318
await Promise.all(teams.map(async (i) => {
307319
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
308-
await zip.add(`teams/${i.team_id}/photo.download.txt`, new Zip.TextReader(i.avatar));
320+
await zip.add(`teams/${i.team_id}/photo.url`, new Zip.TextReader(`URL=${i.avatar}`));
309321
}));
310322
await Promise.all(organizations.map(async (i) => {
311323
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
312324
if (!avatarSrc) return;
313325
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
314-
await zip.add(`organizations/${orgId[i]}/photo.download.txt`, new Zip.TextReader(avatar(avatarSrc)));
326+
await zip.add(`organizations/${orgId[i]}/photo.url`, new Zip.TextReader(`URL=${avatar(avatarSrc)}`));
315327
}));
316328
this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`);
317329
},

packages/onsite-toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"repository": "https://github.com/hydro-dev/Hydro",
55
"dependencies": {
66
"@react-spring/web": "^10.0.3",
7-
"lru-cache": "^11.2.2",
7+
"lru-cache": "^11.2.4",
88
"react-use": "^17.6.0"
99
}
1010
}

packages/scoreboard-xcpcio/index.ts

Lines changed: 96 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import path from 'path';
22
import { LRUCache } from 'lru-cache';
33
import {
4-
avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM,
5-
RecordDoc, Schema, STATUS, Tdoc, Types, UserModel,
4+
avatar, ContestModel, Context, fs, getAlphabeticId, Logger, ObjectId,
5+
PERM, RecordDoc, Schema, STATUS, superagent, Tdoc, Types, UserModel,
66
} from 'hydrooj';
77

8+
const logger = new Logger('scoreboard-xcpcio');
9+
810
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
911
const indexJs = file.match(/index-([\w-]+)\.js"/)?.[1];
1012
const indexCss = file.match(/index-([\w-]+)\.css"/)?.[1];
@@ -88,11 +90,28 @@ async function loadContestState(tdoc: Tdoc, realtime: boolean) {
8890
}
8991

9092
export const name = 'scoreboard-xcpcio';
93+
94+
const PublishConfig = Schema.object({
95+
domainId: Schema.string().required(),
96+
contestId: Schema.string().required(),
97+
publishToken: Schema.string().required(),
98+
publishPath: Schema.string().required(),
99+
publishEndpoint: Schema.string().default('https://scoreboard.hydrooj.com/_publish'),
100+
medals: Schema.object({
101+
gold: Schema.number().required(),
102+
silver: Schema.number().required(),
103+
bronze: Schema.number().required(),
104+
}),
105+
banner: Schema.string().default(''),
106+
badge: Schema.boolean().default(false),
107+
override: Schema.any().default({}).description('Scoreboard contest override'),
108+
});
91109
export const Config = Schema.object({
92110
cacheTTL: Schema.number().default(0).description('Cache TTL in milliseconds'),
93111
cacheSize: Schema.number().default(100).description('Cache size'),
94112
asDefault: Schema.boolean().default(false).description('As default scoreboard'),
95113
override: Schema.any().default({}).description('Scoreboard contest override'),
114+
publish: Schema.array(PublishConfig).description('Scoreboard publish config'),
96115
});
97116

98117
export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
@@ -138,6 +157,80 @@ export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
138157
});
139158
}
140159

160+
const getJson = async (tdoc, realtime: boolean, cfg: Partial<ReturnType<typeof PublishConfig>>) => {
161+
const isLocked = ContestModel.isLocked(tdoc);
162+
const cacheKey = `${tdoc.docId.toHexString()}/${(isLocked && realtime) ? 'realtime' : 'public'}`;
163+
const state = lru.get(cacheKey) || await loadContestState(tdoc, realtime);
164+
if (cfg.cacheTTL) lru.set(cacheKey, state);
165+
const relatedGroups = state.teams.flatMap((i) => i.group);
166+
return {
167+
contest: {
168+
contest_name: tdoc.title,
169+
start_time: Math.floor(tdoc.beginAt.getTime() / 1000),
170+
end_time: Math.floor(tdoc.endAt.getTime() / 1000),
171+
frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0,
172+
penalty: 1200,
173+
problem_quantity: tdoc.pids.length,
174+
problem_id: tdoc.pids.map((i, idx) => getAlphabeticId(idx)),
175+
group: {
176+
official: '正式队伍',
177+
unofficial: '打星队伍',
178+
...Object.fromEntries(cfg.groups?.filter((i) => relatedGroups.includes(i.name)).map((i) => [i.name, i.name]) || []),
179+
},
180+
...(cfg.badge ? { badge: 'Badge' } : {}),
181+
organization: 'School',
182+
status_time_display: {
183+
correct: true,
184+
incorrect: true,
185+
pending: true,
186+
},
187+
medal: {
188+
official: cfg.medals,
189+
},
190+
balloon_color: tdoc.balloon
191+
? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({
192+
color: '#000',
193+
background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color,
194+
}))
195+
: [],
196+
logo: {
197+
preset: 'ICPC',
198+
},
199+
...(cfg.banner ? { banner: { url: 'banner.jpg' } } : {}),
200+
options: {
201+
submission_timestamp_unit: 'millisecond',
202+
},
203+
...(typeof cfg.override === 'object' ? cfg.override || {} : {}),
204+
},
205+
...state,
206+
};
207+
};
208+
209+
if (config.publish?.length && process.env.NODE_APP_INSTANCE === '0') {
210+
const done = [];
211+
const unlocked = [];
212+
logger.debug('Will publish scoreboards', config.publish);
213+
ctx.effect(() => ctx.setInterval(() => {
214+
Promise.allSettled(config.publish.map(async (i) => {
215+
const key = `${i.domainId}/${i.contestId}`;
216+
if (unlocked.includes(key)) return;
217+
const tdoc = await ContestModel.get(i.domainId, new ObjectId(i.contestId));
218+
if (ContestModel.isDone(tdoc) && ContestModel.isLocked(tdoc) && done.includes(key)) return;
219+
if (ContestModel.isDone(tdoc) && !ContestModel.isLocked(tdoc)) unlocked.push(key);
220+
if (ContestModel.isDone(tdoc)) done.push(key);
221+
const groups = await UserModel.listGroup(i.domainId);
222+
const json = await getJson(tdoc, false, { ...i, groups });
223+
logger.info(`Publishing scoreboard ${i.domainId}/${i.contestId} to ${i.publishEndpoint}`);
224+
const res = await superagent.post(i.publishEndpoint).send({
225+
path: i.publishPath,
226+
token: i.publishToken,
227+
json,
228+
});
229+
logger.info(`Published scoreboard ${i.domainId}/${i.contestId} to ${i.publishEndpoint}`, res.body);
230+
})).catch(console.error);
231+
}, 30000));
232+
}
233+
141234
ctx.inject(['scoreboard'], ({ scoreboard }) => {
142235
scoreboard.addView('xcpcio', 'XCPCIO', {
143236
tdoc: 'tdoc',
@@ -155,56 +248,7 @@ export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
155248
}) {
156249
if (realtime && !this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
157250
if (json || this.request.json) {
158-
const isLocked = ContestModel.isLocked(tdoc);
159-
const cacheKey = `${tdoc.docId.toHexString()}/${(isLocked && realtime) ? 'realtime' : 'public'}`;
160-
const state = lru.get(cacheKey) || await loadContestState(tdoc, realtime);
161-
if (config.cacheTTL) lru.set(cacheKey, state);
162-
const relatedGroups = state.teams.flatMap((i) => i.group);
163-
this.response.body = {
164-
contest: {
165-
contest_name: tdoc.title,
166-
start_time: Math.floor(tdoc.beginAt.getTime() / 1000),
167-
end_time: Math.floor(tdoc.endAt.getTime() / 1000),
168-
frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0,
169-
penalty: 1200,
170-
problem_quantity: tdoc.pids.length,
171-
problem_id: tdoc.pids.map((i, idx) => getAlphabeticId(idx)),
172-
group: {
173-
official: '正式队伍',
174-
unofficial: '打星队伍',
175-
...Object.fromEntries(groups.filter((i) => relatedGroups.includes(i.name)).map((i) => [i.name, i.name])),
176-
},
177-
...(badge ? { badge: 'Badge' } : {}),
178-
organization: 'School',
179-
status_time_display: {
180-
correct: true,
181-
incorrect: true,
182-
pending: true,
183-
},
184-
medal: {
185-
official: {
186-
gold,
187-
silver,
188-
bronze,
189-
},
190-
},
191-
balloon_color: tdoc.balloon
192-
? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({
193-
color: '#000',
194-
background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color,
195-
}))
196-
: [],
197-
logo: {
198-
preset: 'ICPC',
199-
},
200-
...(banner ? { banner: { url: 'banner.jpg' } } : {}),
201-
options: {
202-
submission_timestamp_unit: 'millisecond',
203-
},
204-
...(typeof config.override === 'object' ? config.override || {} : {}),
205-
},
206-
...state,
207-
};
251+
this.response.body = await getJson(tdoc, realtime, { badge, banner, groups, medals: { gold, silver, bronze } });
208252
} else {
209253
this.response.template = 'xcpcio_board.html';
210254
let query = '';
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"name": "@hydrooj/scoreboard-xcpcio",
33
"license": "AGPL-3.0-or-later",
4-
"version": "0.0.5-beta.7",
4+
"version": "0.0.5-beta.8",
55
"main": "index.ts",
66
"repository": "https://github.com/hydro-dev/Hydro.git",
77
"scripts": {
88
"postinstall": "node -r @hydrooj/register install.ts"
99
},
1010
"dependencies": {
11-
"@xcpcio/board-app": "^0.75.1"
11+
"@xcpcio/board-app": "^0.76.0"
1212
},
1313
"devDependencies": {
14-
"@xcpcio/types": "^0.75.1"
14+
"@xcpcio/types": "^0.76.0"
1515
}
1616
}

0 commit comments

Comments
 (0)