@zhin.js/adapter-telegram 1.0.37 → 1.0.39

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,982 @@
1
+ /**
2
+ * Telegram Bot 实现
3
+ */
4
+ import { Telegraf, type Context as TelegrafContext } from "telegraf";
5
+ import type { Message as TelegramMessage, Update, MessageEntity, ChatMember } from "telegraf/types";
6
+ import {
7
+ Bot,
8
+ Message,
9
+ SendOptions,
10
+ SendContent,
11
+ MessageSegment,
12
+ segment,
13
+ } from "zhin.js";
14
+ import type { TelegramBotConfig, TelegramSenderInfo } from "./types.js";
15
+ import type { TelegramAdapter } from "./adapter.js";
16
+
17
+ export class TelegramBot extends Telegraf implements Bot<TelegramBotConfig, TelegramMessage> {
18
+ $connected: boolean = false;
19
+
20
+ get pluginLogger() {
21
+ return this.adapter.plugin.logger;
22
+ }
23
+
24
+ get $id() {
25
+ return this.$config.name;
26
+ }
27
+
28
+ constructor(public adapter: TelegramAdapter, public $config: TelegramBotConfig) {
29
+ super($config.token);
30
+ }
31
+
32
+ async $connect(): Promise<void> {
33
+ try {
34
+ // Set up message handler
35
+ this.on("message", async (ctx) => {
36
+ await this.handleTelegramMessage(ctx);
37
+ });
38
+
39
+ // Set up callback query handler (for inline buttons)
40
+ this.on("callback_query", async (ctx) => {
41
+ // Handle callback queries as special messages
42
+ if (ctx.callbackQuery && "data" in ctx.callbackQuery) {
43
+ const message = this.$formatCallbackQuery(ctx);
44
+ this.adapter.emit("message.receive", message);
45
+ this.pluginLogger.debug(
46
+ `${this.$config.name} recv callback ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`
47
+ );
48
+ }
49
+ });
50
+
51
+ // Start bot with polling or webhook
52
+ if (this.$config.polling !== false) {
53
+ // Use polling by default
54
+ await this.launch({
55
+ allowedUpdates: this.$config.allowedUpdates as any,
56
+ });
57
+ } else if (this.$config.webhook) {
58
+ // Use webhook
59
+ const { domain, path = "/telegram-webhook", port } = this.$config.webhook;
60
+ await this.launch({
61
+ webhook: {
62
+ domain,
63
+ port,
64
+ hookPath: path,
65
+ },
66
+ allowedUpdates: this.$config.allowedUpdates as any,
67
+ });
68
+ } else {
69
+ throw new Error("Either polling must be enabled or webhook configuration must be provided");
70
+ }
71
+
72
+ this.$connected = true;
73
+ const me = await this.telegram.getMe();
74
+ this.pluginLogger.info(
75
+ `Telegram bot ${this.$config.name} connected successfully as @${me.username}`
76
+ );
77
+ } catch (error) {
78
+ this.pluginLogger.error("Failed to connect Telegram bot:", error);
79
+ this.$connected = false;
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ async $disconnect(): Promise<void> {
85
+ try {
86
+ await this.stop();
87
+ this.$connected = false;
88
+ this.pluginLogger.info(`Telegram bot ${this.$config.name} disconnected`);
89
+ } catch (error) {
90
+ this.pluginLogger.error("Error disconnecting Telegram bot:", error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ private async handleTelegramMessage(ctx: TelegrafContext): Promise<void> {
96
+ if (!ctx.message) return;
97
+
98
+ const message = this.$formatMessage(ctx.message as TelegramMessage);
99
+ this.adapter.emit("message.receive", message);
100
+ this.pluginLogger.debug(
101
+ `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`
102
+ );
103
+ }
104
+
105
+ $formatMessage(msg: TelegramMessage): Message<TelegramMessage> {
106
+ // Determine channel type
107
+ const channelType = msg.chat.type === "private" ? "private" : "group";
108
+ const channelId = msg.chat.id.toString();
109
+
110
+ // Parse message content
111
+ const content = this.parseMessageContent(msg);
112
+
113
+ const result = Message.from(msg, {
114
+ $id: msg.message_id.toString(),
115
+ $adapter: "telegram",
116
+ $bot: this.$config.name,
117
+ $sender: {
118
+ id: msg.from?.id.toString() || "",
119
+ name: msg.from?.username || msg.from?.first_name || "Unknown",
120
+ },
121
+ $channel: {
122
+ id: channelId,
123
+ type: channelType,
124
+ },
125
+ $content: content,
126
+ $raw: "text" in msg ? msg.text || "" : "",
127
+ $timestamp: msg.date * 1000,
128
+ $recall: async () => {
129
+ try {
130
+ await this.telegram.deleteMessage(parseInt(channelId), parseInt(result.$id));
131
+ } catch (error) {
132
+ this.pluginLogger.error("Error recalling Telegram message:", error);
133
+ throw error;
134
+ }
135
+ },
136
+ $reply: async (
137
+ content: SendContent,
138
+ quote?: boolean | string
139
+ ): Promise<string> => {
140
+ if (!Array.isArray(content)) content = [content];
141
+ // Handle reply
142
+ if (quote) {
143
+ const replyToMessageId = typeof quote === "boolean" ? result.$id : quote;
144
+ content.unshift({ type: "reply", data: { id: replyToMessageId } });
145
+ }
146
+
147
+ return await this.adapter.sendMessage({
148
+ context: "telegram",
149
+ bot: this.$config.name,
150
+ id: channelId,
151
+ type: channelType,
152
+ content: content,
153
+ });
154
+ },
155
+ });
156
+
157
+ return result;
158
+ }
159
+
160
+ private $formatCallbackQuery(ctx: TelegrafContext): Message<any> {
161
+ if (!ctx.callbackQuery || !("data" in ctx.callbackQuery)) {
162
+ throw new Error("Invalid callback query");
163
+ }
164
+
165
+ const query = ctx.callbackQuery;
166
+ const msg = query.message;
167
+
168
+ const channelType = msg && "chat" in msg && msg.chat.type === "private" ? "private" : "group";
169
+ const channelId = msg && "chat" in msg ? msg.chat.id.toString() : query.from.id.toString();
170
+
171
+ const result = Message.from(query, {
172
+ $id: query.id,
173
+ $adapter: "telegram",
174
+ $bot: this.$config.name,
175
+ $sender: {
176
+ id: query.from.id.toString(),
177
+ name: query.from.username || query.from.first_name,
178
+ },
179
+ $channel: {
180
+ id: channelId,
181
+ type: channelType,
182
+ },
183
+ $content: [{ type: "text", data: { text: query.data } }],
184
+ $raw: query.data,
185
+ $timestamp: Date.now(),
186
+ $recall: async () => {
187
+ // Callback queries cannot be recalled
188
+ },
189
+ $reply: async (content: SendContent): Promise<string> => {
190
+ if (!Array.isArray(content)) content = [content];
191
+ const sentMsg = await this.sendContentToChat(
192
+ parseInt(channelId),
193
+ content
194
+ );
195
+ return sentMsg.message_id.toString();
196
+ },
197
+ });
198
+
199
+ return result;
200
+ }
201
+
202
+ private parseMessageContent(msg: TelegramMessage): MessageSegment[] {
203
+ const segments: MessageSegment[] = [];
204
+
205
+ // Handle text messages
206
+ if ("text" in msg && msg.text) {
207
+ // Check for reply
208
+ if (msg.reply_to_message) {
209
+ segments.push({
210
+ type: "reply",
211
+ data: {
212
+ id: msg.reply_to_message.message_id.toString(),
213
+ },
214
+ });
215
+ }
216
+
217
+ // Parse text with entities
218
+ if (msg.entities && msg.entities.length > 0) {
219
+ segments.push(...this.parseTextWithEntities(msg.text, msg.entities));
220
+ } else {
221
+ segments.push({ type: "text", data: { text: msg.text } });
222
+ }
223
+ }
224
+
225
+ // Handle photo
226
+ if ("photo" in msg && msg.photo) {
227
+ const largestPhoto = msg.photo[msg.photo.length - 1];
228
+ segments.push({
229
+ type: "image",
230
+ data: {
231
+ file_id: largestPhoto.file_id,
232
+ file_unique_id: largestPhoto.file_unique_id,
233
+ width: largestPhoto.width,
234
+ height: largestPhoto.height,
235
+ file_size: largestPhoto.file_size,
236
+ },
237
+ });
238
+ if (msg.caption) {
239
+ segments.push({ type: "text", data: { text: msg.caption } });
240
+ }
241
+ }
242
+
243
+ // Handle video
244
+ if ("video" in msg && msg.video) {
245
+ segments.push({
246
+ type: "video",
247
+ data: {
248
+ file_id: msg.video.file_id,
249
+ file_unique_id: msg.video.file_unique_id,
250
+ width: msg.video.width,
251
+ height: msg.video.height,
252
+ duration: msg.video.duration,
253
+ file_size: msg.video.file_size,
254
+ },
255
+ });
256
+ if (msg.caption) {
257
+ segments.push({ type: "text", data: { text: msg.caption } });
258
+ }
259
+ }
260
+
261
+ // Handle audio
262
+ if ("audio" in msg && msg.audio) {
263
+ segments.push({
264
+ type: "audio",
265
+ data: {
266
+ file_id: msg.audio.file_id,
267
+ file_unique_id: msg.audio.file_unique_id,
268
+ duration: msg.audio.duration,
269
+ performer: msg.audio.performer,
270
+ title: msg.audio.title,
271
+ file_size: msg.audio.file_size,
272
+ },
273
+ });
274
+ }
275
+
276
+ // Handle voice
277
+ if ("voice" in msg && msg.voice) {
278
+ segments.push({
279
+ type: "voice",
280
+ data: {
281
+ file_id: msg.voice.file_id,
282
+ file_unique_id: msg.voice.file_unique_id,
283
+ duration: msg.voice.duration,
284
+ file_size: msg.voice.file_size,
285
+ },
286
+ });
287
+ }
288
+
289
+ // Handle document
290
+ if ("document" in msg && msg.document) {
291
+ segments.push({
292
+ type: "file",
293
+ data: {
294
+ file_id: msg.document.file_id,
295
+ file_unique_id: msg.document.file_unique_id,
296
+ file_name: msg.document.file_name,
297
+ mime_type: msg.document.mime_type,
298
+ file_size: msg.document.file_size,
299
+ },
300
+ });
301
+ }
302
+
303
+ // Handle sticker
304
+ if ("sticker" in msg && msg.sticker) {
305
+ segments.push({
306
+ type: "sticker",
307
+ data: {
308
+ file_id: msg.sticker.file_id,
309
+ file_unique_id: msg.sticker.file_unique_id,
310
+ width: msg.sticker.width,
311
+ height: msg.sticker.height,
312
+ is_animated: msg.sticker.is_animated,
313
+ is_video: msg.sticker.is_video,
314
+ emoji: msg.sticker.emoji,
315
+ },
316
+ });
317
+ }
318
+
319
+ // Handle location
320
+ if ("location" in msg && msg.location) {
321
+ segments.push({
322
+ type: "location",
323
+ data: {
324
+ longitude: msg.location.longitude,
325
+ latitude: msg.location.latitude,
326
+ },
327
+ });
328
+ }
329
+
330
+ return segments.length > 0
331
+ ? segments
332
+ : [{ type: "text", data: { text: "" } }];
333
+ }
334
+
335
+ private parseTextWithEntities(
336
+ text: string,
337
+ entities: MessageEntity[]
338
+ ): MessageSegment[] {
339
+ const segments: MessageSegment[] = [];
340
+ let lastOffset = 0;
341
+
342
+ for (const entity of entities) {
343
+ // Add text before entity
344
+ if (entity.offset > lastOffset) {
345
+ const beforeText = text.slice(lastOffset, entity.offset);
346
+ if (beforeText) {
347
+ segments.push({ type: "text", data: { text: beforeText } });
348
+ }
349
+ }
350
+
351
+ const entityText = text.slice(
352
+ entity.offset,
353
+ entity.offset + entity.length
354
+ );
355
+
356
+ switch (entity.type) {
357
+ case "mention":
358
+ case "text_mention":
359
+ segments.push({
360
+ type: "at",
361
+ data: {
362
+ id: ("user" in entity && entity.user?.id.toString()) || entityText.slice(1),
363
+ name: ("user" in entity && entity.user?.username) || entityText,
364
+ text: entityText,
365
+ },
366
+ });
367
+ break;
368
+
369
+ case "url":
370
+ case "text_link":
371
+ segments.push({
372
+ type: "link",
373
+ data: {
374
+ url: ("url" in entity && entity.url) || entityText,
375
+ text: entityText,
376
+ },
377
+ });
378
+ break;
379
+
380
+ case "hashtag":
381
+ segments.push({
382
+ type: "text",
383
+ data: { text: entityText },
384
+ });
385
+ break;
386
+
387
+ case "bold":
388
+ case "italic":
389
+ case "code":
390
+ case "pre":
391
+ case "underline":
392
+ case "strikethrough":
393
+ segments.push({
394
+ type: "text",
395
+ data: { text: entityText, format: entity.type },
396
+ });
397
+ break;
398
+
399
+ default:
400
+ segments.push({ type: "text", data: { text: entityText } });
401
+ }
402
+
403
+ lastOffset = entity.offset + entity.length;
404
+ }
405
+
406
+ // Add remaining text
407
+ if (lastOffset < text.length) {
408
+ const remainingText = text.slice(lastOffset);
409
+ if (remainingText) {
410
+ segments.push({ type: "text", data: { text: remainingText } });
411
+ }
412
+ }
413
+
414
+ return segments;
415
+ }
416
+
417
+ async $sendMessage(options: SendOptions): Promise<string> {
418
+ try {
419
+ const chatId = parseInt(options.id);
420
+ const result = await this.sendContentToChat(chatId, options.content);
421
+ this.pluginLogger.debug(
422
+ `${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`
423
+ );
424
+ return result.message_id.toString();
425
+ } catch (error) {
426
+ this.pluginLogger.error("Failed to send Telegram message:", error);
427
+ throw error;
428
+ }
429
+ }
430
+
431
+ private async sendContentToChat(
432
+ chatId: number,
433
+ content: SendContent,
434
+ extraOptions: any = {}
435
+ ): Promise<TelegramMessage> {
436
+ if (!Array.isArray(content)) content = [content];
437
+
438
+ let textContent = "";
439
+ let hasMedia = false;
440
+
441
+ for (const segment of content) {
442
+ if (typeof segment === "string") {
443
+ textContent += segment;
444
+ continue;
445
+ }
446
+
447
+ const { type, data } = segment;
448
+
449
+ switch (type) {
450
+ case "text":
451
+ textContent += data.text || "";
452
+ break;
453
+
454
+ case "at":
455
+ if (data.id) {
456
+ textContent += `@${data.name || data.id}`;
457
+ }
458
+ break;
459
+
460
+ case "image":
461
+ hasMedia = true;
462
+ if (data.file_id) {
463
+ // Send by file_id
464
+ return await this.telegram.sendPhoto(chatId, data.file_id, {
465
+ caption: textContent || undefined,
466
+ ...extraOptions,
467
+ });
468
+ } else if (data.url) {
469
+ // Send by URL
470
+ return await this.telegram.sendPhoto(chatId, data.url, {
471
+ caption: textContent || undefined,
472
+ ...extraOptions,
473
+ });
474
+ } else if (data.file) {
475
+ // Send by file path
476
+ return await this.telegram.sendPhoto(
477
+ chatId,
478
+ { source: data.file },
479
+ {
480
+ caption: textContent || undefined,
481
+ ...extraOptions,
482
+ }
483
+ );
484
+ }
485
+ break;
486
+
487
+ case "video":
488
+ hasMedia = true;
489
+ if (data.file_id) {
490
+ return await this.telegram.sendVideo(chatId, data.file_id, {
491
+ caption: textContent || undefined,
492
+ ...extraOptions,
493
+ });
494
+ } else if (data.url) {
495
+ return await this.telegram.sendVideo(chatId, data.url, {
496
+ caption: textContent || undefined,
497
+ ...extraOptions,
498
+ });
499
+ } else if (data.file) {
500
+ return await this.telegram.sendVideo(
501
+ chatId,
502
+ { source: data.file },
503
+ {
504
+ caption: textContent || undefined,
505
+ ...extraOptions,
506
+ }
507
+ );
508
+ }
509
+ break;
510
+
511
+ case "audio":
512
+ hasMedia = true;
513
+ if (data.file_id) {
514
+ return await this.telegram.sendAudio(chatId, data.file_id, {
515
+ caption: textContent || undefined,
516
+ ...extraOptions,
517
+ });
518
+ } else if (data.url) {
519
+ return await this.telegram.sendAudio(chatId, data.url, {
520
+ caption: textContent || undefined,
521
+ ...extraOptions,
522
+ });
523
+ } else if (data.file) {
524
+ return await this.telegram.sendAudio(
525
+ chatId,
526
+ { source: data.file },
527
+ {
528
+ caption: textContent || undefined,
529
+ ...extraOptions,
530
+ }
531
+ );
532
+ }
533
+ break;
534
+
535
+ case "voice":
536
+ hasMedia = true;
537
+ if (data.file_id) {
538
+ return await this.telegram.sendVoice(chatId, data.file_id, {
539
+ caption: textContent || undefined,
540
+ ...extraOptions,
541
+ });
542
+ } else if (data.url) {
543
+ return await this.telegram.sendVoice(chatId, data.url, {
544
+ caption: textContent || undefined,
545
+ ...extraOptions,
546
+ });
547
+ } else if (data.file) {
548
+ return await this.telegram.sendVoice(
549
+ chatId,
550
+ { source: data.file },
551
+ {
552
+ caption: textContent || undefined,
553
+ ...extraOptions,
554
+ }
555
+ );
556
+ }
557
+ break;
558
+
559
+ case "file":
560
+ hasMedia = true;
561
+ if (data.file_id) {
562
+ return await this.telegram.sendDocument(chatId, data.file_id, {
563
+ caption: textContent || undefined,
564
+ ...extraOptions,
565
+ });
566
+ } else if (data.url) {
567
+ return await this.telegram.sendDocument(chatId, data.url, {
568
+ caption: textContent || undefined,
569
+ ...extraOptions,
570
+ });
571
+ } else if (data.file) {
572
+ return await this.telegram.sendDocument(
573
+ chatId,
574
+ { source: data.file },
575
+ {
576
+ caption: textContent || undefined,
577
+ ...extraOptions,
578
+ }
579
+ );
580
+ }
581
+ break;
582
+
583
+ case "sticker":
584
+ if (data.file_id) {
585
+ hasMedia = true;
586
+ return await this.telegram.sendSticker(chatId, data.file_id, extraOptions);
587
+ }
588
+ break;
589
+
590
+ case "location":
591
+ return await this.telegram.sendLocation(
592
+ chatId,
593
+ data.latitude,
594
+ data.longitude,
595
+ extraOptions
596
+ );
597
+ case "reply":
598
+ return await this.telegram.sendMessage(chatId, data.id, {
599
+ reply_parameters: { message_id: parseInt(data.id) },
600
+ ...extraOptions,
601
+ });
602
+ default:
603
+ // Unknown segment type, add as text
604
+ textContent += data.text || `[${type}]`;
605
+ }
606
+ }
607
+
608
+ // If no media was sent, send as text message
609
+ if (!hasMedia && textContent.trim()) {
610
+ return await this.telegram.sendMessage(chatId, textContent.trim(), extraOptions);
611
+ }
612
+
613
+ // If neither media nor text was sent, this is an error
614
+ throw new Error("TelegramBot.$sendMessage: No media or text content to send.");
615
+ }
616
+
617
+ async $recallMessage(id: string): Promise<void> {
618
+ // Telegram requires both chat_id and message_id to delete a message
619
+ // The Bot interface only provides message_id, making recall impossible
620
+ // Users should use message.$recall() instead, which has the full context
621
+ throw new Error(
622
+ "TelegramBot.$recallMessage: Message recall not supported without chat_id. " +
623
+ "Use message.$recall() method instead, which contains the required context."
624
+ );
625
+ }
626
+ // ==================== 群组管理 API ====================
627
+
628
+ /**
629
+ * 踢出用户
630
+ * @param chatId 聊天 ID
631
+ * @param userId 用户 ID
632
+ * @param untilDate 封禁截止时间(Unix 时间戳),0 表示永久
633
+ */
634
+ async kickMember(chatId: number, userId: number, untilDate?: number): Promise<boolean> {
635
+ try {
636
+ await this.telegram.banChatMember(chatId, userId, untilDate);
637
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 踢出用户 ${userId} 从聊天 ${chatId}`);
638
+ return true;
639
+ } catch (error) {
640
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 踢出用户失败:`, error);
641
+ throw error;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * 解除封禁
647
+ * @param chatId 聊天 ID
648
+ * @param userId 用户 ID
649
+ */
650
+ async unbanMember(chatId: number, userId: number): Promise<boolean> {
651
+ try {
652
+ await this.telegram.unbanChatMember(chatId, userId, { only_if_banned: true });
653
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 解除用户 ${userId} 的封禁(聊天 ${chatId})`);
654
+ return true;
655
+ } catch (error) {
656
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 解除封禁失败:`, error);
657
+ throw error;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * 限制用户权限(禁言等)
663
+ * @param chatId 聊天 ID
664
+ * @param userId 用户 ID
665
+ * @param permissions 权限设置
666
+ * @param untilDate 限制截止时间
667
+ */
668
+ async restrictMember(chatId: number, userId: number, permissions: {
669
+ can_send_messages?: boolean;
670
+ can_send_media_messages?: boolean;
671
+ can_send_polls?: boolean;
672
+ can_send_other_messages?: boolean;
673
+ can_add_web_page_previews?: boolean;
674
+ can_change_info?: boolean;
675
+ can_invite_users?: boolean;
676
+ can_pin_messages?: boolean;
677
+ }, untilDate?: number): Promise<boolean> {
678
+ try {
679
+ await this.telegram.restrictChatMember(chatId, userId, {
680
+ permissions,
681
+ until_date: untilDate,
682
+ });
683
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 限制用户 ${userId} 权限(聊天 ${chatId})`);
684
+ return true;
685
+ } catch (error) {
686
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 限制权限失败:`, error);
687
+ throw error;
688
+ }
689
+ }
690
+
691
+ /**
692
+ * 禁言用户
693
+ * @param chatId 聊天 ID
694
+ * @param userId 用户 ID
695
+ * @param duration 禁言时长(秒),0 表示解除禁言
696
+ */
697
+ async muteMember(chatId: number, userId: number, duration: number = 600): Promise<boolean> {
698
+ try {
699
+ if (duration === 0) {
700
+ // 解除禁言 - 恢复发送消息权限
701
+ await this.telegram.restrictChatMember(chatId, userId, {
702
+ permissions: {
703
+ can_send_messages: true,
704
+ can_send_audios: true,
705
+ can_send_documents: true,
706
+ can_send_photos: true,
707
+ can_send_videos: true,
708
+ can_send_video_notes: true,
709
+ can_send_voice_notes: true,
710
+ can_send_polls: true,
711
+ can_send_other_messages: true,
712
+ can_add_web_page_previews: true,
713
+ },
714
+ });
715
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 解除用户 ${userId} 禁言(聊天 ${chatId})`);
716
+ } else {
717
+ const untilDate = Math.floor(Date.now() / 1000) + duration;
718
+ await this.telegram.restrictChatMember(chatId, userId, {
719
+ permissions: {
720
+ can_send_messages: false,
721
+ can_send_audios: false,
722
+ can_send_documents: false,
723
+ can_send_photos: false,
724
+ can_send_videos: false,
725
+ can_send_video_notes: false,
726
+ can_send_voice_notes: false,
727
+ can_send_polls: false,
728
+ can_send_other_messages: false,
729
+ can_add_web_page_previews: false,
730
+ },
731
+ until_date: untilDate,
732
+ });
733
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 禁言用户 ${userId} ${duration}秒(聊天 ${chatId})`);
734
+ }
735
+ return true;
736
+ } catch (error) {
737
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 禁言操作失败:`, error);
738
+ throw error;
739
+ }
740
+ }
741
+
742
+ /**
743
+ * 提升/降级管理员
744
+ * @param chatId 聊天 ID
745
+ * @param userId 用户 ID
746
+ * @param promote 是否提升为管理员
747
+ */
748
+ async setAdmin(chatId: number, userId: number, promote: boolean = true): Promise<boolean> {
749
+ try {
750
+ if (promote) {
751
+ await this.telegram.promoteChatMember(chatId, userId, {
752
+ can_manage_chat: true,
753
+ can_delete_messages: true,
754
+ can_manage_video_chats: true,
755
+ can_restrict_members: true,
756
+ can_promote_members: false,
757
+ can_change_info: true,
758
+ can_invite_users: true,
759
+ can_pin_messages: true,
760
+ });
761
+ } else {
762
+ await this.telegram.promoteChatMember(chatId, userId, {
763
+ can_manage_chat: false,
764
+ can_delete_messages: false,
765
+ can_manage_video_chats: false,
766
+ can_restrict_members: false,
767
+ can_promote_members: false,
768
+ can_change_info: false,
769
+ can_invite_users: false,
770
+ can_pin_messages: false,
771
+ });
772
+ }
773
+ this.pluginLogger.info(`Telegram Bot ${this.$id} ${promote ? '提升' : '降级'}用户 ${userId} 为管理员(聊天 ${chatId})`);
774
+ return true;
775
+ } catch (error) {
776
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 设置管理员失败:`, error);
777
+ throw error;
778
+ }
779
+ }
780
+
781
+ /**
782
+ * 设置聊天标题
783
+ * @param chatId 聊天 ID
784
+ * @param title 新标题
785
+ */
786
+ async setChatTitle(chatId: number, title: string): Promise<boolean> {
787
+ try {
788
+ await this.telegram.setChatTitle(chatId, title);
789
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 设置聊天 ${chatId} 标题为 "${title}"`);
790
+ return true;
791
+ } catch (error) {
792
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 设置标题失败:`, error);
793
+ throw error;
794
+ }
795
+ }
796
+
797
+ /**
798
+ * 设置聊天描述
799
+ * @param chatId 聊天 ID
800
+ * @param description 新描述
801
+ */
802
+ async setChatDescription(chatId: number, description: string): Promise<boolean> {
803
+ try {
804
+ await this.telegram.setChatDescription(chatId, description);
805
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 设置聊天 ${chatId} 描述`);
806
+ return true;
807
+ } catch (error) {
808
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 设置描述失败:`, error);
809
+ throw error;
810
+ }
811
+ }
812
+
813
+ /**
814
+ * 置顶消息
815
+ * @param chatId 聊天 ID
816
+ * @param messageId 消息 ID
817
+ */
818
+ async pinMessage(chatId: number, messageId: number): Promise<boolean> {
819
+ try {
820
+ await this.telegram.pinChatMessage(chatId, messageId);
821
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 置顶消息 ${messageId}(聊天 ${chatId})`);
822
+ return true;
823
+ } catch (error) {
824
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 置顶消息失败:`, error);
825
+ throw error;
826
+ }
827
+ }
828
+
829
+ /**
830
+ * 取消置顶消息
831
+ * @param chatId 聊天 ID
832
+ * @param messageId 消息 ID(可选,不提供则取消所有置顶)
833
+ */
834
+ async unpinMessage(chatId: number, messageId?: number): Promise<boolean> {
835
+ try {
836
+ if (messageId) {
837
+ await this.telegram.unpinChatMessage(chatId, messageId);
838
+ } else {
839
+ await this.telegram.unpinAllChatMessages(chatId);
840
+ }
841
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 取消置顶消息(聊天 ${chatId})`);
842
+ return true;
843
+ } catch (error) {
844
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 取消置顶失败:`, error);
845
+ throw error;
846
+ }
847
+ }
848
+
849
+ /**
850
+ * 获取聊天信息
851
+ * @param chatId 聊天 ID
852
+ */
853
+ async getChatInfo(chatId: number): Promise<any> {
854
+ try {
855
+ return await this.telegram.getChat(chatId);
856
+ } catch (error) {
857
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 获取聊天信息失败:`, error);
858
+ throw error;
859
+ }
860
+ }
861
+
862
+ /**
863
+ * 获取聊天成员
864
+ * @param chatId 聊天 ID
865
+ * @param userId 用户 ID
866
+ */
867
+ async getChatMember(chatId: number, userId: number): Promise<ChatMember> {
868
+ try {
869
+ return await this.telegram.getChatMember(chatId, userId);
870
+ } catch (error) {
871
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 获取成员信息失败:`, error);
872
+ throw error;
873
+ }
874
+ }
875
+
876
+ /**
877
+ * 获取管理员列表
878
+ * @param chatId 聊天 ID
879
+ */
880
+ async getChatAdmins(chatId: number): Promise<ChatMember[]> {
881
+ try {
882
+ return await this.telegram.getChatAdministrators(chatId);
883
+ } catch (error) {
884
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 获取管理员列表失败:`, error);
885
+ throw error;
886
+ }
887
+ }
888
+
889
+ /**
890
+ * 获取聊天成员数量
891
+ * @param chatId 聊天 ID
892
+ */
893
+ async getChatMemberCount(chatId: number): Promise<number> {
894
+ try {
895
+ return await this.telegram.getChatMembersCount(chatId);
896
+ } catch (error) {
897
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 获取成员数量失败:`, error);
898
+ throw error;
899
+ }
900
+ }
901
+
902
+ /**
903
+ * 创建邀请链接
904
+ * @param chatId 聊天 ID
905
+ */
906
+ async createInviteLink(chatId: number): Promise<string> {
907
+ try {
908
+ const link = await this.telegram.createChatInviteLink(chatId, {});
909
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 创建邀请链接(聊天 ${chatId})`);
910
+ return link.invite_link;
911
+ } catch (error) {
912
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 创建邀请链接失败:`, error);
913
+ throw error;
914
+ }
915
+ }
916
+
917
+ async sendPoll(chatId: number, question: string, options: string[], isAnonymous: boolean = true, allowsMultipleAnswers: boolean = false): Promise<TelegramMessage> {
918
+ try {
919
+ const result = await this.telegram.sendPoll(chatId, question, options, {
920
+ is_anonymous: isAnonymous,
921
+ allows_multiple_answers: allowsMultipleAnswers,
922
+ } as any);
923
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 发送投票到 ${chatId}`);
924
+ return result;
925
+ } catch (error) {
926
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 发送投票失败:`, error);
927
+ throw error;
928
+ }
929
+ }
930
+
931
+ async setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean> {
932
+ try {
933
+ await (this.telegram as any).callApi('setMessageReaction', {
934
+ chat_id: chatId,
935
+ message_id: messageId,
936
+ reaction: [{ type: 'emoji', emoji: reaction }],
937
+ });
938
+ return true;
939
+ } catch (error) {
940
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 设置反应失败:`, error);
941
+ throw error;
942
+ }
943
+ }
944
+
945
+ async sendStickerMessage(chatId: number, sticker: string): Promise<TelegramMessage> {
946
+ try {
947
+ const result = await this.telegram.sendSticker(chatId, sticker);
948
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 发送贴纸到 ${chatId}`);
949
+ return result;
950
+ } catch (error) {
951
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 发送贴纸失败:`, error);
952
+ throw error;
953
+ }
954
+ }
955
+
956
+ async setChatPermissionsAll(chatId: number, permissions: {
957
+ can_send_messages?: boolean;
958
+ can_send_audios?: boolean;
959
+ can_send_documents?: boolean;
960
+ can_send_photos?: boolean;
961
+ can_send_videos?: boolean;
962
+ can_send_video_notes?: boolean;
963
+ can_send_voice_notes?: boolean;
964
+ can_send_polls?: boolean;
965
+ can_send_other_messages?: boolean;
966
+ can_add_web_page_previews?: boolean;
967
+ can_change_info?: boolean;
968
+ can_invite_users?: boolean;
969
+ can_pin_messages?: boolean;
970
+ can_manage_topics?: boolean;
971
+ }): Promise<boolean> {
972
+ try {
973
+ await this.telegram.setChatPermissions(chatId, permissions);
974
+ this.pluginLogger.info(`Telegram Bot ${this.$id} 设置聊天 ${chatId} 权限`);
975
+ return true;
976
+ } catch (error) {
977
+ this.pluginLogger.error(`Telegram Bot ${this.$id} 设置聊天权限失败:`, error);
978
+ throw error;
979
+ }
980
+ }
981
+ }
982
+