From d48adda992313398f17f4009c142f05d3241263d Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 16 Sep 2021 17:59:39 +0800 Subject: [PATCH] vjudge: draft: luogu --- packages/vjudge/package.json | 2 +- packages/vjudge/src/interface.ts | 1 + packages/vjudge/src/providers/index.ts | 3 +- packages/vjudge/src/providers/luogu.ts | 177 +++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 packages/vjudge/src/providers/luogu.ts diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index ec6d1044d..965cd4e22 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/vjudge", - "version": "1.3.1", + "version": "1.3.2", "description": "Submit problems to remote oj", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/vjudge/src/interface.ts b/packages/vjudge/src/interface.ts index e6d9b540c..75e4fca1a 100644 --- a/packages/vjudge/src/interface.ts +++ b/packages/vjudge/src/interface.ts @@ -6,6 +6,7 @@ export interface RemoteAccount { password: string; endpoint?: string; proxy?: string; + query?: string; } declare module 'hydrooj/src/interface' { interface Collections { diff --git a/packages/vjudge/src/providers/index.ts b/packages/vjudge/src/providers/index.ts index a17b01d07..2da4d4290 100644 --- a/packages/vjudge/src/providers/index.ts +++ b/packages/vjudge/src/providers/index.ts @@ -1,4 +1,5 @@ import codeforces from './codeforces'; +import luogu from './luogu'; import spoj from './spoj'; -export = { codeforces, spoj }; +export = { codeforces, luogu, spoj }; diff --git a/packages/vjudge/src/providers/luogu.ts b/packages/vjudge/src/providers/luogu.ts new file mode 100644 index 000000000..4ecbe4c48 --- /dev/null +++ b/packages/vjudge/src/providers/luogu.ts @@ -0,0 +1,177 @@ +/* eslint-disable no-await-in-loop */ +import { JSDOM } from 'jsdom'; +import { flattenDeep } from 'lodash'; +import * as superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import { STATUS } from '@hydrooj/utils/lib/status'; +import { sleep } from '@hydrooj/utils/lib/utils'; +import { Logger } from 'hydrooj/src/logger'; +import * as setting from 'hydrooj/src/model/setting'; +import { IBasicProvider, RemoteAccount } from '../interface'; + +proxy(superagent as any); +const logger = new Logger('remote/luogu'); + +const STATUS_MAP = [ + STATUS.STATUS_WAITING, + STATUS.STATUS_JUDGING, + STATUS.STATUS_COMPILE_ERROR, + STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED, + STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, + STATUS.STATUS_TIME_LIMIT_EXCEEDED, + STATUS.STATUS_WRONG_ANSWER, + STATUS.STATUS_RUNTIME_ERROR, + 0, + 0, + 0, + STATUS.STATUS_SYSTEM_ERROR, + STATUS.STATUS_ACCEPTED, + 0, + STATUS.STATUS_WRONG_ANSWER, +]; + +const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36'; + +export default class LuoguProvider implements IBasicProvider { + constructor(public account: RemoteAccount, private save: (data: any) => Promise) { + 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://www.luogu.com.cn'}${url}`; + const req = superagent.get(url) + .set('Cookie', this.cookie) + .set('User-Agent', UA); + 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', UA) + .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) { + this.getCsrfToken('/'); + setInterval(() => this.getCsrfToken('/'), 5 * 60 * 1000); + return true; + } + logger.info('retry login'); + // TODO login; + return false; + } + + async getProblem(id: string) { + logger.info(id); + // TODO + return { + title: id, + data: {}, + files: {}, + tag: [], + content: '', + }; + } + + async listProblem() { + return []; + } + + async submitProblem(id: string, lang: string, code: string, info) { + const comment = setting.langs[lang]?.comment; + if (comment) { + const msg = `Hydro submission #${info.rid}@${new Date().toLocaleString()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + lang = lang.includes('luogu.') ? lang.split('luogu.')[1] : '0'; + const result = await this.post(`/fe/api/problem/submit/${id}${this.account.query || ''}`) + .set('referer', `https://www.luogu.com.cn/problem/${id}`) + // TODO O2 + .send({ code, lang: +lang, enableO2: 0 }); + logger.info('RecordID:', result.body.rid); + return result.body.rid; + } + + async waitForSubmission(id: string, next, end) { + const done = {}; + let fail = 0; + let count = 0; + while (count < 60 && 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) { + await next({ compilerText: data.detail.compileResult.message }); + return await end({ + status: STATUS.STATUS_COMPILE_ERROR, score: 0, time: 0, memory: 0, + }); + } + logger.info('Fetched with length', JSON.stringify(body).length); + const total = flattenDeep(body.currentData.testCaseGroup).length; + // TODO sorted + for (const subtask of data.detail?.judgeResult?.subtasks || []) { + for (const cid of Object.keys(subtask.testCases)) { + if (done[`${subtask.id}.${cid}`]) continue; + done[`${subtask.id}.${cid}`] = true; + const testcase = subtask.testCases[cid]; + await next({ + status: STATUS.STATUS_JUDGING, + case: { + status: STATUS_MAP[testcase.status], + time: testcase.time, + memory: testcase.memory, + message: testcase.description, + }, + progress: (Object.keys(done).length / total) * 100, + }); + } + } + if (data.status < 2) continue; + logger.info('RecordID:', id, 'done'); + // TODO calc total status + return await end({ + status: STATUS_MAP[data.status], + score: STATUS_MAP[data.status] === STATUS.STATUS_ACCEPTED ? 100 : 0, + time: data.time, + memory: data.memory, + }); + } catch (e) { + logger.error(e); + fail++; + } + } + return await end({ + status: STATUS.STATUS_SYSTEM_ERROR, + score: 0, + time: 0, + memory: 0, + }); + } +}