@dotsetlabs/dotclaw 1.8.0 → 2.0.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.
Files changed (175) hide show
  1. package/.env.example +6 -0
  2. package/README.md +14 -7
  3. package/config-examples/groups/global/CLAUDE.md +6 -14
  4. package/config-examples/groups/main/CLAUDE.md +8 -39
  5. package/config-examples/runtime.json +4 -4
  6. package/container/Dockerfile +20 -2
  7. package/container/agent-runner/package-lock.json +260 -2
  8. package/container/agent-runner/package.json +2 -1
  9. package/container/agent-runner/src/agent-config.ts +57 -8
  10. package/container/agent-runner/src/browser.ts +180 -0
  11. package/container/agent-runner/src/container-protocol.ts +5 -0
  12. package/container/agent-runner/src/id.ts +3 -2
  13. package/container/agent-runner/src/index.ts +184 -390
  14. package/container/agent-runner/src/ipc.ts +33 -1
  15. package/container/agent-runner/src/mcp-client.ts +222 -0
  16. package/container/agent-runner/src/mcp-registry.ts +163 -0
  17. package/container/agent-runner/src/skill-loader.ts +375 -0
  18. package/container/agent-runner/src/tools.ts +260 -32
  19. package/container/agent-runner/src/tts.ts +61 -0
  20. package/container/agent-runner/tsconfig.json +1 -0
  21. package/dist/admin-commands.d.ts.map +1 -1
  22. package/dist/admin-commands.js +12 -0
  23. package/dist/admin-commands.js.map +1 -1
  24. package/dist/agent-execution.d.ts +3 -1
  25. package/dist/agent-execution.d.ts.map +1 -1
  26. package/dist/agent-execution.js +21 -0
  27. package/dist/agent-execution.js.map +1 -1
  28. package/dist/background-job-classifier.d.ts +1 -1
  29. package/dist/background-job-classifier.d.ts.map +1 -1
  30. package/dist/background-jobs.d.ts.map +1 -1
  31. package/dist/background-jobs.js +30 -10
  32. package/dist/background-jobs.js.map +1 -1
  33. package/dist/cli.js +61 -16
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config.d.ts +0 -2
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +1 -3
  38. package/dist/config.js.map +1 -1
  39. package/dist/container-protocol.d.ts +5 -0
  40. package/dist/container-protocol.d.ts.map +1 -1
  41. package/dist/container-runner.d.ts.map +1 -1
  42. package/dist/container-runner.js +33 -14
  43. package/dist/container-runner.js.map +1 -1
  44. package/dist/dashboard.d.ts +5 -0
  45. package/dist/dashboard.d.ts.map +1 -1
  46. package/dist/dashboard.js +11 -5
  47. package/dist/dashboard.js.map +1 -1
  48. package/dist/db.d.ts +0 -13
  49. package/dist/db.d.ts.map +1 -1
  50. package/dist/db.js +41 -70
  51. package/dist/db.js.map +1 -1
  52. package/dist/error-messages.d.ts.map +1 -1
  53. package/dist/error-messages.js +13 -5
  54. package/dist/error-messages.js.map +1 -1
  55. package/dist/hooks.d.ts +7 -0
  56. package/dist/hooks.d.ts.map +1 -0
  57. package/dist/hooks.js +93 -0
  58. package/dist/hooks.js.map +1 -0
  59. package/dist/id.d.ts.map +1 -1
  60. package/dist/id.js +2 -1
  61. package/dist/id.js.map +1 -1
  62. package/dist/index.js +742 -2793
  63. package/dist/index.js.map +1 -1
  64. package/dist/ipc-dispatcher.d.ts +26 -0
  65. package/dist/ipc-dispatcher.d.ts.map +1 -0
  66. package/dist/ipc-dispatcher.js +1044 -0
  67. package/dist/ipc-dispatcher.js.map +1 -0
  68. package/dist/local-embeddings.d.ts +7 -0
  69. package/dist/local-embeddings.d.ts.map +1 -0
  70. package/dist/local-embeddings.js +60 -0
  71. package/dist/local-embeddings.js.map +1 -0
  72. package/dist/maintenance.d.ts.map +1 -1
  73. package/dist/maintenance.js +7 -1
  74. package/dist/maintenance.js.map +1 -1
  75. package/dist/memory-embeddings.d.ts +1 -1
  76. package/dist/memory-embeddings.d.ts.map +1 -1
  77. package/dist/memory-embeddings.js +59 -31
  78. package/dist/memory-embeddings.js.map +1 -1
  79. package/dist/memory-store.d.ts +0 -10
  80. package/dist/memory-store.d.ts.map +1 -1
  81. package/dist/memory-store.js +12 -28
  82. package/dist/memory-store.js.map +1 -1
  83. package/dist/message-pipeline.d.ts +47 -0
  84. package/dist/message-pipeline.d.ts.map +1 -0
  85. package/dist/message-pipeline.js +876 -0
  86. package/dist/message-pipeline.js.map +1 -0
  87. package/dist/metrics.d.ts +8 -8
  88. package/dist/metrics.d.ts.map +1 -1
  89. package/dist/metrics.js.map +1 -1
  90. package/dist/model-registry.d.ts +0 -14
  91. package/dist/model-registry.d.ts.map +1 -1
  92. package/dist/model-registry.js +0 -36
  93. package/dist/model-registry.js.map +1 -1
  94. package/dist/orchestration.d.ts +39 -0
  95. package/dist/orchestration.d.ts.map +1 -0
  96. package/dist/orchestration.js +136 -0
  97. package/dist/orchestration.js.map +1 -0
  98. package/dist/paths.d.ts.map +1 -1
  99. package/dist/paths.js +2 -0
  100. package/dist/paths.js.map +1 -1
  101. package/dist/providers/discord/discord-format.d.ts +16 -0
  102. package/dist/providers/discord/discord-format.d.ts.map +1 -0
  103. package/dist/providers/discord/discord-format.js +153 -0
  104. package/dist/providers/discord/discord-format.js.map +1 -0
  105. package/dist/providers/discord/discord-provider.d.ts +50 -0
  106. package/dist/providers/discord/discord-provider.d.ts.map +1 -0
  107. package/dist/providers/discord/discord-provider.js +604 -0
  108. package/dist/providers/discord/discord-provider.js.map +1 -0
  109. package/dist/providers/discord/index.d.ts +4 -0
  110. package/dist/providers/discord/index.d.ts.map +1 -0
  111. package/dist/providers/discord/index.js +3 -0
  112. package/dist/providers/discord/index.js.map +1 -0
  113. package/dist/providers/registry.d.ts +14 -0
  114. package/dist/providers/registry.d.ts.map +1 -0
  115. package/dist/providers/registry.js +49 -0
  116. package/dist/providers/registry.js.map +1 -0
  117. package/dist/providers/telegram/index.d.ts +4 -0
  118. package/dist/providers/telegram/index.d.ts.map +1 -0
  119. package/dist/providers/telegram/index.js +3 -0
  120. package/dist/providers/telegram/index.js.map +1 -0
  121. package/dist/providers/telegram/telegram-format.d.ts +3 -0
  122. package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
  123. package/dist/providers/telegram/telegram-format.js +215 -0
  124. package/dist/providers/telegram/telegram-format.js.map +1 -0
  125. package/dist/providers/telegram/telegram-provider.d.ts +51 -0
  126. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
  127. package/dist/providers/telegram/telegram-provider.js +824 -0
  128. package/dist/providers/telegram/telegram-provider.js.map +1 -0
  129. package/dist/providers/types.d.ts +107 -0
  130. package/dist/providers/types.d.ts.map +1 -0
  131. package/dist/providers/types.js +2 -0
  132. package/dist/providers/types.js.map +1 -0
  133. package/dist/request-router.d.ts.map +1 -1
  134. package/dist/request-router.js +12 -26
  135. package/dist/request-router.js.map +1 -1
  136. package/dist/runtime-config.d.ts +70 -6
  137. package/dist/runtime-config.d.ts.map +1 -1
  138. package/dist/runtime-config.js +119 -65
  139. package/dist/runtime-config.js.map +1 -1
  140. package/dist/skill-manager.d.ts +39 -0
  141. package/dist/skill-manager.d.ts.map +1 -0
  142. package/dist/skill-manager.js +286 -0
  143. package/dist/skill-manager.js.map +1 -0
  144. package/dist/task-scheduler.d.ts.map +1 -1
  145. package/dist/task-scheduler.js +56 -11
  146. package/dist/task-scheduler.js.map +1 -1
  147. package/dist/telegram-format.d.ts.map +1 -1
  148. package/dist/telegram-format.js +16 -1
  149. package/dist/telegram-format.js.map +1 -1
  150. package/dist/tool-intent-probe.d.ts +11 -0
  151. package/dist/tool-intent-probe.d.ts.map +1 -0
  152. package/dist/tool-intent-probe.js +63 -0
  153. package/dist/tool-intent-probe.js.map +1 -0
  154. package/dist/tool-policy.d.ts.map +1 -1
  155. package/dist/tool-policy.js +18 -0
  156. package/dist/tool-policy.js.map +1 -1
  157. package/dist/transcription.d.ts +8 -0
  158. package/dist/transcription.d.ts.map +1 -0
  159. package/dist/transcription.js +174 -0
  160. package/dist/transcription.js.map +1 -0
  161. package/dist/types.d.ts +2 -9
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/workflow-engine.d.ts +51 -0
  164. package/dist/workflow-engine.d.ts.map +1 -0
  165. package/dist/workflow-engine.js +281 -0
  166. package/dist/workflow-engine.js.map +1 -0
  167. package/dist/workflow-store.d.ts +39 -0
  168. package/dist/workflow-store.d.ts.map +1 -0
  169. package/dist/workflow-store.js +173 -0
  170. package/dist/workflow-store.js.map +1 -0
  171. package/package.json +15 -3
  172. package/scripts/bootstrap.js +40 -4
  173. package/scripts/configure.js +48 -7
  174. package/scripts/doctor.js +30 -4
  175. package/scripts/init.js +13 -6
