mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-25 21:58:41 +00:00
feat: Remote Judge for Luogu (#34)
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
dfc57f13d3
42
remote_judger/package-lock.json
generated
42
remote_judger/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"crlf-normalize": "^1.0.18",
|
"crlf-normalize": "^1.0.18",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"jsdom": "^21.0.0",
|
"jsdom": "^21.0.0",
|
||||||
|
"lodash.flattendeep": "^4.4.0",
|
||||||
"math-sum": "^2.0.0",
|
"math-sum": "^2.0.0",
|
||||||
"reggol": "^1.3.4",
|
"reggol": "^1.3.4",
|
||||||
"superagent": "^8.0.6",
|
"superagent": "^8.0.6",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsdom": "^20.0.1",
|
"@types/jsdom": "^20.0.1",
|
||||||
|
"@types/lodash.flattendeep": "^4.4.7",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/superagent": "^4.1.16",
|
"@types/superagent": "^4.1.16",
|
||||||
"@types/superagent-proxy": "^3.0.0",
|
"@types/superagent-proxy": "^3.0.0",
|
||||||
@ -78,6 +80,21 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.14.191",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||||
|
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash.flattendeep": {
|
||||||
|
"version": "4.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz",
|
||||||
|
"integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.18",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
@ -790,6 +807,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@ -1567,6 +1589,21 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.14.191",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||||
|
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/lodash.flattendeep": {
|
||||||
|
"version": "4.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz",
|
||||||
|
"integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.11.18",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
@ -2113,6 +2150,11 @@
|
|||||||
"type-check": "~0.3.2"
|
"type-check": "~0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lodash.flattendeep": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ=="
|
||||||
|
},
|
||||||
"lru-cache": {
|
"lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"crlf-normalize": "^1.0.18",
|
"crlf-normalize": "^1.0.18",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"jsdom": "^21.0.0",
|
"jsdom": "^21.0.0",
|
||||||
|
"lodash.flattendeep": "^4.4.0",
|
||||||
"math-sum": "^2.0.0",
|
"math-sum": "^2.0.0",
|
||||||
"reggol": "^1.3.4",
|
"reggol": "^1.3.4",
|
||||||
"superagent": "^8.0.6",
|
"superagent": "^8.0.6",
|
||||||
@ -24,6 +25,7 @@
|
|||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsdom": "^20.0.1",
|
"@types/jsdom": "^20.0.1",
|
||||||
|
"@types/lodash.flattendeep": "^4.4.7",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/superagent": "^4.1.16",
|
"@types/superagent": "^4.1.16",
|
||||||
"@types/superagent-proxy": "^3.0.0",
|
"@types/superagent-proxy": "^3.0.0",
|
||||||
|
@ -7,6 +7,7 @@ import * as TIME from './utils/time';
|
|||||||
import { apply } from './vjudge';
|
import { apply } from './vjudge';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import child from 'child_process';
|
import child from 'child_process';
|
||||||
|
import htmlspecialchars from './utils/htmlspecialchars';
|
||||||
|
|
||||||
proxy(superagent);
|
proxy(superagent);
|
||||||
|
|
||||||
@ -146,7 +147,8 @@ export default async function daemon(config: UOJConfig) {
|
|||||||
config.remote_problem_id,
|
config.remote_problem_id,
|
||||||
config.answer_language,
|
config.answer_language,
|
||||||
code,
|
code,
|
||||||
judge_time
|
judge_time,
|
||||||
|
config
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await request('/submit', {
|
await request('/submit', {
|
||||||
@ -157,7 +159,7 @@ export default async function daemon(config: UOJConfig) {
|
|||||||
status: 'Judged',
|
status: 'Judged',
|
||||||
score: 0,
|
score: 0,
|
||||||
error: 'Judgment Failed',
|
error: 'Judgment Failed',
|
||||||
details: `<error>No details.</error>`,
|
details: `<error>${htmlspecialchars(err.message)}</error>`,
|
||||||
}),
|
}),
|
||||||
judge_time,
|
judge_time,
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
export interface RemoteAccount {
|
export interface RemoteAccount {
|
||||||
type: string;
|
type: string;
|
||||||
cookie?: string[];
|
cookie?: string[];
|
||||||
handle: string;
|
handle?: string;
|
||||||
password: string;
|
password?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,10 @@ export default class AtcoderProvider implements IBasicProvider {
|
|||||||
this.account.endpoint ||= 'https://atcoder.jp';
|
this.account.endpoint ||= 'https://atcoder.jp';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constructFromAccountData(data) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
cookie: string[] = ['language=en'];
|
cookie: string[] = ['language=en'];
|
||||||
csrf: string;
|
csrf: string;
|
||||||
|
|
||||||
|
@ -87,6 +87,10 @@ export default class CodeforcesProvider implements IBasicProvider {
|
|||||||
this.account.endpoint ||= 'https://codeforces.com';
|
this.account.endpoint ||= 'https://codeforces.com';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constructFromAccountData(data) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
cookie: string[] = [];
|
cookie: string[] = [];
|
||||||
csrf: string;
|
csrf: string;
|
||||||
|
|
||||||
|
@ -156,6 +156,10 @@ export default class LibreojProvider implements IBasicProvider {
|
|||||||
this.account.endpoint ||= 'https://api.loj.ac.cn/api';
|
this.account.endpoint ||= 'https://api.loj.ac.cn/api';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constructFromAccountData(data) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
get(url: string) {
|
get(url: string) {
|
||||||
logger.debug('get', url);
|
logger.debug('get', url);
|
||||||
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
|
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
|
||||||
|
350
remote_judger/src/providers/luogu.ts
Normal file
350
remote_judger/src/providers/luogu.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
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 'lodash.flattendeep';
|
||||||
|
import htmlspecialchars from '../utils/htmlspecialchars';
|
||||||
|
|
||||||
|
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: '//',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildLuoguTestCaseInfoBlock(test) {
|
||||||
|
let res = '';
|
||||||
|
|
||||||
|
res += `<test num="${test.id + 1}" info="${STATUS_MAP[test.status]}" time="${
|
||||||
|
test.time || -1
|
||||||
|
}" memory="${test.memory || -1}" score="${test.score || ''}">`;
|
||||||
|
res += `<res>${htmlspecialchars(test.description || '')}</res>`;
|
||||||
|
res += '</test>';
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async safeGet(url: string) {
|
||||||
|
const res = await this.get(url);
|
||||||
|
|
||||||
|
if (res.text.startsWith('<html><script>document.location.reload()')) {
|
||||||
|
const sec = this.getCookie.call(
|
||||||
|
{ cookie: res.header['set-cookie'] },
|
||||||
|
'sec'
|
||||||
|
);
|
||||||
|
this.setCookie('sec', sec);
|
||||||
|
logger.debug('sec', sec);
|
||||||
|
|
||||||
|
return await this.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookie(target: string) {
|
||||||
|
return this.cookie
|
||||||
|
.find(i => i.startsWith(`${target}=`))
|
||||||
|
?.split('=')[1]
|
||||||
|
?.split(';')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(target: string, value: string) {
|
||||||
|
this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`));
|
||||||
|
this.cookie.push(`${target}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCsrfToken(url: string) {
|
||||||
|
let { text: html } = await this.safeGet(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.safeGet('/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) {
|
||||||
|
let fail = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
while (count < 120 && fail < 5) {
|
||||||
|
await sleep(1500);
|
||||||
|
count++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { body } = await this.safeGet(`/record/${id}?_contentOnly=1`);
|
||||||
|
const data = body.currentData.record;
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.detail.compileResult &&
|
||||||
|
data.detail.compileResult.success === false
|
||||||
|
) {
|
||||||
|
return await end({
|
||||||
|
error: true,
|
||||||
|
id: `R${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})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.status < 2) continue;
|
||||||
|
|
||||||
|
logger.info('RecordID:', id, 'done');
|
||||||
|
|
||||||
|
const status = STATUS_MAP[data.status];
|
||||||
|
let details = '';
|
||||||
|
|
||||||
|
details += `<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>`;
|
||||||
|
|
||||||
|
if (data.detail.judgeResult.subtasks.length === 1) {
|
||||||
|
details += Object.entries(
|
||||||
|
data.detail.judgeResult.subtasks[0].testCases
|
||||||
|
)
|
||||||
|
.map(o => o[1])
|
||||||
|
.map(buildLuoguTestCaseInfoBlock)
|
||||||
|
.join('\n');
|
||||||
|
} else {
|
||||||
|
details += Object.entries(data.detail.judgeResult.subtasks)
|
||||||
|
.map(o => o[1])
|
||||||
|
.map(
|
||||||
|
(subtask: any, index) =>
|
||||||
|
`<subtask num="${index}" info="${
|
||||||
|
STATUS_MAP[subtask.status]
|
||||||
|
}" time="${subtask.time || -1}" memory="${
|
||||||
|
subtask.memory || -1
|
||||||
|
}" score="${subtask.score || ''}">${Object.entries(
|
||||||
|
subtask.testCases
|
||||||
|
)
|
||||||
|
.map(o => o[1])
|
||||||
|
.map(buildLuoguTestCaseInfoBlock)
|
||||||
|
.join('\n')}</subtask>`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await end({
|
||||||
|
id: `R${id}`,
|
||||||
|
status,
|
||||||
|
score:
|
||||||
|
status === 'Accepted'
|
||||||
|
? 100
|
||||||
|
: (data.score / data.problem.fullScore) * 100,
|
||||||
|
time: data.time,
|
||||||
|
memory: data.memory,
|
||||||
|
details: `<div>${details}</div>`,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await end({
|
||||||
|
error: true,
|
||||||
|
id: `R${id}`,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: 'Failed to fetch submission details.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -99,6 +99,10 @@ export default class UOJProvider implements IBasicProvider {
|
|||||||
if (account.cookie) this.cookie = account.cookie;
|
if (account.cookie) this.cookie = account.cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constructFromAccountData(data) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
cookie: string[] = [];
|
cookie: string[] = [];
|
||||||
csrf: string;
|
csrf: string;
|
||||||
|
|
||||||
|
@ -8,11 +8,7 @@ const logger = new Logger('vjudge');
|
|||||||
class AccountService {
|
class AccountService {
|
||||||
api: IBasicProvider;
|
api: IBasicProvider;
|
||||||
|
|
||||||
constructor(
|
constructor(public Provider: BasicProvider, public account: RemoteAccount) {
|
||||||
public Provider: BasicProvider,
|
|
||||||
public account: RemoteAccount,
|
|
||||||
private request: any
|
|
||||||
) {
|
|
||||||
this.api = new Provider(account);
|
this.api = new Provider(account);
|
||||||
this.main().catch(e =>
|
this.main().catch(e =>
|
||||||
logger.error(`Error occured in ${account.type}/${account.handle}`, e)
|
logger.error(`Error occured in ${account.type}/${account.handle}`, e)
|
||||||
@ -24,7 +20,81 @@ class AccountService {
|
|||||||
problem_id: string,
|
problem_id: string,
|
||||||
language: string,
|
language: string,
|
||||||
code: string,
|
code: string,
|
||||||
judge_time: string
|
next,
|
||||||
|
end
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const rid = await this.api.submitProblem(
|
||||||
|
problem_id,
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
id,
|
||||||
|
next,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rid) return;
|
||||||
|
|
||||||
|
await this.api.waitForSubmission(problem_id, rid, next, end);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
|
||||||
|
await end({ error: true, status: 'Judgment Failed', message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login() {
|
||||||
|
const login = await this.api.ensureLogin();
|
||||||
|
if (login === true) {
|
||||||
|
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.warn(
|
||||||
|
`${this.account.type}/${this.account.handle}: login fail`,
|
||||||
|
login || ''
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async main() {
|
||||||
|
const res = await this.login();
|
||||||
|
if (!res) return;
|
||||||
|
setInterval(() => this.login(), Time.hour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VJudge {
|
||||||
|
private p_imports: Record<string, any> = {};
|
||||||
|
private providers: Record<string, AccountService> = {};
|
||||||
|
|
||||||
|
constructor(private request: any) {}
|
||||||
|
|
||||||
|
async importProvider(type: string) {
|
||||||
|
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
|
||||||
|
const provider = await import(`./providers/${type}`);
|
||||||
|
|
||||||
|
this.p_imports[type] = provider.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addProvider(type: string) {
|
||||||
|
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
|
||||||
|
const provider = await import(`./providers/${type}`);
|
||||||
|
const account = provider.getAccountInfoFromEnv();
|
||||||
|
|
||||||
|
if (!account) throw new Error(`no account info for ${type}`);
|
||||||
|
|
||||||
|
this.p_imports[type] = provider.default;
|
||||||
|
this.providers[type] = new AccountService(provider.default, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
async judge(
|
||||||
|
id: number,
|
||||||
|
type: string,
|
||||||
|
problem_id: string,
|
||||||
|
language: string,
|
||||||
|
code: string,
|
||||||
|
judge_time: string,
|
||||||
|
config
|
||||||
) {
|
) {
|
||||||
const next = async payload => {
|
const next = async payload => {
|
||||||
return await this.request('/submit', {
|
return await this.request('/submit', {
|
||||||
@ -69,16 +139,35 @@ class AccountService {
|
|||||||
details:
|
details:
|
||||||
payload.details ||
|
payload.details ||
|
||||||
'<div>' +
|
'<div>' +
|
||||||
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
|
`<info-block>REMOTE_SUBMISSION_ID = ${
|
||||||
`<info-block>VERDICT = ${payload.status}</info-block>` +
|
payload.id || 'None'
|
||||||
|
}\nVERDICT = ${payload.status}</info-block>` +
|
||||||
'</div>',
|
'</div>',
|
||||||
}),
|
}),
|
||||||
judge_time,
|
judge_time,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!config.remote_submit_type || config.remote_submit_type == 'bot') {
|
||||||
|
if (!this.providers[type]) throw new Error(`No provider ${type}`);
|
||||||
|
|
||||||
|
await this.providers[type].judge(
|
||||||
|
id,
|
||||||
|
problem_id,
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
next,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
} else if (config.remote_submit_type == 'my') {
|
||||||
|
if (!this.p_imports[type]) throw new Error(`No provider ${type}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rid = await this.api.submitProblem(
|
const provider = this.p_imports[type].constructFromAccountData(
|
||||||
|
JSON.parse(config.remote_account_data)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rid = await provider.submitProblem(
|
||||||
problem_id,
|
problem_id,
|
||||||
language,
|
language,
|
||||||
code,
|
code,
|
||||||
@ -89,63 +178,21 @@ class AccountService {
|
|||||||
|
|
||||||
if (!rid) return;
|
if (!rid) return;
|
||||||
|
|
||||||
await this.api.waitForSubmission(problem_id, rid, next, end);
|
await provider.waitForSubmission(problem_id, rid, next, end);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
await end({ error: true, status: 'Judgment Failed', message: e.message });
|
|
||||||
|
await end({
|
||||||
|
error: true,
|
||||||
|
status: 'Judgment Failed',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
throw new Error(
|
||||||
async login() {
|
'Unsupported remote submit type: ' + config.remote_submit_type
|
||||||
const login = await this.api.ensureLogin();
|
|
||||||
if (login === true) {
|
|
||||||
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
logger.warn(
|
|
||||||
`${this.account.type}/${this.account.handle}: login fail`,
|
|
||||||
login || ''
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async main() {
|
|
||||||
const res = await this.login();
|
|
||||||
if (!res) return;
|
|
||||||
setInterval(() => this.login(), Time.hour);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VJudge {
|
|
||||||
private providers: Record<string, AccountService> = {};
|
|
||||||
|
|
||||||
constructor(private request: any) {}
|
|
||||||
|
|
||||||
async addProvider(type: string) {
|
|
||||||
if (this.providers[type]) throw new Error(`duplicate provider ${type}`);
|
|
||||||
const provider = await import(`./providers/${type}`);
|
|
||||||
const account = provider.getAccountInfoFromEnv();
|
|
||||||
|
|
||||||
if (!account) throw new Error(`no account info for ${type}`);
|
|
||||||
|
|
||||||
this.providers[type] = new AccountService(
|
|
||||||
provider.default,
|
|
||||||
account,
|
|
||||||
this.request
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async judge(
|
|
||||||
id: number,
|
|
||||||
type: string,
|
|
||||||
problem_id: string,
|
|
||||||
language: string,
|
|
||||||
code: string,
|
|
||||||
judge_time: string
|
|
||||||
) {
|
|
||||||
if (!this.providers[type]) throw new Error(`no provider ${type}`);
|
|
||||||
|
|
||||||
await this.providers[type].judge(id, problem_id, language, code, judge_time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +203,7 @@ export async function apply(request: any) {
|
|||||||
await vjudge.addProvider('atcoder');
|
await vjudge.addProvider('atcoder');
|
||||||
await vjudge.addProvider('uoj');
|
await vjudge.addProvider('uoj');
|
||||||
await vjudge.addProvider('loj');
|
await vjudge.addProvider('loj');
|
||||||
|
await vjudge.importProvider('luogu');
|
||||||
|
|
||||||
return vjudge;
|
return vjudge;
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,16 @@ $new_remote_problem_form->addInput('remote_problem_id', [
|
|||||||
|
|
||||||
$vdata['remote_problem_id'] = $id;
|
$vdata['remote_problem_id'] = $id;
|
||||||
|
|
||||||
|
return '';
|
||||||
|
} else if ($remote_oj === 'luogu') {
|
||||||
|
$id = trim(strtoupper($id));
|
||||||
|
|
||||||
|
if (!validateLuoguProblemId($id)) {
|
||||||
|
return '不合法的题目 ID';
|
||||||
|
}
|
||||||
|
|
||||||
|
$vdata['remote_problem_id'] = $id;
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,14 +83,7 @@ $new_remote_problem_form->handle = function (&$vdata) {
|
|||||||
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
|
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
|
||||||
}
|
}
|
||||||
|
|
||||||
$submission_requirement = [
|
$submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
|
||||||
[
|
|
||||||
"name" => "answer",
|
|
||||||
"type" => "source code",
|
|
||||||
"file_name" => "answer.code",
|
|
||||||
"languages" => $remote_provider['languages'],
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$enc_submission_requirement = json_encode($submission_requirement);
|
$enc_submission_requirement = json_encode($submission_requirement);
|
||||||
|
|
||||||
$extra_config = [
|
$extra_config = [
|
||||||
|
@ -104,6 +104,19 @@ $custom_test_enabled = $custom_test_requirement && $pre_submit_check_ret === tru
|
|||||||
|
|
||||||
function handleUpload($zip_file_name, $content, $tot_size) {
|
function handleUpload($zip_file_name, $content, $tot_size) {
|
||||||
global $is_participating;
|
global $is_participating;
|
||||||
|
|
||||||
|
$remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge');
|
||||||
|
$remote_provider = UOJRemoteProblem::$providers[$remote_oj];
|
||||||
|
|
||||||
|
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];
|
||||||
|
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']];
|
||||||
|
}
|
||||||
|
|
||||||
UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating);
|
UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating);
|
||||||
}
|
}
|
||||||
function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
|
function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
|
||||||
@ -182,7 +195,7 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
|
|||||||
|
|
||||||
if (UOJProblem::cur()->userCanUploadSubmissionViaZip(Auth::user())) {
|
if (UOJProblem::cur()->userCanUploadSubmissionViaZip(Auth::user())) {
|
||||||
$zip_answer_form = newZipSubmissionForm(
|
$zip_answer_form = newZipSubmissionForm(
|
||||||
'zip-answer',
|
'zip_answer',
|
||||||
$submission_requirement,
|
$submission_requirement,
|
||||||
'FS::randomAvailableSubmissionFileName',
|
'FS::randomAvailableSubmissionFileName',
|
||||||
'handleUpload'
|
'handleUpload'
|
||||||
@ -198,6 +211,24 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
|
|||||||
'FS::randomAvailableSubmissionFileName',
|
'FS::randomAvailableSubmissionFileName',
|
||||||
'handleUpload'
|
'handleUpload'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (UOJProblem::info('type') == 'remote') {
|
||||||
|
$remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge');
|
||||||
|
$remote_pid = UOJProblem::cur()->getExtraConfig('remote_problem_id');
|
||||||
|
$remote_url = UOJRemoteProblem::getProblemRemoteUrl($remote_oj, $remote_pid);
|
||||||
|
$submit_type = json_encode(UOJRemoteProblem::$providers[$remote_oj]['submit_type']);
|
||||||
|
|
||||||
|
$answer_form->addNoVal('answer_remote_submit_type', '');
|
||||||
|
$answer_form->addNoVal('answer_remote_account_data', '');
|
||||||
|
$answer_form->appendHTML(<<<EOD
|
||||||
|
<h5>Remote Judge 配置</h5>
|
||||||
|
<div class="" id="answer-remote_submit_group"></div>
|
||||||
|
<script>
|
||||||
|
$('#answer-remote_submit_group').remote_submit_type_group("{$remote_oj}", "{$remote_pid}", "{$remote_url}", {$submit_type});
|
||||||
|
</script>
|
||||||
|
EOD);
|
||||||
|
}
|
||||||
|
|
||||||
$answer_form->extra_validator = $submission_extra_validator;
|
$answer_form->extra_validator = $submission_extra_validator;
|
||||||
$answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions';
|
$answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions';
|
||||||
$answer_form->runAtServer();
|
$answer_form->runAtServer();
|
||||||
|
@ -118,7 +118,7 @@ if (UOJProblem::info('type') == 'remote') {
|
|||||||
</ul>
|
</ul>
|
||||||
EOD);
|
EOD);
|
||||||
$re_crawl_form->config['submit_button']['text'] = '重新爬取';
|
$re_crawl_form->config['submit_button']['text'] = '重新爬取';
|
||||||
$re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id, $remote_provider) {
|
$re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id) {
|
||||||
try {
|
try {
|
||||||
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
|
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@ -134,14 +134,7 @@ if (UOJProblem::info('type') == 'remote') {
|
|||||||
$data['difficulty'] = UOJProblem::info('difficulty');
|
$data['difficulty'] = UOJProblem::info('difficulty');
|
||||||
}
|
}
|
||||||
|
|
||||||
$submission_requirement = [
|
$submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
|
||||||
[
|
|
||||||
"name" => "answer",
|
|
||||||
"type" => "source code",
|
|
||||||
"file_name" => "answer.code",
|
|
||||||
"languages" => $remote_provider['languages'],
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$enc_submission_requirement = json_encode($submission_requirement);
|
$enc_submission_requirement = json_encode($submission_requirement);
|
||||||
|
|
||||||
$extra_config = [
|
$extra_config = [
|
||||||
|
@ -46,6 +46,7 @@ function newAddDelCmdForm($form_name, $validate, $handle, $final = null) {
|
|||||||
|
|
||||||
function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
|
function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
|
||||||
$form = new UOJForm($form_name);
|
$form = new UOJForm($form_name);
|
||||||
|
|
||||||
foreach ($requirement as $req) {
|
foreach ($requirement as $req) {
|
||||||
if ($req['type'] == "source code") {
|
if ($req['type'] == "source code") {
|
||||||
$languages = UOJLang::getAvailableLanguages(isset($req['languages']) ? $req['languages'] : null);
|
$languages = UOJLang::getAvailableLanguages(isset($req['languages']) ? $req['languages'] : null);
|
||||||
@ -74,6 +75,7 @@ function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle
|
|||||||
$content = [];
|
$content = [];
|
||||||
$content['file_name'] = $zip_file_name;
|
$content['file_name'] = $zip_file_name;
|
||||||
$content['config'] = [];
|
$content['config'] = [];
|
||||||
|
|
||||||
foreach ($requirement as $req) {
|
foreach ($requirement as $req) {
|
||||||
if ($req['type'] == "source code") {
|
if ($req['type'] == "source code") {
|
||||||
$content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]];
|
$content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]];
|
||||||
|
@ -79,3 +79,7 @@ function is_short_string($str) {
|
|||||||
function validateCodeforcesProblemId($str) {
|
function validateCodeforcesProblemId($str) {
|
||||||
return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true;
|
return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateLuoguProblemId($str) {
|
||||||
|
return preg_match('/P[1-9][0-9]{4,5}/', $str) !== true;
|
||||||
|
}
|
||||||
|
@ -665,6 +665,7 @@ class UOJForm {
|
|||||||
EOD;
|
EOD;
|
||||||
} else {
|
} else {
|
||||||
echo <<<EOD
|
echo <<<EOD
|
||||||
|
$("#button-submit-{$this->form_name}").addClass('disabled');
|
||||||
return ok;
|
return ok;
|
||||||
EOD;
|
EOD;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ class UOJRemoteProblem {
|
|||||||
'ограничение по времени на тест',
|
'ограничение по времени на тест',
|
||||||
],
|
],
|
||||||
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
|
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
|
||||||
|
'submit_type' => ['bot'],
|
||||||
],
|
],
|
||||||
'atcoder' => [
|
'atcoder' => [
|
||||||
'name' => 'AtCoder',
|
'name' => 'AtCoder',
|
||||||
@ -24,6 +25,7 @@ class UOJRemoteProblem {
|
|||||||
'指定されたタスクが見つかりません',
|
'指定されたタスクが見つかりません',
|
||||||
],
|
],
|
||||||
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
|
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
|
||||||
|
'submit_type' => ['bot'],
|
||||||
],
|
],
|
||||||
'uoj' => [
|
'uoj' => [
|
||||||
'name' => 'UniversalOJ',
|
'name' => 'UniversalOJ',
|
||||||
@ -33,12 +35,21 @@ class UOJRemoteProblem {
|
|||||||
'未找到该页面',
|
'未找到该页面',
|
||||||
],
|
],
|
||||||
'languages' => ['C', 'C++03', 'C++11', 'C++', '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'],
|
||||||
|
'submit_type' => ['bot'],
|
||||||
],
|
],
|
||||||
'loj' => [
|
'loj' => [
|
||||||
'name' => 'LibreOJ',
|
'name' => 'LibreOJ',
|
||||||
'short_name' => 'LOJ',
|
'short_name' => 'LOJ',
|
||||||
'url' => 'https://loj.ac',
|
'url' => 'https://loj.ac',
|
||||||
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'],
|
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'],
|
||||||
|
'submit_type' => ['bot'],
|
||||||
|
],
|
||||||
|
'luogu' => [
|
||||||
|
'name' => '洛谷',
|
||||||
|
'short_name' => '洛谷',
|
||||||
|
'url' => 'https://www.luogu.com.cn',
|
||||||
|
'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'],
|
||||||
|
'submit_type' => ['my'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -90,6 +101,10 @@ class UOJRemoteProblem {
|
|||||||
return static::$providers['loj']['url'] . '/p/' . $id;
|
return static::$providers['loj']['url'] . '/p/' . $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static function getLuoguProblemUrl($id) {
|
||||||
|
return static::$providers['luogu']['url'] . '/problem/' . $id;
|
||||||
|
}
|
||||||
|
|
||||||
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
|
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
|
||||||
$remote_provider = static::$providers['codeforces'];
|
$remote_provider = static::$providers['codeforces'];
|
||||||
|
|
||||||
@ -394,6 +409,72 @@ class UOJRemoteProblem {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static function getLuoguProblemBasicInfo($id) {
|
||||||
|
$remote_provider = static::$providers['luogu'];
|
||||||
|
$res = static::curl_get(static::getLuoguProblemUrl($id) . '?_contentOnly=1');
|
||||||
|
|
||||||
|
if (!$res) return null;
|
||||||
|
|
||||||
|
// Convert stdClass to array
|
||||||
|
$res = json_decode(json_encode($res['response']), true);
|
||||||
|
|
||||||
|
if (!isset($res['code']) || $res['code'] != 200) return null;
|
||||||
|
|
||||||
|
$problem = $res['currentData']['problem'];
|
||||||
|
$statement = '';
|
||||||
|
|
||||||
|
if ($problem['background']) {
|
||||||
|
$statement .= "\n### 题目背景\n\n";
|
||||||
|
$statement .= $problem['background'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement .= "\n### 题目描述\n\n";
|
||||||
|
$statement .= $problem['description'] . "\n";
|
||||||
|
|
||||||
|
$statement .= "\n### 输入格式\n\n";
|
||||||
|
$statement .= $problem['inputFormat'] . "\n";
|
||||||
|
|
||||||
|
$statement .= "\n### 输出格式\n\n";
|
||||||
|
$statement .= $problem['outputFormat'] . "\n";
|
||||||
|
|
||||||
|
$statement .= "\n### 输入输出样例\n\n";
|
||||||
|
|
||||||
|
foreach ($problem['samples'] as $id => $sample) {
|
||||||
|
$display_sample_id = $id + 1;
|
||||||
|
|
||||||
|
$statement .= "\n#### 样例输入 #{$display_sample_id}\n\n";
|
||||||
|
$statement .= "\n```text\n{$sample[0]}\n```\n\n";
|
||||||
|
|
||||||
|
$statement .= "\n#### 样例输出 #{$display_sample_id}\n\n";
|
||||||
|
$statement .= "\n```text\n{$sample[1]}\n```\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement .= "\n### 说明/提示\n\n";
|
||||||
|
$statement .= $problem['hint'] . "\n";
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'html',
|
||||||
|
'title' => "【{$remote_provider['short_name']}{$problem['pid']}】{$problem['title']}",
|
||||||
|
'time_limit' => (float)max($problem['limits']['time']) / 1000.0,
|
||||||
|
'memory_limit' => (float)max($problem['limits']['memory']) / 1024.0,
|
||||||
|
'difficulty' => -1,
|
||||||
|
'statement' => HTML::parsedown()->text($statement),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubmissionRequirements($oj) {
|
||||||
|
$remote_provider = UOJRemoteProblem::$providers[$oj];
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
"name" => "answer",
|
||||||
|
"type" => "source code",
|
||||||
|
"file_name" => "answer.code",
|
||||||
|
"languages" => $remote_provider['languages'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public static function getProblemRemoteUrl($oj, $id) {
|
public static function getProblemRemoteUrl($oj, $id) {
|
||||||
if ($oj === 'codeforces') {
|
if ($oj === 'codeforces') {
|
||||||
return static::getCodeforcesProblemUrl($id);
|
return static::getCodeforcesProblemUrl($id);
|
||||||
@ -403,6 +484,8 @@ class UOJRemoteProblem {
|
|||||||
return static::getUojProblemUrl($id);
|
return static::getUojProblemUrl($id);
|
||||||
} else if ($oj === 'loj') {
|
} else if ($oj === 'loj') {
|
||||||
return static::getLojProblemUrl($id);
|
return static::getLojProblemUrl($id);
|
||||||
|
} else if ($oj === 'luogu') {
|
||||||
|
return static::getLuoguProblemUrl($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -418,6 +501,8 @@ class UOJRemoteProblem {
|
|||||||
return static::getUojProblemBasicInfo($id);
|
return static::getUojProblemBasicInfo($id);
|
||||||
} else if ($oj === 'loj') {
|
} else if ($oj === 'loj') {
|
||||||
return static::getLojProblemBasicInfo($id);
|
return static::getLojProblemBasicInfo($id);
|
||||||
|
} else if ($oj === 'luogu') {
|
||||||
|
return static::getLuoguProblemBasicInfo($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -418,6 +418,10 @@ class UOJSubmission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function userCanRejudge(array $user = null) {
|
public function userCanRejudge(array $user = null) {
|
||||||
|
if ($this->getContent('no_rejudge')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSuperUser($user)) {
|
if (isSuperUser($user)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
104
web/js/uoj.js
104
web/js/uoj.js
@ -947,6 +947,110 @@ $.fn.text_file_form_group = function(name, text) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remote judge submit type group
|
||||||
|
$.fn.remote_submit_type_group = function(oj, pid, url, submit_type) {
|
||||||
|
return this.each(function() {
|
||||||
|
var input_submit_type_bot_id = 'input-submit_type_bot';
|
||||||
|
var input_submit_type_my_id = 'input-submit_type_my';
|
||||||
|
var div_submit_type_bot_id = 'div-submit_type_bot';
|
||||||
|
var div_submit_type_my_id = 'div-submit_type_my';
|
||||||
|
|
||||||
|
var input_submit_type_bot = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_bot_id + '" value="bot" />');
|
||||||
|
var input_submit_type_my = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_my_id + '" value="my" />');
|
||||||
|
var input_my_account_data = $('<input type="hidden" name="answer_remote_account_data" value="" />');
|
||||||
|
|
||||||
|
var div_submit_type_bot = $('<div id="' + div_submit_type_bot_id + '" />')
|
||||||
|
.append('<div class="mt-3">将使用公用账号提交本题。</div>');
|
||||||
|
var div_submit_type_my = $('<div id="' + div_submit_type_my_id + '" />')
|
||||||
|
.append('<div class="mt-3">将使用您的账号提交本题。</div>');
|
||||||
|
|
||||||
|
input_submit_type_bot.click(function() {
|
||||||
|
div_submit_type_my.hide('fast');
|
||||||
|
div_submit_type_bot.show('fast');
|
||||||
|
});
|
||||||
|
input_submit_type_my.click(function() {
|
||||||
|
div_submit_type_bot.hide('fast');
|
||||||
|
div_submit_type_my.show('fast');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (submit_type[0] == 'bot') {
|
||||||
|
div_submit_type_my.hide();
|
||||||
|
input_submit_type_bot[0].checked = true;
|
||||||
|
} else if (submit_type[0] == 'my') {
|
||||||
|
div_submit_type_bot.hide();
|
||||||
|
input_submit_type_my[0].checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submit_type.indexOf('bot') == -1) {
|
||||||
|
input_submit_type_bot.attr('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
if (submit_type.indexOf('my') == -1) {
|
||||||
|
input_submit_type_my.attr('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oj == 'luogu') {
|
||||||
|
var luogu_account_data = {"_uid": "", "__client_id": ""};
|
||||||
|
var input_luogu_uid = $('<input class="form-control" type="text" name="luogu_uid" id="input-luogu_uid" />');
|
||||||
|
var input_luogu_client_id = $('<input class="form-control" type="text" name="luogu_client_id" id="input-luogu_client_id" />');
|
||||||
|
|
||||||
|
if ('localStorage' in window) {
|
||||||
|
try {
|
||||||
|
var luogu_account_data_str = localStorage.getItem('uoj_remote_judge_luogu_account_data');
|
||||||
|
if (luogu_account_data_str) {
|
||||||
|
luogu_account_data = JSON.parse(luogu_account_data_str);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
var save_luogu_account_data = function() {
|
||||||
|
localStorage.setItem('uoj_remote_judge_luogu_account_data', JSON.stringify(luogu_account_data));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var save_luogu_account_data = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
|
input_luogu_uid.change(function() {
|
||||||
|
luogu_account_data._uid = $(this).val();
|
||||||
|
input_my_account_data.val(JSON.stringify(luogu_account_data));
|
||||||
|
save_luogu_account_data();
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
$('<div class="row mt-3" />')
|
||||||
|
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_uid" class="form-col-label">_uid</label>'))
|
||||||
|
.append($('<div class="col-sm-4" />').append(input_luogu_uid))
|
||||||
|
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>_uid</code>。')))
|
||||||
|
).append(
|
||||||
|
$('<div class="row mt-3" />')
|
||||||
|
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_client_id" class="form-col-label">__client_id</label>'))
|
||||||
|
.append($('<div class="col-sm-4" />').append(input_luogu_client_id))
|
||||||
|
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>__client_id</code>。')))
|
||||||
|
).append(input_my_account_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).append(
|
||||||
|
$('<div class="mt-3" />').append(
|
||||||
|
$('<div class="form-check d-inline-block" />')
|
||||||
|
.append(input_submit_type_bot)
|
||||||
|
.append($('<label class="form-check-label" for="' + input_submit_type_bot_id + '" />').append(' 公用账号'))
|
||||||
|
).append(
|
||||||
|
$('<div class="form-check d-inline-block ms-3" />')
|
||||||
|
.append(input_submit_type_my)
|
||||||
|
.append($('<label class="form-check-label" for="' + input_submit_type_my_id + '" />').append(' 自有账号'))
|
||||||
|
)
|
||||||
|
).append(div_submit_type_bot).append(div_submit_type_my);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// custom test
|
// custom test
|
||||||
function custom_test_onsubmit(response_text, div_result, url) {
|
function custom_test_onsubmit(response_text, div_result, url) {
|
||||||
if (response_text != '') {
|
if (response_text != '') {
|
||||||
|
Loading…
Reference in New Issue
Block a user