@zhin.js/adapter-icqq 1.0.61 → 1.0.63

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/src/bot.ts ADDED
@@ -0,0 +1,496 @@
1
+ /**
2
+ * ICQQ Bot:继承 icqq Client,实现 zhin Bot 接口
3
+ */
4
+ import {
5
+ Client,
6
+ PrivateMessageEvent,
7
+ GroupMessageEvent,
8
+ Sendable,
9
+ MessageElem,
10
+ MemberInfo,
11
+ MemberIncreaseEvent,
12
+ MemberDecreaseEvent,
13
+ GroupRecallEvent,
14
+ GroupAdminEvent,
15
+ GroupMuteEvent,
16
+ GroupTransferEvent,
17
+ GroupPokeEvent,
18
+ FriendRecallEvent,
19
+ FriendPokeEvent,
20
+ FriendIncreaseEvent,
21
+ FriendRequestEvent,
22
+ GroupRequestEvent,
23
+ GroupInviteEvent,
24
+ } from "@icqqjs/icqq";
25
+ import path from "path";
26
+ import {
27
+ Bot,
28
+ Message,
29
+ SendOptions,
30
+ MessageSegment,
31
+ SendContent,
32
+ segment,
33
+ Notice,
34
+ Request,
35
+ } from "zhin.js";
36
+ import type { IcqqBotConfig, IcqqSenderInfo } from "./types.js";
37
+ import type { IcqqAdapter } from "./adapter.js";
38
+
39
+ export class IcqqBot
40
+ extends Client
41
+ implements Bot<IcqqBotConfig, PrivateMessageEvent | GroupMessageEvent>
42
+ {
43
+ $connected: boolean = false;
44
+ $config!: IcqqBotConfig;
45
+
46
+ get pluginLogger() {
47
+ return this.adapter.plugin.logger;
48
+ }
49
+
50
+ get $id() {
51
+ return this.$config.name;
52
+ }
53
+
54
+ constructor(public adapter: IcqqAdapter, config: IcqqBotConfig) {
55
+ if (!config.scope) config.scope = "icqqjs";
56
+ if (!config.data_dir) config.data_dir = path.join(process.cwd(), "data");
57
+ if (config.scope.startsWith("@")) config.scope = config.scope.slice(1);
58
+ super(config);
59
+ this.$config = config;
60
+ }
61
+
62
+ private handleIcqqMessage(
63
+ msg: PrivateMessageEvent | GroupMessageEvent,
64
+ ): void {
65
+ const message = this.$formatMessage(msg);
66
+ this.adapter.emit("message.receive", message);
67
+ this.pluginLogger.debug(
68
+ `${this.$config.name} recv ${message.$channel.type}(${
69
+ message.$channel.id
70
+ }):${segment.raw(message.$content)}`,
71
+ );
72
+ }
73
+
74
+ async $connect(): Promise<void> {
75
+ this.on("message", this.handleIcqqMessage.bind(this));
76
+ this.on("notice.group.increase", (e: MemberIncreaseEvent) => this.handleGroupNotice(e, 'group_member_increase', 'increase'));
77
+ this.on("notice.group.decrease", (e: MemberDecreaseEvent) => this.handleGroupNotice(e, 'group_member_decrease', 'decrease'));
78
+ this.on("notice.group.recall", (e: GroupRecallEvent) => this.handleGroupNotice(e, 'group_recall', 'recall'));
79
+ this.on("notice.group.admin", (e: GroupAdminEvent) => this.handleGroupNotice(e, 'group_admin_change', 'admin'));
80
+ this.on("notice.group.ban", (e: GroupMuteEvent) => this.handleGroupNotice(e, 'group_ban', 'ban'));
81
+ this.on("notice.group.transfer", (e: GroupTransferEvent) => this.handleGroupNotice(e, 'group_transfer', 'transfer'));
82
+ this.on("notice.group.poke", (e: GroupPokeEvent) => this.handleGroupNotice(e, 'group_poke', 'poke'));
83
+ this.on("notice.friend.increase", (e: FriendIncreaseEvent) => this.handleFriendNotice(e, 'friend_add', 'increase'));
84
+ this.on("notice.friend.recall", (e: FriendRecallEvent) => this.handleFriendNotice(e, 'friend_recall', 'recall'));
85
+ this.on("notice.friend.poke", (e: FriendPokeEvent) => this.handleFriendNotice(e, 'friend_poke', 'poke'));
86
+ this.on("request.friend.add", (e: FriendRequestEvent) => this.handleFriendRequest(e));
87
+ this.on("request.group.add", (e: GroupRequestEvent) => this.handleGroupRequest(e, 'group_add'));
88
+ this.on("request.group.invite", (e: GroupInviteEvent) => this.handleGroupRequest(e, 'group_invite'));
89
+ const root = this.adapter.plugin?.root;
90
+ const loginAssist = root?.inject?.('loginAssist' as any) as { waitForInput: (adapter: string, botId: string, type: string, payload?: Record<string, unknown>) => Promise<string | Record<string, unknown>> } | undefined;
91
+
92
+ this.on("system.login.device", async (e: unknown) => {
93
+ await this.sendSmsCode();
94
+ if (loginAssist) {
95
+ const value = await loginAssist.waitForInput('icqq', this.$config.name, 'device', { message: '请输入短信验证码' });
96
+ const code = typeof value === 'string' ? value : (value?.code as string) ?? '';
97
+ this.submitSmsCode(code.trim());
98
+ } else {
99
+ this.pluginLogger.info("请输入短信验证码:");
100
+ process.stdin.once("data", (data: Buffer) => {
101
+ this.submitSmsCode(data.toString().trim());
102
+ });
103
+ }
104
+ });
105
+ this.on("system.login.qrcode", async (e: any) => {
106
+ if (loginAssist) {
107
+ await loginAssist.waitForInput('icqq', this.$config.name, 'qrcode', {
108
+ message: '请扫码完成后在 Web 控制台或命令行确认',
109
+ image: e.image,
110
+ });
111
+ this.login();
112
+ } else {
113
+ this.pluginLogger.info(`取码地址:${e.image}\n请扫码完成后回车继续:`);
114
+ process.stdin.once("data", () => {
115
+ this.login();
116
+ });
117
+ }
118
+ });
119
+ this.on("system.login.slider", async (e: { url: string }) => {
120
+ if (loginAssist) {
121
+ const value = await loginAssist.waitForInput('icqq', this.$config.name, 'slider', {
122
+ message: '请输入滑块验证 ticket',
123
+ url: e.url,
124
+ });
125
+ const ticket = typeof value === 'string' ? value : (value?.ticket as string) ?? '';
126
+ this.submitSlider(ticket.trim());
127
+ } else {
128
+ this.pluginLogger.info(`取码地址:${e.url}\n请输入滑块验证ticket:`);
129
+ process.stdin.once("data", (data: Buffer) => {
130
+ this.submitSlider(data.toString().trim());
131
+ });
132
+ }
133
+ });
134
+ return new Promise((resolve) => {
135
+ this.once("system.online", () => {
136
+ this.$connected = true;
137
+ resolve();
138
+ });
139
+ this.login(Number(this.$config.name), this.$config.password);
140
+ });
141
+ }
142
+
143
+ async $disconnect(): Promise<void> {
144
+ await this.logout();
145
+ this.$connected = false;
146
+ }
147
+
148
+ $formatMessage(msg: PrivateMessageEvent | GroupMessageEvent) {
149
+ const senderInfo = this.getSenderInfo(msg);
150
+ const result = Message.from(msg, {
151
+ $id: msg.message_id.toString(),
152
+ $adapter: "icqq" as const,
153
+ $bot: `${this.$config.name}`,
154
+ $sender: senderInfo,
155
+ $channel: {
156
+ id:
157
+ msg.message_type === "group"
158
+ ? msg.group_id.toString()
159
+ : msg.from_id.toString(),
160
+ type: msg.message_type,
161
+ },
162
+ $content: IcqqBot.toSegments(msg.message),
163
+ $raw: msg.raw_message,
164
+ $timestamp: msg.time,
165
+ $recall: async () => {
166
+ await this.$recallMessage(result.$id);
167
+ },
168
+ $reply: async (
169
+ content: SendContent,
170
+ quote?: boolean | string,
171
+ ): Promise<string> => {
172
+ if (!Array.isArray(content)) content = [content];
173
+ if (quote)
174
+ content.unshift({
175
+ type: "reply",
176
+ data: { id: typeof quote === "boolean" ? result.$id : quote },
177
+ });
178
+ return await this.adapter.sendMessage({
179
+ ...result.$channel,
180
+ context: "icqq",
181
+ bot: `${this.uin}`,
182
+ content,
183
+ });
184
+ },
185
+ });
186
+ return result;
187
+ }
188
+
189
+ private getSenderInfo(
190
+ msg: PrivateMessageEvent | GroupMessageEvent,
191
+ ): IcqqSenderInfo {
192
+ const senderInfo: IcqqSenderInfo = {
193
+ id: msg.sender.user_id.toString(),
194
+ name: msg.sender.nickname.toString(),
195
+ };
196
+ if (msg.message_type === "group") {
197
+ const groupMsg = msg as GroupMessageEvent;
198
+ const sender = groupMsg.sender as any;
199
+ if (sender.role) {
200
+ senderInfo.role = sender.role;
201
+ senderInfo.isOwner = sender.role === "owner";
202
+ senderInfo.isAdmin = sender.role === "admin" || sender.role === "owner";
203
+ const perms: string[] = [];
204
+ if (sender.role === "owner") perms.push("owner", "admin");
205
+ else if (sender.role === "admin") perms.push("admin");
206
+ senderInfo.permissions = perms;
207
+ }
208
+ if (sender.card) senderInfo.card = sender.card;
209
+ if (sender.title) senderInfo.title = sender.title;
210
+ }
211
+ return senderInfo;
212
+ }
213
+
214
+ private handleGroupNotice(event: any, type: string, subType: string): void {
215
+ const notice = Notice.from(event, {
216
+ $id: `${event.time || Date.now()}_${type}_${event.group_id}`,
217
+ $adapter: 'icqq',
218
+ $bot: this.$config.name,
219
+ $type: type,
220
+ $subType: subType,
221
+ $channel: { id: event.group_id?.toString() || '', type: 'group' },
222
+ $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
223
+ $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : (event.target_id ? { id: event.target_id.toString(), name: event.target_id.toString() } : undefined),
224
+ $timestamp: event.time || Math.floor(Date.now() / 1000),
225
+ });
226
+ this.adapter.emit('notice.receive', notice);
227
+ }
228
+
229
+ private handleFriendNotice(event: any, type: string, subType: string): void {
230
+ const notice = Notice.from(event, {
231
+ $id: `${event.time || Date.now()}_${type}_${event.user_id}`,
232
+ $adapter: 'icqq',
233
+ $bot: this.$config.name,
234
+ $type: type,
235
+ $subType: subType,
236
+ $channel: { id: event.user_id?.toString() || '', type: 'private' },
237
+ $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
238
+ $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : undefined,
239
+ $timestamp: event.time || Math.floor(Date.now() / 1000),
240
+ });
241
+ this.adapter.emit('notice.receive', notice);
242
+ }
243
+
244
+ private handleFriendRequest(event: FriendRequestEvent): void {
245
+ const request = Request.from(event, {
246
+ $id: event.flag || `${event.time}_friend_add_${event.user_id}`,
247
+ $adapter: 'icqq',
248
+ $bot: this.$config.name,
249
+ $type: 'friend_add',
250
+ $subType: event.sub_type,
251
+ $channel: { id: event.user_id.toString(), type: 'private' },
252
+ $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
253
+ $comment: event.comment,
254
+ $timestamp: event.time || Math.floor(Date.now() / 1000),
255
+ $approve: async () => { await event.approve(true); },
256
+ $reject: async () => { await event.approve(false); },
257
+ });
258
+ this.adapter.emit('request.receive', request);
259
+ }
260
+
261
+ private handleGroupRequest(event: GroupRequestEvent | GroupInviteEvent, type: string): void {
262
+ const request = Request.from(event, {
263
+ $id: event.flag || `${event.time}_${type}_${event.user_id}`,
264
+ $adapter: 'icqq',
265
+ $bot: this.$config.name,
266
+ $type: type,
267
+ $subType: event.sub_type,
268
+ $channel: { id: event.group_id.toString(), type: 'group' },
269
+ $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
270
+ $comment: 'comment' in event ? event.comment : undefined,
271
+ $timestamp: event.time || Math.floor(Date.now() / 1000),
272
+ $approve: async () => { await event.approve(true); },
273
+ $reject: async () => { await event.approve(false); },
274
+ });
275
+ this.adapter.emit('request.receive', request);
276
+ }
277
+
278
+ async kickMember(groupId: number, userId: number, block?: boolean): Promise<boolean> {
279
+ try {
280
+ const group = this.pickGroup(groupId);
281
+ const result = await group.kickMember(userId, undefined, block);
282
+ this.pluginLogger.info(
283
+ `ICQQ Bot ${this.$id} 踢出成员 ${userId} 从群 ${groupId}${block ? "(已拉黑)" : ""}`,
284
+ );
285
+ return result;
286
+ } catch (error) {
287
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 踢出成员失败:`, error);
288
+ throw error;
289
+ }
290
+ }
291
+
292
+ async muteMember(groupId: number, userId: number, duration: number = 600): Promise<boolean> {
293
+ try {
294
+ const group = this.pickGroup(groupId);
295
+ const result = await group.muteMember(userId, duration);
296
+ this.pluginLogger.info(
297
+ `ICQQ Bot ${this.$id} ${duration > 0 ? `禁言成员 ${userId} ${duration}秒` : `解除成员 ${userId} 禁言`}(群 ${groupId})`,
298
+ );
299
+ return result;
300
+ } catch (error) {
301
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 禁言操作失败:`, error);
302
+ throw error;
303
+ }
304
+ }
305
+
306
+ async muteAll(groupId: number, enable: boolean = true): Promise<boolean> {
307
+ try {
308
+ const group = this.pickGroup(groupId);
309
+ const result = await group.muteAll(enable);
310
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}全员禁言(群 ${groupId})`);
311
+ return result;
312
+ } catch (error) {
313
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 全员禁言操作失败:`, error);
314
+ throw error;
315
+ }
316
+ }
317
+
318
+ async setAdmin(groupId: number, userId: number, enable: boolean = true): Promise<boolean> {
319
+ try {
320
+ const group = this.pickGroup(groupId);
321
+ const result = await group.setAdmin(userId, enable);
322
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "设置" : "取消"}管理员 ${userId}(群 ${groupId})`);
323
+ return result;
324
+ } catch (error) {
325
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置管理员失败:`, error);
326
+ throw error;
327
+ }
328
+ }
329
+
330
+ async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
331
+ try {
332
+ const group = this.pickGroup(groupId);
333
+ const result = await group.setCard(userId, card);
334
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 群名片为 "${card}"(群 ${groupId})`);
335
+ return result;
336
+ } catch (error) {
337
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置群名片失败:`, error);
338
+ throw error;
339
+ }
340
+ }
341
+
342
+ async setTitle(groupId: number, userId: number, title: string, duration: number = -1): Promise<boolean> {
343
+ try {
344
+ const group = this.pickGroup(groupId);
345
+ const result = await group.setTitle(userId, title, duration);
346
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 头衔为 "${title}"(群 ${groupId})`);
347
+ return result;
348
+ } catch (error) {
349
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置头衔失败:`, error);
350
+ throw error;
351
+ }
352
+ }
353
+
354
+ async setGroupName(groupId: number, name: string): Promise<boolean> {
355
+ try {
356
+ const group = this.pickGroup(groupId);
357
+ const result = await group.setName(name);
358
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置群名为 "${name}"(群 ${groupId})`);
359
+ return result;
360
+ } catch (error) {
361
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置群名失败:`, error);
362
+ throw error;
363
+ }
364
+ }
365
+
366
+ async sendAnnounce(groupId: number, content: string): Promise<boolean> {
367
+ try {
368
+ const group = this.pickGroup(groupId);
369
+ const result = await group.announce(content);
370
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} 发送群公告(群 ${groupId})`);
371
+ return result;
372
+ } catch (error) {
373
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 发送群公告失败:`, error);
374
+ throw error;
375
+ }
376
+ }
377
+
378
+ async pokeMember(groupId: number, userId: number): Promise<boolean> {
379
+ try {
380
+ const group = this.pickGroup(groupId);
381
+ const result = await group.pokeMember(userId);
382
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} 戳了戳 ${userId}(群 ${groupId})`);
383
+ return result;
384
+ } catch (error) {
385
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 戳一戳失败:`, error);
386
+ throw error;
387
+ }
388
+ }
389
+
390
+ async getMemberList(groupId: number): Promise<Map<number, MemberInfo>> {
391
+ try {
392
+ const group = this.pickGroup(groupId);
393
+ return await group.getMemberMap();
394
+ } catch (error) {
395
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取群成员列表失败:`, error);
396
+ throw error;
397
+ }
398
+ }
399
+
400
+ async getMutedMembers(groupId: number): Promise<any[]> {
401
+ try {
402
+ const group = this.pickGroup(groupId);
403
+ return await group.getMuteMemberList();
404
+ } catch (error) {
405
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取禁言列表失败:`, error);
406
+ throw error;
407
+ }
408
+ }
409
+
410
+ async setAnonymous(groupId: number, enable: boolean = true): Promise<boolean> {
411
+ try {
412
+ const group = this.pickGroup(groupId);
413
+ const result = await group.allowAnony(enable);
414
+ this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}匿名(群 ${groupId})`);
415
+ return result;
416
+ } catch (error) {
417
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置匿名失败:`, error);
418
+ throw error;
419
+ }
420
+ }
421
+
422
+ async getGroupFiles(groupId: number): Promise<any> {
423
+ try {
424
+ const group = this.pickGroup(groupId);
425
+ return await group.fs.ls();
426
+ } catch (error) {
427
+ this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取群文件列表失败:`, error);
428
+ throw error;
429
+ }
430
+ }
431
+
432
+ async $recallMessage(id: string): Promise<void> {
433
+ await this.deleteMsg(id);
434
+ }
435
+
436
+ async $sendMessage(options: SendOptions): Promise<string> {
437
+ switch (options.type) {
438
+ case "private": {
439
+ const result = await this.sendPrivateMsg(
440
+ Number(options.id),
441
+ IcqqBot.toSendable(options.content),
442
+ );
443
+ this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
444
+ return result.message_id.toString();
445
+ }
446
+ case "group": {
447
+ const result = await this.sendGroupMsg(
448
+ Number(options.id),
449
+ IcqqBot.toSendable(options.content),
450
+ );
451
+ this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
452
+ return result.message_id.toString();
453
+ }
454
+ default:
455
+ throw new Error(`unsupported channel type ${options.type}`);
456
+ }
457
+ }
458
+ }
459
+
460
+ export namespace IcqqBot {
461
+ const allowTypes = [
462
+ "text", "face", "image", "record", "audio", "dice", "rps", "video",
463
+ "file", "location", "share", "json", "at", "reply", "long_msg",
464
+ "button", "markdown", "xml",
465
+ ];
466
+
467
+ export function toSegments(message: Sendable): MessageSegment[] {
468
+ if (!Array.isArray(message)) message = [message];
469
+ return message
470
+ .filter((item, index) => typeof item === "string" || (item as any).type !== "long_msg" || index !== 0)
471
+ .map((item): MessageSegment => {
472
+ if (typeof item === "string") return { type: "text", data: { text: item } };
473
+ const { type, ...data } = item as any;
474
+ return { type, data };
475
+ });
476
+ }
477
+ }
478
+
479
+ export namespace IcqqBot {
480
+ const allowTypes = [
481
+ "text", "face", "image", "record", "audio", "dice", "rps", "video",
482
+ "file", "location", "share", "json", "at", "reply", "long_msg",
483
+ "button", "markdown", "xml",
484
+ ];
485
+
486
+ export function toSendable(content: SendContent): Sendable {
487
+ if (!Array.isArray(content)) content = [content];
488
+ return content.map((seg): MessageElem => {
489
+ if (typeof seg === "string") return { type: "text", text: seg };
490
+ let { type, data } = seg as any;
491
+ if (typeof type === "function") type = type.name;
492
+ if (!allowTypes.includes(type)) return { type: "text", text: segment.toString(seg) };
493
+ return { type, ...data } as MessageElem;
494
+ });
495
+ }
496
+ }