@goodandready/dsh-messenger-gateway 0.3.21 → 0.4.0
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/LICENSE +1 -1
- package/cordis.patch.yml +1 -1
- package/lib/adapters/discord.js +9 -1
- package/lib/adapters/slack.js +7 -1
- package/lib/adapters/telegram-inbound.js +141 -0
- package/lib/adapters/telegram.js +42 -154
- package/lib/alerts.js +104 -63
- package/lib/api-health.js +38 -0
- package/lib/artifacts.js +118 -118
- package/lib/ask.js +203 -152
- package/lib/client.js +1660 -1312
- package/lib/commands.js +53 -0
- package/lib/config.js +104 -104
- package/lib/content-guard.js +14 -14
- package/lib/documents.js +1 -1
- package/lib/file-manager.js +27 -1
- package/lib/forum-mirror.js +105 -0
- package/lib/gateway-callbacks.js +187 -0
- package/lib/gateway-commands.js +584 -0
- package/lib/gateway-turn.js +285 -0
- package/lib/gateway.js +590 -1737
- package/lib/http.js +12 -1
- package/lib/index.js +48 -8
- package/lib/integrations.js +1 -1
- package/lib/locales/en.js +123 -114
- package/lib/locales/index.js +21 -21
- package/lib/locales/zh.js +123 -114
- package/lib/media.js +1 -9
- package/lib/models.js +6 -3
- package/lib/outbound.js +1 -1
- package/lib/pairing.js +8 -4
- package/lib/personas.js +8 -4
- package/lib/photos.js +1 -1
- package/lib/scheduler.js +14 -4
- package/lib/stream.js +115 -115
- package/lib/text.js +39 -39
- package/lib/topics.js +0 -14
- package/lib/tts.js +0 -10
- package/lib/updater.js +304 -0
- package/lib/voice-prefs.js +8 -4
- package/package.json +69 -65
package/LICENSE
CHANGED
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/cordis.patch.yml
CHANGED
package/lib/adapters/discord.js
CHANGED
|
@@ -84,6 +84,8 @@ export class DiscordAdapter {
|
|
|
84
84
|
method: 'POST',
|
|
85
85
|
headers: { 'Content-Type': 'application/json' },
|
|
86
86
|
body: JSON.stringify(payloadJson),
|
|
87
|
+
keepalive: true,
|
|
88
|
+
signal: AbortSignal.timeout(15000),
|
|
87
89
|
})
|
|
88
90
|
if (!res.ok) {
|
|
89
91
|
const errText = await res.text().catch(() => '')
|
|
@@ -122,6 +124,8 @@ export class DiscordAdapter {
|
|
|
122
124
|
Authorization: `Bot ${this.botToken}`,
|
|
123
125
|
},
|
|
124
126
|
body: form,
|
|
127
|
+
keepalive: true,
|
|
128
|
+
signal: AbortSignal.timeout(30000),
|
|
125
129
|
})
|
|
126
130
|
if (!res.ok) {
|
|
127
131
|
const errText = await res.text().catch(() => '')
|
|
@@ -156,6 +160,8 @@ export class DiscordAdapter {
|
|
|
156
160
|
'Content-Type': 'application/json',
|
|
157
161
|
},
|
|
158
162
|
body: JSON.stringify(body),
|
|
163
|
+
keepalive: true,
|
|
164
|
+
signal: AbortSignal.timeout(15000),
|
|
159
165
|
})
|
|
160
166
|
if (!res.ok) {
|
|
161
167
|
const errText = await res.text().catch(() => '')
|
|
@@ -174,6 +180,8 @@ export class DiscordAdapter {
|
|
|
174
180
|
'Content-Type': 'application/json',
|
|
175
181
|
},
|
|
176
182
|
body: JSON.stringify(body),
|
|
183
|
+
keepalive: true,
|
|
184
|
+
signal: AbortSignal.timeout(15000),
|
|
177
185
|
})
|
|
178
186
|
if (!res.ok) {
|
|
179
187
|
const errText = await res.text().catch(() => '')
|
|
@@ -182,4 +190,4 @@ export class DiscordAdapter {
|
|
|
182
190
|
const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
|
|
183
191
|
return { ok: true, messageId: json?.id }
|
|
184
192
|
}
|
|
185
|
-
}
|
|
193
|
+
}
|
package/lib/adapters/slack.js
CHANGED
|
@@ -83,6 +83,8 @@ export class SlackAdapter {
|
|
|
83
83
|
method: 'POST',
|
|
84
84
|
headers: { 'Content-Type': 'application/json' },
|
|
85
85
|
body: JSON.stringify(payloadJson),
|
|
86
|
+
keepalive: true,
|
|
87
|
+
signal: AbortSignal.timeout(15000),
|
|
86
88
|
})
|
|
87
89
|
if (!res.ok) {
|
|
88
90
|
const errText = await res.text().catch(() => '')
|
|
@@ -116,6 +118,8 @@ export class SlackAdapter {
|
|
|
116
118
|
'Content-Type': 'application/json; charset=utf-8',
|
|
117
119
|
},
|
|
118
120
|
body: JSON.stringify(reqBody),
|
|
121
|
+
keepalive: true,
|
|
122
|
+
signal: AbortSignal.timeout(15000),
|
|
119
123
|
})
|
|
120
124
|
const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
|
|
121
125
|
if (!res.ok || json?.ok === false) {
|
|
@@ -141,6 +145,8 @@ export class SlackAdapter {
|
|
|
141
145
|
'Content-Type': 'application/json; charset=utf-8',
|
|
142
146
|
},
|
|
143
147
|
body: JSON.stringify(body),
|
|
148
|
+
keepalive: true,
|
|
149
|
+
signal: AbortSignal.timeout(15000),
|
|
144
150
|
})
|
|
145
151
|
const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
|
|
146
152
|
if (!res.ok || json?.ok === false) {
|
|
@@ -148,4 +154,4 @@ export class SlackAdapter {
|
|
|
148
154
|
}
|
|
149
155
|
return { ok: true }
|
|
150
156
|
}
|
|
151
|
-
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
IMAGE_EXT_TO_MIME, VIDEO_EXT_TO_MIME, classifyDocument,
|
|
4
|
+
extOf, TELEGRAM_MAX_DOC_BYTES,
|
|
5
|
+
} from '../media.js'
|
|
6
|
+
import { TEXT_INJECT_EXTS } from '../documents.js'
|
|
7
|
+
|
|
8
|
+
export async function extractTelegramInboundMedia(adapter, msg, initialText, maxDocBytes = TELEGRAM_MAX_DOC_BYTES, maxTextInjectBytes = 100 * 1024) {
|
|
9
|
+
let text = initialText || ''
|
|
10
|
+
const attachments = []
|
|
11
|
+
|
|
12
|
+
if (msg.photo?.length) {
|
|
13
|
+
const largest = msg.photo[msg.photo.length - 1]
|
|
14
|
+
const { path, file } = await adapter.downloadByFileId(largest.file_id, 'photo', extOf('', '') || '.jpg')
|
|
15
|
+
const ext = extOf(file.file_path, '') || '.jpg'
|
|
16
|
+
attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || 'image/jpeg' })
|
|
17
|
+
}
|
|
18
|
+
if (msg.sticker) {
|
|
19
|
+
const st = msg.sticker
|
|
20
|
+
if (st.is_video) {
|
|
21
|
+
try {
|
|
22
|
+
const { path } = await adapter.downloadByFileId(st.file_id, 'sticker-video', '.webm', st.file_unique_id || '')
|
|
23
|
+
attachments.push({ kind: 'animation', path, mime: 'video/webm', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webm` })
|
|
24
|
+
if (st.emoji) text = `${text}\n[Video sticker ${st.emoji}]`.trim()
|
|
25
|
+
} catch (e) {
|
|
26
|
+
text = `${text}\n[Video sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
27
|
+
}
|
|
28
|
+
} else if (st.is_animated) {
|
|
29
|
+
try {
|
|
30
|
+
const { path } = await adapter.downloadByFileId(st.file_id, 'sticker-anim', '.tgs', st.file_unique_id || '')
|
|
31
|
+
attachments.push({ kind: 'document', path, mime: 'application/x-tgsticker', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.tgs` })
|
|
32
|
+
if (st.emoji) text = `${text}\n[Animated sticker ${st.emoji}]`.trim()
|
|
33
|
+
} catch (e) {
|
|
34
|
+
text = `${text}\n[Animated sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
const { path } = await adapter.downloadByFileId(st.file_id, 'sticker', '.webp', st.file_unique_id || '')
|
|
38
|
+
attachments.push({ kind: 'sticker', path, mime: 'image/webp', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webp` })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (msg.voice) {
|
|
42
|
+
const { path } = await adapter.downloadByFileId(msg.voice.file_id, 'voice', '.ogg')
|
|
43
|
+
attachments.push({ kind: 'voice', path, mime: 'audio/ogg' })
|
|
44
|
+
}
|
|
45
|
+
if (msg.audio) {
|
|
46
|
+
const ext = extOf(msg.audio.file_name, msg.audio.mime_type) || '.mp3'
|
|
47
|
+
const { path } = await adapter.downloadByFileId(msg.audio.file_id, 'audio', ext, msg.audio.file_name || '')
|
|
48
|
+
attachments.push({ kind: 'audio', path, mime: msg.audio.mime_type || 'audio/mpeg' })
|
|
49
|
+
}
|
|
50
|
+
if (msg.video) {
|
|
51
|
+
const ext = extOf(msg.video.file_name, msg.video.mime_type) || '.mp4'
|
|
52
|
+
if ((msg.video.file_size || 0) > maxDocBytes) {
|
|
53
|
+
text = `${text}\n[Video too large]`.trim()
|
|
54
|
+
} else {
|
|
55
|
+
const { path } = await adapter.downloadByFileId(msg.video.file_id, 'video', ext, msg.video.file_name || '')
|
|
56
|
+
attachments.push({ kind: 'video', path, mime: msg.video.mime_type || VIDEO_EXT_TO_MIME[ext] || 'video/mp4', name: msg.video.file_name || basename(path) })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (msg.video_note) {
|
|
60
|
+
const vn = msg.video_note
|
|
61
|
+
if ((vn.file_size || 0) > maxDocBytes) {
|
|
62
|
+
text = `${text}\n[Video note too large]`.trim()
|
|
63
|
+
} else {
|
|
64
|
+
const { path } = await adapter.downloadByFileId(vn.file_id, 'videonote', '.mp4')
|
|
65
|
+
attachments.push({ kind: 'video', path, mime: 'video/mp4', name: 'video_note.mp4' })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (msg.animation) {
|
|
69
|
+
const an = msg.animation
|
|
70
|
+
const ext = extOf(an.file_name, an.mime_type) || '.mp4'
|
|
71
|
+
if ((an.file_size || 0) > maxDocBytes) {
|
|
72
|
+
text = `${text}\n[Animation too large]`.trim()
|
|
73
|
+
} else {
|
|
74
|
+
const { path } = await adapter.downloadByFileId(an.file_id, 'anim', ext, an.file_name || '')
|
|
75
|
+
attachments.push({ kind: 'animation', path, mime: an.mime_type || 'video/mp4', name: an.file_name || basename(path) })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (msg.document) {
|
|
79
|
+
const doc = msg.document
|
|
80
|
+
const ext = extOf(doc.file_name, doc.mime_type)
|
|
81
|
+
const kind = classifyDocument(ext, doc.mime_type)
|
|
82
|
+
if (doc.file_size > maxDocBytes) {
|
|
83
|
+
text = `${text}\n[Document too large: ${doc.file_name || 'file'}]`.trim()
|
|
84
|
+
} else if (kind === 'unsupported') {
|
|
85
|
+
const { path } = await adapter.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
86
|
+
attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
87
|
+
} else {
|
|
88
|
+
const { path, bytes } = await adapter.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
89
|
+
if (kind === 'image') attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || doc.mime_type || 'image/jpeg', name: doc.file_name })
|
|
90
|
+
else if (kind === 'video') attachments.push({ kind: 'video', path, mime: VIDEO_EXT_TO_MIME[ext] || doc.mime_type || 'video/mp4', name: doc.file_name })
|
|
91
|
+
else if (bytes.length <= maxTextInjectBytes && TEXT_INJECT_EXTS.has(ext)) {
|
|
92
|
+
const body = new TextDecoder('utf-8', { fatal: false }).decode(bytes).slice(0, maxTextInjectBytes)
|
|
93
|
+
text = `${text}\n\n[Document ${doc.file_name}]\n${body}`.trim()
|
|
94
|
+
} else attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
return { text, attachments }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function probeTelegramHealth(adapter, timeoutMs = 10000) {
|
|
103
|
+
if (!adapter.token) {
|
|
104
|
+
return { ok: false, error: 'Telegram bot token is empty' }
|
|
105
|
+
}
|
|
106
|
+
const start = Date.now()
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(`https://api.telegram.org/bot${adapter.token}/getMe`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({}),
|
|
112
|
+
signal: AbortSignal.timeout(Math.max(1000, Number(timeoutMs) || 10000)),
|
|
113
|
+
})
|
|
114
|
+
const latencyMs = Date.now() - start
|
|
115
|
+
const json = await res.json().catch(() => ({}))
|
|
116
|
+
if (!res.ok || json.ok === false) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
latencyMs,
|
|
120
|
+
error: json.description || `HTTP ${res.status}`,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const me = json.result || {}
|
|
124
|
+
adapter.botId = Number(me.id) || adapter.botId
|
|
125
|
+
adapter.botUsername = String(me.username || adapter.botUsername)
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
latencyMs,
|
|
129
|
+
botId: adapter.botId,
|
|
130
|
+
botUsername: adapter.botUsername,
|
|
131
|
+
firstName: me.first_name || '',
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
latencyMs: Date.now() - start,
|
|
137
|
+
error: err instanceof Error ? err.message : String(err),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
package/lib/adapters/telegram.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import {
|
|
3
3
|
IMAGE_EXT_TO_MIME, VIDEO_EXT_TO_MIME, basename, cacheName, classifyDocument,
|
|
4
|
-
extOf, saveToCache, safeName,
|
|
4
|
+
extOf, saveToCache, safeName, TELEGRAM_MAX_DOC_BYTES,
|
|
5
5
|
} from '../media.js'
|
|
6
6
|
import { TEXT_INJECT_EXTS } from '../documents.js'
|
|
7
7
|
import { splitText } from '../text.js'
|
|
@@ -12,21 +12,12 @@ import {
|
|
|
12
12
|
shouldProcessTelegramMessage, stripBotCommandSuffix,
|
|
13
13
|
} from '../groups.js'
|
|
14
14
|
import { isResendSafeNetworkError, isPollingConflict, isTopicGoneError, computePollBackoffMs } from '../telegram-errors.js'
|
|
15
|
+
import { extractTelegramInboundMedia, probeTelegramHealth } from './telegram-inbound.js'
|
|
15
16
|
|
|
16
17
|
const API = 'https://api.telegram.org'
|
|
17
18
|
const TELEGRAM_MAX = 4096
|
|
18
19
|
|
|
19
|
-
export
|
|
20
|
-
return {
|
|
21
|
-
keyboard: [
|
|
22
|
-
[{ text: '🔄 /new' }, { text: '🛑 /stop' }],
|
|
23
|
-
[{ text: '🎙️ /voice' }, { text: '📊 /status' }],
|
|
24
|
-
],
|
|
25
|
-
resize_keyboard: true,
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const REMOVE_REPLY_KEYBOARD = { remove_keyboard: true }
|
|
20
|
+
export { buildQuickActionsKeyboard, REMOVE_REPLY_KEYBOARD } from '../commands.js'
|
|
30
21
|
|
|
31
22
|
export class TelegramAdapter {
|
|
32
23
|
constructor(opts) {
|
|
@@ -138,6 +129,8 @@ export class TelegramAdapter {
|
|
|
138
129
|
return this.call('createForumTopic', {
|
|
139
130
|
chat_id: chatId,
|
|
140
131
|
name: String(name || '').slice(0, 128),
|
|
132
|
+
...(options.iconColor ? { icon_color: options.iconColor } : {}),
|
|
133
|
+
...(options.iconCustomEmojiId ? { icon_custom_emoji_id: options.iconCustomEmojiId } : {}),
|
|
141
134
|
...options,
|
|
142
135
|
})
|
|
143
136
|
}
|
|
@@ -180,27 +173,36 @@ export class TelegramAdapter {
|
|
|
180
173
|
}
|
|
181
174
|
if (this.transport === 'webhook') {
|
|
182
175
|
if (!this.webhookUrl) throw new Error('telegram webhookUrl is required for webhook transport')
|
|
176
|
+
if (!this.webhookSecret) throw new Error('telegram webhookSecret is required for webhook transport')
|
|
183
177
|
try {
|
|
184
178
|
await this.call('deleteWebhook', { drop_pending_updates: false })
|
|
185
|
-
} catch {
|
|
179
|
+
} catch (err) {
|
|
180
|
+
this.logger?.debug?.('telegram deleteWebhook failed (safe to ignore):', err?.message || err)
|
|
181
|
+
}
|
|
186
182
|
const params = {
|
|
187
183
|
url: this.webhookUrl,
|
|
188
184
|
allowed_updates: ['message', 'callback_query'],
|
|
189
185
|
drop_pending_updates: false,
|
|
186
|
+
secret_token: this.webhookSecret,
|
|
190
187
|
}
|
|
191
|
-
if (this.webhookSecret) params.secret_token = this.webhookSecret
|
|
192
188
|
await this.call('setWebhook', params)
|
|
193
189
|
this.logger?.info?.(`dsh-messenger-gateway: telegram webhook set → ${this.webhookUrl}`)
|
|
194
190
|
return
|
|
195
191
|
}
|
|
196
|
-
try {
|
|
192
|
+
try {
|
|
193
|
+
await this.call('deleteWebhook', { drop_pending_updates: false })
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.logger?.debug?.('telegram deleteWebhook failed (safe to ignore):', err?.message || err)
|
|
196
|
+
}
|
|
197
197
|
this.poll()
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
stop() {
|
|
201
201
|
this.stopped = true
|
|
202
202
|
if (this.pollTimer) clearTimeout(this.pollTimer)
|
|
203
|
-
if (this.statusIndicator) this.setStatusIndicator(this.statusOffline).catch(() => {
|
|
203
|
+
if (this.statusIndicator) this.setStatusIndicator(this.statusOffline).catch((err) => {
|
|
204
|
+
this.logger?.debug?.('telegram setStatusIndicator offline failed:', err?.message || err)
|
|
205
|
+
})
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
schedulePoll(delayMs) {
|
|
@@ -212,6 +214,7 @@ export class TelegramAdapter {
|
|
|
212
214
|
|
|
213
215
|
async poll() {
|
|
214
216
|
if (this.stopped) return
|
|
217
|
+
let backoffDelay
|
|
215
218
|
try {
|
|
216
219
|
const updates = await this.call('getUpdates', {
|
|
217
220
|
timeout: this.timeoutSeconds,
|
|
@@ -219,22 +222,25 @@ export class TelegramAdapter {
|
|
|
219
222
|
allowed_updates: ['message', 'callback_query'],
|
|
220
223
|
})
|
|
221
224
|
this.pollingConflict = false
|
|
222
|
-
|
|
225
|
+
this.pollErrorCount = 0
|
|
223
226
|
for (const update of updates || []) {
|
|
224
227
|
this.offset = Math.max(this.offset, update.update_id + 1)
|
|
225
228
|
await this.dispatchUpdate(update)
|
|
226
229
|
}
|
|
227
230
|
} catch (e) {
|
|
228
231
|
if (!this.stopped) {
|
|
232
|
+
this.pollErrorCount = (this.pollErrorCount || 0) + 1
|
|
229
233
|
if (isPollingConflict(e)) {
|
|
230
234
|
this.pollingConflict = true
|
|
235
|
+
backoffDelay = 15_000
|
|
231
236
|
this.logger?.error?.(`poll: TELEGRAM CONFLICT — another bot instance is polling the same token. Stop the duplicate instance. (${e.message})`)
|
|
232
237
|
} else {
|
|
233
|
-
this.
|
|
238
|
+
backoffDelay = computePollBackoffMs(this.pollIntervalMs, this.pollErrorCount)
|
|
239
|
+
this.logger?.warn?.(`poll: ${e.message} (retrying in ${backoffDelay}ms, error #${this.pollErrorCount})`)
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
}
|
|
237
|
-
this.schedulePoll()
|
|
243
|
+
this.schedulePoll(backoffDelay)
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
async dispatchUpdate(update) {
|
|
@@ -352,9 +358,8 @@ export class TelegramAdapter {
|
|
|
352
358
|
|
|
353
359
|
text = stripBotCommandSuffix(text, this.botUsername)
|
|
354
360
|
const threadId = msg.message_thread_id || 0
|
|
355
|
-
const maxDocBytes = this.media.maxDocBytes ??
|
|
361
|
+
const maxDocBytes = this.media.maxDocBytes ?? TELEGRAM_MAX_DOC_BYTES
|
|
356
362
|
const maxTextInjectBytes = this.media.maxTextInjectBytes ?? 100 * 1024
|
|
357
|
-
const attachments = []
|
|
358
363
|
const replyMsg = msg.reply_to_message
|
|
359
364
|
let replyText = ''
|
|
360
365
|
if (replyMsg) {
|
|
@@ -365,95 +370,19 @@ export class TelegramAdapter {
|
|
|
365
370
|
: quoted
|
|
366
371
|
}
|
|
367
372
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const ext = extOf(file.file_path, '') || '.jpg'
|
|
372
|
-
attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || 'image/jpeg' })
|
|
373
|
-
}
|
|
374
|
-
if (msg.sticker) {
|
|
375
|
-
const st = msg.sticker
|
|
376
|
-
if (st.is_video) {
|
|
377
|
-
try {
|
|
378
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker-video', '.webm', st.file_unique_id || '')
|
|
379
|
-
attachments.push({ kind: 'animation', path, mime: 'video/webm', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webm` })
|
|
380
|
-
if (st.emoji) text = `${text}\n[Video sticker ${st.emoji}]`.trim()
|
|
381
|
-
} catch (e) {
|
|
382
|
-
text = `${text}\n[Video sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
383
|
-
}
|
|
384
|
-
} else if (st.is_animated) {
|
|
385
|
-
try {
|
|
386
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker-anim', '.tgs', st.file_unique_id || '')
|
|
387
|
-
attachments.push({ kind: 'document', path, mime: 'application/x-tgsticker', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.tgs` })
|
|
388
|
-
if (st.emoji) text = `${text}\n[Animated sticker ${st.emoji}]`.trim()
|
|
389
|
-
} catch (e) {
|
|
390
|
-
text = `${text}\n[Animated sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker', '.webp', st.file_unique_id || '')
|
|
394
|
-
attachments.push({ kind: 'sticker', path, mime: 'image/webp', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webp` })
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (msg.voice) {
|
|
398
|
-
const { path } = await this.downloadByFileId(msg.voice.file_id, 'voice', '.ogg')
|
|
399
|
-
attachments.push({ kind: 'voice', path, mime: 'audio/ogg' })
|
|
400
|
-
}
|
|
401
|
-
if (msg.audio) {
|
|
402
|
-
const ext = extOf(msg.audio.file_name, msg.audio.mime_type) || '.mp3'
|
|
403
|
-
const { path } = await this.downloadByFileId(msg.audio.file_id, 'audio', ext, msg.audio.file_name || '')
|
|
404
|
-
attachments.push({ kind: 'audio', path, mime: msg.audio.mime_type || 'audio/mpeg' })
|
|
405
|
-
}
|
|
406
|
-
if (msg.video) {
|
|
407
|
-
const ext = extOf(msg.video.file_name, msg.video.mime_type) || '.mp4'
|
|
408
|
-
if ((msg.video.file_size || 0) > maxDocBytes) {
|
|
409
|
-
text = `${text}\n[Video too large]`.trim()
|
|
410
|
-
} else {
|
|
411
|
-
const { path } = await this.downloadByFileId(msg.video.file_id, 'video', ext, msg.video.file_name || '')
|
|
412
|
-
attachments.push({ kind: 'video', path, mime: msg.video.mime_type || VIDEO_EXT_TO_MIME[ext] || 'video/mp4', name: msg.video.file_name || basename(path) })
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (msg.video_note) {
|
|
416
|
-
const vn = msg.video_note
|
|
417
|
-
if ((vn.file_size || 0) > maxDocBytes) {
|
|
418
|
-
text = `${text}\n[Video note too large]`.trim()
|
|
419
|
-
} else {
|
|
420
|
-
const { path } = await this.downloadByFileId(vn.file_id, 'videonote', '.mp4')
|
|
421
|
-
attachments.push({ kind: 'video', path, mime: 'video/mp4', name: 'video_note.mp4' })
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (msg.animation) {
|
|
425
|
-
const an = msg.animation
|
|
426
|
-
const ext = extOf(an.file_name, an.mime_type) || '.mp4'
|
|
427
|
-
if ((an.file_size || 0) > maxDocBytes) {
|
|
428
|
-
text = `${text}\n[Animation too large]`.trim()
|
|
429
|
-
} else {
|
|
430
|
-
const { path } = await this.downloadByFileId(an.file_id, 'anim', ext, an.file_name || '')
|
|
431
|
-
attachments.push({ kind: 'animation', path, mime: an.mime_type || 'video/mp4', name: an.file_name || basename(path) })
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (msg.document) {
|
|
435
|
-
const doc = msg.document
|
|
436
|
-
const ext = extOf(doc.file_name, doc.mime_type)
|
|
437
|
-
const kind = classifyDocument(ext, doc.mime_type)
|
|
438
|
-
if (doc.file_size > maxDocBytes) {
|
|
439
|
-
text = `${text}\n[Document too large: ${doc.file_name || 'file'}]`.trim()
|
|
440
|
-
} else if (kind === 'unsupported') {
|
|
441
|
-
const { path } = await this.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
442
|
-
attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
443
|
-
} else {
|
|
444
|
-
const { path, bytes } = await this.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
445
|
-
if (kind === 'image') attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || doc.mime_type || 'image/jpeg', name: doc.file_name })
|
|
446
|
-
else if (kind === 'video') attachments.push({ kind: 'video', path, mime: VIDEO_EXT_TO_MIME[ext] || doc.mime_type || 'video/mp4', name: doc.file_name })
|
|
447
|
-
else if (bytes.length <= maxTextInjectBytes && TEXT_INJECT_EXTS.has(ext)) {
|
|
448
|
-
const body = new TextDecoder('utf-8', { fatal: false }).decode(bytes).slice(0, maxTextInjectBytes)
|
|
449
|
-
text = `${text}\n\n[Document ${doc.file_name}]\n${body}`.trim()
|
|
450
|
-
} else attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
451
|
-
}
|
|
452
|
-
}
|
|
373
|
+
const inbound = await extractTelegramInboundMedia(this, msg, text, maxDocBytes, maxTextInjectBytes)
|
|
374
|
+
text = inbound.text
|
|
375
|
+
const attachments = inbound.attachments
|
|
453
376
|
|
|
454
377
|
const messageId = msg.message_id
|
|
455
378
|
const reply = async (payload) => this.sendReply(chatId, messageId, payload, threadId)
|
|
456
|
-
const typing = async () => {
|
|
379
|
+
const typing = async () => {
|
|
380
|
+
try {
|
|
381
|
+
await this.call('sendChatAction', { chat_id: chatId, action: 'typing', ...telegramThreadParams(threadId) })
|
|
382
|
+
} catch (err) {
|
|
383
|
+
this.logger?.debug?.('telegram sendChatAction typing failed:', err?.message || err)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
457
386
|
const startStream = async () => this.startStreamMessage(chatId, messageId, threadId)
|
|
458
387
|
const startProgress = async () => this.startProgressMessage(chatId, messageId, threadId)
|
|
459
388
|
const react = async (emoji) => this.setReaction(chatId, messageId, emoji)
|
|
@@ -484,7 +413,11 @@ export class TelegramAdapter {
|
|
|
484
413
|
}
|
|
485
414
|
},
|
|
486
415
|
remove: async () => {
|
|
487
|
-
try {
|
|
416
|
+
try {
|
|
417
|
+
await this.call('deleteMessage', { chat_id: chatId, message_id: messageId })
|
|
418
|
+
} catch (err) {
|
|
419
|
+
this.logger?.debug?.('telegram progress deleteMessage failed:', err?.message || err)
|
|
420
|
+
}
|
|
488
421
|
},
|
|
489
422
|
}
|
|
490
423
|
}
|
|
@@ -658,51 +591,6 @@ export class TelegramAdapter {
|
|
|
658
591
|
}
|
|
659
592
|
|
|
660
593
|
async probeHealth(timeoutMs = 10000) {
|
|
661
|
-
|
|
662
|
-
return { ok: false, error: 'Telegram bot token is empty' }
|
|
663
|
-
}
|
|
664
|
-
const start = Date.now()
|
|
665
|
-
try {
|
|
666
|
-
const res = await fetch(`${API}/bot${this.token}/getMe`, {
|
|
667
|
-
method: 'POST',
|
|
668
|
-
headers: { 'Content-Type': 'application/json' },
|
|
669
|
-
body: JSON.stringify({}),
|
|
670
|
-
signal: AbortSignal.timeout(Math.max(1000, Number(timeoutMs) || 10000)),
|
|
671
|
-
})
|
|
672
|
-
const latencyMs = Date.now() - start
|
|
673
|
-
const json = await res.json().catch(() => ({}))
|
|
674
|
-
if (!res.ok || json.ok === false) {
|
|
675
|
-
return {
|
|
676
|
-
ok: false,
|
|
677
|
-
latencyMs,
|
|
678
|
-
error: json.description || `HTTP ${res.status}`,
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const me = json.result || {}
|
|
682
|
-
this.botId = Number(me.id) || this.botId
|
|
683
|
-
this.botUsername = String(me.username || this.botUsername)
|
|
684
|
-
return {
|
|
685
|
-
ok: true,
|
|
686
|
-
latencyMs,
|
|
687
|
-
botId: this.botId,
|
|
688
|
-
botUsername: this.botUsername,
|
|
689
|
-
firstName: me.first_name || '',
|
|
690
|
-
}
|
|
691
|
-
} catch (err) {
|
|
692
|
-
return {
|
|
693
|
-
ok: false,
|
|
694
|
-
latencyMs: Date.now() - start,
|
|
695
|
-
error: err instanceof Error ? err.message : String(err),
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async createForumTopic(chatId, name, opts = {}) {
|
|
701
|
-
return this.call('createForumTopic', {
|
|
702
|
-
chat_id: chatId,
|
|
703
|
-
name,
|
|
704
|
-
icon_color: opts.iconColor,
|
|
705
|
-
icon_custom_emoji_id: opts.iconCustomEmojiId,
|
|
706
|
-
})
|
|
594
|
+
return probeTelegramHealth(this, timeoutMs)
|
|
707
595
|
}
|
|
708
596
|
}
|