koishi-plugin-bind-bot 2.1.1 → 2.1.3
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/export-utils.js +13 -6
- package/lib/force-bind-utils.js +12 -8
- package/lib/handlers/binding.handler.js +63 -27
- package/lib/handlers/buid.handler.js +102 -45
- package/lib/handlers/group-request-review.handler.d.ts +97 -0
- package/lib/handlers/group-request-review.handler.js +582 -0
- package/lib/handlers/index.d.ts +1 -0
- package/lib/handlers/index.js +1 -0
- package/lib/handlers/lottery.handler.js +11 -9
- package/lib/handlers/mcid.handler.js +197 -86
- package/lib/handlers/tag.handler.js +86 -31
- package/lib/handlers/whitelist.handler.js +252 -77
- package/lib/index.js +282 -142
- package/lib/repositories/mcidbind.repository.js +2 -2
- package/lib/repositories/schedule-mute.repository.js +2 -2
- package/lib/services/api.service.js +27 -23
- package/lib/services/database.service.js +16 -14
- package/lib/services/nickname.service.js +12 -16
- package/lib/types/api.d.ts +90 -0
- package/lib/types/common.d.ts +45 -0
- package/lib/types/config.d.ts +88 -0
- package/lib/types/database.d.ts +50 -0
- package/lib/types/update-data.d.ts +83 -0
- package/lib/utils/error-utils.js +7 -8
- package/lib/utils/helpers.js +45 -7
- package/lib/utils/message-utils.js +36 -23
- package/lib/utils/session-manager.js +6 -1
- package/package.json +12 -2
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GroupRequestReviewHandler = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
const base_handler_1 = require("./base.handler");
|
|
6
|
+
/**
|
|
7
|
+
* 入群申请审批处理器
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* 该处理器实现了通过表情回应审批入群申请的功能:
|
|
11
|
+
* - 监听指定群的入群申请
|
|
12
|
+
* - 生成播报消息到管理群
|
|
13
|
+
* - 自动添加表情选项
|
|
14
|
+
* - 处理管理员的表情回应
|
|
15
|
+
* - 执行批准/拒绝操作
|
|
16
|
+
* - 支持自动绑定和交互式绑定
|
|
17
|
+
*/
|
|
18
|
+
class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
|
|
19
|
+
/** 待审批的申请记录 Map<broadcastMessageId, PendingRequest> */
|
|
20
|
+
pendingRequests = new Map();
|
|
21
|
+
/** 拒绝流程状态 Map<askMessageId, RejectFlow> */
|
|
22
|
+
rejectFlows = new Map();
|
|
23
|
+
/** 管理员权限缓存 Map<groupId, AdminCache> */
|
|
24
|
+
adminCache = new Map();
|
|
25
|
+
/** 用户进群等待列表 Map<userId, resolve> */
|
|
26
|
+
userJoinWaiters = new Map();
|
|
27
|
+
/** 配置 */
|
|
28
|
+
reviewConfig;
|
|
29
|
+
/**
|
|
30
|
+
* 注册事件监听和中间件
|
|
31
|
+
*/
|
|
32
|
+
register() {
|
|
33
|
+
// 检查功能是否启用
|
|
34
|
+
if (!this.config.groupRequestReview?.enabled) {
|
|
35
|
+
this.logger.info('入群审批', '功能未启用');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.reviewConfig = this.config.groupRequestReview;
|
|
39
|
+
this.logger.info('入群审批', '功能已启用');
|
|
40
|
+
this.logger.info('入群审批', `目标群: ${this.reviewConfig.targetGroupId}, 管理群: ${this.reviewConfig.reviewGroupId}`);
|
|
41
|
+
// 监听入群申请
|
|
42
|
+
this.ctx.on('guild-member-request', this.handleRequest.bind(this));
|
|
43
|
+
// 监听用户成功进群
|
|
44
|
+
this.ctx.on('guild-member-added', this.handleUserJoined.bind(this));
|
|
45
|
+
// 监听表情回应(NapCat扩展事件)
|
|
46
|
+
// 使用通用 'message' 事件监听,在 handleNotice 中过滤
|
|
47
|
+
this.ctx.on('message', this.handleNotice.bind(this));
|
|
48
|
+
// 中间件:处理拒绝理由
|
|
49
|
+
this.ctx.middleware(this.handleRejectReason.bind(this));
|
|
50
|
+
// 定时清理过期记录
|
|
51
|
+
this.ctx.setInterval(() => {
|
|
52
|
+
this.cleanupExpiredRecords();
|
|
53
|
+
}, 60 * 60 * 1000); // 每小时清理一次
|
|
54
|
+
this.logger.info('入群审批', '已注册所有事件监听器', true);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 处理入群申请事件
|
|
58
|
+
*/
|
|
59
|
+
async handleRequest(session) {
|
|
60
|
+
try {
|
|
61
|
+
// 只处理目标群的申请
|
|
62
|
+
if (session.guildId !== this.reviewConfig.targetGroupId) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const normalizedUserId = this.deps.normalizeQQId(session.userId);
|
|
66
|
+
this.logger.info('入群审批', `收到申请 - QQ: ${normalizedUserId}, 群: ${session.guildId}`);
|
|
67
|
+
// 获取申请人信息
|
|
68
|
+
const applicantInfo = await this.getApplicantInfo(session);
|
|
69
|
+
// 生成播报消息并发送到管理群
|
|
70
|
+
const broadcastMsgId = await this.sendBroadcastMessage(applicantInfo, session);
|
|
71
|
+
if (!broadcastMsgId) {
|
|
72
|
+
this.logger.error('入群审批', '播报消息发送失败');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// 保存待审批记录
|
|
76
|
+
const pendingReq = {
|
|
77
|
+
broadcastMessageId: broadcastMsgId,
|
|
78
|
+
requestFlag: session.messageId, // OneBot的请求标识
|
|
79
|
+
applicantQQ: normalizedUserId,
|
|
80
|
+
applicantNickname: applicantInfo.nickname,
|
|
81
|
+
applicantAvatar: applicantInfo.avatar,
|
|
82
|
+
targetGroupId: session.guildId,
|
|
83
|
+
answer: session.content || '',
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
status: 'pending'
|
|
86
|
+
};
|
|
87
|
+
this.pendingRequests.set(broadcastMsgId, pendingReq);
|
|
88
|
+
this.logger.info('入群审批', `已保存待审批记录 - 申请人: ${normalizedUserId}, 播报消息ID: ${broadcastMsgId}`, true);
|
|
89
|
+
// 自动添加表情回应选项
|
|
90
|
+
await this.addReactionOptions(broadcastMsgId, session.bot);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.logger.error('入群审批', `处理入群申请失败: ${error.message}`, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 处理通知事件(包括表情回应)
|
|
98
|
+
*/
|
|
99
|
+
async handleNotice(session) {
|
|
100
|
+
try {
|
|
101
|
+
// 只处理群表情回应事件
|
|
102
|
+
if (session.subtype !== 'group-msg-emoji-like') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// 只处理管理群的表情
|
|
106
|
+
if (session.guildId !== this.reviewConfig.reviewGroupId) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// 获取原始事件数据(使用类型断言访问 onebot 扩展属性)
|
|
110
|
+
const sessionAny = session;
|
|
111
|
+
if (!sessionAny.onebot || !sessionAny.onebot.likes) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const emojiData = sessionAny.onebot.likes;
|
|
115
|
+
const msgId = session.messageId;
|
|
116
|
+
const operatorId = this.deps.normalizeQQId(session.userId);
|
|
117
|
+
this.logger.debug('入群审批', `收到表情回应 - 消息: ${msgId}, 操作者: ${operatorId}, 表情数: ${emojiData.length}`);
|
|
118
|
+
// 检查是否是待审批的消息
|
|
119
|
+
const pendingReq = this.pendingRequests.get(msgId);
|
|
120
|
+
if (!pendingReq) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// 检查是否已处理
|
|
124
|
+
if (pendingReq.status !== 'pending') {
|
|
125
|
+
this.logger.info('入群审批', `申请已处理,状态: ${pendingReq.status}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// 检查操作者是否有管理权限
|
|
129
|
+
const hasPermission = await this.checkAdminPermission(operatorId, session.guildId, session.bot);
|
|
130
|
+
if (!hasPermission) {
|
|
131
|
+
this.logger.warn('入群审批', `权限不足 - 操作者: ${operatorId} 不是管理员`);
|
|
132
|
+
await this.deps.sendMessage(session, [koishi_1.h.text('⚠️ 只有管理员才能审批入群申请')]);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// 标记为处理中,防止重复操作
|
|
136
|
+
pendingReq.status = 'processing';
|
|
137
|
+
// 处理表情回应
|
|
138
|
+
await this.handleEmojiReaction(emojiData, pendingReq, operatorId, session);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.logger.error('入群审批', `处理表情回应失败: ${error.message}`, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 获取申请人信息
|
|
146
|
+
*/
|
|
147
|
+
async getApplicantInfo(session) {
|
|
148
|
+
const qq = this.deps.normalizeQQId(session.userId);
|
|
149
|
+
const answer = session.content || '(未填写)';
|
|
150
|
+
// 尝试获取用户信息
|
|
151
|
+
let nickname = qq;
|
|
152
|
+
let avatar = `http://q.qlogo.cn/headimg_dl?dst_uin=${qq}&spec=640`;
|
|
153
|
+
try {
|
|
154
|
+
// 使用 bot.getUser 获取用户信息(如果可用)
|
|
155
|
+
if (session.bot.getUser) {
|
|
156
|
+
const userInfo = await session.bot.getUser(qq);
|
|
157
|
+
if (userInfo.username) {
|
|
158
|
+
nickname = userInfo.username;
|
|
159
|
+
}
|
|
160
|
+
if (userInfo.avatar) {
|
|
161
|
+
avatar = userInfo.avatar;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.logger.warn('入群审批', `获取用户信息失败,使用默认值: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
return { qq, nickname, avatar, answer };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 发送播报消息到管理群
|
|
172
|
+
*/
|
|
173
|
+
async sendBroadcastMessage(applicantInfo, session) {
|
|
174
|
+
const { qq, nickname, avatar, answer } = applicantInfo;
|
|
175
|
+
const elements = [
|
|
176
|
+
koishi_1.h.text('📢 收到新的入群申请\n\n'),
|
|
177
|
+
koishi_1.h.image(avatar),
|
|
178
|
+
koishi_1.h.text(`\n👤 昵称:${nickname}\n`),
|
|
179
|
+
koishi_1.h.text(`🆔 QQ号:${qq}\n`),
|
|
180
|
+
koishi_1.h.text(`💬 回答:${answer}\n\n`),
|
|
181
|
+
koishi_1.h.text('━━━━━━━━━━━━━━━\n'),
|
|
182
|
+
koishi_1.h.text('请管理员点击表情回应:\n'),
|
|
183
|
+
koishi_1.h.text('👍 /太赞了 - 通过并自动绑定\n'),
|
|
184
|
+
koishi_1.h.text('😊 /偷感 - 通过并交互式绑定\n'),
|
|
185
|
+
koishi_1.h.text('❌ /NO - 拒绝申请')
|
|
186
|
+
];
|
|
187
|
+
try {
|
|
188
|
+
const result = await session.bot.sendMessage(this.reviewConfig.reviewGroupId, elements);
|
|
189
|
+
// result 通常是数组,第一个元素是消息ID
|
|
190
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
191
|
+
return result[0];
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
this.logger.error('入群审批', `发送播报消息失败: ${error.message}`, error);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 自动添加表情回应选项
|
|
202
|
+
*/
|
|
203
|
+
async addReactionOptions(messageId, bot) {
|
|
204
|
+
const emojis = [
|
|
205
|
+
this.reviewConfig.approveAutoBindEmoji,
|
|
206
|
+
this.reviewConfig.approveInteractiveBindEmoji,
|
|
207
|
+
this.reviewConfig.rejectEmoji
|
|
208
|
+
];
|
|
209
|
+
for (const emojiId of emojis) {
|
|
210
|
+
try {
|
|
211
|
+
await bot.internal.setMsgEmojiLike(messageId, emojiId);
|
|
212
|
+
this.logger.debug('入群审批', `已添加表情: ${emojiId}`);
|
|
213
|
+
// 避免频繁调用
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
this.logger.error('入群审批', `添加表情失败 - ID: ${emojiId}, 错误: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 处理表情回应动作
|
|
223
|
+
*/
|
|
224
|
+
async handleEmojiReaction(emojiData, pendingReq, operatorId, session) {
|
|
225
|
+
for (const emoji of emojiData) {
|
|
226
|
+
const emojiId = emoji.emoji_id;
|
|
227
|
+
if (emojiId === this.reviewConfig.approveAutoBindEmoji) {
|
|
228
|
+
// /太赞了 - 自动绑定
|
|
229
|
+
this.logger.info('入群审批', `执行自动绑定 - 申请人: ${pendingReq.applicantQQ}`);
|
|
230
|
+
await this.approveAndAutoBind(pendingReq, operatorId, session);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
else if (emojiId === this.reviewConfig.approveInteractiveBindEmoji) {
|
|
234
|
+
// /偷感 - 交互式绑定
|
|
235
|
+
this.logger.info('入群审批', `执行交互式绑定 - 申请人: ${pendingReq.applicantQQ}`);
|
|
236
|
+
await this.approveAndInteractiveBind(pendingReq, operatorId, session);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
else if (emojiId === this.reviewConfig.rejectEmoji) {
|
|
240
|
+
// /NO - 拒绝
|
|
241
|
+
this.logger.info('入群审批', `发起拒绝流程 - 申请人: ${pendingReq.applicantQQ}`);
|
|
242
|
+
await this.initRejectFlow(pendingReq, operatorId, session);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 批准并自动绑定
|
|
249
|
+
*/
|
|
250
|
+
async approveAndAutoBind(pendingReq, operatorId, session) {
|
|
251
|
+
try {
|
|
252
|
+
// 1. 批准入群
|
|
253
|
+
await session.bot.handleGuildMemberRequest(pendingReq.requestFlag, true, '欢迎加入!');
|
|
254
|
+
this.logger.info('入群审批', `已批准入群 - QQ: ${pendingReq.applicantQQ}`, true);
|
|
255
|
+
// 2. 等待用户进群
|
|
256
|
+
const joined = await this.waitForUserJoin(pendingReq.applicantQQ, pendingReq.targetGroupId, 10000);
|
|
257
|
+
if (!joined) {
|
|
258
|
+
await this.notifyAdmin(operatorId, session, `⚠️ 已批准 ${pendingReq.applicantQQ} 入群,但用户未在10秒内进群`);
|
|
259
|
+
pendingReq.status = 'approved';
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// 3. 解析UID
|
|
263
|
+
const uid = this.parseUID(pendingReq.answer);
|
|
264
|
+
if (!uid) {
|
|
265
|
+
await this.notifyAdmin(operatorId, session, `⚠️ 无法解析UID"${pendingReq.answer}",请手动处理\n申请人: ${pendingReq.applicantQQ}`);
|
|
266
|
+
pendingReq.status = 'approved';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.logger.info('入群审批', `开始自动绑定 - QQ: ${pendingReq.applicantQQ}, UID: ${uid}`);
|
|
270
|
+
// 4. 调用 BuidHandler 的绑定逻辑(需要从 handlers 获取)
|
|
271
|
+
// 注意:这里需要访问其他 handler,可能需要调整架构
|
|
272
|
+
// 暂时先记录日志,稍后实现具体绑定逻辑
|
|
273
|
+
await this.performAutoBind(pendingReq.applicantQQ, uid, session.bot);
|
|
274
|
+
// 5. 通知管理员
|
|
275
|
+
await this.notifyAdmin(operatorId, session, `✅ 已批准 ${pendingReq.applicantQQ} 入群并完成自动绑定\nUID: ${uid}`);
|
|
276
|
+
pendingReq.status = 'approved';
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.logger.error('入群审批', `自动绑定失败: ${error.message}`, error);
|
|
280
|
+
await this.notifyAdmin(operatorId, session, `❌ 操作失败:${error.message}`);
|
|
281
|
+
pendingReq.status = 'pending'; // 恢复状态
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 批准并启动交互式绑定
|
|
286
|
+
*/
|
|
287
|
+
async approveAndInteractiveBind(pendingReq, operatorId, session) {
|
|
288
|
+
try {
|
|
289
|
+
// 1. 批准入群
|
|
290
|
+
await session.bot.handleGuildMemberRequest(pendingReq.requestFlag, true, '欢迎加入!');
|
|
291
|
+
this.logger.info('入群审批', `已批准入群 - QQ: ${pendingReq.applicantQQ}`, true);
|
|
292
|
+
// 2. 等待用户进群
|
|
293
|
+
const joined = await this.waitForUserJoin(pendingReq.applicantQQ, pendingReq.targetGroupId, 10000);
|
|
294
|
+
if (joined) {
|
|
295
|
+
// 3. 用户进群后会自动触发 guild-member-added 事件
|
|
296
|
+
// 现有的入群欢迎流程会自动启动交互式绑定
|
|
297
|
+
await this.notifyAdmin(operatorId, session, `✅ 已批准 ${pendingReq.applicantQQ} 入群,交互式绑定已启动`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
await this.notifyAdmin(operatorId, session, `⚠️ 已批准但用户 ${pendingReq.applicantQQ} 未在10秒内进群`);
|
|
301
|
+
}
|
|
302
|
+
pendingReq.status = 'approved';
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
this.logger.error('入群审批', `交互式绑定失败: ${error.message}`, error);
|
|
306
|
+
await this.notifyAdmin(operatorId, session, `❌ 操作失败:${error.message}`);
|
|
307
|
+
pendingReq.status = 'pending';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 发起拒绝流程
|
|
312
|
+
*/
|
|
313
|
+
async initRejectFlow(pendingReq, operatorId, session) {
|
|
314
|
+
try {
|
|
315
|
+
// 发送询问消息
|
|
316
|
+
const askElements = [
|
|
317
|
+
koishi_1.h.text(`❓ 请回复拒绝理由(引用此消息回复)\n`),
|
|
318
|
+
koishi_1.h.text(`申请人:${pendingReq.applicantNickname}(${pendingReq.applicantQQ})`)
|
|
319
|
+
];
|
|
320
|
+
const askResult = await session.bot.sendMessage(session.channelId, askElements);
|
|
321
|
+
const askMsgId = Array.isArray(askResult) ? askResult[0] : null;
|
|
322
|
+
if (!askMsgId) {
|
|
323
|
+
throw new Error('发送询问消息失败');
|
|
324
|
+
}
|
|
325
|
+
// 保存拒绝流程状态
|
|
326
|
+
const rejectFlow = {
|
|
327
|
+
pendingRequest: pendingReq,
|
|
328
|
+
operatorId,
|
|
329
|
+
askMessageId: askMsgId,
|
|
330
|
+
timeout: Date.now() + 5 * 60 * 1000 // 5分钟超时
|
|
331
|
+
};
|
|
332
|
+
this.rejectFlows.set(askMsgId, rejectFlow);
|
|
333
|
+
this.logger.info('入群审批', `已发起拒绝流程 - 询问消息ID: ${askMsgId}`);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
this.logger.error('入群审批', `发起拒绝流程失败: ${error.message}`, error);
|
|
337
|
+
await this.notifyAdmin(operatorId, session, `❌ 发起拒绝流程失败:${error.message}`);
|
|
338
|
+
pendingReq.status = 'pending';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* 处理拒绝理由(中间件)
|
|
343
|
+
*/
|
|
344
|
+
async handleRejectReason(session, next) {
|
|
345
|
+
// 检查是否是引用消息
|
|
346
|
+
if (!session.quote) {
|
|
347
|
+
return next();
|
|
348
|
+
}
|
|
349
|
+
const rejectFlow = this.rejectFlows.get(session.quote.id);
|
|
350
|
+
if (!rejectFlow) {
|
|
351
|
+
return next();
|
|
352
|
+
}
|
|
353
|
+
// 检查是否是同一个管理员
|
|
354
|
+
const operatorId = this.deps.normalizeQQId(session.userId);
|
|
355
|
+
if (operatorId !== rejectFlow.operatorId) {
|
|
356
|
+
return '⚠️ 只有发起拒绝的管理员可以提供理由';
|
|
357
|
+
}
|
|
358
|
+
// 检查是否超时
|
|
359
|
+
if (Date.now() > rejectFlow.timeout) {
|
|
360
|
+
this.rejectFlows.delete(session.quote.id);
|
|
361
|
+
rejectFlow.pendingRequest.status = 'pending';
|
|
362
|
+
return '❌ 拒绝流程已超时,请重新操作';
|
|
363
|
+
}
|
|
364
|
+
// 执行拒绝
|
|
365
|
+
const reason = session.content;
|
|
366
|
+
const { pendingRequest } = rejectFlow;
|
|
367
|
+
try {
|
|
368
|
+
await session.bot.handleGuildMemberRequest(pendingRequest.requestFlag, false, reason);
|
|
369
|
+
this.logger.info('入群审批', `已拒绝入群 - QQ: ${pendingRequest.applicantQQ}, 理由: ${reason}`, true);
|
|
370
|
+
pendingRequest.status = 'rejected';
|
|
371
|
+
this.rejectFlows.delete(session.quote.id);
|
|
372
|
+
return `✅ 已拒绝 ${pendingRequest.applicantQQ} 的入群申请\n拒绝理由:${reason}`;
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
this.logger.error('入群审批', `拒绝入群失败: ${error.message}`, error);
|
|
376
|
+
pendingRequest.status = 'pending';
|
|
377
|
+
return `❌ 拒绝失败:${error.message}`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 处理用户成功进群事件
|
|
382
|
+
*/
|
|
383
|
+
handleUserJoined(session) {
|
|
384
|
+
const userId = this.deps.normalizeQQId(session.userId);
|
|
385
|
+
const waiter = this.userJoinWaiters.get(userId);
|
|
386
|
+
if (waiter) {
|
|
387
|
+
this.logger.debug('入群审批', `用户已进群 - QQ: ${userId}`);
|
|
388
|
+
waiter(true);
|
|
389
|
+
this.userJoinWaiters.delete(userId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 等待用户进群
|
|
394
|
+
*/
|
|
395
|
+
waitForUserJoin(userId, groupId, timeout) {
|
|
396
|
+
return new Promise(resolve => {
|
|
397
|
+
const timer = setTimeout(() => {
|
|
398
|
+
this.userJoinWaiters.delete(userId);
|
|
399
|
+
resolve(false);
|
|
400
|
+
}, timeout);
|
|
401
|
+
this.userJoinWaiters.set(userId, (joined) => {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
resolve(joined);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 检查管理员权限
|
|
409
|
+
*/
|
|
410
|
+
async checkAdminPermission(userId, groupId, bot) {
|
|
411
|
+
// 检查是否是 masterId
|
|
412
|
+
if (userId === this.config.masterId) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
// 先检查缓存
|
|
416
|
+
const cache = this.adminCache.get(groupId);
|
|
417
|
+
if (cache && Date.now() - cache.lastUpdate < 5 * 60 * 1000) {
|
|
418
|
+
return cache.admins.includes(userId);
|
|
419
|
+
}
|
|
420
|
+
// 调用 NapCat 扩展 API 获取群信息
|
|
421
|
+
try {
|
|
422
|
+
const groupInfo = await bot.internal.getGroupInfoEx(groupId);
|
|
423
|
+
const admins = (groupInfo.admins || []).map(String);
|
|
424
|
+
// 更新缓存
|
|
425
|
+
this.adminCache.set(groupId, {
|
|
426
|
+
admins,
|
|
427
|
+
lastUpdate: Date.now()
|
|
428
|
+
});
|
|
429
|
+
return admins.includes(userId);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
this.logger.error('入群审批', `获取管理员列表失败: ${error.message}`);
|
|
433
|
+
// 降级方案:只允许 masterId
|
|
434
|
+
return userId === this.config.masterId;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 解析UID(支持多种格式)
|
|
439
|
+
*/
|
|
440
|
+
parseUID(input) {
|
|
441
|
+
if (!input)
|
|
442
|
+
return null;
|
|
443
|
+
input = input.trim();
|
|
444
|
+
// 格式1: 纯数字
|
|
445
|
+
if (/^\d+$/.test(input)) {
|
|
446
|
+
return input;
|
|
447
|
+
}
|
|
448
|
+
// 格式2: UID:123456789
|
|
449
|
+
const uidMatch = input.match(/^UID:(\d+)$/i);
|
|
450
|
+
if (uidMatch) {
|
|
451
|
+
return uidMatch[1];
|
|
452
|
+
}
|
|
453
|
+
// 格式3: https://space.bilibili.com/123456789
|
|
454
|
+
const urlMatch = input.match(/space\.bilibili\.com\/(\d+)/);
|
|
455
|
+
if (urlMatch) {
|
|
456
|
+
return urlMatch[1];
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* 通知管理员
|
|
462
|
+
*/
|
|
463
|
+
async notifyAdmin(operatorId, session, message) {
|
|
464
|
+
try {
|
|
465
|
+
const elements = [koishi_1.h.at(operatorId), koishi_1.h.text(' '), koishi_1.h.text(message)];
|
|
466
|
+
await session.bot.sendMessage(session.channelId, elements);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
this.logger.error('入群审批', `通知管理员失败: ${error.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* 执行自动绑定
|
|
474
|
+
*/
|
|
475
|
+
async performAutoBind(qq, uid, bot) {
|
|
476
|
+
const axios = require('axios');
|
|
477
|
+
try {
|
|
478
|
+
// 1. 验证 UID
|
|
479
|
+
this.logger.debug('入群审批', `验证 B站 UID: ${uid}`);
|
|
480
|
+
const response = await axios.get(`${this.config.zminfoApiUrl}/api/user/${uid}`, {
|
|
481
|
+
timeout: 10000,
|
|
482
|
+
headers: {
|
|
483
|
+
'User-Agent': 'Koishi-MCID-Bot/1.0'
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
if (response.status !== 200 || !response.data || !response.data.uid) {
|
|
487
|
+
throw new Error(`无法验证B站UID: ${uid}`);
|
|
488
|
+
}
|
|
489
|
+
const buidUser = response.data;
|
|
490
|
+
// 2. 检查是否已被其他人绑定
|
|
491
|
+
const existingBind = await this.repos.mcidbind.findByBuidUid(uid);
|
|
492
|
+
if (existingBind && existingBind.qqId !== qq) {
|
|
493
|
+
throw new Error(`UID ${uid} 已被其他用户绑定`);
|
|
494
|
+
}
|
|
495
|
+
// 3. 获取或创建绑定记录
|
|
496
|
+
let bind = await this.repos.mcidbind.findByQQId(qq);
|
|
497
|
+
if (!bind) {
|
|
498
|
+
// 创建新绑定(使用临时MC用户名)
|
|
499
|
+
const tempMcUsername = `_temp_${Date.now()}`;
|
|
500
|
+
bind = await this.repos.mcidbind.create({
|
|
501
|
+
qqId: qq,
|
|
502
|
+
mcUsername: tempMcUsername,
|
|
503
|
+
mcUuid: '',
|
|
504
|
+
buidUid: buidUser.uid,
|
|
505
|
+
buidUsername: buidUser.username,
|
|
506
|
+
guardLevel: buidUser.guard_level || 0,
|
|
507
|
+
guardLevelText: buidUser.guard_level_text || '',
|
|
508
|
+
maxGuardLevel: buidUser.guard_level || 0,
|
|
509
|
+
maxGuardLevelText: buidUser.guard_level_text || '',
|
|
510
|
+
medalName: buidUser.medal?.name || '',
|
|
511
|
+
medalLevel: buidUser.medal?.level || 0,
|
|
512
|
+
wealthMedalLevel: buidUser.wealth_medal_level || 0,
|
|
513
|
+
lastActiveTime: new Date(),
|
|
514
|
+
lastModified: new Date()
|
|
515
|
+
});
|
|
516
|
+
this.logger.info('入群审批', `已创建新绑定 - QQ: ${qq}, UID: ${uid}`, true);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
// 更新现有绑定
|
|
520
|
+
await this.repos.mcidbind.update(qq, {
|
|
521
|
+
buidUid: buidUser.uid,
|
|
522
|
+
buidUsername: buidUser.username,
|
|
523
|
+
guardLevel: buidUser.guard_level || 0,
|
|
524
|
+
guardLevelText: buidUser.guard_level_text || '',
|
|
525
|
+
maxGuardLevel: Math.max(bind.maxGuardLevel || 0, buidUser.guard_level || 0),
|
|
526
|
+
maxGuardLevelText: buidUser.guard_level > (bind.maxGuardLevel || 0)
|
|
527
|
+
? buidUser.guard_level_text
|
|
528
|
+
: bind.maxGuardLevelText,
|
|
529
|
+
medalName: buidUser.medal?.name || '',
|
|
530
|
+
medalLevel: buidUser.medal?.level || 0,
|
|
531
|
+
wealthMedalLevel: buidUser.wealth_medal_level || 0,
|
|
532
|
+
lastActiveTime: new Date(),
|
|
533
|
+
lastModified: new Date()
|
|
534
|
+
});
|
|
535
|
+
this.logger.info('入群审批', `已更新绑定 - QQ: ${qq}, UID: ${uid}`, true);
|
|
536
|
+
}
|
|
537
|
+
// 4. 更新群昵称
|
|
538
|
+
try {
|
|
539
|
+
const groupId = this.reviewConfig.targetGroupId;
|
|
540
|
+
const nickname = `${buidUser.username}_${bind.mcUsername || 'MCID'}`;
|
|
541
|
+
await bot.internal.setGroupCard(groupId, qq, nickname);
|
|
542
|
+
this.logger.info('入群审批', `已更新群昵称 - QQ: ${qq}, 昵称: ${nickname}`);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
this.logger.warn('入群审批', `更新群昵称失败: ${error.message}`);
|
|
546
|
+
// 昵称更新失败不影响绑定
|
|
547
|
+
}
|
|
548
|
+
this.logger.info('入群审批', `自动绑定完成 - QQ: ${qq}, UID: ${uid}, 用户名: ${buidUser.username}`, true);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
this.logger.error('入群审批', `自动绑定失败: ${error.message}`, error);
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 定时清理过期记录
|
|
557
|
+
*/
|
|
558
|
+
cleanupExpiredRecords() {
|
|
559
|
+
const now = Date.now();
|
|
560
|
+
const cleanupThreshold = this.reviewConfig.autoCleanupHours * 60 * 60 * 1000;
|
|
561
|
+
// 清理过期的待审批记录
|
|
562
|
+
let cleanedPending = 0;
|
|
563
|
+
for (const [msgId, req] of this.pendingRequests.entries()) {
|
|
564
|
+
if (now - req.timestamp > cleanupThreshold) {
|
|
565
|
+
this.pendingRequests.delete(msgId);
|
|
566
|
+
cleanedPending++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// 清理过期的拒绝流程
|
|
570
|
+
let cleanedReject = 0;
|
|
571
|
+
for (const [askMsgId, flow] of this.rejectFlows.entries()) {
|
|
572
|
+
if (now > flow.timeout) {
|
|
573
|
+
this.rejectFlows.delete(askMsgId);
|
|
574
|
+
cleanedReject++;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (cleanedPending > 0 || cleanedReject > 0) {
|
|
578
|
+
this.logger.info('入群审批', `清理过期记录 - 待审批: ${cleanedPending}, 拒绝流程: ${cleanedReject}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
exports.GroupRequestReviewHandler = GroupRequestReviewHandler;
|
package/lib/handlers/index.d.ts
CHANGED
package/lib/handlers/index.js
CHANGED
|
@@ -21,3 +21,4 @@ __exportStar(require("./whitelist.handler"), exports);
|
|
|
21
21
|
__exportStar(require("./buid.handler"), exports);
|
|
22
22
|
__exportStar(require("./mcid.handler"), exports);
|
|
23
23
|
__exportStar(require("./lottery.handler"), exports);
|
|
24
|
+
__exportStar(require("./group-request-review.handler"), exports);
|
|
@@ -122,7 +122,7 @@ class LotteryHandler extends base_handler_1.BaseHandler {
|
|
|
122
122
|
second: '2-digit'
|
|
123
123
|
});
|
|
124
124
|
// 构建简化版群消息(去掉主播信息、统计信息和标签提示)
|
|
125
|
-
let groupMessage =
|
|
125
|
+
let groupMessage = '🎉 天选开奖结果通知\n\n';
|
|
126
126
|
groupMessage += `📅 开奖时间: ${lotteryTime}\n`;
|
|
127
127
|
groupMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
|
|
128
128
|
groupMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
|
|
@@ -131,10 +131,10 @@ class LotteryHandler extends base_handler_1.BaseHandler {
|
|
|
131
131
|
if (stats.notBoundCount > 0) {
|
|
132
132
|
groupMessage += `(其中${stats.notBoundCount}人未绑定跳过)`;
|
|
133
133
|
}
|
|
134
|
-
groupMessage +=
|
|
134
|
+
groupMessage += '\n\n';
|
|
135
135
|
// 如果有匹配的用户,显示详细信息
|
|
136
136
|
if (stats.matchedUsers.length > 0) {
|
|
137
|
-
groupMessage +=
|
|
137
|
+
groupMessage += '🎯 已绑定的中奖用户:\n';
|
|
138
138
|
// 限制显示前10个用户,避免消息过长
|
|
139
139
|
const displayUsers = stats.matchedUsers.slice(0, 10);
|
|
140
140
|
for (let i = 0; i < displayUsers.length; i++) {
|
|
@@ -150,10 +150,10 @@ class LotteryHandler extends base_handler_1.BaseHandler {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
|
-
groupMessage +=
|
|
153
|
+
groupMessage += '😔 暂无已绑定用户中奖\n';
|
|
154
154
|
}
|
|
155
155
|
// 构建完整版私聊消息(包含所有信息和未绑定用户)
|
|
156
|
-
let privateMessage =
|
|
156
|
+
let privateMessage = '🎉 天选开奖结果通知\n\n';
|
|
157
157
|
privateMessage += `📅 开奖时间: ${lotteryTime}\n`;
|
|
158
158
|
privateMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
|
|
159
159
|
privateMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
|
|
@@ -161,7 +161,7 @@ class LotteryHandler extends base_handler_1.BaseHandler {
|
|
|
161
161
|
privateMessage += `👤 主播: ${lotteryData.host_username} (UID: ${lotteryData.host_uid})\n`;
|
|
162
162
|
privateMessage += `🏠 房间号: ${lotteryData.room_id}\n\n`;
|
|
163
163
|
// 统计信息
|
|
164
|
-
privateMessage +=
|
|
164
|
+
privateMessage += '📈 处理统计:\n';
|
|
165
165
|
privateMessage += `• 总中奖人数: ${stats.totalWinners}人\n`;
|
|
166
166
|
privateMessage += `• 已绑定用户: ${stats.matchedCount}人 ✅\n`;
|
|
167
167
|
privateMessage += `• 未绑定用户: ${stats.notBoundCount}人 ⚠️\n`;
|
|
@@ -169,20 +169,22 @@ class LotteryHandler extends base_handler_1.BaseHandler {
|
|
|
169
169
|
privateMessage += `• 已有标签: ${stats.tagExistedCount}人\n\n`;
|
|
170
170
|
// 显示所有中奖用户(包括未绑定的)
|
|
171
171
|
if (lotteryData.winners.length > 0) {
|
|
172
|
-
privateMessage +=
|
|
172
|
+
privateMessage += '🎯 所有中奖用户:\n';
|
|
173
173
|
for (let i = 0; i < lotteryData.winners.length; i++) {
|
|
174
174
|
const winner = lotteryData.winners[i];
|
|
175
175
|
const index = i + 1;
|
|
176
176
|
// 查找对应的绑定用户
|
|
177
177
|
const matchedUser = stats.matchedUsers.find(user => user.uid === winner.uid);
|
|
178
178
|
if (matchedUser) {
|
|
179
|
-
const displayMcName = matchedUser.mcUsername && !matchedUser.mcUsername.startsWith('_temp_')
|
|
179
|
+
const displayMcName = matchedUser.mcUsername && !matchedUser.mcUsername.startsWith('_temp_')
|
|
180
|
+
? matchedUser.mcUsername
|
|
181
|
+
: '未绑定';
|
|
180
182
|
privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
|
|
181
183
|
privateMessage += ` QQ: ${matchedUser.qqId} | MC: ${displayMcName}\n`;
|
|
182
184
|
}
|
|
183
185
|
else {
|
|
184
186
|
privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
|
|
185
|
-
privateMessage +=
|
|
187
|
+
privateMessage += ' 无绑定信息,自动跳过\n';
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
privateMessage += `\n🏷️ 标签"${stats.tagName}"已自动添加到已绑定用户\n`;
|