@zhin.js/adapter-telegram 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/lib/index.d.ts +85 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +778 -0
- package/lib/index.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +929 -0
- package/tsconfig.json +24 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import TelegramBotApi from "node-telegram-bot-api";
|
|
2
|
+
import { Adapter, registerAdapter, Message, segment, useContext } from "zhin.js";
|
|
3
|
+
import { createWriteStream } from 'fs';
|
|
4
|
+
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
// 主要的 TelegramBot 类
|
|
10
|
+
export class TelegramBot extends TelegramBotApi {
|
|
11
|
+
plugin;
|
|
12
|
+
$connected;
|
|
13
|
+
constructor(plugin, config) {
|
|
14
|
+
const options = {
|
|
15
|
+
polling: true,
|
|
16
|
+
...config
|
|
17
|
+
};
|
|
18
|
+
// 如果配置了代理,设置代理
|
|
19
|
+
if (config.proxy) {
|
|
20
|
+
try {
|
|
21
|
+
const proxyUrl = `socks5://${config.proxy.username ? `${config.proxy.username}:${config.proxy.password}@` : ''}${config.proxy.host}:${config.proxy.port}`;
|
|
22
|
+
options.request = {
|
|
23
|
+
agent: new SocksProxyAgent(proxyUrl)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// 代理配置失败,继续不使用代理
|
|
28
|
+
console.warn('Failed to configure proxy, continuing without proxy:', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
super(config.token, options);
|
|
32
|
+
this.plugin = plugin;
|
|
33
|
+
this.$config = config;
|
|
34
|
+
this.$connected = false;
|
|
35
|
+
}
|
|
36
|
+
async handleTelegramMessage(msg) {
|
|
37
|
+
const message = this.$formatMessage(msg);
|
|
38
|
+
this.plugin.dispatch('message.receive', message);
|
|
39
|
+
this.plugin.logger.info(`recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
|
|
40
|
+
this.plugin.dispatch(`message.${message.$channel.type}.receive`, message);
|
|
41
|
+
}
|
|
42
|
+
async $connect() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
// 监听所有消息
|
|
45
|
+
this.on('message', this.handleTelegramMessage.bind(this));
|
|
46
|
+
// 监听连接错误
|
|
47
|
+
this.on('polling_error', (error) => {
|
|
48
|
+
this.plugin.logger.error('Telegram polling error:', error);
|
|
49
|
+
this.$connected = false;
|
|
50
|
+
reject(error);
|
|
51
|
+
});
|
|
52
|
+
// 启动轮询,测试连接
|
|
53
|
+
this.startPolling().then(() => {
|
|
54
|
+
this.$connected = true;
|
|
55
|
+
this.plugin.logger.info(`Telegram bot ${this.$config.name} connected successfully`);
|
|
56
|
+
resolve();
|
|
57
|
+
}).catch((error) => {
|
|
58
|
+
this.plugin.logger.error('Failed to start Telegram bot:', error);
|
|
59
|
+
this.$connected = false;
|
|
60
|
+
reject(error);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async $disconnect() {
|
|
65
|
+
try {
|
|
66
|
+
await this.stopPolling();
|
|
67
|
+
this.$connected = false;
|
|
68
|
+
this.plugin.logger.info(`Telegram bot ${this.$config.name} disconnected`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.plugin.logger.error('Error disconnecting Telegram bot:', error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
$formatMessage(msg) {
|
|
76
|
+
// 确定聊天类型和ID
|
|
77
|
+
let channelType;
|
|
78
|
+
let channelId;
|
|
79
|
+
if (msg.chat.type === 'private') {
|
|
80
|
+
channelType = 'private';
|
|
81
|
+
channelId = msg.chat.id.toString();
|
|
82
|
+
}
|
|
83
|
+
else if (msg.chat.type === 'group' || msg.chat.type === 'supergroup') {
|
|
84
|
+
channelType = 'group';
|
|
85
|
+
channelId = msg.chat.id.toString();
|
|
86
|
+
}
|
|
87
|
+
else if (msg.chat.type === 'channel') {
|
|
88
|
+
channelType = 'channel';
|
|
89
|
+
channelId = msg.chat.id.toString();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
channelType = 'private';
|
|
93
|
+
channelId = msg.chat.id.toString();
|
|
94
|
+
}
|
|
95
|
+
// 转换消息内容为 segment 格式(同步)
|
|
96
|
+
const content = this.parseMessageContentSync(msg);
|
|
97
|
+
// 在后台异步下载文件(不阻塞消息处理)
|
|
98
|
+
this.downloadMessageFiles(msg).catch(error => {
|
|
99
|
+
this.plugin.logger.warn('Failed to download message files:', error);
|
|
100
|
+
});
|
|
101
|
+
const result = Message.from(msg, {
|
|
102
|
+
$id: msg.message_id.toString(),
|
|
103
|
+
$adapter: 'telegram',
|
|
104
|
+
$bot: this.$config.name,
|
|
105
|
+
$sender: {
|
|
106
|
+
id: msg.from?.id.toString() || msg.chat.id.toString(),
|
|
107
|
+
name: msg.from ? (msg.from.first_name + (msg.from.last_name ? ` ${msg.from.last_name}` : '')) : msg.chat.title || 'Unknown'
|
|
108
|
+
},
|
|
109
|
+
$channel: {
|
|
110
|
+
id: channelId,
|
|
111
|
+
type: channelType
|
|
112
|
+
},
|
|
113
|
+
$content: content,
|
|
114
|
+
$raw: msg.text || msg.caption || '',
|
|
115
|
+
$timestamp: msg.date * 1000, // Telegram 使用秒,转换为毫秒
|
|
116
|
+
$reply: async (content, quote) => {
|
|
117
|
+
if (!Array.isArray(content))
|
|
118
|
+
content = [content];
|
|
119
|
+
const sendOptions = {};
|
|
120
|
+
// 处理回复消息
|
|
121
|
+
if (quote) {
|
|
122
|
+
sendOptions.reply_to_message_id = parseInt(typeof quote === "boolean" ? result.$id : quote);
|
|
123
|
+
}
|
|
124
|
+
await this.sendContentToChat(channelId, content, sendOptions);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
async $sendMessage(options) {
|
|
130
|
+
options = await this.plugin.app.handleBeforeSend(options);
|
|
131
|
+
try {
|
|
132
|
+
const chatId = options.id;
|
|
133
|
+
await this.sendContentToChat(chatId, options.content);
|
|
134
|
+
this.plugin.logger.info(`send ${options.type}(${options.id}): ${segment.raw(options.content)}`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.plugin.logger.error('Failed to send Telegram message:', error);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 发送内容到聊天
|
|
142
|
+
async sendContentToChat(chatId, content, extraOptions = {}) {
|
|
143
|
+
if (!Array.isArray(content))
|
|
144
|
+
content = [content];
|
|
145
|
+
const numericChatId = parseInt(chatId);
|
|
146
|
+
let replyToMessageId = extraOptions.reply_to_message_id;
|
|
147
|
+
for (const segment of content) {
|
|
148
|
+
if (typeof segment === 'string') {
|
|
149
|
+
await this.sendMessage(numericChatId, segment, {
|
|
150
|
+
parse_mode: 'HTML',
|
|
151
|
+
reply_to_message_id: replyToMessageId,
|
|
152
|
+
...extraOptions
|
|
153
|
+
});
|
|
154
|
+
replyToMessageId = undefined; // 只有第一条消息回复
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const { type, data } = segment;
|
|
158
|
+
switch (type) {
|
|
159
|
+
case 'text':
|
|
160
|
+
await this.sendMessage(numericChatId, data.text, {
|
|
161
|
+
parse_mode: 'HTML',
|
|
162
|
+
reply_to_message_id: replyToMessageId,
|
|
163
|
+
...extraOptions
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
case 'image':
|
|
167
|
+
await this.sendPhoto(numericChatId, data.file || data.url || data.file_id, {
|
|
168
|
+
caption: data.caption,
|
|
169
|
+
reply_to_message_id: replyToMessageId,
|
|
170
|
+
...extraOptions
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
case 'audio':
|
|
174
|
+
await this.sendAudio(numericChatId, data.file || data.url || data.file_id, {
|
|
175
|
+
duration: data.duration,
|
|
176
|
+
performer: data.performer,
|
|
177
|
+
title: data.title,
|
|
178
|
+
caption: data.caption,
|
|
179
|
+
reply_to_message_id: replyToMessageId,
|
|
180
|
+
...extraOptions
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
183
|
+
case 'voice':
|
|
184
|
+
await this.sendVoice(numericChatId, data.file || data.url || data.file_id, {
|
|
185
|
+
duration: data.duration,
|
|
186
|
+
caption: data.caption,
|
|
187
|
+
reply_to_message_id: replyToMessageId,
|
|
188
|
+
...extraOptions
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
case 'video':
|
|
192
|
+
await this.sendVideo(numericChatId, data.file || data.url || data.file_id, {
|
|
193
|
+
duration: data.duration,
|
|
194
|
+
width: data.width,
|
|
195
|
+
height: data.height,
|
|
196
|
+
caption: data.caption,
|
|
197
|
+
reply_to_message_id: replyToMessageId,
|
|
198
|
+
...extraOptions
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
case 'video_note':
|
|
202
|
+
await this.sendVideoNote(numericChatId, data.file || data.url || data.file_id, {
|
|
203
|
+
duration: data.duration,
|
|
204
|
+
length: data.length,
|
|
205
|
+
reply_to_message_id: replyToMessageId,
|
|
206
|
+
...extraOptions
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
case 'document':
|
|
210
|
+
case 'file':
|
|
211
|
+
await this.sendDocument(numericChatId, data.file || data.url || data.file_id, {
|
|
212
|
+
caption: data.caption,
|
|
213
|
+
reply_to_message_id: replyToMessageId,
|
|
214
|
+
...extraOptions
|
|
215
|
+
});
|
|
216
|
+
break;
|
|
217
|
+
case 'sticker':
|
|
218
|
+
await this.sendSticker(numericChatId, data.file_id, {
|
|
219
|
+
reply_to_message_id: replyToMessageId,
|
|
220
|
+
...extraOptions
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
case 'location':
|
|
224
|
+
await this.sendLocation(numericChatId, data.latitude, data.longitude, {
|
|
225
|
+
live_period: data.live_period,
|
|
226
|
+
heading: data.heading,
|
|
227
|
+
proximity_alert_radius: data.proximity_alert_radius,
|
|
228
|
+
reply_to_message_id: replyToMessageId,
|
|
229
|
+
...extraOptions
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
case 'contact':
|
|
233
|
+
await this.sendContact(numericChatId, data.phone_number, data.first_name, {
|
|
234
|
+
last_name: data.last_name,
|
|
235
|
+
vcard: data.vcard,
|
|
236
|
+
reply_to_message_id: replyToMessageId,
|
|
237
|
+
...extraOptions
|
|
238
|
+
});
|
|
239
|
+
break;
|
|
240
|
+
case 'reply':
|
|
241
|
+
// 回复消息在 extraOptions 中处理
|
|
242
|
+
replyToMessageId = parseInt(data.id);
|
|
243
|
+
break;
|
|
244
|
+
case 'at':
|
|
245
|
+
// @提及转换为文本
|
|
246
|
+
const mentionText = data.name ? `@${data.name}` : data.text || `@${data.id}`;
|
|
247
|
+
await this.sendMessage(numericChatId, mentionText, {
|
|
248
|
+
reply_to_message_id: replyToMessageId,
|
|
249
|
+
...extraOptions
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
// 未知类型,作为文本处理
|
|
254
|
+
const text = data.text || `[${type}]`;
|
|
255
|
+
await this.sendMessage(numericChatId, text, {
|
|
256
|
+
reply_to_message_id: replyToMessageId,
|
|
257
|
+
...extraOptions
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
replyToMessageId = undefined; // 只有第一条消息回复
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// 获取文件信息并下载(如果启用)
|
|
264
|
+
async getFileInfo(fileId) {
|
|
265
|
+
try {
|
|
266
|
+
const file = await this.getFile(fileId);
|
|
267
|
+
const fileUrl = `https://api.telegram.org/file/bot${this.$config.token}/${file.file_path}`;
|
|
268
|
+
return {
|
|
269
|
+
file_id: fileId,
|
|
270
|
+
file_unique_id: file.file_unique_id,
|
|
271
|
+
file_size: file.file_size,
|
|
272
|
+
file_path: file.file_path,
|
|
273
|
+
file_url: fileUrl
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
this.plugin.logger.error('Failed to get file info:', error);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// 下载文件到本地
|
|
282
|
+
async downloadTelegramFile(fileId) {
|
|
283
|
+
if (!this.$config.fileDownload?.enabled) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const fileInfo = await this.getFileInfo(fileId);
|
|
287
|
+
if (!fileInfo)
|
|
288
|
+
return null;
|
|
289
|
+
const downloadPath = this.$config.fileDownload.downloadPath || './downloads';
|
|
290
|
+
const maxFileSize = this.$config.fileDownload.maxFileSize || 20 * 1024 * 1024; // 20MB
|
|
291
|
+
if (fileInfo.file_size && fileInfo.file_size > maxFileSize) {
|
|
292
|
+
this.plugin.logger.warn(`File too large: ${fileInfo.file_size} bytes`);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await fs.mkdir(downloadPath, { recursive: true });
|
|
297
|
+
const fileName = `${fileId}_${Date.now()}.${path.extname(fileInfo.file_path || '')}`;
|
|
298
|
+
const localPath = path.join(downloadPath, fileName);
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const client = fileInfo.file_url?.startsWith('https:') ? https : http;
|
|
301
|
+
const request = client.get(fileInfo.file_url, (response) => {
|
|
302
|
+
if (response.statusCode === 200) {
|
|
303
|
+
const fileStream = createWriteStream(localPath);
|
|
304
|
+
response.pipe(fileStream);
|
|
305
|
+
fileStream.on('finish', () => resolve(localPath));
|
|
306
|
+
fileStream.on('error', reject);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
request.on('error', reject);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
this.plugin.logger.error('Failed to download file:', error);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 同步解析 Telegram 消息内容为 segment 格式(不包含文件下载)
|
|
321
|
+
parseMessageContentSync(msg) {
|
|
322
|
+
const segments = [];
|
|
323
|
+
// 回复消息处理
|
|
324
|
+
if (msg.reply_to_message) {
|
|
325
|
+
segments.push({
|
|
326
|
+
type: 'reply',
|
|
327
|
+
data: {
|
|
328
|
+
id: msg.reply_to_message.message_id.toString(),
|
|
329
|
+
message: this.parseMessageContentSync(msg.reply_to_message)
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// 文本消息(包含实体处理)
|
|
334
|
+
if (msg.text) {
|
|
335
|
+
segments.push(...this.parseTextWithEntities(msg.text, msg.entities));
|
|
336
|
+
}
|
|
337
|
+
// 图片消息
|
|
338
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
339
|
+
const photo = msg.photo[msg.photo.length - 1]; // 取最大尺寸的图片
|
|
340
|
+
segments.push({
|
|
341
|
+
type: 'image',
|
|
342
|
+
data: {
|
|
343
|
+
file_id: photo.file_id,
|
|
344
|
+
file_unique_id: photo.file_unique_id,
|
|
345
|
+
width: photo.width,
|
|
346
|
+
height: photo.height,
|
|
347
|
+
file_size: photo.file_size
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
if (msg.caption) {
|
|
351
|
+
segments.push(...this.parseTextWithEntities(msg.caption, msg.caption_entities));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// 音频消息
|
|
355
|
+
if (msg.audio) {
|
|
356
|
+
segments.push({
|
|
357
|
+
type: 'audio',
|
|
358
|
+
data: {
|
|
359
|
+
file_id: msg.audio.file_id,
|
|
360
|
+
file_unique_id: msg.audio.file_unique_id,
|
|
361
|
+
duration: msg.audio.duration,
|
|
362
|
+
performer: msg.audio.performer,
|
|
363
|
+
title: msg.audio.title,
|
|
364
|
+
mime_type: msg.audio.mime_type,
|
|
365
|
+
file_size: msg.audio.file_size
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// 语音消息
|
|
370
|
+
if (msg.voice) {
|
|
371
|
+
segments.push({
|
|
372
|
+
type: 'voice',
|
|
373
|
+
data: {
|
|
374
|
+
file_id: msg.voice.file_id,
|
|
375
|
+
file_unique_id: msg.voice.file_unique_id,
|
|
376
|
+
duration: msg.voice.duration,
|
|
377
|
+
mime_type: msg.voice.mime_type,
|
|
378
|
+
file_size: msg.voice.file_size
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// 视频消息
|
|
383
|
+
if (msg.video) {
|
|
384
|
+
segments.push({
|
|
385
|
+
type: 'video',
|
|
386
|
+
data: {
|
|
387
|
+
file_id: msg.video.file_id,
|
|
388
|
+
file_unique_id: msg.video.file_unique_id,
|
|
389
|
+
width: msg.video.width,
|
|
390
|
+
height: msg.video.height,
|
|
391
|
+
duration: msg.video.duration,
|
|
392
|
+
mime_type: msg.video.mime_type,
|
|
393
|
+
file_size: msg.video.file_size
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
if (msg.caption) {
|
|
397
|
+
segments.push(...this.parseTextWithEntities(msg.caption, msg.caption_entities));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// 视频笔记(圆形视频)
|
|
401
|
+
if (msg.video_note) {
|
|
402
|
+
segments.push({
|
|
403
|
+
type: 'video_note',
|
|
404
|
+
data: {
|
|
405
|
+
file_id: msg.video_note.file_id,
|
|
406
|
+
file_unique_id: msg.video_note.file_unique_id,
|
|
407
|
+
length: msg.video_note.length,
|
|
408
|
+
duration: msg.video_note.duration,
|
|
409
|
+
file_size: msg.video_note.file_size
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// 文档消息
|
|
414
|
+
if (msg.document) {
|
|
415
|
+
segments.push({
|
|
416
|
+
type: 'document',
|
|
417
|
+
data: {
|
|
418
|
+
file_id: msg.document.file_id,
|
|
419
|
+
file_unique_id: msg.document.file_unique_id,
|
|
420
|
+
file_name: msg.document.file_name,
|
|
421
|
+
mime_type: msg.document.mime_type,
|
|
422
|
+
file_size: msg.document.file_size
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
if (msg.caption) {
|
|
426
|
+
segments.push(...this.parseTextWithEntities(msg.caption, msg.caption_entities));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// 贴纸消息
|
|
430
|
+
if (msg.sticker) {
|
|
431
|
+
segments.push({
|
|
432
|
+
type: 'sticker',
|
|
433
|
+
data: {
|
|
434
|
+
file_id: msg.sticker.file_id,
|
|
435
|
+
file_unique_id: msg.sticker.file_unique_id,
|
|
436
|
+
type: msg.sticker.type,
|
|
437
|
+
width: msg.sticker.width,
|
|
438
|
+
height: msg.sticker.height,
|
|
439
|
+
is_animated: msg.sticker.is_animated,
|
|
440
|
+
is_video: msg.sticker.is_video,
|
|
441
|
+
emoji: msg.sticker.emoji,
|
|
442
|
+
set_name: msg.sticker.set_name,
|
|
443
|
+
file_size: msg.sticker.file_size
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// 位置消息
|
|
448
|
+
if (msg.location) {
|
|
449
|
+
segments.push({
|
|
450
|
+
type: 'location',
|
|
451
|
+
data: {
|
|
452
|
+
longitude: msg.location.longitude,
|
|
453
|
+
latitude: msg.location.latitude
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// 联系人消息
|
|
458
|
+
if (msg.contact) {
|
|
459
|
+
segments.push({
|
|
460
|
+
type: 'contact',
|
|
461
|
+
data: {
|
|
462
|
+
phone_number: msg.contact.phone_number,
|
|
463
|
+
first_name: msg.contact.first_name,
|
|
464
|
+
last_name: msg.contact.last_name,
|
|
465
|
+
user_id: msg.contact.user_id,
|
|
466
|
+
vcard: msg.contact.vcard
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return segments.length > 0 ? segments : [{ type: 'text', data: { text: '' } }];
|
|
471
|
+
}
|
|
472
|
+
// 在后台异步下载消息中的文件
|
|
473
|
+
async downloadMessageFiles(msg) {
|
|
474
|
+
if (!this.$config.fileDownload?.enabled) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const fileIds = [];
|
|
478
|
+
// 收集需要下载的文件ID
|
|
479
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
480
|
+
fileIds.push(msg.photo[msg.photo.length - 1].file_id);
|
|
481
|
+
}
|
|
482
|
+
if (msg.audio) {
|
|
483
|
+
fileIds.push(msg.audio.file_id);
|
|
484
|
+
}
|
|
485
|
+
if (msg.voice) {
|
|
486
|
+
fileIds.push(msg.voice.file_id);
|
|
487
|
+
}
|
|
488
|
+
if (msg.video) {
|
|
489
|
+
fileIds.push(msg.video.file_id);
|
|
490
|
+
}
|
|
491
|
+
if (msg.video_note) {
|
|
492
|
+
fileIds.push(msg.video_note.file_id);
|
|
493
|
+
}
|
|
494
|
+
if (msg.document) {
|
|
495
|
+
fileIds.push(msg.document.file_id);
|
|
496
|
+
}
|
|
497
|
+
if (msg.sticker) {
|
|
498
|
+
fileIds.push(msg.sticker.file_id);
|
|
499
|
+
}
|
|
500
|
+
// 并行下载所有文件
|
|
501
|
+
const downloadPromises = fileIds.map(fileId => this.downloadTelegramFile(fileId).catch(error => {
|
|
502
|
+
this.plugin.logger.warn(`Failed to download file ${fileId}:`, error);
|
|
503
|
+
return null;
|
|
504
|
+
}));
|
|
505
|
+
await Promise.all(downloadPromises);
|
|
506
|
+
}
|
|
507
|
+
// 解析文本实体(@mentions, #hashtags, URLs, etc.)
|
|
508
|
+
parseTextWithEntities(text, entities) {
|
|
509
|
+
if (!entities || entities.length === 0) {
|
|
510
|
+
return [{ type: 'text', data: { text } }];
|
|
511
|
+
}
|
|
512
|
+
const segments = [];
|
|
513
|
+
let lastOffset = 0;
|
|
514
|
+
entities.forEach(entity => {
|
|
515
|
+
// 添加实体前的文本
|
|
516
|
+
if (entity.offset > lastOffset) {
|
|
517
|
+
segments.push({
|
|
518
|
+
type: 'text',
|
|
519
|
+
data: { text: text.slice(lastOffset, entity.offset) }
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const entityText = text.slice(entity.offset, entity.offset + entity.length);
|
|
523
|
+
switch (entity.type) {
|
|
524
|
+
case 'mention':
|
|
525
|
+
segments.push({
|
|
526
|
+
type: 'at',
|
|
527
|
+
data: { text: entityText }
|
|
528
|
+
});
|
|
529
|
+
break;
|
|
530
|
+
case 'text_mention':
|
|
531
|
+
segments.push({
|
|
532
|
+
type: 'at',
|
|
533
|
+
data: {
|
|
534
|
+
id: entity.user?.id?.toString(),
|
|
535
|
+
name: entity.user?.first_name,
|
|
536
|
+
text: entityText
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
break;
|
|
540
|
+
case 'hashtag':
|
|
541
|
+
segments.push({
|
|
542
|
+
type: 'hashtag',
|
|
543
|
+
data: { text: entityText }
|
|
544
|
+
});
|
|
545
|
+
break;
|
|
546
|
+
case 'url':
|
|
547
|
+
case 'text_link':
|
|
548
|
+
segments.push({
|
|
549
|
+
type: 'link',
|
|
550
|
+
data: {
|
|
551
|
+
url: entity.url || entityText,
|
|
552
|
+
text: entityText
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
break;
|
|
556
|
+
case 'bold':
|
|
557
|
+
segments.push({
|
|
558
|
+
type: 'text',
|
|
559
|
+
data: { text: `<b>${entityText}</b>` }
|
|
560
|
+
});
|
|
561
|
+
break;
|
|
562
|
+
case 'italic':
|
|
563
|
+
segments.push({
|
|
564
|
+
type: 'text',
|
|
565
|
+
data: { text: `<i>${entityText}</i>` }
|
|
566
|
+
});
|
|
567
|
+
break;
|
|
568
|
+
case 'code':
|
|
569
|
+
segments.push({
|
|
570
|
+
type: 'text',
|
|
571
|
+
data: { text: `<code>${entityText}</code>` }
|
|
572
|
+
});
|
|
573
|
+
break;
|
|
574
|
+
case 'pre':
|
|
575
|
+
segments.push({
|
|
576
|
+
type: 'text',
|
|
577
|
+
data: { text: `<pre>${entityText}</pre>` }
|
|
578
|
+
});
|
|
579
|
+
break;
|
|
580
|
+
default:
|
|
581
|
+
segments.push({
|
|
582
|
+
type: 'text',
|
|
583
|
+
data: { text: entityText }
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
lastOffset = entity.offset + entity.length;
|
|
587
|
+
});
|
|
588
|
+
// 添加最后剩余的文本
|
|
589
|
+
if (lastOffset < text.length) {
|
|
590
|
+
segments.push({
|
|
591
|
+
type: 'text',
|
|
592
|
+
data: { text: text.slice(lastOffset) }
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return segments;
|
|
596
|
+
}
|
|
597
|
+
// 工具方法:检查文件是否存在
|
|
598
|
+
static async fileExists(filePath) {
|
|
599
|
+
try {
|
|
600
|
+
await fs.access(filePath);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// 工具方法:获取文件扩展名
|
|
608
|
+
static getFileExtension(fileName) {
|
|
609
|
+
return path.extname(fileName).toLowerCase();
|
|
610
|
+
}
|
|
611
|
+
// 工具方法:判断是否为图片文件
|
|
612
|
+
static isImageFile(fileName) {
|
|
613
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
|
614
|
+
return imageExtensions.includes(this.getFileExtension(fileName));
|
|
615
|
+
}
|
|
616
|
+
// 工具方法:判断是否为音频文件
|
|
617
|
+
static isAudioFile(fileName) {
|
|
618
|
+
const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'];
|
|
619
|
+
return audioExtensions.includes(this.getFileExtension(fileName));
|
|
620
|
+
}
|
|
621
|
+
// 工具方法:判断是否为视频文件
|
|
622
|
+
static isVideoFile(fileName) {
|
|
623
|
+
const videoExtensions = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm'];
|
|
624
|
+
return videoExtensions.includes(this.getFileExtension(fileName));
|
|
625
|
+
}
|
|
626
|
+
// 静态方法:将 SendContent 格式化为纯文本(用于日志显示)
|
|
627
|
+
static formatContentToText(content) {
|
|
628
|
+
if (!Array.isArray(content))
|
|
629
|
+
content = [content];
|
|
630
|
+
return content.map(segment => {
|
|
631
|
+
if (typeof segment === 'string')
|
|
632
|
+
return segment;
|
|
633
|
+
switch (segment.type) {
|
|
634
|
+
case 'text':
|
|
635
|
+
return segment.data.text || '';
|
|
636
|
+
case 'at':
|
|
637
|
+
return `@${segment.data.name || segment.data.id}`;
|
|
638
|
+
case 'image':
|
|
639
|
+
return '[图片]';
|
|
640
|
+
case 'audio':
|
|
641
|
+
return '[音频]';
|
|
642
|
+
case 'voice':
|
|
643
|
+
return '[语音]';
|
|
644
|
+
case 'video':
|
|
645
|
+
return '[视频]';
|
|
646
|
+
case 'video_note':
|
|
647
|
+
return '[视频笔记]';
|
|
648
|
+
case 'document':
|
|
649
|
+
case 'file':
|
|
650
|
+
return '[文件]';
|
|
651
|
+
case 'sticker':
|
|
652
|
+
return '[贴纸]';
|
|
653
|
+
case 'location':
|
|
654
|
+
return '[位置]';
|
|
655
|
+
case 'contact':
|
|
656
|
+
return '[联系人]';
|
|
657
|
+
default:
|
|
658
|
+
return `[${segment.type}]`;
|
|
659
|
+
}
|
|
660
|
+
}).join('');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// ================================================================================================
|
|
664
|
+
// TelegramWebhookBot 类(Webhook 模式)
|
|
665
|
+
// ================================================================================================
|
|
666
|
+
export class TelegramWebhookBot extends TelegramBotApi {
|
|
667
|
+
plugin;
|
|
668
|
+
$connected;
|
|
669
|
+
router;
|
|
670
|
+
constructor(plugin, router, config) {
|
|
671
|
+
// webhook 模式不需要 polling
|
|
672
|
+
const options = {
|
|
673
|
+
webHook: false,
|
|
674
|
+
polling: false
|
|
675
|
+
};
|
|
676
|
+
// 如果配置了代理,设置代理
|
|
677
|
+
if (config.proxy) {
|
|
678
|
+
try {
|
|
679
|
+
const proxyUrl = `socks5://${config.proxy.username ? `${config.proxy.username}:${config.proxy.password}@` : ''}${config.proxy.host}:${config.proxy.port}`;
|
|
680
|
+
options.request = {
|
|
681
|
+
agent: new SocksProxyAgent(proxyUrl)
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
console.warn('Failed to configure proxy, continuing without proxy:', error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
super(config.token, options);
|
|
689
|
+
this.plugin = plugin;
|
|
690
|
+
this.$config = config;
|
|
691
|
+
this.$connected = false;
|
|
692
|
+
this.router = router;
|
|
693
|
+
// 设置 webhook 路由
|
|
694
|
+
this.setupWebhookRoute();
|
|
695
|
+
}
|
|
696
|
+
setupWebhookRoute() {
|
|
697
|
+
this.router.post(this.$config.webhookPath, (ctx) => {
|
|
698
|
+
this.handleWebhook(ctx);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async handleWebhook(ctx) {
|
|
702
|
+
try {
|
|
703
|
+
// 验证密钥令牌(如果配置了)
|
|
704
|
+
if (this.$config.secretToken) {
|
|
705
|
+
const authHeader = ctx.get('x-telegram-bot-api-secret-token');
|
|
706
|
+
if (authHeader !== this.$config.secretToken) {
|
|
707
|
+
this.plugin.logger.warn('Invalid secret token in webhook');
|
|
708
|
+
ctx.status = 403;
|
|
709
|
+
ctx.body = 'Forbidden';
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const update = ctx.request.body;
|
|
714
|
+
if (update.message) {
|
|
715
|
+
await this.handleTelegramMessage(update.message);
|
|
716
|
+
}
|
|
717
|
+
ctx.status = 200;
|
|
718
|
+
ctx.body = 'OK';
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
this.plugin.logger.error('Webhook error:', error);
|
|
722
|
+
ctx.status = 500;
|
|
723
|
+
ctx.body = 'Internal Server Error';
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async handleTelegramMessage(msg) {
|
|
727
|
+
const message = this.$formatMessage(msg);
|
|
728
|
+
this.plugin.dispatch('message.receive', message);
|
|
729
|
+
this.plugin.logger.info(`recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
|
|
730
|
+
this.plugin.dispatch(`message.${message.$channel.type}.receive`, message);
|
|
731
|
+
}
|
|
732
|
+
async $connect() {
|
|
733
|
+
try {
|
|
734
|
+
// 设置 webhook URL
|
|
735
|
+
await this.setWebHook(this.$config.webhookUrl, {
|
|
736
|
+
secret_token: this.$config.secretToken
|
|
737
|
+
});
|
|
738
|
+
this.$connected = true;
|
|
739
|
+
this.plugin.logger.info(`Telegram webhook bot connected: ${this.$config.name}`);
|
|
740
|
+
this.plugin.logger.info(`Webhook URL: ${this.$config.webhookUrl}`);
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
this.plugin.logger.error('Failed to set webhook:', error);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async $disconnect() {
|
|
748
|
+
try {
|
|
749
|
+
await this.deleteWebHook();
|
|
750
|
+
this.$connected = false;
|
|
751
|
+
this.plugin.logger.info('Telegram webhook disconnected');
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
this.plugin.logger.error('Error disconnecting webhook:', error);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// 复用原有的消息格式化和发送方法
|
|
758
|
+
$formatMessage(msg) {
|
|
759
|
+
return TelegramBot.prototype.$formatMessage.call(this, msg);
|
|
760
|
+
}
|
|
761
|
+
async $sendMessage(options) {
|
|
762
|
+
return TelegramBot.prototype.$sendMessage.call(this, options);
|
|
763
|
+
}
|
|
764
|
+
// 复用文件下载方法
|
|
765
|
+
async downloadTelegramFile(fileId) {
|
|
766
|
+
return TelegramBot.prototype.downloadTelegramFile.call(this, fileId);
|
|
767
|
+
}
|
|
768
|
+
// 静态方法引用
|
|
769
|
+
static parseMessageContent = TelegramBot.parseMessageContent;
|
|
770
|
+
static formatSendContent = TelegramBot.formatSendContent;
|
|
771
|
+
}
|
|
772
|
+
// 注册 polling 模式适配器
|
|
773
|
+
registerAdapter(new Adapter('telegram', (plugin, config) => new TelegramBot(plugin, config)));
|
|
774
|
+
// 注册 webhook 模式适配器(需要 router)
|
|
775
|
+
useContext('router', (router) => {
|
|
776
|
+
registerAdapter(new Adapter('telegram-webhook', (plugin, config) => new TelegramWebhookBot(plugin, router, config)));
|
|
777
|
+
});
|
|
778
|
+
//# sourceMappingURL=index.js.map
|