diff --git a/remote_judger/src/providers/uoj.ts b/remote_judger/src/providers/uoj.ts
new file mode 100644
index 0000000..662dd05
--- /dev/null
+++ b/remote_judger/src/providers/uoj.ts
@@ -0,0 +1,252 @@
+import { JSDOM } from 'jsdom';
+import superagent from 'superagent';
+import proxy from 'superagent-proxy';
+import Logger from '../utils/logger';
+import { IBasicProvider, RemoteAccount } from '../interface';
+import { parseTimeMS, parseMemoryMB } from '../utils/parse';
+import sleep from '../utils/sleep';
+
+proxy(superagent);
+const logger = new Logger('remote/uoj');
+
+const langs_map = {
+ C: {
+ name: 'C',
+ id: 'C',
+ comment: '//',
+ },
+ 'C++03': {
+ name: 'C++ 03',
+ id: 'C++',
+ comment: '//',
+ },
+ 'C++11': {
+ name: 'C++ 11',
+ id: 'C++11',
+ comment: '//',
+ },
+ 'C++': {
+ name: 'C++ 14',
+ id: 'C++14',
+ comment: '//',
+ },
+ 'C++17': {
+ name: 'C++ 17',
+ id: 'C++17',
+ comment: '//',
+ },
+ 'C++20': {
+ name: 'C++ 20',
+ id: 'C++20',
+ comment: '//',
+ },
+ 'Python2.7': {
+ name: 'Python 2.7',
+ id: 'Python2.7',
+ comment: '#',
+ },
+ Python3: {
+ name: 'Python 3',
+ id: 'Python3',
+ comment: '#',
+ },
+ Java8: {
+ name: 'Java 8',
+ id: 'Java8',
+ comment: '//',
+ },
+ Java11: {
+ name: 'Java 11',
+ id: 'Java11',
+ comment: '//',
+ },
+ Java17: {
+ name: 'Java 17',
+ id: 'Java17',
+ comment: '//',
+ },
+ Pascal: {
+ name: 'Pascal',
+ id: 'Pascal',
+ comment: '//',
+ },
+};
+
+export function getAccountInfoFromEnv(): RemoteAccount | null {
+ const {
+ UOJ_HANDLE,
+ UOJ_PASSWORD,
+ UOJ_ENDPOINT = 'https://uoj.ac',
+ UOJ_PROXY,
+ } = process.env;
+
+ if (!UOJ_HANDLE || !UOJ_PASSWORD) return null;
+
+ const account: RemoteAccount = {
+ type: 'codeforces',
+ handle: UOJ_HANDLE,
+ password: UOJ_PASSWORD,
+ endpoint: UOJ_ENDPOINT,
+ };
+
+ if (UOJ_PROXY) account.proxy = UOJ_PROXY;
+
+ return account;
+}
+
+export default class UOJProvider implements IBasicProvider {
+ constructor(public account: RemoteAccount) {
+ if (account.cookie) this.cookie = account.cookie;
+ }
+
+ cookie: string[] = [];
+ csrf: string;
+
+ get(url: string) {
+ logger.debug('get', url);
+ if (!url.includes('//'))
+ url = `${this.account.endpoint || 'https://uoj.ac'}${url}`;
+ const req = superagent.get(url).set('Cookie', this.cookie);
+ if (this.account.proxy) return req.proxy(this.account.proxy);
+ return req;
+ }
+
+ post(url: string) {
+ logger.debug('post', url, this.cookie);
+ if (!url.includes('//'))
+ url = `${this.account.endpoint || 'https://uoj.ac'}${url}`;
+ const req = superagent.post(url).set('Cookie', this.cookie).type('form');
+ if (this.account.proxy) return req.proxy(this.account.proxy);
+ return req;
+ }
+
+ async getCsrfToken(url: string) {
+ const { text: html, header } = await this.get(url);
+ if (header['set-cookie']) {
+ this.cookie = header['set-cookie'];
+ }
+ let value = /_token *: *"(.+?)"/g.exec(html);
+ if (value) return value[1];
+ value = /_token" value="(.+?)"/g.exec(html);
+ return value?.[1];
+ }
+
+ get loggedIn() {
+ return this.get('/login').then(
+ ({ text: html }) => !html.includes('
登录')
+ );
+ }
+
+ async ensureLogin() {
+ if (await this.loggedIn) return true;
+ logger.info('retry login');
+ const _token = await this.getCsrfToken('/login');
+ const { header, text } = await this.post('/login').send({
+ _token,
+ login: '',
+ username: this.account.handle,
+ // NOTE: you should pass a pre-hashed key!
+ password: this.account.password,
+ });
+ if (header['set-cookie'] && this.cookie.length === 1) {
+ header['set-cookie'].push(...this.cookie);
+ this.cookie = header['set-cookie'];
+ }
+ if (text === 'ok') return true;
+ return text;
+ }
+
+ async submitProblem(id: string, lang: string, code: string, info) {
+ const programType = langs_map[lang] || langs_map['C++'];
+ const comment = programType.comment;
+
+ if (comment) {
+ const msg = `S2OJ Submission #${info.rid} @ ${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 _token = await this.getCsrfToken(`/problem/${id}`);
+ const { text } = await this.post(`/problem/${id}`).send({
+ _token,
+ answer_answer_language: programType.id,
+ answer_answer_upload_type: 'editor',
+ answer_answer_editor: code,
+ 'submit-answer': 'answer',
+ });
+
+ if (!text.includes('我的提交记录')) throw new Error('Submit failed');
+
+ const { text: status } = await this.get(
+ `/submissions?problem_id=${id}&submitter=${this.account.handle}`
+ );
+
+ const $dom = new JSDOM(status);
+ return $dom.window.document
+ .querySelector('tbody>tr>td>a')
+ .innerHTML.split('#')[1];
+ }
+
+ async waitForSubmission(problem_id: string, id: string, next, end) {
+ let i = 0;
+
+ while (true) {
+ if (++i > 60) {
+ return await end({
+ id,
+ error: true,
+ status: 'Judgment Failed',
+ message: 'Failed to fetch submission details.',
+ });
+ }
+
+ await sleep(2000);
+ const { text } = await this.get(`/submission/${id}`);
+ const {
+ window: { document },
+ } = new JSDOM(text);
+ const find = (content: string) =>
+ Array.from(
+ document.querySelectorAll('.panel-heading>.panel-title')
+ ).find(n => n.innerHTML === content).parentElement.parentElement
+ .children[1];
+ if (text.includes('Compile Error')) {
+ return await end({
+ error: true,
+ id,
+ status: 'Compile Error',
+ message: find('详细').children[0].innerHTML,
+ });
+ }
+
+ await next({});
+
+ const summary = document.querySelector('tbody>tr');
+ if (!summary) continue;
+ const time = parseTimeMS(summary.children[4].innerHTML);
+ const memory = parseMemoryMB(summary.children[5].innerHTML) * 1024;
+ let panel = document.getElementById(
+ 'details_details_accordion_collapse_subtask_1'
+ );
+ if (!panel) {
+ panel = document.getElementById('details_details_accordion');
+ if (!panel) continue;
+ }
+
+ if (document.querySelector('tbody').innerHTML.includes('Judging'))
+ continue;
+
+ const score = +summary.children[3]?.children[0]?.innerHTML || 0;
+ const status = score === 100 ? 'Accepted' : 'Unaccepted';
+
+ return await end({
+ id,
+ status,
+ score,
+ time,
+ memory,
+ });
+ }
+ }
+}
diff --git a/remote_judger/src/utils/parse.ts b/remote_judger/src/utils/parse.ts
new file mode 100644
index 0000000..e79e3c9
--- /dev/null
+++ b/remote_judger/src/utils/parse.ts
@@ -0,0 +1,20 @@
+const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i;
+const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
+const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
+const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
+
+export function parseTimeMS(str: string | number, throwOnError = true) {
+ if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str;
+ const match = TIME_RE.exec(str);
+ if (!match && throwOnError) throw new Error(`${str} error parsing time`);
+ if (!match) return 1000;
+ return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]);
+}
+
+export function parseMemoryMB(str: string | number, throwOnError = true) {
+ if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str;
+ const match = MEMORY_RE.exec(str);
+ if (!match && throwOnError) throw new Error(`${str} error parsing memory`);
+ if (!match) return 256;
+ return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
+}
diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts
index b2f3e3a..b18e6b0 100644
--- a/remote_judger/src/vjudge.ts
+++ b/remote_judger/src/vjudge.ts
@@ -31,7 +31,9 @@ class AccountService {
'update-status': true,
fetch_new: false,
id,
- status: `Judging Test #${payload.test_id}`,
+ status:
+ payload.status ||
+ (payload.test_id ? `Judging Test #${payload.test_id}` : 'Judging'),
});
};
@@ -151,6 +153,7 @@ export async function apply(request: any) {
await vjudge.addProvider('codeforces');
await vjudge.addProvider('atcoder');
+ await vjudge.addProvider('uoj');
return vjudge;
}
diff --git a/web/app/models/UOJRemoteProblem.php b/web/app/models/UOJRemoteProblem.php
index 872368c..c3cd1b4 100644
--- a/web/app/models/UOJRemoteProblem.php
+++ b/web/app/models/UOJRemoteProblem.php
@@ -30,7 +30,7 @@ class UOJRemoteProblem {
'not_exist_texts' => [
'未找到该页面',
],
- 'languages' => ['C', 'C++03', 'C++11', 'C++14', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
+ 'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
],
];