@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/lib/ask.js
CHANGED
|
@@ -1,152 +1,203 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { normalizeThreadId } from './topics.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
err
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
return { token:
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (rest === '
|
|
37
|
-
if (rest
|
|
38
|
-
if (rest.startsWith('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{ text:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
if (cb?.
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
pending.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { normalizeThreadId } from './topics.js'
|
|
3
|
+
import { isTopicGoneError } from './telegram-errors.js'
|
|
4
|
+
|
|
5
|
+
export const TELEGRAM_CALLBACK_DATA_MAX = 64
|
|
6
|
+
export const DEFAULT_PAGE_SIZE = 6
|
|
7
|
+
|
|
8
|
+
export function makeAskToken() {
|
|
9
|
+
return randomUUID().replace(/-/g, '').slice(0, 12)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildCallbackData(token, buttonId) {
|
|
13
|
+
const data = `${token}:${buttonId}`
|
|
14
|
+
if (data.length > TELEGRAM_CALLBACK_DATA_MAX) {
|
|
15
|
+
const err = new Error(`callback_data too long (${data.length} > ${TELEGRAM_CALLBACK_DATA_MAX})`)
|
|
16
|
+
err.status = 400
|
|
17
|
+
throw err
|
|
18
|
+
}
|
|
19
|
+
return data
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseCallbackData(data) {
|
|
23
|
+
const raw = String(data || '')
|
|
24
|
+
const idx = raw.indexOf(':')
|
|
25
|
+
if (idx <= 0) return { token: undefined, buttonId: raw }
|
|
26
|
+
return { token: raw.slice(0, idx), buttonId: raw.slice(idx + 1) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseAskCallback(data) {
|
|
30
|
+
const raw = String(data || '')
|
|
31
|
+
const parts = raw.split(':')
|
|
32
|
+
if (parts.length < 2) return { token: undefined, kind: 'unknown', id: raw }
|
|
33
|
+
const token = parts[0]
|
|
34
|
+
const rest = parts.slice(1).join(':')
|
|
35
|
+
|
|
36
|
+
if (rest === 'done') return { token, kind: 'done', id: 'done' }
|
|
37
|
+
if (rest === 'cancel') return { token, kind: 'cancel', id: 'cancel' }
|
|
38
|
+
if (rest.startsWith('t:')) return { token, kind: 'toggle', id: rest.slice(2) }
|
|
39
|
+
if (rest.startsWith('p:')) return { token, kind: 'page', page: Number(rest.slice(2)) || 0 }
|
|
40
|
+
|
|
41
|
+
return { token, kind: 'select', id: rest }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildInlineKeyboard(token, buttons) {
|
|
45
|
+
const callbackKeys = []
|
|
46
|
+
const rows = (buttons || []).map((row) => row.map((btn) => {
|
|
47
|
+
const callback_data = buildCallbackData(token, btn.id)
|
|
48
|
+
callbackKeys.push(callback_data)
|
|
49
|
+
return { text: btn.text, callback_data }
|
|
50
|
+
}))
|
|
51
|
+
return {
|
|
52
|
+
replyMarkup: rows.length ? { inline_keyboard: rows } : undefined,
|
|
53
|
+
callbackKeys,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeAskOptions(options = []) {
|
|
58
|
+
if (Array.isArray(options)) {
|
|
59
|
+
const flat = options.flat ? options.flat() : [].concat(...options)
|
|
60
|
+
return flat.map((opt, i) => {
|
|
61
|
+
if (typeof opt === 'string') return { id: String(i + 1), text: opt, selected: false }
|
|
62
|
+
const id = String(opt?.id ?? opt?.buttonId ?? i + 1).trim()
|
|
63
|
+
const text = String(opt?.text ?? opt?.label ?? id).trim()
|
|
64
|
+
return { id, text, selected: Boolean(opt?.selected) }
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildMultiSelectKeyboard(token, options = [], selectedIds = new Set(), page = 0, pageSize = DEFAULT_PAGE_SIZE, opts = {}) {
|
|
71
|
+
const normalized = normalizeAskOptions(options)
|
|
72
|
+
const total = normalized.length
|
|
73
|
+
const limit = Math.max(1, pageSize)
|
|
74
|
+
const maxPages = Math.ceil(total / limit) || 1
|
|
75
|
+
const curPage = Math.max(0, Math.min(page, maxPages - 1))
|
|
76
|
+
const start = curPage * limit
|
|
77
|
+
const end = Math.min(start + limit, total)
|
|
78
|
+
const slice = normalized.slice(start, end)
|
|
79
|
+
|
|
80
|
+
const callbackKeys = []
|
|
81
|
+
const rows = []
|
|
82
|
+
|
|
83
|
+
for (const opt of slice) {
|
|
84
|
+
const isChecked = selectedIds.has(opt.id)
|
|
85
|
+
const icon = isChecked ? '☑️' : '⬜️'
|
|
86
|
+
const text = `${icon} ${opt.text}`
|
|
87
|
+
const callback_data = buildCallbackData(token, `t:${opt.id}`)
|
|
88
|
+
callbackKeys.push(callback_data)
|
|
89
|
+
rows.push([{ text, callback_data }])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pagination row if more than one page
|
|
93
|
+
if (maxPages > 1) {
|
|
94
|
+
const navRow = []
|
|
95
|
+
if (curPage > 0) {
|
|
96
|
+
const prevData = buildCallbackData(token, `p:${curPage - 1}`)
|
|
97
|
+
callbackKeys.push(prevData)
|
|
98
|
+
navRow.push({ text: '⬅️ Back', callback_data: prevData })
|
|
99
|
+
}
|
|
100
|
+
const indicatorData = buildCallbackData(token, `p:${curPage}`)
|
|
101
|
+
callbackKeys.push(indicatorData)
|
|
102
|
+
navRow.push({ text: `${curPage + 1}/${maxPages}`, callback_data: indicatorData })
|
|
103
|
+
if (curPage < maxPages - 1) {
|
|
104
|
+
const nextData = buildCallbackData(token, `p:${curPage + 1}`)
|
|
105
|
+
callbackKeys.push(nextData)
|
|
106
|
+
navRow.push({ text: 'Next ➡️', callback_data: nextData })
|
|
107
|
+
}
|
|
108
|
+
rows.push(navRow)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Action buttons (Done / Cancel)
|
|
112
|
+
const doneText = opts.doneText || '✅ Done'
|
|
113
|
+
const cancelText = opts.cancelText || '❌ Cancel'
|
|
114
|
+
const doneData = buildCallbackData(token, 'done')
|
|
115
|
+
const cancelData = buildCallbackData(token, 'cancel')
|
|
116
|
+
callbackKeys.push(doneData, cancelData)
|
|
117
|
+
rows.push([
|
|
118
|
+
{ text: doneText, callback_data: doneData },
|
|
119
|
+
{ text: cancelText, callback_data: cancelData },
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
replyMarkup: { inline_keyboard: rows },
|
|
124
|
+
callbackKeys,
|
|
125
|
+
page: curPage,
|
|
126
|
+
maxPages,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function indexCallbacks(callbackIndex, keys, token) {
|
|
131
|
+
for (const key of keys || []) callbackIndex.set(key, token)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function releaseCallbacks(callbackIndex, keys) {
|
|
135
|
+
for (const key of keys || []) callbackIndex.delete(key)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function targetMatchesAsk(pending, cb) {
|
|
139
|
+
const target = pending?.target
|
|
140
|
+
if (!target) return false
|
|
141
|
+
if (cb?.platform && target.platform && cb.platform !== target.platform) return false
|
|
142
|
+
if (cb?.chatId !== undefined && String(target.chatId) !== String(cb.chatId)) return false
|
|
143
|
+
if (normalizeThreadId(target.threadId) !== normalizeThreadId(cb?.threadId)) return false
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function rejectPendingAsk(pending, err) {
|
|
148
|
+
if (!pending) return
|
|
149
|
+
clearTimeout(pending.timer)
|
|
150
|
+
pending.reject(err)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const REMOVE_KEYBOARD = { inline_keyboard: [] }
|
|
154
|
+
|
|
155
|
+
export async function executeMessengerAsk(gw, target, payload, timeoutMs = 300_000) {
|
|
156
|
+
const adapter = gw.getAdapter(target.platform)
|
|
157
|
+
if (!adapter) throw new Error(`adapter ${target.platform} unavailable`)
|
|
158
|
+
const token = makeAskToken()
|
|
159
|
+
const isMulti = payload.mode === 'multi' || (Array.isArray(payload.options) && payload.options.length > 0)
|
|
160
|
+
let replyMarkup
|
|
161
|
+
let callbackKeys
|
|
162
|
+
let selectedSet
|
|
163
|
+
let page = 0
|
|
164
|
+
const pageSize = Number(payload.pageSize) || 6
|
|
165
|
+
|
|
166
|
+
if (isMulti) {
|
|
167
|
+
selectedSet = new Set(Array.isArray(payload.selected) ? payload.selected : [])
|
|
168
|
+
const kb = buildMultiSelectKeyboard(token, payload.options || payload.buttons, selectedSet, page, pageSize, payload)
|
|
169
|
+
replyMarkup = kb.replyMarkup
|
|
170
|
+
callbackKeys = kb.callbackKeys
|
|
171
|
+
} else {
|
|
172
|
+
const kb = buildInlineKeyboard(token, payload.buttons || [])
|
|
173
|
+
replyMarkup = kb.replyMarkup
|
|
174
|
+
callbackKeys = kb.callbackKeys
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
indexCallbacks(gw.callbackIndex, callbackKeys, token)
|
|
178
|
+
try {
|
|
179
|
+
await adapter.sendTo(target.chatId, { text: payload.text, replyMarkup }, { threadId: target.threadId })
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Never leave stale callback keys pointing at an unresolvable ask.
|
|
182
|
+
releaseCallbacks(gw.callbackIndex, callbackKeys)
|
|
183
|
+
if (isTopicGoneError(err)) {
|
|
184
|
+
gw.logger?.warn?.(`messengerAsk: topic gone for ${target.platform}:${target.chatId}:${target.threadId} — ask aborted (chat/topic deleted)`)
|
|
185
|
+
throw new Error('messenger.ask: chat or topic was deleted')
|
|
186
|
+
}
|
|
187
|
+
throw err
|
|
188
|
+
}
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
gw.pendingAsks.delete(token)
|
|
192
|
+
releaseCallbacks(gw.callbackIndex, callbackKeys)
|
|
193
|
+
reject(new Error('messenger.ask timed out'))
|
|
194
|
+
}, timeoutMs)
|
|
195
|
+
timer.unref?.()
|
|
196
|
+
gw.pendingAsks.set(token, {
|
|
197
|
+
resolve, reject, timer, target, callbackKeys, isMulti,
|
|
198
|
+
options: payload.options || payload.buttons,
|
|
199
|
+
selected: selectedSet,
|
|
200
|
+
page, pageSize, payload,
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
}
|