Archived
Formatting
This commit is contained in:
1 parent
7f6f2bec2e
commit
2fffbfb45d
7 files changed
+2072
-111
No files matched your search
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"warnOnUnsupportedTypeScriptVersion": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": false,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"ignorePatterns": ["**/node_modules/**", "/dist/**"],
|
||||
"rules": {
|
||||
"curly": ["error", "all"]
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
# TSC artifacts
|
||||
/dist
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
|
||||
# NPM
|
||||
/node_modules
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.json, **/*.yml"],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+1741
File diff suppressed because it is too large.
Load diff
+40
-23
@@ -1,25 +1,42 @@
|
||||
{
|
||||
"name": "stavdeti-api",
|
||||
"version": "2.0.4",
|
||||
"description": "HTML based api for contest.stavdeti.ru",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"./dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"build": "tsc --build",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"author": "unknowableshade",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.33",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
"name": "stavdeti-api",
|
||||
"version": "2.0.4",
|
||||
"description": "Scraper for contest.stavdeti.ru",
|
||||
"keywords": [
|
||||
"scraper",
|
||||
"api"
|
||||
],
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/unknowableshade/stavdeti-api#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/unknowableshade/stavdeti-api/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Vladimir Eremin",
|
||||
"files": [
|
||||
"./dist"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"repository": "github:unknowableshade/stavdeti-api",
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "eslint --fix . && prettier --write .",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.33",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
||||
+229
-72
@@ -1,98 +1,255 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export interface Task {
|
||||
status: "AC" | "WA" | "NS" | "BN";
|
||||
/**
|
||||
* Shared types definitions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents status of specific task submissioned by contestant.
|
||||
*
|
||||
* @remarks AC - Accepted.
|
||||
* WA - Unsuccesful submission (Wrong Answer, Runtime Error, etc.).
|
||||
* NS - No submissions for task were made.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type taskStatus = "AC" | "WA" | "NS";
|
||||
|
||||
/**
|
||||
* Represents a single contest task submissioned by contestant.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @typeparam Status - {@link taskStatus}
|
||||
*/
|
||||
export interface Task<Status extends taskStatus = taskStatus> {
|
||||
/** Submission status. */
|
||||
status: Status;
|
||||
|
||||
/** Amount of attemps made. */
|
||||
attempts: number;
|
||||
penalty: number | null;
|
||||
|
||||
/**
|
||||
* Penalty gained.
|
||||
*
|
||||
* @remarks null if status is not "AC"
|
||||
*/
|
||||
penalty: Status extends "AC" ? number : null;
|
||||
|
||||
/** Contestant submissioned this task. */
|
||||
contestant: ContestantBase;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
/**
|
||||
* Represents single participant of a contest.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContestantBase {
|
||||
/** Ranking in contest leaderboard. */
|
||||
rank: number;
|
||||
|
||||
/** Username of contestant. */
|
||||
user: string;
|
||||
tasks: Task[];
|
||||
|
||||
/** Amount of "AC" tasks. */
|
||||
solved: number;
|
||||
|
||||
/** Sum of all task penalties. */
|
||||
penalty: number;
|
||||
|
||||
/** Contest bounded with this interface. */
|
||||
contest: ContestBase;
|
||||
}
|
||||
|
||||
export interface Contest {
|
||||
/**
|
||||
* Represents single participant of a contest.
|
||||
* With back reference to contest.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Contestant extends ContestantBase {
|
||||
/** Task statuses of contestant. */
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single contest.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContestBase {
|
||||
/** Contest ID. */
|
||||
id: string;
|
||||
|
||||
/** Contest title. */
|
||||
title: string;
|
||||
|
||||
/** Amount of tasks in the contest. */
|
||||
amount: number;
|
||||
participants: Participant[];
|
||||
}
|
||||
|
||||
export async function getContest(contestId: string): Promise<Contest> {
|
||||
const document = await fetch(
|
||||
`https://contest.stavdeti.ru/olympiad/${contestId}/show-monitor`
|
||||
).then((res) => res.text());
|
||||
/**
|
||||
* Represents a single contest.
|
||||
* With Contestants of this contest.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Contest extends ContestBase {
|
||||
/** List of contestants. */
|
||||
contestants: Contestant[];
|
||||
}
|
||||
|
||||
const title = document.match(/(?<=<span class="page-title">)(\n||.)*?(?=\(\d+?:\d+?:\d+?( ||\n)+?из( ||\n)+?\d+?:\d+?:\d+?\)(.||\n)*?<\/span>)/)?.at(0)?.trim();
|
||||
/**
|
||||
* Internal Functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses single row of task statuses of contestant on specific contest.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @returns List of task bodies.
|
||||
*
|
||||
* @param bodies - HTML table cells representing task status.
|
||||
* @param contestant - Contestant task statuses of.
|
||||
*/
|
||||
function tasksDeserializer(bodies: string[], contestant: ContestantBase): Task[] {
|
||||
return bodies.map((body) => {
|
||||
const _status = body
|
||||
.match(/(?<=<span class="((AC)|(NS)|(WA))">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
const _penalty = body
|
||||
.match(/(?<=<span class="TM">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
|
||||
if (!_status) {
|
||||
throw new Error(
|
||||
"stavdeti-api: Error! Cannot parse task status." +
|
||||
`Contestant: '${contestant.user}'` +
|
||||
`Contest: ${contestant.contest.id}`
|
||||
);
|
||||
}
|
||||
|
||||
const status = (() => {
|
||||
switch (_status[0]) {
|
||||
case "+":
|
||||
return "AC";
|
||||
case "-":
|
||||
return "WA";
|
||||
default:
|
||||
return "NS";
|
||||
}
|
||||
})();
|
||||
const attempts = _status.length > 1 ? parseInt(_status.slice(1)) : +(status === "AC");
|
||||
const penalty = _penalty ? parseFloat(_penalty.replace(":", ".")) : null;
|
||||
|
||||
return {
|
||||
status,
|
||||
penalty,
|
||||
attempts,
|
||||
contestant
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses contest leaderboard to deserialize contestants information.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @returns List of contest's contestants.
|
||||
*
|
||||
* @param body - HTML table representing contestants.
|
||||
* @param contest - Contest which currently deserializes.
|
||||
*/
|
||||
function contestantsDeserializer(body: string, contest: ContestBase): Contestant[] {
|
||||
const result: Contestant[] = [];
|
||||
|
||||
const trs = body.split("<tr>");
|
||||
|
||||
trs.forEach((tr) => {
|
||||
const rank = tr
|
||||
.match(/(?<=<td class="rank">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
const user = tr
|
||||
.match(/(?<=<td class="user">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
const solved = tr
|
||||
.match(/(?<=<td class="solved">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
const penalty = tr
|
||||
.match(/(?<=<td class="time">)(\n||.)*?(?=<)/)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
const _tasks = Array.from(
|
||||
tr.matchAll(/(?<=<td class="task">)(\n||.)*?(?=<\/td>)/g),
|
||||
(value) => value[0]
|
||||
);
|
||||
|
||||
if (!rank || !user || !solved || !penalty || !_tasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base = {
|
||||
rank: parseInt(rank),
|
||||
user,
|
||||
solved: parseInt(solved),
|
||||
penalty: parseInt(penalty),
|
||||
contest
|
||||
};
|
||||
|
||||
result.push({ ...base, tasks: tasksDeserializer(_tasks, base) });
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Scrapes single contest.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @returns {@link Contest} object.
|
||||
*
|
||||
* @param id - ID of contest to fetch.
|
||||
*/
|
||||
export async function contest(id: string): Promise<Contest> {
|
||||
const document = await fetch(`https://contest.stavdeti.ru/olympiad/${id}/show-monitor`).then(
|
||||
(res) => res.text()
|
||||
);
|
||||
|
||||
const title = document
|
||||
.match(
|
||||
/(?<=<span class="page-title">)(\n||.)*?(?=\(\d+?:\d+?:\d+?( ||\n)+?из( ||\n)+?\d+?:\d+?:\d+?\)(.||\n)*?<\/span>)/
|
||||
)
|
||||
?.at(0)
|
||||
?.trim();
|
||||
|
||||
const amount = document
|
||||
.slice(document.indexOf("<thead>"), document.indexOf("</thead>"))
|
||||
.match(/<th class="task">/g)?.length;
|
||||
|
||||
const base = {
|
||||
id: id,
|
||||
title: title ?? "",
|
||||
amount: amount ?? 0
|
||||
};
|
||||
|
||||
return {
|
||||
id: contestId,
|
||||
title: title || "",
|
||||
amount: amount || 0,
|
||||
participants: bodyDeserializer(
|
||||
document.slice(
|
||||
document.indexOf("<tbody>"),
|
||||
document.indexOf("</tbody>")
|
||||
)
|
||||
...base,
|
||||
contestants: contestantsDeserializer(
|
||||
document.slice(document.indexOf("<tbody>"), document.indexOf("</tbody>")),
|
||||
base
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function bodyDeserializer(body: string): Participant[] {
|
||||
let result: Participant[] = [];
|
||||
|
||||
const trs = body.split("<tr>");
|
||||
|
||||
for (const tr of trs) {
|
||||
const rank = tr.match(/(?<=<td class="rank">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
const user = tr.match(/(?<=<td class="user">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
const solved = tr.match(/(?<=<td class="solved">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
const penalty = tr.match(/(?<=<td class="time">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
|
||||
const tmp = Array.from(tr.matchAll(/(?<=<td class="task">)(\n||.)*?(?=<\/td>)/g), (value) => value[0]);
|
||||
|
||||
const tasks = tasksDeserializer(tmp || []);
|
||||
|
||||
if (rank && user && solved && penalty && tasks) {
|
||||
result.push({
|
||||
rank: parseInt(rank),
|
||||
user: user,
|
||||
solved: parseInt(solved),
|
||||
penalty: parseInt(penalty),
|
||||
tasks: tasks,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function tasksDeserializer(tasksInner: string[]): Task[] {
|
||||
let result: Task[] = [];
|
||||
|
||||
for (const task of tasksInner) {
|
||||
const matchStatus = task.match(/(?<=<span class="((AC)|(NS)|(WA))">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
const matchPenalty = task.match(/(?<=<span class="TM">)(\n||.)*?(?=<)/)?.at(0)?.trim();
|
||||
|
||||
if (!matchStatus) continue;
|
||||
|
||||
const status = matchStatus[0] === "+" ? "AC" : matchStatus[0] === "-" ? "WA" : "NS";
|
||||
const attempts = matchStatus.length > 1 ? parseInt(matchStatus.slice(1)) : 0 + status === "AC" ? 1 : 0;;
|
||||
const penalty = matchPenalty ? parseFloat(matchPenalty.replace(":", ".")) : null;
|
||||
|
||||
result.push({
|
||||
status: status,
|
||||
penalty: penalty,
|
||||
attempts: attempts
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
+16
-14
@@ -11,8 +11,8 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": ["ESNext"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
@@ -24,16 +24,18 @@
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"module": "NodeNext" /* Specify what module code is generated. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
"types": ["node"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
"types": [
|
||||
"node"
|
||||
] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
"resolveJsonModule": true /* Enable importing .json files */,
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
@@ -42,12 +44,12 @@
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
@@ -69,12 +71,12 @@
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
@@ -96,6 +98,6 @@
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user