koishi-plugin-group-verification 1.0.29 → 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 +278 -11
- package/package.json +51 -50
- package/readme.md +32 -0
- package/src/index.ts +324 -26
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)) {
|
|
@@ -865,7 +1102,8 @@ ${debugInfo}`];
|
|
|
865
1102
|
审核方式: ${methodDesc}
|
|
866
1103
|
阈值: ${thresholdInfo}
|
|
867
1104
|
提醒消息: ${reminderStatus}
|
|
868
|
-
自定义消息:
|
|
1105
|
+
自定义消息:
|
|
1106
|
+
${config2.reminderMessage || "无"}
|
|
869
1107
|
创建时间: ${createTime}
|
|
870
1108
|
更新时间: ${updateTime}
|
|
871
1109
|
创建者: ${config2.createdBy}
|
|
@@ -1005,7 +1243,8 @@ ${debugInfo}`];
|
|
|
1005
1243
|
feedbackMessage += `提醒状态: ${reminderEnabled ? "启用" : "禁用"}
|
|
1006
1244
|
`;
|
|
1007
1245
|
if (reminderMessage && reminderEnabled) {
|
|
1008
|
-
feedbackMessage += `提醒消息:
|
|
1246
|
+
feedbackMessage += `提醒消息:
|
|
1247
|
+
${reminderMessage}
|
|
1009
1248
|
`;
|
|
1010
1249
|
}
|
|
1011
1250
|
logger.info(`准备存储到数据库的关键词: ${JSON.stringify(encodedKeywords)}`);
|
|
@@ -1081,10 +1320,11 @@ ${debugInfo}`];
|
|
|
1081
1320
|
return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1082
1321
|
}
|
|
1083
1322
|
try {
|
|
1084
|
-
await session.bot.handleGuildMemberRequest(request2.requestId, true);
|
|
1085
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
|
|
1086
1323
|
const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
|
|
1087
|
-
|
|
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;
|
|
1088
1328
|
} catch (error) {
|
|
1089
1329
|
return `处理申请时出错: ${error.message}`;
|
|
1090
1330
|
}
|
|
@@ -1102,10 +1342,11 @@ ${debugInfo}`];
|
|
|
1102
1342
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
|
|
1103
1343
|
}
|
|
1104
1344
|
try {
|
|
1105
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true);
|
|
1106
|
-
await ctx.database.remove("group_verification_pending", { groupId, userId });
|
|
1107
1345
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
|
|
1108
|
-
|
|
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;
|
|
1109
1350
|
} catch (error) {
|
|
1110
1351
|
return `处理申请时出错: ${error.message}`;
|
|
1111
1352
|
}
|
|
@@ -1255,6 +1496,17 @@ ${debugInfo}`];
|
|
|
1255
1496
|
});
|
|
1256
1497
|
return result;
|
|
1257
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
|
+
});
|
|
1258
1510
|
groupVerify.subcommand(".help", "显示帮助信息").alias("gv.帮助", "gverify.帮助", "group-verify.帮助", "帮助", "hlp", "帮助信息").action(() => {
|
|
1259
1511
|
return `群组验证命令帮助:
|
|
1260
1512
|
主指令别名:gv, gverify
|
|
@@ -1307,6 +1559,19 @@ ${debugInfo}`];
|
|
|
1307
1559
|
gv.cfg -m 2 -t 80
|
|
1308
1560
|
gv.cfg -nomsg
|
|
1309
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
|
+
|
|
1310
1575
|
快捷命令:
|
|
1311
1576
|
gvc - 配置命令快捷方式
|
|
1312
1577
|
gva - 同意申请快捷命令
|
|
@@ -1407,9 +1672,11 @@ __name(apply, "apply");
|
|
|
1407
1672
|
handleGuildMemberRequestEvent,
|
|
1408
1673
|
incrementTotal,
|
|
1409
1674
|
inject,
|
|
1675
|
+
isUserBlacklisted,
|
|
1410
1676
|
mergeReminder,
|
|
1411
1677
|
name,
|
|
1412
1678
|
parseConfigArgs,
|
|
1679
|
+
processBlacklistCommand,
|
|
1413
1680
|
resolveThreshold,
|
|
1414
1681
|
syncTotalStats,
|
|
1415
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
|
|
|
@@ -1172,7 +1442,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1172
1442
|
审核方式: ${methodDesc}
|
|
1173
1443
|
阈值: ${thresholdInfo}
|
|
1174
1444
|
提醒消息: ${reminderStatus}
|
|
1175
|
-
|
|
1445
|
+
自定义消息:\n${config.reminderMessage || '无'}
|
|
1176
1446
|
创建时间: ${createTime}
|
|
1177
1447
|
更新时间: ${updateTime}
|
|
1178
1448
|
创建者: ${config.createdBy}
|
|
@@ -1353,7 +1623,8 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1353
1623
|
|
|
1354
1624
|
feedbackMessage += `提醒状态: ${reminderEnabled ? '启用' : '禁用'}\n`
|
|
1355
1625
|
if (reminderMessage && reminderEnabled) {
|
|
1356
|
-
|
|
1626
|
+
// show full message on its own lines so users can see everything
|
|
1627
|
+
feedbackMessage += `提醒消息:\n${reminderMessage}\n`
|
|
1357
1628
|
}
|
|
1358
1629
|
|
|
1359
1630
|
|
|
@@ -1387,7 +1658,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1387
1658
|
'gv.同意', 'gverify.同意', 'group-verify.同意',
|
|
1388
1659
|
'gva'
|
|
1389
1660
|
)
|
|
1390
|
-
.action(async ({ session }, userId) => {
|
|
1661
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1391
1662
|
// 权限检查
|
|
1392
1663
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1393
1664
|
if (!hasPermission) {
|
|
@@ -1420,7 +1691,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1420
1691
|
try {
|
|
1421
1692
|
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1422
1693
|
approvedCount++
|
|
1423
|
-
} catch (error) {
|
|
1694
|
+
} catch (error: any) {
|
|
1424
1695
|
logger.warn(`处理申请 ${request.id} 时出错:`, error)
|
|
1425
1696
|
}
|
|
1426
1697
|
} else {
|
|
@@ -1444,12 +1715,14 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1444
1715
|
return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
|
|
1445
1716
|
}
|
|
1446
1717
|
try {
|
|
1447
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1448
|
-
// 清除该用户的所有待审核记录
|
|
1449
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
|
|
1450
1718
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1451
|
-
|
|
1452
|
-
|
|
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) {
|
|
1453
1726
|
return `处理申请时出错: ${error.message}`
|
|
1454
1727
|
}
|
|
1455
1728
|
}
|
|
@@ -1470,12 +1743,12 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1470
1743
|
return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
|
|
1471
1744
|
}
|
|
1472
1745
|
try {
|
|
1473
|
-
await session.bot.handleGuildMemberRequest(request.requestId, true)
|
|
1474
|
-
// 删除该用户的所有记录
|
|
1475
|
-
await ctx.database.remove('group_verification_pending', { groupId, userId })
|
|
1476
1746
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1477
|
-
|
|
1478
|
-
|
|
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) {
|
|
1479
1752
|
return `处理申请时出错: ${error.message}`
|
|
1480
1753
|
}
|
|
1481
1754
|
})
|
|
@@ -1488,7 +1761,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1488
1761
|
'gv.rej', 'gverify.rej', 'group-verify.rej',
|
|
1489
1762
|
'gvr'
|
|
1490
1763
|
)
|
|
1491
|
-
.action(async ({ session }, userId) => {
|
|
1764
|
+
.action(async ({ session }: any, userId: any) => {
|
|
1492
1765
|
// 权限检查
|
|
1493
1766
|
const [hasPermission, errorMsg] = await checkPermission(session)
|
|
1494
1767
|
if (!hasPermission) {
|
|
@@ -1546,7 +1819,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1546
1819
|
await updateStats(ctx, groupId, 'rejected')
|
|
1547
1820
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1548
1821
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1549
|
-
} catch (error) {
|
|
1822
|
+
} catch (error: any) {
|
|
1550
1823
|
return `处理申请时出错: ${error.message}`
|
|
1551
1824
|
}
|
|
1552
1825
|
}
|
|
@@ -1573,7 +1846,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1573
1846
|
await updateStats(ctx, groupId, 'rejected')
|
|
1574
1847
|
const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
|
|
1575
1848
|
return `已拒绝用户 ${displayName} 的加群申请`
|
|
1576
|
-
} catch (error) {
|
|
1849
|
+
} catch (error: any) {
|
|
1577
1850
|
return `处理申请时出错: ${error.message}`
|
|
1578
1851
|
}
|
|
1579
1852
|
})
|
|
@@ -1585,7 +1858,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1585
1858
|
'gv.统计', 'gverify.统计', 'group-verify.统计',
|
|
1586
1859
|
'gvs'
|
|
1587
1860
|
)
|
|
1588
|
-
.action(async ({ session }, target) => {
|
|
1861
|
+
.action(async ({ session }: any, target: any) => {
|
|
1589
1862
|
// 参数验证:只能是群号、all、total或空
|
|
1590
1863
|
const validTargets = ['all', 'total']
|
|
1591
1864
|
const isGroupId = target && /^\d+$/.test(target)
|
|
@@ -1636,7 +1909,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1636
1909
|
'gv.待处理', 'gverify.待处理', 'group-verify.待处理',
|
|
1637
1910
|
'gvp'
|
|
1638
1911
|
)
|
|
1639
|
-
.action(async ({ session }) => {
|
|
1912
|
+
.action(async ({ session }: any) => {
|
|
1640
1913
|
if (!session.guildId) {
|
|
1641
1914
|
return '请在群聊中使用此命令'
|
|
1642
1915
|
}
|
|
@@ -1661,6 +1934,18 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1661
1934
|
return result
|
|
1662
1935
|
})
|
|
1663
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
|
+
|
|
1664
1949
|
// Subcommand: help information
|
|
1665
1950
|
groupVerify
|
|
1666
1951
|
.subcommand('.help', '显示帮助信息')
|
|
@@ -1717,6 +2002,19 @@ export function apply(ctx: Context, config: Config) {
|
|
|
1717
2002
|
gv.cfg -m 2 -t 80
|
|
1718
2003
|
gv.cfg -nomsg
|
|
1719
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
|
+
|
|
1720
2018
|
快捷命令:
|
|
1721
2019
|
gvc - 配置命令快捷方式
|
|
1722
2020
|
gva - 同意申请快捷命令
|