UNPKG

@cpany/plugin-atcoder

Version:
615 lines (600 loc) 21.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { atcoderPlugin: () => atcoderPlugin, default: () => src_default }); module.exports = __toCommonJS(src_exports); // ../core/src/cpany.ts var import_debug2 = __toESM(require("debug")); var import_kolorist2 = require("kolorist"); // ../types/src/platform.ts function isAtCoder(entity) { return entity.type.startsWith("atcoder"); } // ../core/src/constant.ts var DefaultRecentTime = 30 * 24 * 3600; // ../core/src/logger.ts var import_kolorist = require("kolorist"); var import_debug = __toESM(require("debug")); // ../core/src/utils.ts function sleep(duration) { return new Promise((res) => setTimeout(() => res(), duration)); } function random(left, right) { return left + Math.floor(Math.random() * (right - left + 1)); } function shuffle(array) { let currentIndex = array.length; while (currentIndex != 0) { const randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; } return array; } function diff(f, old, cur) { const delta = []; const set = new Set(old.map(f)); for (const item of cur) { if (!set.has(f(item))) { delta.push(item); } } return delta; } // ../core/src/logger.ts var colorList = shuffle([ import_kolorist.red, import_kolorist.green, import_kolorist.yellow, import_kolorist.blue, import_kolorist.magenta, import_kolorist.cyan, import_kolorist.gray, import_kolorist.lightGray, import_kolorist.lightRed, import_kolorist.lightGreen, import_kolorist.lightYellow, import_kolorist.lightBlue, import_kolorist.lightMagenta, import_kolorist.lightCyan ]); // ../core/src/cpany.ts var debug = (0, import_debug2.default)("cpany:core"); // ../core/src/retry.ts function createRetryContainer(logger, maxRetry = 10) { const tasks = []; const add = (id, fn) => { tasks.push({ id, fn }); }; const run = async () => { for (const task of tasks) { let stop = false; for (let count = 1; count <= maxRetry; count++) { const ok = await task.fn(); if (!ok) { if (count === maxRetry) { stop = true; logger.error(`Error: Task ${task.id} failed`); break; } const suffix = count % 10 === 1 ? "st" : count % 10 === 2 ? "nd" : "th"; logger.info(`Retry: Task ${task.id} failed at the ${count}-${suffix} time`); await sleep(random(2 * 1e3, 5 * 1e3)); } else { break; } } if (stop) break; } }; return { add, run }; } // src/constant.ts var import_axios = __toESM(require("axios")); var atcoder = "atcoder"; function loadCookie() { const session = process.env.REVEL_SESSION; if (!session) { console.error("Please set env variable REVEL_SESSION !"); process.exit(1); } return session; } function getAPI() { const cookie = loadCookie(); return import_axios.default.create({ baseURL: "https://atcoder.jp/", headers: { Cookie: `REVEL_FLASH=; REVEL_SESSION=${cookie}` } }); } // src/handle.ts var import_node_html_parser2 = require("node-html-parser"); var import_html_entities = require("html-entities"); // src/contest.ts var import_node_html_parser = require("node-html-parser"); var handleMap = /* @__PURE__ */ new Map(); var contestantSet = /* @__PURE__ */ new Map(); var contestCache = /* @__PURE__ */ new Map(); var contestPracticeCache = /* @__PURE__ */ new Map(); var contestSubmissionsUrl = /* @__PURE__ */ new Map(); function pushContest(contest, handle) { if (!contestantSet.has(contest)) { contestantSet.set(contest, []); } contestantSet.get(contest).push(handle); } function addContests(contests) { for (const contest of contests) { if (!!contest.standings) { contest.standings = contest.standings.filter( (standing) => standing.author.participantType !== "PRACTICE" /* PRACTICE */ ); } contestCache.set(contest.id, contest); } } function addContestPractice(contestId, handle, submissions) { if (!contestPracticeCache.has(contestId)) { contestPracticeCache.set(contestId, []); } const addSubUrl = (pid, url) => { if (!url) return; if (contestSubmissionsUrl.has(handle)) { contestSubmissionsUrl.get(handle).set(pid, url); } else { contestSubmissionsUrl.set(handle, /* @__PURE__ */ new Map([[pid, url]])); } }; let practiceCount = 0; const allSubs = /* @__PURE__ */ new Map(); for (const sub of submissions.sort((lhs, rhs) => lhs.creationTime - rhs.creationTime)) { const pid = parseIndex(sub.problem.id); if (sub.author.participantType === "PRACTICE" /* PRACTICE */) { practiceCount++; if (allSubs.has(pid)) { const oldSub = allSubs.get(pid); const update = () => { oldSub.id = sub.id; oldSub.creationTime = sub.creationTime; oldSub.relativeTime = sub.creationTime; oldSub.submissionUrl = sub.submissionUrl; }; if (sub.verdict === "OK" /* OK */) { if (oldSub.verdict !== "OK" /* OK */) { oldSub.verdict = "OK" /* OK */; update(); } } else { if (oldSub.verdict !== "OK" /* OK */) { oldSub.dirty += 1; update(); } } } else { allSubs.set(pid, { ...sub, dirty: sub.verdict === "OK" /* OK */ ? 0 : 1, problemIndex: pid, relativeTime: sub.creationTime }); } } else { if (sub.verdict === "OK" /* OK */) { const pid2 = sub.problem.id.split(""); const index = "_" + String.fromCharCode(pid2.pop().charCodeAt(0) - "A".charCodeAt(0) + "a".charCodeAt(0)); addSubUrl(pid2.concat(index).join(""), sub.submissionUrl); } } } if (practiceCount === 0) return; const standing = { author: { members: [handle], teamName: handleMap.get(handle) ?? handle, participantType: "PRACTICE" /* PRACTICE */, participantTime: 0 }, rank: Number.MAX_SAFE_INTEGER, solved: submissions.filter( (sub) => sub.author.participantType === "PRACTICE" /* PRACTICE */ && sub.verdict === "OK" /* OK */ ).length, penalty: 0, submissions: [...allSubs.values()] }; contestPracticeCache.get(contestId).push(standing); } function createAtCoderContestPlugin(handleUserMap) { for (const [handle, user] of handleUserMap) handleMap.set(handle, user); return { name: "contest", platform: atcoder, async fetch({ logger }) { const api = getAPI(); const retry = createRetryContainer(logger, 5); const contests = []; let planSz = 0, curRunSz = 0; for (const [contestId, handlesParticipant] of contestantSet) { const cacheStandings = contestCache.get(contestId)?.standings?.map((standing) => standing.author.members).flat() ?? []; if (contestCache.has(contestId) && isHandlesLte(handlesParticipant, cacheStandings)) { contests.push(contestCache.get(contestId)); } else { planSz++; retry.add(`AtCoder Contest ${contestId}`, async () => { logger.info(`Fetch: AtCoder Contest ${contestId} (${curRunSz + 1}/${planSz})`); try { const contest = await fecthContest(api, contestId); contests.push({ ...contest, ...parseStandings( contestId, contest.startTime, await fetchStandings(api, contestId) ) }); curRunSz++; return true; } catch (error) { logger.error("Error: " + error.message); logger.debug(error); return false; } }); } } logger.info(`Fetch: plan to fetch ${planSz} contests`); await retry.run(); for (const contest of contests) { if (!!contest.standings && !!contest.id && contestPracticeCache.get(String(contest.id))) { const practice = contestPracticeCache.get(String(contest.id)); contest.standings.push( ...practice.map((standing) => { standing.author.participantTime = contest.startTime; standing.submissions = standing.submissions.map((sub) => { sub.relativeTime -= contest.startTime; return sub; }); return standing; }).sort((lhs, rhs) => rhs.solved - lhs.solved) ); } } return JSON.stringify(contests, null, 2); } }; } async function fecthContest(api, contestId) { const { data } = await api.get(`/contests/${contestId}`); const root = (0, import_node_html_parser.parse)(data); const durations = root.querySelectorAll(".contest-duration a"); const startTime = new Date(durations[0].innerText).getTime() / 1e3; const endTime = new Date(durations[1].innerText).getTime() / 1e3; return { type: "atcoder", name: root.querySelector("h1").innerText, startTime, duration: endTime - startTime, participantNumber: 0, id: contestId, contestUrl: `https://atcoder.jp/contests/${contestId}`, standingsUrl: `https://atcoder.jp/contests/${contestId}/standings`, inlinePage: true }; } function parseStandings(contestId, startTime, { problems, standings }) { return { problems: problems.map((problem, index) => ({ type: "atcoder", contestId, index, name: problem.TaskName, problemUrl: `https://atcoder.jp/contests/${contestId}/tasks/${problem.TaskScreenName}` })), standings: standings.map((standing) => { const username = standing.UserScreenName !== "" ? standing.UserScreenName : standing.UserName; let penalty = standing.TotalResult.Penalty * 20 * 60; const submissions = []; for (const pid in standing.TaskResults) { const result = standing.TaskResults[pid]; const problemIndex = parseIndex(pid); if (result.Score === 0) { submissions.push({ id: -1, creationTime: -1, relativeTime: -1, problemIndex, dirty: result.Failure }); } else { const relativeTime = Math.round(result.Elapsed / 1e9); penalty += relativeTime; submissions.push({ id: -1, creationTime: startTime + relativeTime, relativeTime, problemIndex, verdict: "OK" /* OK */, dirty: result.Penalty, submissionUrl: contestSubmissionsUrl.get(username)?.get(pid) }); } } return { author: { members: [username], teamName: handleMap.get(username) ?? username, participantType: standing.IsRated ? "CONTESTANT" /* CONTESTANT */ : "OUT_OF_COMPETITION" /* OUT_OF_COMPETITION */, participantTime: startTime }, rank: standing.Rank, solved: standing.TotalResult.Accepted, penalty, submissions }; }).filter((standing) => standing && standing.submissions.length > 0) }; } async function fetchStandings(api, contestId) { const { data } = await api.get(`/contests/${contestId}/standings/json`); if ((data.TaskInfo === null || data.TaskInfo === void 0) && (data.StandingsData === null || data.StandingsData === void 0)) { throw new Error("Maybe your cookie is expired, please update the env variable REVEL_SESSION."); } const problems = data.TaskInfo ?? []; const standings = (data.StandingsData ?? []).filter( (row) => handleMap.has(row.UserName) || handleMap.has(row.UserScreenName) ); return { problems, standings }; } function parseIndex(index) { if (/[a-z]$/.test(index)) { return index.charCodeAt(index.length - 1) - "a".charCodeAt(0); } else { return index.charCodeAt(index.length - 1) - "A".charCodeAt(0); } } function isHandlesLte(sa, sb) { const set = new Set(sa); for (const handle of sb) set.delete(handle); return set.size === 0; } // src/handle.ts function createAtCoderHandlePlugin(newHandles) { return { name: "handle", platform: atcoder, async query(id, { logger }) { const api = getAPI(); const user = await fetchUser(api, id); user.submissions = await fetchSubmissions(api, id, logger); newHandles.push(user); return JSON.stringify(user, null, 2); } }; } async function fetchUser(api, id) { const { data } = await api.get("/users/" + id); const root = (0, import_node_html_parser2.parse)(data); const color = (() => { const username = root.querySelector("a.username span"); const style = username.getAttribute("style"); if (!style) return void 0; const res = /(#[0-9A-F]{6})/.exec(style); return res ? res[1] : void 0; })(); const avatar = (() => { const raw = root.querySelector("img.avatar")?.getAttribute("src"); if (!raw) return void 0; if (raw === "//img.atcoder.jp/assets/icon/avatar.png") return void 0; return raw; })(); const fields = root.querySelectorAll(".col-md-9 .dl-table tr td"); const rank = 0 < fields.length ? Number.parseInt(fields[0].innerText) : void 0; const rating = 1 < fields.length ? Number.parseInt(fields[1].querySelector("span").innerText) : void 0; const maxRating = 2 < fields.length ? Number.parseInt(fields[2].querySelector("span").innerText) : void 0; return { type: "atcoder/handle", handle: id, submissions: [], avatar, handleUrl: "https://atcoder.jp/users/" + id, atcoder: { rank, rating, maxRating, color } }; } async function fetchSubmissions(api, id, logger) { const { data } = await api.get("/users/" + id + "/history"); const root = (0, import_node_html_parser2.parse)(data); const contests = root.querySelectorAll("tr td.text-left").map((td) => td.querySelector("a").getAttribute("href")?.split("/")[2]).filter((contest) => !!contest); logger.info(`Fetch: ${id} has participated in ${contests.length} contests`); const run = async (contest) => { const submissions2 = []; for (let page = 1; ; page++) { const oldLen = submissions2.length; const { data: data2 } = await api.get(`/contests/${contest}/submissions`, { params: { "f.User": id, page } }); const root2 = (0, import_node_html_parser2.parse)(data2); const durations = root2.querySelectorAll(".contest-duration a"); const startTime = new Date(durations[0].innerText).getTime() / 1e3; const endTime = new Date(durations[1].innerText).getTime() / 1e3; submissions2.push( ...root2.querySelectorAll("table.table tbody tr").map((tr) => { const td = tr.querySelectorAll("td"); const sid = +td[td.length - 1].querySelector("a").getAttribute("href")?.split("/").pop(); const creationTime = new Date(td[0].innerText).getTime() / 1e3; const language = (0, import_html_entities.decode)(td[3].innerText.replace(/\([\s\S]*\)/, "").trim()); const verdict = ((str) => { if (str === "AC") return "OK" /* OK */; if (str === "WA") return "WRONG_ANSWER" /* WRONG_ANSWER */; if (str === "TLE") return "TIME_LIMIT_EXCEEDED" /* TIME_LIMIT_EXCEEDED */; if (str === "MLE") return "MEMORY_LIMIT_EXCEEDED" /* MEMORY_LIMIT_EXCEEDED */; if (str === "OLE") return "IDLENESS_LIMIT_EXCEEDED" /* IDLENESS_LIMIT_EXCEEDED */; if (str === "RE") return "RUNTIME_ERROR" /* RUNTIME_ERROR */; if (str === "CE") return "COMPILATION_ERROR" /* COMPILATION_ERROR */; return "FAILED" /* FAILED */; })(td[6].innerText); const type = startTime <= creationTime && creationTime < endTime ? "CONTESTANT" /* CONTESTANT */ : "PRACTICE" /* PRACTICE */; const problemId = ((id2) => { const [a, b] = id2.split("_"); return a + b.toUpperCase(); })(td[1].querySelector("a").getAttribute("href")?.split("/").pop()); const problemName = (0, import_html_entities.decode)(/^[\s\S]+ - ([\s\S]+)$/.exec(td[1].innerText)[1]); return { type: "atcoder", id: sid, creationTime, language, verdict, author: { members: [id], participantType: type, participantTime: type === "CONTESTANT" /* CONTESTANT */ ? startTime : creationTime }, submissionUrl: "https://atcoder.jp" + td[td.length - 1].querySelector("a").getAttribute("href"), problem: { type: "atcoder", id: problemId, name: problemName, problemUrl: "https://atcoder.jp" + td[1].querySelector("a").getAttribute("href") } }; }) ); if (submissions2.length === oldLen) break; } addContestPractice(contest, id, submissions2); logger.info(`Fetch: ${id} has created ${submissions2.length} submissions in ${contest}`); return submissions2; }; const retry = createRetryContainer(logger, 5); const submissions = []; for (const contest of contests) { retry.add(`${id}'s submissions at ${contest}'`, async () => { try { const newSubs = await run(contest); if (newSubs.findIndex((sub) => sub.author.participantType === "CONTESTANT" /* CONTESTANT */) >= 0) { pushContest(contest, id); } submissions.push(...newSubs); return true; } catch (error) { logger.error("Error: " + error.message); return false; } }); } await retry.run(); return submissions; } // src/index.ts function atcoderPlugin(config) { const handleMap2 = loadHandleMap(config); const oldHandles = []; const newHandles = []; return [ createAtCoderHandlePlugin(newHandles), createAtCoderContestPlugin(handleMap2), { name: "cache", platform: atcoder, async cache(ctx) { const contests = await ctx.readJsonFile("contest.json"); addContests(contests); const handles = await ctx.listDir("handle"); for (const handle of handles) { oldHandles.push(await ctx.readJsonFile(handle)); await ctx.removeFile(handle); } } }, { name: "diff", platform: atcoder, async diff(ctx) { const oldMap = new Map(oldHandles.map((h) => [h.handle, h])); for (const handle of newHandles) { const oldHandle = oldMap.get(handle.handle); const sub = diff((sub2) => "" + sub2.id, oldHandle?.submissions ?? [], handle.submissions); ctx.addHandleSubmission(handle.handle, ...sub); } } }, { name: "load", platform: atcoder, async load(_option, ctx) { const handles = await ctx.readJsonDir("handle"); for (const handle of handles) { ctx.addHandle(handle); } const contests = await ctx.readJsonFile("contest"); for (const rawContest of contests) { const contest = { ...rawContest, key: String(rawContest.id) }; contest.inlinePage = true; ctx.addContest(contest); for (const standing of contest.standings ?? []) { const name = standing.author.teamName; ctx.addUserContest(name, contest, standing.author); } } } } ]; } var src_default = atcoderPlugin; function loadHandleMap(config) { const handleMap2 = /* @__PURE__ */ new Map(); for (const user of config.users) { for (const handle of user.handle) { if (isAtCoder({ type: handle.platform })) { handleMap2.set(handle.handle, user.name); } } } return handleMap2; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { atcoderPlugin });