diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/luogu_remote_judger/.prettierrc b/luogu_remote_judger/.prettierrc
new file mode 100644
index 0000000..ea645c8
--- /dev/null
+++ b/luogu_remote_judger/.prettierrc
@@ -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
+}
diff --git a/luogu_remote_judger/add_judger.sql b/luogu_remote_judger/add_judger.sql
new file mode 100644
index 0000000..900309c
--- /dev/null
+++ b/luogu_remote_judger/add_judger.sql
@@ -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');
diff --git a/luogu_remote_judger/daemon.js b/luogu_remote_judger/daemon.js
new file mode 100644
index 0000000..b3e5f12
--- /dev/null
+++ b/luogu_remote_judger/daemon.js
@@ -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: 'Sample test is not available.',
+ }),
+ 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: `Failed to download and extract source code.`,
+ }),
+ 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: `${htmlspecialchars(err.message)}`,
+ }),
+ 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;
diff --git a/luogu_remote_judger/entrypoint.js b/luogu_remote_judger/entrypoint.js
new file mode 100644
index 0000000..fad17ca
--- /dev/null
+++ b/luogu_remote_judger/entrypoint.js
@@ -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,
+});
diff --git a/luogu_remote_judger/luogu.js b/luogu_remote_judger/luogu.js
new file mode 100644
index 0000000..4582aa0
--- /dev/null
+++ b/luogu_remote_judger/luogu.js
@@ -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 `${desc}`;
+}
+
+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 += '';
+
+ 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) =>
+ `${Object.entries(subtask.cases)
+ .map(o => o[1])
+ .map(buildLuoguTestCaseInfoBlock)
+ .join('\n')}`
+ )
+ .join('\n');
+ }
+
+ details += '';
+
+ 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 ||
+ '
' +
+ `ID = ${payload.id || 'None'}` +
+ `${htmlspecialchars(payload.message)}` +
+ '
',
+ }),
+ 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 ||
+ '' +
+ `REMOTE_SUBMISSION_ID = ${payload.id || 'None'}\nVERDICT = ${payload.status}` +
+ '
',
+ }),
+ 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;
+}
diff --git a/luogu_remote_judger/package-lock.json b/luogu_remote_judger/package-lock.json
new file mode 100644
index 0000000..079a9d7
--- /dev/null
+++ b/luogu_remote_judger/package-lock.json
@@ -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=="
+ }
+ }
+}
diff --git a/luogu_remote_judger/package.json b/luogu_remote_judger/package.json
new file mode 100644
index 0000000..a1a253f
--- /dev/null
+++ b/luogu_remote_judger/package.json
@@ -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 ",
+ "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"
+ }
+}
diff --git a/luogu_remote_judger/utils/htmlspecialchars.js b/luogu_remote_judger/utils/htmlspecialchars.js
new file mode 100644
index 0000000..9eb3556
--- /dev/null
+++ b/luogu_remote_judger/utils/htmlspecialchars.js
@@ -0,0 +1,11 @@
+export default function htmlspecialchars(text) {
+ const map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ };
+
+ return text.replace(/[&<>"']/g, m => map[m]);
+}
diff --git a/luogu_remote_judger/utils/logger.js b/luogu_remote_judger/utils/logger.js
new file mode 100644
index 0000000..e71209b
--- /dev/null
+++ b/luogu_remote_judger/utils/logger.js
@@ -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;
diff --git a/luogu_remote_judger/utils/sleep.js b/luogu_remote_judger/utils/sleep.js
new file mode 100644
index 0000000..ba33476
--- /dev/null
+++ b/luogu_remote_judger/utils/sleep.js
@@ -0,0 +1,5 @@
+export default function sleep(timeout) {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(true), timeout);
+ });
+}
diff --git a/luogu_remote_judger/utils/time.js b/luogu_remote_judger/utils/time.js
new file mode 100644
index 0000000..2a4fcd7
--- /dev/null
+++ b/luogu_remote_judger/utils/time.js
@@ -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;