mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-21 20:28:41 +00:00
feat: add qoj remote judge (#47)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
558bc86c14
296
remote_judger/src/providers/qoj.ts
Normal file
296
remote_judger/src/providers/qoj.ts
Normal file
@ -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('<title>Login') &&
|
||||||
|
!html.includes('<input type="password"')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureLogin() {
|
||||||
|
if (await this.loggedIn) return true;
|
||||||
|
|
||||||
|
if (!this.account.handle) return false;
|
||||||
|
|
||||||
|
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,
|
||||||
|
submissionId: number,
|
||||||
|
next,
|
||||||
|
end
|
||||||
|
) {
|
||||||
|
if (!(await this.ensureLogin())) {
|
||||||
|
await end({
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Login failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
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 _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('href="/submissions?submitter=' + this.account.handle)) {
|
||||||
|
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(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.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -242,6 +242,7 @@ export async function apply(request: any) {
|
|||||||
await vjudge.addProvider('uoj');
|
await vjudge.addProvider('uoj');
|
||||||
await vjudge.addProvider('loj');
|
await vjudge.addProvider('loj');
|
||||||
await vjudge.importProvider('luogu');
|
await vjudge.importProvider('luogu');
|
||||||
|
await vjudge.addProvider('qoj');
|
||||||
|
|
||||||
return vjudge;
|
return vjudge;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,14 @@ $new_remote_problem_form->addInput('remote_problem_id', [
|
|||||||
|
|
||||||
$vdata['remote_problem_id'] = $id;
|
$vdata['remote_problem_id'] = $id;
|
||||||
|
|
||||||
|
return '';
|
||||||
|
} else if ($remote_oj === 'qoj') {
|
||||||
|
if (!validateUInt($id)) {
|
||||||
|
return '不合法的题目 ID';
|
||||||
|
}
|
||||||
|
|
||||||
|
$vdata['remote_problem_id'] = $id;
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +161,7 @@ $new_remote_problem_form->runAtServer();
|
|||||||
<li><a href="https://uoj.ac/problems">UniversalOJ</a></li>
|
<li><a href="https://uoj.ac/problems">UniversalOJ</a></li>
|
||||||
<li><a href="https://loj.ac/p">LibreOJ</a></li>
|
<li><a href="https://loj.ac/p">LibreOJ</a></li>
|
||||||
<li><a href="https://www.luogu.com.cn/problem/list">洛谷</a>(不能使用公用账号提交)</li>
|
<li><a href="https://www.luogu.com.cn/problem/list">洛谷</a>(不能使用公用账号提交)</li>
|
||||||
|
<li><a href="https://qoj.ac/problems">Qingyu Online Judge</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>在导入题目前请先搜索题库中是否已经存在相应题目,避免重复添加。</li>
|
<li>在导入题目前请先搜索题库中是否已经存在相应题目,避免重复添加。</li>
|
||||||
|
@ -51,6 +51,16 @@ class UOJRemoteProblem {
|
|||||||
'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'],
|
'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'],
|
||||||
'submit_type' => ['archive'],
|
'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) {
|
private static function curl_get($url) {
|
||||||
@ -105,6 +115,10 @@ class UOJRemoteProblem {
|
|||||||
return static::$providers['luogu']['url'] . '/problem/' . $id;
|
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) {
|
private static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
|
||||||
$remote_provider = static::$providers['codeforces'];
|
$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) {
|
private static function getCodeforcesProblemBasicInfo($id) {
|
||||||
$res = static::curl_get(static::getCodeforcesProblemUrl($id));
|
$res = static::curl_get(static::getCodeforcesProblemUrl($id));
|
||||||
|
|
||||||
@ -224,14 +287,7 @@ class UOJRemoteProblem {
|
|||||||
'memory_limit' => null,
|
'memory_limit' => null,
|
||||||
'difficulty' => -1,
|
'difficulty' => -1,
|
||||||
'pdf_data' => $res['response'],
|
'pdf_data' => $res['response'],
|
||||||
'statement' => HTML::tag('h3', [], '提示') .
|
'statement' => '',
|
||||||
HTML::tag(
|
|
||||||
'p',
|
|
||||||
[],
|
|
||||||
'若无法正常加载 PDF,请' .
|
|
||||||
HTML::tag('a', ['href' => static::getCodeforcesProblemUrl($id), 'target' => '_blank'], '点此') .
|
|
||||||
'查看原题面。'
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -313,38 +369,19 @@ class UOJRemoteProblem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static function getUojProblemBasicInfo($id) {
|
private static function getUojProblemBasicInfo($id) {
|
||||||
$remote_provider = static::$providers['uoj'];
|
|
||||||
$res = static::curl_get(static::getUojProblemUrl($id));
|
$res = static::curl_get(static::getUojProblemUrl($id));
|
||||||
|
|
||||||
if (!$res) return null;
|
if (!$res) return null;
|
||||||
|
|
||||||
$dom = new \IvoPetkov\HTML5DOMDocument();
|
return static::getUojLikeProblemBasicInfoFromHtml($id, $res['response'], 'uoj');
|
||||||
$dom->loadHTML($res['response']);
|
}
|
||||||
|
|
||||||
$title_dom = $dom->querySelector('.page-header');
|
private static function getQojProblemBasicInfo($id) {
|
||||||
$title_matches = [];
|
$res = static::curl_get(static::getQojProblemUrl($id));
|
||||||
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');
|
if (!$res) return null;
|
||||||
$statement = HTML::tag('h3', [], '题目描述');
|
|
||||||
|
|
||||||
foreach ($statement_dom->querySelectorAll('a') as &$elem) {
|
return static::getUojLikeProblemBasicInfoFromHtml($id, $res['response'], 'qoj');
|
||||||
$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,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getLojProblemBasicInfo($id) {
|
private static function getLojProblemBasicInfo($id) {
|
||||||
@ -490,6 +527,8 @@ class UOJRemoteProblem {
|
|||||||
return static::getLojProblemUrl($id);
|
return static::getLojProblemUrl($id);
|
||||||
} else if ($oj === 'luogu') {
|
} else if ($oj === 'luogu') {
|
||||||
return static::getLuoguProblemUrl($id);
|
return static::getLuoguProblemUrl($id);
|
||||||
|
} else if ($oj === 'qoj') {
|
||||||
|
return static::getQojProblemUrl($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -507,6 +546,8 @@ class UOJRemoteProblem {
|
|||||||
return static::getLojProblemBasicInfo($id);
|
return static::getLojProblemBasicInfo($id);
|
||||||
} else if ($oj === 'luogu') {
|
} else if ($oj === 'luogu') {
|
||||||
return static::getLuoguProblemBasicInfo($id);
|
return static::getLuoguProblemBasicInfo($id);
|
||||||
|
} else if ($oj === 'qoj') {
|
||||||
|
return static::getQojProblemBasicInfo($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
Loading…
Reference in New Issue
Block a user