koishi-plugin-group-verification 1.0.23 → 1.0.25
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 +6 -6
- package/lib/index.js +209 -103
- package/package.json +1 -1
- package/readme.md +3 -1
- package/src/index.ts +223 -124
package/lib/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface ParsedArgs {
|
|
|
80
80
|
* 返回最终的 reminderEnabled 和 reminderMessage。
|
|
81
81
|
* 对于 -nomsg 不会清除已保存的 message,便于后续再次启用时恢复。
|
|
82
82
|
*/
|
|
83
|
+
export declare function usageString(): string;
|
|
83
84
|
export declare function mergeReminder(existingConfig: any | null, cleanedOptions: {
|
|
84
85
|
message?: string;
|
|
85
86
|
enableMessage?: boolean;
|
|
@@ -88,12 +89,11 @@ export declare function mergeReminder(existingConfig: any | null, cleanedOptions
|
|
|
88
89
|
reminderEnabled: boolean;
|
|
89
90
|
reminderMessage: string;
|
|
90
91
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*/
|
|
92
|
+
export declare function syncTotalStats(ctx: Context): Promise<void>;
|
|
93
|
+
export declare function updateStats(ctx: Context, groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected'): Promise<void>;
|
|
94
|
+
export declare function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]>;
|
|
95
|
+
export declare function handleGuildMemberRequestEvent(ctx: Context, session: any): Promise<void>;
|
|
96
|
+
export declare function __getAutoQueue(): Map<string, Set<string>>;
|
|
97
97
|
export declare function parseConfigArgs(raw: string): ParsedArgs;
|
|
98
98
|
export declare function verifyApplication(config: GroupVerificationConfig, message: string, session: any): Promise<{
|
|
99
99
|
isValid: boolean;
|
package/lib/index.js
CHANGED
|
@@ -21,13 +21,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
Config: () => Config,
|
|
24
|
+
__getAutoQueue: () => __getAutoQueue,
|
|
24
25
|
apply: () => apply,
|
|
26
|
+
checkPermission: () => checkPermission,
|
|
25
27
|
handleFailedVerification: () => handleFailedVerification,
|
|
28
|
+
handleGuildMemberRequestEvent: () => handleGuildMemberRequestEvent,
|
|
26
29
|
inject: () => inject,
|
|
27
30
|
mergeReminder: () => mergeReminder,
|
|
28
31
|
name: () => name,
|
|
29
32
|
parseConfigArgs: () => parseConfigArgs,
|
|
33
|
+
syncTotalStats: () => syncTotalStats,
|
|
30
34
|
tokenize: () => tokenize,
|
|
35
|
+
updateStats: () => updateStats,
|
|
36
|
+
usageString: () => usageString,
|
|
31
37
|
verifyApplication: () => verifyApplication
|
|
32
38
|
});
|
|
33
39
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -35,7 +41,7 @@ var import_koishi = require("koishi");
|
|
|
35
41
|
var name = "group-verification";
|
|
36
42
|
var logger = console;
|
|
37
43
|
var Config = import_koishi.Schema.object({
|
|
38
|
-
defaultReminderMessage: import_koishi.Schema.string().description("
|
|
44
|
+
defaultReminderMessage: import_koishi.Schema.string().description("默认提醒消息模板(使用 \n 表示换行,可包含下方变量)").default("{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}\n使用 gva 同意或 gvr 拒绝申请"),
|
|
39
45
|
enableStrictGroupCheck: import_koishi.Schema.boolean().description("是否启用严格的群号检查(检查群号长度)").default(false),
|
|
40
46
|
logLevel: import_koishi.Schema.union(["debug", "info", "warn", "error"]).description("日志级别").default("info")
|
|
41
47
|
}).description("群组验证插件配置");
|
|
@@ -143,6 +149,31 @@ function validateKeywordFormat(raw) {
|
|
|
143
149
|
return true;
|
|
144
150
|
}
|
|
145
151
|
__name(validateKeywordFormat, "validateKeywordFormat");
|
|
152
|
+
function usageString() {
|
|
153
|
+
return `用法:
|
|
154
|
+
# 创建/修改配置
|
|
155
|
+
gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
|
|
156
|
+
gvc -m 1 -t 2 # 修改审核参数
|
|
157
|
+
|
|
158
|
+
# 提醒消息控制(可用变量详见下方)
|
|
159
|
+
gvc -msg "消息内容" # 修改提醒消息
|
|
160
|
+
gvc -nomsg # 禁用提醒消息
|
|
161
|
+
# 查询/删除
|
|
162
|
+
gvc -? # 查询配置
|
|
163
|
+
gvc -r # 删除配置
|
|
164
|
+
|
|
165
|
+
审核方式说明(使用 -m 参数):
|
|
166
|
+
0 全部同意(默认)
|
|
167
|
+
1 按数量同意,需要 -t 指定数量
|
|
168
|
+
2 按比例同意,需要 -t 指定百分比
|
|
169
|
+
3 全部拒绝(拒绝后系统会自动阻止任何通过)
|
|
170
|
+
|
|
171
|
+
提醒消息可用变量:{user} 用户名 {id} 用户ID
|
|
172
|
+
{group} 群号 {gname} 群名称
|
|
173
|
+
{question} 申请理由 {answer} 匹配情况 {threshold} 阈值
|
|
174
|
+
使用 \\n 换行`;
|
|
175
|
+
}
|
|
176
|
+
__name(usageString, "usageString");
|
|
146
177
|
function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasRealEnableMessageParam, hasRealDisableMessageParam, logger2) {
|
|
147
178
|
let reminderEnabled = true;
|
|
148
179
|
let reminderMessage = "{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}";
|
|
@@ -166,6 +197,151 @@ function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasR
|
|
|
166
197
|
return { reminderEnabled, reminderMessage };
|
|
167
198
|
}
|
|
168
199
|
__name(mergeReminder, "mergeReminder");
|
|
200
|
+
var autoQueue = /* @__PURE__ */ new Map();
|
|
201
|
+
async function syncTotalStats(ctx) {
|
|
202
|
+
try {
|
|
203
|
+
const allStats = await ctx.database.get("group_verification_stats", {
|
|
204
|
+
groupId: { $ne: "TOTAL" }
|
|
205
|
+
});
|
|
206
|
+
if (allStats.length > 0) {
|
|
207
|
+
const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0);
|
|
208
|
+
const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0);
|
|
209
|
+
const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0);
|
|
210
|
+
await ctx.database.set("group_verification_stats", { groupId: "TOTAL" }, {
|
|
211
|
+
autoApproved: totalAutoApproved,
|
|
212
|
+
manuallyApproved: totalManuallyApproved,
|
|
213
|
+
rejected: totalRejected,
|
|
214
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
215
|
+
});
|
|
216
|
+
logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.error("同步总计统计时出错:", error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
__name(syncTotalStats, "syncTotalStats");
|
|
223
|
+
async function updateStats(ctx, groupId, action) {
|
|
224
|
+
const existingStats = await ctx.database.get("group_verification_stats", { groupId });
|
|
225
|
+
if (existingStats.length > 0) {
|
|
226
|
+
const stats = existingStats[0];
|
|
227
|
+
await ctx.database.set("group_verification_stats", { id: stats.id }, {
|
|
228
|
+
[action]: stats[action] + 1,
|
|
229
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
await ctx.database.create("group_verification_stats", {
|
|
233
|
+
groupId,
|
|
234
|
+
autoApproved: action === "autoApproved" ? 1 : 0,
|
|
235
|
+
manuallyApproved: action === "manuallyApproved" ? 1 : 0,
|
|
236
|
+
rejected: action === "rejected" ? 1 : 0,
|
|
237
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
await syncTotalStats(ctx);
|
|
241
|
+
}
|
|
242
|
+
__name(updateStats, "updateStats");
|
|
243
|
+
async function checkPermission(session, targetGroupId) {
|
|
244
|
+
const groupId = targetGroupId || session.guildId;
|
|
245
|
+
if (!groupId) {
|
|
246
|
+
return [false, "请在群聊中使用此命令或使用 -i 参数指定群号"];
|
|
247
|
+
}
|
|
248
|
+
logger.info(`权限检查 - 用户ID: ${session.userId}, 群号: ${groupId}`);
|
|
249
|
+
const koishiAuthority = session.author?.authority || session.user?.authority;
|
|
250
|
+
logger.info(`权限检查 - Koishi权限等级: ${koishiAuthority || "未获取到"}`);
|
|
251
|
+
if (!session.author) {
|
|
252
|
+
logger.info(`权限检查 - session中可能的权限字段:`, {
|
|
253
|
+
authority: session.authority,
|
|
254
|
+
permission: session.permission,
|
|
255
|
+
role: session.role
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
logger.info(`权限检查 - author对象中的字段:`, {
|
|
259
|
+
authority: session.author.authority,
|
|
260
|
+
permission: session.author.permission,
|
|
261
|
+
role: session.author.role,
|
|
262
|
+
permissions: session.author.permissions
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (session.user) {
|
|
266
|
+
logger.info(`权限检查 - user对象中的权限信息:`, {
|
|
267
|
+
authority: session.user.authority,
|
|
268
|
+
permission: session.user.permission,
|
|
269
|
+
role: session.user.role
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (koishiAuthority && koishiAuthority >= 3) {
|
|
273
|
+
logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`);
|
|
274
|
+
return [true];
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const member = await session.bot.getGuildMember(groupId, session.userId);
|
|
278
|
+
logger.info(`权限检查 - 获取到成员信息:`, {
|
|
279
|
+
roles: member?.roles,
|
|
280
|
+
permissions: member?.permissions
|
|
281
|
+
});
|
|
282
|
+
if (member) {
|
|
283
|
+
if (member.permissions?.includes("OWNER") || member.roles?.includes("owner")) {
|
|
284
|
+
logger.info(`权限检查 - 用户是群主`);
|
|
285
|
+
return [true];
|
|
286
|
+
}
|
|
287
|
+
if (member.roles?.includes("admin") || member.permissions?.includes("ADMINISTRATOR")) {
|
|
288
|
+
logger.info(`权限检查 - 用户是管理员`);
|
|
289
|
+
return [true];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
logger.warn("权限检查获取成员信息失败", e);
|
|
294
|
+
}
|
|
295
|
+
return [false, "权限不足"];
|
|
296
|
+
}
|
|
297
|
+
__name(checkPermission, "checkPermission");
|
|
298
|
+
async function handleGuildMemberRequestEvent(ctx, session) {
|
|
299
|
+
logger.debug("guild-member-request event", session);
|
|
300
|
+
let guildId = (session.guildId || session.channelId || "").toString().trim();
|
|
301
|
+
const userId = session.userId;
|
|
302
|
+
const message = session.content || "";
|
|
303
|
+
if (!guildId) {
|
|
304
|
+
logger.warn("guild-member-request 没有 guildId,跳过处理");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const requestId = session.event?.requestId || session.messageId || "";
|
|
308
|
+
const groupConfig = await ctx.database.get("group_verification_config", { groupId: guildId });
|
|
309
|
+
if (!groupConfig || groupConfig.length === 0) return;
|
|
310
|
+
const config = groupConfig[0];
|
|
311
|
+
if (config.reviewMethod === 3) {
|
|
312
|
+
logger.info(`配置要求全部拒绝,自动拒绝用户 ${userId}`);
|
|
313
|
+
if (requestId) {
|
|
314
|
+
try {
|
|
315
|
+
await session.bot.handleGuildMemberRequest(requestId, false);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
logger.warn("自动拒绝失败", e);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
await updateStats(ctx, guildId, "rejected");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
|
|
324
|
+
logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
325
|
+
if (isValid) {
|
|
326
|
+
if (requestId) {
|
|
327
|
+
try {
|
|
328
|
+
await session.bot.handleGuildMemberRequest(requestId, true);
|
|
329
|
+
logger.info(`自动同意 requestId=${requestId}`);
|
|
330
|
+
if (!autoQueue.has(guildId)) autoQueue.set(guildId, /* @__PURE__ */ new Set());
|
|
331
|
+
autoQueue.get(guildId).add(userId);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
logger.warn("自动同意失败", e);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
__name(handleGuildMemberRequestEvent, "handleGuildMemberRequestEvent");
|
|
341
|
+
function __getAutoQueue() {
|
|
342
|
+
return autoQueue;
|
|
343
|
+
}
|
|
344
|
+
__name(__getAutoQueue, "__getAutoQueue");
|
|
169
345
|
function parseConfigArgs(raw) {
|
|
170
346
|
const res = tokenize(raw);
|
|
171
347
|
if (res.error) {
|
|
@@ -326,7 +502,16 @@ async function handleFailedVerification(ctx, session, config, matchedCount, requ
|
|
|
326
502
|
const channel = rawChannel || guildId;
|
|
327
503
|
const target = rawChannel ? [channel, guildId] : guildId;
|
|
328
504
|
logger.debug("broadcast target", { channel, guildId, target });
|
|
329
|
-
|
|
505
|
+
if (session.bot && typeof session.bot.broadcast === "function") {
|
|
506
|
+
try {
|
|
507
|
+
await session.bot.broadcast([target], reminderMsg);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
logger.warn("bot.broadcast failed, fallback to ctx.broadcast", err);
|
|
510
|
+
await ctx.broadcast([target], reminderMsg);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
await ctx.broadcast([target], reminderMsg);
|
|
514
|
+
}
|
|
330
515
|
}
|
|
331
516
|
__name(handleFailedVerification, "handleFailedVerification");
|
|
332
517
|
function apply(ctx, config) {
|
|
@@ -377,47 +562,12 @@ function apply(ctx, config) {
|
|
|
377
562
|
primary: "id",
|
|
378
563
|
autoInc: true
|
|
379
564
|
});
|
|
380
|
-
const autoQueue = /* @__PURE__ */ new Map();
|
|
381
|
-
ctx.on("guild-member-request", async (session) => {
|
|
382
|
-
logger.debug("guild-member-request event", session);
|
|
383
|
-
let guildId = (session.guildId || session.channelId || "").toString().trim();
|
|
384
|
-
const userId = session.userId;
|
|
385
|
-
const message = session.content || "";
|
|
386
|
-
if (!guildId) {
|
|
387
|
-
logger.warn("guild-member-request 没有 guildId,跳过处理");
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
const requestId = session.event?.requestId || session.messageId || "";
|
|
391
|
-
const groupConfig = await ctx.database.get("group_verification_config", {
|
|
392
|
-
groupId: guildId
|
|
393
|
-
});
|
|
394
|
-
if (!groupConfig || groupConfig.length === 0) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const config2 = groupConfig[0];
|
|
398
|
-
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config2, message, session);
|
|
399
|
-
logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
400
|
-
if (isValid) {
|
|
401
|
-
if (requestId) {
|
|
402
|
-
try {
|
|
403
|
-
await session.bot.handleGuildMemberRequest(requestId, true);
|
|
404
|
-
logger.info(`自动同意 requestId=${requestId}`);
|
|
405
|
-
if (!autoQueue.has(guildId)) autoQueue.set(guildId, /* @__PURE__ */ new Set());
|
|
406
|
-
autoQueue.get(guildId).add(userId);
|
|
407
|
-
} catch (e) {
|
|
408
|
-
logger.warn("自动同意失败", e);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
await handleFailedVerification(ctx, session, config2, matchedCount, requiredThreshold);
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
565
|
ctx.on("guild-member-added", async (session) => {
|
|
416
566
|
const groupId = session.guildId;
|
|
417
567
|
const userId = session.userId;
|
|
418
568
|
const set = autoQueue.get(groupId);
|
|
419
569
|
if (set && set.has(userId)) {
|
|
420
|
-
await updateStats(groupId, "autoApproved");
|
|
570
|
+
await updateStats(ctx, groupId, "autoApproved");
|
|
421
571
|
set.delete(userId);
|
|
422
572
|
logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`);
|
|
423
573
|
return;
|
|
@@ -427,35 +577,15 @@ function apply(ctx, config) {
|
|
|
427
577
|
userId
|
|
428
578
|
});
|
|
429
579
|
if (pendingRecords.length > 0) {
|
|
430
|
-
await updateStats(groupId, "autoApproved");
|
|
580
|
+
await updateStats(ctx, groupId, "autoApproved");
|
|
431
581
|
await ctx.database.remove("group_verification_pending", { id: pendingRecords[0].id });
|
|
432
582
|
logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`);
|
|
433
583
|
} else {
|
|
434
|
-
await updateStats(groupId, "manuallyApproved");
|
|
584
|
+
await updateStats(ctx, groupId, "manuallyApproved");
|
|
435
585
|
logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`);
|
|
436
586
|
}
|
|
437
587
|
});
|
|
438
|
-
async function
|
|
439
|
-
const existingStats = await ctx.database.get("group_verification_stats", { groupId });
|
|
440
|
-
if (existingStats.length > 0) {
|
|
441
|
-
const stats = existingStats[0];
|
|
442
|
-
await ctx.database.set("group_verification_stats", { id: stats.id }, {
|
|
443
|
-
[action]: stats[action] + 1,
|
|
444
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
445
|
-
});
|
|
446
|
-
} else {
|
|
447
|
-
await ctx.database.create("group_verification_stats", {
|
|
448
|
-
groupId,
|
|
449
|
-
autoApproved: action === "autoApproved" ? 1 : 0,
|
|
450
|
-
manuallyApproved: action === "manuallyApproved" ? 1 : 0,
|
|
451
|
-
rejected: action === "rejected" ? 1 : 0,
|
|
452
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
await syncTotalStats(ctx);
|
|
456
|
-
}
|
|
457
|
-
__name(updateStats, "updateStats");
|
|
458
|
-
async function checkPermission(session, targetGroupId) {
|
|
588
|
+
async function checkPermission2(session, targetGroupId) {
|
|
459
589
|
const groupId = targetGroupId || session.guildId;
|
|
460
590
|
if (!groupId) {
|
|
461
591
|
return [false, "请在群聊中使用此命令或使用 -i 参数指定群号"];
|
|
@@ -513,7 +643,7 @@ function apply(ctx, config) {
|
|
|
513
643
|
return [false, `权限不足:需要群主/管理员权限或koishi三级以上权限
|
|
514
644
|
${debugInfo}`];
|
|
515
645
|
}
|
|
516
|
-
__name(
|
|
646
|
+
__name(checkPermission2, "checkPermission");
|
|
517
647
|
const groupVerify = ctx.command("group-verify", "群组验证管理命令").alias("gv", "gverify");
|
|
518
648
|
groupVerify.subcommand(".config [keywords:text]", "配置群组验证规则").alias(
|
|
519
649
|
"gv.cfg",
|
|
@@ -561,7 +691,7 @@ ${debugInfo}`];
|
|
|
561
691
|
}
|
|
562
692
|
try {
|
|
563
693
|
await session.bot.getGuild(targetGroupId);
|
|
564
|
-
const [hasPermission, errorMsg] = await
|
|
694
|
+
const [hasPermission, errorMsg] = await checkPermission2(session, targetGroupId);
|
|
565
695
|
if (!hasPermission) {
|
|
566
696
|
return errorMsg || "权限不足";
|
|
567
697
|
}
|
|
@@ -649,13 +779,7 @@ ${debugInfo}`];
|
|
|
649
779
|
if (keywordList.length === 0 && !cleanedOptions.query && !cleanedOptions.remove) {
|
|
650
780
|
const hasConfigParams = cleanedOptions.method !== void 0 || cleanedOptions.threshold !== void 0 || hasRealMessageParam || hasRealEnableMessageParam || hasRealDisableMessageParam;
|
|
651
781
|
if (!hasConfigParams) {
|
|
652
|
-
return
|
|
653
|
-
gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
|
|
654
|
-
gvc -m 1 -t 2 # 修改审核参数
|
|
655
|
-
gvc -msg "消息内容" # 修改提醒消息
|
|
656
|
-
gvc -nomsg # 禁用提醒消息
|
|
657
|
-
gvc -? # 查询配置
|
|
658
|
-
gvc -r # 删除配置`;
|
|
782
|
+
return usageString();
|
|
659
783
|
}
|
|
660
784
|
}
|
|
661
785
|
let existingConfig = null;
|
|
@@ -811,7 +935,7 @@ gvc -r # 删除配置`;
|
|
|
811
935
|
"group-verify.同意",
|
|
812
936
|
"gva"
|
|
813
937
|
).action(async ({ session }, userId) => {
|
|
814
|
-
const [hasPermission, errorMsg] = await
|
|
938
|
+
const [hasPermission, errorMsg] = await checkPermission2(session);
|
|
815
939
|
if (!hasPermission) {
|
|
816
940
|
return errorMsg || "权限不足";
|
|
817
941
|
}
|
|
@@ -819,6 +943,10 @@ gvc -r # 删除配置`;
|
|
|
819
943
|
if (!groupId) {
|
|
820
944
|
return "请在群聊中使用此命令";
|
|
821
945
|
}
|
|
946
|
+
const configs = await ctx.database.get("group_verification_config", { groupId });
|
|
947
|
+
if (configs.length > 0 && configs[0].reviewMethod === 3) {
|
|
948
|
+
return "该群已设为全部拒绝,无法手动同意任何申请";
|
|
949
|
+
}
|
|
822
950
|
if (!userId || userId.toLowerCase() === "all") {
|
|
823
951
|
if (userId?.toLowerCase() === "all") {
|
|
824
952
|
const pendingRequests2 = await ctx.database.get("group_verification_pending", { groupId });
|
|
@@ -829,8 +957,6 @@ gvc -r # 删除配置`;
|
|
|
829
957
|
for (const request2 of pendingRequests2) {
|
|
830
958
|
try {
|
|
831
959
|
await ctx.database.remove("group_verification_pending", { id: request2.id });
|
|
832
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
|
|
833
|
-
autoQueue.get(groupId).add(request2.userId);
|
|
834
960
|
approvedCount++;
|
|
835
961
|
} catch (error) {
|
|
836
962
|
logger.warn(`处理申请 ${request2.id} 时出错:`, error);
|
|
@@ -846,8 +972,6 @@ gvc -r # 删除配置`;
|
|
|
846
972
|
try {
|
|
847
973
|
await session.bot.handleGuildMemberRequest(request2.userId, true);
|
|
848
974
|
await ctx.database.remove("group_verification_pending", { id: request2.id });
|
|
849
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
|
|
850
|
-
autoQueue.get(groupId).add(request2.userId);
|
|
851
975
|
return `已同意用户 ${request2.userName}(${request2.userId}) 的加群申请`;
|
|
852
976
|
} catch (error) {
|
|
853
977
|
return `处理申请时出错: ${error.message}`;
|
|
@@ -865,8 +989,6 @@ gvc -r # 删除配置`;
|
|
|
865
989
|
try {
|
|
866
990
|
await session.bot.handleGuildMemberRequest(request.userId, true);
|
|
867
991
|
await ctx.database.remove("group_verification_pending", { id: request.id });
|
|
868
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
|
|
869
|
-
autoQueue.get(groupId).add(userId);
|
|
870
992
|
return `已同意用户 ${request.userName}(${userId}) 的加群申请`;
|
|
871
993
|
} catch (error) {
|
|
872
994
|
return `处理申请时出错: ${error.message}`;
|
|
@@ -881,7 +1003,7 @@ gvc -r # 删除配置`;
|
|
|
881
1003
|
"group-verify.rej",
|
|
882
1004
|
"gvr"
|
|
883
1005
|
).action(async ({ session }, userId) => {
|
|
884
|
-
const [hasPermission, errorMsg] = await
|
|
1006
|
+
const [hasPermission, errorMsg] = await checkPermission2(session);
|
|
885
1007
|
if (!hasPermission) {
|
|
886
1008
|
return errorMsg || "权限不足";
|
|
887
1009
|
}
|
|
@@ -899,7 +1021,7 @@ gvc -r # 删除配置`;
|
|
|
899
1021
|
for (const request2 of pendingRequests2) {
|
|
900
1022
|
try {
|
|
901
1023
|
await ctx.database.remove("group_verification_pending", { id: request2.id });
|
|
902
|
-
await updateStats(groupId, "rejected");
|
|
1024
|
+
await updateStats(ctx, groupId, "rejected");
|
|
903
1025
|
rejectedCount++;
|
|
904
1026
|
} catch (error) {
|
|
905
1027
|
logger.warn(`处理申请 ${request2.id} 时出错:`, error);
|
|
@@ -914,7 +1036,7 @@ gvc -r # 删除配置`;
|
|
|
914
1036
|
const request2 = recentRequest[0];
|
|
915
1037
|
try {
|
|
916
1038
|
await ctx.database.remove("group_verification_pending", { id: request2.id });
|
|
917
|
-
await updateStats(groupId, "rejected");
|
|
1039
|
+
await updateStats(ctx, groupId, "rejected");
|
|
918
1040
|
return `已拒绝用户 ${request2.userName}(${request2.userId}) 的加群申请`;
|
|
919
1041
|
} catch (error) {
|
|
920
1042
|
return `处理申请时出错: ${error.message}`;
|
|
@@ -931,7 +1053,7 @@ gvc -r # 删除配置`;
|
|
|
931
1053
|
const request = pendingRequests[0];
|
|
932
1054
|
try {
|
|
933
1055
|
await ctx.database.remove("group_verification_pending", { id: request.id });
|
|
934
|
-
await updateStats(groupId, "rejected");
|
|
1056
|
+
await updateStats(ctx, groupId, "rejected");
|
|
935
1057
|
return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`;
|
|
936
1058
|
} catch (error) {
|
|
937
1059
|
return `处理申请时出错: ${error.message}`;
|
|
@@ -1136,39 +1258,23 @@ gvc -r # 删除配置`;
|
|
|
1136
1258
|
最后更新: ${lastUpdated}`;
|
|
1137
1259
|
}
|
|
1138
1260
|
__name(getTotalStats, "getTotalStats");
|
|
1139
|
-
async function syncTotalStats(ctx2) {
|
|
1140
|
-
try {
|
|
1141
|
-
const allStats = await ctx2.database.get("group_verification_stats", {
|
|
1142
|
-
groupId: { $ne: "TOTAL" }
|
|
1143
|
-
});
|
|
1144
|
-
if (allStats.length > 0) {
|
|
1145
|
-
const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0);
|
|
1146
|
-
const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0);
|
|
1147
|
-
const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0);
|
|
1148
|
-
await ctx2.database.set("group_verification_stats", { groupId: "TOTAL" }, {
|
|
1149
|
-
autoApproved: totalAutoApproved,
|
|
1150
|
-
manuallyApproved: totalManuallyApproved,
|
|
1151
|
-
rejected: totalRejected,
|
|
1152
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1153
|
-
});
|
|
1154
|
-
logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
|
|
1155
|
-
}
|
|
1156
|
-
} catch (error) {
|
|
1157
|
-
logger.error("同步总计统计时出错:", error);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
__name(syncTotalStats, "syncTotalStats");
|
|
1161
1261
|
}
|
|
1162
1262
|
__name(apply, "apply");
|
|
1163
1263
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1164
1264
|
0 && (module.exports = {
|
|
1165
1265
|
Config,
|
|
1266
|
+
__getAutoQueue,
|
|
1166
1267
|
apply,
|
|
1268
|
+
checkPermission,
|
|
1167
1269
|
handleFailedVerification,
|
|
1270
|
+
handleGuildMemberRequestEvent,
|
|
1168
1271
|
inject,
|
|
1169
1272
|
mergeReminder,
|
|
1170
1273
|
name,
|
|
1171
1274
|
parseConfigArgs,
|
|
1275
|
+
syncTotalStats,
|
|
1172
1276
|
tokenize,
|
|
1277
|
+
updateStats,
|
|
1278
|
+
usageString,
|
|
1173
1279
|
verifyApplication
|
|
1174
1280
|
});
|
package/package.json
CHANGED
package/readme.md
CHANGED
package/src/index.ts
CHANGED
|
@@ -56,7 +56,9 @@ export interface Config {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export const Config: Schema<Config> = Schema.object({
|
|
59
|
-
defaultReminderMessage: Schema.string()
|
|
59
|
+
defaultReminderMessage: Schema.string()
|
|
60
|
+
.description('默认提醒消息模板(使用 \n 表示换行,可包含下方变量)')
|
|
61
|
+
.default('{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}\n使用 gva 同意或 gvr 拒绝申请'),
|
|
60
62
|
enableStrictGroupCheck: Schema.boolean().description('是否启用严格的群号检查(检查群号长度)').default(false),
|
|
61
63
|
logLevel: Schema.union(['debug', 'info', 'warn', 'error']).description('日志级别').default('info')
|
|
62
64
|
}).description('群组验证插件配置')
|
|
@@ -216,6 +218,31 @@ function validateKeywordFormat(raw: string): boolean {
|
|
|
216
218
|
* 返回最终的 reminderEnabled 和 reminderMessage。
|
|
217
219
|
* 对于 -nomsg 不会清除已保存的 message,便于后续再次启用时恢复。
|
|
218
220
|
*/
|
|
221
|
+
export function usageString(): string {
|
|
222
|
+
return `用法:
|
|
223
|
+
# 创建/修改配置
|
|
224
|
+
gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
|
|
225
|
+
gvc -m 1 -t 2 # 修改审核参数
|
|
226
|
+
|
|
227
|
+
# 提醒消息控制(可用变量详见下方)
|
|
228
|
+
gvc -msg "消息内容" # 修改提醒消息
|
|
229
|
+
gvc -nomsg # 禁用提醒消息
|
|
230
|
+
# 查询/删除
|
|
231
|
+
gvc -? # 查询配置
|
|
232
|
+
gvc -r # 删除配置
|
|
233
|
+
|
|
234
|
+
审核方式说明(使用 -m 参数):
|
|
235
|
+
0 全部同意(默认)
|
|
236
|
+
1 按数量同意,需要 -t 指定数量
|
|
237
|
+
2 按比例同意,需要 -t 指定百分比
|
|
238
|
+
3 全部拒绝(拒绝后系统会自动阻止任何通过)
|
|
239
|
+
|
|
240
|
+
提醒消息可用变量:{user} 用户名 {id} 用户ID
|
|
241
|
+
{group} 群号 {gname} 群名称
|
|
242
|
+
{question} 申请理由 {answer} 匹配情况 {threshold} 阈值
|
|
243
|
+
使用 \\n 换行`;
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
export function mergeReminder(
|
|
220
247
|
existingConfig: any | null,
|
|
221
248
|
cleanedOptions: {
|
|
@@ -260,6 +287,177 @@ export function mergeReminder(
|
|
|
260
287
|
* 返回关键词数组和各类 flag 的值,未出现的 flag 保持 undefined。
|
|
261
288
|
* 若检测到格式错误(如纯空格分隔关键词),返回 error 字段。
|
|
262
289
|
*/
|
|
290
|
+
// 全局缓存:记录通过机器人自动批准的用户,供 guild-member-added 事件使用
|
|
291
|
+
const autoQueue = new Map<string, Set<string>>();
|
|
292
|
+
|
|
293
|
+
// 更新统计信息函数,提取到模块层供多个位置调用
|
|
294
|
+
// synchronize overall statistics across groups
|
|
295
|
+
export async function syncTotalStats(ctx: Context) {
|
|
296
|
+
try {
|
|
297
|
+
// 获取所有群组统计(排除TOTAL行)
|
|
298
|
+
const allStats = await ctx.database.get('group_verification_stats', {
|
|
299
|
+
groupId: { $ne: 'TOTAL' }
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
if (allStats.length > 0) {
|
|
303
|
+
// 计算总计
|
|
304
|
+
const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0)
|
|
305
|
+
const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0)
|
|
306
|
+
const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0)
|
|
307
|
+
|
|
308
|
+
// 更新总计行
|
|
309
|
+
await ctx.database.set('group_verification_stats', { groupId: 'TOTAL' }, {
|
|
310
|
+
autoApproved: totalAutoApproved,
|
|
311
|
+
manuallyApproved: totalManuallyApproved,
|
|
312
|
+
rejected: totalRejected,
|
|
313
|
+
lastUpdated: new Date().toISOString()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logger.error('同步总计统计时出错:', error)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function updateStats(ctx: Context, groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected') {
|
|
324
|
+
// 更新群组统计
|
|
325
|
+
const existingStats = await ctx.database.get('group_verification_stats', { groupId })
|
|
326
|
+
|
|
327
|
+
if (existingStats.length > 0) {
|
|
328
|
+
const stats = existingStats[0]
|
|
329
|
+
await ctx.database.set('group_verification_stats', { id: stats.id }, {
|
|
330
|
+
[action]: stats[action] + 1,
|
|
331
|
+
lastUpdated: new Date().toISOString()
|
|
332
|
+
})
|
|
333
|
+
} else {
|
|
334
|
+
await ctx.database.create('group_verification_stats', {
|
|
335
|
+
groupId,
|
|
336
|
+
autoApproved: action === 'autoApproved' ? 1 : 0,
|
|
337
|
+
manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
|
|
338
|
+
rejected: action === 'rejected' ? 1 : 0,
|
|
339
|
+
lastUpdated: new Date().toISOString()
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 同步更新总计统计
|
|
344
|
+
await syncTotalStats(ctx)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 权限检查函数(也可用于命令)
|
|
348
|
+
export async function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]> {
|
|
349
|
+
const groupId = targetGroupId || session.guildId
|
|
350
|
+
|
|
351
|
+
// 私聊情况下必须指定群号
|
|
352
|
+
if (!groupId) {
|
|
353
|
+
return [false, '请在群聊中使用此命令或使用 -i 参数指定群号']
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
logger.info(`权限检查 - 用户ID: ${session.userId}, 群号: ${groupId}`)
|
|
357
|
+
const koishiAuthority = session.author?.authority || session.user?.authority
|
|
358
|
+
logger.info(`权限检查 - Koishi权限等级: ${koishiAuthority || '未获取到'}`)
|
|
359
|
+
|
|
360
|
+
if (!session.author) {
|
|
361
|
+
logger.info(`权限检查 - session中可能的权限字段:`, {
|
|
362
|
+
authority: session.authority,
|
|
363
|
+
permission: session.permission,
|
|
364
|
+
role: session.role
|
|
365
|
+
})
|
|
366
|
+
} else {
|
|
367
|
+
logger.info(`权限检查 - author对象中的字段:`, {
|
|
368
|
+
authority: session.author.authority,
|
|
369
|
+
permission: session.author.permission,
|
|
370
|
+
role: session.author.role,
|
|
371
|
+
permissions: session.author.permissions
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (session.user) {
|
|
376
|
+
logger.info(`权限检查 - user对象中的权限信息:`, {
|
|
377
|
+
authority: session.user.authority,
|
|
378
|
+
permission: session.user.permission,
|
|
379
|
+
role: session.user.role
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (koishiAuthority && koishiAuthority >= 3) {
|
|
384
|
+
logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`)
|
|
385
|
+
return [true]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const member = await session.bot.getGuildMember(groupId, session.userId)
|
|
390
|
+
logger.info(`权限检查 - 获取到成员信息:`, {
|
|
391
|
+
roles: member?.roles,
|
|
392
|
+
permissions: member?.permissions
|
|
393
|
+
})
|
|
394
|
+
if (member) {
|
|
395
|
+
if (member.permissions?.includes('OWNER') || member.roles?.includes('owner')) {
|
|
396
|
+
logger.info(`权限检查 - 用户是群主`)
|
|
397
|
+
return [true]
|
|
398
|
+
}
|
|
399
|
+
if (member.roles?.includes('admin') || member.permissions?.includes('ADMINISTRATOR')) {
|
|
400
|
+
logger.info(`权限检查 - 用户是管理员`)
|
|
401
|
+
return [true]
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (e) {
|
|
405
|
+
logger.warn('权限检查获取成员信息失败', e)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return [false, '权限不足']
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 提供给测试的辅助函数:处理 guild-member-request 事件的逻辑
|
|
412
|
+
export async function handleGuildMemberRequestEvent(ctx: Context, session: any) {
|
|
413
|
+
logger.debug('guild-member-request event', session)
|
|
414
|
+
let guildId = (session.guildId || session.channelId || '').toString().trim();
|
|
415
|
+
const userId = session.userId;
|
|
416
|
+
const message = session.content || '';
|
|
417
|
+
|
|
418
|
+
if (!guildId) {
|
|
419
|
+
logger.warn('guild-member-request 没有 guildId,跳过处理');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const requestId = ((session.event as any)?.requestId) || session.messageId || '';
|
|
424
|
+
const groupConfig = await ctx.database.get('group_verification_config', { groupId: guildId });
|
|
425
|
+
if (!groupConfig || groupConfig.length === 0) return;
|
|
426
|
+
const config = groupConfig[0];
|
|
427
|
+
|
|
428
|
+
if (config.reviewMethod === 3) {
|
|
429
|
+
logger.info(`配置要求全部拒绝,自动拒绝用户 ${userId}`);
|
|
430
|
+
if (requestId) {
|
|
431
|
+
try { await session.bot.handleGuildMemberRequest(requestId, false); } catch (e) { logger.warn('自动拒绝失败', e); }
|
|
432
|
+
}
|
|
433
|
+
await updateStats(ctx, guildId, 'rejected');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
|
|
438
|
+
logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
439
|
+
|
|
440
|
+
if (isValid) {
|
|
441
|
+
if (requestId) {
|
|
442
|
+
try {
|
|
443
|
+
await session.bot.handleGuildMemberRequest(requestId, true);
|
|
444
|
+
logger.info(`自动同意 requestId=${requestId}`);
|
|
445
|
+
if (!autoQueue.has(guildId)) autoQueue.set(guildId, new Set());
|
|
446
|
+
autoQueue.get(guildId)!.add(userId);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
logger.warn('自动同意失败', e);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 供测试读取当前 autoQueue 状态
|
|
457
|
+
export function __getAutoQueue() {
|
|
458
|
+
return autoQueue;
|
|
459
|
+
}
|
|
460
|
+
|
|
263
461
|
export function parseConfigArgs(raw: string): ParsedArgs {
|
|
264
462
|
const res = tokenize(raw);
|
|
265
463
|
if (res.error) {
|
|
@@ -471,8 +669,18 @@ export async function handleFailedVerification(
|
|
|
471
669
|
const channel = rawChannel || guildId
|
|
472
670
|
const target: string | [string, string] = rawChannel ? [channel, guildId] : guildId
|
|
473
671
|
logger.debug('broadcast target', { channel, guildId, target })
|
|
474
|
-
//
|
|
475
|
-
|
|
672
|
+
// prefer using bot.broadcast since ctx.broadcast may not support tuple
|
|
673
|
+
if (session.bot && typeof session.bot.broadcast === 'function') {
|
|
674
|
+
try {
|
|
675
|
+
await session.bot.broadcast([target], reminderMsg as any)
|
|
676
|
+
} catch (err) {
|
|
677
|
+
// fallback to ctx.broadcast if bot.broadcast fails for some reason
|
|
678
|
+
logger.warn('bot.broadcast failed, fallback to ctx.broadcast', err)
|
|
679
|
+
await (ctx.broadcast as any)([target], reminderMsg)
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
await (ctx.broadcast as any)([target], reminderMsg)
|
|
683
|
+
}
|
|
476
684
|
}
|
|
477
685
|
|
|
478
686
|
export function apply(ctx: Context, config: Config) {
|
|
@@ -533,57 +741,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
533
741
|
autoInc: true
|
|
534
742
|
})
|
|
535
743
|
|
|
536
|
-
// 缓存正在审批的用户,用于避免 guild-member-added 重复计数
|
|
537
|
-
const autoQueue = new Map<string, Set<string>>();
|
|
538
|
-
|
|
539
|
-
// 监听加群申请事件
|
|
540
|
-
ctx.on('guild-member-request', async (session) => {
|
|
541
|
-
// debug: 输出整个 session 以便定位字段名
|
|
542
|
-
logger.debug('guild-member-request event', session)
|
|
543
|
-
|
|
544
|
-
let guildId = (session.guildId || session.channelId || '').toString().trim();
|
|
545
|
-
const userId = session.userId;
|
|
546
|
-
const message = session.content || '';
|
|
547
|
-
|
|
548
|
-
if (!guildId) {
|
|
549
|
-
logger.warn('guild-member-request 没有 guildId,跳过处理');
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 获取 requestId(不同平台字段不同)
|
|
554
|
-
const requestId = ((session.event as any)?.requestId) || session.messageId || '';
|
|
555
744
|
|
|
556
|
-
// 获取群组配置
|
|
557
|
-
const groupConfig = await ctx.database.get('group_verification_config', {
|
|
558
|
-
groupId: guildId
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
if (!groupConfig || groupConfig.length === 0) {
|
|
562
|
-
// 无配置直接放行(需要有 requestId)
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const config = groupConfig[0];
|
|
566
|
-
|
|
567
|
-
// 执行验证,记录详细情况
|
|
568
|
-
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
|
|
569
|
-
logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
570
|
-
|
|
571
|
-
if (isValid) {
|
|
572
|
-
if (requestId) {
|
|
573
|
-
try {
|
|
574
|
-
await session.bot.handleGuildMemberRequest(requestId, true);
|
|
575
|
-
logger.info(`自动同意 requestId=${requestId}`);
|
|
576
|
-
// 将此用户标记为自动批准,等待 guild-member-added 更新统计
|
|
577
|
-
if (!autoQueue.has(guildId)) autoQueue.set(guildId, new Set());
|
|
578
|
-
autoQueue.get(guildId)!.add(userId);
|
|
579
|
-
} catch (e) {
|
|
580
|
-
logger.warn('自动同意失败', e);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
} else {
|
|
584
|
-
await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
745
|
|
|
588
746
|
// 监听群成员增加事件(包括手动邀请入群)
|
|
589
747
|
ctx.on('guild-member-added', async (session) => {
|
|
@@ -593,7 +751,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
593
751
|
// 先检查 autoQueue
|
|
594
752
|
const set = autoQueue.get(groupId)
|
|
595
753
|
if (set && set.has(userId)) {
|
|
596
|
-
await updateStats(groupId, 'autoApproved')
|
|
754
|
+
await updateStats(ctx, groupId, 'autoApproved')
|
|
597
755
|
set.delete(userId)
|
|
598
756
|
logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`)
|
|
599
757
|
return
|
|
@@ -607,13 +765,13 @@ export function apply(ctx: Context, config: Config) {
|
|
|
607
765
|
|
|
608
766
|
if (pendingRecords.length > 0) {
|
|
609
767
|
// 通过验证的用户入群,更新统计
|
|
610
|
-
await updateStats(groupId, 'autoApproved')
|
|
768
|
+
await updateStats(ctx, groupId, 'autoApproved')
|
|
611
769
|
// 清除待审核记录
|
|
612
770
|
await ctx.database.remove('group_verification_pending', { id: pendingRecords[0].id })
|
|
613
771
|
logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`)
|
|
614
772
|
} else {
|
|
615
773
|
// 手动邀请入群,记录到手动批准统计
|
|
616
|
-
await updateStats(groupId, 'manuallyApproved')
|
|
774
|
+
await updateStats(ctx, groupId, 'manuallyApproved')
|
|
617
775
|
logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`)
|
|
618
776
|
}
|
|
619
777
|
})
|
|
@@ -621,29 +779,6 @@ export function apply(ctx: Context, config: Config) {
|
|
|
621
779
|
|
|
622
780
|
|
|
623
781
|
// 更新统计信息
|
|
624
|
-
async function updateStats(groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected') {
|
|
625
|
-
// 更新群组统计
|
|
626
|
-
const existingStats = await ctx.database.get('group_verification_stats', { groupId })
|
|
627
|
-
|
|
628
|
-
if (existingStats.length > 0) {
|
|
629
|
-
const stats = existingStats[0]
|
|
630
|
-
await ctx.database.set('group_verification_stats', { id: stats.id }, {
|
|
631
|
-
[action]: stats[action] + 1,
|
|
632
|
-
lastUpdated: new Date().toISOString()
|
|
633
|
-
})
|
|
634
|
-
} else {
|
|
635
|
-
await ctx.database.create('group_verification_stats', {
|
|
636
|
-
groupId,
|
|
637
|
-
autoApproved: action === 'autoApproved' ? 1 : 0,
|
|
638
|
-
manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
|
|
639
|
-
rejected: action === 'rejected' ? 1 : 0,
|
|
640
|
-
lastUpdated: new Date().toISOString()
|
|
641
|
-
})
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// 同步更新总计统计
|
|
645
|
-
await syncTotalStats(ctx)
|
|
646
|
-
}
|
|
647
782
|
|
|
648
783
|
// 权限检查函数
|
|
649
784
|
async function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]> {
|
|
@@ -917,13 +1052,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
917
1052
|
hasRealEnableMessageParam ||
|
|
918
1053
|
hasRealDisableMessageParam
|
|
919
1054
|
if (!hasConfigParams) {
|
|
920
|
-
return
|
|
921
|
-
gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
|
|
922
|
-
gvc -m 1 -t 2 # 修改审核参数
|
|
923
|
-
gvc -msg "消息内容" # 修改提醒消息
|
|
924
|
-
gvc -nomsg # 禁用提醒消息
|
|
925
|
-
gvc -? # 查询配置
|
|
926
|
-
gvc -r # 删除配置`
|
|
1055
|
+
return usageString()
|
|
927
1056
|
}
|
|
928
1057
|
// 这里会在下面获取一次 existingConfig,故暂不重复查询
|
|
929
1058
|
}
|
|
@@ -1135,6 +1264,11 @@ gvc -r # 删除配置`
|
|
|
1135
1264
|
return '请在群聊中使用此命令'
|
|
1136
1265
|
}
|
|
1137
1266
|
|
|
1267
|
+
// 在开始之前检查配置是否为全部拒绝
|
|
1268
|
+
const configs = await ctx.database.get('group_verification_config', { groupId })
|
|
1269
|
+
if (configs.length > 0 && configs[0].reviewMethod === 3) {
|
|
1270
|
+
return '该群已设为全部拒绝,无法手动同意任何申请'
|
|
1271
|
+
}
|
|
1138
1272
|
// 处理默认情况和all情况
|
|
1139
1273
|
if (!userId || userId.toLowerCase() === 'all') {
|
|
1140
1274
|
if (userId?.toLowerCase() === 'all') {
|
|
@@ -1150,9 +1284,6 @@ gvc -r # 删除配置`
|
|
|
1150
1284
|
// TODO: 需要获取实际的requestId来进行审批
|
|
1151
1285
|
// await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1152
1286
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1153
|
-
// 标记为自动批准,实际统计在 guild-member-added 处理
|
|
1154
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
|
|
1155
|
-
autoQueue.get(groupId)!.add(request.userId)
|
|
1156
1287
|
approvedCount++
|
|
1157
1288
|
} catch (error) {
|
|
1158
1289
|
logger.warn(`处理申请 ${request.id} 时出错:`, error)
|
|
@@ -1172,8 +1303,6 @@ gvc -r # 删除配置`
|
|
|
1172
1303
|
// 这里需要获取实际的requestId,暂时用userId作为示例
|
|
1173
1304
|
await session.bot.handleGuildMemberRequest(request.userId, true)
|
|
1174
1305
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1175
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
|
|
1176
|
-
autoQueue.get(groupId)!.add(request.userId)
|
|
1177
1306
|
return `已同意用户 ${request.userName}(${request.userId}) 的加群申请`
|
|
1178
1307
|
} catch (error) {
|
|
1179
1308
|
return `处理申请时出错: ${error.message}`
|
|
@@ -1196,8 +1325,6 @@ gvc -r # 删除配置`
|
|
|
1196
1325
|
// 这里需要获取实际的requestId来进行审批
|
|
1197
1326
|
await session.bot.handleGuildMemberRequest(request.userId, true)
|
|
1198
1327
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1199
|
-
if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
|
|
1200
|
-
autoQueue.get(groupId)!.add(userId)
|
|
1201
1328
|
return `已同意用户 ${request.userName}(${userId}) 的加群申请`
|
|
1202
1329
|
} catch (error) {
|
|
1203
1330
|
return `处理申请时出错: ${error.message}`
|
|
@@ -1239,7 +1366,7 @@ gvc -r # 删除配置`
|
|
|
1239
1366
|
// TODO: 需要获取实际的requestId来进行拒绝
|
|
1240
1367
|
// await session.bot.handleGuildMemberRequest(request.requestId, false)
|
|
1241
1368
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1242
|
-
await updateStats(groupId, 'rejected')
|
|
1369
|
+
await updateStats(ctx, groupId, 'rejected')
|
|
1243
1370
|
rejectedCount++
|
|
1244
1371
|
} catch (error) {
|
|
1245
1372
|
logger.warn(`处理申请 ${request.id} 时出错:`, error)
|
|
@@ -1259,7 +1386,7 @@ gvc -r # 删除配置`
|
|
|
1259
1386
|
// TODO: 需要获取实际的requestId来进行拒绝
|
|
1260
1387
|
// await session.bot.handleGuildMemberRequest(request.requestId, false)
|
|
1261
1388
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1262
|
-
await updateStats(groupId, 'rejected')
|
|
1389
|
+
await updateStats(ctx, groupId, 'rejected')
|
|
1263
1390
|
return `已拒绝用户 ${request.userName}(${request.userId}) 的加群申请`
|
|
1264
1391
|
} catch (error) {
|
|
1265
1392
|
return `处理申请时出错: ${error.message}`
|
|
@@ -1282,7 +1409,7 @@ gvc -r # 删除配置`
|
|
|
1282
1409
|
// TODO: 需要获取实际的requestId来进行拒绝
|
|
1283
1410
|
// await session.bot.handleGuildMemberRequest(request.requestId, false)
|
|
1284
1411
|
await ctx.database.remove('group_verification_pending', { id: request.id })
|
|
1285
|
-
await updateStats(groupId, 'rejected')
|
|
1412
|
+
await updateStats(ctx, groupId, 'rejected')
|
|
1286
1413
|
return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`
|
|
1287
1414
|
} catch (error) {
|
|
1288
1415
|
return `处理申请时出错: ${error.message}`
|
|
@@ -1532,32 +1659,4 @@ gvc -r # 删除配置`
|
|
|
1532
1659
|
最后更新: ${lastUpdated}`
|
|
1533
1660
|
}
|
|
1534
1661
|
|
|
1535
|
-
// 同步统计数据到总计行的函数
|
|
1536
|
-
async function syncTotalStats(ctx: Context) {
|
|
1537
|
-
try {
|
|
1538
|
-
// 获取所有群组统计(排除TOTAL行)
|
|
1539
|
-
const allStats = await ctx.database.get('group_verification_stats', {
|
|
1540
|
-
groupId: { $ne: 'TOTAL' }
|
|
1541
|
-
})
|
|
1542
|
-
|
|
1543
|
-
if (allStats.length > 0) {
|
|
1544
|
-
// 计算总计
|
|
1545
|
-
const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0)
|
|
1546
|
-
const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0)
|
|
1547
|
-
const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0)
|
|
1548
|
-
|
|
1549
|
-
// 更新总计行
|
|
1550
|
-
await ctx.database.set('group_verification_stats', { groupId: 'TOTAL' }, {
|
|
1551
|
-
autoApproved: totalAutoApproved,
|
|
1552
|
-
manuallyApproved: totalManuallyApproved,
|
|
1553
|
-
rejected: totalRejected,
|
|
1554
|
-
lastUpdated: new Date().toISOString()
|
|
1555
|
-
})
|
|
1556
|
-
|
|
1557
|
-
logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)
|
|
1558
|
-
}
|
|
1559
|
-
} catch (error) {
|
|
1560
|
-
logger.error('同步总计统计时出错:', error)
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
1662
|
}
|