mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
bug fixes & improvements for on site contest
This commit is contained in:
19 files changed
+383
-68
No files matched your search
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable max-len */
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync,
|
||||
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel,
|
||||
STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS, Time, UserModel, Zip,
|
||||
_, avatar, 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,
|
||||
} from 'hydrooj';
|
||||
import { ResolverInput } from './interface';
|
||||
|
||||
@@ -18,31 +20,79 @@ declare module 'hydrooj' {
|
||||
}
|
||||
}
|
||||
const coll = db.collection('iplogin');
|
||||
const ipLoginCache = new LRUCache<string, IpLoginInfo>({ max: 1000, ttl: 60 * 1000 });
|
||||
|
||||
function normalizeIp(ip: string) {
|
||||
if (ip.startsWith('::ffff:')) return ip.slice(7);
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('handler/init', async (that) => {
|
||||
const iplogin = await coll.findOne({ _id: normalizeIp(that.request.ip) });
|
||||
if (iplogin) {
|
||||
const QuickImportSchema = Schema.array(Schema.object({
|
||||
id: Schema.union([Schema.string().required(), Schema.number().required()]),
|
||||
name: Schema.string().required(),
|
||||
password: Schema.string(),
|
||||
school: Schema.string(),
|
||||
members: Schema.array(Schema.string()).default([]),
|
||||
member1: Schema.string(),
|
||||
member2: Schema.string(),
|
||||
member3: Schema.string(),
|
||||
member4: Schema.string(),
|
||||
coach: Schema.string(),
|
||||
seat: Schema.string(),
|
||||
rank: Schema.boolean(),
|
||||
ip: Schema.string(),
|
||||
}));
|
||||
|
||||
export const Config = Schema.object({
|
||||
ipLogin: Schema.boolean().default(false),
|
||||
extraFields: Schema.boolean().default(false),
|
||||
submit: Schema.boolean().default(false).description('Enable submit script'),
|
||||
});
|
||||
|
||||
export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
||||
if (config.ipLogin) {
|
||||
ctx.on('handler/init', async (that) => {
|
||||
const iplogin = ipLoginCache.get(normalizeIp(that.request.ip)) || await coll.findOne({ _id: normalizeIp(that.request.ip) });
|
||||
if (!iplogin) {
|
||||
if (that.session.ipLoggedIn && that.session.ipLoggedIn !== normalizeIp(that.request.ip)) {
|
||||
that.session.uid = 0;
|
||||
that.session.user = await UserModel.getById(that.domain._id, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ipLoginCache.set(normalizeIp(that.request.ip), iplogin);
|
||||
that.user = await UserModel.getById(that.domain._id, iplogin.uid);
|
||||
if (!that.user) {
|
||||
that.user = await UserModel.getById(that.domain._id, 0);
|
||||
throw new ForbiddenError(`User ${iplogin.uid}not found`);
|
||||
throw new ForbiddenError(`User ${iplogin.uid} not found`);
|
||||
}
|
||||
that.session.ipLoggedIn = true;
|
||||
that.session.ipLoggedIn = that.request.ip;
|
||||
that.session.uid = iplogin.uid;
|
||||
that.session.user = that.user;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const disable = (that) => {
|
||||
if (!that.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) throw new ForbiddenError('Not available');
|
||||
};
|
||||
ctx.on('handler/before/HomeDomain', disable);
|
||||
ctx.on('handler/before/DomainUser', disable);
|
||||
ctx.on('handler/before/HomeMessages#post', disable);
|
||||
ctx.on('handler/before/Files', disable);
|
||||
ctx.on('handler/before/HomeAvatar', disable);
|
||||
ctx.on('handler/before/HomeSecurity', disable);
|
||||
ctx.on('handler/before/UserDetail', disable);
|
||||
ctx.on('handler/before/UserLostPass', disable);
|
||||
|
||||
ctx.on('handler/before/HomeSettings', (that) => {
|
||||
if (that.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) return;
|
||||
if (['domain', 'account'].includes(that.args.category)) throw new ForbiddenError('Not available');
|
||||
});
|
||||
}
|
||||
|
||||
ctx.inject(['scoreboard'], ({ scoreboard }) => {
|
||||
scoreboard.addView('resolver-tiny', 'Resolver(Tiny)', { tdoc: 'tdoc' }, {
|
||||
async display({ tdoc }) {
|
||||
if (!this.user.own(tdoc) && ContestModel.isLocked(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
const teams = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray();
|
||||
const udict = await UserModel.getList(tdoc.domainId, teams.map((i) => i.uid));
|
||||
const teamIds: Record<number, number> = {};
|
||||
@@ -82,9 +132,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
scoreboard.addView('cdp', 'CDP', { tdoc: 'tdoc' }, {
|
||||
async display({ tdoc }) {
|
||||
if (ContestModel.isLocked(tdoc) && !this.user.own(tdoc)) {
|
||||
this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
||||
let token = 0;
|
||||
const getFeed = (type: string, data: any) => ({
|
||||
@@ -100,7 +148,7 @@ export function apply(ctx: Context) {
|
||||
return {
|
||||
team_id: `team-${udoc._id}`,
|
||||
name: udoc.uname,
|
||||
displayName: (i.unrank ? '⭐' : '') + udoc.displayName,
|
||||
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
|
||||
organization: udoc.school || udoc.uname,
|
||||
avatar: avatar(udoc.avatar),
|
||||
group: [
|
||||
@@ -270,4 +318,71 @@ export function apply(ctx: Context) {
|
||||
supportedRules: ['acm'],
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
// @ts-ignore
|
||||
Hydro.model.system.onsiteImport = async function (filepath: string, tidsInput: string, format = 'webp') {
|
||||
const data = QuickImportSchema(JSON.parse(fs.readFileSync(filepath, 'utf-8')));
|
||||
const tids = tidsInput.split(',').map((i) => i.trim()).filter((i) => i).map((i) => new ObjectId(i));
|
||||
const tdocs = await Promise.all(tids.map((i) => ContestModel.get('system', i)));
|
||||
const convertUname = Types.Username[0];
|
||||
for (const line of data) {
|
||||
const uname = convertUname(line.name);
|
||||
const email = `${line.id}@onsite.local`;
|
||||
let team = await UserModel.getByEmail('system', email);
|
||||
if (!team) {
|
||||
const uid = await UserModel.create(email, uname, line.password || randomstring());
|
||||
team = await UserModel.getById('system', uid);
|
||||
} else {
|
||||
await UserModel.setById(team._id, { uname });
|
||||
if (line.password) await UserModel.setPassword(team._id, line.password);
|
||||
}
|
||||
if (line.member1) line.members.push(line.member1);
|
||||
if (line.member2) line.members.push(line.member2);
|
||||
if (line.member3) line.members.push(line.member3);
|
||||
if (line.member4) line.members.push(line.member4);
|
||||
const set: any = _.pick(line, 'school', 'members', 'coach', 'seat');
|
||||
if (line.school) set.avatar = `url:/avatars/${line.school.replace(/[ ()]/g, '')}.${format}`;
|
||||
if (Object.keys(set).length) await UserModel.setById(team._id, set);
|
||||
for (const tdoc of tdocs) {
|
||||
const tsdoc = await ContestModel.getStatus('system', tdoc.docId, team._id);
|
||||
if (!tsdoc?.attend) await ContestModel.attend('system', tdoc.docId, team._id, 'rank' in line ? { unrank: !line.rank } : {});
|
||||
else if ('rank' in line && tsdoc.unrank === line.rank) await ContestModel.setStatus('system', tdoc.docId, team._id, { unrank: !line.rank });
|
||||
}
|
||||
if (line.ip) await coll.updateOne({ _id: normalizeIp(line.ip) }, { $set: { uid: team._id } }, { upsert: true });
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
Hydro.model.system.setIpLogin = async function (filepath: string) {
|
||||
const data = fs.readFileSync(filepath, 'utf-8').split('\n').filter((i) => i);
|
||||
for (const line of data) {
|
||||
const [seat, ip] = line.split(',').map((i) => i.trim());
|
||||
const user = await UserModel.coll.findOne({ seat });
|
||||
if (!user) {
|
||||
console.warn(`User ${seat} not found`);
|
||||
continue;
|
||||
}
|
||||
await coll.updateOne({ _id: normalizeIp(ip) }, { $set: { uid: user._id } }, { upsert: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (config.submit) ctx.plugin(require('./submit'));
|
||||
|
||||
if (config.extraFields) {
|
||||
ctx.inject(['setting'], (c) => {
|
||||
c.setting.AccountSetting(
|
||||
SettingModel.Setting('setting_info', 'coach', null, 'text', 'coach', '', SettingModel.FLAG_DISABLED | SettingModel.FLAG_PUBLIC),
|
||||
SettingModel.Setting('setting_info', 'members', null, 'text', 'members', '', SettingModel.FLAG_DISABLED | SettingModel.FLAG_PUBLIC),
|
||||
SettingModel.Setting('setting_info', 'seat', null, 'text', 'seat', '', SettingModel.FLAG_DISABLED | SettingModel.FLAG_PUBLIC),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ctx.on('handler/before/ContestScoreboard#get', (that) => {
|
||||
if (that.request.path.endsWith('/scoreboard')) {
|
||||
that.response.redirect = `${that.request.path}/xcpcio`;
|
||||
return 'cleanup';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"repository": "https://github.com/hydro-dev/Hydro",
|
||||
"dependencies": {
|
||||
"@react-spring/web": "^10.0.3",
|
||||
"lru-cache": "^11.2.2",
|
||||
"react-use": "^17.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ContestModel, Context, DomainModel, fs, nanoid, ProblemDoc, ProblemModel,
|
||||
RecordModel, SettingModel, StorageModel, SystemModel,
|
||||
} from 'hydrooj';
|
||||
import { ContestDetailBaseHandler } from 'hydrooj/src/handler/contest';
|
||||
|
||||
class AutoSubmitHandler extends ContestDetailBaseHandler {
|
||||
pdoc: ProblemDoc;
|
||||
|
||||
async submit({ domainId, dryrun }: { domainId: string, dryrun: boolean }) {
|
||||
if (!this.tdoc || !ContestModel.isOngoing(this.tdoc, this.tsdoc)) return { error: 'Contest not live' };
|
||||
if (!this.tdoc.allowPrint) return { error: 'Not a on-site contest' };
|
||||
const file = this.request.files?.file;
|
||||
if (!file) return { error: 'No file uploaded' };
|
||||
const filename = file.originalFilename.split('/').pop();
|
||||
if (!/^[A-Z]\./i.test(filename.toUpperCase())) return { error: 'Unsupported file name' };
|
||||
const pids = this.tdoc.pids;
|
||||
const pidIndex = filename.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0);
|
||||
if (pidIndex < 0 || pidIndex >= pids.length) return { error: 'Unsupported file name' };
|
||||
const pid = pids[pidIndex];
|
||||
this.pdoc = await ProblemModel.get(domainId, pid);
|
||||
|
||||
const ext = filename.split('.').pop();
|
||||
let lang = ext.toLowerCase();
|
||||
|
||||
const config = this.pdoc.config;
|
||||
const lengthLimit = SystemModel.get('limit.codelength') || 128 * 1024;
|
||||
if (typeof config === 'string' || config === null) return { error: 'system error' };
|
||||
const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : lengthLimit;
|
||||
if (file.size > sizeLimit) return { error: 'File too large' };
|
||||
|
||||
if (['submit_answer', 'objective'].includes(config.type)) {
|
||||
lang = '_';
|
||||
} else if ((config.langs && !config.langs.includes(lang)) || !SettingModel.langs[lang] || SettingModel.langs[lang].disabled) {
|
||||
return { error: 'Unsupported language' };
|
||||
}
|
||||
const shouldReadFile = () => {
|
||||
if (config.type === 'objective') return true;
|
||||
if (lang === '_') return false;
|
||||
return file.size < lengthLimit && !file.filepath.endsWith('.zip') && !SettingModel.langs[lang].isBinary;
|
||||
};
|
||||
let code = '';
|
||||
const files: Record<string, string> = {};
|
||||
if (shouldReadFile()) code = await fs.readFile(file.filepath, 'utf-8');
|
||||
else {
|
||||
const id = nanoid();
|
||||
await StorageModel.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id);
|
||||
files.code = `${this.user._id}/${id}#${file.originalFilename}`;
|
||||
}
|
||||
|
||||
if (!code) return { error: 'Cannot submit empty code' };
|
||||
|
||||
if (dryrun) {
|
||||
return {
|
||||
info: `Info
|
||||
|
||||
Contest ${this.tdoc.title}
|
||||
Problem ${this.pdoc.title}
|
||||
User ${this.user.uname}
|
||||
School ${this.user.school}
|
||||
Language ${SettingModel.langs[lang].display} (${lang})
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
await this.limitRate('add_record', 60, SystemModel.get('limit.submission_user'), '{{user}}');
|
||||
await this.limitRate('add_record', 60, SystemModel.get('limit.submission'));
|
||||
|
||||
const rid = await RecordModel.add(
|
||||
domainId, this.pdoc.docId, this.user._id, lang, code, true,
|
||||
{ contest: this.tdoc.docId, files, type: 'judge' },
|
||||
);
|
||||
await Promise.all([
|
||||
ProblemModel.inc(domainId, this.pdoc.docId, 'nSubmit', 1),
|
||||
DomainModel.incUserInDomain(domainId, this.user._id, 'nSubmit'),
|
||||
ContestModel.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
|
||||
]);
|
||||
return { rid };
|
||||
}
|
||||
|
||||
async post({ domainId, dryrun }) {
|
||||
try {
|
||||
this.response.body = await this.submit({ domainId, dryrun });
|
||||
} catch (e) {
|
||||
this.response.body = { error: e.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.Route('contest_autosubmit', '/contest/:tid/autosubmit', AutoSubmitHandler);
|
||||
}
|
||||
Reference in new issue
Block a user