koishi-plugin-group-verification 1.0.30 → 1.0.32
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 +12 -0
- package/lib/index.js +285 -10
- package/package.json +2 -2
- package/readme.md +32 -0
- package/src/index.ts +335 -25
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ declare module 'koishi' {
|
|
|
5
5
|
group_verification_config: GroupVerificationConfig;
|
|
6
6
|
group_verification_stats: GroupVerificationStats;
|
|
7
7
|
group_verification_pending: PendingVerification;
|
|
8
|
+
group_verification_blacklist: GroupBlacklistEntry;
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
export interface GroupVerificationConfig {
|
|
@@ -29,6 +30,11 @@ export interface GroupVerificationStats {
|
|
|
29
30
|
totalJoined: number;
|
|
30
31
|
lastUpdated: string | Date;
|
|
31
32
|
}
|
|
33
|
+
export interface GroupBlacklistEntry {
|
|
34
|
+
id: number;
|
|
35
|
+
groupId: string;
|
|
36
|
+
entries: Record<string, string>;
|
|
37
|
+
}
|
|
32
38
|
export interface PendingVerification {
|
|
33
39
|
id: number;
|
|
34
40
|
groupId: string;
|
|
@@ -46,6 +52,10 @@ export interface Config {
|
|
|
46
52
|
invalidGroupMessage?: string;
|
|
47
53
|
parameterConflictMessage?: string;
|
|
48
54
|
noKeywordsMessage?: string;
|
|
55
|
+
blacklistAddSuccess?: string;
|
|
56
|
+
blacklistRemoveSuccess?: string;
|
|
57
|
+
blacklistListEmpty?: string;
|
|
58
|
+
blacklistInfoTemplate?: string;
|
|
49
59
|
}
|
|
50
60
|
export declare const Config: Schema<Config>;
|
|
51
61
|
export declare const inject: string[];
|
|
@@ -114,4 +124,6 @@ export declare function verifyApplication(config: GroupVerificationConfig, messa
|
|
|
114
124
|
requiredThreshold: string;
|
|
115
125
|
}>;
|
|
116
126
|
export declare function handleFailedVerification(ctx: Context, session: any, config: GroupVerificationConfig, matchedCount?: number, requiredThreshold?: string): Promise<void>;
|
|
127
|
+
export declare function isUserBlacklisted(ctx: Context, groupId: string, userId: string): Promise<boolean>;
|
|
128
|
+
export declare function processBlacklistCommand(ctx: Context, session: any, rawArgs: string, config?: Config): Promise<string>;
|
|
117
129
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -28,9 +28,11 @@ __export(src_exports, {
|
|
|
28
28
|
handleGuildMemberRequestEvent: () => handleGuildMemberRequestEvent,
|
|
29
29
|
incrementTotal: () => incrementTotal,
|
|
30
30
|
inject: () => inject,
|
|
31
|
+
isUserBlacklisted: () => isUserBlacklisted,
|
|
31
32
|
mergeReminder: () => mergeReminder,
|
|
32
33
|
name: () => name,
|
|
33
34
|
parseConfigArgs: () => parseConfigArgs,
|
|
35
|
+
processBlacklistCommand: () => processBlacklistCommand,
|
|
34
36
|
resolveThreshold: () => resolveThreshold,
|
|
35
37
|
syncTotalStats: () => syncTotalStats,
|
|
36
38
|
tokenize: () => tokenize,
|
|
@@ -49,7 +51,11 @@ var Config = import_koishi.Schema.object({
|
|
|
49
51
|
permissionDeniedMessage: import_koishi.Schema.string().description("权限不足时返回给调用者的提示").default("权限不足:需要群主/管理员权限或koishi三级以上权限"),
|
|
50
52
|
invalidGroupMessage: import_koishi.Schema.string().description("无效群号或机器人未在该群时的提示").default("群号 {group} 格式不合法或机器人不在该群中"),
|
|
51
53
|
parameterConflictMessage: import_koishi.Schema.string().description("参数冲突时提示").default("参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)"),
|
|
52
|
-
noKeywordsMessage: import_koishi.Schema.string().description("未提供关键词且无法从现有配置继承时的提示").default("请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置")
|
|
54
|
+
noKeywordsMessage: import_koishi.Schema.string().description("未提供关键词且无法从现有配置继承时的提示").default("请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置"),
|
|
55
|
+
blacklistAddSuccess: import_koishi.Schema.string().description("将用户加入黑名单后的提示,可使用 {user},{group},{reason}").default("已将用户 {user} 加入群 {group} 黑名单{reason}"),
|
|
56
|
+
blacklistRemoveSuccess: import_koishi.Schema.string().description("从黑名单移除用户后的提示,可使用 {user},{group}").default("已从群 {group} 的黑名单中移除用户 {user}"),
|
|
57
|
+
blacklistListEmpty: import_koishi.Schema.string().description("黑名单为空时提示").default("群 {group} 的黑名单为空"),
|
|
58
|
+
blacklistInfoTemplate: import_koishi.Schema.string().description("查询指定用户状态时的模板,可用 {global},{group}").default("全局黑名单: {global}\n本群黑名单: {group}")
|
|
53
59
|
}).description("群组验证插件配置");
|
|
54
60
|
var inject = ["database"];
|
|
55
61
|
var ESC_QUOTE = "\0";
|
|
@@ -342,7 +348,7 @@ async function checkPermission(session, targetGroupId) {
|
|
|
342
348
|
});
|
|
343
349
|
}
|
|
344
350
|
if (koishiAuthority && koishiAuthority >= 3) {
|
|
345
|
-
logger.
|
|
351
|
+
logger.debug(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`);
|
|
346
352
|
return [true];
|
|
347
353
|
}
|
|
348
354
|
try {
|
|
@@ -392,6 +398,23 @@ async function handleGuildMemberRequestEvent(ctx, session) {
|
|
|
392
398
|
await updateStats(ctx, guildId, "rejected");
|
|
393
399
|
return;
|
|
394
400
|
}
|
|
401
|
+
try {
|
|
402
|
+
const blacklisted = await isUserBlacklisted(ctx, guildId, userId);
|
|
403
|
+
if (blacklisted) {
|
|
404
|
+
logger.info(`用户 ${userId} 在群 ${guildId} 或全局黑名单中,自动拒绝申请`);
|
|
405
|
+
if (requestId) {
|
|
406
|
+
try {
|
|
407
|
+
await session.bot.handleGuildMemberRequest(requestId, false);
|
|
408
|
+
} catch (e) {
|
|
409
|
+
logger.warn("自动拒绝失败", e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
await updateStats(ctx, guildId, "rejected");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
logger.warn("黑名单检查失败", e);
|
|
417
|
+
}
|
|
395
418
|
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
|
|
396
419
|
logger.debug(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
397
420
|
if (isValid) {
|
|
@@ -603,6 +626,218 @@ async function handleFailedVerification(ctx, session, config, matchedCount, requ
|
|
|
603
626
|
}
|
|
604
627
|
}
|
|
605
628
|
__name(handleFailedVerification, "handleFailedVerification");
|
|
629
|
+
async function isUserBlacklisted(ctx, groupId, userId) {
|
|
630
|
+
const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
|
|
631
|
+
if (globalRows.length > 0) {
|
|
632
|
+
const entries = globalRows[0].entries || {};
|
|
633
|
+
if (entries[userId] !== void 0) return true;
|
|
634
|
+
}
|
|
635
|
+
const groupRows = await ctx.database.get("group_verification_blacklist", { groupId });
|
|
636
|
+
if (groupRows.length > 0) {
|
|
637
|
+
const entries = groupRows[0].entries || {};
|
|
638
|
+
if (entries[userId] !== void 0) return true;
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
__name(isUserBlacklisted, "isUserBlacklisted");
|
|
643
|
+
async function processBlacklistCommand(ctx, session, rawArgs, config) {
|
|
644
|
+
const parts = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
645
|
+
const op = parts[0]?.toLowerCase();
|
|
646
|
+
if (!op || !["a", "r", "l", "i"].includes(op)) {
|
|
647
|
+
return [
|
|
648
|
+
"用法:",
|
|
649
|
+
" gvb a id [reason] [group] 将用户加入黑名单",
|
|
650
|
+
" gvb r id [group] 将用户移出黑名单",
|
|
651
|
+
" gvb l [group] 查询群黑名单",
|
|
652
|
+
" gvb i id [group] 查询账号黑名单",
|
|
653
|
+
" [group]传入all表全局"
|
|
654
|
+
].join("\n");
|
|
655
|
+
}
|
|
656
|
+
const getCurrentGroup = /* @__PURE__ */ __name(() => session.guildId || "", "getCurrentGroup");
|
|
657
|
+
let group;
|
|
658
|
+
let targetUser;
|
|
659
|
+
let reason = "";
|
|
660
|
+
if (op === "a") {
|
|
661
|
+
targetUser = parts[1];
|
|
662
|
+
if (!targetUser) return "请提供用户ID";
|
|
663
|
+
const rest = parts.slice(2);
|
|
664
|
+
if (rest.length === 1) {
|
|
665
|
+
reason = rest[0];
|
|
666
|
+
} else if (rest.length > 1) {
|
|
667
|
+
const last = rest[rest.length - 1];
|
|
668
|
+
if (/^\d+$/.test(last) || last.toLowerCase() === "all") {
|
|
669
|
+
group = last;
|
|
670
|
+
reason = rest.slice(0, -1).join(" ");
|
|
671
|
+
} else {
|
|
672
|
+
reason = rest.join(" ");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
group = group || getCurrentGroup();
|
|
676
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
677
|
+
if (group.toLowerCase() === "all") {
|
|
678
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
679
|
+
if (!(auth && auth >= 3)) return "设置全局黑名单需要 koishi 3 级以上权限";
|
|
680
|
+
} else {
|
|
681
|
+
if (config?.enableStrictGroupCheck) {
|
|
682
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
683
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const [ok, err] = await checkPermission(session, group);
|
|
687
|
+
if (!ok) return err || "权限不足";
|
|
688
|
+
}
|
|
689
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
690
|
+
const timePrefix = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
691
|
+
const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix;
|
|
692
|
+
if (rows.length > 0) {
|
|
693
|
+
const row = rows[0];
|
|
694
|
+
const entries = row.entries || {};
|
|
695
|
+
if (entries[targetUser] !== void 0) {
|
|
696
|
+
return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`;
|
|
697
|
+
}
|
|
698
|
+
entries[targetUser] = storedReason;
|
|
699
|
+
await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
|
|
700
|
+
} else {
|
|
701
|
+
const entries = {};
|
|
702
|
+
entries[targetUser] = storedReason;
|
|
703
|
+
await ctx.database.create("group_verification_blacklist", { groupId: group, entries });
|
|
704
|
+
}
|
|
705
|
+
if (session.bot && typeof session.bot.kickGuildMember === "function") {
|
|
706
|
+
try {
|
|
707
|
+
await session.bot.kickGuildMember(group, targetUser);
|
|
708
|
+
logger.info(`已将黑名单用户 ${targetUser} 从群 ${group} 踢出`);
|
|
709
|
+
} catch (e) {
|
|
710
|
+
logger.warn(`踢出用户 ${targetUser} 失败`, e);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const tmpl = config && config.blacklistAddSuccess || "已将用户 {user} 加入群 {group} 黑名单{reason}";
|
|
714
|
+
return tmpl.replace("{user}", targetUser).replace("{group}", group).replace("{reason}", reason ? `,原因:${reason}` : "");
|
|
715
|
+
}
|
|
716
|
+
if (op === "r") {
|
|
717
|
+
targetUser = parts[1];
|
|
718
|
+
if (!targetUser) return "请提供用户ID";
|
|
719
|
+
group = parts[2] || getCurrentGroup();
|
|
720
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
721
|
+
if (group.toLowerCase() === "all") {
|
|
722
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
723
|
+
if (!(auth && auth >= 3)) return "修改全局黑名单需要 koishi 3 级以上权限";
|
|
724
|
+
} else {
|
|
725
|
+
if (config?.enableStrictGroupCheck) {
|
|
726
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
727
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const [ok, err] = await checkPermission(session, group);
|
|
731
|
+
if (!ok) return err || "权限不足";
|
|
732
|
+
}
|
|
733
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
734
|
+
if (rows.length > 0) {
|
|
735
|
+
const row = rows[0];
|
|
736
|
+
const entries = row.entries || {};
|
|
737
|
+
delete entries[targetUser];
|
|
738
|
+
await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
|
|
739
|
+
}
|
|
740
|
+
const tmpl = config && config.blacklistRemoveSuccess || "已从群 {group} 的黑名单中移除用户 {user}";
|
|
741
|
+
return tmpl.replace("{user}", targetUser).replace("{group}", group);
|
|
742
|
+
}
|
|
743
|
+
if (op === "l") {
|
|
744
|
+
group = parts[1] || getCurrentGroup();
|
|
745
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
746
|
+
if (group.toLowerCase() === "all") {
|
|
747
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
748
|
+
if (!(auth && auth >= 3)) return "查看全局黑名单需要 koishi 3 级以上权限";
|
|
749
|
+
} else {
|
|
750
|
+
if (config?.enableStrictGroupCheck) {
|
|
751
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
752
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const [ok, err] = await checkPermission(session, group);
|
|
756
|
+
if (!ok) return err || "权限不足";
|
|
757
|
+
}
|
|
758
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
759
|
+
if (rows.length === 0) {
|
|
760
|
+
if (group && group.toLowerCase() === "all") {
|
|
761
|
+
return "全局黑名单为空";
|
|
762
|
+
}
|
|
763
|
+
const tmpl = config && config.blacklistListEmpty || "群 {group} 的黑名单为空";
|
|
764
|
+
return tmpl.replace("{group}", group);
|
|
765
|
+
}
|
|
766
|
+
const entries = rows[0].entries || {};
|
|
767
|
+
let prefix = group && group.toLowerCase() === "all" ? "全局黑名单: \n" : `群 ${group} 黑名单:
|
|
768
|
+
`;
|
|
769
|
+
let msg = prefix;
|
|
770
|
+
for (const uid in entries) {
|
|
771
|
+
msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ""}
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
return msg;
|
|
775
|
+
}
|
|
776
|
+
if (op === "i") {
|
|
777
|
+
targetUser = parts[1];
|
|
778
|
+
if (!targetUser) return "请提供用户ID";
|
|
779
|
+
const groupArg = parts[2];
|
|
780
|
+
const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
|
|
781
|
+
const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== void 0;
|
|
782
|
+
const tmpl = config && config.blacklistInfoTemplate || "全局黑名单: {global}\n本群黑名单: {group}";
|
|
783
|
+
const formatReply = /* @__PURE__ */ __name((localHit, groupsList) => {
|
|
784
|
+
if (groupsList) {
|
|
785
|
+
return tmpl.replace("{global}", globalHit ? "有" : "无").replace(
|
|
786
|
+
"{group}",
|
|
787
|
+
groupsList.length ? groupsList.join(",") : "无"
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return tmpl.replace("{global}", globalHit ? "有" : "无").replace("{group}", localHit ? "有" : "无");
|
|
791
|
+
}, "formatReply");
|
|
792
|
+
if (!groupArg) {
|
|
793
|
+
const groupId2 = getCurrentGroup();
|
|
794
|
+
if (!groupId2) return "请在群聊中使用此命令";
|
|
795
|
+
const [ok2, err2] = await checkPermission(session, groupId2);
|
|
796
|
+
if (!ok2) return err2 || "权限不足";
|
|
797
|
+
const rows2 = await ctx.database.get("group_verification_blacklist", { groupId: groupId2 });
|
|
798
|
+
const localReason2 = rows2.length > 0 ? (rows2[0].entries || {})[targetUser] : void 0;
|
|
799
|
+
const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : void 0;
|
|
800
|
+
return `全局黑名单: ${globalReason2 || "无"}
|
|
801
|
+
群${groupId2}黑名单: ${localReason2 || "无"}`;
|
|
802
|
+
}
|
|
803
|
+
if (groupArg.toLowerCase() === "all") {
|
|
804
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
805
|
+
if (!(auth && auth >= 3)) return "权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限";
|
|
806
|
+
const rows2 = await ctx.database.get("group_verification_blacklist", {});
|
|
807
|
+
const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : "无";
|
|
808
|
+
const lines = [];
|
|
809
|
+
for (const r of rows2) {
|
|
810
|
+
if (r.groupId === "all") continue;
|
|
811
|
+
const reason2 = (r.entries || {})[targetUser];
|
|
812
|
+
if (reason2 !== void 0) {
|
|
813
|
+
lines.push(`群${r.groupId}:${reason2}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
let reply = `全局黑名单: ${globalReason2}`;
|
|
817
|
+
if (lines.length) {
|
|
818
|
+
reply += "\n群黑名单: \n" + lines.join("\n");
|
|
819
|
+
} else {
|
|
820
|
+
reply += "\n群黑名单: 无";
|
|
821
|
+
}
|
|
822
|
+
return reply;
|
|
823
|
+
}
|
|
824
|
+
const groupId = groupArg;
|
|
825
|
+
if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== "all") {
|
|
826
|
+
if (!/^\d{5,15}$/.test(groupId)) {
|
|
827
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", groupId);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const [ok, err] = await checkPermission(session, groupId);
|
|
831
|
+
if (!ok) return err || "权限不足";
|
|
832
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId });
|
|
833
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : void 0;
|
|
834
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : void 0;
|
|
835
|
+
return `全局黑名单: ${globalReason || "无"}
|
|
836
|
+
群${groupId}黑名单: ${localReason || "无"}`;
|
|
837
|
+
}
|
|
838
|
+
return "";
|
|
839
|
+
}
|
|
840
|
+
__name(processBlacklistCommand, "processBlacklistCommand");
|
|
606
841
|
function apply(ctx, config) {
|
|
607
842
|
ctx.model.extend("group_verification_config", {
|
|
608
843
|
id: "unsigned",
|
|
@@ -655,13 +890,25 @@ function apply(ctx, config) {
|
|
|
655
890
|
primary: "id",
|
|
656
891
|
autoInc: true
|
|
657
892
|
});
|
|
893
|
+
ctx.model.extend("group_verification_blacklist", {
|
|
894
|
+
id: "unsigned",
|
|
895
|
+
groupId: "string",
|
|
896
|
+
entries: "json"
|
|
897
|
+
}, {
|
|
898
|
+
primary: "id",
|
|
899
|
+
autoInc: true,
|
|
900
|
+
indexes: [["groupId"]]
|
|
901
|
+
});
|
|
658
902
|
ctx.on("guild-member-request", async (session) => {
|
|
659
903
|
logger.info("收到 guild-member-request 事件,转发给处理函数");
|
|
660
904
|
await handleGuildMemberRequestEvent(ctx, session);
|
|
661
905
|
});
|
|
662
906
|
ctx.on("guild-member-added", async (session) => {
|
|
663
|
-
const groupId = session.guildId;
|
|
664
|
-
const userId = session.userId;
|
|
907
|
+
const groupId = session.guildId || "";
|
|
908
|
+
const userId = session.userId || "";
|
|
909
|
+
if (!groupId || !userId) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
665
912
|
await incrementTotal(ctx, groupId);
|
|
666
913
|
const set = autoQueue.get(groupId);
|
|
667
914
|
if (set && set.has(userId)) {
|
|
@@ -1083,10 +1330,11 @@ ${reminderMessage}
|
|
|
1083
1330
|
return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1084
1331
|
}
|
|
1085
1332
|
try {
|
|
1086
|
-
await session.bot.handleGuildMemberRequest(request2.requestId, true);
|
|
1087
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
|
|
1088
1333
|
const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
|
|
1089
|
-
|
|
1334
|
+
const reply = `已同意用户 ${displayName} 的加群申请`;
|
|
1335
|
+
session.bot.handleGuildMemberRequest(request2.requestId, true).catch((e) => logger.warn("自动同意失败", e));
|
|
1336
|
+
await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
|
|
1337
|
+
return reply;
|
|
1090
1338
|
} catch (error) {
|
|
1091
1339
|
return `处理申请时出错: ${error.message}`;
|
|
1092
1340
|
}
|
|
@@ -1104,10 +1352,11 @@ ${reminderMessage}
|
|
|
1104
1352
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
|
|
1105
1353
|
}
|
|
1106
1354
|
try {
|
|
1107
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true);
|
|
1108
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId });
|
|
1109
1355
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
|
|
1110
|
-
|
|
1356
|
+
const reply = `已同意用户 ${displayName} 的加群申请`;
|
|
1357
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e) => logger.warn("自动同意失败", e));
|
|
1358
|
+
await ctx.database.remove("group_verification_pending", { groupId, userId });
|
|
1359
|
+
return reply;
|
|
1111
1360
|
} catch (error) {
|
|
1112
1361
|
return `处理申请时出错: ${error.message}`;
|
|
1113
1362
|
}
|
|
@@ -1257,6 +1506,17 @@ ${reminderMessage}
|
|
|
1257
1506
|
});
|
|
1258
1507
|
return result;
|
|
1259
1508
|
});
|
|
1509
|
+
groupVerify.subcommand(".blacklist [args:text]", "管理加群黑名单").alias(
|
|
1510
|
+
"gvb",
|
|
1511
|
+
"gv.blacklist",
|
|
1512
|
+
"gverify.blacklist",
|
|
1513
|
+
"group-verify.blacklist",
|
|
1514
|
+
"gv.黑名单",
|
|
1515
|
+
"gverify.黑名单",
|
|
1516
|
+
"group-verify.黑名单"
|
|
1517
|
+
).action(async ({ session }, args) => {
|
|
1518
|
+
return await processBlacklistCommand(ctx, session, args || "", config);
|
|
1519
|
+
});
|
|
1260
1520
|
groupVerify.subcommand(".help", "显示帮助信息").alias("gv.帮助", "gverify.帮助", "group-verify.帮助", "帮助", "hlp", "帮助信息").action(() => {
|
|
1261
1521
|
return `群组验证命令帮助:
|
|
1262
1522
|
主指令别名:gv, gverify
|
|
@@ -1309,6 +1569,19 @@ ${reminderMessage}
|
|
|
1309
1569
|
gv.cfg -m 2 -t 80
|
|
1310
1570
|
gv.cfg -nomsg
|
|
1311
1571
|
|
|
1572
|
+
黑名单命令 (.blacklist / gvb):
|
|
1573
|
+
a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
|
|
1574
|
+
r id [group] 删除条目;group 为 all 时移除全局黑名单
|
|
1575
|
+
l [group] 查询群黑名单;传入 all 时查看全局黑名单
|
|
1576
|
+
i id [group] 查询账号黑名单;不带参数时查询本群与全局,
|
|
1577
|
+
指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
|
|
1578
|
+
使用示例:
|
|
1579
|
+
gvb a 12345 作弊记录
|
|
1580
|
+
gvb r 12345 67890
|
|
1581
|
+
gvb l
|
|
1582
|
+
gvb l all
|
|
1583
|
+
gvb i 12345
|
|
1584
|
+
|
|
1312
1585
|
快捷命令:
|
|
1313
1586
|
gvc - 配置命令快捷方式
|
|
1314
1587
|
gva - 同意申请快捷命令
|
|
@@ -1409,9 +1682,11 @@ __name(apply, "apply");
|
|
|
1409
1682
|
handleGuildMemberRequestEvent,
|
|
1410
1683
|
incrementTotal,
|
|
1411
1684
|
inject,
|
|
1685
|
+
isUserBlacklisted,
|
|
1412
1686
|
mergeReminder,
|
|
1413
1687
|
name,
|
|
1414
1688
|
parseConfigArgs,
|
|
1689
|
+
processBlacklistCommand,
|
|
1415
1690
|
resolveThreshold,
|
|
1416
1691
|
syncTotalStats,
|
|
1417
1692
|
tokenize,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-group-verification",
|
|
3
|
-
"description": "Koishi
|
|
3
|
+
"description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/LHDyx/koishi-plugin-group-verification.git"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"bugs": {
|
|
9
9
|
"url": "https://github.com/LHDyx/koishi-plugin-group-verification/issues"
|
|
10
10
|
},
|
|
11
|
-
"version": "1.0.
|
|
11
|
+
"version": "1.0.32",
|
|
12
12
|
"main": "lib/index.js",
|
|
13
13
|
"typings": "lib/index.d.ts",
|
|
14
14
|
"files": [
|
package/readme.md
CHANGED
|
@@ -62,6 +62,7 @@ group-verify.config -i 123456789 关键词1,关键词2 -m 1 -t 1
|
|
|
62
62
|
|
|
63
63
|
> **命令增强**
|
|
64
64
|
> - 未提供参数时 `gva`/`gvr` 会处理最近一条申请,`all` 可以批量处理。
|
|
65
|
+
> - 手动使用 `gva` 时会先在群内返回“已同意用户…的加群申请”等提示,随后再将用户加入群,避免用户看到提示时已在群中的尴尬情形。
|
|
65
66
|
> - 如果申请记录缺少 requestId,则无法通过机器人接口处理,会提示管理员请在客户端手动操作。
|
|
66
67
|
> - 输出结果会智能展示用户名和ID,避免出现 "12345(12345)" 这样的重复显示。
|
|
67
68
|
|
|
@@ -101,6 +102,36 @@ group-verify.stats 123456789
|
|
|
101
102
|
group-verify.stats total
|
|
102
103
|
```
|
|
103
104
|
|
|
105
|
+
### 黑名单命令
|
|
106
|
+
```
|
|
107
|
+
# 添加黑名单条目(可指定原因和群号)
|
|
108
|
+
group-verify.blacklist a <用户ID> [原因] [群号]
|
|
109
|
+
# 别名:gvb, gv.blacklist, gverify.blacklist, group-verify.blacklist
|
|
110
|
+
# gv.黑名单, gverify.黑名单, group-verify.黑名单
|
|
111
|
+
|
|
112
|
+
# 删除黑名单条目
|
|
113
|
+
group-verify.blacklist r <用户ID> [群号]
|
|
114
|
+
|
|
115
|
+
# 查看群黑名单
|
|
116
|
+
group-verify.blacklist l [群号]
|
|
117
|
+
# 传入 all 可查看全局黑名单
|
|
118
|
+
|
|
119
|
+
# 查询用户在黑名单的状态
|
|
120
|
+
group-verify.blacklist i <用户ID> [群号|all]
|
|
121
|
+
|
|
122
|
+
* 不指定群号时会查询当前群和全局黑名单状态(必须在群聊中使用)。
|
|
123
|
+
* 指定单个群号时会查询该群和全局状态,执行此操作需要该群的管理员权限或 Koishi 三级以上权限。
|
|
124
|
+
* 输入 `all` 会列出用户在所有群的黑名单条目与全局黑名单(仅限 Koishi `authority>=3`)。
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 黑名单消息模板
|
|
128
|
+
插件会在配置界面显示当前黑名单存在状态的提示词,用户可以通过修改以下字段自定义这些提示:
|
|
129
|
+
|
|
130
|
+
- `blacklistLocalExists`:当前群已有黑名单条目时的提示,例如 “本群已设置黑名单”。
|
|
131
|
+
- `blacklistGlobalExists`:全局黑名单存在时的提示,例如 “已启用全局黑名单”。
|
|
132
|
+
|
|
133
|
+
如果将这两个字段留空,则对应的提示不会显示(界面将保持简洁)。
|
|
134
|
+
|
|
104
135
|
## ⚙️ 参数说明
|
|
105
136
|
|
|
106
137
|
### 审核方式 (-m)
|
|
@@ -186,6 +217,7 @@ group-verify.stats total
|
|
|
186
217
|
- `group_verification_config` - 群组配置表
|
|
187
218
|
- `group_verification_stats` - 统计信息表
|
|
188
219
|
- `group_verification_pending` - 待审核申请表
|
|
220
|
+
- `group_verification_blacklist` - 群组黑名单条目,每行记录一个用户(groupId=all 表示全局)
|
|
189
221
|
|
|
190
222
|
## 📝 使用示例
|
|
191
223
|
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ declare module 'koishi' {
|
|
|
11
11
|
group_verification_config: GroupVerificationConfig
|
|
12
12
|
group_verification_stats: GroupVerificationStats
|
|
13
13
|
group_verification_pending: PendingVerification
|
|
14
|
+
group_verification_blacklist: GroupBlacklistEntry
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -41,6 +42,14 @@ export interface GroupVerificationStats {
|
|
|
41
42
|
lastUpdated: string | Date
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// 黑名单行:每个群/全局对应一条记录,entries 保存 userId->reason 对象
|
|
46
|
+
export interface GroupBlacklistEntry {
|
|
47
|
+
id: number
|
|
48
|
+
groupId: string // 群号或 "all" 表示全局
|
|
49
|
+
entries: Record<string, string> // key=userId, value=reason
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
44
53
|
// 待审核申请表
|
|
45
54
|
export interface PendingVerification {
|
|
46
55
|
id: number
|
|
@@ -62,6 +71,11 @@ export interface Config {
|
|
|
62
71
|
invalidGroupMessage?: string
|
|
63
72
|
parameterConflictMessage?: string
|
|
64
73
|
noKeywordsMessage?: string
|
|
74
|
+
// 黑名单提示消息
|
|
75
|
+
blacklistAddSuccess?: string
|
|
76
|
+
blacklistRemoveSuccess?: string
|
|
77
|
+
blacklistListEmpty?: string
|
|
78
|
+
blacklistInfoTemplate?: string
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
export const Config: Schema<Config> = Schema.object({
|
|
@@ -74,6 +88,10 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
74
88
|
invalidGroupMessage: Schema.string().description('无效群号或机器人未在该群时的提示').default('群号 {group} 格式不合法或机器人不在该群中'),
|
|
75
89
|
parameterConflictMessage: Schema.string().description('参数冲突时提示').default('参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)'),
|
|
76
90
|
noKeywordsMessage: Schema.string().description('未提供关键词且无法从现有配置继承时的提示').default('请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置'),
|
|
91
|
+
blacklistAddSuccess: Schema.string().description('将用户加入黑名单后的提示,可使用 {user},{group},{reason}').default('已将用户 {user} 加入群 {group} 黑名单{reason}'),
|
|
92
|
+
blacklistRemoveSuccess: Schema.string().description('从黑名单移除用户后的提示,可使用 {user},{group}').default('已从群 {group} 的黑名单中移除用户 {user}'),
|
|
93
|
+
blacklistListEmpty: Schema.string().description('黑名单为空时提示').default('群 {group} 的黑名单为空'),
|
|
94
|
+
blacklistInfoTemplate: Schema.string().description('查询指定用户状态时的模板,可用 {global},{group}').default('全局黑名单: {global}\n本群黑名单: {group}')
|
|
77
95
|
})
|
|
78
96
|
.description('群组验证插件配置')
|
|
79
97
|
|
|
@@ -480,7 +498,7 @@ export async function checkPermission(session: any, targetGroupId?: string): Pro
|
|
|
480
498
|
}
|
|
481
499
|
|
|
482
500
|
if (koishiAuthority && koishiAuthority >= 3) {
|
|
483
|
-
logger.
|
|
501
|
+
logger.debug(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`)
|
|
484
502
|
return [true]
|
|
485
503
|
}
|
|
486
504
|
|
|
@@ -533,6 +551,21 @@ export async function handleGuildMemberRequestEvent(ctx: Context, session: any)
|
|
|
533
551
|
return;
|
|
534
552
|
}
|
|
535
553
|
|
|
554
|
+
// 黑名单优先检查(仅在非全拒模式下)
|
|
555
|
+
try {
|
|
556
|
+
const blacklisted = await isUserBlacklisted(ctx, guildId, userId);
|
|
557
|
+
if (blacklisted) {
|
|
558
|
+
logger.info(`用户 ${userId} 在群 ${guildId} 或全局黑名单中,自动拒绝申请`);
|
|
559
|
+
if (requestId) {
|
|
560
|
+
try { await session.bot.handleGuildMemberRequest(requestId, false); } catch (e) { logger.warn('自动拒绝失败', e); }
|
|
561
|
+
}
|
|
562
|
+
await updateStats(ctx, guildId, 'rejected');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {
|
|
566
|
+
logger.warn('黑名单检查失败', e);
|
|
567
|
+
}
|
|
568
|
+
|
|
536
569
|
const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
|
|
537
570
|
logger.debug(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
|
|
538
571
|
|
|
@@ -744,7 +777,7 @@ export async function handleFailedVerification(
|
|
|
744
777
|
try {
|
|
745
778
|
const guild = await session.bot.getGuild(guildId)
|
|
746
779
|
groupName = guild.name || groupName
|
|
747
|
-
} catch (error) {
|
|
780
|
+
} catch (error: any) {
|
|
748
781
|
// 无法获取群名称时使用默认值
|
|
749
782
|
}
|
|
750
783
|
|
|
@@ -809,6 +842,241 @@ export async function handleFailedVerification(
|
|
|
809
842
|
}
|
|
810
843
|
}
|
|
811
844
|
|
|
845
|
+
// 黑名单相关辅助函数 ----------------------------------------------------------
|
|
846
|
+
|
|
847
|
+
// 检查指定用户是否在黑名单(群级或全局)中
|
|
848
|
+
export async function isUserBlacklisted(ctx: Context, groupId: string, userId: string): Promise<boolean> {
|
|
849
|
+
// 全局黑名单
|
|
850
|
+
const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
|
|
851
|
+
if (globalRows.length > 0) {
|
|
852
|
+
const entries = globalRows[0].entries || {}
|
|
853
|
+
if (entries[userId] !== undefined) return true
|
|
854
|
+
}
|
|
855
|
+
// 群级黑名单
|
|
856
|
+
const groupRows = await ctx.database.get('group_verification_blacklist', { groupId })
|
|
857
|
+
if (groupRows.length > 0) {
|
|
858
|
+
const entries = groupRows[0].entries || {}
|
|
859
|
+
if (entries[userId] !== undefined) return true
|
|
860
|
+
}
|
|
861
|
+
return false
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 解析并执行黑名单管理命令,返回要回复的字符串
|
|
865
|
+
export async function processBlacklistCommand(ctx: Context, session: any, rawArgs: string, config?: Config): Promise<string> {
|
|
866
|
+
const parts = rawArgs.trim().split(/\s+/).filter(Boolean)
|
|
867
|
+
const op = parts[0]?.toLowerCase()
|
|
868
|
+
if (!op || !['a','r','l','i'].includes(op)) {
|
|
869
|
+
// present a multiline Chinese usage guide without angle brackets
|
|
870
|
+
return [
|
|
871
|
+
'用法:',
|
|
872
|
+
' gvb a id [reason] [group] 将用户加入黑名单',
|
|
873
|
+
' gvb r id [group] 将用户移出黑名单',
|
|
874
|
+
' gvb l [group] 查询群黑名单',
|
|
875
|
+
' gvb i id [group] 查询账号黑名单',
|
|
876
|
+
' [group]传入all表全局'
|
|
877
|
+
].join('\n')
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const getCurrentGroup = () => session.guildId || ''
|
|
881
|
+
let group: string | undefined
|
|
882
|
+
let targetUser: string | undefined
|
|
883
|
+
let reason = ''
|
|
884
|
+
|
|
885
|
+
if (op === 'a') {
|
|
886
|
+
targetUser = parts[1]
|
|
887
|
+
if (!targetUser) return '请提供用户ID'
|
|
888
|
+
// handle optional reason and group at end
|
|
889
|
+
// 规则:如果只有一个附加参数,则作为 reason;两个及以上时最后一个为群号,其余拼成 reason
|
|
890
|
+
const rest = parts.slice(2)
|
|
891
|
+
if (rest.length === 1) {
|
|
892
|
+
reason = rest[0]
|
|
893
|
+
} else if (rest.length > 1) {
|
|
894
|
+
const last = rest[rest.length - 1]
|
|
895
|
+
if (/^\d+$/.test(last) || last.toLowerCase() === 'all') {
|
|
896
|
+
group = last
|
|
897
|
+
reason = rest.slice(0, -1).join(' ')
|
|
898
|
+
} else {
|
|
899
|
+
// 虽然数量>=2,但最后一个不是数字,全部作为reason
|
|
900
|
+
reason = rest.join(' ')
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
group = group || getCurrentGroup()
|
|
904
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
905
|
+
// 权限检查
|
|
906
|
+
if (group.toLowerCase() === 'all') {
|
|
907
|
+
const auth = session.author?.authority || session.user?.authority
|
|
908
|
+
if (!(auth && auth >= 3)) return '设置全局黑名单需要 koishi 3 级以上权限'
|
|
909
|
+
} else {
|
|
910
|
+
// 严格群号检查(若开启)
|
|
911
|
+
if (config?.enableStrictGroupCheck) {
|
|
912
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
913
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const [ok, err] = await checkPermission(session, group)
|
|
917
|
+
if (!ok) return err || '权限不足'
|
|
918
|
+
}
|
|
919
|
+
// add entry to map with timestamp, but refuse duplicates
|
|
920
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
921
|
+
// build stored reason with current time prefix
|
|
922
|
+
const timePrefix = new Date().toLocaleString()
|
|
923
|
+
const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix
|
|
924
|
+
if (rows.length > 0) {
|
|
925
|
+
const row = rows[0]
|
|
926
|
+
const entries = row.entries || {}
|
|
927
|
+
if (entries[targetUser] !== undefined) {
|
|
928
|
+
return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`
|
|
929
|
+
}
|
|
930
|
+
entries[targetUser] = storedReason
|
|
931
|
+
await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
|
|
932
|
+
} else {
|
|
933
|
+
const entries: Record<string,string> = {}
|
|
934
|
+
entries[targetUser] = storedReason
|
|
935
|
+
await ctx.database.create('group_verification_blacklist', { groupId: group, entries })
|
|
936
|
+
}
|
|
937
|
+
// 添加成功后尝试踢人
|
|
938
|
+
if (session.bot && typeof session.bot.kickGuildMember === 'function') {
|
|
939
|
+
try {
|
|
940
|
+
await session.bot.kickGuildMember(group, targetUser)
|
|
941
|
+
logger.info(`已将黑名单用户 ${targetUser} 从群 ${group} 踢出`)
|
|
942
|
+
} catch (e) {
|
|
943
|
+
logger.warn(`踢出用户 ${targetUser} 失败`, e)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const tmpl = (config && config.blacklistAddSuccess) || '已将用户 {user} 加入群 {group} 黑名单{reason}'
|
|
947
|
+
return tmpl.replace('{user}', targetUser).replace('{group}', group).replace('{reason}', reason ? `,原因:${reason}` : '')
|
|
948
|
+
}
|
|
949
|
+
if (op === 'r') {
|
|
950
|
+
targetUser = parts[1]
|
|
951
|
+
if (!targetUser) return '请提供用户ID'
|
|
952
|
+
group = parts[2] || getCurrentGroup()
|
|
953
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
954
|
+
if (group.toLowerCase() === 'all') {
|
|
955
|
+
const auth = session.author?.authority || session.user?.authority
|
|
956
|
+
if (!(auth && auth >= 3)) return '修改全局黑名单需要 koishi 3 级以上权限'
|
|
957
|
+
} else {
|
|
958
|
+
if (config?.enableStrictGroupCheck) {
|
|
959
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
960
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const [ok, err] = await checkPermission(session, group)
|
|
964
|
+
if (!ok) return err || '权限不足'
|
|
965
|
+
}
|
|
966
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
967
|
+
if (rows.length > 0) {
|
|
968
|
+
const row = rows[0]
|
|
969
|
+
const entries = row.entries || {}
|
|
970
|
+
delete entries[targetUser]
|
|
971
|
+
await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
|
|
972
|
+
}
|
|
973
|
+
const tmpl = (config && config.blacklistRemoveSuccess) || '已从群 {group} 的黑名单中移除用户 {user}'
|
|
974
|
+
return tmpl.replace('{user}', targetUser).replace('{group}', group)
|
|
975
|
+
}
|
|
976
|
+
if (op === 'l') {
|
|
977
|
+
group = parts[1] || getCurrentGroup()
|
|
978
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
979
|
+
if (group.toLowerCase() === 'all') {
|
|
980
|
+
const auth = session.author?.authority || session.user?.authority
|
|
981
|
+
if (!(auth && auth >= 3)) return '查看全局黑名单需要 koishi 3 级以上权限'
|
|
982
|
+
} else {
|
|
983
|
+
if (config?.enableStrictGroupCheck) {
|
|
984
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
985
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const [ok, err] = await checkPermission(session, group)
|
|
989
|
+
if (!ok) return err || '权限不足'
|
|
990
|
+
}
|
|
991
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
992
|
+
if (rows.length === 0) {
|
|
993
|
+
// 特殊处理 all 表示全局黑名单
|
|
994
|
+
if (group && group.toLowerCase() === 'all') {
|
|
995
|
+
// 不使用模板,因为默认模板会产生 "群 all 的黑名单为空" 这种奇怪输出
|
|
996
|
+
return '全局黑名单为空'
|
|
997
|
+
}
|
|
998
|
+
const tmpl = (config && config.blacklistListEmpty) || '群 {group} 的黑名单为空'
|
|
999
|
+
return tmpl.replace('{group}', group)
|
|
1000
|
+
}
|
|
1001
|
+
const entries = rows[0].entries || {}
|
|
1002
|
+
// 构造列表消息,all 也是专用前缀
|
|
1003
|
+
let prefix = group && group.toLowerCase() === 'all' ? '全局黑名单: \n' : `群 ${group} 黑名单: \n`
|
|
1004
|
+
let msg = prefix
|
|
1005
|
+
for (const uid in entries) {
|
|
1006
|
+
msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ''}\n`
|
|
1007
|
+
}
|
|
1008
|
+
return msg
|
|
1009
|
+
}
|
|
1010
|
+
if (op === 'i') {
|
|
1011
|
+
targetUser = parts[1]
|
|
1012
|
+
if (!targetUser) return '请提供用户ID'
|
|
1013
|
+
const groupArg = parts[2]
|
|
1014
|
+
const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
|
|
1015
|
+
const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== undefined
|
|
1016
|
+
|
|
1017
|
+
// helper to format reply using template
|
|
1018
|
+
const tmpl = (config && config.blacklistInfoTemplate) || '全局黑名单: {global}\n本群黑名单: {group}'
|
|
1019
|
+
const formatReply = (localHit: boolean, groupsList?: string[]) => {
|
|
1020
|
+
if (groupsList) {
|
|
1021
|
+
return tmpl.replace('{global}', globalHit ? '有' : '无').replace(
|
|
1022
|
+
'{group}', groupsList.length ? groupsList.join(',') : '无'
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
return tmpl.replace('{global}', globalHit ? '有' : '无').replace('{group}', localHit ? '有' : '无')
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// no group specified -> use current session guild
|
|
1029
|
+
if (!groupArg) {
|
|
1030
|
+
const groupId = getCurrentGroup()
|
|
1031
|
+
if (!groupId) return '请在群聊中使用此命令'
|
|
1032
|
+
const [ok, err] = await checkPermission(session, groupId)
|
|
1033
|
+
if (!ok) return err || '权限不足'
|
|
1034
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId })
|
|
1035
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
|
|
1036
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
|
|
1037
|
+
return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// groupArg provided
|
|
1041
|
+
if (groupArg.toLowerCase() === 'all') {
|
|
1042
|
+
const auth = session.author?.authority || session.user?.authority
|
|
1043
|
+
if (!(auth && auth >= 3)) return '权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限'
|
|
1044
|
+
const rows = await ctx.database.get('group_verification_blacklist', {})
|
|
1045
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : '无'
|
|
1046
|
+
const lines: string[] = []
|
|
1047
|
+
for (const r of rows) {
|
|
1048
|
+
if (r.groupId === 'all') continue
|
|
1049
|
+
const reason = (r.entries || {})[targetUser]
|
|
1050
|
+
if (reason !== undefined) {
|
|
1051
|
+
lines.push(`群${r.groupId}:${reason}`)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
let reply = `全局黑名单: ${globalReason}`
|
|
1055
|
+
if (lines.length) {
|
|
1056
|
+
reply += '\n群黑名单: \n' + lines.join('\n')
|
|
1057
|
+
} else {
|
|
1058
|
+
reply += '\n群黑名单: 无'
|
|
1059
|
+
}
|
|
1060
|
+
return reply
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// specific group provided
|
|
1064
|
+
const groupId = groupArg
|
|
1065
|
+
if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== 'all') {
|
|
1066
|
+
if (!/^\d{5,15}$/.test(groupId)) {
|
|
1067
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', groupId)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const [ok, err] = await checkPermission(session, groupId)
|
|
1071
|
+
if (!ok) return err || '权限不足'
|
|
1072
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId })
|
|
1073
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
|
|
1074
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
|
|
1075
|
+
return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
|
|
1076
|
+
}
|
|
1077
|
+
return ''
|
|
1078
|
+
}
|
|
1079
|
+
|
|
812
1080
|
export function apply(ctx: Context, config: Config) {
|
|
813
1081
|
// 创建数据库表
|
|
814
1082
|
ctx.model.extend('group_verification_config', {
|
|
@@ -873,6 +1141,17 @@ export function apply(ctx: Context, config: Config) {
|
|
|
873
1141
|
autoInc: true
|
|
874
1142
|
})
|
|
875
1143
|
|
|
1144
|
+
// 黑名单表:每条记录对应一个群(或全局),entries 存储 userId->reason 键值对
|
|
1145
|
+
ctx.model.extend('group_verification_blacklist', {
|
|
1146
|
+
id: 'unsigned',
|
|
1147
|
+
groupId: 'string',
|
|
1148
|
+
entries: 'json'
|
|
1149
|
+
} as any, {
|
|
1150
|
+
primary: 'id',
|
|
1151
|
+
autoInc: true,
|
|
1152
|
+
indexes: [ ['groupId'] ]
|
|
1153
|
+
})
|
|
1154
|
+
|
|
876
1155
|
|
|
877
1156
|
|
|
878
1157
|
// 监听 guild-member-request 事件,以便对新申请执行自动审批或拒绝
|
|
@@ -882,9 +1161,13 @@ export function apply(ctx: Context, config: Config) {
|
|
|
882
1161
|
})
|
|
883
1162
|
|
|
884
1163
|
// 监听群成员增加事件(包括手动邀请入群)
|
|
885
|
-
ctx.on('guild-member-added', async (session) => {
|
|
886
|
-
const groupId = session.guildId
|
|
887
|
-
const userId = session.userId
|
|
1164
|
+
ctx.on('guild-member-added', async (session: any) => {
|
|
1165
|
+
const groupId = session.guildId || ''
|
|
1166
|
+
const userId = session.userId || ''
|
|
1167
|
+
if (!groupId || !userId) {
|
|
1168
|
+
// 无效会话,忽略
|
|
1169
|
+
return
|
|
1170
|
+
}
|
|
888
1171
|
|
|
889
1172
|
// 无论什么情况只要检测到加入就累加总入群
|
|
890
1173
|
await incrementTotal(ctx, groupId)
|
|
@@ -991,7 +1274,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
991
1274
|
return [true]
|
|
992
1275
|
}
|
|
993
1276
|
}
|
|
994
|
-
} catch (error) {
|
|
1277
|
+
} catch (error: any) {
|
|
995
1278
|
logger.warn(`权限检查 - 获取群成员信息失败:`, error)
|
|
996
1279
|
return [false, `无法获取群 ${groupId} 的成员信息,请确认机器人已在该群中`]
|
|
997
1280
|
}
|
|
@@ -1020,7 +1303,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1020
1303
|
.option('disableMessage', '-nomsg 禁用提醒消息')
|
|
1021
1304
|
.option('query', '-? 查询当前配置')
|
|
1022
1305
|
.option('remove', '-r 删除配置')
|
|
1023
|
-
.action(async ({ session, options }, keywords) => {
|
|
1306
|
+
.action(async ({ session, options }: any, keywords: any) => {
|
|
1024
1307
|
// 详细调试:记录所有输入信息
|
|
1025
1308
|
logger.info(`=== 命令解析调试 ===`)
|
|
1026
1309
|
logger.info(`session内容: guildId=${session.guildId}, userId=${session.userId}`)
|
|
@@ -1115,7 +1398,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1115
1398
|
try {
|
|
1116
1399
|
const guild = await session.bot.getGuild(targetGroupId)
|
|
1117
1400
|
groupName = guild.name || groupName
|
|
1118
|
-
} catch (error) {
|
|
1401
|
+
} catch (error: any) {
|
|
1119
1402
|
// 无法获取群名称时使用默认值
|
|
1120
1403
|
}
|
|
1121
1404
|
|
|
@@ -1388,7 +1671,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1388
1671
|
'gv.同意', 'gverify.同意', 'group-verify.同意',
|
|
1389
1672
|
'gva'
|
|
1390
1673
|
)
|
|
1391
|
-
.action(async ({ session }, userId) => {
|
|
1674
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1392
1675
|
// 权限检查
|
|
1393
1676
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1394
1677
|
if (!hasPermission) {
|
|
@@ -1421,7 +1704,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1421
1704
|
try {
|
|
1422
1705
|
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1423
1706
|
approvedCount++
|
|
1424
|
-
} catch (error) {
|
|
1707
|
+
} catch (error: any) {
|
|
1425
1708
|
logger.warn(`处理申请 ${request.id} 时出错:`, error)
|
|
1426
1709
|
}
|
|
1427
1710
|
} else {
|
|
@@ -1445,12 +1728,14 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1445
1728
|
return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1446
1729
|
}
|
|
1447
1730
|
try {
|
|
1448
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1449
|
-
// 清除该用户的所有待审核记录
|
|
1450
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
|
|
1451
1731
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1452
|
-
|
|
1453
|
-
|
|
1732
|
+
const reply = `已同意用户 ${displayName} 的加群申请`
|
|
1733
|
+
// 先返回提示消息,再异步执行批准请求
|
|
1734
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
|
|
1735
|
+
// 清除该用户的所有待审核记录(异步也可以,顺序无关)
|
|
1736
|
+
await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
|
|
1737
|
+
return reply
|
|
1738
|
+
} catch (error: any) {
|
|
1454
1739
|
return `处理申请时出错: ${error.message}`
|
|
1455
1740
|
}
|
|
1456
1741
|
}
|
|
@@ -1471,12 +1756,12 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1471
1756
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
|
|
1472
1757
|
}
|
|
1473
1758
|
try {
|
|
1474
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1475
|
-
// 删除该用户的所有记录
|
|
1476
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId })
|
|
1477
1759
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1478
|
-
|
|
1479
|
-
|
|
1760
|
+
const reply = `已同意用户 ${displayName} 的加群申请`
|
|
1761
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
|
|
1762
|
+
await ctx.database.remove('group_verification_pending', { groupId, userId })
|
|
1763
|
+
return reply
|
|
1764
|
+
} catch (error: any) {
|
|
1480
1765
|
return `处理申请时出错: ${error.message}`
|
|
1481
1766
|
}
|
|
1482
1767
|
})
|
|
@@ -1489,7 +1774,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1489
1774
|
'gv.rej', 'gverify.rej', 'group-verify.rej',
|
|
1490
1775
|
'gvr'
|
|
1491
1776
|
)
|
|
1492
|
-
.action(async ({ session }, userId) => {
|
|
1777
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1493
1778
|
// 权限检查
|
|
1494
1779
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1495
1780
|
if (!hasPermission) {
|
|
@@ -1547,7 +1832,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1547
1832
|
await updateStats(ctx, groupId, 'rejected')
|
|
1548
1833
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1549
1834
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1550
|
-
} catch (error) {
|
|
1835
|
+
} catch (error: any) {
|
|
1551
1836
|
return `处理申请时出错: ${error.message}`
|
|
1552
1837
|
}
|
|
1553
1838
|
}
|
|
@@ -1574,7 +1859,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1574
1859
|
await updateStats(ctx, groupId, 'rejected')
|
|
1575
1860
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1576
1861
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1577
|
-
} catch (error) {
|
|
1862
|
+
} catch (error: any) {
|
|
1578
1863
|
return `处理申请时出错: ${error.message}`
|
|
1579
1864
|
}
|
|
1580
1865
|
})
|
|
@@ -1586,7 +1871,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1586
1871
|
'gv.统计', 'gverify.统计', 'group-verify.统计',
|
|
1587
1872
|
'gvs'
|
|
1588
1873
|
)
|
|
1589
|
-
.action(async ({ session }, target) => {
|
|
1874
|
+
.action(async ({ session }: any, target: any) => {
|
|
1590
1875
|
// 参数验证:只能是群号、all、total或空
|
|
1591
1876
|
const validTargets = ['all', 'total']
|
|
1592
1877
|
const isGroupId = target && /^\d+$/.test(target)
|
|
@@ -1637,7 +1922,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1637
1922
|
'gv.待处理', 'gverify.待处理', 'group-verify.待处理',
|
|
1638
1923
|
'gvp'
|
|
1639
1924
|
)
|
|
1640
|
-
.action(async ({ session }) => {
|
|
1925
|
+
.action(async ({ session }: any) => {
|
|
1641
1926
|
if (!session.guildId) {
|
|
1642
1927
|
return '请在群聊中使用此命令'
|
|
1643
1928
|
}
|
|
@@ -1662,6 +1947,18 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1662
1947
|
return result
|
|
1663
1948
|
})
|
|
1664
1949
|
|
|
1950
|
+
// Subcommand: blacklist management
|
|
1951
|
+
groupVerify
|
|
1952
|
+
.subcommand('.blacklist [args:text]', '管理加群黑名单')
|
|
1953
|
+
.alias(
|
|
1954
|
+
'gvb',
|
|
1955
|
+
'gv.blacklist', 'gverify.blacklist', 'group-verify.blacklist',
|
|
1956
|
+
'gv.黑名单', 'gverify.黑名单', 'group-verify.黑名单'
|
|
1957
|
+
)
|
|
1958
|
+
.action(async ({ session }: any, args: any) => {
|
|
1959
|
+
return await processBlacklistCommand(ctx, session, args || '', config)
|
|
1960
|
+
})
|
|
1961
|
+
|
|
1665
1962
|
// Subcommand: help information
|
|
1666
1963
|
groupVerify
|
|
1667
1964
|
.subcommand('.help', '显示帮助信息')
|
|
@@ -1718,6 +2015,19 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1718
2015
|
gv.cfg -m 2 -t 80
|
|
1719
2016
|
gv.cfg -nomsg
|
|
1720
2017
|
|
|
2018
|
+
黑名单命令 (.blacklist / gvb):
|
|
2019
|
+
a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
|
|
2020
|
+
r id [group] 删除条目;group 为 all 时移除全局黑名单
|
|
2021
|
+
l [group] 查询群黑名单;传入 all 时查看全局黑名单
|
|
2022
|
+
i id [group] 查询账号黑名单;不带参数时查询本群与全局,
|
|
2023
|
+
指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
|
|
2024
|
+
使用示例:
|
|
2025
|
+
gvb a 12345 作弊记录
|
|
2026
|
+
gvb r 12345 67890
|
|
2027
|
+
gvb l
|
|
2028
|
+
gvb l all
|
|
2029
|
+
gvb i 12345
|
|
2030
|
+
|
|
1721
2031
|
快捷命令:
|
|
1722
2032
|
gvc - 配置命令快捷方式
|
|
1723
2033
|
gva - 同意申请快捷命令
|