1
0
mirror of https://github.com/renbaoshuo/UOJ-Luogu-RemoteJudge.git synced 2024-11-09 16:18:49 +00:00
UOJ-Luogu-RemoteJudge/luogu_remote_judger/luogu.js
2023-03-21 22:03:45 +08:00

378 lines
9.1 KiB
JavaScript

import superagent from 'superagent';
import Logger from './utils/logger.js';
import sleep from './utils/sleep.js';
import htmlspecialchars from './utils/htmlspecialchars.js';
const logger = new Logger('remote/luogu');
const USER_AGENT = 'UniversalOJ/1.0 UOJ-Luogu-RemoteJudge/1.0 ( https://github.com/renbaoshuo/UOJ-Luogu-RemoteJudge )';
const HTTP_ERROR_MAP = {
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
};
const STATUS_MAP = [
'Waiting',
'Judging',
'Compile Error',
'Output Limit Exceeded',
'Memory Limit Exceeded',
'Time Limit Exceeded',
'Wrong Answer',
'Runtime Error',
0,
0,
0,
'Judgement Failed',
'Accepted',
0,
'Wrong Answer', // WA
];
const LANGS_MAP = {
C: {
id: 'c/99/gcc',
name: 'C',
comment: '//',
},
'C++': {
id: 'cxx/98/gcc',
name: 'C++ 98',
comment: '//',
},
'C++11': {
id: 'cxx/11/gcc',
name: 'C++ 11',
comment: '//',
},
Python3: {
id: 'python3/c',
name: 'Python 3',
comment: '#',
},
Java8: {
id: 'java/8',
name: 'Java 8',
comment: '//',
},
Pascal: {
id: 'pascal/fpc',
name: 'Pascal',
comment: '//',
},
};
function buildLuoguTestCaseInfoBlock(test) {
const attrs = [
['num', test.id],
['info', STATUS_MAP[test.status]],
['time', test.time || -1],
['memory', test.memory || -1],
['score', test.score || ''],
]
.map(o => `${o[0]}="${o[1]}"`)
.join(' ');
const desc = htmlspecialchars(test.description || '');
return `<test ${attrs}><res>${desc}</res></test>`;
}
export default class Luogu {
account;
constructor(account) {
if (!account) {
throw new Error('No account info provided');
}
this.account = account;
}
get(url) {
if (!url.includes('//')) {
url = `${this.account.endpoint || 'https://open-v1.lgapi.cn'}${url}`;
}
logger.debug('get', url, this.cookie);
const req = superagent
.get(url)
.set('User-Agent', USER_AGENT)
.auth(this.account.username, this.account.password)
.set('X-Requested-With', 'S2OJ Remote Judge (OpenSource Version)');
return req;
}
post(url) {
if (!url.includes('//')) {
url = `${this.account.endpoint || 'https://open-v1.lgapi.cn'}${url}`;
}
logger.debug('post', url);
const req = superagent
.post(url)
.set('User-Agent', USER_AGENT)
.auth(this.account.username, this.account.password)
.set('X-Requested-With', 'S2OJ Remote Judge (OpenSource Version)');
return req;
}
async submitProblem(id, lang, code, submissionId, next, end) {
if (code.length < 10) {
await end({
error: true,
status: 'Compile Error',
message: 'Code too short',
});
return null;
}
const programType = LANGS_MAP[lang] || LANGS_MAP['C++'];
const comment = programType.comment;
if (comment) {
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
else if (comment instanceof Array) code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
}
const result = await this.post('/judge/problem')
.send({
pid: id,
code,
lang: programType.id,
o2: 1,
trackId: submissionId,
})
.ok(status => true);
if (HTTP_ERROR_MAP[result.status]) {
await end({
error: true,
status: 'Judgement Failed',
message: `[Luogu] ${HTTP_ERROR_MAP[result.status]}.`,
});
logger.error('submitProblem', result.status, HTTP_ERROR_MAP[result.status]);
return null;
}
return result.body.requestId;
}
async waitForSubmission(id, next, end) {
let fail = 0;
let count = 0;
while (count < 360 && fail < 60) {
await sleep(500);
count++;
try {
const result = await this.get(`/judge/result?id=${id}`).ok((status) => true);
if (HTTP_ERROR_MAP[result.status]) {
await end({
error: true,
status: 'Judgement Failed',
message: `[Luogu] ${HTTP_ERROR_MAP[result.status]}.`,
});
logger.error('submitProblem', result.status, HTTP_ERROR_MAP[result.status]);
return null;
}
const data = result.body.data;
if (result.status == 204) {
await next({ status: '[Luogu] Judging' });
continue;
}
if (result.status == 200 && !data) {
return await end({
error: true,
id,
status: 'Judgement Failed',
message: 'Failed to fetch submission details.',
});
}
if (data.compile && data.compile.success === false) {
return await end({
error: true,
id,
status: 'Compile Error',
message: data.compile.message,
});
}
if (!data.judge?.subtasks) continue;
const finishedTestCases = Object.entries(data.judge.subtasks)
.map(o => o[1])
.reduce(
(acc, sub) =>
acc +
Object.entries(sub.cases)
.map(o => o[1])
.filter(test => test.status >= 2).length,
0
);
await next({
status: `[Luogu] Judging (${finishedTestCases} judged)`,
});
if (data.status < 2) continue;
logger.info('RecordID:', id, 'done');
let details = '';
details += '<tests>';
if (data.judge.subtasks.length === 1) {
details += Object.entries(data.judge.subtasks[0].cases)
.map(o => o[1])
.map(buildLuoguTestCaseInfoBlock)
.join('\n');
} else {
details += Object.entries(data.judge.subtasks)
.map(o => o[1])
.map(
(subtask, index) =>
`<subtask num="${index}" info="${STATUS_MAP[subtask.status]}" time="${subtask.time || -1}" memory="${
subtask.memory || -1
}" score="${subtask.score || ''}">${Object.entries(subtask.cases)
.map(o => o[1])
.map(buildLuoguTestCaseInfoBlock)
.join('\n')}</subtask>`
)
.join('\n');
}
details += '</tests>';
return await end({
id,
status: STATUS_MAP[data.judge.status],
score:
STATUS_MAP[data.judge.status] === 'Accepted'
? 100
: // Workaround for UOJ feature
Math.min(97, data.judge.score),
time: data.judge.time,
memory: data.judge.memory,
details,
});
} catch (e) {
logger.error(e);
fail++;
}
}
return await end({
error: true,
id,
status: 'Judgement Failed',
message: 'Failed to fetch submission details.',
});
}
async judge(id, pid, lang, code, judge_time, config, request) {
const next = payload =>
request('/submit', {
'update-status': 1,
fetch_new: 0,
id,
status: payload.status || (payload.test_id ? `Judging Test #${payload.test_id}` : 'Judging'),
});
const end = payload => {
if (payload.error) {
return request('/submit', {
submit: 1,
fetch_new: 0,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error: payload.status,
details:
payload.details ||
'<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
`<error>${htmlspecialchars(payload.message)}</error>` +
'</div>',
}),
judge_time,
});
}
return request('/submit', {
submit: 1,
fetch_new: 0,
id,
result: JSON.stringify({
status: 'Judged',
score: payload.score,
time: payload.time,
memory: payload.memory,
details:
payload.details ||
'<div>' +
`<info-block>REMOTE_SUBMISSION_ID = ${payload.id || 'None'}\nVERDICT = ${payload.status}</info-block>` +
'</div>',
}),
judge_time,
});
};
try {
const rid = await this.submitProblem(pid, lang, code, id, next, end);
if (!rid) return;
await this.waitForSubmission(rid, next, end);
} catch (e) {
logger.error(e);
await end({
error: true,
status: 'Judgement Failed',
message: e.message,
});
}
}
}
export function getAccountInfoFromEnv() {
const { LUOGU_API_USERNAME, LUOGU_API_PASSWORD, LUOGU_API_ENDPOINT = 'https://open-v1.lgapi.cn' } = process.env;
if (!LUOGU_API_USERNAME || !LUOGU_API_PASSWORD) return null;
const account = {
type: 'luogu-api',
username: LUOGU_API_USERNAME,
password: LUOGU_API_PASSWORD,
endpoint: LUOGU_API_ENDPOINT,
};
return account;
}