@goodandready/dsh-messenger-gateway 0.4.0 → 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/lib/adapters/telegram-inbound.js +141 -141
- package/lib/adapters/telegram.js +596 -596
- package/lib/alerts.js +105 -105
- package/lib/api-health.js +38 -38
- package/lib/client.js +38 -0
- package/lib/commands.js +126 -126
- package/lib/forum-mirror.js +105 -105
- package/lib/gateway-callbacks.js +187 -187
- package/lib/gateway-commands.js +584 -584
- package/lib/gateway-turn.js +285 -285
- package/lib/index.js +540 -540
- package/lib/scheduler.js +169 -169
- package/lib/telegram-errors.js +0 -0
- package/package.json +69 -69
|
@@ -1,141 +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
|
+
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
|
+
|