From 3a0bb632b86887c6f74e18e6276fae004cf81dd1 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Tue, 21 Mar 2023 11:02:50 +0800 Subject: [PATCH] feat(remote_judger/luogu): luogu open api --- remote_judger/package-lock.json | 11 -- remote_judger/package.json | 1 - remote_judger/src/providers/luogu.ts | 265 +++++++++++++++++++++------ remote_judger/src/vjudge.ts | 2 +- web/app/models/UOJRemoteProblem.php | 2 +- 5 files changed, 211 insertions(+), 70 deletions(-) diff --git a/remote_judger/package-lock.json b/remote_judger/package-lock.json index a7666aa..190b730 100644 --- a/remote_judger/package-lock.json +++ b/remote_judger/package-lock.json @@ -12,7 +12,6 @@ "crlf-normalize": "^1.0.18", "fs-extra": "^11.1.0", "jsdom": "^21.0.0", - "lodash.flattendeep": "^4.4.0", "math-sum": "^2.0.0", "reggol": "^1.3.4", "string-strip-html": "^13.1.0", @@ -826,11 +825,6 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" - }, "node_modules/lodash.trim": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/lodash.trim/-/lodash.trim-4.5.1.tgz", @@ -2288,11 +2282,6 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" - }, "lodash.trim": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/lodash.trim/-/lodash.trim-4.5.1.tgz", diff --git a/remote_judger/package.json b/remote_judger/package.json index d981f57..1add897 100644 --- a/remote_judger/package.json +++ b/remote_judger/package.json @@ -15,7 +15,6 @@ "crlf-normalize": "^1.0.18", "fs-extra": "^11.1.0", "jsdom": "^21.0.0", - "lodash.flattendeep": "^4.4.0", "math-sum": "^2.0.0", "reggol": "^1.3.4", "string-strip-html": "^13.1.0", diff --git a/remote_judger/src/providers/luogu.ts b/remote_judger/src/providers/luogu.ts index bacbbf2..965fb53 100644 --- a/remote_judger/src/providers/luogu.ts +++ b/remote_judger/src/providers/luogu.ts @@ -4,7 +4,6 @@ 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 'lodash.flattendeep'; import htmlspecialchars from '../utils/htmlspecialchars'; proxy(superagent); @@ -76,10 +75,58 @@ const LANGS_MAP = { }, }; +const API_LANGS_MAP = { + C: { + id: 'c/99/gcc', + name: 'C', + comment: '//', + }, + 'C++98': { + id: 'cxx/98/gcc', + name: 'C++98', + comment: '//', + }, + 'C++11': { + id: 'cxx/11/gcc', + name: 'C++11', + comment: '//', + }, + 'C++': { + id: 'cxx/14/gcc', + name: 'C++14', + comment: '//', + }, + 'C++17': { + id: 'cxx/17/gcc', + name: 'C++17', + comment: '//', + }, + 'C++20': { + id: 'cxx/20/gcc', + name: 'C++20', + comment: '//', + }, + Python3: { + id: 'python3/c', + name: 'Python 3', + comment: '#', + }, + Java8: { + id: 'java/8', + name: 'Java 8', + comment: '//', + }, + Pascal: { + id: 'pascal/fpc', + name: 'Pascal', + comment: '//', + }, +}; + function buildLuoguTestCaseInfoBlock(test) { let res = ''; - res += ``; res += `${htmlspecialchars(test.description || '')}`; @@ -88,6 +135,28 @@ function buildLuoguTestCaseInfoBlock(test) { return res; } +export function getAccountInfoFromEnv(): RemoteAccount | null { + const { + LUOGU_HANDLE, + LUOGU_PASSWORD, + LUOGU_ENDPOINT = 'https://open-v1.lgapi.cn', + LUOGU_PROXY, + } = process.env; + + if (!LUOGU_HANDLE || !LUOGU_PASSWORD) return null; + + const account: RemoteAccount = { + type: 'luogu-api', + handle: LUOGU_HANDLE, + password: LUOGU_PASSWORD, + endpoint: LUOGU_ENDPOINT, + }; + + if (LUOGU_PROXY) account.proxy = LUOGU_PROXY; + + return account; +} + export default class LuoguProvider implements IBasicProvider { constructor(public account: RemoteAccount) { if (account.cookie) this.cookie = account.cookie; @@ -101,19 +170,23 @@ export default class LuoguProvider implements IBasicProvider { } cookie: string[] = []; - csrf: string; + csrf: string = null; get(url: string) { - logger.debug('get', url, this.cookie); - if (!url.includes('//')) url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`; + logger.debug('get', url, this.cookie); + const req = superagent .get(url) .set('Cookie', this.cookie) .set('User-Agent', USER_AGENT); + if (this.account.type == 'luogu-api') { + req.auth(this.account.handle, this.account.password); + } + if (this.account.proxy) return req.proxy(this.account.proxy); return req; @@ -137,11 +210,11 @@ export default class LuoguProvider implements IBasicProvider { } post(url: string) { - logger.debug('post', url, this.cookie); - if (!url.includes('//')) url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`; + logger.debug('post', url, this.cookie); + const req = superagent .post(url) .set('Cookie', this.cookie) @@ -150,6 +223,10 @@ export default class LuoguProvider implements IBasicProvider { .set('x-requested-with', 'XMLHttpRequest') .set('origin', 'https://www.luogu.com.cn'); + if (this.account.type == 'luogu-api') { + req.auth(this.account.handle, this.account.password); + } + if (this.account.proxy) return req.proxy(this.account.proxy); return req; @@ -186,6 +263,8 @@ export default class LuoguProvider implements IBasicProvider { } async ensureLogin() { + if (this.account.type == 'luogu-api') return true; + if (await this.loggedIn) { await this.getCsrfToken('/user/setting'); @@ -227,7 +306,10 @@ export default class LuoguProvider implements IBasicProvider { return null; } - const programType = LANGS_MAP[lang] || LANGS_MAP['C++']; + const programType = + this.account.type == 'luogu-api' + ? API_LANGS_MAP[lang] || API_LANGS_MAP['C++'] + : LANGS_MAP[lang] || LANGS_MAP['C++']; const comment = programType.comment; if (comment) { @@ -237,20 +319,45 @@ export default class LuoguProvider implements IBasicProvider { 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({ + if (this.account.type == 'luogu-api') { + const result = await this.post('/judge/problem').send({ + pid: id, code, lang: programType.id, - enableO2: 1, + o2: 1, + trackId: submissionId, }); - logger.info('RecordID:', result.body.rid); + if (result.status == 402) { + await end({ + error: true, + id, + status: 'Judgment Failed', + message: 'Payment required.', + }); - return result.body.rid; + return null; + } + + return result.body.requestId; + } else { + 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 ensureIsOwnSubmission(id: string) { + if (this.account.type == 'luogu-api') return true; + const { body } = await this.safeGet(`/record/${id}?_contentOnly=1`); const current_uid = body.currentUser?.uid; @@ -278,19 +385,37 @@ export default class LuoguProvider implements IBasicProvider { count++; try { - const { body } = await this.safeGet(`/record/${id}?_contentOnly=1`); - const data = body.currentData.record; + let result, data, body; - if (!data) { + if (this.account.type == 'luogu-api') { + result = await this.get(`/judge/result?id=${id}`); + body = result.body; + data = body.data; + } else { + result = await this.safeGet(`/record/${id}?_contentOnly=1`); + body = result.body; + data = body.currentData.record; + } + + if (result.status == 204) { + await next({}); + + continue; + } + + if (result.status == 200 && !data) { return await end({ error: true, - id: `R${id}`, + id, status: 'Judgment Failed', message: 'Failed to fetch submission details.', }); } - if (data.problem.pid != problem_id) { + if ( + this.account.type != 'luogu-api' && + data.problem.pid != problem_id + ) { return await end({ id, error: true, @@ -299,30 +424,41 @@ export default class LuoguProvider implements IBasicProvider { }); } + if (this.account.type == 'luogu-api') { + data = { + ...data, + status: data.judge.status, + score: data.judge.score, + time: data.judge.time, + memory: data.judge.memory, + detail: { + compileResult: data.compile, + judgeResult: { + ...data.judge, + subtasks: data.judge.subtasks.map(sub => ({ + ...sub, + testCases: sub.cases, + })), + }, + }, + }; + } + if ( data.detail.compileResult && data.detail.compileResult.success === false ) { return await end({ error: true, - id: `R${id}`, + id, status: 'Compile Error', message: data.detail.compileResult.message, }); } - logger.info('Fetched with length', JSON.stringify(body).length); - const total = flattenDeep( - Object.entries(body.currentData.testCaseGroup || {}).map(o => o[1]) - ).length; - if (!data.detail.judgeResult?.subtasks) continue; - await next({ - status: `Judging (${ - data.detail.judgeResult?.finishedCaseCount || '?' - }/${total})`, - }); + await next({}); if (data.status < 2) continue; @@ -331,24 +467,31 @@ export default class LuoguProvider implements IBasicProvider { const status = STATUS_MAP[data.status]; let details = ''; - details += - '' + - '' + - Object.entries({ - 题目: `${data.problem.pid} ${htmlspecialchars(data.problem.title)}`, - 提交记录: `R${id}`, - 提交时间: new Date(data.submitTime * 1000).toLocaleString('zh-CN'), - 账号: `${data.user.name}`, - 状态: status, - }) - .map( - o => `${o[1]}` - ) - .join('') + - '' + - ''; + if (this.account.type != 'luogu-api') { + details += + '' + + '' + + Object.entries({ + 题目: `${data.problem.pid} ${htmlspecialchars( + data.problem.title + )}`, + 提交记录: `R${id}`, + 提交时间: new Date(data.submitTime * 1000).toLocaleString( + 'zh-CN' + ), + 账号: `${data.user.name}`, + 状态: status, + }) + .map( + o => + `${o[1]}` + ) + .join('') + + '' + + ''; + } details += ''; @@ -357,7 +500,12 @@ export default class LuoguProvider implements IBasicProvider { data.detail.judgeResult.subtasks[0].testCases ) .map(o => o[1]) - .map(buildLuoguTestCaseInfoBlock) + .map((testcase: any) => + buildLuoguTestCaseInfoBlock({ + ...testcase, + id: testcase.id + (this.account.type != 'luogu-api'), + }) + ) .join('\n'); } else { details += Object.entries(data.detail.judgeResult.subtasks) @@ -372,7 +520,12 @@ export default class LuoguProvider implements IBasicProvider { subtask.testCases ) .map(o => o[1]) - .map(buildLuoguTestCaseInfoBlock) + .map((testcase: any) => + buildLuoguTestCaseInfoBlock({ + ...testcase, + id: testcase.id + (this.account.type != 'luogu-api'), + }) + ) .join('\n')}` ) .join('\n'); @@ -381,15 +534,15 @@ export default class LuoguProvider implements IBasicProvider { details += ''; return await end({ - id: `R${id}`, + id, status, - score: - status === 'Accepted' - ? 100 - : (data.score / data.problem.fullScore) * 100, + score: status === 'Accepted' ? 100 : Math.min(97, data.score), time: data.time, memory: data.memory, - details: `
${details}
`, + details: + this.account.type != 'luogu-api' + ? `
${details}
` + : details, }); } catch (e) { logger.error(e); diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts index a3d0394..4365f8c 100644 --- a/remote_judger/src/vjudge.ts +++ b/remote_judger/src/vjudge.ts @@ -241,7 +241,7 @@ export async function apply(request: any) { await vjudge.addProvider('atcoder'); await vjudge.addProvider('uoj'); await vjudge.addProvider('loj'); - await vjudge.importProvider('luogu'); + await vjudge.addProvider('luogu'); await vjudge.addProvider('qoj'); return vjudge; diff --git a/web/app/models/UOJRemoteProblem.php b/web/app/models/UOJRemoteProblem.php index ce9fa63..b13f506 100644 --- a/web/app/models/UOJRemoteProblem.php +++ b/web/app/models/UOJRemoteProblem.php @@ -49,7 +49,7 @@ class UOJRemoteProblem { 'short_name' => '洛谷', 'url' => 'https://www.luogu.com.cn', 'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'], - 'submit_type' => ['archive'], + 'submit_type' => ['bot', 'my', 'archive'], ], 'qoj' => [ 'name' => 'Qingyu Online Judge',