koishi-plugin-sourcebanspp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +27 -0
- package/lib/index.js +350 -0
- package/package.json +28 -0
- package/readme.md +5 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "sourcebanspp";
|
|
3
|
+
export declare const inject: {
|
|
4
|
+
required: string[];
|
|
5
|
+
};
|
|
6
|
+
declare module 'koishi' {
|
|
7
|
+
interface Tables {
|
|
8
|
+
sbpp_settings: SbppSettings;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export interface SbppSettings {
|
|
12
|
+
id: number;
|
|
13
|
+
guildId: string;
|
|
14
|
+
channelId: string;
|
|
15
|
+
platform: string;
|
|
16
|
+
notifyEnabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface Config {
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
user: string;
|
|
22
|
+
password: string;
|
|
23
|
+
database: string;
|
|
24
|
+
interval: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const Config: Schema<Config>;
|
|
27
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Config: () => Config,
|
|
34
|
+
apply: () => apply,
|
|
35
|
+
inject: () => inject,
|
|
36
|
+
name: () => name
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
var import_koishi = require("koishi");
|
|
40
|
+
var import_promise = __toESM(require("mysql2/promise"));
|
|
41
|
+
var name = "sourcebanspp";
|
|
42
|
+
var inject = {
|
|
43
|
+
required: ["database", "puppeteer"]
|
|
44
|
+
};
|
|
45
|
+
var logger = new import_koishi.Logger("sbpp");
|
|
46
|
+
var Config = import_koishi.Schema.object({
|
|
47
|
+
host: import_koishi.Schema.string().default("localhost").description("SourceBans 数据库地址"),
|
|
48
|
+
port: import_koishi.Schema.number().default(3306).description("数据库端口"),
|
|
49
|
+
user: import_koishi.Schema.string().default("root").description("数据库用户名"),
|
|
50
|
+
password: import_koishi.Schema.string().role("secret").description("数据库密码"),
|
|
51
|
+
database: import_koishi.Schema.string().default("sourcebans").description("数据库名"),
|
|
52
|
+
interval: import_koishi.Schema.number().default(60).description("新封禁检查间隔(秒)")
|
|
53
|
+
});
|
|
54
|
+
function apply(ctx, config) {
|
|
55
|
+
ctx.model.extend("sbpp_settings", {
|
|
56
|
+
id: "unsigned",
|
|
57
|
+
guildId: "string",
|
|
58
|
+
channelId: "string",
|
|
59
|
+
platform: "string",
|
|
60
|
+
notifyEnabled: "boolean"
|
|
61
|
+
}, {
|
|
62
|
+
primary: "id",
|
|
63
|
+
autoInc: true,
|
|
64
|
+
unique: [["channelId", "platform"]]
|
|
65
|
+
});
|
|
66
|
+
const pool = import_promise.default.createPool({
|
|
67
|
+
host: config.host,
|
|
68
|
+
port: config.port,
|
|
69
|
+
user: config.user,
|
|
70
|
+
password: config.password,
|
|
71
|
+
database: config.database,
|
|
72
|
+
waitForConnections: true,
|
|
73
|
+
connectionLimit: 10
|
|
74
|
+
});
|
|
75
|
+
let lastCheckTime = Math.floor(Date.now() / 1e3) - 300;
|
|
76
|
+
function formatDate(timestamp) {
|
|
77
|
+
if (timestamp <= 0) return "永久";
|
|
78
|
+
const date = new Date(timestamp * 1e3);
|
|
79
|
+
return date.toLocaleString("zh-CN", { hour12: false });
|
|
80
|
+
}
|
|
81
|
+
__name(formatDate, "formatDate");
|
|
82
|
+
function convertSeconds(seconds) {
|
|
83
|
+
if (seconds <= 0) return "永久";
|
|
84
|
+
const periods = [
|
|
85
|
+
{ unit: "年", value: 31536e3 },
|
|
86
|
+
{ unit: "月", value: 2592e3 },
|
|
87
|
+
{ unit: "周", value: 604800 },
|
|
88
|
+
{ unit: "天", value: 86400 },
|
|
89
|
+
{ unit: "小时", value: 3600 },
|
|
90
|
+
{ unit: "分钟", value: 60 }
|
|
91
|
+
];
|
|
92
|
+
const result = [];
|
|
93
|
+
let remaining = seconds;
|
|
94
|
+
for (const period of periods) {
|
|
95
|
+
if (remaining >= period.value) {
|
|
96
|
+
const count = Math.floor(remaining / period.value);
|
|
97
|
+
remaining %= period.value;
|
|
98
|
+
result.push(`${count}${period.unit}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result.join(" ") || "0分钟";
|
|
102
|
+
}
|
|
103
|
+
__name(convertSeconds, "convertSeconds");
|
|
104
|
+
function generateHtml(data, type) {
|
|
105
|
+
const css = `
|
|
106
|
+
body { font-family: 'Microsoft YaHei', sans-serif; background-color: #f5f5f5; padding: 20px; color: #333; }
|
|
107
|
+
.container { background-color: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 20px; max-width: 600px; margin: 0 auto; }
|
|
108
|
+
.header { text-align: center; padding-bottom: 15px; border-bottom: 1px solid #eee; margin-bottom: 20px; }
|
|
109
|
+
.header h1 { color: #e74c3c; margin: 0; }
|
|
110
|
+
.info-item { margin-bottom: 10px; padding: 8px 15px; background-color: #f9f9f9; border-radius: 5px; display: flex; }
|
|
111
|
+
.info-label { font-weight: bold; min-width: 100px; color: #555; }
|
|
112
|
+
.info-content { flex: 1; word-break: break-all; }
|
|
113
|
+
.footer { text-align: center; margin-top: 20px; color: #888; font-size: 12px; }
|
|
114
|
+
.timestamp { text-align: right; font-size: 12px; color: #999; margin-bottom: 10px; }
|
|
115
|
+
.ban-record { margin-bottom: 15px; padding: 10px; border-radius: 5px; background-color: #f8f9fa; border-left: 4px solid #e74c3c; }
|
|
116
|
+
.ban-record .name { font-weight: bold; margin-bottom: 5px; }
|
|
117
|
+
.ban-record .details { display: flex; justify-content: space-between; font-size: 13px; color: #666; }
|
|
118
|
+
`;
|
|
119
|
+
let content = "";
|
|
120
|
+
if (type === "single") {
|
|
121
|
+
content = `
|
|
122
|
+
<div class="info-item"><div class="info-label">玩家名称:</div><div class="info-content">${data.name}</div></div>
|
|
123
|
+
<div class="info-item"><div class="info-label">SteamID:</div><div class="info-content">${data.steamid}</div></div>
|
|
124
|
+
<div class="info-item"><div class="info-label">服务器IP:</div><div class="info-content">${data.server_ip}</div></div>
|
|
125
|
+
<div class="info-item"><div class="info-label">管理员:</div><div class="info-content">${data.admin_name}</div></div>
|
|
126
|
+
<div class="info-item"><div class="info-label">开始时间:</div><div class="info-content">${data.start_time}</div></div>
|
|
127
|
+
<div class="info-item"><div class="info-label">结束时间:</div><div class="info-content">${data.end_time}</div></div>
|
|
128
|
+
<div class="info-item"><div class="info-label">持续时间:</div><div class="info-content">${data.duration}</div></div>
|
|
129
|
+
<div class="info-item"><div class="info-label">原因:</div><div class="info-content">${data.reason}</div></div>
|
|
130
|
+
`;
|
|
131
|
+
} else {
|
|
132
|
+
content = data.bans.map((ban) => `
|
|
133
|
+
<div class="ban-record">
|
|
134
|
+
<div class="name">${ban.name}</div>
|
|
135
|
+
<div>SteamID: ${ban.steamid}</div>
|
|
136
|
+
<div class="details">
|
|
137
|
+
<div>时间: ${ban.time}</div>
|
|
138
|
+
<div>时长: ${ban.duration}</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div>原因: ${ban.reason}</div>
|
|
141
|
+
</div>
|
|
142
|
+
`).join("");
|
|
143
|
+
}
|
|
144
|
+
return `
|
|
145
|
+
<!DOCTYPE html>
|
|
146
|
+
<html lang="en">
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="UTF-8">
|
|
149
|
+
<style>${css}</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="container">
|
|
153
|
+
<div class="timestamp">生成时间: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}</div>
|
|
154
|
+
<div class="header"><h1>${data.title}</h1></div>
|
|
155
|
+
${content}
|
|
156
|
+
<div class="footer">SourceBans++ | Koishi Query</div>
|
|
157
|
+
</div>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
__name(generateHtml, "generateHtml");
|
|
163
|
+
async function renderImage(html, height) {
|
|
164
|
+
const page = await ctx.puppeteer.page();
|
|
165
|
+
try {
|
|
166
|
+
await page.setContent(html);
|
|
167
|
+
const element = await page.$(".container");
|
|
168
|
+
const boundingBox = await element.boundingBox();
|
|
169
|
+
const img = await page.screenshot({
|
|
170
|
+
clip: boundingBox,
|
|
171
|
+
encoding: "binary"
|
|
172
|
+
});
|
|
173
|
+
return img;
|
|
174
|
+
} finally {
|
|
175
|
+
await page.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
__name(renderImage, "renderImage");
|
|
179
|
+
ctx.command("sbpp.newban <switch:string>", "设置新封禁通知开关", { authority: 3 }).alias("新封禁通知").action(async ({ session }, toggle) => {
|
|
180
|
+
if (!toggle || !["on", "off"].includes(toggle.toLowerCase())) {
|
|
181
|
+
return "参数错误,请使用:sbpp newban on/off";
|
|
182
|
+
}
|
|
183
|
+
const enable = toggle.toLowerCase() === "on";
|
|
184
|
+
const { platform, channelId } = session;
|
|
185
|
+
const guildId = session.guildId || "";
|
|
186
|
+
const existing = await ctx.database.get("sbpp_settings", {
|
|
187
|
+
platform,
|
|
188
|
+
channelId
|
|
189
|
+
});
|
|
190
|
+
if (existing.length > 0) {
|
|
191
|
+
await ctx.database.set(
|
|
192
|
+
"sbpp_settings",
|
|
193
|
+
{ platform, channelId },
|
|
194
|
+
{ notifyEnabled: enable }
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
await ctx.database.create("sbpp_settings", {
|
|
198
|
+
platform,
|
|
199
|
+
channelId,
|
|
200
|
+
guildId,
|
|
201
|
+
notifyEnabled: enable
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return `新封禁通知已${enable ? "启用" : "禁用"}`;
|
|
205
|
+
});
|
|
206
|
+
ctx.command("sbpp.recent [count:number]", "查询最近封禁").action(async ({ session }, count) => {
|
|
207
|
+
const limit = Math.min(count || 5, 20);
|
|
208
|
+
try {
|
|
209
|
+
const [rows] = await pool.query(
|
|
210
|
+
"SELECT created, ends, authid, name, reason, length FROM sb_bans ORDER BY created DESC LIMIT ?",
|
|
211
|
+
[limit]
|
|
212
|
+
);
|
|
213
|
+
if (!rows.length) return "⚠️ 暂无近期封禁记录";
|
|
214
|
+
const bans = rows.map((r) => ({
|
|
215
|
+
name: r.name || "未知玩家",
|
|
216
|
+
steamid: r.authid,
|
|
217
|
+
time: formatDate(r.created),
|
|
218
|
+
duration: convertSeconds(r.length),
|
|
219
|
+
reason: (r.reason || "无原因").substring(0, 50)
|
|
220
|
+
}));
|
|
221
|
+
const html = generateHtml({ title: "📜 最近封禁记录", bans }, "multiple");
|
|
222
|
+
const img = await renderImage(html, 400 + bans.length * 80);
|
|
223
|
+
return import_koishi.h.image(img, "image/png");
|
|
224
|
+
} catch (e) {
|
|
225
|
+
logger.error(e);
|
|
226
|
+
return "数据库查询失败";
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const cmdMap = {
|
|
230
|
+
"mute": { type: 1, title: "静音", table: "sb_comms" },
|
|
231
|
+
"gag": { type: 2, title: "禁言", table: "sb_comms" },
|
|
232
|
+
"ban": { type: 3, title: "封禁", table: "sb_bans" }
|
|
233
|
+
};
|
|
234
|
+
ctx.command("sbpp <type> <query>", "查询 SourceBans 记录").action(async ({ session }, type, query) => {
|
|
235
|
+
if (!type || !query) return "参数错误!格式:sbpp [mute/gag/ban] [SteamID/名称]";
|
|
236
|
+
if (type && !query) {
|
|
237
|
+
await session.send(`请输入要查询的 ${type} 对象(SteamID 或名称):`);
|
|
238
|
+
query = await session.prompt();
|
|
239
|
+
if (!query) return;
|
|
240
|
+
}
|
|
241
|
+
const config2 = cmdMap[type.toLowerCase()];
|
|
242
|
+
if (!config2) return "类型错误!只支持 mute/gag/ban";
|
|
243
|
+
try {
|
|
244
|
+
let sql = "";
|
|
245
|
+
let params = [];
|
|
246
|
+
let whereClause = "";
|
|
247
|
+
if (query.startsWith("STEAM_")) {
|
|
248
|
+
whereClause = `${config2.table === "sb_bans" ? "b" : "c"}.authid = ?`;
|
|
249
|
+
params = [query];
|
|
250
|
+
} else {
|
|
251
|
+
whereClause = `${config2.table === "sb_bans" ? "b" : "c"}.name LIKE ?`;
|
|
252
|
+
params = [`%${query}%`];
|
|
253
|
+
}
|
|
254
|
+
if (config2.table === "sb_bans") {
|
|
255
|
+
sql = `
|
|
256
|
+
SELECT b.created, b.ends, b.reason, b.authid, b.name, b.length AS duration,
|
|
257
|
+
s.ip AS server_ip, a.user AS admin_name
|
|
258
|
+
FROM sb_bans AS b
|
|
259
|
+
LEFT JOIN sb_servers AS s ON b.sid = s.sid
|
|
260
|
+
LEFT JOIN sb_admins AS a ON b.aid = a.aid
|
|
261
|
+
WHERE ${whereClause}
|
|
262
|
+
ORDER BY b.created DESC LIMIT 1
|
|
263
|
+
`;
|
|
264
|
+
} else {
|
|
265
|
+
sql = `
|
|
266
|
+
SELECT c.created, c.ends, c.reason, c.authid, c.name, c.length AS duration,
|
|
267
|
+
s.ip AS server_ip, a.user AS admin_name
|
|
268
|
+
FROM sb_comms AS c
|
|
269
|
+
LEFT JOIN sb_servers AS s ON c.sid = s.sid
|
|
270
|
+
LEFT JOIN sb_admins AS a ON c.aid = a.aid
|
|
271
|
+
WHERE ${whereClause} AND c.type = ?
|
|
272
|
+
ORDER BY c.created DESC LIMIT 1
|
|
273
|
+
`;
|
|
274
|
+
params.push(config2.type);
|
|
275
|
+
}
|
|
276
|
+
const [rows] = await pool.query(sql, params);
|
|
277
|
+
const result = rows[0];
|
|
278
|
+
if (!result) return "⚠️ 未找到有效记录";
|
|
279
|
+
const data = {
|
|
280
|
+
title: `🔍 ${config2.title} 记录详情`,
|
|
281
|
+
name: result.name || "未知玩家",
|
|
282
|
+
steamid: result.authid || "未知SteamID",
|
|
283
|
+
server_ip: result.server_ip || "未知服务器",
|
|
284
|
+
admin_name: result.admin_name || "未知管理员",
|
|
285
|
+
duration: convertSeconds(result.duration),
|
|
286
|
+
reason: result.reason || "无原因",
|
|
287
|
+
start_time: formatDate(result.created),
|
|
288
|
+
end_time: formatDate(result.ends)
|
|
289
|
+
};
|
|
290
|
+
const html = generateHtml(data, "single");
|
|
291
|
+
const img = await renderImage(html, 600);
|
|
292
|
+
return import_koishi.h.image(img, "image/png");
|
|
293
|
+
} catch (e) {
|
|
294
|
+
logger.error(e);
|
|
295
|
+
return "查询出错:" + e.message;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
ctx.setInterval(async () => {
|
|
299
|
+
const currentTime = Math.floor(Date.now() / 1e3);
|
|
300
|
+
try {
|
|
301
|
+
const [rows] = await pool.query(
|
|
302
|
+
`SELECT b.created, b.ends, b.authid, b.name, b.reason, b.length,
|
|
303
|
+
s.ip AS server_ip, a.user AS admin_name
|
|
304
|
+
FROM sb_bans AS b
|
|
305
|
+
LEFT JOIN sb_servers AS s ON b.sid = s.sid
|
|
306
|
+
LEFT JOIN sb_admins AS a ON b.aid = a.aid
|
|
307
|
+
WHERE b.created > ? ORDER BY b.created DESC`,
|
|
308
|
+
[lastCheckTime]
|
|
309
|
+
);
|
|
310
|
+
if (rows && rows.length > 0) {
|
|
311
|
+
const settings = await ctx.database.get("sbpp_settings", { notifyEnabled: true });
|
|
312
|
+
if (settings.length > 0) {
|
|
313
|
+
for (const ban of rows) {
|
|
314
|
+
const data = {
|
|
315
|
+
title: "📣 新封禁通知",
|
|
316
|
+
name: ban.name || "未知玩家",
|
|
317
|
+
steamid: ban.authid,
|
|
318
|
+
server_ip: ban.server_ip || "未知服务器",
|
|
319
|
+
admin_name: ban.admin_name || "未知管理员",
|
|
320
|
+
duration: convertSeconds(ban.length),
|
|
321
|
+
reason: (ban.reason || "无原因").substring(0, 50),
|
|
322
|
+
start_time: formatDate(ban.created),
|
|
323
|
+
end_time: formatDate(ban.ends)
|
|
324
|
+
};
|
|
325
|
+
const html = generateHtml(data, "single");
|
|
326
|
+
const img = await renderImage(html, 600);
|
|
327
|
+
for (const setting of settings) {
|
|
328
|
+
await ctx.broadcast([`${setting.platform}:${setting.channelId}`], import_koishi.h.image(img, "image/png"));
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
lastCheckTime = currentTime;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
logger.error("Check ban task failed:", e);
|
|
337
|
+
}
|
|
338
|
+
}, config.interval * 1e3);
|
|
339
|
+
ctx.on("dispose", () => {
|
|
340
|
+
pool.end();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
__name(apply, "apply");
|
|
344
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
345
|
+
0 && (module.exports = {
|
|
346
|
+
Config,
|
|
347
|
+
apply,
|
|
348
|
+
inject,
|
|
349
|
+
name
|
|
350
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-sourcebanspp",
|
|
3
|
+
"description": "用于查询SourceBans++指定内容/推送最新的Ban",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin",
|
|
16
|
+
"sourcebanspp"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"koishi": "^4.18.9"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"mysql2": "^3.16.0"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
}
|
|
28
|
+
}
|