@goodandready/dsh-messenger-gateway 0.3.14 → 0.3.19
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/README.md +6 -0
- package/lib/adapters/telegram.js +122 -22
- package/lib/client.js +1312 -629
- package/lib/content-guard.js +14 -0
- package/lib/gateway.js +47 -18
- package/lib/index.js +49 -15
- package/lib/models.js +4 -3
- package/lib/outbound.js +2 -1
- package/lib/photos.js +2 -1
- package/lib/session-ops.js +14 -4
- package/lib/stream.js +114 -101
- package/lib/telegram-errors.js +10 -0
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -104,6 +104,12 @@ In addition to Telegram, outbound messages can be dispatched to Discord and Slac
|
|
|
104
104
|
- `notifyBridge` — forward non-messenger web session events to a home
|
|
105
105
|
- Bot token is a DSH secret field — never commit it
|
|
106
106
|
|
|
107
|
+
## Client UI & Settings Card
|
|
108
|
+
|
|
109
|
+
- Mounted into `settings.plugin.item` on the Plugins tab (`key: dsh-messenger-gateway`).
|
|
110
|
+
- Configuration is managed reactively via `settingsScope.bind({ namespace: 'dsh-messenger-gateway' })` with snapshot status checking (`loading`, `unavailable`, `ready`).
|
|
111
|
+
- Protected dictionary registration: duplicate registration attempts on page reload are safely caught with non-fatal warnings, ensuring the settings card never fails to mount.
|
|
112
|
+
|
|
107
113
|
## Requirements
|
|
108
114
|
|
|
109
115
|
- DeepSeek Harness web (or compatible) profile
|
package/lib/adapters/telegram.js
CHANGED
|
@@ -11,7 +11,7 @@ import { normalizeThreadId, telegramThreadParams } from '../topics.js'
|
|
|
11
11
|
import {
|
|
12
12
|
shouldProcessTelegramMessage, stripBotCommandSuffix,
|
|
13
13
|
} from '../groups.js'
|
|
14
|
-
import { isResendSafeNetworkError, isPollingConflict } from '../telegram-errors.js'
|
|
14
|
+
import { isResendSafeNetworkError, isPollingConflict, isTopicGoneError, computePollBackoffMs } from '../telegram-errors.js'
|
|
15
15
|
|
|
16
16
|
const API = 'https://api.telegram.org'
|
|
17
17
|
const TELEGRAM_MAX = 4096
|
|
@@ -55,6 +55,7 @@ export class TelegramAdapter {
|
|
|
55
55
|
this.sendRetryMax = 2
|
|
56
56
|
this.sendRetryBaseMs = 400
|
|
57
57
|
this.pollingConflict = false
|
|
58
|
+
this.pollErrorCount = 0
|
|
58
59
|
this.webhookUrl = String(opts.webhookUrl || '').trim()
|
|
59
60
|
this.webhookSecret = String(opts.webhookSecret || '').trim()
|
|
60
61
|
this.offset = 0
|
|
@@ -74,6 +75,7 @@ export class TelegramAdapter {
|
|
|
74
75
|
method: 'POST',
|
|
75
76
|
headers: { 'Content-Type': 'application/json' },
|
|
76
77
|
body: JSON.stringify(params),
|
|
78
|
+
keepalive: true,
|
|
77
79
|
signal: AbortSignal.timeout(timeoutMs),
|
|
78
80
|
})
|
|
79
81
|
const json = await res.json().catch(() => ({}))
|
|
@@ -86,6 +88,7 @@ export class TelegramAdapter {
|
|
|
86
88
|
const res = await fetch(`${API}/bot${this.token}/${method}`, {
|
|
87
89
|
method: 'POST',
|
|
88
90
|
body: form,
|
|
91
|
+
keepalive: true,
|
|
89
92
|
signal: AbortSignal.timeout(timeoutMs),
|
|
90
93
|
})
|
|
91
94
|
const json = await res.json().catch(() => ({}))
|
|
@@ -116,6 +119,7 @@ export class TelegramAdapter {
|
|
|
116
119
|
async downloadFile(filePath) {
|
|
117
120
|
const timeoutMs = (this.timeoutSeconds * 1000) + 30000
|
|
118
121
|
const res = await fetch(`${API}/file/bot${this.token}/${filePath}`, {
|
|
122
|
+
keepalive: true,
|
|
119
123
|
signal: AbortSignal.timeout(timeoutMs),
|
|
120
124
|
})
|
|
121
125
|
if (!res.ok) throw new Error(`telegram download HTTP ${res.status}`)
|
|
@@ -181,9 +185,10 @@ export class TelegramAdapter {
|
|
|
181
185
|
if (this.statusIndicator) this.setStatusIndicator(this.statusOffline).catch(() => {})
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
schedulePoll() {
|
|
188
|
+
schedulePoll(delayMs) {
|
|
185
189
|
if (this.stopped) return
|
|
186
|
-
|
|
190
|
+
const delay = delayMs !== undefined ? delayMs : this.pollIntervalMs
|
|
191
|
+
this.pollTimer = setTimeout(() => this.poll(), delay)
|
|
187
192
|
this.pollTimer.unref?.()
|
|
188
193
|
}
|
|
189
194
|
|
|
@@ -196,6 +201,7 @@ export class TelegramAdapter {
|
|
|
196
201
|
allowed_updates: ['message', 'callback_query'],
|
|
197
202
|
})
|
|
198
203
|
this.pollingConflict = false
|
|
204
|
+
this.pollErrorCount = 0
|
|
199
205
|
for (const update of updates || []) {
|
|
200
206
|
this.offset = Math.max(this.offset, update.update_id + 1)
|
|
201
207
|
await this.dispatchUpdate(update)
|
|
@@ -466,12 +472,26 @@ export class TelegramAdapter {
|
|
|
466
472
|
}
|
|
467
473
|
|
|
468
474
|
async startStreamMessage(chatId, replyTo, threadId = 0) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
+
let result
|
|
476
|
+
try {
|
|
477
|
+
result = await this.call('sendMessage', {
|
|
478
|
+
chat_id: chatId,
|
|
479
|
+
text: '…',
|
|
480
|
+
reply_to_message_id: replyTo,
|
|
481
|
+
...telegramThreadParams(threadId),
|
|
482
|
+
})
|
|
483
|
+
} catch (err) {
|
|
484
|
+
if (threadId && isTopicGoneError(err)) {
|
|
485
|
+
this.logger?.warn?.(`telegram stream start topic gone (thread ${threadId}), fallback main chat: ${err.message}`)
|
|
486
|
+
result = await this.call('sendMessage', {
|
|
487
|
+
chat_id: chatId,
|
|
488
|
+
text: '…',
|
|
489
|
+
reply_to_message_id: replyTo,
|
|
490
|
+
})
|
|
491
|
+
} else {
|
|
492
|
+
throw err
|
|
493
|
+
}
|
|
494
|
+
}
|
|
475
495
|
const messageId = result?.message_id
|
|
476
496
|
return {
|
|
477
497
|
messageId,
|
|
@@ -511,12 +531,30 @@ export class TelegramAdapter {
|
|
|
511
531
|
const blob = new Blob([file.bytes])
|
|
512
532
|
const name = safeName(file.name || 'file')
|
|
513
533
|
const isSvg = file.mime === 'image/svg+xml' || (file.name && file.name.toLowerCase().endsWith('.svg'))
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
534
|
+
const send = (m, f) => this.sendWithRetry(m, f, { multipart: true })
|
|
535
|
+
const method = (file.kind === 'photo' && !isSvg) ? 'sendPhoto'
|
|
536
|
+
: (file.kind === 'voice') ? 'sendVoice'
|
|
537
|
+
: (file.kind === 'audio') ? 'sendAudio'
|
|
538
|
+
: (file.kind === 'video') ? 'sendVideo'
|
|
539
|
+
: 'sendDocument'
|
|
540
|
+
const fieldName = (file.kind === 'photo' && !isSvg) ? 'photo'
|
|
541
|
+
: (file.kind === 'voice') ? 'voice'
|
|
542
|
+
: (file.kind === 'audio') ? 'audio'
|
|
543
|
+
: (file.kind === 'video') ? 'video'
|
|
544
|
+
: 'document'
|
|
545
|
+
form.append(fieldName, blob, name)
|
|
546
|
+
try {
|
|
547
|
+
return await send(method, form)
|
|
548
|
+
} catch (err) {
|
|
549
|
+
if (threadId && isTopicGoneError(err)) {
|
|
550
|
+
this.logger?.warn?.(`telegram sendMedia topic gone (thread ${threadId}), fallback main chat: ${err.message}`)
|
|
551
|
+
const fallbackForm = new FormData()
|
|
552
|
+
fallbackForm.append('chat_id', String(chatId))
|
|
553
|
+
fallbackForm.append(fieldName, blob, name)
|
|
554
|
+
return await send(method, fallbackForm)
|
|
555
|
+
}
|
|
556
|
+
throw err
|
|
557
|
+
}
|
|
520
558
|
}
|
|
521
559
|
|
|
522
560
|
formatOutgoingText(text, payload = {}) {
|
|
@@ -547,19 +585,41 @@ export class TelegramAdapter {
|
|
|
547
585
|
try {
|
|
548
586
|
await this.sendWithRetry('sendMessage', params)
|
|
549
587
|
} catch (err) {
|
|
588
|
+
if (threadId && isTopicGoneError(err)) {
|
|
589
|
+
this.logger?.warn?.(`telegram send topic gone (thread ${threadId}), fallback main chat: ${err.message}`)
|
|
590
|
+
params.message_thread_id = undefined
|
|
591
|
+
delete params.message_thread_id
|
|
592
|
+
await this.sendWithRetry('sendMessage', params)
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
550
595
|
if (!parseMode) throw err
|
|
551
596
|
this.logger?.warn?.(`telegram HTML send failed, fallback plain: ${err.message}`)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
597
|
+
try {
|
|
598
|
+
await this.call('sendMessage', {
|
|
599
|
+
chat_id: chatId,
|
|
600
|
+
text: plainChunks[i] ?? chunks[i],
|
|
601
|
+
reply_to_message_id: replyTo,
|
|
602
|
+
reply_markup: i === 0 ? effectiveMarkup : undefined,
|
|
603
|
+
...telegramThreadParams(threadId),
|
|
604
|
+
})
|
|
605
|
+
} catch (err2) {
|
|
606
|
+
if (threadId && isTopicGoneError(err2)) {
|
|
607
|
+
this.logger?.warn?.(`telegram plain send topic gone (thread ${threadId}), fallback main chat: ${err2.message}`)
|
|
608
|
+
await this.call('sendMessage', {
|
|
609
|
+
chat_id: chatId,
|
|
610
|
+
text: plainChunks[i] ?? chunks[i],
|
|
611
|
+
reply_to_message_id: replyTo,
|
|
612
|
+
reply_markup: i === 0 ? effectiveMarkup : undefined,
|
|
613
|
+
})
|
|
614
|
+
} else {
|
|
615
|
+
throw err2
|
|
616
|
+
}
|
|
617
|
+
}
|
|
559
618
|
}
|
|
560
619
|
}
|
|
561
620
|
}
|
|
562
621
|
|
|
622
|
+
|
|
563
623
|
async sendReply(chatId, replyTo, payload, threadId = 0) {
|
|
564
624
|
const body = typeof payload === 'string' ? { text: payload } : (payload || {})
|
|
565
625
|
const files = Array.isArray(body.files) ? body.files : []
|
|
@@ -579,6 +639,46 @@ export class TelegramAdapter {
|
|
|
579
639
|
return this.sendReply(chatId, undefined, payload, normalizeThreadId(opts.threadId))
|
|
580
640
|
}
|
|
581
641
|
|
|
642
|
+
async probeHealth(timeoutMs = 10000) {
|
|
643
|
+
if (!this.token) {
|
|
644
|
+
return { ok: false, error: 'Telegram bot token is empty' }
|
|
645
|
+
}
|
|
646
|
+
const start = Date.now()
|
|
647
|
+
try {
|
|
648
|
+
const res = await fetch(`${API}/bot${this.token}/getMe`, {
|
|
649
|
+
method: 'POST',
|
|
650
|
+
headers: { 'Content-Type': 'application/json' },
|
|
651
|
+
body: JSON.stringify({}),
|
|
652
|
+
signal: AbortSignal.timeout(Math.max(1000, Number(timeoutMs) || 10000)),
|
|
653
|
+
})
|
|
654
|
+
const latencyMs = Date.now() - start
|
|
655
|
+
const json = await res.json().catch(() => ({}))
|
|
656
|
+
if (!res.ok || json.ok === false) {
|
|
657
|
+
return {
|
|
658
|
+
ok: false,
|
|
659
|
+
latencyMs,
|
|
660
|
+
error: json.description || `HTTP ${res.status}`,
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const me = json.result || {}
|
|
664
|
+
this.botId = Number(me.id) || this.botId
|
|
665
|
+
this.botUsername = String(me.username || this.botUsername)
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
latencyMs,
|
|
669
|
+
botId: this.botId,
|
|
670
|
+
botUsername: this.botUsername,
|
|
671
|
+
firstName: me.first_name || '',
|
|
672
|
+
}
|
|
673
|
+
} catch (err) {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
latencyMs: Date.now() - start,
|
|
677
|
+
error: err instanceof Error ? err.message : String(err),
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
582
682
|
async createForumTopic(chatId, name, opts = {}) {
|
|
583
683
|
return this.call('createForumTopic', {
|
|
584
684
|
chat_id: chatId,
|