@diegoaltoworks/chatter 0.58.0 → 0.59.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/telegram/api.d.ts +17 -0
- package/dist/channels/telegram/api.d.ts.map +1 -1
- package/dist/channels/telegram/channel.d.ts +4 -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 +301 -79
- package/dist/channels/telegram/index.mjs +298 -79
- package/dist/channels/telegram/webhook.d.ts +95 -0
- package/dist/channels/telegram/webhook.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -117,6 +117,23 @@ export interface TelegramApi {
|
|
|
117
117
|
}): Promise<void>;
|
|
118
118
|
sendMedia(chatId: string, payload: unknown): Promise<void>;
|
|
119
119
|
setMessageReaction(chatId: string, messageId: number, emoji: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Registers `url` as this bot's webhook, switching Telegram from queuing
|
|
122
|
+
* updates for `getUpdates` to POSTing them there instead. `secretToken`,
|
|
123
|
+
* when given, is echoed back on every webhook POST as
|
|
124
|
+
* `X-Telegram-Bot-Api-Secret-Token` — see `./webhook`'s fail-closed check.
|
|
125
|
+
*/
|
|
126
|
+
setWebhook(url: string, options?: {
|
|
127
|
+
secretToken?: string;
|
|
128
|
+
}): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Clears the registered webhook so `getUpdates` starts working again: a
|
|
131
|
+
* registered webhook suppresses long-polling entirely, so switching a bot
|
|
132
|
+
* from webhook mode back to `createTelegramChannel` needs this called
|
|
133
|
+
* first — the channel itself never calls it, since it has no way to know
|
|
134
|
+
* a webhook was ever registered.
|
|
135
|
+
*/
|
|
136
|
+
deleteWebhook(): Promise<void>;
|
|
120
137
|
}
|
|
121
138
|
export declare function createTelegramApi(config: TelegramApiConfig): TelegramApi;
|
|
122
139
|
//# sourceMappingURL=api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/api.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* per bot token. It is deliberately self-contained — `start(deps)` has
|
|
11
11
|
* everything it needs, so there is no `customRoutes` wiring step (contrast
|
|
12
12
|
* `./channels/whatsapp/inbound.ts`, whose transport and interpretation are
|
|
13
|
-
* separate modules).
|
|
13
|
+
* separate modules). `./webhook` is the alternative transport for hosts that
|
|
14
|
+
* would rather receive updates over HTTPS than long-poll; the two share
|
|
15
|
+
* their update-handling logic via `./handler` so gate behaviour never
|
|
16
|
+
* diverges between them — see docs/telegram.md on choosing a mode.
|
|
14
17
|
*
|
|
15
18
|
* One caveat worth knowing before choosing this over a user-mode client: a bot
|
|
16
19
|
* cannot start a conversation (the user must message it first), and in groups
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC;AAQ5D,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0IAA0I;IAC1I,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iJAAiJ;IACjJ,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,kIAAkI;IAClI,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KACpE,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gHAAgH;IAChH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,kBAAkB;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qJAAqJ;IACrJ,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,uGAAuG;IACvG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAuG5E"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-update handling logic shared by both Telegram transports: the
|
|
3
|
+
* long-poll loop (`./channel`, via `./poll`) and the webhook route
|
|
4
|
+
* (`./webhook`). Kept in one place so addressing, allowlist logging and
|
|
5
|
+
* pipeline dispatch are defined exactly once — a second copy would drift the
|
|
6
|
+
* moment one transport's gate behaviour changed and the other didn't.
|
|
7
|
+
*/
|
|
8
|
+
import type { Logger } from "../../core/logger";
|
|
9
|
+
import type { InboundPipeline } from "../pipeline";
|
|
10
|
+
import type { ChannelSender } from "../senders";
|
|
11
|
+
import type { TelegramApi, TelegramUpdate } from "./api";
|
|
12
|
+
import { type TelegramBotIdentity } from "./updates";
|
|
13
|
+
export interface TelegramUpdateHandlerDeps {
|
|
14
|
+
api: TelegramApi;
|
|
15
|
+
me: TelegramBotIdentity;
|
|
16
|
+
pipeline: InboundPipeline;
|
|
17
|
+
/** Group chats eligible for a reply. Empty = every group. */
|
|
18
|
+
allowedChats: string[];
|
|
19
|
+
logger: Logger;
|
|
20
|
+
/** Log-line prefix, e.g. `Telegram[mybot]`. */
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One update -> one pipeline turn. Builds its own allowlist-log dedup set, so
|
|
25
|
+
* each transport instance gets its own "skipped chat" throttling rather than
|
|
26
|
+
* sharing (or fighting over) state with another.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createTelegramUpdateHandler(deps: TelegramUpdateHandlerDeps): (update: TelegramUpdate) => Promise<void>;
|
|
29
|
+
/** The `ChannelSender` every Telegram transport registers — text, media and reactions over the same `TelegramApi`, regardless of how updates arrive. */
|
|
30
|
+
export declare function createTelegramSender(api: TelegramApi): ChannelSender;
|
|
31
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,KAAK,mBAAmB,EAAuC,MAAM,WAAW,CAAC;AAE1F,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,WAAW,CAAC;IACjB,EAAE,EAAE,mBAAmB,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,yBAAyB,GAC9B,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CA8B3C;AAED,wJAAwJ;AACxJ,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAOpE"}
|
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
* docs/telegram.md for configuration and docs/build-a-channel.md for the SPI
|
|
18
18
|
* this implements.
|
|
19
19
|
*
|
|
20
|
+
* `createTelegramWebhookRoute` is the alternative to `createTelegramChannel`:
|
|
21
|
+
* updates over an HTTPS webhook instead of long-polling, mounted via
|
|
22
|
+
* `ChatterConfig.customRoutes` — see docs/telegram.md for choosing between
|
|
23
|
+
* the two.
|
|
24
|
+
*
|
|
20
25
|
* @packageDocumentation
|
|
21
26
|
*/
|
|
22
27
|
export { createTelegramApi, redactToken, splitTelegramText, TELEGRAM_API_BASE, TELEGRAM_TEXT_LIMIT, type TelegramApi, type TelegramApiConfig, TelegramApiError, type TelegramChat, type TelegramMediaPayload, type TelegramMessage, type TelegramMessageEntity, type TelegramUpdate, type TelegramUser, toTelegramMediaRequest, } from "./api";
|
|
23
28
|
export { createTelegramChannel, type TelegramChannelConfig } from "./channel";
|
|
29
|
+
export { createTelegramSender, createTelegramUpdateHandler, type TelegramUpdateHandlerDeps, } from "./handler";
|
|
24
30
|
export { type LongPollDeps, pollBackoffMs, retryDelayMs, runLongPoll } from "./poll";
|
|
25
31
|
export { mentionsBot, messageEntities, messageText, nextOffset, type TelegramBotIdentity, telegramSenderKey, toChannelMessage, } from "./updates";
|
|
32
|
+
export { createTelegramWebhookRoute, type TelegramWebhookConfig } from "./webhook";
|
|
26
33
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,sBAAsB,GACvB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,qBAAqB,EAAE,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,KAAK,yBAAyB,GAC/B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrF,OAAO,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,UAAU,EACV,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,0BAA0B,EAAE,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -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,56 @@ ${ctx.join("\n\n")}`
|
|
|
429
393
|
return { system, messages, context: ctx };
|
|
430
394
|
}
|
|
431
395
|
|
|
396
|
+
// src/channels/gates.ts
|
|
397
|
+
function matches(re, text) {
|
|
398
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
399
|
+
}
|
|
400
|
+
function isBlockedByAllowlist(msg, config) {
|
|
401
|
+
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
402
|
+
}
|
|
403
|
+
function decideChannelAction(msg, config) {
|
|
404
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
405
|
+
if (!msg.isDirectMessage) {
|
|
406
|
+
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
407
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
408
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
409
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
410
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
411
|
+
}
|
|
412
|
+
return "reply";
|
|
413
|
+
}
|
|
414
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
415
|
+
const log = /* @__PURE__ */ new Map();
|
|
416
|
+
let lastSweep = now();
|
|
417
|
+
function sweep(current) {
|
|
418
|
+
if (current - lastSweep < windowMs) return;
|
|
419
|
+
lastSweep = current;
|
|
420
|
+
const cutoff = current - windowMs;
|
|
421
|
+
for (const [key, entries] of log) {
|
|
422
|
+
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function allow(key) {
|
|
426
|
+
const current = now();
|
|
427
|
+
sweep(current);
|
|
428
|
+
const cutoff = current - windowMs;
|
|
429
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
430
|
+
if (entries.length >= maxPerWindow) {
|
|
431
|
+
if (entries.length > 0) log.set(key, entries);
|
|
432
|
+
else log.delete(key);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
entries.push(current);
|
|
436
|
+
log.set(key, entries);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
allow.size = () => log.size;
|
|
440
|
+
return allow;
|
|
441
|
+
}
|
|
442
|
+
function underReplyRateLimit(msg, limiters) {
|
|
443
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
444
|
+
}
|
|
445
|
+
|
|
432
446
|
// src/channels/pipeline.ts
|
|
433
447
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
434
448
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -581,6 +595,41 @@ function telegramSenderKey(senderId) {
|
|
|
581
595
|
return `tg:${senderId}`;
|
|
582
596
|
}
|
|
583
597
|
|
|
598
|
+
// src/channels/telegram/handler.ts
|
|
599
|
+
function createTelegramUpdateHandler(deps) {
|
|
600
|
+
const { api, me, pipeline, allowedChats, logger, label } = deps;
|
|
601
|
+
const loggedUnallowedChats = /* @__PURE__ */ new Set();
|
|
602
|
+
return async function handleUpdate(update) {
|
|
603
|
+
const message = update.message;
|
|
604
|
+
const msg = toChannelMessage(update, me);
|
|
605
|
+
if (!msg || !message) return;
|
|
606
|
+
if (isBlockedByAllowlist(msg, { allowedChats })) {
|
|
607
|
+
if (!loggedUnallowedChats.has(msg.chatId)) {
|
|
608
|
+
loggedUnallowedChats.add(msg.chatId);
|
|
609
|
+
logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const reply = {
|
|
613
|
+
// Threaded onto the incoming message: in a busy group an untethered
|
|
614
|
+
// answer reads as a non-sequitur.
|
|
615
|
+
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
616
|
+
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
617
|
+
};
|
|
618
|
+
await pipeline(msg, {
|
|
619
|
+
reply,
|
|
620
|
+
conversationId: msg.chatId,
|
|
621
|
+
sender: telegramSenderKey(msg.senderId)
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function createTelegramSender(api) {
|
|
626
|
+
return {
|
|
627
|
+
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
628
|
+
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
629
|
+
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
584
633
|
// src/channels/telegram/poll.ts
|
|
585
634
|
var BASE_DELAY_MS = 1e3;
|
|
586
635
|
var MAX_DELAY_MS = 6e4;
|
|
@@ -651,12 +700,7 @@ function createTelegramChannel(config) {
|
|
|
651
700
|
});
|
|
652
701
|
const me = await api.getMe();
|
|
653
702
|
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);
|
|
703
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
660
704
|
senders = deps.senders;
|
|
661
705
|
registered = true;
|
|
662
706
|
const pipeline = createInboundPipeline(
|
|
@@ -683,29 +727,14 @@ function createTelegramChannel(config) {
|
|
|
683
727
|
logger: log
|
|
684
728
|
}
|
|
685
729
|
);
|
|
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
|
-
}
|
|
730
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
731
|
+
api,
|
|
732
|
+
me,
|
|
733
|
+
pipeline,
|
|
734
|
+
allowedChats,
|
|
735
|
+
logger: log,
|
|
736
|
+
label
|
|
737
|
+
});
|
|
709
738
|
abort = new AbortController();
|
|
710
739
|
const signal = abort.signal;
|
|
711
740
|
void runLongPoll({
|
|
@@ -730,6 +759,196 @@ function createTelegramChannel(config) {
|
|
|
730
759
|
}
|
|
731
760
|
};
|
|
732
761
|
}
|
|
762
|
+
|
|
763
|
+
// src/channels/telegram/webhook.ts
|
|
764
|
+
var import_node_crypto = require("node:crypto");
|
|
765
|
+
|
|
766
|
+
// node_modules/hono/dist/http-exception.js
|
|
767
|
+
var HTTPException = class extends Error {
|
|
768
|
+
res;
|
|
769
|
+
status;
|
|
770
|
+
/**
|
|
771
|
+
* Creates an instance of `HTTPException`.
|
|
772
|
+
* @param status - HTTP status code for the exception. Defaults to 500.
|
|
773
|
+
* @param options - Additional options for the exception.
|
|
774
|
+
*/
|
|
775
|
+
constructor(status = 500, options) {
|
|
776
|
+
super(options?.message, { cause: options?.cause });
|
|
777
|
+
this.res = options?.res;
|
|
778
|
+
this.status = status;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Returns the response object associated with the exception.
|
|
782
|
+
* If a response object is not provided, a new response is created with the error message and status code.
|
|
783
|
+
* @returns The response object.
|
|
784
|
+
*/
|
|
785
|
+
getResponse() {
|
|
786
|
+
if (this.res) {
|
|
787
|
+
const newResponse = new Response(this.res.body, {
|
|
788
|
+
status: this.status,
|
|
789
|
+
headers: this.res.headers
|
|
790
|
+
});
|
|
791
|
+
return newResponse;
|
|
792
|
+
}
|
|
793
|
+
return new Response(this.message, {
|
|
794
|
+
status: this.status
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// node_modules/hono/dist/middleware/body-limit/index.js
|
|
800
|
+
var ERROR_MESSAGE = "Payload Too Large";
|
|
801
|
+
var bodyLimit = (options) => {
|
|
802
|
+
const onError = options.onError || (() => {
|
|
803
|
+
const res = new Response(ERROR_MESSAGE, {
|
|
804
|
+
status: 413
|
|
805
|
+
});
|
|
806
|
+
throw new HTTPException(413, { res });
|
|
807
|
+
});
|
|
808
|
+
const maxSize = options.maxSize;
|
|
809
|
+
return async function bodyLimit2(c, next) {
|
|
810
|
+
if (!c.req.raw.body) {
|
|
811
|
+
return next();
|
|
812
|
+
}
|
|
813
|
+
const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
|
|
814
|
+
const hasContentLength = c.req.raw.headers.has("content-length");
|
|
815
|
+
if (hasContentLength && !hasTransferEncoding) {
|
|
816
|
+
const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
|
|
817
|
+
return contentLength > maxSize ? onError(c) : next();
|
|
818
|
+
}
|
|
819
|
+
let size = 0;
|
|
820
|
+
const chunks = [];
|
|
821
|
+
const rawReader = c.req.raw.body.getReader();
|
|
822
|
+
for (; ; ) {
|
|
823
|
+
const { done, value } = await rawReader.read();
|
|
824
|
+
if (done) {
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
size += value.length;
|
|
828
|
+
if (size > maxSize) {
|
|
829
|
+
return onError(c);
|
|
830
|
+
}
|
|
831
|
+
chunks.push(value);
|
|
832
|
+
}
|
|
833
|
+
const requestInit = {
|
|
834
|
+
body: new ReadableStream({
|
|
835
|
+
start(controller) {
|
|
836
|
+
for (const chunk of chunks) {
|
|
837
|
+
controller.enqueue(chunk);
|
|
838
|
+
}
|
|
839
|
+
controller.close();
|
|
840
|
+
}
|
|
841
|
+
}),
|
|
842
|
+
duplex: "half"
|
|
843
|
+
};
|
|
844
|
+
c.req.raw = new Request(c.req.raw, requestInit);
|
|
845
|
+
return next();
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/middleware/bodyLimit.ts
|
|
850
|
+
var DEFAULT_MAX_REQUEST_BYTES = 262144;
|
|
851
|
+
function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
|
|
852
|
+
return bodyLimit({
|
|
853
|
+
maxSize,
|
|
854
|
+
onError: (c) => c.json(toError("Request body too large"), 413)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/channels/telegram/webhook.ts
|
|
859
|
+
var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
|
|
860
|
+
function secureCompare(a, b) {
|
|
861
|
+
const bufA = Buffer.from(a);
|
|
862
|
+
const bufB = Buffer.from(b);
|
|
863
|
+
return bufA.length === bufB.length && (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
|
|
864
|
+
}
|
|
865
|
+
var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
|
|
866
|
+
function createTelegramWebhookRoute(config) {
|
|
867
|
+
const webhookSecret = config.webhookSecret;
|
|
868
|
+
if (!webhookSecret?.trim()) {
|
|
869
|
+
throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
|
|
870
|
+
}
|
|
871
|
+
if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
"Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const channelName = config.name ?? "telegram";
|
|
877
|
+
const allowedChats = config.allowedChats ?? [];
|
|
878
|
+
const path = config.path ?? "/webhooks/telegram";
|
|
879
|
+
return async (app, deps) => {
|
|
880
|
+
const log = config.logger ?? deps.logger ?? createConsoleLogger();
|
|
881
|
+
const api = config.api ?? createTelegramApi({
|
|
882
|
+
botToken: config.botToken,
|
|
883
|
+
fetch: config.fetch,
|
|
884
|
+
baseUrl: config.apiBaseUrl
|
|
885
|
+
});
|
|
886
|
+
const me = await api.getMe();
|
|
887
|
+
const label = `Telegram[${me.username ?? me.id}] (webhook)`;
|
|
888
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
889
|
+
const pipeline = createInboundPipeline(
|
|
890
|
+
{ client: deps.client, store: deps.store, prompts: deps.prompts },
|
|
891
|
+
{
|
|
892
|
+
channel: channelName,
|
|
893
|
+
answerFn: config.answerFn ?? deps.config.answerFn,
|
|
894
|
+
bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
|
|
895
|
+
rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
|
|
896
|
+
rerankContext: config.rerankContext ?? deps.config.rerankContext,
|
|
897
|
+
transformReply: config.transformReply ?? deps.config.transformReply,
|
|
898
|
+
model: config.model,
|
|
899
|
+
channelHint: config.channelHint ?? "Channel: Telegram.",
|
|
900
|
+
personaResolver: config.personaResolver,
|
|
901
|
+
history: config.history,
|
|
902
|
+
allowedChats,
|
|
903
|
+
muteRegex: config.muteRegex,
|
|
904
|
+
unmuteRegex: config.unmuteRegex,
|
|
905
|
+
muteReply: config.muteReply,
|
|
906
|
+
unmuteReply: config.unmuteReply,
|
|
907
|
+
dmRateLimit: config.dmRateLimit,
|
|
908
|
+
groupRateLimit: config.groupRateLimit,
|
|
909
|
+
now: config.now,
|
|
910
|
+
logger: log
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
914
|
+
api,
|
|
915
|
+
me,
|
|
916
|
+
pipeline,
|
|
917
|
+
allowedChats,
|
|
918
|
+
logger: log,
|
|
919
|
+
label
|
|
920
|
+
});
|
|
921
|
+
app.post(
|
|
922
|
+
path,
|
|
923
|
+
chatBodyLimit(deps.config.server?.maxRequestBytes),
|
|
924
|
+
// Reject oversized bodies first
|
|
925
|
+
async (c) => {
|
|
926
|
+
const provided = c.req.header(SECRET_TOKEN_HEADER);
|
|
927
|
+
if (!provided || !secureCompare(provided, webhookSecret)) {
|
|
928
|
+
log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
|
|
929
|
+
return c.text("", 403);
|
|
930
|
+
}
|
|
931
|
+
let body;
|
|
932
|
+
try {
|
|
933
|
+
body = await c.req.json();
|
|
934
|
+
} catch {
|
|
935
|
+
return c.text("", 400);
|
|
936
|
+
}
|
|
937
|
+
if (typeof body !== "object" || body === null) {
|
|
938
|
+
return c.text("", 400);
|
|
939
|
+
}
|
|
940
|
+
const update = body;
|
|
941
|
+
try {
|
|
942
|
+
await handleUpdate(update);
|
|
943
|
+
} catch (error) {
|
|
944
|
+
log.warn(`${label}: update handling failed:`, error);
|
|
945
|
+
}
|
|
946
|
+
return c.text("", 200);
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
|
|
950
|
+
};
|
|
951
|
+
}
|
|
733
952
|
// Annotate the CommonJS export names for ESM import in node:
|
|
734
953
|
0 && (module.exports = {
|
|
735
954
|
TELEGRAM_API_BASE,
|
|
@@ -737,6 +956,9 @@ function createTelegramChannel(config) {
|
|
|
737
956
|
TelegramApiError,
|
|
738
957
|
createTelegramApi,
|
|
739
958
|
createTelegramChannel,
|
|
959
|
+
createTelegramSender,
|
|
960
|
+
createTelegramUpdateHandler,
|
|
961
|
+
createTelegramWebhookRoute,
|
|
740
962
|
mentionsBot,
|
|
741
963
|
messageEntities,
|
|
742
964
|
messageText,
|
|
@@ -146,6 +146,17 @@ function createTelegramApi(config) {
|
|
|
146
146
|
message_id: messageId,
|
|
147
147
|
reaction: [{ type: "emoji", emoji }]
|
|
148
148
|
});
|
|
149
|
+
},
|
|
150
|
+
async setWebhook(url, options) {
|
|
151
|
+
await call("setWebhook", {
|
|
152
|
+
url,
|
|
153
|
+
// Only what this channel interprets — see the matching comment on getUpdates.
|
|
154
|
+
allowed_updates: ["message"],
|
|
155
|
+
...options?.secretToken ? { secret_token: options.secretToken } : {}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
async deleteWebhook() {
|
|
159
|
+
await call("deleteWebhook");
|
|
149
160
|
}
|
|
150
161
|
};
|
|
151
162
|
}
|
|
@@ -166,56 +177,6 @@ function createConsoleLogger(level = "info") {
|
|
|
166
177
|
};
|
|
167
178
|
}
|
|
168
179
|
|
|
169
|
-
// src/channels/gates.ts
|
|
170
|
-
function matches(re, text) {
|
|
171
|
-
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
172
|
-
}
|
|
173
|
-
function isBlockedByAllowlist(msg, config) {
|
|
174
|
-
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
175
|
-
}
|
|
176
|
-
function decideChannelAction(msg, config) {
|
|
177
|
-
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
178
|
-
if (!msg.isDirectMessage) {
|
|
179
|
-
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
180
|
-
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
181
|
-
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
182
|
-
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
183
|
-
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
184
|
-
}
|
|
185
|
-
return "reply";
|
|
186
|
-
}
|
|
187
|
-
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
188
|
-
const log = /* @__PURE__ */ new Map();
|
|
189
|
-
let lastSweep = now();
|
|
190
|
-
function sweep(current) {
|
|
191
|
-
if (current - lastSweep < windowMs) return;
|
|
192
|
-
lastSweep = current;
|
|
193
|
-
const cutoff = current - windowMs;
|
|
194
|
-
for (const [key, entries] of log) {
|
|
195
|
-
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
function allow(key) {
|
|
199
|
-
const current = now();
|
|
200
|
-
sweep(current);
|
|
201
|
-
const cutoff = current - windowMs;
|
|
202
|
-
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
203
|
-
if (entries.length >= maxPerWindow) {
|
|
204
|
-
if (entries.length > 0) log.set(key, entries);
|
|
205
|
-
else log.delete(key);
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
entries.push(current);
|
|
209
|
-
log.set(key, entries);
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
allow.size = () => log.size;
|
|
213
|
-
return allow;
|
|
214
|
-
}
|
|
215
|
-
function underReplyRateLimit(msg, limiters) {
|
|
216
|
-
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
180
|
// src/core/guardrails.ts
|
|
220
181
|
function detectLeakage(text) {
|
|
221
182
|
const flags = [
|
|
@@ -387,6 +348,56 @@ ${ctx.join("\n\n")}`
|
|
|
387
348
|
return { system, messages, context: ctx };
|
|
388
349
|
}
|
|
389
350
|
|
|
351
|
+
// src/channels/gates.ts
|
|
352
|
+
function matches(re, text) {
|
|
353
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
354
|
+
}
|
|
355
|
+
function isBlockedByAllowlist(msg, config) {
|
|
356
|
+
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
357
|
+
}
|
|
358
|
+
function decideChannelAction(msg, config) {
|
|
359
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
360
|
+
if (!msg.isDirectMessage) {
|
|
361
|
+
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
362
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
363
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
364
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
365
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
366
|
+
}
|
|
367
|
+
return "reply";
|
|
368
|
+
}
|
|
369
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
370
|
+
const log = /* @__PURE__ */ new Map();
|
|
371
|
+
let lastSweep = now();
|
|
372
|
+
function sweep(current) {
|
|
373
|
+
if (current - lastSweep < windowMs) return;
|
|
374
|
+
lastSweep = current;
|
|
375
|
+
const cutoff = current - windowMs;
|
|
376
|
+
for (const [key, entries] of log) {
|
|
377
|
+
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function allow(key) {
|
|
381
|
+
const current = now();
|
|
382
|
+
sweep(current);
|
|
383
|
+
const cutoff = current - windowMs;
|
|
384
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
385
|
+
if (entries.length >= maxPerWindow) {
|
|
386
|
+
if (entries.length > 0) log.set(key, entries);
|
|
387
|
+
else log.delete(key);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
entries.push(current);
|
|
391
|
+
log.set(key, entries);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
allow.size = () => log.size;
|
|
395
|
+
return allow;
|
|
396
|
+
}
|
|
397
|
+
function underReplyRateLimit(msg, limiters) {
|
|
398
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
399
|
+
}
|
|
400
|
+
|
|
390
401
|
// src/channels/pipeline.ts
|
|
391
402
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
392
403
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -539,6 +550,41 @@ function telegramSenderKey(senderId) {
|
|
|
539
550
|
return `tg:${senderId}`;
|
|
540
551
|
}
|
|
541
552
|
|
|
553
|
+
// src/channels/telegram/handler.ts
|
|
554
|
+
function createTelegramUpdateHandler(deps) {
|
|
555
|
+
const { api, me, pipeline, allowedChats, logger, label } = deps;
|
|
556
|
+
const loggedUnallowedChats = /* @__PURE__ */ new Set();
|
|
557
|
+
return async function handleUpdate(update) {
|
|
558
|
+
const message = update.message;
|
|
559
|
+
const msg = toChannelMessage(update, me);
|
|
560
|
+
if (!msg || !message) return;
|
|
561
|
+
if (isBlockedByAllowlist(msg, { allowedChats })) {
|
|
562
|
+
if (!loggedUnallowedChats.has(msg.chatId)) {
|
|
563
|
+
loggedUnallowedChats.add(msg.chatId);
|
|
564
|
+
logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const reply = {
|
|
568
|
+
// Threaded onto the incoming message: in a busy group an untethered
|
|
569
|
+
// answer reads as a non-sequitur.
|
|
570
|
+
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
571
|
+
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
572
|
+
};
|
|
573
|
+
await pipeline(msg, {
|
|
574
|
+
reply,
|
|
575
|
+
conversationId: msg.chatId,
|
|
576
|
+
sender: telegramSenderKey(msg.senderId)
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function createTelegramSender(api) {
|
|
581
|
+
return {
|
|
582
|
+
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
583
|
+
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
584
|
+
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
542
588
|
// src/channels/telegram/poll.ts
|
|
543
589
|
var BASE_DELAY_MS = 1e3;
|
|
544
590
|
var MAX_DELAY_MS = 6e4;
|
|
@@ -609,12 +655,7 @@ function createTelegramChannel(config) {
|
|
|
609
655
|
});
|
|
610
656
|
const me = await api.getMe();
|
|
611
657
|
const label = `Telegram[${me.username ?? me.id}]`;
|
|
612
|
-
|
|
613
|
-
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
614
|
-
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
615
|
-
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
616
|
-
};
|
|
617
|
-
deps.senders.register(channelName, sender);
|
|
658
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
618
659
|
senders = deps.senders;
|
|
619
660
|
registered = true;
|
|
620
661
|
const pipeline = createInboundPipeline(
|
|
@@ -641,29 +682,14 @@ function createTelegramChannel(config) {
|
|
|
641
682
|
logger: log
|
|
642
683
|
}
|
|
643
684
|
);
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
const reply = {
|
|
656
|
-
// Threaded onto the incoming message: in a busy group an untethered
|
|
657
|
-
// answer reads as a non-sequitur.
|
|
658
|
-
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
659
|
-
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
660
|
-
};
|
|
661
|
-
await pipeline(msg, {
|
|
662
|
-
reply,
|
|
663
|
-
conversationId: msg.chatId,
|
|
664
|
-
sender: telegramSenderKey(msg.senderId)
|
|
665
|
-
});
|
|
666
|
-
}
|
|
685
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
686
|
+
api,
|
|
687
|
+
me,
|
|
688
|
+
pipeline,
|
|
689
|
+
allowedChats,
|
|
690
|
+
logger: log,
|
|
691
|
+
label
|
|
692
|
+
});
|
|
667
693
|
abort = new AbortController();
|
|
668
694
|
const signal = abort.signal;
|
|
669
695
|
void runLongPoll({
|
|
@@ -688,12 +714,205 @@ function createTelegramChannel(config) {
|
|
|
688
714
|
}
|
|
689
715
|
};
|
|
690
716
|
}
|
|
717
|
+
|
|
718
|
+
// src/channels/telegram/webhook.ts
|
|
719
|
+
import { timingSafeEqual } from "node:crypto";
|
|
720
|
+
|
|
721
|
+
// node_modules/hono/dist/http-exception.js
|
|
722
|
+
var HTTPException = class extends Error {
|
|
723
|
+
res;
|
|
724
|
+
status;
|
|
725
|
+
/**
|
|
726
|
+
* Creates an instance of `HTTPException`.
|
|
727
|
+
* @param status - HTTP status code for the exception. Defaults to 500.
|
|
728
|
+
* @param options - Additional options for the exception.
|
|
729
|
+
*/
|
|
730
|
+
constructor(status = 500, options) {
|
|
731
|
+
super(options?.message, { cause: options?.cause });
|
|
732
|
+
this.res = options?.res;
|
|
733
|
+
this.status = status;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Returns the response object associated with the exception.
|
|
737
|
+
* If a response object is not provided, a new response is created with the error message and status code.
|
|
738
|
+
* @returns The response object.
|
|
739
|
+
*/
|
|
740
|
+
getResponse() {
|
|
741
|
+
if (this.res) {
|
|
742
|
+
const newResponse = new Response(this.res.body, {
|
|
743
|
+
status: this.status,
|
|
744
|
+
headers: this.res.headers
|
|
745
|
+
});
|
|
746
|
+
return newResponse;
|
|
747
|
+
}
|
|
748
|
+
return new Response(this.message, {
|
|
749
|
+
status: this.status
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// node_modules/hono/dist/middleware/body-limit/index.js
|
|
755
|
+
var ERROR_MESSAGE = "Payload Too Large";
|
|
756
|
+
var bodyLimit = (options) => {
|
|
757
|
+
const onError = options.onError || (() => {
|
|
758
|
+
const res = new Response(ERROR_MESSAGE, {
|
|
759
|
+
status: 413
|
|
760
|
+
});
|
|
761
|
+
throw new HTTPException(413, { res });
|
|
762
|
+
});
|
|
763
|
+
const maxSize = options.maxSize;
|
|
764
|
+
return async function bodyLimit2(c, next) {
|
|
765
|
+
if (!c.req.raw.body) {
|
|
766
|
+
return next();
|
|
767
|
+
}
|
|
768
|
+
const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
|
|
769
|
+
const hasContentLength = c.req.raw.headers.has("content-length");
|
|
770
|
+
if (hasContentLength && !hasTransferEncoding) {
|
|
771
|
+
const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
|
|
772
|
+
return contentLength > maxSize ? onError(c) : next();
|
|
773
|
+
}
|
|
774
|
+
let size = 0;
|
|
775
|
+
const chunks = [];
|
|
776
|
+
const rawReader = c.req.raw.body.getReader();
|
|
777
|
+
for (; ; ) {
|
|
778
|
+
const { done, value } = await rawReader.read();
|
|
779
|
+
if (done) {
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
size += value.length;
|
|
783
|
+
if (size > maxSize) {
|
|
784
|
+
return onError(c);
|
|
785
|
+
}
|
|
786
|
+
chunks.push(value);
|
|
787
|
+
}
|
|
788
|
+
const requestInit = {
|
|
789
|
+
body: new ReadableStream({
|
|
790
|
+
start(controller) {
|
|
791
|
+
for (const chunk of chunks) {
|
|
792
|
+
controller.enqueue(chunk);
|
|
793
|
+
}
|
|
794
|
+
controller.close();
|
|
795
|
+
}
|
|
796
|
+
}),
|
|
797
|
+
duplex: "half"
|
|
798
|
+
};
|
|
799
|
+
c.req.raw = new Request(c.req.raw, requestInit);
|
|
800
|
+
return next();
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/middleware/bodyLimit.ts
|
|
805
|
+
var DEFAULT_MAX_REQUEST_BYTES = 262144;
|
|
806
|
+
function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
|
|
807
|
+
return bodyLimit({
|
|
808
|
+
maxSize,
|
|
809
|
+
onError: (c) => c.json(toError("Request body too large"), 413)
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/channels/telegram/webhook.ts
|
|
814
|
+
var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
|
|
815
|
+
function secureCompare(a, b) {
|
|
816
|
+
const bufA = Buffer.from(a);
|
|
817
|
+
const bufB = Buffer.from(b);
|
|
818
|
+
return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
|
|
819
|
+
}
|
|
820
|
+
var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
|
|
821
|
+
function createTelegramWebhookRoute(config) {
|
|
822
|
+
const webhookSecret = config.webhookSecret;
|
|
823
|
+
if (!webhookSecret?.trim()) {
|
|
824
|
+
throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
|
|
825
|
+
}
|
|
826
|
+
if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
"Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const channelName = config.name ?? "telegram";
|
|
832
|
+
const allowedChats = config.allowedChats ?? [];
|
|
833
|
+
const path = config.path ?? "/webhooks/telegram";
|
|
834
|
+
return async (app, deps) => {
|
|
835
|
+
const log = config.logger ?? deps.logger ?? createConsoleLogger();
|
|
836
|
+
const api = config.api ?? createTelegramApi({
|
|
837
|
+
botToken: config.botToken,
|
|
838
|
+
fetch: config.fetch,
|
|
839
|
+
baseUrl: config.apiBaseUrl
|
|
840
|
+
});
|
|
841
|
+
const me = await api.getMe();
|
|
842
|
+
const label = `Telegram[${me.username ?? me.id}] (webhook)`;
|
|
843
|
+
deps.senders.register(channelName, createTelegramSender(api));
|
|
844
|
+
const pipeline = createInboundPipeline(
|
|
845
|
+
{ client: deps.client, store: deps.store, prompts: deps.prompts },
|
|
846
|
+
{
|
|
847
|
+
channel: channelName,
|
|
848
|
+
answerFn: config.answerFn ?? deps.config.answerFn,
|
|
849
|
+
bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
|
|
850
|
+
rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
|
|
851
|
+
rerankContext: config.rerankContext ?? deps.config.rerankContext,
|
|
852
|
+
transformReply: config.transformReply ?? deps.config.transformReply,
|
|
853
|
+
model: config.model,
|
|
854
|
+
channelHint: config.channelHint ?? "Channel: Telegram.",
|
|
855
|
+
personaResolver: config.personaResolver,
|
|
856
|
+
history: config.history,
|
|
857
|
+
allowedChats,
|
|
858
|
+
muteRegex: config.muteRegex,
|
|
859
|
+
unmuteRegex: config.unmuteRegex,
|
|
860
|
+
muteReply: config.muteReply,
|
|
861
|
+
unmuteReply: config.unmuteReply,
|
|
862
|
+
dmRateLimit: config.dmRateLimit,
|
|
863
|
+
groupRateLimit: config.groupRateLimit,
|
|
864
|
+
now: config.now,
|
|
865
|
+
logger: log
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
const handleUpdate = createTelegramUpdateHandler({
|
|
869
|
+
api,
|
|
870
|
+
me,
|
|
871
|
+
pipeline,
|
|
872
|
+
allowedChats,
|
|
873
|
+
logger: log,
|
|
874
|
+
label
|
|
875
|
+
});
|
|
876
|
+
app.post(
|
|
877
|
+
path,
|
|
878
|
+
chatBodyLimit(deps.config.server?.maxRequestBytes),
|
|
879
|
+
// Reject oversized bodies first
|
|
880
|
+
async (c) => {
|
|
881
|
+
const provided = c.req.header(SECRET_TOKEN_HEADER);
|
|
882
|
+
if (!provided || !secureCompare(provided, webhookSecret)) {
|
|
883
|
+
log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
|
|
884
|
+
return c.text("", 403);
|
|
885
|
+
}
|
|
886
|
+
let body;
|
|
887
|
+
try {
|
|
888
|
+
body = await c.req.json();
|
|
889
|
+
} catch {
|
|
890
|
+
return c.text("", 400);
|
|
891
|
+
}
|
|
892
|
+
if (typeof body !== "object" || body === null) {
|
|
893
|
+
return c.text("", 400);
|
|
894
|
+
}
|
|
895
|
+
const update = body;
|
|
896
|
+
try {
|
|
897
|
+
await handleUpdate(update);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
log.warn(`${label}: update handling failed:`, error);
|
|
900
|
+
}
|
|
901
|
+
return c.text("", 200);
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
|
|
905
|
+
};
|
|
906
|
+
}
|
|
691
907
|
export {
|
|
692
908
|
TELEGRAM_API_BASE,
|
|
693
909
|
TELEGRAM_TEXT_LIMIT,
|
|
694
910
|
TelegramApiError,
|
|
695
911
|
createTelegramApi,
|
|
696
912
|
createTelegramChannel,
|
|
913
|
+
createTelegramSender,
|
|
914
|
+
createTelegramUpdateHandler,
|
|
915
|
+
createTelegramWebhookRoute,
|
|
697
916
|
mentionsBot,
|
|
698
917
|
messageEntities,
|
|
699
918
|
messageText,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Bot API transport: a `CustomRoutes` mount that receives updates
|
|
3
|
+
* over HTTPS instead of long-polling `getUpdates` (see `./channel` for that
|
|
4
|
+
* transport). Update handling is identical between the two — both build on
|
|
5
|
+
* `./handler`'s shared `createTelegramUpdateHandler`/`createTelegramSender` —
|
|
6
|
+
* so choosing one over the other is purely an infrastructure decision: a
|
|
7
|
+
* webhook needs a publicly reachable HTTPS endpoint and no open connection,
|
|
8
|
+
* long-polling needs neither but holds one request open at a time. See
|
|
9
|
+
* docs/telegram.md for the full comparison and `TelegramApi.setWebhook` for
|
|
10
|
+
* registering the URL with Telegram once this route is mounted.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* await createServer({
|
|
14
|
+
* ...,
|
|
15
|
+
* customRoutes: createTelegramWebhookRoute({
|
|
16
|
+
* botToken: process.env.TELEGRAM_BOT_TOKEN as string,
|
|
17
|
+
* webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET as string,
|
|
18
|
+
* }),
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import type { AnswerFn, TransformReply } from "../../core/answer";
|
|
23
|
+
import type { BucketsFor } from "../../core/buckets";
|
|
24
|
+
import { type Logger } from "../../core/logger";
|
|
25
|
+
import type { RerankContext, RewriteQuery } from "../../core/pipeline";
|
|
26
|
+
import type { HistoryStore } from "../../history/types";
|
|
27
|
+
import type { CustomRoutes } from "../../types";
|
|
28
|
+
import { type TelegramApi } from "./api";
|
|
29
|
+
export interface TelegramWebhookConfig {
|
|
30
|
+
/** From @BotFather. A credential — pass it from the environment, never commit it. */
|
|
31
|
+
botToken: string;
|
|
32
|
+
/**
|
|
33
|
+
* Shared secret Telegram is told about via `setWebhook({ secretToken })`
|
|
34
|
+
* and then echoes back on every POST as `X-Telegram-Bot-Api-Secret-Token`.
|
|
35
|
+
* Required: without one there is no way to tell a genuine Telegram POST
|
|
36
|
+
* from a forged one, so this fails closed by construction — a missing or
|
|
37
|
+
* blank value throws rather than falling back to accepting every request.
|
|
38
|
+
*/
|
|
39
|
+
webhookSecret: string;
|
|
40
|
+
/** Where the route is mounted. @default "/webhooks/telegram" */
|
|
41
|
+
path?: string;
|
|
42
|
+
/** Channel and sender-registry name. Override to run more than one bot in one process. @default "telegram" */
|
|
43
|
+
name?: string;
|
|
44
|
+
/** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
|
|
45
|
+
allowedChats?: string[];
|
|
46
|
+
answerFn?: AnswerFn;
|
|
47
|
+
bucketsFor?: BucketsFor;
|
|
48
|
+
rewriteQuery?: RewriteQuery;
|
|
49
|
+
rerankContext?: RerankContext;
|
|
50
|
+
transformReply?: TransformReply;
|
|
51
|
+
model?: string;
|
|
52
|
+
/** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. @default "Channel: Telegram." */
|
|
53
|
+
channelHint?: string;
|
|
54
|
+
/** A throw/rejection is treated as "no persona" for that turn. `sender` is the namespaced `tg:<id>` key. */
|
|
55
|
+
personaResolver?: (ctx: {
|
|
56
|
+
sender: string;
|
|
57
|
+
text: string;
|
|
58
|
+
}) => string | undefined | Promise<string | undefined>;
|
|
59
|
+
/** Off by default — the channel stays single-turn until a store is configured. */
|
|
60
|
+
history?: {
|
|
61
|
+
store: HistoryStore;
|
|
62
|
+
/** Most recent turns to load per reply. @default 20 */
|
|
63
|
+
limit?: number;
|
|
64
|
+
historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
|
|
65
|
+
};
|
|
66
|
+
muteRegex?: RegExp;
|
|
67
|
+
unmuteRegex?: RegExp;
|
|
68
|
+
muteReply?: string;
|
|
69
|
+
unmuteReply?: string;
|
|
70
|
+
dmRateLimit?: {
|
|
71
|
+
max: number;
|
|
72
|
+
windowMs: number;
|
|
73
|
+
};
|
|
74
|
+
groupRateLimit?: {
|
|
75
|
+
max: number;
|
|
76
|
+
windowMs: number;
|
|
77
|
+
};
|
|
78
|
+
now?: () => number;
|
|
79
|
+
/** Overridable for tests and for hosts routing through a proxy; defaults to `globalThis.fetch`. */
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
/** Self-hosted Bot API server origin. */
|
|
82
|
+
apiBaseUrl?: string;
|
|
83
|
+
/** Overridable for tests, which fake the Bot API instead of calling it; defaults to a `fetch` client over {@link TelegramWebhookConfig.botToken}. */
|
|
84
|
+
api?: TelegramApi;
|
|
85
|
+
logger?: Logger;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Builds a `CustomRoutes` mount that receives Telegram updates over HTTPS.
|
|
89
|
+
* Throws synchronously if `webhookSecret` is missing, blank, or outside the
|
|
90
|
+
* charset Telegram accepts for it — before the route ever exists, rather
|
|
91
|
+
* than mounting an endpoint that would silently accept everything or reject
|
|
92
|
+
* every request.
|
|
93
|
+
*/
|
|
94
|
+
export declare function createTelegramWebhookRoute(config: TelegramWebhookConfig): CustomRoutes;
|
|
95
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAMjF,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KACpE,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qJAAqJ;IACrJ,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,GAAG,YAAY,CAkHtF"}
|
package/package.json
CHANGED