koishi-plugin-group-verification 1.0.30 → 1.0.31
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 +274 -9
- package/package.json +51 -50
- package/readme.md +32 -0
- package/src/index.ts +321 -24
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";
|
|
@@ -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,208 @@ 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 > 0) {
|
|
665
|
+
const last = rest[rest.length - 1];
|
|
666
|
+
if (/^\d+$/.test(last) || last.toLowerCase() === "all") {
|
|
667
|
+
group = last;
|
|
668
|
+
reason = rest.slice(0, -1).join(" ");
|
|
669
|
+
} else {
|
|
670
|
+
reason = rest.join(" ");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
group = group || getCurrentGroup();
|
|
674
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
675
|
+
if (group.toLowerCase() === "all") {
|
|
676
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
677
|
+
if (!(auth && auth >= 3)) return "设置全局黑名单需要 koishi 3 级以上权限";
|
|
678
|
+
} else {
|
|
679
|
+
if (config?.enableStrictGroupCheck) {
|
|
680
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
681
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const [ok, err] = await checkPermission(session, group);
|
|
685
|
+
if (!ok) return err || "权限不足";
|
|
686
|
+
}
|
|
687
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
688
|
+
const timePrefix = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
689
|
+
const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix;
|
|
690
|
+
if (rows.length > 0) {
|
|
691
|
+
const row = rows[0];
|
|
692
|
+
const entries = row.entries || {};
|
|
693
|
+
if (entries[targetUser] !== void 0) {
|
|
694
|
+
return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`;
|
|
695
|
+
}
|
|
696
|
+
entries[targetUser] = storedReason;
|
|
697
|
+
await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
|
|
698
|
+
} else {
|
|
699
|
+
const entries = {};
|
|
700
|
+
entries[targetUser] = storedReason;
|
|
701
|
+
await ctx.database.create("group_verification_blacklist", { groupId: group, entries });
|
|
702
|
+
}
|
|
703
|
+
const tmpl = config && config.blacklistAddSuccess || "已将用户 {user} 加入群 {group} 黑名单{reason}";
|
|
704
|
+
return tmpl.replace("{user}", targetUser).replace("{group}", group).replace("{reason}", reason ? `,原因:${reason}` : "");
|
|
705
|
+
}
|
|
706
|
+
if (op === "r") {
|
|
707
|
+
targetUser = parts[1];
|
|
708
|
+
if (!targetUser) return "请提供用户ID";
|
|
709
|
+
group = parts[2] || getCurrentGroup();
|
|
710
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
711
|
+
if (group.toLowerCase() === "all") {
|
|
712
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
713
|
+
if (!(auth && auth >= 3)) return "修改全局黑名单需要 koishi 3 级以上权限";
|
|
714
|
+
} else {
|
|
715
|
+
if (config?.enableStrictGroupCheck) {
|
|
716
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
717
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const [ok, err] = await checkPermission(session, group);
|
|
721
|
+
if (!ok) return err || "权限不足";
|
|
722
|
+
}
|
|
723
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
724
|
+
if (rows.length > 0) {
|
|
725
|
+
const row = rows[0];
|
|
726
|
+
const entries = row.entries || {};
|
|
727
|
+
delete entries[targetUser];
|
|
728
|
+
await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
|
|
729
|
+
}
|
|
730
|
+
const tmpl = config && config.blacklistRemoveSuccess || "已从群 {group} 的黑名单中移除用户 {user}";
|
|
731
|
+
return tmpl.replace("{user}", targetUser).replace("{group}", group);
|
|
732
|
+
}
|
|
733
|
+
if (op === "l") {
|
|
734
|
+
group = parts[1] || getCurrentGroup();
|
|
735
|
+
if (!group) return "请在群聊中使用此命令或指定群号";
|
|
736
|
+
if (group.toLowerCase() === "all") {
|
|
737
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
738
|
+
if (!(auth && auth >= 3)) return "查看全局黑名单需要 koishi 3 级以上权限";
|
|
739
|
+
} else {
|
|
740
|
+
if (config?.enableStrictGroupCheck) {
|
|
741
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
742
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const [ok, err] = await checkPermission(session, group);
|
|
746
|
+
if (!ok) return err || "权限不足";
|
|
747
|
+
}
|
|
748
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
|
|
749
|
+
if (rows.length === 0) {
|
|
750
|
+
if (group && group.toLowerCase() === "all") {
|
|
751
|
+
return "全局黑名单为空";
|
|
752
|
+
}
|
|
753
|
+
const tmpl = config && config.blacklistListEmpty || "群 {group} 的黑名单为空";
|
|
754
|
+
return tmpl.replace("{group}", group);
|
|
755
|
+
}
|
|
756
|
+
const entries = rows[0].entries || {};
|
|
757
|
+
let prefix = group && group.toLowerCase() === "all" ? "全局黑名单: \n" : `群 ${group} 黑名单:
|
|
758
|
+
`;
|
|
759
|
+
let msg = prefix;
|
|
760
|
+
for (const uid in entries) {
|
|
761
|
+
msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ""}
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
return msg;
|
|
765
|
+
}
|
|
766
|
+
if (op === "i") {
|
|
767
|
+
targetUser = parts[1];
|
|
768
|
+
if (!targetUser) return "请提供用户ID";
|
|
769
|
+
const groupArg = parts[2];
|
|
770
|
+
const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
|
|
771
|
+
const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== void 0;
|
|
772
|
+
const tmpl = config && config.blacklistInfoTemplate || "全局黑名单: {global}\n本群黑名单: {group}";
|
|
773
|
+
const formatReply = /* @__PURE__ */ __name((localHit, groupsList) => {
|
|
774
|
+
if (groupsList) {
|
|
775
|
+
return tmpl.replace("{global}", globalHit ? "有" : "无").replace(
|
|
776
|
+
"{group}",
|
|
777
|
+
groupsList.length ? groupsList.join(",") : "无"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return tmpl.replace("{global}", globalHit ? "有" : "无").replace("{group}", localHit ? "有" : "无");
|
|
781
|
+
}, "formatReply");
|
|
782
|
+
if (!groupArg) {
|
|
783
|
+
const groupId2 = getCurrentGroup();
|
|
784
|
+
if (!groupId2) return "请在群聊中使用此命令";
|
|
785
|
+
const [ok2, err2] = await checkPermission(session, groupId2);
|
|
786
|
+
if (!ok2) return err2 || "权限不足";
|
|
787
|
+
const rows2 = await ctx.database.get("group_verification_blacklist", { groupId: groupId2 });
|
|
788
|
+
const localReason2 = rows2.length > 0 ? (rows2[0].entries || {})[targetUser] : void 0;
|
|
789
|
+
const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : void 0;
|
|
790
|
+
return `全局黑名单: ${globalReason2 || "无"}
|
|
791
|
+
群${groupId2}黑名单: ${localReason2 || "无"}`;
|
|
792
|
+
}
|
|
793
|
+
if (groupArg.toLowerCase() === "all") {
|
|
794
|
+
const auth = session.author?.authority || session.user?.authority;
|
|
795
|
+
if (!(auth && auth >= 3)) return "权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限";
|
|
796
|
+
const rows2 = await ctx.database.get("group_verification_blacklist", {});
|
|
797
|
+
const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : "无";
|
|
798
|
+
const lines = [];
|
|
799
|
+
for (const r of rows2) {
|
|
800
|
+
if (r.groupId === "all") continue;
|
|
801
|
+
const reason2 = (r.entries || {})[targetUser];
|
|
802
|
+
if (reason2 !== void 0) {
|
|
803
|
+
lines.push(`群${r.groupId}:${reason2}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
let reply = `全局黑名单: ${globalReason2}`;
|
|
807
|
+
if (lines.length) {
|
|
808
|
+
reply += "\n群黑名单: \n" + lines.join("\n");
|
|
809
|
+
} else {
|
|
810
|
+
reply += "\n群黑名单: 无";
|
|
811
|
+
}
|
|
812
|
+
return reply;
|
|
813
|
+
}
|
|
814
|
+
const groupId = groupArg;
|
|
815
|
+
if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== "all") {
|
|
816
|
+
if (!/^\d{5,15}$/.test(groupId)) {
|
|
817
|
+
return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", groupId);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const [ok, err] = await checkPermission(session, groupId);
|
|
821
|
+
if (!ok) return err || "权限不足";
|
|
822
|
+
const rows = await ctx.database.get("group_verification_blacklist", { groupId });
|
|
823
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : void 0;
|
|
824
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : void 0;
|
|
825
|
+
return `全局黑名单: ${globalReason || "无"}
|
|
826
|
+
群${groupId}黑名单: ${localReason || "无"}`;
|
|
827
|
+
}
|
|
828
|
+
return "";
|
|
829
|
+
}
|
|
830
|
+
__name(processBlacklistCommand, "processBlacklistCommand");
|
|
606
831
|
function apply(ctx, config) {
|
|
607
832
|
ctx.model.extend("group_verification_config", {
|
|
608
833
|
id: "unsigned",
|
|
@@ -655,13 +880,25 @@ function apply(ctx, config) {
|
|
|
655
880
|
primary: "id",
|
|
656
881
|
autoInc: true
|
|
657
882
|
});
|
|
883
|
+
ctx.model.extend("group_verification_blacklist", {
|
|
884
|
+
id: "unsigned",
|
|
885
|
+
groupId: "string",
|
|
886
|
+
entries: "json"
|
|
887
|
+
}, {
|
|
888
|
+
primary: "id",
|
|
889
|
+
autoInc: true,
|
|
890
|
+
indexes: [["groupId"]]
|
|
891
|
+
});
|
|
658
892
|
ctx.on("guild-member-request", async (session) => {
|
|
659
893
|
logger.info("收到 guild-member-request 事件,转发给处理函数");
|
|
660
894
|
await handleGuildMemberRequestEvent(ctx, session);
|
|
661
895
|
});
|
|
662
896
|
ctx.on("guild-member-added", async (session) => {
|
|
663
|
-
const groupId = session.guildId;
|
|
664
|
-
const userId = session.userId;
|
|
897
|
+
const groupId = session.guildId || "";
|
|
898
|
+
const userId = session.userId || "";
|
|
899
|
+
if (!groupId || !userId) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
665
902
|
await incrementTotal(ctx, groupId);
|
|
666
903
|
const set = autoQueue.get(groupId);
|
|
667
904
|
if (set && set.has(userId)) {
|
|
@@ -1083,10 +1320,11 @@ ${reminderMessage}
|
|
|
1083
1320
|
return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1084
1321
|
}
|
|
1085
1322
|
try {
|
|
1086
|
-
await session.bot.handleGuildMemberRequest(request2.requestId, true);
|
|
1087
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
|
|
1088
1323
|
const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
|
|
1089
|
-
|
|
1324
|
+
const reply = `已同意用户 ${displayName} 的加群申请`;
|
|
1325
|
+
session.bot.handleGuildMemberRequest(request2.requestId, true).catch((e) => logger.warn("自动同意失败", e));
|
|
1326
|
+
await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
|
|
1327
|
+
return reply;
|
|
1090
1328
|
} catch (error) {
|
|
1091
1329
|
return `处理申请时出错: ${error.message}`;
|
|
1092
1330
|
}
|
|
@@ -1104,10 +1342,11 @@ ${reminderMessage}
|
|
|
1104
1342
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
|
|
1105
1343
|
}
|
|
1106
1344
|
try {
|
|
1107
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true);
|
|
1108
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId });
|
|
1109
1345
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
|
|
1110
|
-
|
|
1346
|
+
const reply = `已同意用户 ${displayName} 的加群申请`;
|
|
1347
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e) => logger.warn("自动同意失败", e));
|
|
1348
|
+
await ctx.database.remove("group_verification_pending", { groupId, userId });
|
|
1349
|
+
return reply;
|
|
1111
1350
|
} catch (error) {
|
|
1112
1351
|
return `处理申请时出错: ${error.message}`;
|
|
1113
1352
|
}
|
|
@@ -1257,6 +1496,17 @@ ${reminderMessage}
|
|
|
1257
1496
|
});
|
|
1258
1497
|
return result;
|
|
1259
1498
|
});
|
|
1499
|
+
groupVerify.subcommand(".blacklist [args:text]", "管理加群黑名单").alias(
|
|
1500
|
+
"gvb",
|
|
1501
|
+
"gv.blacklist",
|
|
1502
|
+
"gverify.blacklist",
|
|
1503
|
+
"group-verify.blacklist",
|
|
1504
|
+
"gv.黑名单",
|
|
1505
|
+
"gverify.黑名单",
|
|
1506
|
+
"group-verify.黑名单"
|
|
1507
|
+
).action(async ({ session }, args) => {
|
|
1508
|
+
return await processBlacklistCommand(ctx, session, args || "", config);
|
|
1509
|
+
});
|
|
1260
1510
|
groupVerify.subcommand(".help", "显示帮助信息").alias("gv.帮助", "gverify.帮助", "group-verify.帮助", "帮助", "hlp", "帮助信息").action(() => {
|
|
1261
1511
|
return `群组验证命令帮助:
|
|
1262
1512
|
主指令别名:gv, gverify
|
|
@@ -1309,6 +1559,19 @@ ${reminderMessage}
|
|
|
1309
1559
|
gv.cfg -m 2 -t 80
|
|
1310
1560
|
gv.cfg -nomsg
|
|
1311
1561
|
|
|
1562
|
+
黑名单命令 (.blacklist / gvb):
|
|
1563
|
+
a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
|
|
1564
|
+
r id [group] 删除条目;group 为 all 时移除全局黑名单
|
|
1565
|
+
l [group] 查询群黑名单;传入 all 时查看全局黑名单
|
|
1566
|
+
i id [group] 查询账号黑名单;不带参数时查询本群与全局,
|
|
1567
|
+
指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
|
|
1568
|
+
使用示例:
|
|
1569
|
+
gvb a 12345 作弊记录
|
|
1570
|
+
gvb r 12345 67890
|
|
1571
|
+
gvb l
|
|
1572
|
+
gvb l all
|
|
1573
|
+
gvb i 12345
|
|
1574
|
+
|
|
1312
1575
|
快捷命令:
|
|
1313
1576
|
gvc - 配置命令快捷方式
|
|
1314
1577
|
gva - 同意申请快捷命令
|
|
@@ -1409,9 +1672,11 @@ __name(apply, "apply");
|
|
|
1409
1672
|
handleGuildMemberRequestEvent,
|
|
1410
1673
|
incrementTotal,
|
|
1411
1674
|
inject,
|
|
1675
|
+
isUserBlacklisted,
|
|
1412
1676
|
mergeReminder,
|
|
1413
1677
|
name,
|
|
1414
1678
|
parseConfigArgs,
|
|
1679
|
+
processBlacklistCommand,
|
|
1415
1680
|
resolveThreshold,
|
|
1416
1681
|
syncTotalStats,
|
|
1417
1682
|
tokenize,
|
package/package.json
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "koishi-plugin-group-verification",
|
|
3
|
-
"description": "Koishi
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-group-verification",
|
|
3
|
+
"description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能",
|
|
4
|
+
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/LHDyx/koishi-plugin-group-verification.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/LHDyx/koishi-plugin-group-verification/issues"
|
|
11
|
+
},
|
|
12
|
+
"version": "1.0.31",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"typings": "lib/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"USAGE_EXAMPLE.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc -w",
|
|
25
|
+
"test": "ts-node test/basic-test.ts"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"chatbot",
|
|
30
|
+
"koishi",
|
|
31
|
+
"plugin",
|
|
32
|
+
"group-verification",
|
|
33
|
+
"guild-management",
|
|
34
|
+
"join-request",
|
|
35
|
+
"moderation"
|
|
36
|
+
],
|
|
37
|
+
"koishi": {
|
|
38
|
+
"service": {
|
|
39
|
+
"required": [
|
|
40
|
+
"database"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"koishi": "^4.15.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^4.9.0",
|
|
49
|
+
"@types/node": "^16.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
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
|
|
|
@@ -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,228 @@ 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
|
+
const rest = parts.slice(2)
|
|
890
|
+
if (rest.length > 0) {
|
|
891
|
+
const last = rest[rest.length - 1]
|
|
892
|
+
if (/^\d+$/.test(last) || last.toLowerCase() === 'all') {
|
|
893
|
+
group = last
|
|
894
|
+
reason = rest.slice(0, -1).join(' ')
|
|
895
|
+
} else {
|
|
896
|
+
reason = rest.join(' ')
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
group = group || getCurrentGroup()
|
|
900
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
901
|
+
// 权限检查
|
|
902
|
+
if (group.toLowerCase() === 'all') {
|
|
903
|
+
const auth = session.author?.authority || session.user?.authority
|
|
904
|
+
if (!(auth && auth >= 3)) return '设置全局黑名单需要 koishi 3 级以上权限'
|
|
905
|
+
} else {
|
|
906
|
+
// 严格群号检查(若开启)
|
|
907
|
+
if (config?.enableStrictGroupCheck) {
|
|
908
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
909
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const [ok, err] = await checkPermission(session, group)
|
|
913
|
+
if (!ok) return err || '权限不足'
|
|
914
|
+
}
|
|
915
|
+
// add entry to map with timestamp, but refuse duplicates
|
|
916
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
917
|
+
// build stored reason with current time prefix
|
|
918
|
+
const timePrefix = new Date().toLocaleString()
|
|
919
|
+
const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix
|
|
920
|
+
if (rows.length > 0) {
|
|
921
|
+
const row = rows[0]
|
|
922
|
+
const entries = row.entries || {}
|
|
923
|
+
if (entries[targetUser] !== undefined) {
|
|
924
|
+
return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`
|
|
925
|
+
}
|
|
926
|
+
entries[targetUser] = storedReason
|
|
927
|
+
await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
|
|
928
|
+
} else {
|
|
929
|
+
const entries: Record<string,string> = {}
|
|
930
|
+
entries[targetUser] = storedReason
|
|
931
|
+
await ctx.database.create('group_verification_blacklist', { groupId: group, entries })
|
|
932
|
+
}
|
|
933
|
+
const tmpl = (config && config.blacklistAddSuccess) || '已将用户 {user} 加入群 {group} 黑名单{reason}'
|
|
934
|
+
return tmpl.replace('{user}', targetUser).replace('{group}', group).replace('{reason}', reason ? `,原因:${reason}` : '')
|
|
935
|
+
}
|
|
936
|
+
if (op === 'r') {
|
|
937
|
+
targetUser = parts[1]
|
|
938
|
+
if (!targetUser) return '请提供用户ID'
|
|
939
|
+
group = parts[2] || getCurrentGroup()
|
|
940
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
941
|
+
if (group.toLowerCase() === 'all') {
|
|
942
|
+
const auth = session.author?.authority || session.user?.authority
|
|
943
|
+
if (!(auth && auth >= 3)) return '修改全局黑名单需要 koishi 3 级以上权限'
|
|
944
|
+
} else {
|
|
945
|
+
if (config?.enableStrictGroupCheck) {
|
|
946
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
947
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const [ok, err] = await checkPermission(session, group)
|
|
951
|
+
if (!ok) return err || '权限不足'
|
|
952
|
+
}
|
|
953
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
954
|
+
if (rows.length > 0) {
|
|
955
|
+
const row = rows[0]
|
|
956
|
+
const entries = row.entries || {}
|
|
957
|
+
delete entries[targetUser]
|
|
958
|
+
await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
|
|
959
|
+
}
|
|
960
|
+
const tmpl = (config && config.blacklistRemoveSuccess) || '已从群 {group} 的黑名单中移除用户 {user}'
|
|
961
|
+
return tmpl.replace('{user}', targetUser).replace('{group}', group)
|
|
962
|
+
}
|
|
963
|
+
if (op === 'l') {
|
|
964
|
+
group = parts[1] || getCurrentGroup()
|
|
965
|
+
if (!group) return '请在群聊中使用此命令或指定群号'
|
|
966
|
+
if (group.toLowerCase() === 'all') {
|
|
967
|
+
const auth = session.author?.authority || session.user?.authority
|
|
968
|
+
if (!(auth && auth >= 3)) return '查看全局黑名单需要 koishi 3 级以上权限'
|
|
969
|
+
} else {
|
|
970
|
+
if (config?.enableStrictGroupCheck) {
|
|
971
|
+
if (!/^\d{5,15}$/.test(group)) {
|
|
972
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const [ok, err] = await checkPermission(session, group)
|
|
976
|
+
if (!ok) return err || '权限不足'
|
|
977
|
+
}
|
|
978
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
|
|
979
|
+
if (rows.length === 0) {
|
|
980
|
+
// 特殊处理 all 表示全局黑名单
|
|
981
|
+
if (group && group.toLowerCase() === 'all') {
|
|
982
|
+
// 不使用模板,因为默认模板会产生 "群 all 的黑名单为空" 这种奇怪输出
|
|
983
|
+
return '全局黑名单为空'
|
|
984
|
+
}
|
|
985
|
+
const tmpl = (config && config.blacklistListEmpty) || '群 {group} 的黑名单为空'
|
|
986
|
+
return tmpl.replace('{group}', group)
|
|
987
|
+
}
|
|
988
|
+
const entries = rows[0].entries || {}
|
|
989
|
+
// 构造列表消息,all 也是专用前缀
|
|
990
|
+
let prefix = group && group.toLowerCase() === 'all' ? '全局黑名单: \n' : `群 ${group} 黑名单: \n`
|
|
991
|
+
let msg = prefix
|
|
992
|
+
for (const uid in entries) {
|
|
993
|
+
msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ''}\n`
|
|
994
|
+
}
|
|
995
|
+
return msg
|
|
996
|
+
}
|
|
997
|
+
if (op === 'i') {
|
|
998
|
+
targetUser = parts[1]
|
|
999
|
+
if (!targetUser) return '请提供用户ID'
|
|
1000
|
+
const groupArg = parts[2]
|
|
1001
|
+
const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
|
|
1002
|
+
const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== undefined
|
|
1003
|
+
|
|
1004
|
+
// helper to format reply using template
|
|
1005
|
+
const tmpl = (config && config.blacklistInfoTemplate) || '全局黑名单: {global}\n本群黑名单: {group}'
|
|
1006
|
+
const formatReply = (localHit: boolean, groupsList?: string[]) => {
|
|
1007
|
+
if (groupsList) {
|
|
1008
|
+
return tmpl.replace('{global}', globalHit ? '有' : '无').replace(
|
|
1009
|
+
'{group}', groupsList.length ? groupsList.join(',') : '无'
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
return tmpl.replace('{global}', globalHit ? '有' : '无').replace('{group}', localHit ? '有' : '无')
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// no group specified -> use current session guild
|
|
1016
|
+
if (!groupArg) {
|
|
1017
|
+
const groupId = getCurrentGroup()
|
|
1018
|
+
if (!groupId) return '请在群聊中使用此命令'
|
|
1019
|
+
const [ok, err] = await checkPermission(session, groupId)
|
|
1020
|
+
if (!ok) return err || '权限不足'
|
|
1021
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId })
|
|
1022
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
|
|
1023
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
|
|
1024
|
+
return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// groupArg provided
|
|
1028
|
+
if (groupArg.toLowerCase() === 'all') {
|
|
1029
|
+
const auth = session.author?.authority || session.user?.authority
|
|
1030
|
+
if (!(auth && auth >= 3)) return '权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限'
|
|
1031
|
+
const rows = await ctx.database.get('group_verification_blacklist', {})
|
|
1032
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : '无'
|
|
1033
|
+
const lines: string[] = []
|
|
1034
|
+
for (const r of rows) {
|
|
1035
|
+
if (r.groupId === 'all') continue
|
|
1036
|
+
const reason = (r.entries || {})[targetUser]
|
|
1037
|
+
if (reason !== undefined) {
|
|
1038
|
+
lines.push(`群${r.groupId}:${reason}`)
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
let reply = `全局黑名单: ${globalReason}`
|
|
1042
|
+
if (lines.length) {
|
|
1043
|
+
reply += '\n群黑名单: \n' + lines.join('\n')
|
|
1044
|
+
} else {
|
|
1045
|
+
reply += '\n群黑名单: 无'
|
|
1046
|
+
}
|
|
1047
|
+
return reply
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// specific group provided
|
|
1051
|
+
const groupId = groupArg
|
|
1052
|
+
if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== 'all') {
|
|
1053
|
+
if (!/^\d{5,15}$/.test(groupId)) {
|
|
1054
|
+
return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', groupId)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const [ok, err] = await checkPermission(session, groupId)
|
|
1058
|
+
if (!ok) return err || '权限不足'
|
|
1059
|
+
const rows = await ctx.database.get('group_verification_blacklist', { groupId })
|
|
1060
|
+
const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
|
|
1061
|
+
const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
|
|
1062
|
+
return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
|
|
1063
|
+
}
|
|
1064
|
+
return ''
|
|
1065
|
+
}
|
|
1066
|
+
|
|
812
1067
|
export function apply(ctx: Context, config: Config) {
|
|
813
1068
|
// 创建数据库表
|
|
814
1069
|
ctx.model.extend('group_verification_config', {
|
|
@@ -873,6 +1128,17 @@ export function apply(ctx: Context, config: Config) {
|
|
|
873
1128
|
autoInc: true
|
|
874
1129
|
})
|
|
875
1130
|
|
|
1131
|
+
// 黑名单表:每条记录对应一个群(或全局),entries 存储 userId->reason 键值对
|
|
1132
|
+
ctx.model.extend('group_verification_blacklist', {
|
|
1133
|
+
id: 'unsigned',
|
|
1134
|
+
groupId: 'string',
|
|
1135
|
+
entries: 'json'
|
|
1136
|
+
} as any, {
|
|
1137
|
+
primary: 'id',
|
|
1138
|
+
autoInc: true,
|
|
1139
|
+
indexes: [ ['groupId'] ]
|
|
1140
|
+
})
|
|
1141
|
+
|
|
876
1142
|
|
|
877
1143
|
|
|
878
1144
|
// 监听 guild-member-request 事件,以便对新申请执行自动审批或拒绝
|
|
@@ -882,9 +1148,13 @@ export function apply(ctx: Context, config: Config) {
|
|
|
882
1148
|
})
|
|
883
1149
|
|
|
884
1150
|
// 监听群成员增加事件(包括手动邀请入群)
|
|
885
|
-
ctx.on('guild-member-added', async (session) => {
|
|
886
|
-
const groupId = session.guildId
|
|
887
|
-
const userId = session.userId
|
|
1151
|
+
ctx.on('guild-member-added', async (session: any) => {
|
|
1152
|
+
const groupId = session.guildId || ''
|
|
1153
|
+
const userId = session.userId || ''
|
|
1154
|
+
if (!groupId || !userId) {
|
|
1155
|
+
// 无效会话,忽略
|
|
1156
|
+
return
|
|
1157
|
+
}
|
|
888
1158
|
|
|
889
1159
|
// 无论什么情况只要检测到加入就累加总入群
|
|
890
1160
|
await incrementTotal(ctx, groupId)
|
|
@@ -991,7 +1261,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
991
1261
|
return [true]
|
|
992
1262
|
}
|
|
993
1263
|
}
|
|
994
|
-
} catch (error) {
|
|
1264
|
+
} catch (error: any) {
|
|
995
1265
|
logger.warn(`权限检查 - 获取群成员信息失败:`, error)
|
|
996
1266
|
return [false, `无法获取群 ${groupId} 的成员信息,请确认机器人已在该群中`]
|
|
997
1267
|
}
|
|
@@ -1020,7 +1290,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1020
1290
|
.option('disableMessage', '-nomsg 禁用提醒消息')
|
|
1021
1291
|
.option('query', '-? 查询当前配置')
|
|
1022
1292
|
.option('remove', '-r 删除配置')
|
|
1023
|
-
.action(async ({ session, options }, keywords) => {
|
|
1293
|
+
.action(async ({ session, options }: any, keywords: any) => {
|
|
1024
1294
|
// 详细调试:记录所有输入信息
|
|
1025
1295
|
logger.info(`=== 命令解析调试 ===`)
|
|
1026
1296
|
logger.info(`session内容: guildId=${session.guildId}, userId=${session.userId}`)
|
|
@@ -1115,7 +1385,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1115
1385
|
try {
|
|
1116
1386
|
const guild = await session.bot.getGuild(targetGroupId)
|
|
1117
1387
|
groupName = guild.name || groupName
|
|
1118
|
-
} catch (error) {
|
|
1388
|
+
} catch (error: any) {
|
|
1119
1389
|
// 无法获取群名称时使用默认值
|
|
1120
1390
|
}
|
|
1121
1391
|
|
|
@@ -1388,7 +1658,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1388
1658
|
'gv.同意', 'gverify.同意', 'group-verify.同意',
|
|
1389
1659
|
'gva'
|
|
1390
1660
|
)
|
|
1391
|
-
.action(async ({ session }, userId) => {
|
|
1661
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1392
1662
|
// 权限检查
|
|
1393
1663
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1394
1664
|
if (!hasPermission) {
|
|
@@ -1421,7 +1691,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1421
1691
|
try {
|
|
1422
1692
|
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1423
1693
|
approvedCount++
|
|
1424
|
-
} catch (error) {
|
|
1694
|
+
} catch (error: any) {
|
|
1425
1695
|
logger.warn(`处理申请 ${request.id} 时出错:`, error)
|
|
1426
1696
|
}
|
|
1427
1697
|
} else {
|
|
@@ -1445,12 +1715,14 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1445
1715
|
return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1446
1716
|
}
|
|
1447
1717
|
try {
|
|
1448
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1449
|
-
// 清除该用户的所有待审核记录
|
|
1450
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
|
|
1451
1718
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1452
|
-
|
|
1453
|
-
|
|
1719
|
+
const reply = `已同意用户 ${displayName} 的加群申请`
|
|
1720
|
+
// 先返回提示消息,再异步执行批准请求
|
|
1721
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
|
|
1722
|
+
// 清除该用户的所有待审核记录(异步也可以,顺序无关)
|
|
1723
|
+
await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
|
|
1724
|
+
return reply
|
|
1725
|
+
} catch (error: any) {
|
|
1454
1726
|
return `处理申请时出错: ${error.message}`
|
|
1455
1727
|
}
|
|
1456
1728
|
}
|
|
@@ -1471,12 +1743,12 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1471
1743
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
|
|
1472
1744
|
}
|
|
1473
1745
|
try {
|
|
1474
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1475
|
-
// 删除该用户的所有记录
|
|
1476
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId })
|
|
1477
1746
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1478
|
-
|
|
1479
|
-
|
|
1747
|
+
const reply = `已同意用户 ${displayName} 的加群申请`
|
|
1748
|
+
session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
|
|
1749
|
+
await ctx.database.remove('group_verification_pending', { groupId, userId })
|
|
1750
|
+
return reply
|
|
1751
|
+
} catch (error: any) {
|
|
1480
1752
|
return `处理申请时出错: ${error.message}`
|
|
1481
1753
|
}
|
|
1482
1754
|
})
|
|
@@ -1489,7 +1761,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1489
1761
|
'gv.rej', 'gverify.rej', 'group-verify.rej',
|
|
1490
1762
|
'gvr'
|
|
1491
1763
|
)
|
|
1492
|
-
.action(async ({ session }, userId) => {
|
|
1764
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1493
1765
|
// 权限检查
|
|
1494
1766
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1495
1767
|
if (!hasPermission) {
|
|
@@ -1547,7 +1819,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1547
1819
|
await updateStats(ctx, groupId, 'rejected')
|
|
1548
1820
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1549
1821
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1550
|
-
} catch (error) {
|
|
1822
|
+
} catch (error: any) {
|
|
1551
1823
|
return `处理申请时出错: ${error.message}`
|
|
1552
1824
|
}
|
|
1553
1825
|
}
|
|
@@ -1574,7 +1846,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1574
1846
|
await updateStats(ctx, groupId, 'rejected')
|
|
1575
1847
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1576
1848
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1577
|
-
} catch (error) {
|
|
1849
|
+
} catch (error: any) {
|
|
1578
1850
|
return `处理申请时出错: ${error.message}`
|
|
1579
1851
|
}
|
|
1580
1852
|
})
|
|
@@ -1586,7 +1858,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1586
1858
|
'gv.统计', 'gverify.统计', 'group-verify.统计',
|
|
1587
1859
|
'gvs'
|
|
1588
1860
|
)
|
|
1589
|
-
.action(async ({ session }, target) => {
|
|
1861
|
+
.action(async ({ session }: any, target: any) => {
|
|
1590
1862
|
// 参数验证:只能是群号、all、total或空
|
|
1591
1863
|
const validTargets = ['all', 'total']
|
|
1592
1864
|
const isGroupId = target && /^\d+$/.test(target)
|
|
@@ -1637,7 +1909,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1637
1909
|
'gv.待处理', 'gverify.待处理', 'group-verify.待处理',
|
|
1638
1910
|
'gvp'
|
|
1639
1911
|
)
|
|
1640
|
-
.action(async ({ session }) => {
|
|
1912
|
+
.action(async ({ session }: any) => {
|
|
1641
1913
|
if (!session.guildId) {
|
|
1642
1914
|
return '请在群聊中使用此命令'
|
|
1643
1915
|
}
|
|
@@ -1662,6 +1934,18 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1662
1934
|
return result
|
|
1663
1935
|
})
|
|
1664
1936
|
|
|
1937
|
+
// Subcommand: blacklist management
|
|
1938
|
+
groupVerify
|
|
1939
|
+
.subcommand('.blacklist [args:text]', '管理加群黑名单')
|
|
1940
|
+
.alias(
|
|
1941
|
+
'gvb',
|
|
1942
|
+
'gv.blacklist', 'gverify.blacklist', 'group-verify.blacklist',
|
|
1943
|
+
'gv.黑名单', 'gverify.黑名单', 'group-verify.黑名单'
|
|
1944
|
+
)
|
|
1945
|
+
.action(async ({ session }: any, args: any) => {
|
|
1946
|
+
return await processBlacklistCommand(ctx, session, args || '', config)
|
|
1947
|
+
})
|
|
1948
|
+
|
|
1665
1949
|
// Subcommand: help information
|
|
1666
1950
|
groupVerify
|
|
1667
1951
|
.subcommand('.help', '显示帮助信息')
|
|
@@ -1718,6 +2002,19 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1718
2002
|
gv.cfg -m 2 -t 80
|
|
1719
2003
|
gv.cfg -nomsg
|
|
1720
2004
|
|
|
2005
|
+
黑名单命令 (.blacklist / gvb):
|
|
2006
|
+
a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
|
|
2007
|
+
r id [group] 删除条目;group 为 all 时移除全局黑名单
|
|
2008
|
+
l [group] 查询群黑名单;传入 all 时查看全局黑名单
|
|
2009
|
+
i id [group] 查询账号黑名单;不带参数时查询本群与全局,
|
|
2010
|
+
指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
|
|
2011
|
+
使用示例:
|
|
2012
|
+
gvb a 12345 作弊记录
|
|
2013
|
+
gvb r 12345 67890
|
|
2014
|
+
gvb l
|
|
2015
|
+
gvb l all
|
|
2016
|
+
gvb i 12345
|
|
2017
|
+
|
|
1721
2018
|
快捷命令:
|
|
1722
2019
|
gvc - 配置命令快捷方式
|
|
1723
2020
|
gva - 同意申请快捷命令
|