mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-27 20:16:22 +00:00
feat(remote_judger/luogu): luogu open api (#48)
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
95733e3905
11
remote_judger/package-lock.json
generated
11
remote_judger/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 += `<test num="${test.id + 1}" info="${STATUS_MAP[test.status]}" time="${
|
||||
res += `<test num="${test.id}" info="${STATUS_MAP[test.status]}" time="${
|
||||
test.time || -1
|
||||
}" memory="${test.memory || -1}" score="${test.score || ''}">`;
|
||||
res += `<res>${htmlspecialchars(test.description || '')}</res>`;
|
||||
@ -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,6 +319,28 @@ export default class LuoguProvider implements IBasicProvider {
|
||||
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
|
||||
}
|
||||
|
||||
if (this.account.type == 'luogu-api') {
|
||||
const result = await this.post('/judge/problem').send({
|
||||
pid: id,
|
||||
code,
|
||||
lang: programType.id,
|
||||
o2: 1,
|
||||
trackId: submissionId,
|
||||
});
|
||||
|
||||
if (result.status == 402) {
|
||||
await end({
|
||||
error: true,
|
||||
id,
|
||||
status: 'Judgment Failed',
|
||||
message: 'Payment required.',
|
||||
});
|
||||
|
||||
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({
|
||||
@ -249,8 +353,11 @@ export default class LuoguProvider implements IBasicProvider {
|
||||
|
||||
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;
|
||||
@ -273,24 +380,42 @@ export default class LuoguProvider implements IBasicProvider {
|
||||
let fail = 0;
|
||||
let count = 0;
|
||||
|
||||
while (count < 180 && fail < 10) {
|
||||
await sleep(1000);
|
||||
while (count < 360 && fail < 10) {
|
||||
await sleep(500);
|
||||
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({ status: '[Luogu] Judging' });
|
||||
|
||||
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,29 +424,55 @@ 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;
|
||||
|
||||
const finishedTestCases = Object.entries(
|
||||
data.detail.judgeResult.subtasks
|
||||
)
|
||||
.map(o => o[1])
|
||||
.reduce(
|
||||
(acc: number, sub: any) =>
|
||||
acc +
|
||||
Object.entries(sub.testCases as any[])
|
||||
.map(o => o[1])
|
||||
.filter(test => test.status >= 2).length,
|
||||
0
|
||||
);
|
||||
|
||||
await next({
|
||||
status: `Judging (${
|
||||
data.detail.judgeResult?.finishedCaseCount || '?'
|
||||
}/${total})`,
|
||||
status: `[Luogu] Judging (${finishedTestCases} judged)`,
|
||||
});
|
||||
|
||||
if (data.status < 2) continue;
|
||||
@ -331,24 +482,31 @@ export default class LuoguProvider implements IBasicProvider {
|
||||
const status = STATUS_MAP[data.status];
|
||||
let details = '';
|
||||
|
||||
if (this.account.type != 'luogu-api') {
|
||||
details +=
|
||||
'<remote-result-container>' +
|
||||
'<remote-result-table>' +
|
||||
Object.entries({
|
||||
题目: `<a href="https://www.luogu.com.cn/problem/${
|
||||
data.problem.pid
|
||||
}">${data.problem.pid} ${htmlspecialchars(data.problem.title)}</a>`,
|
||||
}">${data.problem.pid} ${htmlspecialchars(
|
||||
data.problem.title
|
||||
)}</a>`,
|
||||
提交记录: `<a href="https://www.luogu.com.cn/record/${id}">R${id}</a>`,
|
||||
提交时间: new Date(data.submitTime * 1000).toLocaleString('zh-CN'),
|
||||
提交时间: new Date(data.submitTime * 1000).toLocaleString(
|
||||
'zh-CN'
|
||||
),
|
||||
账号: `<a href="https://www.luogu.com.cn/user/${data.user.uid}">${data.user.name}</a>`,
|
||||
状态: status,
|
||||
})
|
||||
.map(
|
||||
o => `<remote-result-tr name="${o[0]}">${o[1]}</remote-result-tr>`
|
||||
o =>
|
||||
`<remote-result-tr name="${o[0]}">${o[1]}</remote-result-tr>`
|
||||
)
|
||||
.join('') +
|
||||
'</remote-result-table>' +
|
||||
'</remote-result-container>';
|
||||
}
|
||||
|
||||
details += '<tests>';
|
||||
|
||||
@ -357,7 +515,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 +535,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')}</subtask>`
|
||||
)
|
||||
.join('\n');
|
||||
@ -381,15 +549,15 @@ export default class LuoguProvider implements IBasicProvider {
|
||||
details += '</tests>';
|
||||
|
||||
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: `<div>${details}</div>`,
|
||||
details:
|
||||
this.account.type != 'luogu-api'
|
||||
? `<div>${details}</div>`
|
||||
: details,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user