UNPKG

koishi-plugin-oi-contest-sniffer

Version:
281 lines (274 loc) 11.4 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, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var cheerio = __toESM(require("cheerio")); var name = "oi-contest-sniffer"; var usage = `本插件用于获取各大 OJ 平台的近期比赛信息,目前支持 Codeforces、AtCoder、洛谷。 在群聊中输入 \`oi\` 命令即可查询比赛,支持以下选项: - \`-p <平台>\`:筛选平台(参考别名设置) - \`-s <阶段>\`:筛选比赛阶段(\`upcoming\`/\`coding\`/\`ended\`) - \`-n <数量>\`:指定输出的比赛个数 - \`-d <日期>\`:指定日期(格式:YYYY-MM-DD 或 today) ## 使用示例: | 命令 | 说明 | |:-----|:-----| | \`oi -p cf -s upcoming -n 3\` | 查询未来 3 场 Codeforces 比赛 | | \`oi -d today -s coding\` | 查询今天正在进行的比赛 | `; var Config = import_koishi.Schema.object({ defaultMaxContests: import_koishi.Schema.number().min(1).max(30).default(5).description("不指定 -n 时,默认返回的比赛数量"), startSearchFrom: import_koishi.Schema.number().min(-15).max(15).default(0).description(`从( )天前存在的比赛开始查询<br>- 默认为 0 表示只查询今天及之后的比赛`), timeout: import_koishi.Schema.number().min(1e3).max(6e4).default(1e4).description("请求超时阈值(毫秒)"), platformAliases: import_koishi.Schema.dict(String).default({ "codeforces": "Codeforces", "cf": "Codeforces", "atcoder": "AtCoder", "at": "AtCoder", "ac": "AtCoder", "lg": "Luogu", "洛谷": "Luogu", "luogu": "Luogu" }).description("平台别名设置<br>适用于 -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("已结束比赛的显示文本"), greetings: import_koishi.Schema.array(String).default([ "还在打OI哦,休息一下吧 ♪(´▽`)", "今日宜:AK", "ο(=•ω<=)ρ⌒☆" ]).description("问候语列表<br>(会出现在回复消息的第一行)") }).description("显示文本") }); function apply(ctx, config) { ctx.command("oi", "获取近期的 OI 线上赛事的日程").usage("查询近期的 OI 线上赛事,支持多种筛选条件").example("oi").example("oi -d today -s ended").example("oi -p cf -d 2025-01-01 -s upcoming -n 3").option("platform", "-p <platform> 筛选比赛平台").option("phase", "-s <phase> 筛选比赛阶段 (upcoming/coding/ended)").option("count", "-n <count> 指定输出比赛的个数").option("date", "-d <date> 指定日期 (YYYY-MM-DD 或 today)").action(async ({ session, options }) => { try { const contests = await fetchAllContests(ctx, config); const processed = filterAndSortContests(contests, options, config); return processed.length === 0 ? "嗯?这里没有找到符合条件的比赛 O_O" : formatOutput(processed, config); } catch (error) { console.error("Failed to fetch contests:", error); return "出错了(T_T) 请稍后再试 (ง •_•)ง"; } }); } __name(apply, "apply"); async function fetchAllContests(ctx, config) { const fetchers = [ { fetch: getCodeforcesContests, platform: "Codeforces" }, { fetch: getAtcoderContests, platform: "AtCoder" }, { fetch: getLuoguContests, platform: "Luogu" } ]; const results = await Promise.allSettled( fetchers.map((f) => f.fetch(ctx, config)) ); const allContests = results.flatMap((result, i) => { if (result.status === "fulfilled") { return result.value.map((c) => ({ ...c, platform: fetchers[i].platform })); } console.warn(`Failed to fetch from ${fetchers[i].platform}`); return []; }); const minTime = Math.floor(Date.now() / 1e3) - config.startSearchFrom * 86400; return allContests.filter((c) => c.startTime + c.duration >= minTime); } __name(fetchAllContests, "fetchAllContests"); function filterAndSortContests(contests, options, config) { let result = [...contests]; if (options.platform) { const platform = resolvePlatformAlias(options.platform, config); if (!platform) return []; result = result.filter((c) => c.platform === platform); } if (options.phase) { result = result.filter((c) => c.phase === options.phase); } if (options.date) { const targetDate = parseDate(options.date); if (targetDate) { const dayStart = Math.floor(targetDate.getTime() / 1e3); const dayEnd = dayStart + 86400; result = result.filter( (c) => c.startTime < dayEnd && c.startTime + c.duration > dayStart ); } } result.sort((a, b) => a.startTime - b.startTime); return result.slice(0, options.count || config.defaultMaxContests); } __name(filterAndSortContests, "filterAndSortContests"); function resolvePlatformAlias(input, config) { const lowerInput = input.toLowerCase(); for (const [alias, platform2] of Object.entries(config.platformAliases)) { if (alias.toLowerCase() === lowerInput) return platform2; } const platform = Object.values(config.platformAliases).find( (p) => p.toLowerCase() === lowerInput ); return platform || null; } __name(resolvePlatformAlias, "resolvePlatformAlias"); function parseDate(dateStr) { if (dateStr.toLowerCase() === "today") return /* @__PURE__ */ new Date(); const match = /^\d{4}-\d{2}-\d{2}$/.test(dateStr); if (!match) return null; const date = new Date(dateStr); return isNaN(date.getTime()) ? null : date; } __name(parseDate, "parseDate"); function formatOutput(contests, config) { const greeting = config.statusText.greetings[Math.floor(Math.random() * config.statusText.greetings.length)]; const statusMap = { upcoming: config.statusText.upcoming, coding: config.statusText.coding, ended: config.statusText.ended }; let output = greeting + "\n" + "-".repeat(13) + "\n"; contests.forEach((c) => { output += `比赛平台: ${c.platform} `; output += `比赛名: ${c.name} `; output += `开始时间: ${new Date(c.startTime * 1e3).toLocaleString("zh-CN")} `; output += `比赛时长: ${Math.round(c.duration / 60)}min `; output += `比赛状态: ${statusMap[c.phase]} `; output += `直达赛场: ${c.url} `; output += "-".repeat(13) + "\n"; }); return output; } __name(formatOutput, "formatOutput"); 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" } }); if (response?.status !== "OK" || !Array.isArray(response.result)) return []; const now = Math.floor(Date.now() / 1e3); return response.result.map((c) => ({ name: c.name, startTime: c.startTimeSeconds, duration: c.durationSeconds, phase: getPhase(now, c.startTimeSeconds, c.startTimeSeconds + c.durationSeconds), url: `https://codeforces.com/contests/${c.id}` })); } catch (e) { console.warn("Codeforces fetch failed:", e); 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); $(".table-default tbody tr").each((_, el) => { const cells = $(el).find("td"); const startTimeText = cells.eq(0).text().trim(); const contestLink = cells.eq(1).find("a").attr("href"); const contestName = cells.eq(1).find("a").text().trim(); const durationText = cells.eq(2).text().trim(); if (!startTimeText || !contestLink) return; const startTime = Math.floor(new Date(startTimeText).getTime() / 1e3); const [h, m] = durationText.split(":").map(Number); const duration = (h || 0) * 3600 + (m || 0) * 60; const endTime = startTime + duration; contests.push({ name: contestName, startTime, duration, phase: getPhase(now, startTime, endTime), url: `https://atcoder.jp${contestLink}` }); }); return contests; } catch (e) { console.warn("AtCoder fetch failed:", e); 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" } }); const contests = response?.currentData?.contests?.result || []; const now = Math.floor(Date.now() / 1e3); return contests.map((c) => ({ name: c.name, startTime: c.startTime, duration: c.endTime - c.startTime, phase: getPhase(now, c.startTime, c.endTime), url: `https://www.luogu.com.cn/contest/${c.id}` })); } catch (e) { console.warn("Luogu fetch failed:", e); return []; } } __name(getLuoguContests, "getLuoguContests"); function getPhase(now, startTime, endTime) { if (now < startTime) return "upcoming"; if (now < endTime) return "coding"; return "ended"; } __name(getPhase, "getPhase"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name, usage });