koishi-plugin-onebot-verifier 0.0.1 → 1.0.1
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/LICENSE +661 -0
- package/lib/index.d.ts +13 -12
- package/lib/index.js +193 -229
- package/package.json +1 -1
- package/readme.md +63 -1
- package/lib/request.d.ts +0 -84
package/lib/index.d.ts
CHANGED
|
@@ -2,21 +2,22 @@ import { Context, Schema } from 'koishi';
|
|
|
2
2
|
export declare const name = "onebot-verifier";
|
|
3
3
|
export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
|
|
4
4
|
export interface Config {
|
|
5
|
-
enable?: boolean;
|
|
6
5
|
notifyTarget?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
notifyAuto?: boolean;
|
|
7
|
+
debugMode?: boolean;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
timeoutAction?: 'accept' | 'reject';
|
|
10
|
+
friendLevel?: number;
|
|
11
|
+
friendRegex?: string;
|
|
12
|
+
minMembers?: number;
|
|
13
|
+
maxCapacity?: number;
|
|
14
|
+
verifyMode?: 'strict' | 'assist' | 'manual';
|
|
15
|
+
verifyRules?: {
|
|
11
16
|
guildId: string;
|
|
12
|
-
keyword
|
|
13
|
-
minLevel
|
|
17
|
+
keyword?: string;
|
|
18
|
+
minLevel?: number;
|
|
19
|
+
groupAction?: 'accept' | 'reject';
|
|
14
20
|
}[];
|
|
15
|
-
GuildAllowUsers?: string[];
|
|
16
|
-
GuildMinMemberCount?: number;
|
|
17
|
-
GuildMaxCapacity?: number;
|
|
18
|
-
manualTimeout?: number;
|
|
19
|
-
manualTimeoutAction?: 'accept' | 'reject';
|
|
20
21
|
}
|
|
21
22
|
export declare const Config: Schema<Config>;
|
|
22
23
|
export declare function apply(ctx: Context, config?: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -40,267 +40,231 @@ var usage = `
|
|
|
40
40
|
<p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
|
|
41
41
|
</div>
|
|
42
42
|
`;
|
|
43
|
-
var Config = import_koishi.Schema.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
import_koishi.Schema.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
43
|
+
var Config = import_koishi.Schema.intersect([
|
|
44
|
+
import_koishi.Schema.object({
|
|
45
|
+
notifyTarget: import_koishi.Schema.string().description("通知目标(guild/private:number)").required(),
|
|
46
|
+
notifyAuto: import_koishi.Schema.boolean().description("发送全部通知").default(true),
|
|
47
|
+
debugMode: import_koishi.Schema.boolean().description("输出调试日志").default(false)
|
|
48
|
+
}).description("基础配置"),
|
|
49
|
+
import_koishi.Schema.object({
|
|
50
|
+
timeout: import_koishi.Schema.number().description("请求超时时长").default(360).min(0),
|
|
51
|
+
timeoutAction: import_koishi.Schema.union([
|
|
52
|
+
import_koishi.Schema.const("accept").description("同意"),
|
|
53
|
+
import_koishi.Schema.const("reject").description("拒绝")
|
|
54
|
+
]).description("默认超时操作").default("accept"),
|
|
55
|
+
friendLevel: import_koishi.Schema.number().description("最低好友等级").default(-1).min(-1).max(256),
|
|
56
|
+
friendRegex: import_koishi.Schema.string().description("好友验证正则"),
|
|
57
|
+
minMembers: import_koishi.Schema.number().description("最低群成员数").default(-1).min(-1).max(3e3),
|
|
58
|
+
maxCapacity: import_koishi.Schema.number().description("最低受邀容量").default(-1).min(-1).max(3e3)
|
|
59
|
+
}).description("好友邀群配置"),
|
|
60
|
+
import_koishi.Schema.object({
|
|
61
|
+
verifyMode: import_koishi.Schema.union([
|
|
62
|
+
import_koishi.Schema.const("strict").description("规则"),
|
|
63
|
+
import_koishi.Schema.const("assist").description("超时"),
|
|
64
|
+
import_koishi.Schema.const("manual").description("手动")
|
|
65
|
+
]).description("处理模式").default("assist"),
|
|
66
|
+
verifyRules: import_koishi.Schema.array(import_koishi.Schema.object({
|
|
67
|
+
guildId: import_koishi.Schema.string().description("群号").required(),
|
|
68
|
+
keyword: import_koishi.Schema.string().description("正则"),
|
|
69
|
+
minLevel: import_koishi.Schema.number().description("等级").default(-1),
|
|
70
|
+
groupAction: import_koishi.Schema.union([
|
|
71
|
+
import_koishi.Schema.const("accept").description("同意"),
|
|
72
|
+
import_koishi.Schema.const("reject").description("拒绝")
|
|
73
|
+
]).description("操作")
|
|
74
|
+
})).description("加群验证配置").role("table")
|
|
75
|
+
}).description("加群请求配置")
|
|
76
|
+
]);
|
|
71
77
|
function apply(ctx, config = {}) {
|
|
72
78
|
const logger = new import_koishi.Logger("onebot-verifier");
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const activeRequests = /* @__PURE__ */ new Map();
|
|
76
|
-
const processedFlags = /* @__PURE__ */ new Set();
|
|
77
|
-
const cleanupActiveRequest = /* @__PURE__ */ __name((requestKey) => {
|
|
78
|
-
const activeRequest = activeRequests.get(requestKey);
|
|
79
|
-
if (!activeRequest) return;
|
|
80
|
-
activeRequest.disposer?.();
|
|
81
|
-
if (activeRequest.timeoutTimer) clearTimeout(activeRequest.timeoutTimer);
|
|
82
|
-
requestNumberMap.delete(activeRequest.requestNumber);
|
|
83
|
-
activeRequests.delete(requestKey);
|
|
84
|
-
}, "cleanupActiveRequest");
|
|
85
|
-
const processRequestAction = /* @__PURE__ */ __name(async (session, type, approve, reason = "", remark = "") => {
|
|
79
|
+
const activeTasks = /* @__PURE__ */ new Map();
|
|
80
|
+
const executeAction = /* @__PURE__ */ __name(async (session, kind, pass, reason = "", remark = "") => {
|
|
86
81
|
try {
|
|
87
82
|
const eventData = session.event?._data || {};
|
|
88
|
-
if (!
|
|
83
|
+
if (!pass && kind === "guild" && session.guildId && (session.event?.type === "guild-added" || eventData.notice_type === "group_increase")) {
|
|
89
84
|
if (reason) {
|
|
90
85
|
try {
|
|
91
|
-
await session.bot.sendMessage(session.guildId,
|
|
92
|
-
} catch (
|
|
93
|
-
logger.warn(`发送退群通知失败: ${
|
|
86
|
+
await session.bot.sendMessage(session.guildId, `${reason},将退出该群`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.warn(`发送退群通知失败: ${error}`);
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return true;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
logger.error(`退出群组 ${session.guildId} 失败: ${e}`);
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
91
|
+
await session.onebot?.setGroupLeave(session.guildId, false);
|
|
92
|
+
return true;
|
|
103
93
|
}
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
else await session.onebot.setGroupAddRequest(flag, eventData.sub_type ?? "add", approve, approve ? "" : reason);
|
|
94
|
+
if (!eventData.flag || !session.onebot) return false;
|
|
95
|
+
if (kind === "friend") await session.onebot.setFriendAddRequest(eventData.flag, pass, remark);
|
|
96
|
+
else await session.onebot.setGroupAddRequest(eventData.flag, eventData.sub_type ?? "add", pass, pass ? "" : reason);
|
|
108
97
|
return true;
|
|
109
|
-
} catch (
|
|
110
|
-
logger.error(
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger.error(`操作失败: ${error}`);
|
|
111
100
|
return false;
|
|
112
101
|
}
|
|
113
|
-
}, "
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
const [targetType, targetId] =
|
|
118
|
-
if (!targetId || targetType !== "guild" && targetType !== "private") return;
|
|
102
|
+
}, "executeAction");
|
|
103
|
+
const sendNotice = /* @__PURE__ */ __name(async (session, kind, status = "waiting") => {
|
|
104
|
+
const notifyConfig = config.notifyTarget || "";
|
|
105
|
+
if (!notifyConfig) return [];
|
|
106
|
+
const [targetType, targetId] = notifyConfig.split(":");
|
|
107
|
+
if (!targetId || targetType !== "guild" && targetType !== "private") return [];
|
|
108
|
+
if (status !== "waiting" && !config.notifyAuto) return [];
|
|
119
109
|
try {
|
|
120
110
|
const eventData = session.event?._data || {};
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await sendFunc(`请回复以下命令处理请求 #${details.requestNumber}:
|
|
139
|
-
通过[y/ya]${details.requestNumber} [备注] | 拒绝[n/na]${details.requestNumber} [理由]`);
|
|
140
|
-
}
|
|
141
|
-
} catch (e) {
|
|
142
|
-
logger.error(`发送通知失败: ${e}`);
|
|
111
|
+
const userInfo = session.userId ? await session.bot.getUser?.(session.userId)?.catch(() => null) : null;
|
|
112
|
+
const groupInfo = kind !== "friend" && session.guildId ? await session.bot.getGuild?.(session.guildId)?.catch(() => null) : null;
|
|
113
|
+
const adminInfo = eventData.operator_id && session.userId && eventData.operator_id !== session.userId ? await session.bot.getUser?.(eventData.operator_id)?.catch(() => null) : null;
|
|
114
|
+
const infoLines = [];
|
|
115
|
+
if (userInfo?.avatar) infoLines.push(`<image url="${userInfo.avatar}"/>`);
|
|
116
|
+
const typeName = kind === "friend" ? "好友申请" : kind === "member" ? "加群请求" : "群组邀请";
|
|
117
|
+
const statusText = status === "auto_pass" ? "[自动通过]" : status === "auto_reject" ? "[自动拒绝]" : "[等待处理]";
|
|
118
|
+
infoLines.push(`类型:${typeName} ${statusText}`);
|
|
119
|
+
if (kind !== "guild" || session.userId !== session.selfId) infoLines.push(`用户:${userInfo?.name || session.userId}${session.userId ? `(${session.userId})` : ""}`);
|
|
120
|
+
if (adminInfo) infoLines.push(`管理:${adminInfo.name ? `${adminInfo.name}(${eventData.operator_id})` : eventData.operator_id}`);
|
|
121
|
+
if (groupInfo) infoLines.push(`群组:${groupInfo.name ? `${groupInfo.name}(${session.guildId})` : session.guildId}`);
|
|
122
|
+
if (eventData.comment) infoLines.push(`验证信息:${eventData.comment}`);
|
|
123
|
+
const content = infoLines.join("\n");
|
|
124
|
+
return await (targetType === "private" ? session.bot.sendPrivateMessage(targetId, content) : session.bot.sendMessage(targetId, content)) || [];
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error(`通知失败: ${error}`);
|
|
127
|
+
return [];
|
|
143
128
|
}
|
|
144
|
-
}, "
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
129
|
+
}, "sendNotice");
|
|
130
|
+
const checkCriteria = /* @__PURE__ */ __name(async (session, kind) => {
|
|
131
|
+
const rawText = session.event?._data?.comment || "";
|
|
132
|
+
const cleanLines = rawText.split(/[\r\n]+/).map((s) => s.trim()).filter((s) => /^(回答)[::]/i.test(s)).map((s) => s.replace(/^(回答)[::]\s*/i, ""));
|
|
133
|
+
const verifyText = cleanLines.length > 0 ? cleanLines.join("\n") : rawText;
|
|
134
|
+
const toRegex = /* @__PURE__ */ __name((text) => {
|
|
135
|
+
const match = text.match(/^\/(.+)\/([a-z]*)$/);
|
|
136
|
+
return match ? new RegExp(match[1], match[2]) : new RegExp(text, "i");
|
|
137
|
+
}, "toRegex");
|
|
138
|
+
if (kind === "member") {
|
|
139
|
+
const groupRule = config.verifyRules?.find((r) => r.guildId === session.guildId);
|
|
140
|
+
if (!groupRule) return false;
|
|
141
|
+
try {
|
|
142
|
+
if (groupRule.keyword && !toRegex(groupRule.keyword).test(verifyText)) return false;
|
|
143
|
+
const limitLevel = groupRule.minLevel ?? -1;
|
|
144
|
+
if (limitLevel >= 0 && session.onebot && session.userId) {
|
|
145
|
+
const stats = await session.onebot.getStrangerInfo(session.userId, true);
|
|
146
|
+
if ((stats.qqLevel ?? 0) < limitLevel) return `QQ 等级低于 ${limitLevel} 级`;
|
|
162
147
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const userInfo = await session.onebot.getStrangerInfo(session.userId, true);
|
|
166
|
-
if ((userInfo.qqLevel ?? 0) < rule.minLevel) return `QQ 等级低于${rule.minLevel}级`;
|
|
167
|
-
} catch (e) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
172
150
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if ((userInfo.qqLevel ?? 0) < config.FriendLevel) return `QQ 等级低于${config.FriendLevel}级`;
|
|
184
|
-
return true;
|
|
185
|
-
} catch (e) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (kind === "friend") {
|
|
154
|
+
try {
|
|
155
|
+
if (config.friendRegex && toRegex(config.friendRegex).test(verifyText)) return true;
|
|
156
|
+
const limitLevel = config.friendLevel ?? -1;
|
|
157
|
+
if (limitLevel >= 0 && session.onebot && session.userId) {
|
|
158
|
+
const stats = await session.onebot.getStrangerInfo(session.userId, true);
|
|
159
|
+
if ((stats.qqLevel ?? 0) < limitLevel) return `QQ 等级低于 ${limitLevel} 级`;
|
|
160
|
+
return true;
|
|
188
161
|
}
|
|
162
|
+
} catch {
|
|
189
163
|
return false;
|
|
190
164
|
}
|
|
191
|
-
|
|
192
|
-
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (kind === "guild") {
|
|
168
|
+
try {
|
|
169
|
+
const userData = session.userId ? await ctx.database.getUser(session.platform, session.userId) : null;
|
|
170
|
+
if (userData && userData.authority > 1) return true;
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
if (session.onebot && session.guildId && ((config.minMembers ?? -1) >= 0 || (config.maxCapacity ?? -1) >= 0)) {
|
|
193
174
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
175
|
+
const stats = await session.onebot.getGroupInfo(session.guildId, true);
|
|
176
|
+
if ((config.minMembers ?? -1) >= 0 && stats.member_count < (config.minMembers ?? 0)) return `群成员不足 ${config.minMembers} 人`;
|
|
177
|
+
if ((config.maxCapacity ?? -1) >= 0 && stats.max_member_count < (config.maxCapacity ?? 0)) return `群容量不足 ${config.maxCapacity} 人`;
|
|
178
|
+
return true;
|
|
198
179
|
} catch {
|
|
180
|
+
return false;
|
|
199
181
|
}
|
|
200
|
-
if ((config.GuildMinMemberCount ?? -1) >= 0 || (config.GuildMaxCapacity ?? -1) >= 0) {
|
|
201
|
-
if (session.onebot && session.guildId) {
|
|
202
|
-
try {
|
|
203
|
-
const info = await session.onebot.getGroupInfo(session.guildId, true);
|
|
204
|
-
if (config.GuildMinMemberCount >= 0 && info.member_count < config.GuildMinMemberCount) return `群成员不足${config.GuildMinMemberCount}人`;
|
|
205
|
-
if (config.GuildMaxCapacity >= 0 && info.max_member_count < config.GuildMaxCapacity) return `群容量不足${config.GuildMaxCapacity}`;
|
|
206
|
-
return true;
|
|
207
|
-
} catch (e) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return false;
|
|
213
182
|
}
|
|
183
|
+
return false;
|
|
214
184
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
if (!
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const send = tType === "private" ? (m) => session.bot.sendPrivateMessage(tId, m) : (m) => session.bot.sendMessage(tId, m);
|
|
232
|
-
await send(`请求 #${requestNumber} 超时,已自动${action === "accept" ? "通过" : "拒绝"}`);
|
|
185
|
+
return false;
|
|
186
|
+
}, "checkCriteria");
|
|
187
|
+
const setupManual = /* @__PURE__ */ __name(async (session, kind) => {
|
|
188
|
+
const msgIds = await sendNotice(session, kind, "waiting");
|
|
189
|
+
if (!msgIds?.length) return;
|
|
190
|
+
const task = { session, kind, messages: msgIds };
|
|
191
|
+
msgIds.forEach((id) => activeTasks.set(id, task));
|
|
192
|
+
const waitMinutes = config.timeout ?? 0;
|
|
193
|
+
if (waitMinutes > 0) {
|
|
194
|
+
task.timer = setTimeout(async () => {
|
|
195
|
+
if (!activeTasks.has(msgIds[0])) return;
|
|
196
|
+
msgIds.forEach((id) => activeTasks.delete(id));
|
|
197
|
+
let finalAction = config.timeoutAction;
|
|
198
|
+
if (kind === "member") {
|
|
199
|
+
const groupRule = config.verifyRules?.find((r) => r.guildId === session.guildId);
|
|
200
|
+
if (groupRule?.groupAction) finalAction = groupRule.groupAction;
|
|
233
201
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const bulkMatch = s.content.trim().match(/^(ya|na|全部同意|全部拒绝)\s*(.*)$/);
|
|
242
|
-
if (bulkMatch && activeRequests.size > 0) {
|
|
243
|
-
const reqs = [...activeRequests.values()];
|
|
244
|
-
activeRequests.clear();
|
|
245
|
-
requestNumberMap.clear();
|
|
246
|
-
const isApprove2 = bulkMatch[1] === "ya" || bulkMatch[1] === "全部同意";
|
|
247
|
-
const extra2 = bulkMatch[2]?.trim() || "";
|
|
248
|
-
for (const r of reqs) {
|
|
249
|
-
r.disposer?.();
|
|
250
|
-
if (r.timeoutTimer) clearTimeout(r.timeoutTimer);
|
|
251
|
-
await processRequestAction(r.session, r.type, isApprove2, !isApprove2 ? extra2 : "", isApprove2 && r.type === "friend" ? extra2 : "");
|
|
202
|
+
const isPass = finalAction === "accept";
|
|
203
|
+
await executeAction(session, kind, isPass, isPass ? "" : "等待人工超时,自动拒绝");
|
|
204
|
+
const notifyConfig = config.notifyTarget || "";
|
|
205
|
+
const [targetType, targetId] = notifyConfig.split(":");
|
|
206
|
+
if (targetId) {
|
|
207
|
+
const statusText = `已自动${isPass ? "通过" : "拒绝"}该请求`;
|
|
208
|
+
await (targetType === "private" ? session.bot.sendPrivateMessage(targetId, statusText) : session.bot.sendMessage(targetId, statusText));
|
|
252
209
|
}
|
|
253
|
-
|
|
254
|
-
await send2(`已${isApprove2 ? "通过" : "拒绝"} ${reqs.length} 个请求`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
const match = s.content.trim().match(new RegExp(`^(y|n|通过|拒绝)(${requestNumber})\\s*(.*)$`));
|
|
258
|
-
if (!match) return next();
|
|
259
|
-
cleanupActiveRequest(requestId);
|
|
260
|
-
const isApprove = match[1] === "y" || match[1] === "通过";
|
|
261
|
-
const extra = match[3]?.trim() || "";
|
|
262
|
-
const success = await processRequestAction(session, type, isApprove, !isApprove ? extra : "", isApprove && type === "friend" ? extra : "");
|
|
263
|
-
const send = targetType === "private" ? (m) => s.bot.sendPrivateMessage(targetId, m) : (m) => s.bot.sendMessage(targetId, m);
|
|
264
|
-
if (success) await send(`请求 #${requestNumber} 已${isApprove ? "通过" : "拒绝"}`);
|
|
265
|
-
else await send(`处理请求 #${requestNumber} 失败`);
|
|
266
|
-
});
|
|
267
|
-
}, "setupManualHandling");
|
|
268
|
-
const processRequest = /* @__PURE__ */ __name(async (session, type) => {
|
|
269
|
-
const flag = session.event?._data?.flag;
|
|
270
|
-
if (flag) {
|
|
271
|
-
if (processedFlags.has(flag)) return;
|
|
272
|
-
processedFlags.add(flag);
|
|
273
|
-
setTimeout(() => processedFlags.delete(flag), 6e4);
|
|
210
|
+
}, waitMinutes * 6e4);
|
|
274
211
|
}
|
|
275
|
-
|
|
276
|
-
|
|
212
|
+
}, "setupManual");
|
|
213
|
+
const handleEvent = /* @__PURE__ */ __name(async (session, kind) => {
|
|
277
214
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
215
|
+
if (kind === "member") {
|
|
216
|
+
if (config.verifyMode === "manual") {
|
|
217
|
+
await setupManual(session, kind);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (config.verifyMode === "strict") {
|
|
221
|
+
const matched = config.verifyRules?.some((r) => r.guildId === session.guildId);
|
|
222
|
+
if (!matched) return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const verdict = await checkCriteria(session, kind);
|
|
226
|
+
if (verdict === true) {
|
|
227
|
+
await executeAction(session, kind, true);
|
|
228
|
+
await sendNotice(session, kind, "auto_pass");
|
|
229
|
+
} else if (typeof verdict === "string") {
|
|
230
|
+
await executeAction(session, kind, false, verdict);
|
|
231
|
+
await sendNotice(session, kind, "auto_reject");
|
|
285
232
|
} else {
|
|
286
|
-
|
|
233
|
+
if (kind === "member" && config.verifyMode === "strict") return;
|
|
234
|
+
await setupManual(session, kind);
|
|
287
235
|
}
|
|
288
|
-
} catch (
|
|
289
|
-
logger.error(
|
|
236
|
+
} catch (error) {
|
|
237
|
+
logger.error(`处理失败: ${error}`);
|
|
290
238
|
}
|
|
291
|
-
}, "
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
239
|
+
}, "handleEvent");
|
|
240
|
+
const hookEvent = /* @__PURE__ */ __name((kind) => async (session) => {
|
|
241
|
+
const eventData = session.event?._data || {};
|
|
242
|
+
session.userId = eventData.user_id;
|
|
243
|
+
if (kind !== "friend") session.guildId = eventData.group_id;
|
|
244
|
+
await handleEvent(session, kind);
|
|
245
|
+
}, "hookEvent");
|
|
246
|
+
ctx.on("friend-request", hookEvent("friend"));
|
|
247
|
+
ctx.on("guild-request", hookEvent("guild"));
|
|
248
|
+
ctx.on("guild-member-request", hookEvent("member"));
|
|
249
|
+
ctx.on("guild-added", hookEvent("guild"));
|
|
250
|
+
ctx.middleware(async (session, next) => {
|
|
251
|
+
if (typeof session.content !== "string" || !session.quote?.id) return next();
|
|
252
|
+
const activeTask = activeTasks.get(session.quote.id);
|
|
253
|
+
if (!activeTask) return next();
|
|
254
|
+
const notifyConfig = config.notifyTarget || "";
|
|
255
|
+
const [targetType, targetId] = notifyConfig.split(":");
|
|
256
|
+
if (targetType === "private" ? session.userId !== targetId : session.guildId !== targetId) return next();
|
|
257
|
+
const input = session.content.replace(/<(quote|at)\s+[^>]*\/>/gi, "").trim();
|
|
258
|
+
const cmdMatch = input.match(/^(y|n|通过|拒绝)(?:\s+(.*))?$/);
|
|
259
|
+
if (!cmdMatch) return next();
|
|
260
|
+
if (activeTask.timer) clearTimeout(activeTask.timer);
|
|
261
|
+
activeTask.messages.forEach((msg) => activeTasks.delete(msg));
|
|
262
|
+
const isApprove = cmdMatch[1] === "y" || cmdMatch[1] === "通过";
|
|
263
|
+
const extraInfo = cmdMatch[2]?.trim() || "";
|
|
264
|
+
const isSuccess = await executeAction(activeTask.session, activeTask.kind, isApprove, isApprove ? "" : extraInfo, isApprove && activeTask.kind === "friend" ? extraInfo : "");
|
|
265
|
+
const replyText = isSuccess ? `已${isApprove ? "通过" : "拒绝"}该请求` : `处理请求失败`;
|
|
266
|
+
await (targetType === "private" ? session.bot.sendPrivateMessage(targetId, replyText) : session.bot.sendMessage(targetId, replyText));
|
|
267
|
+
});
|
|
304
268
|
}
|
|
305
269
|
__name(apply, "apply");
|
|
306
270
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,4 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/koishi-plugin-onebot-verifier)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**适用于 OneBot 协议的综合审核管理插件**。本插件支持自动处理好友申请、加群申请以及进群邀请,并提供灵活的人工审核通知与指令操作功能。
|
|
6
|
+
|
|
7
|
+
## ✨ 功能特性
|
|
8
|
+
|
|
9
|
+
- **多场景覆盖**:支持好友申请、他人加群、邀请机器人进群三种场景。
|
|
10
|
+
- **自动审核机制**:
|
|
11
|
+
- **等级校验**:根据 QQ 等级进行初步筛选。
|
|
12
|
+
- **正则匹配**:对验证信息进行关键词正则匹配。
|
|
13
|
+
- **群规模校验**:针对机器人被邀请进群,可设置最小成员数或最小群容量要求。
|
|
14
|
+
- **三种工作模式**:
|
|
15
|
+
- `strict`(规则模式):仅处理配置了规则的请求。
|
|
16
|
+
- `assist`(辅助模式):规则不匹配时转为人工审核。
|
|
17
|
+
- `manual`(手动模式):全人工介入。
|
|
18
|
+
- **交互式人工审核**:通过引用通知消息并回复 `y/n` 即可快速通过或拒绝申请。
|
|
19
|
+
- **超时自动处理**:防止人工审核积压,支持超时后自动通过或拒绝。
|
|
20
|
+
|
|
21
|
+
## ⚙️ 配置项说明
|
|
22
|
+
|
|
23
|
+
### 1. 基础配置
|
|
24
|
+
|
|
25
|
+
- **通知目标 (`notifyTarget`)**: 审核信息的推送位置。格式为 `private:QQ号` 或 `guild:群号`。
|
|
26
|
+
- **发送全部通知 (`notifyAuto`)**: 开启后,即便被插件自动通过/拒绝的请求也会发送通知。
|
|
27
|
+
- **调试模式 (`debugMode`)**: 开启后将输出更详细的日志。
|
|
28
|
+
|
|
29
|
+
### 2. 好友与邀群配置
|
|
30
|
+
|
|
31
|
+
- **请求超时时长 (`timeout`)**: 人工审核的等待时间(单位:分钟)。
|
|
32
|
+
- **默认超时操作 (`timeoutAction`)**: 超时未处理时执行的操作(同意/拒绝)。
|
|
33
|
+
- **最低好友等级 (`friendLevel`)**: 申请人 QQ 等级需达到的标准(-1 为不限制)。
|
|
34
|
+
- **好友验证正则 (`friendRegex`)**: 匹配好友申请时的验证信息。
|
|
35
|
+
- **最低群成员数 (`minMembers`)**: 机器人被邀请进群时,该群的现有人数要求。
|
|
36
|
+
- **最低受邀容量 (`maxCapacity`)**: 机器人被邀请进群时,该群的群上限人数要求。
|
|
37
|
+
|
|
38
|
+
### 3. 加群请求配置 (用户加群)
|
|
39
|
+
|
|
40
|
+
- **处理模式 (`verifyMode`)**:
|
|
41
|
+
- `规则`: 仅处理在下方“加群验证配置”中列出的群。
|
|
42
|
+
- `超时`: 匹配不到规则时,发送人工审核,超时后执行默认操作。
|
|
43
|
+
- `手动`: 所有申请直接进入人工审核。
|
|
44
|
+
- **加群验证配置 (`verifyRules`)**:
|
|
45
|
+
- 为特定群设置专属的验证正则、最低等级要求及超时处理动作。
|
|
46
|
+
|
|
47
|
+
## 🎮 使用方法
|
|
48
|
+
|
|
49
|
+
### 人工审核交互
|
|
50
|
+
|
|
51
|
+
当有新的申请触发人工审核时,插件会向 `notifyTarget` 发送通知。你只需**引用该通知消息**并回复:
|
|
52
|
+
|
|
53
|
+
- **通过申请**:回复 `y` 或 `通过`。
|
|
54
|
+
- *提示:若是好友申请,后面可跟备注(如:`y 朋友`)。*
|
|
55
|
+
- **拒绝申请**:回复 `n` 或 `拒绝`。
|
|
56
|
+
- *提示:后面可以跟拒绝理由(如:`n 等级太低`)。*
|
|
57
|
+
|
|
58
|
+
### 自动处理逻辑
|
|
59
|
+
|
|
60
|
+
1. **好友申请**:先匹配正则,再校验等级。
|
|
61
|
+
2. **机器人进群**:管理员(权限 > 1)邀请直接通过;普通用户邀请则校验成员数和群容量。
|
|
62
|
+
3. **用户进群**:按照 `verifyRules` 中的设置执行。若不满足条件,则根据 `verifyMode` 决定直接拒绝或转人工。
|
|
63
|
+
|
|
64
|
+
## 🛠️ 技术细节
|
|
65
|
+
|
|
66
|
+
- 插件会自动剥离 OneBot 验证信息中的 `回答:` 等前缀,直接提取核心答案进行正则匹配。
|
|
67
|
+
- 对于已经进入群组但被判定不符合条件的邀请,插件会自动发送通知并执行退群操作。
|