@@ -0,0 +1,824 @@
1
+ import { Telegraf } from 'telegraf';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { ProviderRegistry } from '../registry.js';
5
+ import { formatTelegramMessage, TELEGRAM_PARSE_MODE } from './telegram-format.js';
6
+ import { generateId } from '../../id.js';
7
+ import { logger } from '../../logger.js';
8
+ const MAX_MESSAGE_LENGTH = 4000;
9
+ const SEND_DELAY_MS = 250;
10
+ const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
11
+ const FILE_DOWNLOAD_TIMEOUT_MS = 45_000;
12
+ function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(resolve, ms));
14
+ }
15
+ function getErrorCode(err) {
16
+ const anyErr = err;
17
+ if (typeof anyErr?.code === 'number')
18
+ return anyErr.code;
19
+ if (typeof anyErr?.response?.error_code === 'number')
20
+ return anyErr.response.error_code;
21
+ return null;
22
+ }
23
+ function getRetryAfterMs(err) {
24
+ const anyErr = err;
25
+ const retryAfter = anyErr?.parameters?.retry_after ?? anyErr?.response?.parameters?.retry_after;
26
+ if (typeof retryAfter === 'number' && Number.isFinite(retryAfter))
27
+ return retryAfter * 1000;
28
+ if (typeof retryAfter === 'string') {
29
+ const parsed = Number.parseInt(retryAfter, 10);
30
+ if (Number.isFinite(parsed))
31
+ return parsed * 1000;
32
+ }
33
+ return null;
34
+ }
35
+ function isRetryableError(err) {
36
+ const code = getErrorCode(err);
37
+ if (code === 429)
38
+ return true;
39
+ if (code && code >= 500 && code < 600)
40
+ return true;
41
+ const anyErr = err;
42
+ if (!anyErr?.code)
43
+ return false;
44
+ return ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'EAI_AGAIN', 'ENOTFOUND'].includes(anyErr.code);
45
+ }
46
+ function splitPlainText(text, maxLength) {
47
+ if (text.length <= maxLength)
48
+ return [text];
49
+ const chunks = [];
50
+ for (let i = 0; i < text.length; i += maxLength) {
51
+ chunks.push(text.slice(i, i + maxLength));
52
+ }
53
+ return chunks;
54
+ }
55
+ function isAllowedInlineButtonUrl(value) {
56
+ try {
57
+ const parsed = new URL(value);
58
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'tg:';
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ function normalizeInlineKeyboard(rawButtons) {
65
+ if (rawButtons.length === 0)
66
+ return null;
67
+ const rows = [];
68
+ for (const rawRow of rawButtons) {
69
+ if (rawRow.length === 0)
70
+ return null;
71
+ const row = [];
72
+ for (const rawButton of rawRow) {
73
+ const text = rawButton.text?.trim() || '';
74
+ const url = rawButton.url?.trim() || '';
75
+ const callbackData = rawButton.callbackData || '';
76
+ const hasUrl = url.length > 0;
77
+ const hasCallback = callbackData.length > 0;
78
+ if (!text || hasUrl === hasCallback)
79
+ return null;
80
+ if (hasUrl && !isAllowedInlineButtonUrl(url))
81
+ return null;
82
+ if (hasCallback && callbackData.length > 64)
83
+ return null;
84
+ if (hasUrl)
85
+ row.push({ text, url });
86
+ else
87
+ row.push({ text, callback_data: callbackData });
88
+ }
89
+ rows.push(row);
90
+ }
91
+ return rows;
92
+ }
93
+ function normalizePollOptions(rawOptions) {
94
+ const options = rawOptions
95
+ .map(option => option.trim())
96
+ .filter(Boolean);
97
+ if (options.length < 2 || options.length > 10)
98
+ return null;
99
+ if (options.some(option => option.length > 100))
100
+ return null;
101
+ if (new Set(options.map(option => option.toLowerCase())).size !== options.length)
102
+ return null;
103
+ return options;
104
+ }
105
+ export class TelegramProvider {
106
+ name = 'telegram';
107
+ capabilities = {
108
+ maxMessageLength: MAX_MESSAGE_LENGTH,
109
+ maxAttachmentBytes: MAX_ATTACHMENT_BYTES,
110
+ supportsInlineButtons: true,
111
+ supportsPoll: true,
112
+ supportsVoiceMessages: true,
113
+ supportsLocation: true,
114
+ supportsContact: true,
115
+ supportsReactions: true,
116
+ supportsThreads: true,
117
+ };
118
+ bot;
119
+ config;
120
+ connected = false;
121
+ botUsername = '';
122
+ botId;
123
+ // Callback data store for inline buttons (5-minute TTL)
124
+ callbackDataStore = new Map();
125
+ callbackCleanupInterval = null;
126
+ constructor(config) {
127
+ this.config = config;
128
+ this.bot = new Telegraf(config.token, {
129
+ handlerTimeout: config.handlerTimeoutMs,
130
+ });
131
+ this.bot.catch((err, ctx) => {
132
+ logger.error({ err, chatId: ctx?.chat?.id }, 'Unhandled Telegraf error');
133
+ });
134
+ }
135
+ get telegrafBot() {
136
+ return this.bot;
137
+ }
138
+ async start(handlers) {
139
+ this.setupHandlers(handlers);
140
+ this.bot.launch();
141
+ this.connected = true;
142
+ // Eagerly fetch bot info so isBotMentioned() works immediately
143
+ try {
144
+ const me = await this.bot.telegram.getMe();
145
+ this.botUsername = me.username || '';
146
+ this.botId = me.id;
147
+ }
148
+ catch (err) {
149
+ logger.warn({ err }, 'Failed to fetch bot info on start; will populate on first message');
150
+ this.botUsername = this.bot.botInfo?.username || '';
151
+ this.botId = this.bot.botInfo?.id;
152
+ }
153
+ this.callbackCleanupInterval = setInterval(() => {
154
+ const cutoff = Date.now() - 5 * 60 * 1000;
155
+ for (const [id, entry] of this.callbackDataStore) {
156
+ if (entry.createdAt < cutoff) {
157
+ this.callbackDataStore.delete(id);
158
+ }
159
+ }
160
+ }, 60_000);
161
+ }
162
+ stop() {
163
+ this.connected = false;
164
+ this.bot.stop('SHUTDOWN');
165
+ if (this.callbackCleanupInterval) {
166
+ clearInterval(this.callbackCleanupInterval);
167
+ this.callbackCleanupInterval = null;
168
+ }
169
+ return Promise.resolve();
170
+ }
171
+ isConnected() {
172
+ return this.connected;
173
+ }
174
+ async sendMessage(chatId, text, opts) {
175
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
176
+ const parseMode = opts?.parseMode === undefined ? TELEGRAM_PARSE_MODE : opts.parseMode;
177
+ const chunks = parseMode
178
+ ? formatTelegramMessage(text, MAX_MESSAGE_LENGTH)
179
+ : splitPlainText(text, MAX_MESSAGE_LENGTH);
180
+ let firstMessageId;
181
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
182
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
183
+ for (let i = 0; i < chunks.length; i += 1) {
184
+ const ok = await this.sendChunk(rawChatId, chunks[i], parseMode, threadId, i === 0 ? replyToId : undefined);
185
+ if (!ok.success)
186
+ return { success: false };
187
+ if (!firstMessageId && ok.messageId) {
188
+ firstMessageId = ok.messageId;
189
+ }
190
+ if (i < chunks.length - 1) {
191
+ await sleep(SEND_DELAY_MS);
192
+ }
193
+ }
194
+ logger.info({ chatId: rawChatId, length: text.length }, 'Message sent');
195
+ return { success: true, messageId: firstMessageId };
196
+ }
197
+ async sendChunk(chatId, chunk, parseMode, threadId, replyToId) {
198
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
199
+ try {
200
+ const payload = {};
201
+ if (parseMode)
202
+ payload.parse_mode = parseMode;
203
+ if (threadId)
204
+ payload.message_thread_id = threadId;
205
+ if (replyToId) {
206
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
207
+ }
208
+ const sent = await this.bot.telegram.sendMessage(chatId, chunk, payload);
209
+ return { success: true, messageId: String(sent.message_id) };
210
+ }
211
+ catch (err) {
212
+ const retryAfterMs = getRetryAfterMs(err);
213
+ const retryable = isRetryableError(err);
214
+ if (!retryable || attempt === this.config.sendRetries) {
215
+ logger.error({ chatId, attempt, err }, 'Failed to send Telegram message chunk');
216
+ return { success: false };
217
+ }
218
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
219
+ logger.warn({ chatId, attempt, delayMs }, 'Telegram send failed; retrying');
220
+ await sleep(delayMs);
221
+ }
222
+ }
223
+ return { success: false };
224
+ }
225
+ async sendPhoto(chatId, filePath, opts) {
226
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
227
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
228
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
229
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
230
+ try {
231
+ const payload = {};
232
+ if (opts?.caption)
233
+ payload.caption = opts.caption;
234
+ if (threadId)
235
+ payload.message_thread_id = threadId;
236
+ if (replyToId) {
237
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
238
+ }
239
+ const sent = await this.bot.telegram.sendPhoto(rawChatId, { source: filePath }, payload);
240
+ logger.info({ chatId: rawChatId, filePath }, 'Photo sent');
241
+ return { success: true, messageId: String(sent.message_id) };
242
+ }
243
+ catch (err) {
244
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
245
+ logger.error({ chatId: rawChatId, filePath, attempt, err }, 'Failed to send photo');
246
+ return { success: false };
247
+ }
248
+ const retryAfterMs = getRetryAfterMs(err);
249
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
250
+ logger.warn({ chatId: rawChatId, attempt, delayMs }, 'Photo send failed; retrying');
251
+ await sleep(delayMs);
252
+ }
253
+ }
254
+ return { success: false };
255
+ }
256
+ async sendDocument(chatId, filePath, opts) {
257
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
258
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
259
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
260
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
261
+ try {
262
+ const payload = {};
263
+ if (opts?.caption)
264
+ payload.caption = opts.caption;
265
+ if (threadId)
266
+ payload.message_thread_id = threadId;
267
+ if (replyToId) {
268
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
269
+ }
270
+ const sent = await this.bot.telegram.sendDocument(rawChatId, { source: filePath }, payload);
271
+ logger.info({ chatId: rawChatId, filePath }, 'Document sent');
272
+ return { success: true, messageId: String(sent.message_id) };
273
+ }
274
+ catch (err) {
275
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
276
+ logger.error({ chatId: rawChatId, filePath, attempt, err }, 'Failed to send document');
277
+ return { success: false };
278
+ }
279
+ const retryAfterMs = getRetryAfterMs(err);
280
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
281
+ logger.warn({ chatId: rawChatId, attempt, delayMs }, 'Document send failed; retrying');
282
+ await sleep(delayMs);
283
+ }
284
+ }
285
+ return { success: false };
286
+ }
287
+ async sendVoice(chatId, filePath, opts) {
288
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
289
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
290
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
291
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
292
+ try {
293
+ const payload = {};
294
+ if (opts?.caption)
295
+ payload.caption = opts.caption;
296
+ if (opts?.duration)
297
+ payload.duration = opts.duration;
298
+ if (threadId)
299
+ payload.message_thread_id = threadId;
300
+ if (replyToId) {
301
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
302
+ }
303
+ const sent = await this.bot.telegram.sendVoice(rawChatId, { source: filePath }, payload);
304
+ logger.info({ chatId: rawChatId, filePath }, 'Voice sent');
305
+ return { success: true, messageId: String(sent.message_id) };
306
+ }
307
+ catch (err) {
308
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
309
+ logger.error({ chatId: rawChatId, filePath, attempt, err }, 'Failed to send voice');
310
+ return { success: false };
311
+ }
312
+ const retryAfterMs = getRetryAfterMs(err);
313
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
314
+ await sleep(delayMs);
315
+ }
316
+ }
317
+ return { success: false };
318
+ }
319
+ async sendAudio(chatId, filePath, opts) {
320
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
321
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
322
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
323
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
324
+ try {
325
+ const payload = {};
326
+ if (opts?.caption)
327
+ payload.caption = opts.caption;
328
+ if (opts?.duration)
329
+ payload.duration = opts.duration;
330
+ if (opts?.performer)
331
+ payload.performer = opts.performer;
332
+ if (opts?.title)
333
+ payload.title = opts.title;
334
+ if (threadId)
335
+ payload.message_thread_id = threadId;
336
+ if (replyToId) {
337
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
338
+ }
339
+ const sent = await this.bot.telegram.sendAudio(rawChatId, { source: filePath }, payload);
340
+ logger.info({ chatId: rawChatId, filePath }, 'Audio sent');
341
+ return { success: true, messageId: String(sent.message_id) };
342
+ }
343
+ catch (err) {
344
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
345
+ logger.error({ chatId: rawChatId, filePath, attempt, err }, 'Failed to send audio');
346
+ return { success: false };
347
+ }
348
+ const retryAfterMs = getRetryAfterMs(err);
349
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
350
+ await sleep(delayMs);
351
+ }
352
+ }
353
+ return { success: false };
354
+ }
355
+ async sendLocation(chatId, lat, lng, opts) {
356
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
357
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
358
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
359
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
360
+ try {
361
+ const payload = {};
362
+ if (threadId)
363
+ payload.message_thread_id = threadId;
364
+ if (replyToId) {
365
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
366
+ }
367
+ await this.bot.telegram.sendLocation(rawChatId, lat, lng, payload);
368
+ logger.info({ chatId: rawChatId, lat, lng }, 'Location sent');
369
+ return { success: true };
370
+ }
371
+ catch (err) {
372
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
373
+ logger.error({ chatId: rawChatId, attempt, err }, 'Failed to send location');
374
+ return { success: false };
375
+ }
376
+ const retryAfterMs = getRetryAfterMs(err);
377
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
378
+ logger.warn({ chatId: rawChatId, attempt, delayMs }, 'Location send failed; retrying');
379
+ await sleep(delayMs);
380
+ }
381
+ }
382
+ return { success: false };
383
+ }
384
+ async sendContact(chatId, phone, name, opts) {
385
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
386
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
387
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
388
+ for (let attempt = 1; attempt <= this.config.sendRetries; attempt += 1) {
389
+ try {
390
+ const payload = {};
391
+ if (opts?.lastName)
392
+ payload.last_name = opts.lastName;
393
+ if (threadId)
394
+ payload.message_thread_id = threadId;
395
+ if (replyToId) {
396
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
397
+ }
398
+ await this.bot.telegram.sendContact(rawChatId, phone, name, payload);
399
+ logger.info({ chatId: rawChatId, phone }, 'Contact sent');
400
+ return { success: true };
401
+ }
402
+ catch (err) {
403
+ if (!isRetryableError(err) || attempt === this.config.sendRetries) {
404
+ logger.error({ chatId: rawChatId, attempt, err }, 'Failed to send contact');
405
+ return { success: false };
406
+ }
407
+ const retryAfterMs = getRetryAfterMs(err);
408
+ const delayMs = retryAfterMs ?? (this.config.sendRetryDelayMs * attempt);
409
+ logger.warn({ chatId: rawChatId, attempt, delayMs }, 'Contact send failed; retrying');
410
+ await sleep(delayMs);
411
+ }
412
+ }
413
+ return { success: false };
414
+ }
415
+ async sendPoll(chatId, question, options, opts) {
416
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
417
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
418
+ const normalized = normalizePollOptions(options);
419
+ if (!normalized) {
420
+ logger.warn({ chatId: rawChatId }, 'Invalid poll options');
421
+ return { success: false };
422
+ }
423
+ try {
424
+ const payload = {};
425
+ if (opts?.isAnonymous !== undefined)
426
+ payload.is_anonymous = opts.isAnonymous;
427
+ if (opts?.type)
428
+ payload.type = opts.type;
429
+ if (opts?.allowsMultipleAnswers !== undefined)
430
+ payload.allows_multiple_answers = opts.allowsMultipleAnswers;
431
+ if (opts?.correctOptionId !== undefined)
432
+ payload.correct_option_id = opts.correctOptionId;
433
+ if (replyToId) {
434
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
435
+ }
436
+ const sent = await this.bot.telegram.sendPoll(rawChatId, question, normalized, payload);
437
+ logger.info({ chatId: rawChatId, question }, 'Poll sent');
438
+ return { success: true, messageId: String(sent.message_id) };
439
+ }
440
+ catch (err) {
441
+ logger.error({ chatId: rawChatId, err }, 'Failed to send poll');
442
+ return { success: false };
443
+ }
444
+ }
445
+ async sendButtons(chatId, text, buttons, opts) {
446
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
447
+ const replyToId = opts?.replyToMessageId ? Number(opts.replyToMessageId) : undefined;
448
+ const threadId = opts?.threadId ? Number(opts.threadId) : undefined;
449
+ const normalized = normalizeInlineKeyboard(buttons);
450
+ if (!normalized) {
451
+ logger.warn({ chatId: rawChatId }, 'Invalid button layout');
452
+ return { success: false };
453
+ }
454
+ // Register callback data and replace with IDs
455
+ const registered = normalized.map(row => row.map(btn => {
456
+ if (btn.callback_data && !btn.url) {
457
+ const cbId = this.registerCallbackData(chatId, btn.callback_data, btn.text);
458
+ return { text: btn.text, callback_data: cbId };
459
+ }
460
+ return btn;
461
+ }));
462
+ try {
463
+ const payload = {
464
+ reply_markup: { inline_keyboard: registered }
465
+ };
466
+ if (threadId)
467
+ payload.message_thread_id = threadId;
468
+ if (replyToId) {
469
+ payload.reply_parameters = { message_id: replyToId, allow_sending_without_reply: true };
470
+ }
471
+ const sent = await this.bot.telegram.sendMessage(rawChatId, text, payload);
472
+ logger.info({ chatId: rawChatId }, 'Inline keyboard sent');
473
+ return { success: true, messageId: String(sent.message_id) };
474
+ }
475
+ catch (err) {
476
+ logger.error({ chatId: rawChatId, err }, 'Failed to send inline keyboard');
477
+ return { success: false };
478
+ }
479
+ }
480
+ async editMessage(chatId, messageId, text) {
481
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
482
+ const numericMsgId = Number.parseInt(messageId, 10);
483
+ if (!Number.isFinite(numericMsgId))
484
+ return { success: false };
485
+ try {
486
+ await this.bot.telegram.editMessageText(rawChatId, numericMsgId, undefined, text);
487
+ return { success: true };
488
+ }
489
+ catch (err) {
490
+ logger.error({ chatId: rawChatId, messageId, err }, 'Failed to edit message');
491
+ return { success: false };
492
+ }
493
+ }
494
+ async deleteMessage(chatId, messageId) {
495
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
496
+ const numericMsgId = Number.parseInt(messageId, 10);
497
+ if (!Number.isFinite(numericMsgId))
498
+ return { success: false };
499
+ try {
500
+ await this.bot.telegram.deleteMessage(rawChatId, numericMsgId);
501
+ return { success: true };
502
+ }
503
+ catch (err) {
504
+ logger.error({ chatId: rawChatId, messageId, err }, 'Failed to delete message');
505
+ return { success: false };
506
+ }
507
+ }
508
+ async downloadFile(ref, groupFolder, filename) {
509
+ let localPath = null;
510
+ let tmpPath = null;
511
+ try {
512
+ const fileLink = await this.bot.telegram.getFileLink(ref);
513
+ const url = fileLink.href || String(fileLink);
514
+ const abortController = new AbortController();
515
+ const timeout = setTimeout(() => abortController.abort(), FILE_DOWNLOAD_TIMEOUT_MS);
516
+ let response;
517
+ try {
518
+ response = await fetch(url, { signal: abortController.signal });
519
+ }
520
+ finally {
521
+ clearTimeout(timeout);
522
+ }
523
+ if (!response.ok) {
524
+ logger.warn({ fileId: ref, status: response.status }, 'Failed to download Telegram file');
525
+ return { path: null, error: 'download_failed' };
526
+ }
527
+ const contentLength = response.headers.get('content-length');
528
+ const declaredSize = contentLength ? parseInt(contentLength, 10) : NaN;
529
+ if (Number.isFinite(declaredSize) && declaredSize > MAX_ATTACHMENT_BYTES) {
530
+ logger.warn({ fileId: ref, size: contentLength }, 'Telegram file too large (>20MB)');
531
+ return { path: null, error: 'too_large' };
532
+ }
533
+ const inboxDir = path.join(this.config.groupsDir, groupFolder, 'inbox');
534
+ fs.mkdirSync(inboxDir, { recursive: true });
535
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
536
+ const localName = `${Date.now()}_${safeName}`;
537
+ localPath = path.join(inboxDir, localName);
538
+ tmpPath = `${localPath}.tmp`;
539
+ const fileStream = fs.createWriteStream(tmpPath, { flags: 'wx' });
540
+ let bytesWritten = 0;
541
+ const body = response.body;
542
+ if (!body) {
543
+ throw new Error('Telegram response had no body');
544
+ }
545
+ const reader = body.getReader();
546
+ try {
547
+ while (true) {
548
+ const { done, value } = await reader.read();
549
+ if (done)
550
+ break;
551
+ if (!value || value.byteLength === 0)
552
+ continue;
553
+ bytesWritten += value.byteLength;
554
+ if (bytesWritten > MAX_ATTACHMENT_BYTES) {
555
+ await reader.cancel();
556
+ throw new Error('STREAMING_TOO_LARGE');
557
+ }
558
+ if (!fileStream.write(Buffer.from(value))) {
559
+ await new Promise(resolve => fileStream.once('drain', resolve));
560
+ }
561
+ }
562
+ await new Promise((resolve, reject) => {
563
+ fileStream.end((err) => {
564
+ if (err)
565
+ reject(err);
566
+ else
567
+ resolve();
568
+ });
569
+ });
570
+ }
571
+ catch (streamErr) {
572
+ fileStream.destroy();
573
+ throw streamErr;
574
+ }
575
+ fs.renameSync(tmpPath, localPath);
576
+ tmpPath = null;
577
+ logger.info({ fileId: ref, localPath, size: bytesWritten }, 'Downloaded Telegram file');
578
+ return { path: localPath };
579
+ }
580
+ catch (err) {
581
+ const isTooLarge = err instanceof Error && err.message === 'STREAMING_TOO_LARGE';
582
+ if (isTooLarge) {
583
+ logger.warn({ fileId: ref }, 'Telegram file too large (>20MB) during streaming');
584
+ }
585
+ else {
586
+ logger.error({ fileId: ref, err }, 'Error downloading Telegram file');
587
+ }
588
+ return { path: null, error: isTooLarge ? 'too_large' : 'download_failed' };
589
+ }
590
+ finally {
591
+ if (tmpPath && fs.existsSync(tmpPath)) {
592
+ try {
593
+ fs.unlinkSync(tmpPath);
594
+ }
595
+ catch { /* ignore */ }
596
+ }
597
+ if (localPath && fs.existsSync(localPath) && fs.statSync(localPath).size === 0) {
598
+ try {
599
+ fs.unlinkSync(localPath);
600
+ }
601
+ catch { /* ignore */ }
602
+ }
603
+ }
604
+ }
605
+ formatMessage(text, maxLength) {
606
+ return formatTelegramMessage(text, maxLength);
607
+ }
608
+ async setTyping(chatId) {
609
+ const rawChatId = ProviderRegistry.stripPrefix(chatId);
610
+ try {
611
+ await this.bot.telegram.sendChatAction(rawChatId, 'typing');
612
+ }
613
+ catch (err) {
614
+ logger.debug({ chatId: rawChatId, err }, 'Failed to set typing indicator');
615
+ }
616
+ }
617
+ isBotMentioned(message) {
618
+ const raw = message.rawProviderData;
619
+ const entities = raw?.entities;
620
+ if (!entities || entities.length === 0)
621
+ return false;
622
+ const normalized = this.botUsername ? this.botUsername.toLowerCase() : '';
623
+ for (const entity of entities) {
624
+ const segment = message.content.slice(entity.offset, entity.offset + entity.length);
625
+ if (entity.type === 'mention') {
626
+ if (segment.toLowerCase() === `@${normalized}`)
627
+ return true;
628
+ }
629
+ if (entity.type === 'text_mention' && this.botId && entity.user?.id === this.botId)
630
+ return true;
631
+ if (entity.type === 'bot_command') {
632
+ if (segment.toLowerCase().includes(`@${normalized}`))
633
+ return true;
634
+ }
635
+ }
636
+ return false;
637
+ }
638
+ isBotReplied(message) {
639
+ const raw = message.rawProviderData;
640
+ if (!raw?.reply_to_message?.from?.id || !this.botId)
641
+ return false;
642
+ return raw.reply_to_message.from.id === this.botId;
643
+ }
644
+ getBotUsername() {
645
+ return this.botUsername;
646
+ }
647
+ registerCallbackData(chatJid, data, label) {
648
+ const id = generateId('cb');
649
+ this.callbackDataStore.set(id, { chatJid, data, label, createdAt: Date.now() });
650
+ return id;
651
+ }
652
+ setupHandlers(handlers) {
653
+ // Handle message reactions (for feedback)
654
+ this.bot.on('message_reaction', async (ctx) => {
655
+ try {
656
+ const update = ctx.update;
657
+ const reaction = update.message_reaction;
658
+ if (!reaction)
659
+ return;
660
+ const emoji = reaction.new_reaction?.[0]?.emoji;
661
+ if (!emoji)
662
+ return;
663
+ const chatId = ProviderRegistry.addPrefix('telegram', String(reaction.chat.id));
664
+ const messageId = String(reaction.message_id);
665
+ const userId = reaction.user?.id ? String(reaction.user.id) : undefined;
666
+ handlers.onReaction(chatId, messageId, userId, emoji);
667
+ }
668
+ catch (err) {
669
+ logger.debug({ err }, 'Error handling message reaction');
670
+ }
671
+ });
672
+ // Handle callback queries from inline keyboard buttons
673
+ this.bot.on('callback_query', async (ctx) => {
674
+ try {
675
+ const cbQuery = ctx.callbackQuery;
676
+ if (!cbQuery || !('data' in cbQuery) || !cbQuery.data)
677
+ return;
678
+ const callbackId = cbQuery.data;
679
+ const entry = this.callbackDataStore.get(callbackId);
680
+ await ctx.answerCbQuery();
681
+ if (!entry) {
682
+ logger.debug({ callbackId }, 'Unknown callback data');
683
+ return;
684
+ }
685
+ this.callbackDataStore.delete(callbackId);
686
+ const callbackChatId = ctx.chat?.id ? ProviderRegistry.addPrefix('telegram', String(ctx.chat.id)) : '';
687
+ if (callbackChatId && callbackChatId !== entry.chatJid) {
688
+ logger.warn({ callbackChatId, expectedChatId: entry.chatJid }, 'Callback chat mismatch; ignoring');
689
+ return;
690
+ }
691
+ const chatId = callbackChatId || entry.chatJid;
692
+ const senderId = String(cbQuery.from?.id || '');
693
+ const senderName = cbQuery.from?.first_name || cbQuery.from?.username || 'User';
694
+ const rawThreadId = typeof cbQuery.message === 'object' && cbQuery.message && 'message_thread_id' in cbQuery.message
695
+ ? cbQuery.message.message_thread_id
696
+ : undefined;
697
+ const threadId = Number.isFinite(rawThreadId) ? String(rawThreadId) : undefined;
698
+ handlers.onButtonClick(chatId, senderId, senderName, entry.label, entry.data, threadId);
699
+ }
700
+ catch (err) {
701
+ logger.debug({ err }, 'Error handling callback query');
702
+ }
703
+ });
704
+ // Handle all messages (text + media)
705
+ this.bot.on('message', async (ctx) => {
706
+ if (!ctx.message)
707
+ return;
708
+ const msg = ctx.message;
709
+ const content = (typeof msg.text === 'string' ? msg.text : '')
710
+ || (typeof msg.caption === 'string' ? msg.caption : '');
711
+ const rawChatId = String(ctx.chat.id);
712
+ const chatId = ProviderRegistry.addPrefix('telegram', rawChatId);
713
+ const messageId = String(msg.message_id);
714
+ // Build attachment metadata
715
+ const attachments = [];
716
+ if (Array.isArray(msg.photo) && msg.photo.length > 0) {
717
+ const photos = msg.photo;
718
+ const largest = photos[photos.length - 1];
719
+ attachments.push({
720
+ type: 'photo',
721
+ providerFileRef: largest.file_id,
722
+ fileName: `photo_${messageId}.jpg`,
723
+ mimeType: 'image/jpeg',
724
+ fileSize: largest.file_size,
725
+ width: largest.width,
726
+ height: largest.height,
727
+ });
728
+ }
729
+ if (msg.document && typeof msg.document === 'object') {
730
+ const doc = msg.document;
731
+ attachments.push({
732
+ type: 'document',
733
+ providerFileRef: doc.file_id,
734
+ fileName: doc.file_name || `document_${messageId}`,
735
+ mimeType: doc.mime_type,
736
+ fileSize: doc.file_size,
737
+ });
738
+ }
739
+ if (msg.voice && typeof msg.voice === 'object') {
740
+ const voice = msg.voice;
741
+ attachments.push({
742
+ type: 'voice',
743
+ providerFileRef: voice.file_id,
744
+ fileName: `voice_${messageId}.ogg`,
745
+ mimeType: voice.mime_type || 'audio/ogg',
746
+ fileSize: voice.file_size,
747
+ duration: voice.duration,
748
+ });
749
+ }
750
+ if (msg.video && typeof msg.video === 'object') {
751
+ const video = msg.video;
752
+ attachments.push({
753
+ type: 'video',
754
+ providerFileRef: video.file_id,
755
+ fileName: video.file_name || `video_${messageId}.mp4`,
756
+ mimeType: video.mime_type,
757
+ fileSize: video.file_size,
758
+ duration: video.duration,
759
+ width: video.width,
760
+ height: video.height,
761
+ });
762
+ }
763
+ if (msg.audio && typeof msg.audio === 'object') {
764
+ const audio = msg.audio;
765
+ attachments.push({
766
+ type: 'audio',
767
+ providerFileRef: audio.file_id,
768
+ fileName: audio.file_name || `audio_${messageId}.mp3`,
769
+ mimeType: audio.mime_type,
770
+ fileSize: audio.file_size,
771
+ duration: audio.duration,
772
+ });
773
+ }
774
+ if (!content && attachments.length === 0)
775
+ return;
776
+ const chatType = ctx.chat.type;
777
+ const isGroup = chatType === 'group' || chatType === 'supergroup';
778
+ const senderId = String(ctx.from?.id || ctx.chat.id);
779
+ const senderName = ctx.from?.first_name || ctx.from?.username || 'User';
780
+ const timestamp = new Date(msg.date * 1000).toISOString();
781
+ const rawThreadId = msg.message_thread_id;
782
+ const threadId = Number.isFinite(rawThreadId) ? String(rawThreadId) : undefined;
783
+ const entities = 'entities' in msg ? msg.entities : undefined;
784
+ const chatName = ('title' in ctx.chat && ctx.chat.title)
785
+ || ('username' in ctx.chat && ctx.chat.username)
786
+ || ctx.from?.first_name
787
+ || ctx.from?.username
788
+ || senderName;
789
+ const storedContent = content || `[${attachments.map(a => a.type).join(', ')}]`;
790
+ const incoming = {
791
+ chatId,
792
+ messageId,
793
+ senderId,
794
+ senderName,
795
+ content: storedContent,
796
+ timestamp,
797
+ isGroup,
798
+ chatType,
799
+ threadId,
800
+ attachments: attachments.length > 0 ? attachments : undefined,
801
+ rawProviderData: {
802
+ entities,
803
+ reply_to_message: msg.reply_to_message,
804
+ chatName,
805
+ },
806
+ };
807
+ handlers.onMessage(incoming);
808
+ });
809
+ }
810
+ }
811
+ export function createTelegramProvider(runtime, groupsDir) {
812
+ const token = process.env.TELEGRAM_BOT_TOKEN;
813
+ if (!token) {
814
+ throw new Error('TELEGRAM_BOT_TOKEN environment variable is required');
815
+ }
816
+ return new TelegramProvider({
817
+ token,
818
+ handlerTimeoutMs: runtime.host.telegram.handlerTimeoutMs,
819
+ sendRetries: runtime.host.telegram.sendRetries,
820
+ sendRetryDelayMs: runtime.host.telegram.sendRetryDelayMs,
821
+ groupsDir,
822
+ });
823
+ }
824
+ //# sourceMappingURL=telegram-provider.js.map