@zhin.js/adapter-telegram 1.0.2 → 1.0.4
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 +33 -6
- package/README.md +70 -290
- package/lib/index.d.ts +36 -70
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +405 -644
- package/lib/index.js.map +1 -1
- package/package.json +25 -15
- package/src/index.ts +0 -929
- package/tsconfig.json +0 -24
package/lib/index.js
CHANGED
|
@@ -1,778 +1,539 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Adapter,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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;
|
|
1
|
+
import { Telegraf } from "telegraf";
|
|
2
|
+
import { Adapter, Message, segment, usePlugin, } from "zhin.js";
|
|
3
|
+
const plugin = usePlugin();
|
|
4
|
+
const { provide, useContext } = plugin;
|
|
5
|
+
export class TelegramBot extends Telegraf {
|
|
6
|
+
adapter;
|
|
7
|
+
$config;
|
|
8
|
+
$connected = false;
|
|
9
|
+
get $id() {
|
|
10
|
+
return this.$config.name;
|
|
35
11
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
this
|
|
40
|
-
this.plugin.dispatch(`message.${message.$channel.type}.receive`, message);
|
|
12
|
+
constructor(adapter, $config) {
|
|
13
|
+
super($config.token);
|
|
14
|
+
this.adapter = adapter;
|
|
15
|
+
this.$config = $config;
|
|
41
16
|
}
|
|
42
17
|
async $connect() {
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
this.on(
|
|
46
|
-
|
|
47
|
-
this.on('polling_error', (error) => {
|
|
48
|
-
this.plugin.logger.error('Telegram polling error:', error);
|
|
49
|
-
this.$connected = false;
|
|
50
|
-
reject(error);
|
|
18
|
+
try {
|
|
19
|
+
// Set up message handler
|
|
20
|
+
this.on("message", async (ctx) => {
|
|
21
|
+
await this.handleTelegramMessage(ctx);
|
|
51
22
|
});
|
|
52
|
-
//
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
reject(error);
|
|
23
|
+
// Set up callback query handler (for inline buttons)
|
|
24
|
+
this.on("callback_query", async (ctx) => {
|
|
25
|
+
// Handle callback queries as special messages
|
|
26
|
+
if (ctx.callbackQuery && "data" in ctx.callbackQuery) {
|
|
27
|
+
const message = this.$formatCallbackQuery(ctx);
|
|
28
|
+
this.adapter.emit("message.receive", message);
|
|
29
|
+
plugin.logger.debug(`${this.$config.name} recv callback ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
|
|
30
|
+
}
|
|
61
31
|
});
|
|
62
|
-
|
|
32
|
+
// Start bot with polling or webhook
|
|
33
|
+
if (this.$config.polling !== false) {
|
|
34
|
+
// Use polling by default
|
|
35
|
+
await this.launch({
|
|
36
|
+
allowedUpdates: this.$config.allowedUpdates,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (this.$config.webhook) {
|
|
40
|
+
// Use webhook
|
|
41
|
+
const { domain, path = "/telegram-webhook", port } = this.$config.webhook;
|
|
42
|
+
await this.launch({
|
|
43
|
+
webhook: {
|
|
44
|
+
domain,
|
|
45
|
+
port,
|
|
46
|
+
hookPath: path,
|
|
47
|
+
},
|
|
48
|
+
allowedUpdates: this.$config.allowedUpdates,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new Error("Either polling must be enabled or webhook configuration must be provided");
|
|
53
|
+
}
|
|
54
|
+
this.$connected = true;
|
|
55
|
+
const me = await this.telegram.getMe();
|
|
56
|
+
plugin.logger.info(`Telegram bot ${this.$config.name} connected successfully as @${me.username}`);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
plugin.logger.error("Failed to connect Telegram bot:", error);
|
|
60
|
+
this.$connected = false;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
63
|
}
|
|
64
64
|
async $disconnect() {
|
|
65
65
|
try {
|
|
66
|
-
await this.
|
|
66
|
+
await this.stop();
|
|
67
67
|
this.$connected = false;
|
|
68
|
-
|
|
68
|
+
plugin.logger.info(`Telegram bot ${this.$config.name} disconnected`);
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
71
|
-
|
|
71
|
+
plugin.logger.error("Error disconnecting Telegram bot:", error);
|
|
72
72
|
throw error;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
async handleTelegramMessage(ctx) {
|
|
76
|
+
if (!ctx.message)
|
|
77
|
+
return;
|
|
78
|
+
const message = this.$formatMessage(ctx.message);
|
|
79
|
+
this.adapter.emit("message.receive", message);
|
|
80
|
+
plugin.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
|
|
81
|
+
}
|
|
75
82
|
$formatMessage(msg) {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
});
|
|
83
|
+
// Determine channel type
|
|
84
|
+
const channelType = msg.chat.type === "private" ? "private" : "group";
|
|
85
|
+
const channelId = msg.chat.id.toString();
|
|
86
|
+
// Parse message content
|
|
87
|
+
const content = this.parseMessageContent(msg);
|
|
101
88
|
const result = Message.from(msg, {
|
|
102
89
|
$id: msg.message_id.toString(),
|
|
103
|
-
$adapter:
|
|
90
|
+
$adapter: "telegram",
|
|
104
91
|
$bot: this.$config.name,
|
|
105
92
|
$sender: {
|
|
106
|
-
id: msg.from?.id.toString() ||
|
|
107
|
-
name: msg.from
|
|
93
|
+
id: msg.from?.id.toString() || "",
|
|
94
|
+
name: msg.from?.username || msg.from?.first_name || "Unknown",
|
|
108
95
|
},
|
|
109
96
|
$channel: {
|
|
110
97
|
id: channelId,
|
|
111
|
-
type: channelType
|
|
98
|
+
type: channelType,
|
|
112
99
|
},
|
|
113
100
|
$content: content,
|
|
114
|
-
$raw:
|
|
115
|
-
$timestamp: msg.date * 1000,
|
|
101
|
+
$raw: "text" in msg ? msg.text || "" : "",
|
|
102
|
+
$timestamp: msg.date * 1000,
|
|
103
|
+
$recall: async () => {
|
|
104
|
+
try {
|
|
105
|
+
await this.telegram.deleteMessage(parseInt(channelId), parseInt(result.$id));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
plugin.logger.error("Error recalling Telegram message:", error);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
116
112
|
$reply: async (content, quote) => {
|
|
117
113
|
if (!Array.isArray(content))
|
|
118
114
|
content = [content];
|
|
119
115
|
const sendOptions = {};
|
|
120
|
-
//
|
|
116
|
+
// Handle reply
|
|
121
117
|
if (quote) {
|
|
122
|
-
|
|
118
|
+
const replyToMessageId = typeof quote === "boolean" ? result.$id : quote;
|
|
119
|
+
sendOptions.reply_parameters = { message_id: parseInt(replyToMessageId) };
|
|
123
120
|
}
|
|
124
|
-
await this.sendContentToChat(channelId, content, sendOptions);
|
|
125
|
-
|
|
121
|
+
const sentMsg = await this.sendContentToChat(parseInt(channelId), content, sendOptions);
|
|
122
|
+
return sentMsg.message_id.toString();
|
|
123
|
+
},
|
|
126
124
|
});
|
|
127
125
|
return result;
|
|
128
126
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
$formatCallbackQuery(ctx) {
|
|
128
|
+
if (!ctx.callbackQuery || !("data" in ctx.callbackQuery)) {
|
|
129
|
+
throw new Error("Invalid callback query");
|
|
130
|
+
}
|
|
131
|
+
const query = ctx.callbackQuery;
|
|
132
|
+
const msg = query.message;
|
|
133
|
+
const channelType = msg && "chat" in msg && msg.chat.type === "private" ? "private" : "group";
|
|
134
|
+
const channelId = msg && "chat" in msg ? msg.chat.id.toString() : query.from.id.toString();
|
|
135
|
+
const result = Message.from(query, {
|
|
136
|
+
$id: query.id,
|
|
137
|
+
$adapter: "telegram",
|
|
138
|
+
$bot: this.$config.name,
|
|
139
|
+
$sender: {
|
|
140
|
+
id: query.from.id.toString(),
|
|
141
|
+
name: query.from.username || query.from.first_name,
|
|
142
|
+
},
|
|
143
|
+
$channel: {
|
|
144
|
+
id: channelId,
|
|
145
|
+
type: channelType,
|
|
146
|
+
},
|
|
147
|
+
$content: [{ type: "text", data: { text: query.data } }],
|
|
148
|
+
$raw: query.data,
|
|
149
|
+
$timestamp: Date.now(),
|
|
150
|
+
$recall: async () => {
|
|
151
|
+
// Callback queries cannot be recalled
|
|
152
|
+
},
|
|
153
|
+
$reply: async (content) => {
|
|
154
|
+
if (!Array.isArray(content))
|
|
155
|
+
content = [content];
|
|
156
|
+
const sentMsg = await this.sendContentToChat(parseInt(channelId), content);
|
|
157
|
+
return sentMsg.message_id.toString();
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
return result;
|
|
140
161
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...extraOptions
|
|
162
|
+
parseMessageContent(msg) {
|
|
163
|
+
const segments = [];
|
|
164
|
+
// Handle text messages
|
|
165
|
+
if ("text" in msg && msg.text) {
|
|
166
|
+
// Check for reply
|
|
167
|
+
if (msg.reply_to_message) {
|
|
168
|
+
segments.push({
|
|
169
|
+
type: "reply",
|
|
170
|
+
data: {
|
|
171
|
+
id: msg.reply_to_message.message_id.toString(),
|
|
172
|
+
},
|
|
153
173
|
});
|
|
154
|
-
replyToMessageId = undefined; // 只有第一条消息回复
|
|
155
|
-
continue;
|
|
156
174
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
});
|
|
175
|
+
// Parse text with entities
|
|
176
|
+
if (msg.entities && msg.entities.length > 0) {
|
|
177
|
+
segments.push(...this.parseTextWithEntities(msg.text, msg.entities));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
segments.push({ type: "text", data: { text: msg.text } });
|
|
259
181
|
}
|
|
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
182
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const segments = [];
|
|
323
|
-
// 回复消息处理
|
|
324
|
-
if (msg.reply_to_message) {
|
|
183
|
+
// Handle photo
|
|
184
|
+
if ("photo" in msg && msg.photo) {
|
|
185
|
+
const largestPhoto = msg.photo[msg.photo.length - 1];
|
|
325
186
|
segments.push({
|
|
326
|
-
type:
|
|
187
|
+
type: "image",
|
|
327
188
|
data: {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
189
|
+
file_id: largestPhoto.file_id,
|
|
190
|
+
file_unique_id: largestPhoto.file_unique_id,
|
|
191
|
+
width: largestPhoto.width,
|
|
192
|
+
height: largestPhoto.height,
|
|
193
|
+
file_size: largestPhoto.file_size,
|
|
194
|
+
},
|
|
331
195
|
});
|
|
196
|
+
if (msg.caption) {
|
|
197
|
+
segments.push({ type: "text", data: { text: msg.caption } });
|
|
198
|
+
}
|
|
332
199
|
}
|
|
333
|
-
//
|
|
334
|
-
if (msg.
|
|
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]; // 取最大尺寸的图片
|
|
200
|
+
// Handle video
|
|
201
|
+
if ("video" in msg && msg.video) {
|
|
340
202
|
segments.push({
|
|
341
|
-
type:
|
|
203
|
+
type: "video",
|
|
342
204
|
data: {
|
|
343
|
-
file_id:
|
|
344
|
-
file_unique_id:
|
|
345
|
-
width:
|
|
346
|
-
height:
|
|
347
|
-
|
|
348
|
-
|
|
205
|
+
file_id: msg.video.file_id,
|
|
206
|
+
file_unique_id: msg.video.file_unique_id,
|
|
207
|
+
width: msg.video.width,
|
|
208
|
+
height: msg.video.height,
|
|
209
|
+
duration: msg.video.duration,
|
|
210
|
+
file_size: msg.video.file_size,
|
|
211
|
+
},
|
|
349
212
|
});
|
|
350
213
|
if (msg.caption) {
|
|
351
|
-
segments.push(
|
|
214
|
+
segments.push({ type: "text", data: { text: msg.caption } });
|
|
352
215
|
}
|
|
353
216
|
}
|
|
354
|
-
//
|
|
355
|
-
if (msg.audio) {
|
|
217
|
+
// Handle audio
|
|
218
|
+
if ("audio" in msg && msg.audio) {
|
|
356
219
|
segments.push({
|
|
357
|
-
type:
|
|
220
|
+
type: "audio",
|
|
358
221
|
data: {
|
|
359
222
|
file_id: msg.audio.file_id,
|
|
360
223
|
file_unique_id: msg.audio.file_unique_id,
|
|
361
224
|
duration: msg.audio.duration,
|
|
362
225
|
performer: msg.audio.performer,
|
|
363
226
|
title: msg.audio.title,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
227
|
+
file_size: msg.audio.file_size,
|
|
228
|
+
},
|
|
367
229
|
});
|
|
368
230
|
}
|
|
369
|
-
//
|
|
370
|
-
if (msg.voice) {
|
|
231
|
+
// Handle voice
|
|
232
|
+
if ("voice" in msg && msg.voice) {
|
|
371
233
|
segments.push({
|
|
372
|
-
type:
|
|
234
|
+
type: "voice",
|
|
373
235
|
data: {
|
|
374
236
|
file_id: msg.voice.file_id,
|
|
375
237
|
file_unique_id: msg.voice.file_unique_id,
|
|
376
238
|
duration: msg.voice.duration,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
239
|
+
file_size: msg.voice.file_size,
|
|
240
|
+
},
|
|
380
241
|
});
|
|
381
242
|
}
|
|
382
|
-
//
|
|
383
|
-
if (msg.
|
|
243
|
+
// Handle document
|
|
244
|
+
if ("document" in msg && msg.document) {
|
|
384
245
|
segments.push({
|
|
385
|
-
type:
|
|
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',
|
|
246
|
+
type: "file",
|
|
417
247
|
data: {
|
|
418
248
|
file_id: msg.document.file_id,
|
|
419
249
|
file_unique_id: msg.document.file_unique_id,
|
|
420
250
|
file_name: msg.document.file_name,
|
|
421
251
|
mime_type: msg.document.mime_type,
|
|
422
|
-
file_size: msg.document.file_size
|
|
423
|
-
}
|
|
252
|
+
file_size: msg.document.file_size,
|
|
253
|
+
},
|
|
424
254
|
});
|
|
425
|
-
if (msg.caption) {
|
|
426
|
-
segments.push(...this.parseTextWithEntities(msg.caption, msg.caption_entities));
|
|
427
|
-
}
|
|
428
255
|
}
|
|
429
|
-
//
|
|
430
|
-
if (msg.sticker) {
|
|
256
|
+
// Handle sticker
|
|
257
|
+
if ("sticker" in msg && msg.sticker) {
|
|
431
258
|
segments.push({
|
|
432
|
-
type:
|
|
259
|
+
type: "sticker",
|
|
433
260
|
data: {
|
|
434
261
|
file_id: msg.sticker.file_id,
|
|
435
262
|
file_unique_id: msg.sticker.file_unique_id,
|
|
436
|
-
type: msg.sticker.type,
|
|
437
263
|
width: msg.sticker.width,
|
|
438
264
|
height: msg.sticker.height,
|
|
439
265
|
is_animated: msg.sticker.is_animated,
|
|
440
266
|
is_video: msg.sticker.is_video,
|
|
441
267
|
emoji: msg.sticker.emoji,
|
|
442
|
-
|
|
443
|
-
file_size: msg.sticker.file_size
|
|
444
|
-
}
|
|
268
|
+
},
|
|
445
269
|
});
|
|
446
270
|
}
|
|
447
|
-
//
|
|
448
|
-
if (msg.location) {
|
|
271
|
+
// Handle location
|
|
272
|
+
if ("location" in msg && msg.location) {
|
|
449
273
|
segments.push({
|
|
450
|
-
type:
|
|
274
|
+
type: "location",
|
|
451
275
|
data: {
|
|
452
276
|
longitude: msg.location.longitude,
|
|
453
|
-
latitude: msg.location.latitude
|
|
454
|
-
}
|
|
277
|
+
latitude: msg.location.latitude,
|
|
278
|
+
},
|
|
455
279
|
});
|
|
456
280
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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: '' } }];
|
|
281
|
+
return segments.length > 0
|
|
282
|
+
? segments
|
|
283
|
+
: [{ type: "text", data: { text: "" } }];
|
|
471
284
|
}
|
|
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
285
|
parseTextWithEntities(text, entities) {
|
|
509
|
-
if (!entities || entities.length === 0) {
|
|
510
|
-
return [{ type: 'text', data: { text } }];
|
|
511
|
-
}
|
|
512
286
|
const segments = [];
|
|
513
287
|
let lastOffset = 0;
|
|
514
|
-
|
|
515
|
-
//
|
|
288
|
+
for (const entity of entities) {
|
|
289
|
+
// Add text before entity
|
|
516
290
|
if (entity.offset > lastOffset) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
data: { text:
|
|
520
|
-
}
|
|
291
|
+
const beforeText = text.slice(lastOffset, entity.offset);
|
|
292
|
+
if (beforeText) {
|
|
293
|
+
segments.push({ type: "text", data: { text: beforeText } });
|
|
294
|
+
}
|
|
521
295
|
}
|
|
522
296
|
const entityText = text.slice(entity.offset, entity.offset + entity.length);
|
|
523
297
|
switch (entity.type) {
|
|
524
|
-
case
|
|
298
|
+
case "mention":
|
|
299
|
+
case "text_mention":
|
|
525
300
|
segments.push({
|
|
526
|
-
type:
|
|
527
|
-
data: { text: entityText }
|
|
528
|
-
});
|
|
529
|
-
break;
|
|
530
|
-
case 'text_mention':
|
|
531
|
-
segments.push({
|
|
532
|
-
type: 'at',
|
|
301
|
+
type: "at",
|
|
533
302
|
data: {
|
|
534
|
-
id: entity.user?.id
|
|
535
|
-
name: entity.user?.
|
|
536
|
-
text: entityText
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
break;
|
|
540
|
-
case 'hashtag':
|
|
541
|
-
segments.push({
|
|
542
|
-
type: 'hashtag',
|
|
543
|
-
data: { text: entityText }
|
|
303
|
+
id: ("user" in entity && entity.user?.id.toString()) || entityText.slice(1),
|
|
304
|
+
name: ("user" in entity && entity.user?.username) || entityText,
|
|
305
|
+
text: entityText,
|
|
306
|
+
},
|
|
544
307
|
});
|
|
545
308
|
break;
|
|
546
|
-
case
|
|
547
|
-
case
|
|
309
|
+
case "url":
|
|
310
|
+
case "text_link":
|
|
548
311
|
segments.push({
|
|
549
|
-
type:
|
|
312
|
+
type: "link",
|
|
550
313
|
data: {
|
|
551
|
-
url: entity.url || entityText,
|
|
552
|
-
text: entityText
|
|
553
|
-
}
|
|
314
|
+
url: ("url" in entity && entity.url) || entityText,
|
|
315
|
+
text: entityText,
|
|
316
|
+
},
|
|
554
317
|
});
|
|
555
318
|
break;
|
|
556
|
-
case
|
|
319
|
+
case "hashtag":
|
|
557
320
|
segments.push({
|
|
558
|
-
type:
|
|
559
|
-
data: { text:
|
|
321
|
+
type: "text",
|
|
322
|
+
data: { text: entityText },
|
|
560
323
|
});
|
|
561
324
|
break;
|
|
562
|
-
case
|
|
325
|
+
case "bold":
|
|
326
|
+
case "italic":
|
|
327
|
+
case "code":
|
|
328
|
+
case "pre":
|
|
329
|
+
case "underline":
|
|
330
|
+
case "strikethrough":
|
|
563
331
|
segments.push({
|
|
564
|
-
type:
|
|
565
|
-
data: { text:
|
|
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>` }
|
|
332
|
+
type: "text",
|
|
333
|
+
data: { text: entityText, format: entity.type },
|
|
578
334
|
});
|
|
579
335
|
break;
|
|
580
336
|
default:
|
|
581
|
-
segments.push({
|
|
582
|
-
type: 'text',
|
|
583
|
-
data: { text: entityText }
|
|
584
|
-
});
|
|
337
|
+
segments.push({ type: "text", data: { text: entityText } });
|
|
585
338
|
}
|
|
586
339
|
lastOffset = entity.offset + entity.length;
|
|
587
|
-
}
|
|
588
|
-
//
|
|
340
|
+
}
|
|
341
|
+
// Add remaining text
|
|
589
342
|
if (lastOffset < text.length) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
data: { text:
|
|
593
|
-
}
|
|
343
|
+
const remainingText = text.slice(lastOffset);
|
|
344
|
+
if (remainingText) {
|
|
345
|
+
segments.push({ type: "text", data: { text: remainingText } });
|
|
346
|
+
}
|
|
594
347
|
}
|
|
595
348
|
return segments;
|
|
596
349
|
}
|
|
597
|
-
|
|
598
|
-
static async fileExists(filePath) {
|
|
350
|
+
async $sendMessage(options) {
|
|
599
351
|
try {
|
|
600
|
-
|
|
601
|
-
|
|
352
|
+
const chatId = parseInt(options.id);
|
|
353
|
+
const result = await this.sendContentToChat(chatId, options.content);
|
|
354
|
+
plugin.logger.debug(`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`);
|
|
355
|
+
return result.message_id.toString();
|
|
602
356
|
}
|
|
603
|
-
catch {
|
|
604
|
-
|
|
357
|
+
catch (error) {
|
|
358
|
+
plugin.logger.error("Failed to send Telegram message:", error);
|
|
359
|
+
throw error;
|
|
605
360
|
}
|
|
606
361
|
}
|
|
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) {
|
|
362
|
+
async sendContentToChat(chatId, content, extraOptions = {}) {
|
|
628
363
|
if (!Array.isArray(content))
|
|
629
364
|
content = [content];
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
}
|
|
365
|
+
let textContent = "";
|
|
366
|
+
let hasMedia = false;
|
|
367
|
+
for (const segment of content) {
|
|
368
|
+
if (typeof segment === "string") {
|
|
369
|
+
textContent += segment;
|
|
370
|
+
continue;
|
|
712
371
|
}
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
372
|
+
const { type, data } = segment;
|
|
373
|
+
switch (type) {
|
|
374
|
+
case "text":
|
|
375
|
+
textContent += data.text || "";
|
|
376
|
+
break;
|
|
377
|
+
case "at":
|
|
378
|
+
if (data.id) {
|
|
379
|
+
textContent += `@${data.name || data.id}`;
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
case "image":
|
|
383
|
+
hasMedia = true;
|
|
384
|
+
if (data.file_id) {
|
|
385
|
+
// Send by file_id
|
|
386
|
+
return await this.telegram.sendPhoto(chatId, data.file_id, {
|
|
387
|
+
caption: textContent || undefined,
|
|
388
|
+
...extraOptions,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else if (data.url) {
|
|
392
|
+
// Send by URL
|
|
393
|
+
return await this.telegram.sendPhoto(chatId, data.url, {
|
|
394
|
+
caption: textContent || undefined,
|
|
395
|
+
...extraOptions,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else if (data.file) {
|
|
399
|
+
// Send by file path
|
|
400
|
+
return await this.telegram.sendPhoto(chatId, { source: data.file }, {
|
|
401
|
+
caption: textContent || undefined,
|
|
402
|
+
...extraOptions,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case "video":
|
|
407
|
+
hasMedia = true;
|
|
408
|
+
if (data.file_id) {
|
|
409
|
+
return await this.telegram.sendVideo(chatId, data.file_id, {
|
|
410
|
+
caption: textContent || undefined,
|
|
411
|
+
...extraOptions,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
else if (data.url) {
|
|
415
|
+
return await this.telegram.sendVideo(chatId, data.url, {
|
|
416
|
+
caption: textContent || undefined,
|
|
417
|
+
...extraOptions,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else if (data.file) {
|
|
421
|
+
return await this.telegram.sendVideo(chatId, { source: data.file }, {
|
|
422
|
+
caption: textContent || undefined,
|
|
423
|
+
...extraOptions,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
case "audio":
|
|
428
|
+
hasMedia = true;
|
|
429
|
+
if (data.file_id) {
|
|
430
|
+
return await this.telegram.sendAudio(chatId, data.file_id, {
|
|
431
|
+
caption: textContent || undefined,
|
|
432
|
+
...extraOptions,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
else if (data.url) {
|
|
436
|
+
return await this.telegram.sendAudio(chatId, data.url, {
|
|
437
|
+
caption: textContent || undefined,
|
|
438
|
+
...extraOptions,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else if (data.file) {
|
|
442
|
+
return await this.telegram.sendAudio(chatId, { source: data.file }, {
|
|
443
|
+
caption: textContent || undefined,
|
|
444
|
+
...extraOptions,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
case "voice":
|
|
449
|
+
hasMedia = true;
|
|
450
|
+
if (data.file_id) {
|
|
451
|
+
return await this.telegram.sendVoice(chatId, data.file_id, {
|
|
452
|
+
caption: textContent || undefined,
|
|
453
|
+
...extraOptions,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
else if (data.url) {
|
|
457
|
+
return await this.telegram.sendVoice(chatId, data.url, {
|
|
458
|
+
caption: textContent || undefined,
|
|
459
|
+
...extraOptions,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
else if (data.file) {
|
|
463
|
+
return await this.telegram.sendVoice(chatId, { source: data.file }, {
|
|
464
|
+
caption: textContent || undefined,
|
|
465
|
+
...extraOptions,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
case "file":
|
|
470
|
+
hasMedia = true;
|
|
471
|
+
if (data.file_id) {
|
|
472
|
+
return await this.telegram.sendDocument(chatId, data.file_id, {
|
|
473
|
+
caption: textContent || undefined,
|
|
474
|
+
...extraOptions,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
else if (data.url) {
|
|
478
|
+
return await this.telegram.sendDocument(chatId, data.url, {
|
|
479
|
+
caption: textContent || undefined,
|
|
480
|
+
...extraOptions,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
else if (data.file) {
|
|
484
|
+
return await this.telegram.sendDocument(chatId, { source: data.file }, {
|
|
485
|
+
caption: textContent || undefined,
|
|
486
|
+
...extraOptions,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
break;
|
|
490
|
+
case "sticker":
|
|
491
|
+
if (data.file_id) {
|
|
492
|
+
hasMedia = true;
|
|
493
|
+
return await this.telegram.sendSticker(chatId, data.file_id, extraOptions);
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
case "location":
|
|
497
|
+
return await this.telegram.sendLocation(chatId, data.latitude, data.longitude, extraOptions);
|
|
498
|
+
default:
|
|
499
|
+
// Unknown segment type, add as text
|
|
500
|
+
textContent += data.text || `[${type}]`;
|
|
716
501
|
}
|
|
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
502
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
503
|
+
// If no media was sent, send as text message
|
|
504
|
+
if (!hasMedia && textContent.trim()) {
|
|
505
|
+
return await this.telegram.sendMessage(chatId, textContent.trim(), extraOptions);
|
|
745
506
|
}
|
|
507
|
+
// If neither media nor text was sent, this is an error
|
|
508
|
+
throw new Error("TelegramBot.$sendMessage: No media or text content to send.");
|
|
746
509
|
}
|
|
747
|
-
async $
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
catch (error) {
|
|
754
|
-
this.plugin.logger.error('Error disconnecting webhook:', error);
|
|
755
|
-
}
|
|
510
|
+
async $recallMessage(id) {
|
|
511
|
+
// Telegram requires both chat_id and message_id to delete a message
|
|
512
|
+
// The Bot interface only provides message_id, making recall impossible
|
|
513
|
+
// Users should use message.$recall() instead, which has the full context
|
|
514
|
+
throw new Error("TelegramBot.$recallMessage: Message recall not supported without chat_id. " +
|
|
515
|
+
"Use message.$recall() method instead, which contains the required context.");
|
|
756
516
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
async $sendMessage(options) {
|
|
762
|
-
return TelegramBot.prototype.$sendMessage.call(this, options);
|
|
517
|
+
}
|
|
518
|
+
class TelegramAdapter extends Adapter {
|
|
519
|
+
constructor(plugin) {
|
|
520
|
+
super(plugin, "telegram", []);
|
|
763
521
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return TelegramBot.prototype.downloadTelegramFile.call(this, fileId);
|
|
522
|
+
createBot(config) {
|
|
523
|
+
return new TelegramBot(this, config);
|
|
767
524
|
}
|
|
768
|
-
// 静态方法引用
|
|
769
|
-
static parseMessageContent = TelegramBot.parseMessageContent;
|
|
770
|
-
static formatSendContent = TelegramBot.formatSendContent;
|
|
771
525
|
}
|
|
772
|
-
//
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
526
|
+
// 使用新的 provide() API 注册适配器
|
|
527
|
+
provide({
|
|
528
|
+
name: "telegram",
|
|
529
|
+
description: "Telegram Bot Adapter",
|
|
530
|
+
mounted: async (p) => {
|
|
531
|
+
const adapter = new TelegramAdapter(p);
|
|
532
|
+
await adapter.start();
|
|
533
|
+
return adapter;
|
|
534
|
+
},
|
|
535
|
+
dispose: async (adapter) => {
|
|
536
|
+
await adapter.stop();
|
|
537
|
+
},
|
|
777
538
|
});
|
|
778
539
|
//# sourceMappingURL=index.js.map
|