diff --git a/remote_judger/src/providers/qoj.ts b/remote_judger/src/providers/qoj.ts
new file mode 100644
index 0000000..314e832
--- /dev/null
+++ b/remote_judger/src/providers/qoj.ts
@@ -0,0 +1,296 @@
+import { JSDOM } from 'jsdom';
+import superagent from 'superagent';
+import proxy from 'superagent-proxy';
+import Logger from '../utils/logger';
+import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface';
+import { parseTimeMS, parseMemoryMB } from '../utils/parse';
+import sleep from '../utils/sleep';
+import UOJProvider from './uoj';
+
+proxy(superagent);
+const logger = new Logger('remote/qoj');
+
+const LANGS_MAP = {
+ C: {
+ name: 'C',
+ id: 'C',
+ comment: '//',
+ },
+ 'C++98': {
+ name: 'C++ 98',
+ 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',
+ id: 'Python2',
+ comment: '#',
+ },
+ Python3: {
+ name: 'Python 3',
+ id: 'Python3',
+ comment: '#',
+ },
+ Java8: {
+ name: 'Java 8',
+ id: 'Java8',
+ comment: '//',
+ },
+ Java11: {
+ name: 'Java 11',
+ id: 'Java11',
+ comment: '//',
+ },
+ Pascal: {
+ name: 'Pascal',
+ id: 'Pascal',
+ comment: '//',
+ },
+};
+
+export function getAccountInfoFromEnv(): RemoteAccount | null {
+ const {
+ QOJ_HANDLE,
+ QOJ_PASSWORD,
+ QOJ_ENDPOINT = 'https://qoj.ac',
+ QOJ_PROXY,
+ } = process.env;
+
+ if (!QOJ_HANDLE || !QOJ_PASSWORD) return null;
+
+ const account: RemoteAccount = {
+ type: 'qoj',
+ handle: QOJ_HANDLE,
+ password: QOJ_PASSWORD,
+ endpoint: QOJ_ENDPOINT,
+ };
+
+ if (QOJ_PROXY) account.proxy = QOJ_PROXY;
+
+ return account;
+}
+
+export default class QOJProvider extends UOJProvider implements IBasicProvider {
+ constructor(public account: RemoteAccount) {
+ super(account);
+ }
+
+ static constructFromAccountData(data) {
+ return new this({
+ type: 'qoj',
+ cookie: Object.entries(data).map(([key, value]) => `${key}=${value}`),
+ });
+ }
+
+ cookie: string[] = [];
+ csrf: string = null;
+
+ get(url: string) {
+ logger.debug('get', url, this.cookie);
+
+ if (!url.includes('//'))
+ url = `${this.account.endpoint || 'https://qoj.ac'}${url}`;
+
+ const req = superagent
+ .get(url)
+ .set('Cookie', this.cookie)
+ .set('User-Agent', USER_AGENT);
+
+ 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://qoj.ac'}${url}`;
+
+ const req = superagent
+ .post(url)
+ .set('Cookie', this.cookie)
+ .set('User-Agent', USER_AGENT)
+ .type('form');
+
+ if (this.account.proxy) return req.proxy(this.account.proxy);
+
+ return req;
+ }
+
+ get loggedIn() {
+ return this.get('/login').then(
+ ({ text: html }) =>
+ !html.includes('
Login') &&
+ !html.includes('tr>td>a')
+ .innerHTML.split('#')[1];
+ }
+
+ async waitForSubmission(id: string, next, end) {
+ let count = 0;
+ let fail = 0;
+
+ while (count < 180 && fail < 10) {
+ count++;
+ await sleep(1000);
+
+ try {
+ 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 =
+ parseInt(summary.children[3]?.children[0]?.innerHTML || '') || 0;
+ const status = score === 100 ? 'Accepted' : 'Unaccepted';
+
+ return await end({
+ id,
+ status,
+ score,
+ time,
+ memory,
+ });
+ } catch (e) {
+ logger.error(e);
+
+ fail++;
+ }
+ }
+
+ return await end({
+ id,
+ error: true,
+ status: 'Judgment Failed',
+ message: 'Failed to fetch submission details.',
+ });
+ }
+}
diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts
index ab4deff..a3d0394 100644
--- a/remote_judger/src/vjudge.ts
+++ b/remote_judger/src/vjudge.ts
@@ -242,6 +242,7 @@ export async function apply(request: any) {
await vjudge.addProvider('uoj');
await vjudge.addProvider('loj');
await vjudge.importProvider('luogu');
+ await vjudge.addProvider('qoj');
return vjudge;
}
diff --git a/web/app/controllers/new_remote_problem.php b/web/app/controllers/new_remote_problem.php
index 6af5b90..d1bfb58 100644
--- a/web/app/controllers/new_remote_problem.php
+++ b/web/app/controllers/new_remote_problem.php
@@ -60,6 +60,14 @@ $new_remote_problem_form->addInput('remote_problem_id', [
$vdata['remote_problem_id'] = $id;
+ return '';
+ } else if ($remote_oj === 'qoj') {
+ if (!validateUInt($id)) {
+ return '不合法的题目 ID';
+ }
+
+ $vdata['remote_problem_id'] = $id;
+
return '';
}
@@ -153,6 +161,7 @@ $new_remote_problem_form->runAtServer();
UniversalOJ
LibreOJ
洛谷(不能使用公用账号提交)
+ Qingyu Online Judge
在导入题目前请先搜索题库中是否已经存在相应题目,避免重复添加。
diff --git a/web/app/models/UOJRemoteProblem.php b/web/app/models/UOJRemoteProblem.php
index c2eb23b..ce9fa63 100644
--- a/web/app/models/UOJRemoteProblem.php
+++ b/web/app/models/UOJRemoteProblem.php
@@ -51,6 +51,16 @@ class UOJRemoteProblem {
'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'],
'submit_type' => ['archive'],
],
+ 'qoj' => [
+ 'name' => 'Qingyu Online Judge',
+ 'short_name' => 'QOJ',
+ 'url' => 'https://qoj.ac',
+ 'not_exist_texts' => [
+ '未找到该页面',
+ ],
+ 'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Pascal'],
+ 'submit_type' => ['bot'],
+ ],
];
private static function curl_get($url) {
@@ -105,6 +115,10 @@ class UOJRemoteProblem {
return static::$providers['luogu']['url'] . '/problem/' . $id;
}
+ private static function getQojProblemUrl($id) {
+ return static::$providers['qoj']['url'] . '/problem/' . $id;
+ }
+
private static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
$remote_provider = static::$providers['codeforces'];
@@ -205,6 +219,55 @@ class UOJRemoteProblem {
];
}
+ private static function getUojLikeProblemBasicInfoFromHtml($id, $html, $oj = 'uoj') {
+ $remote_provider = static::$providers[$oj];
+
+ $dom = new \IvoPetkov\HTML5DOMDocument();
+ $dom->loadHTML($html);
+
+ $title_dom = $dom->querySelector('.page-header');
+ $title_matches = [];
+ preg_match('/^#[1-9][0-9]*\. (.*)$/', trim($title_dom->textContent), $title_matches);
+ $title = "【{$remote_provider['short_name']}{$id}】{$title_matches[1]}";
+
+ $statement_dom = $dom->querySelector('.uoj-article');
+ $statement = HTML::tag('h3', [], '题目描述');
+
+ foreach ($statement_dom->querySelectorAll('a') as &$elem) {
+ $href = $elem->getAttribute('href');
+ $href = getAbsoluteUrl($href, $remote_provider['url']);
+ $elem->setAttribute('href', $href);
+ }
+
+ $statement .= $statement_dom->innerHTML;
+
+ $res = [
+ 'type' => 'html',
+ 'title' => $title,
+ 'time_limit' => null,
+ 'memory_limit' => null,
+ 'difficulty' => -1,
+ 'statement' => $statement,
+ ];
+
+ if ($oj == 'qoj') { // QOJ PDF
+ $pdf_statement_dom = $dom->getElementById('statements-pdf');
+
+ if ($pdf_statement_dom) {
+ $pdf_url = $pdf_statement_dom->getAttribute('src');
+ $pdf_res = static::curl_get(getAbsoluteUrl($pdf_url, $remote_provider['url']));
+
+ if (str_starts_with($pdf_res['content-type'], 'application/pdf')) {
+ $res['type'] = 'pdf';
+ $res['pdf_data'] = $pdf_res['response'];
+ $res['statement'] = '';
+ }
+ }
+ }
+
+ return $res;
+ }
+
private static function getCodeforcesProblemBasicInfo($id) {
$res = static::curl_get(static::getCodeforcesProblemUrl($id));
@@ -224,14 +287,7 @@ class UOJRemoteProblem {
'memory_limit' => null,
'difficulty' => -1,
'pdf_data' => $res['response'],
- 'statement' => HTML::tag('h3', [], '提示') .
- HTML::tag(
- 'p',
- [],
- '若无法正常加载 PDF,请' .
- HTML::tag('a', ['href' => static::getCodeforcesProblemUrl($id), 'target' => '_blank'], '点此') .
- '查看原题面。'
- ),
+ 'statement' => '',
];
} else {
return null;
@@ -313,38 +369,19 @@ class UOJRemoteProblem {
}
private static function getUojProblemBasicInfo($id) {
- $remote_provider = static::$providers['uoj'];
$res = static::curl_get(static::getUojProblemUrl($id));
if (!$res) return null;
- $dom = new \IvoPetkov\HTML5DOMDocument();
- $dom->loadHTML($res['response']);
+ return static::getUojLikeProblemBasicInfoFromHtml($id, $res['response'], 'uoj');
+ }
- $title_dom = $dom->querySelector('.page-header');
- $title_matches = [];
- preg_match('/^#[1-9][0-9]*\. (.*)$/', trim($title_dom->textContent), $title_matches);
- $title = "【{$remote_provider['short_name']}{$id}】{$title_matches[1]}";
+ private static function getQojProblemBasicInfo($id) {
+ $res = static::curl_get(static::getQojProblemUrl($id));
- $statement_dom = $dom->querySelector('.uoj-article');
- $statement = HTML::tag('h3', [], '题目描述');
+ if (!$res) return null;
- foreach ($statement_dom->querySelectorAll('a') as &$elem) {
- $href = $elem->getAttribute('href');
- $href = getAbsoluteUrl($href, $remote_provider['url']);
- $elem->setAttribute('href', $href);
- }
-
- $statement .= $statement_dom->innerHTML;
-
- return [
- 'type' => 'html',
- 'title' => $title,
- 'time_limit' => null,
- 'memory_limit' => null,
- 'difficulty' => -1,
- 'statement' => $statement,
- ];
+ return static::getUojLikeProblemBasicInfoFromHtml($id, $res['response'], 'qoj');
}
private static function getLojProblemBasicInfo($id) {
@@ -490,6 +527,8 @@ class UOJRemoteProblem {
return static::getLojProblemUrl($id);
} else if ($oj === 'luogu') {
return static::getLuoguProblemUrl($id);
+ } else if ($oj === 'qoj') {
+ return static::getQojProblemUrl($id);
}
return null;
@@ -507,6 +546,8 @@ class UOJRemoteProblem {
return static::getLojProblemBasicInfo($id);
} else if ($oj === 'luogu') {
return static::getLuoguProblemBasicInfo($id);
+ } else if ($oj === 'qoj') {
+ return static::getQojProblemBasicInfo($id);
}
return null;