From 48811657307e9826e938b69eea26c0581a545742 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 30 Jun 2021 21:24:55 +0800 Subject: [PATCH] vjudge: Hello, SPOJ Co-authored-by: Spacelessd@outlook.com --- .../ui-default/components/monaco/index.js | 5 +- packages/ui-default/package.json | 2 +- .../ui-default/pages/problem_submit.page.js | 3 +- packages/vjudge/package.json | 2 +- packages/vjudge/src/interface.ts | 2 +- packages/vjudge/src/model.ts | 6 +- packages/vjudge/src/providers/index.ts | 3 +- packages/vjudge/src/providers/spoj.ts | 176 ++++++++++++++++++ 8 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 packages/vjudge/src/providers/spoj.ts diff --git a/packages/ui-default/components/monaco/index.js b/packages/ui-default/components/monaco/index.js index f9cdc743d..a3505e4a1 100644 --- a/packages/ui-default/components/monaco/index.js +++ b/packages/ui-default/components/monaco/index.js @@ -54,7 +54,10 @@ const problemConfigSchema = { def: problemConfigSchemaDef, properties: { key: { type: 'string', pattern: '[0-9a-f]{32}' }, - type: { enum: ['default', 'interactive', 'submit_answer', 'subjective'] }, + type: { enum: ['default', 'interactive', 'submit_answer', 'subjective', 'remote_judge'] }, + subType: { type: 'string' }, + langs: { type: 'array', items: { type: 'string' } }, + target: { type: 'string' }, checker_type: { enum: ['default', 'lemon', 'syzoj', 'testlib', 'strict', 'qduoj'] }, checker: { type: 'string', pattern: '\\.' }, interactor: { type: 'string', pattern: '\\.' }, diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index f766aa2a0..c71279afc 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.12.3", + "version": "4.12.4", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/problem_submit.page.js b/packages/ui-default/pages/problem_submit.page.js index b0e63596f..f8c48cd8e 100644 --- a/packages/ui-default/pages/problem_submit.page.js +++ b/packages/ui-default/pages/problem_submit.page.js @@ -30,9 +30,8 @@ const page = new NamedPage(['problem_submit', 'contest_detail_problem_submit', ' } } const main = {}; - const prefix = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0])); for (const key in window.LANGS) { - if (UiContext.pdocConfig.langs && !prefix.has(key) && !UiContext.pdocConfig.langs.includes(key)) continue; + if (UiContext.pdocConfig.langs && !UiContext.pdocConfig.langs.filter((i) => i === key || i.startsWith(`${key}.`)).length) continue; if (window.LANGS[key].domain && !window.LANGS[key].domain.includes(UiContext.domainId)) continue; if (UiContext.domain.langs && !`,${UiContext.domain.langs},`.includes(`,${key},`)) continue; if (!key.includes('.')) main[key] = window.LANGS[key].display; diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index b4b914b63..81771d6dc 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/vjudge", - "version": "1.0.15", + "version": "1.1.0", "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 8aaf8b506..dc9d09d54 100644 --- a/packages/vjudge/src/interface.ts +++ b/packages/vjudge/src/interface.ts @@ -27,7 +27,7 @@ export interface IBasicProvider { content: string; }>; listProblem(page: number, resync: boolean): Promise; - submitProblem(id: string, lang: string, code: string, info): Promise; + submitProblem(id: string, lang: string, code: string, info: any, next: any, end: any): Promise; waitForSubmission(id: string, next: any, end: any): Promise; } diff --git a/packages/vjudge/src/model.ts b/packages/vjudge/src/model.ts index 6914b5f95..abc20d3ff 100644 --- a/packages/vjudge/src/model.ts +++ b/packages/vjudge/src/model.ts @@ -6,6 +6,7 @@ import * as Judge from 'hydrooj/dist/handler/judge'; import TaskModel from 'hydrooj/dist/model/task'; import DomainModel from 'hydrooj/dist/model/domain'; import ProblemModel from 'hydrooj/dist/model/problem'; +import { STATUS } from 'hydrooj/dist/model/builtin'; import { sleep } from '@hydrooj/utils/lib/utils'; import providers from './providers/index'; import { BasicProvider, IBasicProvider, RemoteAccount } from './interface'; @@ -26,9 +27,12 @@ class Service { } async judge(task) { - const rid = await this.api.submitProblem(task.target, task.lang, task.code, task); const next = (payload) => Judge.next({ ...payload, rid: task.rid }); const end = (payload) => Judge.end({ ...payload, rid: task.rid }); + await next({ status: STATUS.STATUS_FETCHED }); + const rid = await this.api.submitProblem(task.target, task.lang, task.code, task, next, end); + if (!rid) return; + await next({ status: STATUS.STATUS_JUDGING }); await this.api.waitForSubmission(rid, next, end); } diff --git a/packages/vjudge/src/providers/index.ts b/packages/vjudge/src/providers/index.ts index 8734aabbd..a17b01d07 100644 --- a/packages/vjudge/src/providers/index.ts +++ b/packages/vjudge/src/providers/index.ts @@ -1,3 +1,4 @@ import codeforces from './codeforces'; +import spoj from './spoj'; -export = { codeforces }; +export = { codeforces, spoj }; diff --git a/packages/vjudge/src/providers/spoj.ts b/packages/vjudge/src/providers/spoj.ts new file mode 100644 index 000000000..092086ece --- /dev/null +++ b/packages/vjudge/src/providers/spoj.ts @@ -0,0 +1,176 @@ +/* eslint-disable no-await-in-loop */ +import * as superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import { JSDOM } from 'jsdom'; +import { PassThrough } from 'stream'; +import { STATUS } from '@hydrooj/utils/lib/status'; +import { parseMemoryMB, sleep } from '@hydrooj/utils/lib/utils'; +import * as setting from 'hydrooj/dist/model/setting'; +import { Logger } from 'hydrooj/dist/logger'; +import { IBasicProvider, RemoteAccount } from '../interface'; + +proxy(superagent); +const logger = new Logger('spoj'); + +const VERDICT = { + 11: STATUS.STATUS_COMPILE_ERROR, + 12: STATUS.STATUS_RUNTIME_ERROR, + 13: STATUS.STATUS_TIME_LIMIT_EXCEEDED, + 14: STATUS.STATUS_WRONG_ANSWER, + 15: STATUS.STATUS_ACCEPTED, +}; + +export default class SPOJProvider implements IBasicProvider { + constructor(public account: RemoteAccount, private save: (data: any) => Promise) { + if (account.cookie) this.cookie = account.cookie; + } + + cookie: string[] = []; + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) url = `${this.account.endpoint || 'https://www.spoj.com'}${url}`; + const req = superagent.get(url).set('Cookie', this.cookie); + 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.spoj.com'}${url}`; + const req = superagent.post(url).type('form').set('Cookie', this.cookie); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + get loggedIn() { + return this.get('/').then(({ text: html }) => { + const $dom = new JSDOM(html); + if ($dom.window.document.querySelectorAll('span.fa.fa-sign-out.fa-fw').length) return true; + return false; + }); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry login'); + const res = await this.post('/login/').send({ + next_raw: '/', + autologin: 1, + login_user: this.account.handle, + password: this.account.password, + }); + console.log(res.body, res.header); + const cookie = res.header['set-cookie']; + if (cookie) { + await this.save({ cookie }); + this.cookie = cookie; + } + if (await this.loggedIn) return true; + return false; + } + + async getProblem(id: string) { + logger.info(id); + const { text } = await this.get(`/problems/${id}/`); + const { window: { document } } = new JSDOM(text); + const files = {}; + document.querySelector('#problem-body').querySelectorAll('img[src]').forEach((ele) => { + const src = ele.getAttribute('src'); + if (!src.startsWith('http')) return; + const file = new PassThrough(); + this.get(src).pipe(file); + const fid = String.random(8); + files[`${fid}.png`] = file; + ele.setAttribute('src', `file://${fid}.png`); + }); + const meta = document.querySelector('#problem-meta').children[1]; + const { text: submit } = await this.get(`/submit/${id}/`); + const $dom = new JSDOM(submit); + const langs = Array.from($dom.window.document.querySelector('#lang').querySelectorAll('option')) + .map((i) => `spoj.${i.getAttribute('value')}`); + let time = meta.children[2].children[1].innerHTML.trim().toLowerCase(); + if (time.includes('-')) time = time.split('-')[1]; + let memory = meta.children[4].children[1].innerHTML.trim().toLowerCase(); + if (memory.includes('-')) memory = memory.split('-')[1]; + return { + title: document.querySelector('#problem-name').innerHTML.trim().split(`${id} - `)[1], + data: { + 'config.yaml': Buffer.from(`time: ${time} +memory: ${memory} +type: remote_judge +subType: spoj +target: ${id} +langs: ${JSON.stringify(langs)}`), + }, + files, + tag: Array.from(document.querySelectorAll('[data-tagid]')).map((i) => i.childNodes[0].nodeValue.substr(1, 999)), + content: document.querySelector('#problem-body').innerHTML.trim(), + }; + } + + async listProblem(page: number, resync = false) { + if (resync) return []; + const res = await this.get(`/problems/classical/sort=0,start=${page * 50 - 50}`); + const { window: { document } } = new JSDOM(res.text); + const index = document.querySelector('ul.pagination').querySelector('li.active').children[0].innerHTML.trim(); + if (index !== page.toString()) return []; + return Array.from(document.querySelectorAll('td[align=left]')).map((i) => i.children[0].getAttribute('href').split('/problems/')[1]); + } + + async submitProblem(problemcode: string, lang: string, code: string, info, next, end) { + const comment = setting.langs[lang]?.comment; + if (comment) { + const msg = `Hydro submission #${info.rid}@${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}`; + } + // TODO check submit time to ensure submission + const { text } = await this.post('/submit/complete/').send({ + submit: 'Submit!', + lang: lang.split('spoj.')[1] || '44', + problemcode, + file: code, + }); + if (text.includes('Wrong problem code!')) { + await next({ message: 'Wrong problem code!' }); + await end({ status: STATUS.STATUS_COMPILE_ERROR }); + return null; + } + const { window } = new JSDOM(text); + return window.document.querySelector('[name="newSubmissionId"]').getAttribute('value'); + } + + async waitForSubmission(id: string, next, end) { + logger.debug('Waiting for %s', id); + // eslint-disable-next-line no-constant-condition + while (true) { + await sleep(3000); + const { text } = await this.get(`/status/ns=${id}`); + if (!text.includes(id)) { + return await end({ + status: STATUS.STATUS_ETC, + score: 0, + time: 0, + memory: 0, + }); + } + const { window: { document } } = new JSDOM(text); + if (!document.querySelector(`#statusres_${id}[final='1']`)) continue; + const status = VERDICT[document.querySelector(`#statusres_${id}`).getAttribute('status')] || STATUS.STATUS_WRONG_ANSWER; + const timestr = document.querySelector(`#statustime_${id}`).children[0].innerHTML.trim(); + const time = timestr === '-' ? 0 : (+timestr) * 1000; + const memorystr = document.querySelector(`#statusmem_${id}`).innerHTML.trim().toLowerCase(); + const memory = memorystr === '-' ? 0 : parseMemoryMB(memorystr) / 1024; + await next({ + message: document.querySelector(`#statusres_${id}`).childNodes[0].nodeValue.trim(), + }); + return await end({ + status, + score: status === STATUS.STATUS_ACCEPTED ? 100 : 0, + time, + memory, + }); + } + } +}