koishi-plugin-checkin-custom
Version:
一个高度可配置的、支持多类型打卡和独立排行榜的 Koishi 插件。
268 lines (265 loc) • 12.9 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 __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,
inject: () => inject,
name: () => name
});
module.exports = __toCommonJS(src_exports);
var import_koishi = require("koishi");
var import_node_fs = require("node:fs");
var import_node_path = __toESM(require("node:path"));
var name = "checkin-custom";
var inject = {
required: ["database", "puppeteer"]
};
var checkinCommandSchema = import_koishi.Schema.object({
commandName: import_koishi.Schema.string().description("打卡指令的名称(这也是类型的唯一标识符)。").required(),
successMessage: import_koishi.Schema.string().default("打卡成功!这是你本月的打卡日历:").description("成功打卡后发送的提示文案。"),
alreadyCheckedMessage: import_koishi.Schema.string().default("你今天已经打过卡啦!").description("重复打卡时发送的提示文案。"),
checkinTitle: import_koishi.Schema.string().default("每日打卡").description("打卡日历上显示的标题。"),
backgroundImage: import_koishi.Schema.string().role("path").description("【可选】自定义日历的背景图片路径。"),
backgroundSize: import_koishi.Schema.union(["cover", "contain"]).default("cover").description("背景图片缩放模式。"),
backgroundBlur: import_koishi.Schema.number().min(0).max(50).default(5).description("背景模糊程度(0-50像素)。"),
themeColor: import_koishi.Schema.string().role("color").default("#fde2e2").description("【可选】自定义日历中“今天”高亮显示的颜色。")
}).description("打卡指令配置");
var rankCommandSchema = import_koishi.Schema.object({
commandName: import_koishi.Schema.string().description("排行榜指令的名称。").required(),
title: import_koishi.Schema.string().default("本月打卡排行榜").description("排行榜上显示的标题。"),
includedTypes: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").description("要统计的打卡类型(填写打卡指令的名称)。如果留空,将统计所有类型的打卡总和。")
}).description("排行榜配置");
var Config = import_koishi.Schema.intersect([
import_koishi.Schema.object({
checkinCommands: import_koishi.Schema.array(checkinCommandSchema).description("在这里定义一个或多个打卡指令,每个指令都是一个独立的“打卡类型”。")
}).description("打卡指令设置"),
import_koishi.Schema.object({
rankCommands: import_koishi.Schema.array(rankCommandSchema).description("在这里定义一个或多个排行榜,每个排行榜可以独立统计不同的打卡类型。")
}).description("排行榜设置"),
import_koishi.Schema.object({
debug: import_koishi.Schema.boolean().default(false).description("是否开启调试模式。")
}).description("高级设置")
]);
function apply(ctx, config) {
ctx.model.extend("checkin-custom-record", {
id: "unsigned",
userId: "string",
platform: "string",
checkinType: "string",
date: "date"
}, {
primary: "id",
autoInc: true
});
for (const cmdConfig of config.checkinCommands) {
ctx.command(cmdConfig.commandName, "进行每日打卡并查看日历").action(async ({ session }) => {
const { userId, platform } = session;
const today = /* @__PURE__ */ new Date();
today.setHours(0, 0, 0, 0);
const checkinType = cmdConfig.commandName;
try {
const existingRecord = await ctx.database.get("checkin-custom-record", { userId, platform, date: today, checkinType });
let message;
if (existingRecord.length > 0) {
message = (0, import_koishi.h)("p", cmdConfig.alreadyCheckedMessage);
} else {
await ctx.database.create("checkin-custom-record", { userId, platform, date: today, checkinType });
message = (0, import_koishi.h)("p", cmdConfig.successMessage);
}
if (config.debug) ctx.logger.info(`Generating calendar for user ${userId}, type: ${checkinType}`);
const calendarImage = await generateCalendar(ctx, userId, platform, cmdConfig, config.debug);
return [message, calendarImage];
} catch (error) {
ctx.logger.error(`[${cmdConfig.commandName}] command error:`, error);
return "打卡时发生内部错误,请联系管理员。";
}
});
}
for (const rankConfig of config.rankCommands) {
ctx.command(rankConfig.commandName, `查看 ${rankConfig.title}`).action(async ({ session }) => {
const now = /* @__PURE__ */ new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
try {
const whereClause = {
date: { $gte: firstDayOfMonth, $lte: lastDayOfMonth }
};
if (rankConfig.includedTypes && rankConfig.includedTypes.length > 0) {
whereClause.checkinType = { $in: rankConfig.includedTypes };
}
const rankData = await ctx.database.select("checkin-custom-record").where(whereClause).groupBy("userId", {
count: /* @__PURE__ */ __name((row) => import_koishi.$.count(row.id), "count")
}).orderBy("count", "desc").limit(10).execute();
if (rankData.length === 0) {
return `本月还没有相关的打卡记录哦,快来争做第一名吧!`;
}
let rankMessage = `📅 ${rankConfig.title} Top ${rankData.length} 📅
`;
let rankIndex = 1;
for (const item of rankData) {
let userName;
try {
const user = await session.bot.getUser(item.userId);
userName = user.name || user.nick || item.userId;
} catch {
userName = item.userId;
}
const medal = rankIndex === 1 ? "🥇" : rankIndex === 2 ? "🥈" : rankIndex === 3 ? "🥉" : `${rankIndex}.`;
rankMessage += `${medal} ${userName} - ${item.count} 天
`;
rankIndex++;
}
return rankMessage;
} catch (error) {
ctx.logger.error(`[${rankConfig.commandName}] rank command error:`, error);
return "查询排行榜时发生错误。";
}
});
}
}
__name(apply, "apply");
async function generateCalendar(ctx, userId, platform, cmdConfig, debug) {
const now = /* @__PURE__ */ new Date();
const year = now.getFullYear();
const month = now.getMonth();
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
const checkinType = cmdConfig.commandName;
const records = await ctx.database.get("checkin-custom-record", {
userId,
platform,
checkinType,
date: { $gte: firstDayOfMonth, $lte: lastDayOfMonth }
});
const checkedInDays = new Set(records.map((r) => new Date(r.date).getDate()));
if (debug) ctx.logger.info(`User ${userId} checked in on days: ${[...checkedInDays].join(", ")} for type: ${checkinType}`);
let customStyle = "";
if (cmdConfig.backgroundImage) {
try {
const imagePath = import_node_path.default.resolve(ctx.baseDir, cmdConfig.backgroundImage);
if (debug) ctx.logger.info(`Attempting to load background image from: ${imagePath}`);
const buffer = await import_node_fs.promises.readFile(imagePath);
const base64 = buffer.toString("base64");
const mimeType = import_node_path.default.extname(imagePath).substring(1) || "png";
customStyle += `body { background-image: url(data:image/${mimeType};base64,${base64}); background-size: ${cmdConfig.backgroundSize}; background-position: center; background-repeat: no-repeat; }`;
} catch (error) {
ctx.logger.warn(`加载背景图片失败: ${cmdConfig.backgroundImage}. 请检查路径和文件权限。`);
ctx.logger.warn(error);
}
}
const calendarBackground = cmdConfig.backgroundBlur > 0 ? `background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(${cmdConfig.backgroundBlur}px);` : `background: rgba(255, 255, 255, 1);`;
const html = `
<html>
<head>
<style>
body { font-family: sans-serif; background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; margin: 0; }
.calendar { width: 350px; ${calendarBackground} border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 20px; }
.header { text-align: center; margin-bottom: 20px; }
.header h1 { margin: 0; font-size: 1.5em; color: #333; }
.header p { margin: 5px 0 0; color: #888; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: center; padding: 10px; }
th { color: #555; }
td { color: #444; font-weight: bold; }
.day { position: relative; }
.day.checked::after { content: '✔️'; font-size: 0.7em; color: #4CAF50; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); }
.day.today { background-color: ${cmdConfig.themeColor}; border-radius: 50%; }
.empty { background-color: transparent; }
${customStyle}
</style>
</head>
<body>
<div class="calendar">
<div class="header">
<h1>${cmdConfig.checkinTitle}</h1>
<p>${year}年 ${month + 1}月</p>
</div>
<table>
<thead>
<tr><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr>
</thead>
<tbody>
${generateCalendarGrid(year, month, checkedInDays)}
</tbody>
</table>
</div>
</body>
</html>
`;
const page = await ctx.puppeteer.page();
try {
await page.setViewport({ width: 450, height: 600 });
await page.setContent(html);
const imageBuffer = await page.screenshot({ type: "png" });
return import_koishi.h.image(imageBuffer, "image/png");
} finally {
await page.close();
}
}
__name(generateCalendar, "generateCalendar");
function generateCalendarGrid(year, month, checkedInDays) {
let html = "<tr>";
const firstDayOfWeek = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const todayDate = (/* @__PURE__ */ new Date()).getDate();
const isCurrentMonth = (/* @__PURE__ */ new Date()).getMonth() === month && (/* @__PURE__ */ new Date()).getFullYear() === year;
for (let i = 0; i < firstDayOfWeek; i++) {
html += '<td class="empty"></td>';
}
for (let day = 1; day <= daysInMonth; day++) {
const dayOfWeek = (firstDayOfWeek + day - 1) % 7;
if (dayOfWeek === 0 && day > 1) {
html += "</tr><tr>";
}
const classes = ["day"];
if (checkedInDays.has(day)) {
classes.push("checked");
}
if (isCurrentMonth && day === todayDate) {
classes.push("today");
}
html += `<td class="${classes.join(" ")}">${day}</td>`;
}
const lastDayOfWeek = (firstDayOfWeek + daysInMonth - 1) % 7;
for (let i = lastDayOfWeek + 1; i < 7; i++) {
html += '<td class="empty"></td>';
}
html += "</tr>";
return html;
}
__name(generateCalendarGrid, "generateCalendarGrid");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Config,
apply,
inject,
name
});