1
0
mirror of https://github.com/renbaoshuo/UOJ-Luogu-RemoteJudge.git synced 2024-11-11 12:58:48 +00:00

feat: luogu remote judger

This commit is contained in:
Baoshuo Ren 2023-03-21 19:14:43 +08:00
parent e92475e70a
commit ce4fdc1523
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
12 changed files with 1036 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,14 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "lf",
"jsxSingleQuote": false,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"organizeImportsSkipDestructiveCodeActions": true
}

View File

@ -0,0 +1,2 @@
USE `app_uoj233`;
insert into judger_info (judger_name, password, ip) values ('luogu_remote_judger', '_judger_password_', 's2oj-luogu-remote-judger');

View File

@ -0,0 +1,181 @@
import fs from 'fs-extra';
import superagent from 'superagent';
import path from 'node:path';
import child from 'node:child_process';
import Luogu, { getAccountInfoFromEnv } from './luogu.js';
import Logger from './utils/logger.js';
import sleep from './utils/sleep.js';
import * as TIME from './utils/time.js';
import htmlspecialchars from './utils/htmlspecialchars.js';
const logger = new Logger('daemon');
async function daemon(config) {
function request(url, data) {
const req_url = `${config.server_url}/judge${url}`;
logger.debug('request', req_url, data);
return superagent
.post(req_url)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(
Object.entries({
judger_name: config.judger_name,
password: config.password,
...data,
})
.map(([k, v]) => `${k}=${encodeURIComponent(typeof v === 'string' ? v : JSON.stringify(v))}`)
.join('&')
);
}
const luogu = new Luogu(getAccountInfoFromEnv());
logger.info('Daemon started.');
while (true) {
try {
await sleep(TIME.second);
const { text, error } = await request('/submit');
if (error) {
logger.error('/submit', error.message);
continue;
}
if (text.startsWith('Nothing to judge')) {
logger.debug('Nothing to judge.');
continue;
}
const data = JSON.parse(text);
const { id, content, judge_time } = data;
const config = Object.fromEntries(content.config);
const tmpdir = `/tmp/s2oj_rmj/${id}/`;
if (config.test_sample_only === 'on') {
await request('/submit', {
submit: 1,
fetch_new: 0,
id,
result: JSON.stringify({
status: 'Judged',
score: 100,
time: 0,
memory: 0,
details: '<info-block>Sample test is not available.</info-block>',
}),
judge_time,
});
continue;
}
fs.ensureDirSync(tmpdir);
let code = '';
try {
// =========================
// Download source code
// =========================
logger.debug('Downloading source code for ' + id);
const zipFilePath = path.resolve(tmpdir, 'all.zip');
const res = request(`/download${content.file_name}`);
const stream = fs.createWriteStream(zipFilePath);
res.pipe(stream);
await new Promise((resolve, reject) => {
stream.on('finish', resolve);
stream.on('error', reject);
});
// =========================
// Extract source code
// =========================
logger.debug('Extracting source code for ' + id);
const extractedPath = path.resolve(tmpdir, 'all');
await new Promise((resolve, reject) => {
child.exec(`unzip ${zipFilePath} -d ${extractedPath}`, e => {
if (e) reject(e);
else resolve(true);
});
});
// =========================
// Read source code
// =========================
logger.debug('Reading source code.', id);
const sourceCodePath = path.resolve(extractedPath, 'answer.code');
code = fs.readFileSync(sourceCodePath, 'utf-8');
} catch (e) {
await request('/submit', {
submit: 1,
fetch_new: 0,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error: 'Judgement Failed',
details: `<error>Failed to download and extract source code.</error>`,
}),
judge_time,
});
logger.error('Failed to download and extract source code.', id, e.message);
fs.removeSync(tmpdir);
continue;
}
// =========================
// Start judging
// =========================
logger.info('Start judging', id, `(problem ${data.problem_id})`);
try {
await luogu.judge(id, config.luogu_pid, config.answer_language, code, judge_time, config, request);
} catch (err) {
await request('/submit', {
submit: 1,
fetch_new: 0,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error: 'Judgement Failed',
details: `<error>${htmlspecialchars(err.message)}</error>`,
}),
judge_time,
});
logger.error('Judgement Failed.', id, err.message);
fs.removeSync(tmpdir);
continue;
}
fs.removeSync(tmpdir);
} catch (err) {
logger.error(err.message);
}
}
}
export default daemon;

