koishi-plugin-bind-bot 2.1.2 → 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/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/index.js +22 -0
- package/lib/services/nickname.service.js +2 -6
- package/lib/types/common.d.ts +45 -0
- package/lib/types/config.d.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BaseHandler } from './base.handler';
|
|
2
|
+
/**
|
|
3
|
+
* 入群申请审批处理器
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* 该处理器实现了通过表情回应审批入群申请的功能:
|
|
7
|
+
* - 监听指定群的入群申请
|
|
8
|
+
* - 生成播报消息到管理群
|
|
9
|
+
* - 自动添加表情选项
|
|
10
|
+
* - 处理管理员的表情回应
|
|
11
|
+
* - 执行批准/拒绝操作
|
|
12
|
+
* - 支持自动绑定和交互式绑定
|
|
13
|
+
*/
|
|
14
|
+
export declare class GroupRequestReviewHandler extends BaseHandler {
|
|
15
|
+
/** 待审批的申请记录 Map<broadcastMessageId, PendingRequest> */
|
|
16
|
+
private pendingRequests;
|
|
17
|
+
/** 拒绝流程状态 Map<askMessageId, RejectFlow> */
|
|
18
|
+
private rejectFlows;
|
|
19
|
+
/** 管理员权限缓存 Map<groupId, AdminCache> */
|
|
20
|
+
private adminCache;
|
|
21
|
+
/** 用户进群等待列表 Map<userId, resolve> */
|
|
22
|
+
private userJoinWaiters;
|
|
23
|
+
/** 配置 */
|
|
24
|
+
private reviewConfig;
|
|
25
|
+
/**
|
|
26
|
+
* 注册事件监听和中间件
|
|
27
|
+
*/
|
|
28
|
+
register(): void;
|
|
29
|
+
/**
|
|
30
|
+
* 处理入群申请事件
|
|
31
|
+
*/
|
|
32
|
+
private handleRequest;
|
|
33
|
+
/**
|
|
34
|
+
* 处理通知事件(包括表情回应)
|
|
35
|
+
*/
|
|
36
|
+
private handleNotice;
|
|
37
|
+
/**
|
|
38
|
+
* 获取申请人信息
|
|
39
|
+
*/
|
|
40
|
+
private getApplicantInfo;
|
|
41
|
+
/**
|
|
42
|
+
* 发送播报消息到管理群
|
|
43
|
+
*/
|
|
44
|
+
private sendBroadcastMessage;
|
|
45
|
+
/**
|
|
46
|
+
* 自动添加表情回应选项
|
|
47
|
+
*/
|
|
48
|
+
private addReactionOptions;
|
|
49
|
+
/**
|
|
50
|
+
* 处理表情回应动作
|
|
51
|
+
*/
|
|
52
|
+
private handleEmojiReaction;
|
|
53
|
+
/**
|
|
54
|
+
* 批准并自动绑定
|
|
55
|
+
*/
|
|
56
|
+
private approveAndAutoBind;
|
|
57
|
+
/**
|
|
58
|
+
* 批准并启动交互式绑定
|
|
59
|
+
*/
|
|
60
|
+
private approveAndInteractiveBind;
|
|
61
|
+
/**
|
|
62
|
+
* 发起拒绝流程
|
|
63
|
+
*/
|
|
64
|
+
private initRejectFlow;
|
|
65
|
+
/**
|
|
66
|
+
* 处理拒绝理由(中间件)
|
|
67
|
+
*/
|
|
68
|
+
private handleRejectReason;
|
|
69
|
+
/**
|
|
70
|
+
* 处理用户成功进群事件
|
|
71
|
+
*/
|
|
72
|
+
private handleUserJoined;
|
|
73
|
+
/**
|
|
74
|
+
* 等待用户进群
|
|
75
|
+
*/
|
|
76
|
+
private waitForUserJoin;
|
|
77
|
+
/**
|
|
78
|
+
* 检查管理员权限
|
|
79
|
+
*/
|
|
80
|
+
private checkAdminPermission;
|
|
81
|
+
/**
|
|
82
|
+
* 解析UID(支持多种格式)
|
|
83
|
+
*/
|
|
84
|
+
private parseUID;
|
|
85
|
+
/**
|
|
86
|
+
* 通知管理员
|
|
87
|
+
*/
|
|
88
|
+
private notifyAdmin;
|
|
89
|
+
/**
|
|
90
|
+
* 执行自动绑定
|
|
91
|
+
*/
|
|
92
|
+
private performAutoBind;
|
|
93
|
+
/**
|
|
94
|
+
* 定时清理过期记录
|
|
95
|
+
*/
|
|
96
|
+
private cleanupExpiredRecords;
|
|
97
|
+
}
|
|
@@ -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);
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,25 @@ exports.Config = koishi_1.Schema.object({
|
|
|
52
52
|
forceBindTargetUpUid: koishi_1.Schema.number().description('强制绑定目标UP主UID').default(686127),
|
|
53
53
|
forceBindTargetRoomId: koishi_1.Schema.number().description('强制绑定目标房间号').default(544853),
|
|
54
54
|
forceBindTargetMedalName: koishi_1.Schema.string().description('强制绑定目标粉丝牌名称').default('生态'),
|
|
55
|
+
groupRequestReview: koishi_1.Schema.object({
|
|
56
|
+
enabled: koishi_1.Schema.boolean().description('是否启用入群申请审批功能').default(false),
|
|
57
|
+
targetGroupId: koishi_1.Schema.string()
|
|
58
|
+
.description('需要审批的目标群ID(入群申请来源群)')
|
|
59
|
+
.default('931805503'),
|
|
60
|
+
reviewGroupId: koishi_1.Schema.string()
|
|
61
|
+
.description('管理员审批操作所在的群ID(播报群)')
|
|
62
|
+
.default('290238092'),
|
|
63
|
+
approveAutoBindEmoji: koishi_1.Schema.string()
|
|
64
|
+
.description('批准并自动绑定的表情ID(/太赞了)')
|
|
65
|
+
.default('389'),
|
|
66
|
+
approveInteractiveBindEmoji: koishi_1.Schema.string()
|
|
67
|
+
.description('批准并交互式绑定的表情ID(/偷感)')
|
|
68
|
+
.default('427'),
|
|
69
|
+
rejectEmoji: koishi_1.Schema.string().description('拒绝申请的表情ID(/NO)').default('123'),
|
|
70
|
+
autoCleanupHours: koishi_1.Schema.number()
|
|
71
|
+
.description('待审批记录自动清理时间(小时)')
|
|
72
|
+
.default(24)
|
|
73
|
+
}).description('入群申请审批功能配置'),
|
|
55
74
|
servers: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
56
75
|
id: koishi_1.Schema.string().description('服务器唯一ID(不允许重复)').required(),
|
|
57
76
|
name: koishi_1.Schema.string().description('服务器名称(用于指令显示)').required(),
|
|
@@ -1131,6 +1150,9 @@ function apply(ctx, config) {
|
|
|
1131
1150
|
// 实例化McidCommandHandler并注册命令
|
|
1132
1151
|
const mcidHandler = new handlers_1.McidCommandHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1133
1152
|
mcidHandler.register();
|
|
1153
|
+
// 实例化并注册入群申请审批Handler
|
|
1154
|
+
const groupRequestReviewHandler = new handlers_1.GroupRequestReviewHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1155
|
+
groupRequestReviewHandler.register();
|
|
1134
1156
|
// 自定义文本前缀匹配
|
|
1135
1157
|
if (config.allowTextPrefix && config.botNickname) {
|
|
1136
1158
|
// 创建一个前缀匹配器
|
|
@@ -189,16 +189,12 @@ class NicknameService {
|
|
|
189
189
|
const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
190
190
|
const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
|
|
191
191
|
this.logger.debug('群昵称设置', `当前昵称: "${currentNickname}"`);
|
|
192
|
-
//
|
|
192
|
+
// 【调试信息】提取当前昵称中的BUID用户名(仅用于日志)
|
|
193
193
|
if (buidUid && currentNickname) {
|
|
194
194
|
const currentNicknameUsername = (0, helpers_1.extractBuidUsernameFromNickname)(currentNickname);
|
|
195
195
|
this.logger.debug('群昵称设置', `[层4-群昵称] "${currentNicknameUsername || '(无法提取)'}"`);
|
|
196
|
-
if (currentNicknameUsername && currentNicknameUsername === latestBuidUsername) {
|
|
197
|
-
this.logger.info('群昵称设置', `✅ 群昵称已包含最新名称"${latestBuidUsername}",跳过修改`, true);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
196
|
}
|
|
201
|
-
//
|
|
197
|
+
// 如果昵称完全一致,跳过修改
|
|
202
198
|
if (currentNickname === targetNickname) {
|
|
203
199
|
this.logger.info('群昵称设置', `QQ(${normalizedUserId})群昵称已经是"${targetNickname}",跳过修改`, true);
|
|
204
200
|
return;
|
package/lib/types/common.d.ts
CHANGED
|
@@ -38,3 +38,48 @@ export interface LotteryResult {
|
|
|
38
38
|
host_uid: number;
|
|
39
39
|
host_username: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* 待审批的入群申请信息
|
|
43
|
+
*/
|
|
44
|
+
export interface PendingRequest {
|
|
45
|
+
/** 播报消息的ID(用于监听表情回应) */
|
|
46
|
+
broadcastMessageId: string;
|
|
47
|
+
/** OneBot的请求标识(用于批准/拒绝) */
|
|
48
|
+
requestFlag: string;
|
|
49
|
+
/** 申请人QQ号 */
|
|
50
|
+
applicantQQ: string;
|
|
51
|
+
/** 申请人昵称 */
|
|
52
|
+
applicantNickname: string;
|
|
53
|
+
/** 申请人头像URL */
|
|
54
|
+
applicantAvatar: string;
|
|
55
|
+
/** 目标群号 */
|
|
56
|
+
targetGroupId: string;
|
|
57
|
+
/** 申请人回答的内容(UID) */
|
|
58
|
+
answer: string;
|
|
59
|
+
/** 申请时间戳 */
|
|
60
|
+
timestamp: number;
|
|
61
|
+
/** 审批状态 */
|
|
62
|
+
status: 'pending' | 'approved' | 'rejected' | 'processing';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 拒绝流程状态
|
|
66
|
+
*/
|
|
67
|
+
export interface RejectFlow {
|
|
68
|
+
/** 对应的待审批申请 */
|
|
69
|
+
pendingRequest: PendingRequest;
|
|
70
|
+
/** 发起拒绝的管理员QQ号 */
|
|
71
|
+
operatorId: string;
|
|
72
|
+
/** 询问消息ID */
|
|
73
|
+
askMessageId: string;
|
|
74
|
+
/** 超时时间戳 */
|
|
75
|
+
timeout: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 管理员权限缓存
|
|
79
|
+
*/
|
|
80
|
+
export interface AdminCache {
|
|
81
|
+
/** 管理员QQ号列表 */
|
|
82
|
+
admins: string[];
|
|
83
|
+
/** 最后更新时间戳 */
|
|
84
|
+
lastUpdate: number;
|
|
85
|
+
}
|
package/lib/types/config.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface Config {
|
|
|
53
53
|
forceBindTargetRoomId: number;
|
|
54
54
|
/** 强制绑定目标粉丝牌名称 */
|
|
55
55
|
forceBindTargetMedalName: string;
|
|
56
|
+
/** 入群申请审批功能配置 */
|
|
57
|
+
groupRequestReview?: GroupRequestReviewConfig;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* 服务器配置接口
|
|
@@ -116,3 +118,28 @@ export interface ForceBindConfig {
|
|
|
116
118
|
targetMedalName: string;
|
|
117
119
|
debugMode: boolean;
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* 入群申请审批功能配置接口
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* 该功能允许通过表情回应来审批入群申请
|
|
126
|
+
* - 支持自动绑定 B站 UID
|
|
127
|
+
* - 支持交互式绑定流程
|
|
128
|
+
* - 支持拒绝申请并回传理由
|
|
129
|
+
*/
|
|
130
|
+
export interface GroupRequestReviewConfig {
|
|
131
|
+
/** 是否启用入群申请审批功能 */
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
/** 需要审批的目标群ID */
|
|
134
|
+
targetGroupId: string;
|
|
135
|
+
/** 管理员审批操作所在的群ID(播报群) */
|
|
136
|
+
reviewGroupId: string;
|
|
137
|
+
/** 批准并自动绑定的表情ID(默认:389 /太赞了) */
|
|
138
|
+
approveAutoBindEmoji: string;
|
|
139
|
+
/** 批准并交互式绑定的表情ID(默认:427 /偷感) */
|
|
140
|
+
approveInteractiveBindEmoji: string;
|
|
141
|
+
/** 拒绝申请的表情ID(默认:123 /NO) */
|
|
142
|
+
rejectEmoji: string;
|
|
143
|
+
/** 待审批记录自动清理时间(小时) */
|
|
144
|
+
autoCleanupHours: number;
|
|
145
|
+
}
|