@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 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
@@ -1,4 +1,4 @@
1
1
  - insert:
2
2
  - id: dsh-messenger-gateway
3
3
  name: '@goodandready/dsh-messenger-gateway'
4
- config: {}
4
+ config: {}
@@ -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
+ }
@@ -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
+
@@ -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 function buildQuickActionsKeyboard() {
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 { await this.call('deleteWebhook', { drop_pending_updates: false }) } catch {}
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
- this.pollErrorCount = 0
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.logger?.warn?.(`poll: ${e.message}`)
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 ?? 20 * 1024 * 1024
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
- if (msg.photo?.length) {
369
- const largest = msg.photo[msg.photo.length - 1]
370
- const { path, file } = await this.downloadByFileId(largest.file_id, 'photo', extOf('', '') || '.jpg')
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 () => { try { await this.call('sendChatAction', { chat_id: chatId, action: 'typing', ...telegramThreadParams(threadId) }) } catch {} }
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 { await this.call('deleteMessage', { chat_id: chatId, message_id: messageId }) } catch {}
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
- if (!this.token) {
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
  }