koishi-plugin-onebot-verifier 1.1.9 → 1.2.0

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.
Files changed (3) hide show
  1. package/lib/index.js +21 -7
  2. package/package.json +2 -2
  3. package/readme.md +74 -36
package/lib/index.js CHANGED
@@ -102,6 +102,7 @@ function apply(ctx, config) {
102
102
  const activeCaptchas = /* @__PURE__ */ new Map();
103
103
  const inviterMap = /* @__PURE__ */ new Map();
104
104
  const requestMap = /* @__PURE__ */ new Map();
105
+ const recentRemovals = /* @__PURE__ */ new Map();
105
106
  const getComment = /* @__PURE__ */ __name((comment) => {
106
107
  if (!comment) return "";
107
108
  const lines = comment.split(/[\r\n]+/).map((s) => s.trim());
@@ -111,7 +112,7 @@ function apply(ctx, config) {
111
112
  const executeAction = /* @__PURE__ */ __name(async (session, kind, pass, reason = "", remark = "") => {
112
113
  try {
113
114
  const eventData = session.event?._data || {};
114
- if (config.debugMode) logger.info(`[操作] 类型:${kind} 结果:${pass ? "同意" : "拒绝"} 原因:${reason || "无"}`);
115
+ if (config.debugMode) logger.info(`[操作] 类型: ${kind} 结果: ${pass ? "同意" : "拒绝"} 原因: ${reason || "无"}`);
115
116
  if (pass && kind === "guild" && session.guildId && session.userId) inviterMap.set(session.guildId, session.userId);
116
117
  if (!pass && kind === "guild" && session.guildId && (session.event?.type === "guild-added" || eventData.notice_type === "group_increase")) {
117
118
  if (reason) await session.bot?.sendMessage(session.guildId, `${reason},将退出该群`).catch(() => {
@@ -199,6 +200,16 @@ function apply(ctx, config) {
199
200
  if (eventData.group_id) session.guildId = String(eventData.group_id);
200
201
  try {
201
202
  if (config.debugMode) logger.info(`[请求] 类型: ${kind} 数据: ${JSON.stringify(eventData)}`);
203
+ const curTime = eventData.time || 0;
204
+ for (const task of activeTasks.values()) {
205
+ const o = task.session.event?._data || {};
206
+ if (Math.abs(curTime - (o.time || 0)) < 300) {
207
+ if (task.kind === kind && o.self_id === eventData.self_id && o.user_id === eventData.user_id && o.group_id === eventData.group_id && o.comment === eventData.comment) {
208
+ task.session = session;
209
+ return;
210
+ }
211
+ }
212
+ }
202
213
  const verifyText = getComment(eventData.comment);
203
214
  if (kind === "member") {
204
215
  const rules = config.verifyRules?.filter((r) => String(r.guildId) === String(session.guildId)) || [];
@@ -207,7 +218,7 @@ function apply(ctx, config) {
207
218
  const levelMatch = (stats?.qqLevel ?? 0) >= (rule.minLevel ?? 0);
208
219
  const keywordMatch = !rule.keyword || new RegExp(rule.keyword, "i").test(verifyText);
209
220
  if (config.debugMode) {
210
- if ((rule.minLevel ?? 0) > 0) logger.info(`[加群请求] ${session.userId} 等级 ${stats?.qqLevel ?? 0} ${levelMatch ? ">" : "<"} "${rule.minLevel ?? 0}"`);
221
+ if ((rule.minLevel ?? 0) > 0) logger.info(`[加群请求] ${session.userId} 等级 ${stats?.qqLevel ?? 0} ${levelMatch ? ">" : "<"} ${rule.minLevel ?? 0}`);
211
222
  if (rule.keyword) logger.info(`[加群请求] ${session.userId} 内容 "${verifyText}" ${keywordMatch ? "=" : "≠"} "${rule.keyword}"`);
212
223
  }
213
224
  if (levelMatch && keywordMatch) {
@@ -252,7 +263,7 @@ function apply(ctx, config) {
252
263
  if (config.friendLevel && config.friendLevel > 0 && session.onebot && session.userId) {
253
264
  const stats = await session.onebot.getStrangerInfo(session.userId, true).catch(() => ({}));
254
265
  levelPass = (stats.qqLevel ?? 0) >= config.friendLevel;
255
- if (config.debugMode) logger.info(`[好友验证] ${session.userId} 等级 ${stats.qqLevel ?? 0} ${levelPass ? ">" : "<"} "${config.friendLevel}"`);
266
+ if (config.debugMode) logger.info(`[好友验证] ${session.userId} 等级 ${stats.qqLevel ?? 0} ${levelPass ? ">" : "<"} ${config.friendLevel}`);
256
267
  }
257
268
  if (config.friendRegex) {
258
269
  regexPass = new RegExp(config.friendRegex, "i").test(verifyText);
@@ -272,8 +283,8 @@ function apply(ctx, config) {
272
283
  const minPass = (stats.member_count ?? 0) >= (config.minMembers ?? 0);
273
284
  const maxPass = (stats.max_member_count ?? 0) >= (config.maxCapacity ?? 0);
274
285
  if (config.debugMode) {
275
- if ((config.minMembers ?? 0) > 0) logger.info(`[群组邀请] ${session.guildId} 人数 ${stats.member_count ?? 0} ${minPass ? ">" : "<"} "${config.minMembers ?? 0}"`);
276
- if ((config.maxCapacity ?? 0) > 0) logger.info(`[群组邀请] ${session.guildId} 容量 ${stats.max_member_count ?? 0} ${maxPass ? ">" : "<"} "${config.maxCapacity ?? 0}"`);
286
+ if ((config.minMembers ?? 0) > 0) logger.info(`[群组邀请] ${session.guildId} 人数 ${stats.member_count ?? 0} ${minPass ? ">" : "<"} ${config.minMembers ?? 0}`);
287
+ if ((config.maxCapacity ?? 0) > 0) logger.info(`[群组邀请] ${session.guildId} 容量 ${stats.max_member_count ?? 0} ${maxPass ? ">" : "<"} ${config.maxCapacity ?? 0}`);
277
288
  }
278
289
  if (!minPass) verdict = `群人数不足 ${config.minMembers ?? 0} 人`;
279
290
  else if (!maxPass) verdict = `群容量不足 ${config.maxCapacity ?? 0} 人`;
@@ -367,8 +378,11 @@ function apply(ctx, config) {
367
378
  });
368
379
  ctx.on("guild-removed", async (session) => {
369
380
  if (session.guildId) {
370
- if (config.debugMode) logger.info(`[事件] 退出: ${session.guildId} 数据: ${JSON.stringify(session.event?._data)}`);
371
381
  const eventData = session.event?._data || {};
382
+ const curTime = eventData.time || 0;
383
+ if (Math.abs(curTime - (recentRemovals.get(session.guildId) || 0)) < 300) return;
384
+ recentRemovals.set(session.guildId, curTime);
385
+ if (config.debugMode) logger.info(`[事件] 退出: ${session.guildId} 数据: ${JSON.stringify(eventData)}`);
372
386
  if (eventData.sub_type === "kick_me") {
373
387
  const inviterId = inviterMap.get(session.guildId);
374
388
  if (inviterId) {
@@ -387,8 +401,8 @@ function apply(ctx, config) {
387
401
  await session.execute(`analyse.clear -g ${session.guildId}`).catch(() => {
388
402
  });
389
403
  if (config.debugMode) logger.info(`[操作] 清理群组数据: ${session.guildId}`);
404
+ await sendNotice(session, "removed");
390
405
  }
391
- await sendNotice(session, "removed");
392
406
  });
393
407
  ctx.middleware(async (session, next) => {
394
408
  if (typeof session.content !== "string") return next();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-onebot-verifier",
3
- "description": "适用于 OneBot 的审核插件。不仅支持好友/加群自动审核,还支持入群验证码、投票表决、被踢自动清理等功能。",
4
- "version": "1.1.9",
3
+ "description": "[仅支持 Onebot 平台]支持好友申请/群组邀请/加群请求等的自动审核,可根据等级与正则进行筛选,并有入群验证码、投票表决、被踢清理等功能。",
4
+ "version": "1.2.0",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
package/readme.md CHANGED
@@ -2,63 +2,101 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/koishi-plugin-onebot-verifier?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-onebot-verifier)
4
4
 
5
- 适用于 OneBot 的审核插件。不仅支持好友/加群自动审核,还支持**入群验证码**、**投票表决**、**被踢自动清理**等功能。
5
+ [仅支持 Onebot 平台]支持好友申请/群组邀请/加群请求等的自动审核,可根据等级与正则进行筛选,并有入群验证码、投票表决、被踢清理等功能。
6
+
7
+ ---
6
8
 
7
9
  ## ✨ 核心特性
8
10
 
9
- - 🛡️ **全场景覆盖**:好友申请、用户加群、机器人受邀进群、被踢后续处理。
10
- - 🧩 **多种验证模式**:
11
- - **自动规则**:基于 QQ 等级、正则关键词、群规模(成员数/容量)的秒级自动化处理。
12
- - **入群验证码**:新成员进群后触发数学运算挑战,失败自动移出。
13
- - **民主投票**:将加群申请转为群内投票,达到指定票数自动通过/拒绝。
14
- - 交互式人工干预:引用通知消息回复 `y/n` 即可操作,支持超时自动决策。
15
- - 🧹 **智能清理**:机器人被恶意踢出时,可自动删除邀请者或操作者的好友,并清理数据统计。
16
- - 📍 **就地审核**:支持在发生申请的群内直接发起审核,无需跳转管理群。
11
+ - 🛡️ **多维度自动化**:基于 QQ 等级、正则匹配、群员规模(成员数/总容量)自动处理请求。
12
+ - 🧩 **高级验证模式**:
13
+ - **数学验证码 (Captcha)**:进群后自动触发数学计算挑战,限时未通过自动移出。
14
+ - **民主投票 (Vote)**:加群申请可转为群内投票,支持设置通过/拒绝的票数阈值。
15
+ - 交互式人工干预:引用通知消息回复 `y/n` 即可操作,支持设置超时后的默认决策。
16
+ - 🧹 **智能清理与反制**:
17
+ - 被移出群组时,自动清理 `analyse` 插件的相关统计数据。
18
+ - **被踢反制**:自动删除将机器人移出群组的操作者或原始邀请者的好友。
19
+ - 📍 **就地审核逻辑**:支持“原群投票”模式,无需在管理私聊间跳转,直接在申请发生的群内解决。
20
+
21
+ ---
17
22
 
18
- ## ⚙️ 配置项深度解析
23
+ ## ⚙️ 配置项详解
19
24
 
20
25
  ### 1. 基础配置
21
26
 
22
- - **通知目标 (`notifyTarget`)**: 必填。格式为 `private:QQ号` 或 `guild:群号`。
23
- - **被踢自动处理 (`kickBan`)**: 开启后,若机器人被移出群组,将自动尝试删除邀请者和操作者的好友。
27
+ - **通知目标 (`notifyTarget`)**: 必填。格式为 `private:QQ号` 或 `guild:群号`。用于接收各类审核申请通知。
28
+ - **调试模式 (`debugMode`)**: 开启后在日志中输出详细的匹配逻辑和操作记录。
29
+ - **被踢自动处理 (`kickBan`)**: 开启后,若机器人被移出群组,会自动尝试删除相关管理员/邀请者的好友关系。
24
30
 
25
- ### 2. 好友与邀群 (机器人被邀)
31
+ ### 2. 机器人被邀请与好友申请
26
32
 
27
- - **超时处理 (`friendTimeout`)**: 数字表示分钟。正数表示超时自动同意,负数表示超时自动拒绝,`false` 表示永久等待。
33
+ - **超时处理 (`friendTimeout`)**:
34
+ - `false`: 永久等待人工处理。
35
+ - `正数`: X 分钟后自动**通过**。
36
+ - `负数`: X 分钟后自动**拒绝**。
28
37
  - **验证维度**:
29
- - `friendLevel`: 申请人最低 QQ 等级。
30
- - `friendRegex`: 匹配好友申请说明。
31
- - `minMembers` / `maxCapacity`: 校验目标群的活跃程度与规模。
38
+ - `friendLevel`: 申请人的最低 QQ 等级要求。
39
+ - `friendRegex`: 匹配好友申请或进群邀请的文字信息。
40
+ - `minMembers`: 机器人受邀进群时,目标群的最低人数门槛。
41
+ - `maxCapacity`: 目标群的最低群容量(如要求必须是 500 人以上规模的群)。
32
42
 
33
- ### 3. 加群请求 (用户入群)
43
+ ### 3. 用户加群请求 (普通验证)
34
44
 
35
- - **普通验证规则 (`verifyRules`)**: 表格化配置。可针对不同群设置不同的关键词正则和等级要求。
36
- - **特殊验证模式 (`specialRules`)**:
37
- - **投票模式 (vote)**: 引用通知回复 `y/n` 累计票数。
38
- - **验证码模式 (captcha)**: 机器人自动先通过申请,用户进群后需在 60s 内回复计算题。
45
+ - **频率限制模式 (`frequencyMode`)**: 当用户频繁申请时的策略:
46
+ - `delay`: 强制转为人工手动审核。
47
+ - `ignore`: 直接忽略该请求。
48
+ - `reject`: 自动拒绝。
49
+ - **普通验证规则 (`verifyRules`)**: 表格配置,可针对特定群号设置:
50
+ - 关键词正则匹配、最低等级要求、申请频率限制、以及匹配后的预设动作(接受/拒绝)。
39
51
 
40
- ### 4. 进阶安全
52
+ ### 4. 高级验证 (特殊模式)
41
53
 
42
- - **投票比例 (`voteRatio`)**: 格式如 `3:2`,表示 3 票赞成则通过,2 票反对则拒绝。
43
- - **就地投票 (`voteInSitu`)**: 开启后,投票通知将直接发往“该申请所属的群”而非全局通知目标。
44
- - **验证码难度 (`captchaDiff`)**: 简单(100以内加减)、中等(两位数乘一位数)、困难(两位数乘两位数)。
54
+ - **特殊验证模式 (`specialRules`)**:
55
+ - **投票模式 (`vote`)**: 申请信息发至通知目标(或原群),群员投票决定。
56
+ - **验证码模式 (`captcha`)**: 机器人先自动通过申请,用户入群后需完成挑战。
57
+ - **模式细节**:
58
+ - `voteRatio`: 投票通过/拒绝比例(如 `3:2` 表示 3 人赞成通过,2 人反对则拒绝)。
59
+ - `voteInSitu`: 是否在发生申请的群内直接发起投票。
60
+ - `captchaDiff`: 验证码难度(简单:加减法;中等:两位数乘一位数;困难:两位数乘法)。
61
+
62
+ ---
45
63
 
46
64
  ## 🎮 指令交互指南
47
65
 
48
- ### 人工审核 / 投票
66
+ ### 人工审核与投票
67
+
68
+ 当收到插件推送的审核通知时,**通过引用(回复)该消息**来进行操作:
69
+
70
+ | 动作 | 指令 | 效果 |
71
+ | :--- | :--- | :--- |
72
+ | **通过申请** | `y` 或 `通过` | 同意该请求 |
73
+ | **拒绝申请** | `n` 或 `拒绝 [理由]` | 拒绝请求并(可选)发送拒绝理由 |
74
+ | **好友备注** | `y [备注名]` | 通过好友申请并直接设置备注(仅限好友类型) |
75
+ | **参与投票** | 引用消息回复 `y` 或 `n` | 累加赞成或反对票数,达到阈值自动执行 |
76
+
77
+ ### 验证码挑战
78
+
79
+ 在开启了 `captcha` 模式的群组中:
80
+
81
+ 1. 用户进群后,机器人会 `@` 该用户并发出题目。
82
+ 2. 用户需在 **60 秒** 内直接回复正确数字。
83
+ 3. **超时或错误**:机器人将自动将该用户移出群组(需机器人拥有管理员权限)。
84
+
85
+ ---
86
+
87
+ ## 🛠️ 进阶功能说明
49
88
 
50
- 当收到审核通知时,**引用该消息**并回复:
89
+ ### 自动化数据清理
51
90
 
52
- - **通过**:回复 `y` 或 `通过`。
53
- - **拒绝**:回复 `n` 或 `拒绝 [理由]`。
54
- - **备注**:好友申请通过时,`y 备注名` 可直接修改好友备注。
91
+ 当机器人退出群组或被踢出时,插件会自动触发以下逻辑:
55
92
 
56
- ### 自动清理逻辑
93
+ 1. **统计清理**:自动执行 `analyse.clear -g [群号]`,确保数据准确。
94
+ 2. **好友切断**:如果开启了 `kickBan`,插件会识别操作者 ID,并自动执行 `delete_friend`,防止被恶意刷群后残留大量垃圾好友。
57
95
 
58
- 1. **数据清理**:当机器人退出或被踢出群组时,插件会自动执行 `analyse.clear`清理相关统计。
59
- 2. **反制措施**:若开启 `kickBan`,被踢后会自动切断与相关管理员的好友关系。
96
+ ---
60
97
 
61
98
  ## ⚠️ 注意事项
62
99
 
63
- - 使用验证码模式时,请确保机器人拥有**管理员权限**,否则无法在验证失败时踢出成员。
64
- - `notifyTarget` 必须符合 `platform:id` 格式,且机器人必须与该目标存在好友或群成员关系。
100
+ 1. **权限要求**:使用“验证码”或“自动踢人”功能,请务必保证机器人在该群拥有**管理员或群主**权限。
101
+ 2. **通知目标**:`notifyTarget` 必须符合 `platform:id` 格式,且机器人必须与该目标存在好友关系或身处该群内。
102
+ 3. **优先级**:普通验证规则 (`verifyRules`) 的优先级高于默认的手动审核逻辑。