koishi-plugin-group-control 1.0.2 → 1.0.4
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/database.d.ts +16 -0
- package/lib/index.js +124 -19
- package/package.json +1 -1
package/lib/database.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface SmallGroupWhitelist {
|
|
|
25
25
|
platform: string;
|
|
26
26
|
guildId: string;
|
|
27
27
|
}
|
|
28
|
+
export interface SelfLeftGuild {
|
|
29
|
+
platform: string;
|
|
30
|
+
guildId: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
28
33
|
export interface PendingInvite {
|
|
29
34
|
platform: string;
|
|
30
35
|
groupId: string;
|
|
@@ -48,6 +53,7 @@ declare module 'koishi' {
|
|
|
48
53
|
command_frequency_record: CommandFrequencyRecord;
|
|
49
54
|
group_bot_status: GroupBotStatus;
|
|
50
55
|
small_group_whitelist: SmallGroupWhitelist;
|
|
56
|
+
self_left_guild: SelfLeftGuild;
|
|
51
57
|
pending_invite: PendingInvite;
|
|
52
58
|
pending_friend_request: PendingFriendRequest;
|
|
53
59
|
}
|
|
@@ -60,6 +66,16 @@ export declare function removeBlacklistedGuild(ctx: Context, guildId: string): P
|
|
|
60
66
|
export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<import("minato").Driver.WriteResult>;
|
|
61
67
|
export declare function getAllBlacklistedGuilds(ctx: Context): Promise<BlacklistedGuild[]>;
|
|
62
68
|
export declare function clearBlacklistedGuilds(ctx: Context): Promise<import("minato").Driver.WriteResult>;
|
|
69
|
+
/** 统一写入被踢黑名单行,保证 platform = BLACKLIST_PLATFORM */
|
|
70
|
+
export declare function blacklistKicked(ctx: Context, guildId: string): Promise<import("minato").Driver.WriteResult>;
|
|
71
|
+
/** 在主动退群前写入标记,让 guild-removed 能区分「自己退的」和「被踢的」*/
|
|
72
|
+
export declare function markSelfLeft(ctx: Context, guildId: string): Promise<void>;
|
|
73
|
+
/** 消费标记(单次读取后删除),返回是否在 maxAgeSec 内。用于 guild-removed 判断是自己退的 */
|
|
74
|
+
export declare function consumeSelfLeft(ctx: Context, guildId: string, maxAgeSec?: number): Promise<boolean>;
|
|
75
|
+
/** 清理标记(退群失败时回滚,或 unban 时清理)*/
|
|
76
|
+
export declare function clearSelfLeft(ctx: Context, guildId: string): Promise<void>;
|
|
77
|
+
/** 定期清理过期的主动退群标记(超过 maxAgeSec 秒未消费的)*/
|
|
78
|
+
export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<number>;
|
|
63
79
|
export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
|
|
64
80
|
export declare function updateCommandFrequencyRecord(ctx: Context, platform: string, guildId: string, data: Partial<CommandFrequencyRecord>): Promise<void>;
|
|
65
81
|
export declare function getGroupBotStatus(ctx: Context, platform: string, guildId: string): Promise<GroupBotStatus | null>;
|
package/lib/index.js
CHANGED
|
@@ -34,9 +34,13 @@ __export(database_exports, {
|
|
|
34
34
|
addPendingInvite: () => addPendingInvite,
|
|
35
35
|
addToSmallGroupWhitelist: () => addToSmallGroupWhitelist,
|
|
36
36
|
apply: () => apply,
|
|
37
|
+
blacklistKicked: () => blacklistKicked,
|
|
37
38
|
clearBlacklistedGuilds: () => clearBlacklistedGuilds,
|
|
38
39
|
clearExpiredPendingFriendRequests: () => clearExpiredPendingFriendRequests,
|
|
39
40
|
clearExpiredPendingInvites: () => clearExpiredPendingInvites,
|
|
41
|
+
clearExpiredSelfLeft: () => clearExpiredSelfLeft,
|
|
42
|
+
clearSelfLeft: () => clearSelfLeft,
|
|
43
|
+
consumeSelfLeft: () => consumeSelfLeft,
|
|
40
44
|
createBlacklistedGuild: () => createBlacklistedGuild,
|
|
41
45
|
getAllBlacklistedGuilds: () => getAllBlacklistedGuilds,
|
|
42
46
|
getAllPendingFriendRequests: () => getAllPendingFriendRequests,
|
|
@@ -48,6 +52,7 @@ __export(database_exports, {
|
|
|
48
52
|
getPendingFriendRequest: () => getPendingFriendRequest,
|
|
49
53
|
getPendingInvite: () => getPendingInvite,
|
|
50
54
|
isInSmallGroupWhitelist: () => isInSmallGroupWhitelist,
|
|
55
|
+
markSelfLeft: () => markSelfLeft,
|
|
51
56
|
name: () => name,
|
|
52
57
|
removeBlacklistedGuild: () => removeBlacklistedGuild,
|
|
53
58
|
removeFromSmallGroupWhitelist: () => removeFromSmallGroupWhitelist,
|
|
@@ -84,6 +89,11 @@ function apply(ctx) {
|
|
|
84
89
|
platform: "string",
|
|
85
90
|
guildId: "string"
|
|
86
91
|
}, { primary: ["platform", "guildId"] });
|
|
92
|
+
ctx.model.extend("self_left_guild", {
|
|
93
|
+
platform: "string",
|
|
94
|
+
guildId: "string",
|
|
95
|
+
timestamp: "integer"
|
|
96
|
+
}, { primary: ["platform", "guildId"] });
|
|
87
97
|
ctx.model.extend("pending_invite", {
|
|
88
98
|
platform: "string",
|
|
89
99
|
groupId: "string",
|
|
@@ -129,6 +139,44 @@ async function clearBlacklistedGuilds(ctx) {
|
|
|
129
139
|
return await ctx.model.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
|
|
130
140
|
}
|
|
131
141
|
__name(clearBlacklistedGuilds, "clearBlacklistedGuilds");
|
|
142
|
+
async function blacklistKicked(ctx, guildId) {
|
|
143
|
+
return await ctx.model.upsert("blacklisted_guild", [{
|
|
144
|
+
platform: BLACKLIST_PLATFORM,
|
|
145
|
+
guildId,
|
|
146
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
147
|
+
reason: "kicked"
|
|
148
|
+
}]);
|
|
149
|
+
}
|
|
150
|
+
__name(blacklistKicked, "blacklistKicked");
|
|
151
|
+
async function markSelfLeft(ctx, guildId) {
|
|
152
|
+
await ctx.model.upsert("self_left_guild", [{
|
|
153
|
+
platform: BLACKLIST_PLATFORM,
|
|
154
|
+
guildId,
|
|
155
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
156
|
+
}]);
|
|
157
|
+
}
|
|
158
|
+
__name(markSelfLeft, "markSelfLeft");
|
|
159
|
+
async function consumeSelfLeft(ctx, guildId, maxAgeSec = 120) {
|
|
160
|
+
const [row] = await ctx.model.get("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
161
|
+
if (!row) return false;
|
|
162
|
+
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
163
|
+
return Math.floor(Date.now() / 1e3) - row.timestamp <= maxAgeSec;
|
|
164
|
+
}
|
|
165
|
+
__name(consumeSelfLeft, "consumeSelfLeft");
|
|
166
|
+
async function clearSelfLeft(ctx, guildId) {
|
|
167
|
+
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
168
|
+
}
|
|
169
|
+
__name(clearSelfLeft, "clearSelfLeft");
|
|
170
|
+
async function clearExpiredSelfLeft(ctx, maxAgeSec = 300) {
|
|
171
|
+
const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSec;
|
|
172
|
+
const all = await ctx.model.get("self_left_guild", { platform: BLACKLIST_PLATFORM });
|
|
173
|
+
const expired = all.filter((r) => r.timestamp < cutoff);
|
|
174
|
+
for (const record of expired) {
|
|
175
|
+
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId: record.guildId });
|
|
176
|
+
}
|
|
177
|
+
return expired.length;
|
|
178
|
+
}
|
|
179
|
+
__name(clearExpiredSelfLeft, "clearExpiredSelfLeft");
|
|
132
180
|
async function getCommandFrequencyRecord(ctx, platform, guildId) {
|
|
133
181
|
const records = await ctx.model.get("command_frequency_record", { platform, guildId });
|
|
134
182
|
return records.length > 0 ? records[0] : null;
|
|
@@ -367,17 +415,24 @@ function apply2(ctx, config) {
|
|
|
367
415
|
if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
|
|
368
416
|
}
|
|
369
417
|
}, 30 * 1e3);
|
|
418
|
+
setInterval(async () => {
|
|
419
|
+
try {
|
|
420
|
+
await clearExpiredSelfLeft(ctx);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}, 5 * 60 * 1e3);
|
|
370
424
|
ctx.on("guild-added", async (session) => {
|
|
371
425
|
const { guildId, platform } = session;
|
|
372
426
|
ctx.logger("group-control-basic").info(`[guild-added] 触发!guildId=${guildId}, platform=${platform}`);
|
|
373
427
|
if (config.basic.enableBlacklist) {
|
|
374
|
-
const [blacklisted] = await ctx.model.get("blacklisted_guild", { platform, guildId });
|
|
428
|
+
const [blacklisted] = await ctx.model.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
375
429
|
if (blacklisted) {
|
|
376
430
|
try {
|
|
377
431
|
await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
|
|
378
432
|
} catch (e) {
|
|
379
433
|
}
|
|
380
|
-
quittingGuilds.set(`${
|
|
434
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
435
|
+
await markSelfLeft(ctx, guildId);
|
|
381
436
|
try {
|
|
382
437
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
383
438
|
} catch (e) {
|
|
@@ -437,12 +492,14 @@ function apply2(ctx, config) {
|
|
|
437
492
|
机器人已自动退出该群。`;
|
|
438
493
|
await notifyAdmins(session.bot, config, adminMsg);
|
|
439
494
|
}
|
|
440
|
-
quittingGuilds.set(`${
|
|
495
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
496
|
+
await markSelfLeft(ctx, guildId);
|
|
441
497
|
try {
|
|
442
498
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
443
499
|
} catch (e) {
|
|
444
500
|
console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
|
|
445
|
-
quittingGuilds.delete(`${
|
|
501
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
502
|
+
await clearSelfLeft(ctx, guildId);
|
|
446
503
|
}
|
|
447
504
|
} else if (memberCount > config.basic.smallGroupThreshold) {
|
|
448
505
|
if (config.basic.smallGroupQualifiedNotifyAdmin) {
|
|
@@ -464,25 +521,37 @@ function apply2(ctx, config) {
|
|
|
464
521
|
}
|
|
465
522
|
});
|
|
466
523
|
ctx.on("guild-removed", async (session) => {
|
|
467
|
-
const { guildId
|
|
468
|
-
const
|
|
469
|
-
|
|
524
|
+
const { guildId } = session;
|
|
525
|
+
const platform = BLACKLIST_PLATFORM;
|
|
526
|
+
const dedupKey = `${platform}:${guildId}`;
|
|
527
|
+
if (quittingGuilds.has(dedupKey)) {
|
|
528
|
+
try {
|
|
529
|
+
await clearSelfLeft(ctx, guildId);
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
470
532
|
return;
|
|
471
533
|
}
|
|
472
|
-
|
|
473
|
-
|
|
534
|
+
const isSelfLeft = await consumeSelfLeft(ctx, guildId);
|
|
535
|
+
if (isSelfLeft) return;
|
|
536
|
+
const raw = session.event?._data || session.original || session.onebot || {};
|
|
537
|
+
const subType = String(raw.sub_type ?? session.subtype ?? "");
|
|
538
|
+
const eventTs = typeof raw.time === "number" ? raw.time : Math.floor(Date.now() / 1e3);
|
|
539
|
+
const ageSec = Math.floor(Date.now() / 1e3) - eventTs;
|
|
540
|
+
if (subType === "leave") return;
|
|
541
|
+
if (!subType && ageSec > 60) return;
|
|
542
|
+
if (processedKicks.has(dedupKey)) return;
|
|
543
|
+
processedKicks.set(dedupKey, Date.now());
|
|
544
|
+
if (config.basic.enableBlacklist || config.basic.notifyAdminOnKick) {
|
|
545
|
+
const [existing] = await ctx.model.get("blacklisted_guild", { platform, guildId });
|
|
546
|
+
if (existing && existing.reason === "kicked") {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
474
549
|
}
|
|
475
|
-
processedKicks.set(quittingKey, Date.now());
|
|
476
|
-
const groupName = await getGroupName(session.bot, guildId);
|
|
477
550
|
if (config.basic.enableBlacklist) {
|
|
478
|
-
await ctx
|
|
479
|
-
platform,
|
|
480
|
-
guildId,
|
|
481
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
482
|
-
reason: "kicked"
|
|
483
|
-
}]);
|
|
551
|
+
await blacklistKicked(ctx, guildId);
|
|
484
552
|
}
|
|
485
553
|
if (config.basic.notifyAdminOnKick) {
|
|
554
|
+
const groupName = await getGroupName(session.bot, guildId);
|
|
486
555
|
const kickMsg = config.basic.kickNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName);
|
|
487
556
|
await notifyAdmins(session.bot, config, kickMsg);
|
|
488
557
|
}
|
|
@@ -516,7 +585,8 @@ function apply2(ctx, config) {
|
|
|
516
585
|
群名称:${groupName}
|
|
517
586
|
群号:${guildId}`;
|
|
518
587
|
await notifyAdmins(session.bot, config, adminMsg);
|
|
519
|
-
quittingGuilds.set(`${
|
|
588
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
589
|
+
await markSelfLeft(ctx, guildId);
|
|
520
590
|
try {
|
|
521
591
|
await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
|
|
522
592
|
} catch (e) {
|
|
@@ -524,7 +594,8 @@ function apply2(ctx, config) {
|
|
|
524
594
|
try {
|
|
525
595
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
526
596
|
} catch (e) {
|
|
527
|
-
quittingGuilds.delete(`${
|
|
597
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
598
|
+
await clearSelfLeft(ctx, guildId);
|
|
528
599
|
return `退出失败: ${e.message}`;
|
|
529
600
|
}
|
|
530
601
|
return "";
|
|
@@ -562,6 +633,33 @@ function apply3(ctx, config) {
|
|
|
562
633
|
const rawUserId = raw.user_id ? String(raw.user_id) : session.userId;
|
|
563
634
|
const rawGroupId = raw.group_id ? String(raw.group_id) : session.guildId;
|
|
564
635
|
const { platform } = session;
|
|
636
|
+
if (config.basic.enableBlacklist && rawGroupId && /^\d+$/.test(String(rawGroupId))) {
|
|
637
|
+
const bl = await getBlacklistedGuild(ctx, String(rawGroupId));
|
|
638
|
+
if (bl.length > 0) {
|
|
639
|
+
try {
|
|
640
|
+
await session.bot.internal.setGroupAddRequest(flag, "invite", false, "该群已被机器人拉黑");
|
|
641
|
+
} catch (e) {
|
|
642
|
+
ctx.logger("group-control-invite").warn("拒绝黑名单群邀请失败 (flag 可能已失效):", e);
|
|
643
|
+
}
|
|
644
|
+
const rejectNotify = `已自动拒绝黑名单群邀请
|
|
645
|
+
群号:${rawGroupId}
|
|
646
|
+
邀请者 QQ:${rawUserId}
|
|
647
|
+
如需放行请先执行 gc.unban ${rawGroupId} 再让对方重新邀请。`;
|
|
648
|
+
try {
|
|
649
|
+
await notifyAdmins(session.bot, config, rejectNotify);
|
|
650
|
+
} catch (e) {
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const rejectMsg = `您邀请加入的群 ${rawGroupId} 已被机器人拉黑,邀请已被自动拒绝。如有疑问请联系机器人管理员。`;
|
|
654
|
+
await session.bot.sendPrivateMessage(rawUserId, rejectMsg);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
}
|
|
657
|
+
if (config.invite.showDetailedLog) {
|
|
658
|
+
console.log(`已自动拒绝黑名单群邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
565
663
|
if (!flag && config.invite.showDetailedLog) {
|
|
566
664
|
console.warn("未能提取到邀请 flag,可能导致无法处理邀请。Raw event:", JSON.stringify(raw));
|
|
567
665
|
}
|
|
@@ -655,6 +753,12 @@ function apply3(ctx, config) {
|
|
|
655
753
|
ctx.command("gc.approve <groupId:string>", "同意群聊邀请").action(async ({ session }, groupId) => {
|
|
656
754
|
if (!groupId) return "请指定群号。用法:gc.approve <群号>";
|
|
657
755
|
if (!hasGlobalPermission(session, config)) return "权限不足,只有管理员可以审核邀请。";
|
|
756
|
+
if (config.basic.enableBlacklist) {
|
|
757
|
+
const bl = await getBlacklistedGuild(ctx, groupId);
|
|
758
|
+
if (bl.length > 0) {
|
|
759
|
+
return `群 ${groupId} 在黑名单中,无法通过审核。如需放行请先执行 gc.unban ${groupId}。`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
658
762
|
const inviteData = await getPendingInvite(ctx, groupId);
|
|
659
763
|
if (!inviteData) {
|
|
660
764
|
const allInvites = await getAllPendingInvites(ctx);
|
|
@@ -940,6 +1044,7 @@ function apply5(ctx, config) {
|
|
|
940
1044
|
const guildId = parseGuildId(input);
|
|
941
1045
|
if (!guildId) return `输入格式错误。`;
|
|
942
1046
|
const removed = await removeBlacklistedGuild(ctx, guildId);
|
|
1047
|
+
await clearSelfLeft(ctx, guildId);
|
|
943
1048
|
return removed ? `已移除群聊 ${guildId}` : `群聊 ${guildId} 不在黑名单中。`;
|
|
944
1049
|
});
|
|
945
1050
|
ctx.command("gc.banlist", "查看黑名单").action(async ({ session }) => {
|
package/package.json
CHANGED