View File

@ -0,0 +1,16 @@
import daemon from './daemon.js';
const {
UOJ_PROTOCOL = 'http',
UOJ_HOST = 'uoj-web',
UOJ_JUDGER_NAME = 'luogu_remote_judger',
UOJ_JUDGER_PASSWORD = '',
} = process.env;
const UOJ_BASEURL = `${UOJ_PROTOCOL}://${UOJ_HOST}`;
daemon({
server_url: UOJ_BASEURL,
judger_name: UOJ_JUDGER_NAME,
password: UOJ_JUDGER_PASSWORD,
});

View File

@ -0,0 +1,377 @@
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,
'Judgment 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: 'Judgment 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}`);
if (HTTP_ERROR_MAP[result.status]) {
await end({
error: true,
status: 'Judgment 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: 'Judgment 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: 'Judgment 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: 'Judgment 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;
}

388
luogu_remote_judger/package-lock.json generated Normal file
View File

@ -0,0 +1,388 @@
{
"name": "s2oj-luogu-remote-judger",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "s2oj-luogu-remote-judger",
"version": "0.0.0",
"dependencies": {
"fs-extra": "^11.1.1",
"reggol": "^1.3.5",
"superagent": "^8.0.9"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/cosmokit": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.4.1.tgz",
"integrity": "sha512-d3ZRpKFahJRvLbo1T4y0ELCudjk9AeDUsfgKm+iAti6yPCeoPLGNUGT4expTWsNkrSA1uk7CKhtBPiizFYvDgA=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formidable": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
"integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
"dependencies": {
"dezalgo": "^1.0.4",
"hexoid": "^1.0.0",
"once": "^1.4.0",
"qs": "^6.11.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
"engines": {
"node": ">=8"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/qs": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
"integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reggol": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/reggol/-/reggol-1.3.5.tgz",
"integrity": "sha512-kzkzs4nhZeiphyh+amekq25/3PndZDq+5Yt8qCJqPSyMXPC1pkwhfYCQyJdXxoRz3/uqt0+VqHulagUCVY84vA==",
"dependencies": {
"cosmokit": "^1.4.0",
"object-inspect": "^1.12.2",
"supports-color": "^8.1.1"
}
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/superagent": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz",
"integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==",
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.4",
"debug": "^4.3.4",
"fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.0",
"formidable": "^2.1.2",
"methods": "^1.1.2",
"mime": "2.6.0",
"qs": "^6.11.0",
"semver": "^7.3.8"
},
"engines": {
"node": ">=6.4.0 <13 || >=14"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@ -0,0 +1,25 @@
{
"name": "s2oj-luogu-remote-judger",
"version": "0.0.0",
"description": "Luogu remote judger for S2OJ.",
"private": true,
"main": "entrypoint.js",
"type": "module",
"scripts": {
"start": "node entrypoint.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/renbaoshuo/S2OJ.git"
},
"author": "Baoshuo <i@baoshuo.ren>",
"bugs": {
"url": "https://github.com/renbaoshuo/S2OJ/issues"
},
"homepage": "https://github.com/renbaoshuo/S2OJ#readme",
"dependencies": {
"fs-extra": "^11.1.1",
"reggol": "^1.3.5",
"superagent": "^8.0.9"
}
}

View File

@ -0,0 +1,11 @@
export default function htmlspecialchars(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
};
return text.replace(/[&<>"']/g, m => map[m]);
}

View File

@ -0,0 +1,11 @@
import Logger from 'reggol';
Logger.levels.base = process.env.DEV ? 3 : 2;
Logger.targets[0].showTime = 'dd hh:mm:ss';
Logger.targets[0].label = {
align: 'right',
width: 9,
margin: 1,
};
export default Logger;

View File

@ -0,0 +1,5 @@
export default function sleep(timeout) {
return new Promise(resolve => {
setTimeout(() => resolve(true), timeout);
});
}

View File

@ -0,0 +1,5 @@
export const second = 1000;
export const minute = second * 60;
export const hour = minute * 60;
export const day = hour * 24;
export const week = day * 7;