Formatting

This commit is contained in:
deytenit committed 2024-01-30 18:18:44 +03:00
1 parent 7f6f2bec2e
commit 2fffbfb45d
7 files changed
+2072 -111

No files matched your search

+19
View File
@@ -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
View File
@@ -1,3 +1,5 @@
# TSC artifacts
/dist
/node_modules
/package-lock.json
# NPM
/node_modules
+23
View File
@@ -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
}
}
]
}
+1741
View File
File diff suppressed because it is too large. Load diff
+40 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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. */
}
}