Skip to content

Commit 6eaa6ff

Browse files
committed
feat: lyrio platform
1 parent 73f484c commit 6eaa6ff

File tree

6 files changed

+203
-1
lines changed

6 files changed

+203
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Currently supported platforms:
3131
- [Codeforces](https://jsr.io/@un-oj/core/doc/platforms/codeforces) (`/platforms/codeforces`)
3232
- [Hydro](https://jsr.io/@un-oj/core/doc/platforms/hydro) (`/platforms/hydro`)
3333
- [LeetCode](https://jsr.io/@un-oj/core/doc/platforms/leetcode) (`/platforms/leetcode`)
34+
- [Lyrio](https://jsr.io/@un-oj/core/doc/platforms/lyrio) (LibreOJ) (`/platforms/lyrio`)
3435
- [Luogu](https://jsr.io/@un-oj/core/doc/platforms/luogu) (`/platforms/luogu`)
3536
- [MXOJ](https://jsr.io/@un-oj/core/doc/platforms/mxoj) (`/platforms/mxoj`)
3637

jsr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"./platforms/codeforces": "./src/platforms/codeforces.ts",
99
"./platforms/hydro": "./src/platforms/hydro.ts",
1010
"./platforms/leetcode": "./src/platforms/leetcode.ts",
11+
"./platforms/lyrio": "./src/platforms/lyrio.ts",
1112
"./platforms/luogu": "./src/platforms/luogu.ts",
1213
"./platforms/mxoj": "./src/platforms/mxoj.ts"
1314
},

src/platforms/lyrio.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Lyrio platform, which is used by [LibreOJ](https://loj.ac) under the hood.
3+
* @module
4+
*/
5+
6+
import type { PlatformOptions } from '../platform';
7+
import type { Problem as BaseProblem } from '../problem';
8+
import { NotFoundError, Platform, UnexpectedResponseError } from '../platform';
9+
import { UnOJError } from '../utils';
10+
11+
export type ProblemType = 'Traditional' | 'SubmitAnswer' | 'Interaction';
12+
13+
/**
14+
* Lyrio-specific problem type.
15+
*
16+
* Description is JSON-encoded Markdown.
17+
*
18+
* @example
19+
* ```ts
20+
* ({
21+
* description: `[{"sectionTitle":"题目描述","type":"Text","text":"输入 $ a $ 和 $ b $,输出 $ a + b $ 的结果。"},{"sectionTitle":"输入格式","type":"Text","text":"一行两个正整数 $ a $ 和 $ b $。"},{"sectionTitle":"输出格式","type":"Text","text":"一行一个正整数 $ a + b $。"},{"sectionTitle":"样例","type":"Sample","sampleId":0,"text":"根据数学知识有 $ 1 + 2 = 3 $。"},{"sectionTitle":"数据范围","type":"Text","text":"对于 $ 100\\% $ 的数据,$ 1 \\leq a, b \\leq 10 ^ 6 $。"}]`,
22+
* difficulty: undefined,
23+
* id: '1',
24+
* link: 'https://loj.ac/p/1',
25+
* memoryLimit: 536870912,
26+
* samples: [{
27+
* input: '1 2',
28+
* output: '3',
29+
* }],
30+
* tags: [{
31+
* color: 'black',
32+
* id: 1,
33+
* name: '系统测试',
34+
* nameLocale: 'zh_CN',
35+
* }],
36+
* timeLimit: 2000,
37+
* title: 'A + B 问题',
38+
* type: 'Traditional',
39+
* })
40+
* ```
41+
*/
42+
export type Problem = BaseProblem<
43+
string,
44+
number,
45+
undefined,
46+
Array<{ id: number, name: string, color: string }>,
47+
ProblemType
48+
>;
49+
50+
export const DEFAULT_BASE_URL = 'https://api.loj.ac';
51+
52+
/**
53+
* Lyrio platform.
54+
*
55+
* I18n is supported.
56+
*/
57+
export default class Lyrio extends Platform<string> {
58+
constructor(options?: PlatformOptions<string>) {
59+
super(options, DEFAULT_BASE_URL);
60+
}
61+
62+
/** Fetches a problem from LibreOJ using API. */
63+
override async getProblem(id: string): Promise<Problem> {
64+
const displayId = Number.parseInt(id);
65+
if (Number.isNaN(displayId))
66+
throw new NotFoundError('problem');
67+
68+
let data: any;
69+
try {
70+
data = await this.ofetch('/api/problem/getProblem', {
71+
method: 'POST',
72+
body: {
73+
displayId,
74+
localizedContentsOfLocale: this.locale || 'zh_CN',
75+
tagsOfLocale: this.locale || 'zh_CN',
76+
samples: true,
77+
judgeInfo: true,
78+
judgeInfoToBePreprocessed: true,
79+
statistics: false,
80+
discussionCount: false,
81+
permissionOfCurrentUser: false,
82+
lastSubmissionAndLastAcceptedSubmission: false,
83+
},
84+
responseType: 'json',
85+
});
86+
} catch (e) {
87+
throw new UnOJError(`Failed to fetch problem ${id}`, { cause: e });
88+
}
89+
90+
if (data.error === 'NO_SUCH_PROBLEM')
91+
throw new NotFoundError('problem');
92+
if (!data.meta || !data.localizedContentsOfLocale)
93+
throw new UnexpectedResponseError(data);
94+
95+
return {
96+
id,
97+
type: data.meta.type,
98+
title: data.localizedContentsOfLocale.title,
99+
link: `https://loj.ac/p/${displayId}`,
100+
description: JSON.stringify(data.localizedContentsOfLocale.contentSections),
101+
102+
samples: data.samples.map((sample: any) => ({
103+
input: sample.inputData,
104+
output: sample.outputData,
105+
})),
106+
timeLimit: data.judgeInfo.timeLimit,
107+
memoryLimit: data.judgeInfo.memoryLimit * 1024 * 1024,
108+
tags: data.tagsOfLocale || [],
109+
difficulty: undefined,
110+
};
111+
}
112+
}

src/problem.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ export interface ProblemDescriptionObject {
1414
hint: string
1515
}
1616

17+
/** A tag for a {@link Problem}. */
18+
export interface TagInfo<Id extends string | number = string | number> {
19+
id: Id
20+
name: string
21+
}
22+
1723
/** General problem information. */
1824
export interface Problem<
1925
Desc extends string | ProblemDescriptionObject = string | ProblemDescriptionObject,
2026
Limits extends number | number[] | undefined = number | number[] | undefined,
2127
Difficulty extends string | number | undefined = string | number | undefined,
22-
Tags extends string[] | number[] | undefined = string[] | number[] | undefined,
28+
Tags extends TagInfo[] = TagInfo[],
2329
Type extends string = string,
2430
> {
2531
id: string
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Bun Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Lyrio platform (problem) should work 1`] = `
4+
{
5+
"description": "[{"sectionTitle":"题目描述","type":"Text","text":"输入 $ a $$ b $,输出 $ a + b $ 的结果。"},{"sectionTitle":"输入格式","type":"Text","text":"一行两个正整数 $ a $$ b $"},{"sectionTitle":"输出格式","type":"Text","text":"一行一个正整数 $ a + b $"},{"sectionTitle":"样例","type":"Sample","sampleId":0,"text":"根据数学知识有 $ 1 + 2 = 3 $"},{"sectionTitle":"数据范围","type":"Text","text":"对于 $ 100\\\\% $ 的数据,$ 1 \\\\leq a, b \\\\leq 10 ^ 6 $"}]",
6+
"difficulty": undefined,
7+
"id": "1",
8+
"link": "https://loj.ac/p/1",
9+
"memoryLimit": 536870912,
10+
"samples": [
11+
{
12+
"input": "1 2",
13+
"output": "3",
14+
},
15+
],
16+
"tags": [
17+
{
18+
"color": "black",
19+
"id": 1,
20+
"name": "系统测试",
21+
"nameLocale": "zh_CN",
22+
},
23+
],
24+
"timeLimit": 2000,
25+
"title": "A + B 问题",
26+
"type": "Traditional",
27+
}
28+
`;
29+
30+
exports[`Lyrio platform (problem) should work 2`] = `
31+
{
32+
"description": "[{"sectionTitle":"题目描述","type":"Text","text":"输出 \`Hello, World!\`,大小写不限。"},{"sectionTitle":"样例 1","type":"Sample","sampleId":0,"text":""},{"sectionTitle":"样例 2","type":"Sample","sampleId":1,"text":""}]",
33+
"difficulty": undefined,
34+
"id": "2",
35+
"link": "https://loj.ac/p/2",
36+
"memoryLimit": 268435456,
37+
"samples": [
38+
{
39+
"input": "",
40+
"output": "Hello, World!",
41+
},
42+
{
43+
"input": "",
44+
"output": "hello, WORLD!",
45+
},
46+
],
47+
"tags": [
48+
{
49+
"color": "black",
50+
"id": 1,
51+
"name": "系统测试",
52+
"nameLocale": "zh_CN",
53+
},
54+
{
55+
"color": "violet",
56+
"id": 4,
57+
"name": "Special Judge",
58+
"nameLocale": "zh_CN",
59+
},
60+
],
61+
"timeLimit": 1000,
62+
"title": "你好,世界!",
63+
"type": "Traditional",
64+
}
65+
`;

tests/platforms/lyrio.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { NotFoundError } from '@un-oj/core';
2+
import Lyrio from '@un-oj/core/platforms/lyrio';
3+
import { describe, expect, it } from 'bun:test';
4+
import { assertProblem } from './utils';
5+
6+
describe('Lyrio platform (problem)', () => {
7+
const loj = new Lyrio();
8+
9+
it('should work', async () => {
10+
assertProblem(await loj.getProblem('1'));
11+
assertProblem(await loj.getProblem('2'));
12+
});
13+
14+
it('should throw NotFoundError w/ non-existent problem', async () => {
15+
expect(loj.getProblem('999999')).rejects.toThrow(NotFoundError);
16+
});
17+
});

0 commit comments

Comments
 (0)