@goodandready/dsh-messenger-gateway 0.3.22 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/cordis.patch.yml +1 -1
- package/lib/adapters/discord.js +1 -1
- package/lib/adapters/slack.js +1 -1
- package/lib/adapters/telegram-inbound.js +141 -0
- package/lib/adapters/telegram.js +35 -151
- package/lib/alerts.js +42 -1
- package/lib/api-health.js +38 -0
- package/lib/artifacts.js +118 -118
- package/lib/ask.js +203 -152
- package/lib/client.js +1698 -1631
- package/lib/commands.js +52 -0
- 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 +58 -1240
- package/lib/http.js +12 -1
- package/lib/index.js +13 -5
- package/lib/integrations.js +1 -1
- 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/topics.js +0 -14
- package/lib/tts.js +0 -10
- package/lib/voice-prefs.js +8 -4
- package/package.json +6 -2
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
package/lib/adapters/slack.js
CHANGED
|
@@ -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) {
|
|
@@ -356,9 +358,8 @@ export class TelegramAdapter {
|
|
|
356
358
|
|
|
357
359
|
text = stripBotCommandSuffix(text, this.botUsername)
|
|
358
360
|
const threadId = msg.message_thread_id || 0
|
|
359
|
-
const maxDocBytes = this.media.maxDocBytes ??
|
|
361
|
+
const maxDocBytes = this.media.maxDocBytes ?? TELEGRAM_MAX_DOC_BYTES
|
|
360
362
|
const maxTextInjectBytes = this.media.maxTextInjectBytes ?? 100 * 1024
|
|
361
|
-
const attachments = []
|
|
362
363
|
const replyMsg = msg.reply_to_message
|
|
363
364
|
let replyText = ''
|
|
364
365
|
if (replyMsg) {
|
|
@@ -369,95 +370,19 @@ export class TelegramAdapter {
|
|
|
369
370
|
: quoted
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const ext = extOf(file.file_path, '') || '.jpg'
|
|
376
|
-
attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || 'image/jpeg' })
|
|
377
|
-
}
|
|
378
|
-
if (msg.sticker) {
|
|
379
|
-
const st = msg.sticker
|
|
380
|
-
if (st.is_video) {
|
|
381
|
-
try {
|
|
382
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker-video', '.webm', st.file_unique_id || '')
|
|
383
|
-
attachments.push({ kind: 'animation', path, mime: 'video/webm', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webm` })
|
|
384
|
-
if (st.emoji) text = `${text}\n[Video sticker ${st.emoji}]`.trim()
|
|
385
|
-
} catch (e) {
|
|
386
|
-
text = `${text}\n[Video sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
387
|
-
}
|
|
388
|
-
} else if (st.is_animated) {
|
|
389
|
-
try {
|
|
390
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker-anim', '.tgs', st.file_unique_id || '')
|
|
391
|
-
attachments.push({ kind: 'document', path, mime: 'application/x-tgsticker', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.tgs` })
|
|
392
|
-
if (st.emoji) text = `${text}\n[Animated sticker ${st.emoji}]`.trim()
|
|
393
|
-
} catch (e) {
|
|
394
|
-
text = `${text}\n[Animated sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
const { path } = await this.downloadByFileId(st.file_id, 'sticker', '.webp', st.file_unique_id || '')
|
|
398
|
-
attachments.push({ kind: 'sticker', path, mime: 'image/webp', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webp` })
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
if (msg.voice) {
|
|
402
|
-
const { path } = await this.downloadByFileId(msg.voice.file_id, 'voice', '.ogg')
|
|
403
|
-
attachments.push({ kind: 'voice', path, mime: 'audio/ogg' })
|
|
404
|
-
}
|
|
405
|
-
if (msg.audio) {
|
|
406
|
-
const ext = extOf(msg.audio.file_name, msg.audio.mime_type) || '.mp3'
|
|
407
|
-
const { path } = await this.downloadByFileId(msg.audio.file_id, 'audio', ext, msg.audio.file_name || '')
|
|
408
|
-
attachments.push({ kind: 'audio', path, mime: msg.audio.mime_type || 'audio/mpeg' })
|
|
409
|
-
}
|
|
410
|
-
if (msg.video) {
|
|
411
|
-
const ext = extOf(msg.video.file_name, msg.video.mime_type) || '.mp4'
|
|
412
|
-
if ((msg.video.file_size || 0) > maxDocBytes) {
|
|
413
|
-
text = `${text}\n[Video too large]`.trim()
|
|
414
|
-
} else {
|
|
415
|
-
const { path } = await this.downloadByFileId(msg.video.file_id, 'video', ext, msg.video.file_name || '')
|
|
416
|
-
attachments.push({ kind: 'video', path, mime: msg.video.mime_type || VIDEO_EXT_TO_MIME[ext] || 'video/mp4', name: msg.video.file_name || basename(path) })
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (msg.video_note) {
|
|
420
|
-
const vn = msg.video_note
|
|
421
|
-
if ((vn.file_size || 0) > maxDocBytes) {
|
|
422
|
-
text = `${text}\n[Video note too large]`.trim()
|
|
423
|
-
} else {
|
|
424
|
-
const { path } = await this.downloadByFileId(vn.file_id, 'videonote', '.mp4')
|
|
425
|
-
attachments.push({ kind: 'video', path, mime: 'video/mp4', name: 'video_note.mp4' })
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
if (msg.animation) {
|
|
429
|
-
const an = msg.animation
|
|
430
|
-
const ext = extOf(an.file_name, an.mime_type) || '.mp4'
|
|
431
|
-
if ((an.file_size || 0) > maxDocBytes) {
|
|
432
|
-
text = `${text}\n[Animation too large]`.trim()
|
|
433
|
-
} else {
|
|
434
|
-
const { path } = await this.downloadByFileId(an.file_id, 'anim', ext, an.file_name || '')
|
|
435
|
-
attachments.push({ kind: 'animation', path, mime: an.mime_type || 'video/mp4', name: an.file_name || basename(path) })
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if (msg.document) {
|
|
439
|
-
const doc = msg.document
|
|
440
|
-
const ext = extOf(doc.file_name, doc.mime_type)
|
|
441
|
-
const kind = classifyDocument(ext, doc.mime_type)
|
|
442
|
-
if (doc.file_size > maxDocBytes) {
|
|
443
|
-
text = `${text}\n[Document too large: ${doc.file_name || 'file'}]`.trim()
|
|
444
|
-
} else if (kind === 'unsupported') {
|
|
445
|
-
const { path } = await this.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
446
|
-
attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
447
|
-
} else {
|
|
448
|
-
const { path, bytes } = await this.downloadByFileId(doc.file_id, 'doc', ext, doc.file_name || '')
|
|
449
|
-
if (kind === 'image') attachments.push({ kind: 'photo', path, mime: IMAGE_EXT_TO_MIME[ext] || doc.mime_type || 'image/jpeg', name: doc.file_name })
|
|
450
|
-
else if (kind === 'video') attachments.push({ kind: 'video', path, mime: VIDEO_EXT_TO_MIME[ext] || doc.mime_type || 'video/mp4', name: doc.file_name })
|
|
451
|
-
else if (bytes.length <= maxTextInjectBytes && TEXT_INJECT_EXTS.has(ext)) {
|
|
452
|
-
const body = new TextDecoder('utf-8', { fatal: false }).decode(bytes).slice(0, maxTextInjectBytes)
|
|
453
|
-
text = `${text}\n\n[Document ${doc.file_name}]\n${body}`.trim()
|
|
454
|
-
} else attachments.push({ kind: 'document', path, mime: doc.mime_type || 'application/octet-stream', name: doc.file_name || basename(path) })
|
|
455
|
-
}
|
|
456
|
-
}
|
|
373
|
+
const inbound = await extractTelegramInboundMedia(this, msg, text, maxDocBytes, maxTextInjectBytes)
|
|
374
|
+
text = inbound.text
|
|
375
|
+
const attachments = inbound.attachments
|
|
457
376
|
|
|
458
377
|
const messageId = msg.message_id
|
|
459
378
|
const reply = async (payload) => this.sendReply(chatId, messageId, payload, threadId)
|
|
460
|
-
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
|
+
}
|
|
461
386
|
const startStream = async () => this.startStreamMessage(chatId, messageId, threadId)
|
|
462
387
|
const startProgress = async () => this.startProgressMessage(chatId, messageId, threadId)
|
|
463
388
|
const react = async (emoji) => this.setReaction(chatId, messageId, emoji)
|
|
@@ -488,7 +413,11 @@ export class TelegramAdapter {
|
|
|
488
413
|
}
|
|
489
414
|
},
|
|
490
415
|
remove: async () => {
|
|
491
|
-
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
|
+
}
|
|
492
421
|
},
|
|
493
422
|
}
|
|
494
423
|
}
|
|
@@ -662,51 +591,6 @@ export class TelegramAdapter {
|
|
|
662
591
|
}
|
|
663
592
|
|
|
664
593
|
async probeHealth(timeoutMs = 10000) {
|
|
665
|
-
|
|
666
|
-
return { ok: false, error: 'Telegram bot token is empty' }
|
|
667
|
-
}
|
|
668
|
-
const start = Date.now()
|
|
669
|
-
try {
|
|
670
|
-
const res = await fetch(`${API}/bot${this.token}/getMe`, {
|
|
671
|
-
method: 'POST',
|
|
672
|
-
headers: { 'Content-Type': 'application/json' },
|
|
673
|
-
body: JSON.stringify({}),
|
|
674
|
-
signal: AbortSignal.timeout(Math.max(1000, Number(timeoutMs) || 10000)),
|
|
675
|
-
})
|
|
676
|
-
const latencyMs = Date.now() - start
|
|
677
|
-
const json = await res.json().catch(() => ({}))
|
|
678
|
-
if (!res.ok || json.ok === false) {
|
|
679
|
-
return {
|
|
680
|
-
ok: false,
|
|
681
|
-
latencyMs,
|
|
682
|
-
error: json.description || `HTTP ${res.status}`,
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
const me = json.result || {}
|
|
686
|
-
this.botId = Number(me.id) || this.botId
|
|
687
|
-
this.botUsername = String(me.username || this.botUsername)
|
|
688
|
-
return {
|
|
689
|
-
ok: true,
|
|
690
|
-
latencyMs,
|
|
691
|
-
botId: this.botId,
|
|
692
|
-
botUsername: this.botUsername,
|
|
693
|
-
firstName: me.first_name || '',
|
|
694
|
-
}
|
|
695
|
-
} catch (err) {
|
|
696
|
-
return {
|
|
697
|
-
ok: false,
|
|
698
|
-
latencyMs: Date.now() - start,
|
|
699
|
-
error: err instanceof Error ? err.message : String(err),
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async createForumTopic(chatId, name, opts = {}) {
|
|
705
|
-
return this.call('createForumTopic', {
|
|
706
|
-
chat_id: chatId,
|
|
707
|
-
name,
|
|
708
|
-
icon_color: opts.iconColor,
|
|
709
|
-
icon_custom_emoji_id: opts.iconCustomEmojiId,
|
|
710
|
-
})
|
|
594
|
+
return probeTelegramHealth(this, timeoutMs)
|
|
711
595
|
}
|
|
712
596
|
}
|
package/lib/alerts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { escapeHtml } from './telegram-format.js'
|
|
2
2
|
import { normalizeThreadId } from './topics.js'
|
|
3
|
+
import { t } from './locales/index.js'
|
|
3
4
|
|
|
4
5
|
export function formatAlertMessage(type, payload = {}) {
|
|
5
6
|
const timestamp = new Date().toLocaleTimeString()
|
|
@@ -61,4 +62,44 @@ export function resolveAlertTarget(gateway) {
|
|
|
61
62
|
chatId,
|
|
62
63
|
threadId: normalizeThreadId(alertsCfg.threadId),
|
|
63
64
|
}
|
|
64
|
-
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function handleSetAlertCommand(gw, input, locale) {
|
|
68
|
+
const { reply, userId, chatId, threadId = 0 } = input
|
|
69
|
+
if (!gw.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
70
|
+
const nextTg = {
|
|
71
|
+
...gw.tg(),
|
|
72
|
+
alerts: {
|
|
73
|
+
...(gw.tg().alerts || {}),
|
|
74
|
+
enabled: true,
|
|
75
|
+
chatId,
|
|
76
|
+
threadId: threadId || 0,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
gw.config.telegram = nextTg
|
|
80
|
+
try { await gw.hooks?.persistHomes?.(nextTg) } catch (err) { gw.logger?.warn?.('persistHomes error:', err?.message || err) }
|
|
81
|
+
return reply(`🔔 This chat assigned as alert channel (chat: ${chatId}${threadId ? `, topic: ${threadId}` : ''}).`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleAlertCommand(gw, input, parts) {
|
|
85
|
+
const { reply, userId } = input
|
|
86
|
+
const sub = parts[1]?.toLowerCase()
|
|
87
|
+
if (sub === 'test') {
|
|
88
|
+
const target = resolveAlertTarget(gw)
|
|
89
|
+
if (!target) return reply('Alert channel not configured. Configure: /setalert')
|
|
90
|
+
await gw.sendAlert('status', { title: 'Test Alert', details: `Sent by user ID ${userId}` })
|
|
91
|
+
return reply('Test alert sent to alert channel.')
|
|
92
|
+
}
|
|
93
|
+
const target = resolveAlertTarget(gw)
|
|
94
|
+
const alertsCfg = gw.tg().alerts || {}
|
|
95
|
+
return reply([
|
|
96
|
+
'🔔 <b>Alert Channel:</b>',
|
|
97
|
+
`Status: ${alertsCfg.enabled ? 'enabled' : 'disabled'}`,
|
|
98
|
+
`Chat: ${target ? `${target.chatId}${target.threadId ? ` (topic: ${target.threadId})` : ''}` : '(not assigned)'}`,
|
|
99
|
+
`Events: ${(alertsCfg.events || ['error', 'pairing']).join(', ')}`,
|
|
100
|
+
'',
|
|
101
|
+
'Commands:',
|
|
102
|
+
'/setalert — assign current chat as alert channel',
|
|
103
|
+
'/alert test — send test alert',
|
|
104
|
+
].join('\n'))
|
|
105
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ApiHealthTracker {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.logger = options.logger || console
|
|
4
|
+
this.degradedThreshold = Number(options.degradedThreshold) || 3
|
|
5
|
+
this.warnThreshold = Number(options.warnThreshold) || 5
|
|
6
|
+
this.consecutiveFailures = 0
|
|
7
|
+
this.lastError = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
recordFailure(op, err) {
|
|
11
|
+
this.consecutiveFailures++
|
|
12
|
+
this.lastError = {
|
|
13
|
+
op,
|
|
14
|
+
message: err?.message || String(err),
|
|
15
|
+
time: Date.now(),
|
|
16
|
+
}
|
|
17
|
+
this.logger?.debug?.(`dsh-messenger-gateway: Telegram API ${op} failed (${this.consecutiveFailures} consecutive):`, err?.message || err)
|
|
18
|
+
if (this.consecutiveFailures >= this.warnThreshold && this.consecutiveFailures % this.warnThreshold === 0) {
|
|
19
|
+
this.logger?.warn?.(`dsh-messenger-gateway: Telegram API experiencing repeated failures (${this.consecutiveFailures} in a row). Last op: ${op}, error: ${err?.message || err}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
recordSuccess() {
|
|
24
|
+
this.consecutiveFailures = 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isDegraded() {
|
|
28
|
+
return this.consecutiveFailures >= this.degradedThreshold
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getSnapshot() {
|
|
32
|
+
return {
|
|
33
|
+
degraded: this.isDegraded(),
|
|
34
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
35
|
+
lastError: this.lastError,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|