From 5cfd8d58468c6ecd203d96f8f303b8d53bdd961e Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Fri, 3 Feb 2023 10:47:58 +0800 Subject: [PATCH] feat(remote_judger): add luogu --- remote_judger/src/interface.ts | 4 +- remote_judger/src/providers/luogu.ts | 281 +++++++++++++++++++++++++ remote_judger/src/utils/flattenDeep.ts | 6 + remote_judger/src/vjudge.ts | 14 +- web/app/controllers/problem.php | 4 +- web/js/uoj.js | 16 +- 6 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 remote_judger/src/providers/luogu.ts create mode 100644 remote_judger/src/utils/flattenDeep.ts diff --git a/remote_judger/src/interface.ts b/remote_judger/src/interface.ts index 77af4c5..579d507 100644 --- a/remote_judger/src/interface.ts +++ b/remote_judger/src/interface.ts @@ -1,8 +1,8 @@ export interface RemoteAccount { type: string; cookie?: string[]; - handle: string; - password: string; + handle?: string; + password?: string; endpoint?: string; proxy?: string; } diff --git a/remote_judger/src/providers/luogu.ts b/remote_judger/src/providers/luogu.ts new file mode 100644 index 0000000..7932499 --- /dev/null +++ b/remote_judger/src/providers/luogu.ts @@ -0,0 +1,281 @@ +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 sleep from '../utils/sleep'; +import flattenDeep from '../utils/flattenDeep'; + +proxy(superagent); +const logger = new Logger('remote/luogu'); + +const STATUS_MAP = [ + 'Waiting', // WAITING, + 'Judging', // JUDGING, + 'Compile Error', // CE + 'Output Limit Exceeded', // OLE + 'Memory Limit Exceeded', // MLE + 'Time Limit Exceeded', // TLE + 'Wrong Answer', // WA + 'Runtime Error', // RE + 0, + 0, + 0, + 'Judgment Failed', // UKE + 'Accepted', // AC + 0, + 'Wrong Answer', // WA +]; + +const LANGS_MAP = { + C: { + id: 2, + name: 'C', + comment: '//', + }, + 'C++98': { + id: 3, + name: 'C++98', + comment: '//', + }, + 'C++11': { + id: 4, + name: 'C++11', + comment: '//', + }, + 'C++': { + id: 11, + name: 'C++14', + comment: '//', + }, + 'C++17': { + id: 12, + name: 'C++17', + comment: '//', + }, + 'C++20': { + id: 27, + name: 'C++20', + comment: '//', + }, + Python3: { + id: 7, + name: 'Python 3', + comment: '#', + }, + Java8: { + id: 8, + name: 'Java 8', + comment: '//', + }, + Pascal: { + id: 1, + name: 'Pascal', + comment: '//', + }, +}; + +export default class LuoguProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + if (account.cookie) this.cookie = account.cookie; + } + + static constructFromAccountData(data) { + return new this({ + type: 'luogu', + cookie: Object.entries(data).map(([key, value]) => `${key}=${value}`), + }); + } + + cookie: string[] = []; + csrf: string; + + get(url: string) { + logger.debug('get', url, this.cookie); + + if (!url.includes('//')) + url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${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://www.luogu.com.cn'}${url}`; + + const req = superagent + .post(url) + .set('Cookie', this.cookie) + .set('x-csrf-token', this.csrf) + .set('User-Agent', USER_AGENT) + .set('x-requested-with', 'XMLHttpRequest') + .set('origin', 'https://www.luogu.com.cn'); + + if (this.account.proxy) return req.proxy(this.account.proxy); + + return req; + } + + async getCsrfToken(url: string) { + const { text: html } = await this.get(url); + const $dom = new JSDOM(html); + + this.csrf = $dom.window.document + .querySelector('meta[name="csrf-token"]') + .getAttribute('content'); + + logger.info('csrf-token=', this.csrf); + } + + get loggedIn() { + return this.get('/user/setting?_contentOnly=1').then( + ({ body }) => body.currentTemplate !== 'AuthLogin' + ); + } + + async ensureLogin() { + if (await this.loggedIn) { + await this.getCsrfToken('/user/setting'); + + return true; + } + + logger.info('retry login'); + + // TODO login; + + return false; + } + + async submitProblem( + id: string, + lang: string, + code: string, + submissionId: number, + next, + end + ) { + if (!(await this.ensureLogin())) { + end({ + error: true, + status: 'Judgment Failed', + message: 'Login failed', + }); + + return null; + } + + if (code.length < 10) { + end({ + error: true, + status: 'Compile Error', + message: 'Code too short', + }); + + 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 result = await this.post(`/fe/api/problem/submit/${id}`) + .set('referer', `https://www.luogu.com.cn/problem/${id}`) + .send({ + code, + lang: programType.id, + enableO2: 1, + }); + + logger.info('RecordID:', result.body.rid); + + return result.body.rid; + } + + async waitForSubmission(problem_id: string, id: string, next, end) { + const done = {}; + let fail = 0; + let count = 0; + let finished = 0; + + while (count < 120 && fail < 5) { + await sleep(1500); + count++; + + try { + const { body } = await this.get(`/record/${id}?_contentOnly=1`); + const data = body.currentData.record; + + if ( + data.detail.compileResult && + data.detail.compileResult.success === false + ) { + return await end({ + error: true, + id, + status: 'Compile Error', + message: data.detail.compileResult.message, + }); + } + + logger.info('Fetched with length', JSON.stringify(body).length); + const total = flattenDeep(body.currentData.testCaseGroup).length; + + // TODO sorted + + if (!data.detail.judgeResult?.subtasks) continue; + + for (const key in data.detail.judgeResult.subtasks) { + const subtask = data.detail.judgeResult.subtasks[key]; + for (const cid in subtask.testCases || {}) { + if (done[`${subtask.id}.${cid}`]) continue; + finished++; + done[`${subtask.id}.${cid}`] = true; + await next({ + status: `Judging (${(finished / total) * 100})`, + }); + } + } + + if (data.status < 2) continue; + + logger.info('RecordID:', id, 'done'); + + // TODO calc total status + + return await end({ + id: 'R' + id, + status: STATUS_MAP[data.status], + score: data.score, + time: data.time, + memory: data.memory, + }); + } catch (e) { + logger.error(e); + + fail++; + } + } + + return await end({ + error: true, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } +} diff --git a/remote_judger/src/utils/flattenDeep.ts b/remote_judger/src/utils/flattenDeep.ts new file mode 100644 index 0000000..92e68aa --- /dev/null +++ b/remote_judger/src/utils/flattenDeep.ts @@ -0,0 +1,6 @@ +const flattenDeep = arr => + Array.isArray(arr) + ? arr.reduce((a, b) => a.concat(flattenDeep(b)), []) + : [arr]; + +export default flattenDeep; diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts index dda6303..50d611f 100644 --- a/remote_judger/src/vjudge.ts +++ b/remote_judger/src/vjudge.ts @@ -139,8 +139,9 @@ class VJudge { details: payload.details || '
' + - `ID = ${payload.id || 'None'}` + - `VERDICT = ${payload.status}` + + `REMOTE_SUBMISSION_ID = ${ + payload.id || 'None' + }\nVERDICT = ${payload.status}` + '
', }), judge_time, @@ -187,11 +188,11 @@ class VJudge { message: e.message, }); } + } else { + throw new Error( + 'Unsupported remote submit type: ' + config.remote_submit_type + ); } - - throw new Error( - 'Unsupported remote submit type: ' + config.remote_submit_type - ); } } @@ -202,6 +203,7 @@ export async function apply(request: any) { await vjudge.addProvider('atcoder'); await vjudge.addProvider('uoj'); await vjudge.addProvider('loj'); + await vjudge.importProvider('luogu'); return vjudge; } diff --git a/web/app/controllers/problem.php b/web/app/controllers/problem.php index 29d2e00..ed3c704 100644 --- a/web/app/controllers/problem.php +++ b/web/app/controllers/problem.php @@ -110,7 +110,9 @@ function handleUpload($zip_file_name, $content, $tot_size) { if (UOJProblem::info('type') == 'remote') { $submit_type = in_array($_POST['answer_remote_submit_type'], $remote_provider['submit_type']) ? $_POST['answer_remote_submit_type'] : $remote_provider['submit_type'][0]; - $content['no_rejudge'] = true; + if ($submit_type != 'bot') { + $content['no_rejudge'] = true; + } $content['config'][] = ['remote_submit_type', $submit_type]; $content['config'][] = ['remote_account_data', $_POST['answer_remote_account_data']]; } diff --git a/web/js/uoj.js b/web/js/uoj.js index 652b357..3e1f1c2 100644 --- a/web/js/uoj.js +++ b/web/js/uoj.js @@ -989,9 +989,9 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { } if (oj == 'luogu') { - var luogu_account_data = {"_uid": "", "__clientid": ""}; + var luogu_account_data = {"_uid": "", "__client_id": ""}; var input_luogu_uid = $(''); - var input_luogu_clientid = $(''); + var input_luogu_client_id = $(''); if ('localStorage' in window) { try { @@ -1014,13 +1014,15 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { save_luogu_account_data(); }); - input_luogu_clientid.change(function() { - luogu_account_data.__clientid = $(this).val(); + input_luogu_client_id.change(function() { + luogu_account_data.__client_id = $(this).val(); input_my_account_data.val(JSON.stringify(luogu_account_data)); save_luogu_account_data(); }); input_my_account_data.val(JSON.stringify(luogu_account_data)); + input_luogu_uid.val(luogu_account_data._uid); + input_luogu_client_id.val(luogu_account_data.__client_id); div_submit_type_my.append( $('
') @@ -1029,9 +1031,9 @@ $.fn.remote_submit_type_group = function(oj, pid, url, submit_type) { .append($('
').append($('
').append('请填入 Cookie 中的 _uid。'))) ).append( $('
') - .append($('
').append('')) - .append($('
').append(input_luogu_clientid)) - .append($('
').append($('
').append('请填入 Cookie 中的 __clientid。'))) + .append($('
').append('')) + .append($('
').append(input_luogu_client_id)) + .append($('
').append($('
').append('请填入 Cookie 中的 __client_id。'))) ).append(input_my_account_data); }