@diegoaltoworks/chatter 0.58.0 → 0.60.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/dist/channels/index.js +81 -0
- package/dist/channels/index.mjs +81 -0
- package/dist/channels/pipeline.d.ts +8 -0
- package/dist/channels/pipeline.d.ts.map +1 -1
- package/dist/channels/telegram/api.d.ts +17 -0
- package/dist/channels/telegram/api.d.ts.map +1 -1
- package/dist/channels/telegram/channel.d.ts +11 -1
- package/dist/channels/telegram/channel.d.ts.map +1 -1
- package/dist/channels/telegram/handler.d.ts +31 -0
- package/dist/channels/telegram/handler.d.ts.map +1 -0
- package/dist/channels/telegram/index.d.ts +7 -0
- package/dist/channels/telegram/index.d.ts.map +1 -1
- package/dist/channels/telegram/index.js +382 -79
- package/dist/channels/telegram/index.mjs +379 -79
- package/dist/channels/telegram/webhook.d.ts +102 -0
- package/dist/channels/telegram/webhook.d.ts.map +1 -0
- package/dist/channels/whatsapp/inbound.d.ts +7 -0
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +81 -0
- package/dist/channels/whatsapp/index.mjs +81 -0
- package/dist/history/compaction.d.ts +61 -0
- package/dist/history/compaction.d.ts.map +1 -0
- package/dist/history/index.d.ts +7 -0
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/index.js +143 -0
- package/dist/history/index.mjs +141 -0
- package/package.json +1 -1
|
@@ -25,6 +25,9 @@ __export(index_exports, {
|
|
|
25
25
|
TelegramApiError: () => TelegramApiError,
|
|
26
26
|
createTelegramApi: () => createTelegramApi,
|
|
27
27
|
createTelegramChannel: () => createTelegramChannel,
|
|
28
|
+
createTelegramSender: () => createTelegramSender,
|
|
29
|
+
createTelegramUpdateHandler: () => createTelegramUpdateHandler,
|
|
30
|
+
createTelegramWebhookRoute: () => createTelegramWebhookRoute,
|
|
28
31
|
mentionsBot: () => mentionsBot,
|
|
29
32
|
messageEntities: () => messageEntities,
|
|
30
33
|
messageText: () => messageText,
|
|
@@ -188,6 +191,17 @@ function createTelegramApi(config) {
|
|
|
188
191
|
message_id: messageId,
|
|
189
192
|
reaction: [{ type: "emoji", emoji }]
|
|
190
193
|
});
|
|
194
|
+
},
|
|
195
|
+
async setWebhook(url, options) {
|
|
196
|
+
await call("setWebhook", {
|
|
197
|
+
url,
|
|
198
|
+
// Only what this channel interprets — see the matching comment on getUpdates.
|
|
199
|
+
allowed_updates: ["message"],
|
|
200
|
+
...options?.secretToken ? { secret_token: options.secretToken } : {}
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
async deleteWebhook() {
|
|
204
|
+
await call("deleteWebhook");
|
|
191
205
|
}
|
|
192
206
|
};
|
|
193
207
|
}
|
|
@@ -208,56 +222,6 @@ function createConsoleLogger(level = "info") {
|
|
|
208
222
|
};
|
|
209
223
|
}
|
|
210
224
|
|
|
211
|
-
// src/channels/gates.ts
|
|
212
|
-
function matches(re, text) {
|
|
213
|
-
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
214
|
-
}
|
|
215
|
-
function isBlockedByAllowlist(msg, config) {
|
|
216
|
-
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
217
|
-
}
|
|
218
|
-
function decideChannelAction(msg, config) {
|
|
219
|
-
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
220
|
-
if (!msg.isDirectMessage) {
|
|
221
|
-
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
222
|
-
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
223
|
-
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
224
|
-
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
225
|
-
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
226
|
-
}
|
|
227
|
-
return "reply";
|
|
228
|
-
}
|
|
229
|
-
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
230
|
-
const log = /* @__PURE__ */ new Map();
|
|
231
|
-
let lastSweep = now();
|
|
232
|
-
function sweep(current) {
|
|
233
|
-
if (current - lastSweep < windowMs) return;
|
|
234
|
-
lastSweep = current;
|
|
235
|
-
const cutoff = current - windowMs;
|
|
236
|
-
for (const [key, entries] of log) {
|
|
237
|
-
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function allow(key) {
|
|
241
|
-
const current = now();
|
|
242
|
-
sweep(current);
|
|
243
|
-
const cutoff = current - windowMs;
|
|
244
|
-
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
245
|
-
if (entries.length >= maxPerWindow) {
|
|
246
|
-
if (entries.length > 0) log.set(key, entries);
|
|
247
|
-
else log.delete(key);
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
entries.push(current);
|
|
251
|
-
log.set(key, entries);
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
allow.size = () => log.size;
|
|
255
|
-
return allow;
|
|
256
|
-
}
|
|
257
|
-
function underReplyRateLimit(msg, limiters) {
|
|
258
|
-
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
225
|
// src/core/guardrails.ts
|
|
262
226
|
function detectLeakage(text) {
|
|
263
227
|
const flags = [
|
|
@@ -429,6 +393,125 @@ ${ctx.join("\n\n")}`
|
|
|
429
393
|
return { system, messages, context: ctx };
|
|
430
394
|
}
|
|
431
395
|
|
|
396
|
+
// src/history/compaction.ts
|
|
397
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
398
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
399
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
400
|
+
function defaultSummarize(client, model) {
|
|
401
|
+
return async ({ turns }) => {
|
|
402
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
403
|
+
const { content } = await answerOnce({
|
|
404
|
+
client,
|
|
405
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
406
|
+
messages: [{ role: "user", content: transcript }],
|
|
407
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
408
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
409
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
410
|
+
mode: "public",
|
|
411
|
+
model
|
|
412
|
+
});
|
|
413
|
+
return content;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function createHistoryCompactor(deps, options) {
|
|
417
|
+
const { threshold, keep, logger } = options;
|
|
418
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
419
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
420
|
+
}
|
|
421
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
422
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
423
|
+
}
|
|
424
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
425
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
426
|
+
return {
|
|
427
|
+
async maybeCompact(store, conversationId) {
|
|
428
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
429
|
+
if (turns.length < threshold) return;
|
|
430
|
+
const older = turns.slice(0, turns.length - keep);
|
|
431
|
+
let summary;
|
|
432
|
+
let timer;
|
|
433
|
+
try {
|
|
434
|
+
summary = await Promise.race([
|
|
435
|
+
summarize({ conversationId, turns: older }),
|
|
436
|
+
new Promise((_, reject) => {
|
|
437
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
438
|
+
})
|
|
439
|
+
]);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger?.error(
|
|
442
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
443
|
+
error
|
|
444
|
+
);
|
|
445
|
+
return;
|
|
446
|
+
} finally {
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
}
|
|
449
|
+
const trimmed = summary.trim();
|
|
450
|
+
if (!trimmed) return;
|
|
451
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
452
|
+
const recent = fresh.slice(older.length);
|
|
453
|
+
await store.clear(conversationId);
|
|
454
|
+
await store.append(conversationId, {
|
|
455
|
+
role: "assistant",
|
|
456
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
457
|
+
});
|
|
458
|
+
for (const turn of recent) {
|
|
459
|
+
await store.append(conversationId, turn);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/channels/gates.ts
|
|
466
|
+
function matches(re, text) {
|
|
467
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
468
|
+
}
|
|
469
|
+
function isBlockedByAllowlist(msg, config) {
|
|
470
|
+
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
471
|
+
}
|
|
472
|
+
function decideChannelAction(msg, config) {
|
|
473
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
474
|
+
if (!msg.isDirectMessage) {
|
|
475
|
+
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
476
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
477
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
478
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
479
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
480
|
+
}
|
|
481
|
+
return "reply";
|
|
482
|
+
}
|
|
483
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
484
|
+
const log = /* @__PURE__ */ new Map();
|
|
485
|
+
let lastSweep = now();
|
|
486
|
+
function sweep(current) {
|
|
487
|
+
if (current - lastSweep < windowMs) return;
|
|
488
|
+
lastSweep = current;
|
|
489
|
+
const cutoff = current - windowMs;
|
|
490
|
+
for (const [key, entries] of log) {
|
|
491
|
+
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function allow(key) {
|
|
495
|
+
const current = now();
|
|
496
|
+
sweep(current);
|
|
497
|
+
const cutoff = current - windowMs;
|
|
498
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
499
|
+
if (entries.length >= maxPerWindow) {
|
|
500
|
+
if (entries.length > 0) log.set(key, entries);
|
|
501
|
+
else log.delete(key);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
entries.push(current);
|
|
505
|
+
log.set(key, entries);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
allow.size = () => log.size;
|
|
509
|
+
return allow;
|
|
510
|
+
}
|
|
511
|
+
function underReplyRateLimit(msg, limiters) {
|
|
512
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
513
|
+
}
|
|
514
|
+
|
|
432
515
|
// src/channels/pipeline.ts
|
|
433
516
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
434
517
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -459,6 +542,10 @@ function createInboundPipeline(deps, config) {
|
|
|
459
542
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
460
543
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
461
544
|
const mode = "public";
|
|
545
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
546
|
+
{ client: deps.client },
|
|
547
|
+
{ model: config.model, ...config.history.compaction }
|
|
548
|
+
) : void 0;
|
|
462
549
|
return async function handleInboundMessage(msg, turn) {
|
|
463
550
|
const allowedChats = config.allowedChats ?? [];
|
|
464
551
|
const action = decideChannelAction(msg, {
|
|
@@ -534,6 +621,14 @@ function createInboundPipeline(deps, config) {
|
|
|
534
621
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
535
622
|
}
|
|
536
623
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
624
|
+
const history = config.history;
|
|
625
|
+
if (compactor && historyEnabled && history) {
|
|
626
|
+
try {
|
|
627
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
537
632
|
return { action: "reply", content };
|
|
538
633
|
};
|
|
539
634
|
}
|
|
@@ -581,6 +676,41 @@ function telegramSenderKey(senderId) {
|
|
|
581
676
|
return `tg:${senderId}`;
|
|
582
677
|
}
|
|
583
678
|
|
|
679
|
+
// src/channels/telegram/handler.ts
|
|
680
|
+
function createTelegramUpdateHandler(deps) {
|
|
681
|
+
const { api, me, pipeline, allowedChats, logger, label } = deps;
|
|
682
|
+
const loggedUnallowedChats = /* @__PURE__ */ new Set();
|
|
683
|
+
return async function handleUpdate(update) {
|
|
684
|
+
const message = update.message;
|
|
685
|
+
const msg = toChannelMessage(update, me);
|
|
686
|
+
if (!msg || !message) return;
|
|
687
|
+
if (isBlockedByAllowlist(msg, { allowedChats })) {
|
|
688
|
+
if (!loggedUnallowedChats.has(msg.chatId)) {
|
|
689
|
+
loggedUnallowedChats.add(msg.chatId);
|
|
690
|
+
logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const reply = {
|
|
694
|
+
// Threaded onto the incoming message: in a busy group an untethered
|
|
695
|
+
// answer reads as a non-sequitur.
|
|
696
|
+
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
697
|
+
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
698
|
+
};
|
|
699
|
+
await pipeline(msg, {
|
|
700
|
+
reply,
|
|
701
|
+
conversationId: msg.chatId,
|
|
702
|
+
sender: telegramSenderKey(msg.senderId)
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function createTelegramSender(api) {
|
|
707
|
+
return {
|
|
708
|
+
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
709
|
+
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
710
|
+
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
584
714
|
// src/channels/telegram/poll.ts
|
|
585
715
|
var BASE_DELAY_MS = 1e3;
|
|
586
716
|
var MAX_DELAY_MS = 6e4;
|
|
@@ -651,12 +781,7 @@ function createTelegramChannel(config) {
|
|
|
651
781
|
});
|
|
652
782
|
const me = await api.getMe();
|
|
653
783
|
const label = `Telegram[${me.username ?? me.id}]`;
|
|
654
|
-
|
|
655
|
-
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
656
|
-
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
657
|
-
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
658
|
-
};
|
|
659
|
-
deps.senders.register(channelName, sender);
|
|
784
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
660
785
|
senders = deps.senders;
|
|
661
786
|
registered = true;
|
|
662
787
|
const pipeline = createInboundPipeline(
|
|
@@ -683,29 +808,14 @@ function createTelegramChannel(config) {
|
|
|
683
808
|
logger: log
|
|
684
809
|
}
|
|
685
810
|
);
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
const reply = {
|
|
698
|
-
// Threaded onto the incoming message: in a busy group an untethered
|
|
699
|
-
// answer reads as a non-sequitur.
|
|
700
|
-
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
701
|
-
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
702
|
-
};
|
|
703
|
-
await pipeline(msg, {
|
|
704
|
-
reply,
|
|
705
|
-
conversationId: msg.chatId,
|
|
706
|
-
sender: telegramSenderKey(msg.senderId)
|
|
707
|
-
});
|
|
708
|
-
}
|
|
811
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
812
|
+
api,
|
|
813
|
+
me,
|
|
814
|
+
pipeline,
|
|
815
|
+
allowedChats,
|
|
816
|
+
logger: log,
|
|
817
|
+
label
|
|
818
|
+
});
|
|
709
819
|
abort = new AbortController();
|
|
710
820
|
const signal = abort.signal;
|
|
711
821
|
void runLongPoll({
|
|
@@ -730,6 +840,196 @@ function createTelegramChannel(config) {
|
|
|
730
840
|
}
|
|
731
841
|
};
|
|
732
842
|
}
|
|
843
|
+
|
|
844
|
+
// src/channels/telegram/webhook.ts
|
|
845
|
+
var import_node_crypto = require("node:crypto");
|
|
846
|
+
|
|
847
|
+
// node_modules/hono/dist/http-exception.js
|
|
848
|
+
var HTTPException = class extends Error {
|
|
849
|
+
res;
|
|
850
|
+
status;
|
|
851
|
+
/**
|
|
852
|
+
* Creates an instance of `HTTPException`.
|
|
853
|
+
* @param status - HTTP status code for the exception. Defaults to 500.
|
|
854
|
+
* @param options - Additional options for the exception.
|
|
855
|
+
*/
|
|
856
|
+
constructor(status = 500, options) {
|
|
857
|
+
super(options?.message, { cause: options?.cause });
|
|
858
|
+
this.res = options?.res;
|
|
859
|
+
this.status = status;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Returns the response object associated with the exception.
|
|
863
|
+
* If a response object is not provided, a new response is created with the error message and status code.
|
|
864
|
+
* @returns The response object.
|
|
865
|
+
*/
|
|
866
|
+
getResponse() {
|
|
867
|
+
if (this.res) {
|
|
868
|
+
const newResponse = new Response(this.res.body, {
|
|
869
|
+
status: this.status,
|
|
870
|
+
headers: this.res.headers
|
|
871
|
+
});
|
|
872
|
+
return newResponse;
|
|
873
|
+
}
|
|
874
|
+
return new Response(this.message, {
|
|
875
|
+
status: this.status
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// node_modules/hono/dist/middleware/body-limit/index.js
|
|
881
|
+
var ERROR_MESSAGE = "Payload Too Large";
|
|
882
|
+
var bodyLimit = (options) => {
|
|
883
|
+
const onError = options.onError || (() => {
|
|
884
|
+
const res = new Response(ERROR_MESSAGE, {
|
|
885
|
+
status: 413
|
|
886
|
+
});
|
|
887
|
+
throw new HTTPException(413, { res });
|
|
888
|
+
});
|
|
889
|
+
const maxSize = options.maxSize;
|
|
890
|
+
return async function bodyLimit2(c, next) {
|
|
891
|
+
if (!c.req.raw.body) {
|
|
892
|
+
return next();
|
|
893
|
+
}
|
|
894
|
+
const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
|
|
895
|
+
const hasContentLength = c.req.raw.headers.has("content-length");
|
|
896
|
+
if (hasContentLength && !hasTransferEncoding) {
|
|
897
|
+
const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
|
|
898
|
+
return contentLength > maxSize ? onError(c) : next();
|
|
899
|
+
}
|
|
900
|
+
let size = 0;
|
|
901
|
+
const chunks = [];
|
|
902
|
+
const rawReader = c.req.raw.body.getReader();
|
|
903
|
+
for (; ; ) {
|
|
904
|
+
const { done, value } = await rawReader.read();
|
|
905
|
+
if (done) {
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
size += value.length;
|
|
909
|
+
if (size > maxSize) {
|
|
910
|
+
return onError(c);
|
|
911
|
+
}
|
|
912
|
+
chunks.push(value);
|
|
913
|
+
}
|
|
914
|
+
const requestInit = {
|
|
915
|
+
body: new ReadableStream({
|
|
916
|
+
start(controller) {
|
|
917
|
+
for (const chunk of chunks) {
|
|
918
|
+
controller.enqueue(chunk);
|
|
919
|
+
}
|
|
920
|
+
controller.close();
|
|
921
|
+
}
|
|
922
|
+
}),
|
|
923
|
+
duplex: "half"
|
|
924
|
+
};
|
|
925
|
+
c.req.raw = new Request(c.req.raw, requestInit);
|
|
926
|
+
return next();
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// src/middleware/bodyLimit.ts
|
|
931
|
+
var DEFAULT_MAX_REQUEST_BYTES = 262144;
|
|
932
|
+
function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
|
|
933
|
+
return bodyLimit({
|
|
934
|
+
maxSize,
|
|
935
|
+
onError: (c) => c.json(toError("Request body too large"), 413)
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/channels/telegram/webhook.ts
|
|
940
|
+
var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
|
|
941
|
+
function secureCompare(a, b) {
|
|
942
|
+
const bufA = Buffer.from(a);
|
|
943
|
+
const bufB = Buffer.from(b);
|
|
944
|
+
return bufA.length === bufB.length && (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
|
|
945
|
+
}
|
|
946
|
+
var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
|
|
947
|
+
function createTelegramWebhookRoute(config) {
|
|
948
|
+
const webhookSecret = config.webhookSecret;
|
|
949
|
+
if (!webhookSecret?.trim()) {
|
|
950
|
+
throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
|
|
951
|
+
}
|
|
952
|
+
if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
"Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const channelName = config.name ?? "telegram";
|
|
958
|
+
const allowedChats = config.allowedChats ?? [];
|
|
959
|
+
const path = config.path ?? "/webhooks/telegram";
|
|
960
|
+
return async (app, deps) => {
|
|
961
|
+
const log = config.logger ?? deps.logger ?? createConsoleLogger();
|
|
962
|
+
const api = config.api ?? createTelegramApi({
|
|
963
|
+
botToken: config.botToken,
|
|
964
|
+
fetch: config.fetch,
|
|
965
|
+
baseUrl: config.apiBaseUrl
|
|
966
|
+
});
|
|
967
|
+
const me = await api.getMe();
|
|
968
|
+
const label = `Telegram[${me.username ?? me.id}] (webhook)`;
|
|
969
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
970
|
+
const pipeline = createInboundPipeline(
|
|
971
|
+
{ client: deps.client, store: deps.store, prompts: deps.prompts },
|
|
972
|
+
{
|
|
973
|
+
channel: channelName,
|
|
974
|
+
answerFn: config.answerFn ?? deps.config.answerFn,
|
|
975
|
+
bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
|
|
976
|
+
rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
|
|
977
|
+
rerankContext: config.rerankContext ?? deps.config.rerankContext,
|
|
978
|
+
transformReply: config.transformReply ?? deps.config.transformReply,
|
|
979
|
+
model: config.model,
|
|
980
|
+
channelHint: config.channelHint ?? "Channel: Telegram.",
|
|
981
|
+
personaResolver: config.personaResolver,
|
|
982
|
+
history: config.history,
|
|
983
|
+
allowedChats,
|
|
984
|
+
muteRegex: config.muteRegex,
|
|
985
|
+
unmuteRegex: config.unmuteRegex,
|
|
986
|
+
muteReply: config.muteReply,
|
|
987
|
+
unmuteReply: config.unmuteReply,
|
|
988
|
+
dmRateLimit: config.dmRateLimit,
|
|
989
|
+
groupRateLimit: config.groupRateLimit,
|
|
990
|
+
now: config.now,
|
|
991
|
+
logger: log
|
|
992
|
+
}
|
|
993
|
+
);
|
|
994
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
995
|
+
api,
|
|
996
|
+
me,
|
|
997
|
+
pipeline,
|
|
998
|
+
allowedChats,
|
|
999
|
+
logger: log,
|
|
1000
|
+
label
|
|
1001
|
+
});
|
|
1002
|
+
app.post(
|
|
1003
|
+
path,
|
|
1004
|
+
chatBodyLimit(deps.config.server?.maxRequestBytes),
|
|
1005
|
+
// Reject oversized bodies first
|
|
1006
|
+
async (c) => {
|
|
1007
|
+
const provided = c.req.header(SECRET_TOKEN_HEADER);
|
|
1008
|
+
if (!provided || !secureCompare(provided, webhookSecret)) {
|
|
1009
|
+
log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
|
|
1010
|
+
return c.text("", 403);
|
|
1011
|
+
}
|
|
1012
|
+
let body;
|
|
1013
|
+
try {
|
|
1014
|
+
body = await c.req.json();
|
|
1015
|
+
} catch {
|
|
1016
|
+
return c.text("", 400);
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof body !== "object" || body === null) {
|
|
1019
|
+
return c.text("", 400);
|
|
1020
|
+
}
|
|
1021
|
+
const update = body;
|
|
1022
|
+
try {
|
|
1023
|
+
await handleUpdate(update);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
log.warn(`${label}: update handling failed:`, error);
|
|
1026
|
+
}
|
|
1027
|
+
return c.text("", 200);
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
733
1033
|
// Annotate the CommonJS export names for ESM import in node:
|
|
734
1034
|
0 && (module.exports = {
|
|
735
1035
|
TELEGRAM_API_BASE,
|
|
@@ -737,6 +1037,9 @@ function createTelegramChannel(config) {
|
|
|
737
1037
|
TelegramApiError,
|
|
738
1038
|
createTelegramApi,
|
|
739
1039
|
createTelegramChannel,
|
|
1040
|
+
createTelegramSender,
|
|
1041
|
+
createTelegramUpdateHandler,
|
|
1042
|
+
createTelegramWebhookRoute,
|
|
740
1043
|
mentionsBot,
|
|
741
1044
|
messageEntities,
|
|
742
1045
|
messageText,
|