@jungjaehoon/mama-os 0.25.0 → 0.27.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/CHANGELOG.md +9 -0
- package/README.md +37 -17
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +8 -0
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/attachment-text-extractor.d.ts +2 -0
- package/dist/agent/attachment-text-extractor.d.ts.map +1 -0
- package/dist/agent/attachment-text-extractor.js +128 -0
- package/dist/agent/attachment-text-extractor.js.map +1 -0
- package/dist/agent/code-act/constants.d.ts.map +1 -1
- package/dist/agent/code-act/constants.js +17 -2
- package/dist/agent/code-act/constants.js.map +1 -1
- package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +107 -0
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/code-act/tool-policy.d.ts +9 -1
- package/dist/agent/code-act/tool-policy.d.ts.map +1 -1
- package/dist/agent/code-act/tool-policy.js +30 -1
- package/dist/agent/code-act/tool-policy.js.map +1 -1
- package/dist/agent/codex-app-server-process.d.ts +1 -0
- package/dist/agent/codex-app-server-process.d.ts.map +1 -1
- package/dist/agent/codex-app-server-process.js +16 -0
- package/dist/agent/codex-app-server-process.js.map +1 -1
- package/dist/agent/drive-tools.d.ts +55 -0
- package/dist/agent/drive-tools.d.ts.map +1 -0
- package/dist/agent/drive-tools.js +263 -0
- package/dist/agent/drive-tools.js.map +1 -0
- package/dist/agent/gateway-tool-executor.d.ts +11 -3
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +249 -19
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +9 -0
- package/dist/agent/image-translation-tools.d.ts +48 -0
- package/dist/agent/image-translation-tools.d.ts.map +1 -0
- package/dist/agent/image-translation-tools.js +274 -0
- package/dist/agent/image-translation-tools.js.map +1 -0
- package/dist/agent/role-manager.d.ts.map +1 -1
- package/dist/agent/role-manager.js +9 -1
- package/dist/agent/role-manager.js.map +1 -1
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +53 -0
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +22 -33
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +18 -8
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js +12 -4
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.d.ts.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.js +8 -1
- package/dist/cli/runtime/envelope-bootstrap.js.map +1 -1
- package/dist/connectors/drive/index.d.ts +1 -1
- package/dist/connectors/drive/index.d.ts.map +1 -1
- package/dist/connectors/drive/index.js +100 -56
- package/dist/connectors/drive/index.js.map +1 -1
- package/dist/connectors/framework/gws-utils.d.ts +3 -0
- package/dist/connectors/framework/gws-utils.d.ts.map +1 -1
- package/dist/connectors/framework/gws-utils.js +17 -0
- package/dist/connectors/framework/gws-utils.js.map +1 -1
- package/dist/envelope/enforcer.d.ts.map +1 -1
- package/dist/envelope/enforcer.js +18 -3
- package/dist/envelope/enforcer.js.map +1 -1
- package/dist/envelope/reactive-config.d.ts +1 -1
- package/dist/envelope/reactive-config.d.ts.map +1 -1
- package/dist/envelope/reactive-config.js +10 -2
- package/dist/envelope/reactive-config.js.map +1 -1
- package/dist/envelope/types.d.ts +1 -1
- package/dist/envelope/types.d.ts.map +1 -1
- package/dist/envelope/types.js.map +1 -1
- package/dist/gateways/message-router.d.ts +4 -0
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +433 -335
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/session-store.d.ts +2 -0
- package/dist/gateways/session-store.d.ts.map +1 -1
- package/dist/gateways/session-store.js +41 -1
- package/dist/gateways/session-store.js.map +1 -1
- package/dist/gateways/telegram-media.d.ts +41 -0
- package/dist/gateways/telegram-media.d.ts.map +1 -0
- package/dist/gateways/telegram-media.js +242 -0
- package/dist/gateways/telegram-media.js.map +1 -0
- package/dist/gateways/telegram-message-ledger.d.ts +49 -0
- package/dist/gateways/telegram-message-ledger.d.ts.map +1 -0
- package/dist/gateways/telegram-message-ledger.js +266 -0
- package/dist/gateways/telegram-message-ledger.js.map +1 -0
- package/dist/gateways/telegram-response-presenter.d.ts +41 -0
- package/dist/gateways/telegram-response-presenter.d.ts.map +1 -0
- package/dist/gateways/telegram-response-presenter.js +250 -0
- package/dist/gateways/telegram-response-presenter.js.map +1 -0
- package/dist/gateways/telegram.d.ts +30 -7
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +444 -124
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/types.d.ts +4 -2
- package/dist/gateways/types.d.ts.map +1 -1
- package/dist/operator/operator-interfaces.d.ts +1 -1
- package/dist/operator/operator-interfaces.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.d.ts +12 -0
- package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.js +184 -38
- package/dist/operator/operator-trigger-loop.js.map +1 -1
- package/dist/operator/pending-report-store.d.ts +35 -0
- package/dist/operator/pending-report-store.d.ts.map +1 -0
- package/dist/operator/pending-report-store.js +155 -0
- package/dist/operator/pending-report-store.js.map +1 -0
- package/dist/operator/runtime-config.d.ts +10 -0
- package/dist/operator/runtime-config.d.ts.map +1 -0
- package/dist/operator/runtime-config.js +21 -0
- package/dist/operator/runtime-config.js.map +1 -0
- package/dist/operator/situation-report.d.ts +37 -0
- package/dist/operator/situation-report.d.ts.map +1 -1
- package/dist/operator/situation-report.js +122 -9
- package/dist/operator/situation-report.js.map +1 -1
- package/package.json +3 -1
- package/scripts/attachment/extract-office-text.js +179 -0
- package/scripts/attachment/extract-pdf-text.swift +36 -0
- package/scripts/image/fb-overlay.py +128 -0
- package/scripts/image/ocr-image.py +38 -0
- package/scripts/setup-ocr.js +120 -0
- package/templates/skills/image-translate.md +17 -22
|
@@ -3,24 +3,56 @@
|
|
|
3
3
|
* Telegram Gateway for MAMA Standalone
|
|
4
4
|
*
|
|
5
5
|
* Production-hardened Telegram bot integration using grammY:
|
|
6
|
-
* -
|
|
6
|
+
* - Telegram message_id dedup without dropping repeated short replies
|
|
7
|
+
* - Photo/document/caption ingestion through MAMA content blocks
|
|
7
8
|
* - Group chat filtering (mention/command/reply-to-bot only)
|
|
8
9
|
* - Sticker receive/send with emotion mapping
|
|
9
|
-
* -
|
|
10
|
+
* - Single-message streaming presenter (placeholder → final answer)
|
|
10
11
|
* - Typing indicator, error handling
|
|
11
12
|
*/
|
|
12
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
14
|
exports.TelegramGateway = void 0;
|
|
15
|
+
const promises_1 = require("node:fs/promises");
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
const node_os_1 = require("node:os");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
14
20
|
const grammy_1 = require("grammy");
|
|
15
21
|
const base_gateway_js_1 = require("./base-gateway.js");
|
|
16
22
|
const memory_logger_js_1 = require("../memory/memory-logger.js");
|
|
17
23
|
const untrusted_content_js_1 = require("../utils/untrusted-content.js");
|
|
18
|
-
const
|
|
24
|
+
const attachment_utils_js_1 = require("./attachment-utils.js");
|
|
25
|
+
const telegram_media_js_1 = require("./telegram-media.js");
|
|
26
|
+
const telegram_response_presenter_js_1 = require("./telegram-response-presenter.js");
|
|
27
|
+
const telegram_message_ledger_js_1 = require("./telegram-message-ledger.js");
|
|
19
28
|
const TELEGRAM_MAX_LENGTH = 4096;
|
|
20
29
|
const MESSAGE_DEDUP_TTL_MS = 60_000;
|
|
21
|
-
const MESSAGE_CONTENT_DEDUP_TTL_MS = 5_000;
|
|
22
30
|
const REJECTED_CHAT_WARN_INTERVAL_MS = 60_000;
|
|
23
31
|
const TYPING_INTERVAL_MS = 4_000;
|
|
32
|
+
function parseOutboundChunkProgress(value) {
|
|
33
|
+
const parsed = JSON.parse(value);
|
|
34
|
+
if (!parsed || typeof parsed !== 'object')
|
|
35
|
+
throw new Error('Invalid Telegram outbound progress');
|
|
36
|
+
const record = parsed;
|
|
37
|
+
if (record.version !== 1 ||
|
|
38
|
+
!Number.isSafeInteger(record.nextIndex) ||
|
|
39
|
+
record.nextIndex < 0 ||
|
|
40
|
+
typeof record.uncertain !== 'boolean') {
|
|
41
|
+
throw new Error('Invalid Telegram outbound progress');
|
|
42
|
+
}
|
|
43
|
+
return { nextIndex: record.nextIndex, uncertain: record.uncertain };
|
|
44
|
+
}
|
|
45
|
+
function isDefiniteTelegramApiRejection(error) {
|
|
46
|
+
if (!error || typeof error !== 'object')
|
|
47
|
+
return false;
|
|
48
|
+
const record = error;
|
|
49
|
+
if (Number.isSafeInteger(record.error_code))
|
|
50
|
+
return true;
|
|
51
|
+
const response = record.response;
|
|
52
|
+
return (Boolean(response) &&
|
|
53
|
+
typeof response === 'object' &&
|
|
54
|
+
Number.isSafeInteger(response.error_code));
|
|
55
|
+
}
|
|
24
56
|
const EMOTION_EMOJI = {
|
|
25
57
|
happy: ['😊', '😀', '😄', '🙂'],
|
|
26
58
|
laugh: ['😂', '🤣', '😆'],
|
|
@@ -51,9 +83,13 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
51
83
|
botUsername = '';
|
|
52
84
|
lastError = null;
|
|
53
85
|
lastMessageAt;
|
|
54
|
-
|
|
86
|
+
mediaRoot;
|
|
87
|
+
fetchImpl;
|
|
88
|
+
messageLedger;
|
|
89
|
+
chatTails = new Map();
|
|
90
|
+
activeChat = new node_async_hooks_1.AsyncLocalStorage();
|
|
91
|
+
// Telegram update dedup
|
|
55
92
|
recentMessageIds = new Map();
|
|
56
|
-
recentMessageSignatures = new Map();
|
|
57
93
|
rejectedChatWarnAt = new Map();
|
|
58
94
|
// Dedup cleanup timer
|
|
59
95
|
dedupCleanupTimer = null;
|
|
@@ -71,6 +107,13 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
71
107
|
token: options.token,
|
|
72
108
|
allowedChats: options.config?.allowedChats || [],
|
|
73
109
|
};
|
|
110
|
+
this.mediaRoot =
|
|
111
|
+
options.mediaRoot ??
|
|
112
|
+
(0, node_path_1.join)(process.env.MAMA_WORKSPACE || (0, node_path_1.join)((0, node_os_1.homedir)(), '.mama', 'workspace'), 'media', 'inbound', 'telegram');
|
|
113
|
+
this.fetchImpl = options.fetchImpl;
|
|
114
|
+
this.messageLedger = new telegram_message_ledger_js_1.TelegramMessageLedger(options.messageLedgerPath ??
|
|
115
|
+
process.env.MAMA_TELEGRAM_MESSAGE_LEDGER_PATH ??
|
|
116
|
+
`${this.mediaRoot}.processed-message-ids.json`, { log: (line) => console.error(line) });
|
|
74
117
|
}
|
|
75
118
|
async start() {
|
|
76
119
|
if (this.connected) {
|
|
@@ -93,6 +136,9 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
93
136
|
timestamp: new Date(),
|
|
94
137
|
data: { error: error instanceof Error ? error.message : String(error) },
|
|
95
138
|
});
|
|
139
|
+
// Let grammY treat the update as failed. The durable message ledger
|
|
140
|
+
// prevents unsafe turn re-execution and replays a ready outbox response.
|
|
141
|
+
throw error;
|
|
96
142
|
}
|
|
97
143
|
});
|
|
98
144
|
this.bot.catch((err) => {
|
|
@@ -103,6 +149,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
103
149
|
this.botId = this.bot.botInfo.id;
|
|
104
150
|
this.botUsername = this.bot.botInfo.username || '';
|
|
105
151
|
console.log(`Telegram bot logged in as @${this.botUsername}`);
|
|
152
|
+
await this.recoverPendingInboundDeliveries();
|
|
106
153
|
if (this.config.allowedChats && this.config.allowedChats.length > 0) {
|
|
107
154
|
console.log(`[Telegram] Inbound allowlist active: ${this.config.allowedChats.length} chat(s)`);
|
|
108
155
|
}
|
|
@@ -113,6 +160,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
113
160
|
}
|
|
114
161
|
this.connected = true;
|
|
115
162
|
this.lastError = null;
|
|
163
|
+
await (0, telegram_media_js_1.pruneTelegramMediaRoot)(this.mediaRoot);
|
|
116
164
|
// Periodic dedup cleanup
|
|
117
165
|
this.dedupCleanupTimer = setInterval(() => {
|
|
118
166
|
const now = Date.now();
|
|
@@ -120,15 +168,17 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
120
168
|
if (now - ts > MESSAGE_DEDUP_TTL_MS)
|
|
121
169
|
this.recentMessageIds.delete(key);
|
|
122
170
|
}
|
|
123
|
-
for (const [key, ts] of this.recentMessageSignatures) {
|
|
124
|
-
if (now - ts > MESSAGE_CONTENT_DEDUP_TTL_MS)
|
|
125
|
-
this.recentMessageSignatures.delete(key);
|
|
126
|
-
}
|
|
127
171
|
for (const [key, ts] of this.rejectedChatWarnAt) {
|
|
128
172
|
if (now - ts > REJECTED_CHAT_WARN_INTERVAL_MS) {
|
|
129
173
|
this.rejectedChatWarnAt.delete(key);
|
|
130
174
|
}
|
|
131
175
|
}
|
|
176
|
+
void (0, telegram_media_js_1.pruneTelegramMediaRoot)(this.mediaRoot).catch((error) => {
|
|
177
|
+
console.error(`[Telegram] media retention cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
});
|
|
179
|
+
void this.recoverPendingInboundDeliveries().catch((error) => {
|
|
180
|
+
console.error(`[Telegram] pending response recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
});
|
|
132
182
|
}, 60_000);
|
|
133
183
|
this.emitEvent({
|
|
134
184
|
type: 'connected',
|
|
@@ -179,32 +229,47 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
179
229
|
});
|
|
180
230
|
}
|
|
181
231
|
async handleMessage(msg) {
|
|
232
|
+
const chatKey = String(msg.chat?.id ?? 'unknown');
|
|
233
|
+
await this.runInChatQueue(chatKey, () => this.processMessage(msg));
|
|
234
|
+
}
|
|
235
|
+
async runInChatQueue(chatKey, work, allowReentrant = false) {
|
|
236
|
+
if (allowReentrant && this.activeChat.getStore() === chatKey)
|
|
237
|
+
return work();
|
|
238
|
+
const previous = this.chatTails.get(chatKey);
|
|
239
|
+
let release;
|
|
240
|
+
const gate = new Promise((resolve) => {
|
|
241
|
+
release = resolve;
|
|
242
|
+
});
|
|
243
|
+
const currentTail = (previous ?? Promise.resolve()).catch(() => { }).then(() => gate);
|
|
244
|
+
this.chatTails.set(chatKey, currentTail);
|
|
245
|
+
try {
|
|
246
|
+
if (previous)
|
|
247
|
+
await previous.catch(() => { });
|
|
248
|
+
return await this.activeChat.run(chatKey, work);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
release();
|
|
252
|
+
if (this.chatTails.get(chatKey) === currentTail)
|
|
253
|
+
this.chatTails.delete(chatKey);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async processMessage(msg) {
|
|
182
257
|
if (!msg.from || !msg.chat)
|
|
183
258
|
return;
|
|
184
259
|
const now = Date.now();
|
|
185
|
-
//
|
|
260
|
+
// Telegram update dedup (60s TTL). Do not deduplicate by text content:
|
|
261
|
+
// repeated short replies and emoji are distinct conversation turns.
|
|
186
262
|
const messageKey = `${msg.chat.id}:${msg.message_id}`;
|
|
263
|
+
if (this.messageLedger.get(messageKey)?.state === 'delivered')
|
|
264
|
+
return;
|
|
187
265
|
if (this.recentMessageIds.has(messageKey))
|
|
188
266
|
return;
|
|
189
267
|
this.recentMessageIds.set(messageKey, now);
|
|
190
|
-
// Stage 2: content signature dedup (5s TTL)
|
|
191
|
-
const rawText = msg.text || msg.sticker?.emoji || '';
|
|
192
|
-
const signature = `${msg.chat.id}:${msg.from.id}:${rawText.trim()}`;
|
|
193
|
-
if (rawText.trim()) {
|
|
194
|
-
const seenAt = this.recentMessageSignatures.get(signature);
|
|
195
|
-
if (seenAt && now - seenAt < MESSAGE_CONTENT_DEDUP_TTL_MS)
|
|
196
|
-
return;
|
|
197
|
-
this.recentMessageSignatures.set(signature, now);
|
|
198
|
-
}
|
|
199
268
|
// Cleanup expired entries
|
|
200
269
|
for (const [key, ts] of this.recentMessageIds) {
|
|
201
270
|
if (now - ts > MESSAGE_DEDUP_TTL_MS)
|
|
202
271
|
this.recentMessageIds.delete(key);
|
|
203
272
|
}
|
|
204
|
-
for (const [key, ts] of this.recentMessageSignatures) {
|
|
205
|
-
if (now - ts > MESSAGE_CONTENT_DEDUP_TTL_MS)
|
|
206
|
-
this.recentMessageSignatures.delete(key);
|
|
207
|
-
}
|
|
208
273
|
// Allowed chats filter
|
|
209
274
|
if (this.config.allowedChats && this.config.allowedChats.length > 0) {
|
|
210
275
|
if (!this.config.allowedChats.includes(String(msg.chat.id))) {
|
|
@@ -218,38 +283,193 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
218
283
|
return;
|
|
219
284
|
}
|
|
220
285
|
}
|
|
286
|
+
const hasMedia = Boolean((msg.photo && msg.photo.length > 0) || msg.document);
|
|
287
|
+
if (hasMedia && (!this.config.allowedChats || this.config.allowedChats.length === 0)) {
|
|
288
|
+
const lastWarn = this.rejectedChatWarnAt.get(String(msg.chat.id)) ?? 0;
|
|
289
|
+
if (now - lastWarn > REJECTED_CHAT_WARN_INTERVAL_MS) {
|
|
290
|
+
this.rejectedChatWarnAt.set(String(msg.chat.id), now);
|
|
291
|
+
console.warn('[Telegram] Dropped media because telegram.allowed_chats is not configured');
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const selectedText = msg.text ?? msg.caption ?? '';
|
|
296
|
+
const selectedEntities = msg.text !== undefined ? msg.entities : msg.caption_entities;
|
|
221
297
|
// Group chat filtering
|
|
222
298
|
const isGroup = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
|
|
223
299
|
if (isGroup) {
|
|
224
|
-
const text = msg.text || '';
|
|
225
300
|
const isMention = this.botUsername &&
|
|
226
|
-
|
|
227
|
-
|
|
301
|
+
selectedEntities?.some((e) => e.type === 'mention' &&
|
|
302
|
+
selectedText.slice(e.offset, e.offset + e.length).toLowerCase() ===
|
|
228
303
|
`@${this.botUsername.toLowerCase()}`);
|
|
229
|
-
const isBotCommand =
|
|
304
|
+
const isBotCommand = selectedEntities?.some((e) => e.type === 'bot_command');
|
|
230
305
|
const isReplyToBot = msg.reply_to_message?.from?.id === this.botId;
|
|
231
306
|
if (!isMention && !isBotCommand && !isReplyToBot)
|
|
232
307
|
return;
|
|
233
308
|
}
|
|
234
|
-
|
|
235
|
-
let text = msg.text || '';
|
|
309
|
+
let text = selectedText;
|
|
236
310
|
if (msg.sticker) {
|
|
237
311
|
text = `[sticker: ${msg.sticker.emoji || '😊'}]`;
|
|
238
312
|
}
|
|
239
|
-
if (
|
|
240
|
-
|
|
313
|
+
if (isGroup && this.botUsername) {
|
|
314
|
+
const escaped = this.botUsername.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
315
|
+
text = text.replace(new RegExp(`@${escaped}\\b`, 'gi'), '').trim();
|
|
316
|
+
}
|
|
241
317
|
// Provenance labeling: a FORWARDED message is third-party content the
|
|
242
|
-
// owner relayed, not the owner speaking.
|
|
243
|
-
//
|
|
244
|
-
// the save-candidate extractor ignores directives inside it.
|
|
318
|
+
// owner relayed, not the owner speaking. Selection and mention removal
|
|
319
|
+
// happen first so caption provenance follows the same trust boundary.
|
|
245
320
|
const isForwarded = Boolean(msg.forward_origin);
|
|
321
|
+
const durableEntry = this.messageLedger.get(messageKey);
|
|
322
|
+
const numChatId = msg.chat.id;
|
|
323
|
+
const api = this.bot.api;
|
|
324
|
+
const presenter = this.createResponsePresenter(numChatId, messageKey, durableEntry?.nextChunkIndex ?? 0);
|
|
325
|
+
if (durableEntry?.state !== 'ready')
|
|
326
|
+
await presenter.start();
|
|
327
|
+
if (durableEntry?.state === 'ready' && durableEntry.response !== undefined) {
|
|
328
|
+
try {
|
|
329
|
+
await presenter.finalize(durableEntry.response);
|
|
330
|
+
this.messageLedger.markDelivered(messageKey);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
this.recentMessageIds.delete(messageKey);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (durableEntry?.state === 'processing') {
|
|
339
|
+
const interruptedNotice = 'The previous processing attempt was interrupted. It was not rerun because its external ' +
|
|
340
|
+
'side effects could not be proven safe to repeat. Please send a new message if you want ' +
|
|
341
|
+
'to retry it.';
|
|
342
|
+
this.messageLedger.markReady(messageKey, interruptedNotice);
|
|
343
|
+
try {
|
|
344
|
+
await presenter.finalize(interruptedNotice);
|
|
345
|
+
this.messageLedger.markDelivered(messageKey);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
this.recentMessageIds.delete(messageKey);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const attachments = [];
|
|
354
|
+
const contentBlocks = [];
|
|
355
|
+
let transientMediaPath;
|
|
356
|
+
try {
|
|
357
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
358
|
+
const photo = msg.photo[msg.photo.length - 1];
|
|
359
|
+
const downloaded = await (0, telegram_media_js_1.downloadTelegramMedia)({
|
|
360
|
+
botToken: this.token,
|
|
361
|
+
fileId: photo.file_id,
|
|
362
|
+
fileUniqueId: photo.file_unique_id,
|
|
363
|
+
declaredSize: photo.file_size,
|
|
364
|
+
kind: 'photo',
|
|
365
|
+
mediaRoot: this.mediaRoot,
|
|
366
|
+
getFile: (fileId) => api.getFile(fileId),
|
|
367
|
+
fetchImpl: this.fetchImpl,
|
|
368
|
+
});
|
|
369
|
+
transientMediaPath = downloaded.localPath;
|
|
370
|
+
const detectedType = (0, attachment_utils_js_1.detectImageType)(await (0, promises_1.readFile)(downloaded.localPath));
|
|
371
|
+
if (!detectedType) {
|
|
372
|
+
await presenter.fail('This image format is not supported.');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const attachment = {
|
|
376
|
+
type: 'image',
|
|
377
|
+
sourceRef: downloaded.sourceRef,
|
|
378
|
+
localPath: downloaded.localPath,
|
|
379
|
+
filename: downloaded.filename,
|
|
380
|
+
contentType: detectedType,
|
|
381
|
+
size: downloaded.size,
|
|
382
|
+
};
|
|
383
|
+
attachments.push(attachment);
|
|
384
|
+
const builtBlocks = await (0, attachment_utils_js_1.buildContentBlocks)([attachment]);
|
|
385
|
+
const imageBlocks = builtBlocks.filter((block) => block.type === 'image');
|
|
386
|
+
if (imageBlocks.length === 0) {
|
|
387
|
+
await presenter.fail('This image format is not supported.');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const block of imageBlocks) {
|
|
391
|
+
contentBlocks.push({ type: 'image', source: block.source });
|
|
392
|
+
}
|
|
393
|
+
// Keep the private local path for bounded OCR/overlay follow-ups. The
|
|
394
|
+
// media retention sweeper removes it after the configured TTL/quota.
|
|
395
|
+
transientMediaPath = undefined;
|
|
396
|
+
if (!text.trim()) {
|
|
397
|
+
text = '[Image]';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (msg.document) {
|
|
401
|
+
const downloaded = await (0, telegram_media_js_1.downloadTelegramMedia)({
|
|
402
|
+
botToken: this.token,
|
|
403
|
+
fileId: msg.document.file_id,
|
|
404
|
+
fileUniqueId: msg.document.file_unique_id,
|
|
405
|
+
filename: msg.document.file_name,
|
|
406
|
+
mimeType: msg.document.mime_type,
|
|
407
|
+
declaredSize: msg.document.file_size,
|
|
408
|
+
kind: 'document',
|
|
409
|
+
mediaRoot: this.mediaRoot,
|
|
410
|
+
getFile: (fileId) => api.getFile(fileId),
|
|
411
|
+
fetchImpl: this.fetchImpl,
|
|
412
|
+
});
|
|
413
|
+
transientMediaPath = downloaded.localPath;
|
|
414
|
+
const detectedType = (0, attachment_utils_js_1.detectImageType)(await (0, promises_1.readFile)(downloaded.localPath));
|
|
415
|
+
if (downloaded.mimeType.startsWith('image/') && !detectedType) {
|
|
416
|
+
await presenter.fail('This image format is not supported.');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const attachment = {
|
|
420
|
+
type: detectedType ? 'image' : 'file',
|
|
421
|
+
sourceRef: downloaded.sourceRef,
|
|
422
|
+
localPath: downloaded.localPath,
|
|
423
|
+
filename: downloaded.filename,
|
|
424
|
+
contentType: detectedType ?? downloaded.mimeType,
|
|
425
|
+
size: downloaded.size,
|
|
426
|
+
};
|
|
427
|
+
attachments.push(attachment);
|
|
428
|
+
if (detectedType) {
|
|
429
|
+
const builtBlocks = await (0, attachment_utils_js_1.buildContentBlocks)([attachment]);
|
|
430
|
+
const imageBlocks = builtBlocks.filter((block) => block.type === 'image');
|
|
431
|
+
if (imageBlocks.length === 0) {
|
|
432
|
+
await presenter.fail('This image format is not supported.');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
for (const block of imageBlocks) {
|
|
436
|
+
contentBlocks.push({ type: 'image', source: block.source });
|
|
437
|
+
}
|
|
438
|
+
if (!text.trim()) {
|
|
439
|
+
text = `[Image: ${downloaded.filename}]`;
|
|
440
|
+
}
|
|
441
|
+
transientMediaPath = undefined;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
contentBlocks.push({
|
|
445
|
+
type: 'text',
|
|
446
|
+
text: `[File: ${downloaded.filename}, type: ${downloaded.mimeType}]`,
|
|
447
|
+
});
|
|
448
|
+
if (!text.trim()) {
|
|
449
|
+
text = `[File: ${downloaded.filename}]`;
|
|
450
|
+
}
|
|
451
|
+
// Documents remain in the private inbound workspace so the routed
|
|
452
|
+
// turn and a bounded follow-up can read the actual attachment.
|
|
453
|
+
transientMediaPath = undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
await presenter.fail(msg.document ? 'The file could not be downloaded.' : 'The image could not be downloaded.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
if (transientMediaPath) {
|
|
463
|
+
await (0, promises_1.unlink)(transientMediaPath).catch(() => { });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (!text.trim()) {
|
|
467
|
+
await presenter.fail('This Telegram message type is not supported.');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
246
470
|
if (isForwarded) {
|
|
247
471
|
text = (0, untrusted_content_js_1.wrapUntrustedContent)('telegram-forward', text);
|
|
248
472
|
}
|
|
249
|
-
if (isGroup && this.botUsername) {
|
|
250
|
-
const escaped = this.botUsername.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
251
|
-
text = text.replace(new RegExp(`@${escaped}\\b`, 'gi'), '').trim();
|
|
252
|
-
}
|
|
253
473
|
const sender = msg.from.username || String(msg.from.id);
|
|
254
474
|
console.log(`[Telegram] Message from ${sender} (${text.length} chars)`);
|
|
255
475
|
const memoryLogger = (0, memory_logger_js_1.getMemoryLogger)();
|
|
@@ -265,10 +485,12 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
265
485
|
channelId: String(msg.chat.id),
|
|
266
486
|
userId: String(msg.from.id),
|
|
267
487
|
text,
|
|
488
|
+
contentBlocks: contentBlocks.length > 0 ? contentBlocks : undefined,
|
|
268
489
|
metadata: {
|
|
269
490
|
username: msg.from.username,
|
|
270
491
|
messageId: String(msg.message_id),
|
|
271
492
|
chatType: msg.chat.type,
|
|
493
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
272
494
|
// Trusted provenance flag: downstream strips untrusted blocks ONLY
|
|
273
495
|
// when the GATEWAY wrapped them - sender-typed markers are not a
|
|
274
496
|
// security boundary (forgeable in-band data).
|
|
@@ -277,118 +499,233 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
277
499
|
};
|
|
278
500
|
// Typing indicator
|
|
279
501
|
const chatId = String(msg.chat.id);
|
|
280
|
-
const numChatId = msg.chat.id;
|
|
281
502
|
const sendTyping = () => this.bot?.api.sendChatAction(numChatId, 'typing').catch(() => { });
|
|
282
503
|
sendTyping();
|
|
283
504
|
const typingInterval = setInterval(sendTyping, TYPING_INTERVAL_MS);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
await api.editMessageText(numChatId, Number(handle), content);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
/* ignore same-text errors */
|
|
302
|
-
}
|
|
303
|
-
},
|
|
304
|
-
deletePlaceholder: async (handle) => {
|
|
505
|
+
try {
|
|
506
|
+
// Process through message router
|
|
507
|
+
let result;
|
|
508
|
+
this.messageLedger.claim(messageKey);
|
|
509
|
+
try {
|
|
510
|
+
result = await this.messageRouter.process(normalizedMessage, {
|
|
511
|
+
onStream: presenter.callbacks(),
|
|
512
|
+
onQueued: () => presenter.markQueued(),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
const failureNotice = 'An error occurred while processing the message.';
|
|
517
|
+
this.messageLedger.markReady(messageKey, failureNotice);
|
|
305
518
|
try {
|
|
306
|
-
await
|
|
519
|
+
await presenter.fail(failureNotice);
|
|
520
|
+
this.messageLedger.markDelivered(messageKey);
|
|
307
521
|
}
|
|
308
522
|
catch {
|
|
309
|
-
|
|
523
|
+
this.recentMessageIds.delete(messageKey);
|
|
310
524
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
this.messageLedger.markReady(messageKey, result.response);
|
|
528
|
+
try {
|
|
529
|
+
await presenter.finalize(result.response);
|
|
530
|
+
this.messageLedger.markDelivered(messageKey);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
this.recentMessageIds.delete(messageKey);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
this.lastMessageAt = Date.now();
|
|
537
|
+
memoryLogger.logMessage('Telegram', 'MAMA', result.response, true);
|
|
538
|
+
this.emitEvent({
|
|
539
|
+
type: 'message_sent',
|
|
540
|
+
source: 'telegram',
|
|
541
|
+
timestamp: new Date(),
|
|
542
|
+
data: {
|
|
543
|
+
chatId,
|
|
544
|
+
responseLength: result.response.length,
|
|
545
|
+
duration: result.duration,
|
|
546
|
+
},
|
|
323
547
|
});
|
|
324
548
|
}
|
|
325
549
|
finally {
|
|
326
550
|
clearInterval(typingInterval);
|
|
327
|
-
await tracker.cleanup();
|
|
328
551
|
}
|
|
329
|
-
this.lastMessageAt = Date.now();
|
|
330
|
-
memoryLogger.logMessage('Telegram', 'MAMA', result.response, true);
|
|
331
|
-
await this.sendMessage(chatId, result.response);
|
|
332
|
-
this.emitEvent({
|
|
333
|
-
type: 'message_sent',
|
|
334
|
-
source: 'telegram',
|
|
335
|
-
timestamp: new Date(),
|
|
336
|
-
data: {
|
|
337
|
-
chatId,
|
|
338
|
-
responseLength: result.response.length,
|
|
339
|
-
duration: result.duration,
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
552
|
}
|
|
343
|
-
async sendMessage(chatId, text) {
|
|
553
|
+
async sendMessage(chatId, text, idempotencyKey) {
|
|
554
|
+
if (!this.bot)
|
|
555
|
+
throw new Error('Telegram gateway not connected');
|
|
556
|
+
if (!text.trim())
|
|
557
|
+
return;
|
|
558
|
+
await this.runInChatQueue(chatId, () => this.sendMessageNow(chatId, text, idempotencyKey));
|
|
559
|
+
}
|
|
560
|
+
async sendMessageFromActiveTurn(chatId, text, idempotencyKey) {
|
|
344
561
|
if (!this.bot)
|
|
345
562
|
throw new Error('Telegram gateway not connected');
|
|
346
563
|
if (!text.trim())
|
|
347
564
|
return;
|
|
348
|
-
|
|
565
|
+
await this.runInChatQueue(chatId, () => this.sendMessageNow(chatId, text, idempotencyKey), true);
|
|
566
|
+
}
|
|
567
|
+
async sendMessageNow(chatId, text, idempotencyKey) {
|
|
568
|
+
const bot = this.bot;
|
|
569
|
+
if (!bot)
|
|
570
|
+
throw new Error('Telegram gateway not connected');
|
|
571
|
+
const chunks = (0, telegram_response_presenter_js_1.splitTelegramMessage)(text, TELEGRAM_MAX_LENGTH);
|
|
349
572
|
const numChatId = Number(chatId);
|
|
350
|
-
|
|
573
|
+
if (!idempotencyKey) {
|
|
574
|
+
for (const chunk of chunks)
|
|
575
|
+
await bot.api.sendMessage(numChatId, chunk);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const ledgerKey = this.outboundLedgerKey(idempotencyKey, 'text');
|
|
579
|
+
const existing = this.messageLedger.get(ledgerKey);
|
|
580
|
+
if (existing?.state === 'delivered')
|
|
581
|
+
return;
|
|
582
|
+
if (!existing)
|
|
583
|
+
this.messageLedger.claim(ledgerKey);
|
|
584
|
+
let nextIndex = 0;
|
|
585
|
+
if (existing?.state === 'ready' && existing.response) {
|
|
586
|
+
const progress = parseOutboundChunkProgress(existing.response);
|
|
587
|
+
nextIndex = progress.nextIndex;
|
|
588
|
+
if (progress.uncertain) {
|
|
589
|
+
console.warn(`[Telegram] Retrying delivery ${ledgerKey} from chunk ${nextIndex} after an ` +
|
|
590
|
+
'ambiguous prior acceptance; at-least-once delivery may duplicate that chunk');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
for (let index = nextIndex; index < chunks.length; index += 1) {
|
|
594
|
+
this.messageLedger.markReady(ledgerKey, JSON.stringify({ version: 1, nextIndex: index, uncertain: true }));
|
|
351
595
|
try {
|
|
352
|
-
await
|
|
596
|
+
await bot.api.sendMessage(numChatId, chunks[index]);
|
|
353
597
|
}
|
|
354
|
-
catch (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (status === 400) {
|
|
358
|
-
await this.bot.api.sendMessage(numChatId, chunk);
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
throw err;
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (isDefiniteTelegramApiRejection(error)) {
|
|
600
|
+
this.messageLedger.markReady(ledgerKey, JSON.stringify({ version: 1, nextIndex: index, uncertain: false }));
|
|
362
601
|
}
|
|
602
|
+
throw error;
|
|
363
603
|
}
|
|
604
|
+
this.messageLedger.markReady(ledgerKey, JSON.stringify({ version: 1, nextIndex: index + 1, uncertain: false }));
|
|
364
605
|
}
|
|
606
|
+
this.messageLedger.markDelivered(ledgerKey);
|
|
607
|
+
}
|
|
608
|
+
async sendFile(chatId, filePath, caption, idempotencyKey) {
|
|
609
|
+
if (!this.bot)
|
|
610
|
+
throw new Error('Telegram gateway not connected');
|
|
611
|
+
const { InputFile } = await import('grammy');
|
|
612
|
+
await this.runInChatQueue(chatId, () => this.sendOutboundOnce(idempotencyKey, 'file', () => this.bot.api.sendDocument(Number(chatId), new InputFile(filePath), { caption }).then(() => { })));
|
|
613
|
+
}
|
|
614
|
+
async sendFileFromActiveTurn(chatId, filePath, caption, idempotencyKey) {
|
|
615
|
+
if (!this.bot)
|
|
616
|
+
throw new Error('Telegram gateway not connected');
|
|
617
|
+
const { InputFile } = await import('grammy');
|
|
618
|
+
await this.runInChatQueue(chatId, () => this.sendOutboundOnce(idempotencyKey, 'file', () => this.bot.api.sendDocument(Number(chatId), new InputFile(filePath), { caption }).then(() => { })), true);
|
|
365
619
|
}
|
|
366
|
-
async
|
|
620
|
+
async sendImage(chatId, imagePath, caption, idempotencyKey) {
|
|
367
621
|
if (!this.bot)
|
|
368
622
|
throw new Error('Telegram gateway not connected');
|
|
369
623
|
const { InputFile } = await import('grammy');
|
|
370
|
-
await this.bot.api.
|
|
624
|
+
await this.runInChatQueue(chatId, () => this.sendOutboundOnce(idempotencyKey, 'image', () => this.bot.api.sendPhoto(Number(chatId), new InputFile(imagePath), { caption }).then(() => { })));
|
|
371
625
|
}
|
|
372
|
-
async
|
|
626
|
+
async sendImageFromActiveTurn(chatId, imagePath, caption, idempotencyKey) {
|
|
373
627
|
if (!this.bot)
|
|
374
628
|
throw new Error('Telegram gateway not connected');
|
|
375
629
|
const { InputFile } = await import('grammy');
|
|
376
|
-
await this.bot.api.sendPhoto(Number(chatId), new InputFile(imagePath), { caption });
|
|
630
|
+
await this.runInChatQueue(chatId, () => this.sendOutboundOnce(idempotencyKey, 'image', () => this.bot.api.sendPhoto(Number(chatId), new InputFile(imagePath), { caption }).then(() => { })), true);
|
|
631
|
+
}
|
|
632
|
+
async sendOutboundOnce(idempotencyKey, kind, send) {
|
|
633
|
+
if (!idempotencyKey) {
|
|
634
|
+
await send();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const ledgerKey = this.outboundLedgerKey(idempotencyKey, kind);
|
|
638
|
+
const existing = this.messageLedger.get(ledgerKey);
|
|
639
|
+
if (existing?.state === 'delivered')
|
|
640
|
+
return;
|
|
641
|
+
if (!existing)
|
|
642
|
+
this.messageLedger.claim(ledgerKey);
|
|
643
|
+
await send();
|
|
644
|
+
this.messageLedger.markDelivered(ledgerKey);
|
|
645
|
+
}
|
|
646
|
+
async recoverPendingInboundDeliveries() {
|
|
647
|
+
for (const snapshot of this.messageLedger.listUndelivered()) {
|
|
648
|
+
if (snapshot.key.startsWith('outbound:'))
|
|
649
|
+
continue;
|
|
650
|
+
const separator = snapshot.key.lastIndexOf(':');
|
|
651
|
+
if (separator <= 0)
|
|
652
|
+
continue;
|
|
653
|
+
const chatId = snapshot.key.slice(0, separator);
|
|
654
|
+
await this.runInChatQueue(chatId, async () => {
|
|
655
|
+
const entry = this.messageLedger.get(snapshot.key);
|
|
656
|
+
if (!entry || entry.state === 'delivered')
|
|
657
|
+
return;
|
|
658
|
+
if (entry.state === 'processing' && this.messageLedger.isOwnedByCurrentProcess(entry)) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const response = entry.state === 'ready' && entry.response !== undefined
|
|
662
|
+
? entry.response
|
|
663
|
+
: 'The previous processing attempt was interrupted. It was not rerun because its ' +
|
|
664
|
+
'external side effects could not be proven safe to repeat. Please send a new message ' +
|
|
665
|
+
'if you want to retry it.';
|
|
666
|
+
if (entry.state !== 'ready')
|
|
667
|
+
this.messageLedger.markReady(entry.key, response);
|
|
668
|
+
if (entry.deliveryUncertain) {
|
|
669
|
+
console.warn(`[Telegram] Resuming inbound delivery ${entry.key} from uncertain chunk ` +
|
|
670
|
+
`${entry.nextChunkIndex ?? 0}; that chunk may appear twice`);
|
|
671
|
+
}
|
|
672
|
+
const presenter = this.createResponsePresenter(Number(chatId), entry.key, entry.nextChunkIndex ?? 0);
|
|
673
|
+
await presenter.finalize(response);
|
|
674
|
+
this.messageLedger.markDelivered(entry.key);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
createResponsePresenter(chatId, messageKey, resumeFromChunk = 0) {
|
|
679
|
+
const api = this.bot.api;
|
|
680
|
+
return new telegram_response_presenter_js_1.TelegramResponsePresenter({
|
|
681
|
+
send: async (content) => {
|
|
682
|
+
const sent = await api.sendMessage(chatId, content);
|
|
683
|
+
return String(sent.message_id);
|
|
684
|
+
},
|
|
685
|
+
edit: async (handle, content) => {
|
|
686
|
+
await api.editMessageText(chatId, Number(handle), content);
|
|
687
|
+
},
|
|
688
|
+
delete: async (handle) => {
|
|
689
|
+
await api.deleteMessage(chatId, Number(handle));
|
|
690
|
+
},
|
|
691
|
+
}, {
|
|
692
|
+
resumeFromChunk,
|
|
693
|
+
onChunkProgress: (nextIndex, uncertain) => {
|
|
694
|
+
if (this.messageLedger.get(messageKey)?.state === 'ready') {
|
|
695
|
+
this.messageLedger.markDeliveryProgress(messageKey, nextIndex, uncertain);
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
outboundLedgerKey(idempotencyKey, kind) {
|
|
701
|
+
const digest = (0, node_crypto_1.createHash)('sha256').update(`${kind}\0${idempotencyKey}`).digest('hex');
|
|
702
|
+
return `outbound:${digest}`;
|
|
377
703
|
}
|
|
378
704
|
async sendSticker(chatId, emotion) {
|
|
379
705
|
if (!this.bot)
|
|
380
706
|
throw new Error('Telegram gateway not connected');
|
|
707
|
+
return this.runInChatQueue(String(chatId), () => this.sendStickerNow(chatId, emotion));
|
|
708
|
+
}
|
|
709
|
+
async sendStickerFromActiveTurn(chatId, emotion) {
|
|
710
|
+
if (!this.bot)
|
|
711
|
+
throw new Error('Telegram gateway not connected');
|
|
712
|
+
return this.runInChatQueue(String(chatId), () => this.sendStickerNow(chatId, emotion), true);
|
|
713
|
+
}
|
|
714
|
+
async sendStickerNow(chatId, emotion) {
|
|
715
|
+
const bot = this.bot;
|
|
716
|
+
if (!bot)
|
|
717
|
+
throw new Error('Telegram gateway not connected');
|
|
381
718
|
await this.loadStickerSet();
|
|
382
719
|
const candidates = EMOTION_EMOJI[emotion] ?? EMOTION_EMOJI.happy;
|
|
383
720
|
const numChatId = typeof chatId === 'string' ? Number(chatId) : chatId;
|
|
384
721
|
for (const emoji of candidates) {
|
|
385
722
|
const fileId = this.stickerCache.get(emoji);
|
|
386
723
|
if (fileId) {
|
|
387
|
-
await
|
|
724
|
+
await bot.api.sendSticker(numChatId, fileId);
|
|
388
725
|
return true;
|
|
389
726
|
}
|
|
390
727
|
}
|
|
391
|
-
await
|
|
728
|
+
await bot.api.sendMessage(numChatId, candidates[0]);
|
|
392
729
|
return false;
|
|
393
730
|
}
|
|
394
731
|
getLastError() {
|
|
@@ -397,23 +734,6 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
|
|
|
397
734
|
getLastMessageAt() {
|
|
398
735
|
return this.lastMessageAt;
|
|
399
736
|
}
|
|
400
|
-
splitMessage(text, maxLength) {
|
|
401
|
-
if (text.length <= maxLength)
|
|
402
|
-
return [text];
|
|
403
|
-
const chunks = [];
|
|
404
|
-
let remaining = text;
|
|
405
|
-
while (remaining.length > 0) {
|
|
406
|
-
if (remaining.length <= maxLength) {
|
|
407
|
-
chunks.push(remaining);
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
const newline = remaining.lastIndexOf('\n', maxLength);
|
|
411
|
-
const splitAt = newline > maxLength * 0.3 ? newline + 1 : maxLength;
|
|
412
|
-
chunks.push(remaining.slice(0, splitAt));
|
|
413
|
-
remaining = remaining.slice(splitAt);
|
|
414
|
-
}
|
|
415
|
-
return chunks;
|
|
416
|
-
}
|
|
417
737
|
async loadStickerSet() {
|
|
418
738
|
if (this.stickerSetLoaded || !this.bot)
|
|
419
739
|
return;
|