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'], ], ];