@cpany/plugin-atcoder
Version:
CPany AtCoder plugin
615 lines (600 loc) • 21.4 kB
JavaScript
;
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
});