UNPKG

koishi-plugin-oi-contest-sniffer

Version:
332 lines (330 loc) 13.3 kB
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 __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], 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, { Config: () => Config, apply: () => apply, name: () => name }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var cheerio = __toESM(require("cheerio")); var name = "oi-contest-sniffer"; var Config = import_koishi.Schema.object({ defaultMaxContests: import_koishi.Schema.number().min(1).max(30).default(5).description("默认返回比赛数量"), startSearchFrom: import_koishi.Schema.number().min(-15).max(15).default(2).description("从( )天前存在的比赛开始查询(不查询太过久远的比赛),可以取非正数。这个值会影响到 -s ended 的查找范围"), timeout: import_koishi.Schema.number().min(1e3).max(6e4).default(1e4).description("比赛数据请求超时阈值(单位毫秒)"), greetings: import_koishi.Schema.array(String).default([ "还在打OI哦,休息一下吧 ♪(´▽`)", "今日宜:AK", "ο(=•ω<=)ρ⌒☆", "想打比赛吗?那就来吧!(o゜▽゜)o☆", "今天你开long long了吗 ( •̀ ω •́ )", "唉你们OIer好可怕 O_O" ]).description("自定义问候语列表,每次随机选择一条显示在查询结果顶部"), platformAliases: import_koishi.Schema.dict(String).default({ "codeforces": "Codeforces", "cf": "Codeforces", "atcoder": "AtCoder", "at": "AtCoder", "ac": "AtCoder", "lg": "Luogu", "洛谷": "Luogu", "luogu": "Luogu", "LeetCode": "obsolete_do_not_use_it" }).description("平台别名设置,在指定 -p 参数时可简化平台名称输入").role("table"), statusText: import_koishi.Schema.object({ upcoming: import_koishi.Schema.string().default("还没开始 (●' - '●)").description("未开始比赛的显示文本"), coding: import_koishi.Schema.string().default("正在火热进行 OwO").description("进行中比赛的显示文本"), ended: import_koishi.Schema.string().default("结束嘞 o_o ....").description("已结束比赛的显示文本") }).description("比赛状态显示文本") }); function apply(ctx, config) { ctx.command("oi", "获取近期的 OI 线上赛事的日程").usage("查询近期的 OI 线上赛事,支持多种筛选条件").example("查询近期所有比赛(默认):\n oi").example("查询在今天举办的,已经结束的所有比赛:\n oi -d today -s ended").example("查询 2025 年 1 月 1 日在 Codeforces 举办的比赛,只输出在查询时刻尚未开始的前 3 场比赛:\n oi -p cf -d 2025-01-01 -s upcoming -n 3").option("platform", "-p <platform> 筛选比赛平台,可用字段参见 platformAliases 设置").option("phase", '-s <phase> 筛选比赛阶段 (支持 "upcoming", "coding", "ended" 三种参数)').option("count", "-n <count> 限制一次性输出的比赛总数").option("date", "-d <date> 查询最近指定日期的比赛(格式:YYYY-MM-DD,比如 2025-01-01,只能输入最近的日期)").action(async ({ session, options }) => { try { const contests = await fetchContests(ctx, config); const processed = processContests(contests, options, config); if (processed.length === 0) { return "嗯?这里没有找到符合条件的比赛 O_O"; } return generateOutput(processed, config); } catch (error) { console.error("Error occurs when fetching contests :(", error); return `出错了(T_T) 请稍后再试 (ง •_•)ง`; } }); } __name(apply, "apply"); async function fetchContests(ctx, config) { const [cfContests, atcoderContests, luoguContests] = await Promise.all([ getCodeforcesContests(ctx, config), getAtcoderContests(ctx, config), getLuoguContests(ctx, config) ]); const allContests = [ ...cfContests.map((c) => ({ ...c, platform: "Codeforces" })), ...atcoderContests.map((c) => ({ ...c, platform: "AtCoder" })), ...luoguContests.map((c) => ({ ...c, platform: "Luogu" })) ]; const dayStart = Math.floor(Date.now() / 1e3) - config.startSearchFrom * 86400; return allContests.filter((contest) => contest.startTime + contest.duration >= dayStart); } __name(fetchContests, "fetchContests"); function processContests(contests, options, config) { let processed = [...contests]; if (options.platform) { const inputPlatform = options.platform.toLowerCase(); let matchedPlatform = null; for (const [alias, platform] of Object.entries(config.platformAliases)) { if (alias.toLowerCase() === inputPlatform) { matchedPlatform = platform; break; } } if (!matchedPlatform) { for (const platform of Object.values(config.platformAliases)) { if (platform.toLowerCase() === inputPlatform) { matchedPlatform = platform; break; } } } if (matchedPlatform) { processed = processed.filter((c) => c.platform === matchedPlatform); } else { return []; } } if (options.phase) { processed = processed.filter( (c) => c.phase.toLowerCase().includes(options.phase.toLowerCase()) ); } if (options.date) { let targetDate = null; try { if (options.date.toLowerCase() === "today") { targetDate = /* @__PURE__ */ new Date(); } else { const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (dateRegex.test(options.date)) { targetDate = new Date(options.date); if (isNaN(targetDate.getTime())) { targetDate = null; } } } if (targetDate) { processed = processed.filter((contest) => { if (!contest.startTime) return false; const contestDate = new Date(contest.startTime * 1e3); return contestDate.getFullYear() === targetDate.getFullYear() && contestDate.getMonth() === targetDate.getMonth() && contestDate.getDate() === targetDate.getDate(); }); } } catch (e) { console.log("Invalid date format! -d option ignored!"); } } processed.sort((a, b) => { if (a.startTime && b.startTime) return a.startTime - b.startTime; if (a.phase.includes("coding")) return -1; if (b.phase.includes("coding")) return 1; if (a.phase.includes("ended")) return 1; if (b.phase.includes("ended")) return -1; return 0; }); return processed.slice(0, options.count || config.defaultMaxContests); } __name(processContests, "processContests"); function generateOutput(contests, config) { const statusTexts = config.statusText || { upcoming: "还没开始 (●' - '●)", coding: "正在火热进行 OwO", ended: "结束嘞 o_o ...." }; let output = config.greetings[Math.floor(Math.random() * config.greetings.length)] + "\n-------------\n"; contests.forEach((contest) => { output += `比赛平台: ${contest.platform} `; output += `比赛名: ${contest.name} `; if (contest.startTime) { const startTime = new Date(contest.startTime * 1e3).toLocaleString("zh-CN"); output += `开始时间: ${startTime} `; } if (contest.duration) { const duration = contest.duration / 60; output += `比赛时长: ${duration.toFixed(0)}min `; } let status = ""; if (contest.phase.includes("upcoming")) { status = statusTexts.upcoming; } else if (contest.phase.includes("coding")) { status = statusTexts.coding; } else if (contest.phase.includes("ended")) { status = statusTexts.ended; } output += `比赛状态: ${status} `; if (contest.url) { output += `直达赛场: ${contest.url} `; } output += "------------\n"; }); return output; } __name(generateOutput, "generateOutput"); async function getCodeforcesContests(ctx, config) { try { const response = await ctx.http.get("https://codeforces.com/api/contest.list", { params: { gym: false }, timeout: config.timeout, headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json" } }); if (!response || response.status !== "OK" || !Array.isArray(response.result)) { console.error("Codeforces API Structure mismatched:", response?.status || "No response"); return []; } const now = Math.floor(Date.now() / 1e3); return response.result.map((contest) => { const startTime = contest.startTimeSeconds; const endTime = startTime + contest.durationSeconds; let phase = "upcoming"; if (now > endTime) phase = "ended"; else if (now > startTime) phase = "coding"; return { name: contest.name, startTime, duration: contest.durationSeconds, phase, url: `https://codeforces.com/contests/${contest.id}` }; }); } catch (error) { console.error("Error occurs when fetching Codeforces contests :(", error); return []; } } __name(getCodeforcesContests, "getCodeforcesContests"); async function getAtcoderContests(ctx, config) { try { const html = await ctx.http.get("https://atcoder.jp/contests", { timeout: config.timeout, headers: { "User-Agent": "Mozilla/5.0" } }); const $ = cheerio.load(html); const contests = []; const now = Math.floor(Date.now() / 1e3); const parseDuration = /* @__PURE__ */ __name((text) => { const [hours, minutes] = text.split(":").map(Number); return hours * 3600 + minutes * 60; }, "parseDuration"); $(".table-default tbody tr").each((i, el) => { const row = $(el); const startTimeText = row.find("td:nth-child(1)").text().trim(); const contestLink = row.find("td:nth-child(2) a").attr("href"); const contestName = row.find("td:nth-child(2) a").text().trim(); const durationText = row.find("td:nth-child(3)").text().trim(); if (!startTimeText || !contestLink) return; const startTime = new Date(startTimeText).getTime() / 1e3; const duration = parseDuration(durationText); const endTime = startTime + duration; let phase; if (now < startTime) { phase = "upcoming"; } else if (now < endTime) { phase = "coding"; } else { phase = "ended"; } contests.push({ name: contestName, startTime, duration, phase, url: `https://atcoder.jp${contestLink}` }); }); return contests; } catch (error) { console.error("Error occurs when fetching AtCoder contests :(", error); return []; } } __name(getAtcoderContests, "getAtcoderContests"); async function getLuoguContests(ctx, config) { try { const response = await ctx.http.get("https://www.luogu.com.cn/contest/list?_contentOnly=1", { timeout: config.timeout, headers: { "User-Agent": "Mozilla/5.0", "X-Requested-With": "XMLHttpRequest", "Referer": "https://www.luogu.com.cn/contest/list" } }); if (!response?.currentData?.contests?.result) { console.error("Luogu API Structure mismatched", response); return []; } const now = Math.floor(Date.now() / 1e3); return response.currentData.contests.result.map((contest) => { const startTime = contest.startTime; const endTime = contest.endTime; let phase = "upcoming"; if (now > endTime) phase = "ended"; else if (now > startTime) phase = "coding"; return { name: contest.name, startTime, duration: endTime - startTime, phase, url: `https://www.luogu.com.cn/contest/${contest.id}` }; }); } catch (error) { console.error("Error occurs when fetching Luogu contests :(", error); return []; } } __name(getLuoguContests, "getLuoguContests"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name });