mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-08 13:38:41 +00:00
feat(remote_judger): add uoj
This commit is contained in:
parent
6e945ef711
commit
61545c6807
252
remote_judger/src/providers/uoj.ts
Normal file
252
remote_judger/src/providers/uoj.ts
Normal file
@ -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('<title>登录')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
remote_judger/src/utils/parse.ts
Normal file
20
remote_judger/src/utils/parse.ts
Normal file
@ -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()]);
|
||||||
|
}
|
@ -31,7 +31,9 @@ class AccountService {
|
|||||||
'update-status': true,
|
'update-status': true,
|
||||||
fetch_new: false,
|
fetch_new: false,
|
||||||
id,
|
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('codeforces');
|
||||||
await vjudge.addProvider('atcoder');
|
await vjudge.addProvider('atcoder');
|
||||||
|
await vjudge.addProvider('uoj');
|
||||||
|
|
||||||
return vjudge;
|
return vjudge;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ class UOJRemoteProblem {
|
|||||||
'not_exist_texts' => [
|
'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'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user