@diegoaltoworks/chatter 0.54.2 → 0.56.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 (39) hide show
  1. package/README.md +2 -1
  2. package/dist/channels/index.js +18 -1
  3. package/dist/channels/index.mjs +18 -1
  4. package/dist/channels/pipeline.d.ts +11 -1
  5. package/dist/channels/pipeline.d.ts.map +1 -1
  6. package/dist/channels/telegram/api.d.ts +122 -0
  7. package/dist/channels/telegram/api.d.ts.map +1 -0
  8. package/dist/channels/telegram/channel.d.ts +82 -0
  9. package/dist/channels/telegram/channel.d.ts.map +1 -0
  10. package/dist/channels/telegram/index.d.ts +26 -0
  11. package/dist/channels/telegram/index.d.ts.map +1 -0
  12. package/dist/channels/telegram/index.js +710 -0
  13. package/dist/channels/telegram/index.mjs +667 -0
  14. package/dist/channels/telegram/poll.d.ts +47 -0
  15. package/dist/channels/telegram/poll.d.ts.map +1 -0
  16. package/dist/channels/telegram/updates.d.ts +63 -0
  17. package/dist/channels/telegram/updates.d.ts.map +1 -0
  18. package/dist/channels/whatsapp/inbound.d.ts +4 -1
  19. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  20. package/dist/channels/whatsapp/index.js +22 -2
  21. package/dist/channels/whatsapp/index.mjs +22 -2
  22. package/dist/core/answer.d.ts +31 -0
  23. package/dist/core/answer.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +54 -10
  27. package/dist/index.mjs +53 -10
  28. package/dist/mcp-server.d.ts.map +1 -1
  29. package/dist/mcp-server.js +26 -4
  30. package/dist/mcp-server.mjs +26 -4
  31. package/dist/routes/demo.d.ts.map +1 -1
  32. package/dist/routes/openai.d.ts.map +1 -1
  33. package/dist/routes/private.d.ts.map +1 -1
  34. package/dist/routes/public.d.ts.map +1 -1
  35. package/dist/server.js +38 -6
  36. package/dist/server.mjs +38 -6
  37. package/dist/types.d.ts +17 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +8 -2
package/README.md CHANGED
@@ -26,7 +26,7 @@
26
26
  - ⚡ **High Performance**: Built on Hono with streaming support
27
27
  - 🛡️ **Security First**: Rate limiting, CORS, referrer checking, and input guardrails
28
28
  - 💸 **Usage Metering**: Per-caller and global daily caps for paid features, multi-instance safe, via `@diegoaltoworks/chatter/usage` ([guide](./docs/usage.md))
29
- - 💬 **Channels**: Built-in WhatsApp transport plus a Channel SPI for plugging in any other one — allowlist/mute gates, reply rate-limiting, and a shared inbound pipeline every channel reuses ([WhatsApp guide](./docs/channels.md), [build your own](./docs/build-a-channel.md))
29
+ - 💬 **Channels**: Built-in WhatsApp and Telegram transports plus a Channel SPI for plugging in any other one — allowlist/mute gates, reply rate-limiting, and a shared inbound pipeline every channel reuses ([WhatsApp](./docs/channels.md), [Telegram](./docs/telegram.md), [build your own](./docs/build-a-channel.md))
30
30
  - 🧩 **Flows**: Multi-turn, schema-driven slot-filling for structured conversations, with hybrid keyword + LLM intent matching ([guide](./docs/flows.md))
31
31
  - 🖼️ **Images**: On-demand generation and editing with cache-before-spend ordering and optional Cloudinary upload ([guide](./docs/images.md))
32
32
  - 🎭 **Personas**: Windowed, per-contact prompt layers and named greetings from a JSON registry ([guide](./docs/personas.md))
@@ -102,6 +102,7 @@ Complete guides for setup, deployment, and integration — see
102
102
  - **[Personas](./docs/personas.md)** - Dynamic prompt layers and named greetings
103
103
  - **[WhatsApp Channel](./docs/channels.md)** - Link a WhatsApp number as a transport
104
104
  - **[Building a Channel](./docs/build-a-channel.md)** - Plug in a new transport
105
+ - **[Telegram Channel](./docs/telegram.md)** - Run a bot on the official Bot API, no extra dependency
105
106
  - **[Flows](./docs/flows.md)** - Multi-turn, schema-driven slot-filling flows
106
107
  - **[Images](./docs/images.md)** - Generate and cache images on demand
107
108
  - **[Conversation History](./docs/history.md)** - Structural, host-replaceable multi-turn context
@@ -160,6 +160,18 @@ async function answerOnce({
160
160
  }
161
161
  return completeOnce({ client, system, messages, temperature, model });
162
162
  }
163
+ async function applyTransformReply(transformReply, input, logger) {
164
+ if (!transformReply) return input.text;
165
+ try {
166
+ return await transformReply(input);
167
+ } catch (error) {
168
+ logger?.error(
169
+ `transformReply threw for channel "${input.channel}"; sending the original reply`,
170
+ error
171
+ );
172
+ return input.text;
173
+ }
174
+ }
163
175
 
164
176
  // src/core/buckets.ts
165
177
  function defaultBuckets(mode) {
@@ -295,7 +307,7 @@ function createInboundPipeline(deps, config) {
295
307
  channelHint: config.channelHint,
296
308
  buckets
297
309
  });
298
- const { content } = await answerOnce({
310
+ const { content: produced } = await answerOnce({
299
311
  answerFn: config.answerFn,
300
312
  client: deps.client,
301
313
  system,
@@ -305,6 +317,11 @@ function createInboundPipeline(deps, config) {
305
317
  conversationId,
306
318
  model: config.model
307
319
  });
320
+ const content = await applyTransformReply(
321
+ config.transformReply,
322
+ { channel: config.channel ?? "channel", sender, conversationId, text: produced },
323
+ config.logger
324
+ );
308
325
  if (!content) {
309
326
  return { action: "ignore" };
310
327
  }
@@ -127,6 +127,18 @@ async function answerOnce({
127
127
  }
128
128
  return completeOnce({ client, system, messages, temperature, model });
129
129
  }
130
+ async function applyTransformReply(transformReply, input, logger) {
131
+ if (!transformReply) return input.text;
132
+ try {
133
+ return await transformReply(input);
134
+ } catch (error) {
135
+ logger?.error(
136
+ `transformReply threw for channel "${input.channel}"; sending the original reply`,
137
+ error
138
+ );
139
+ return input.text;
140
+ }
141
+ }
130
142
 
131
143
  // src/core/buckets.ts
132
144
  function defaultBuckets(mode) {
@@ -262,7 +274,7 @@ function createInboundPipeline(deps, config) {
262
274
  channelHint: config.channelHint,
263
275
  buckets
264
276
  });
265
- const { content } = await answerOnce({
277
+ const { content: produced } = await answerOnce({
266
278
  answerFn: config.answerFn,
267
279
  client: deps.client,
268
280
  system,
@@ -272,6 +284,11 @@ function createInboundPipeline(deps, config) {
272
284
  conversationId,
273
285
  model: config.model
274
286
  });
287
+ const content = await applyTransformReply(
288
+ config.transformReply,
289
+ { channel: config.channel ?? "channel", sender, conversationId, text: produced },
290
+ config.logger
291
+ );
275
292
  if (!content) {
276
293
  return { action: "ignore" };
277
294
  }
@@ -14,8 +14,9 @@
14
14
  * re-derive it. See docs/build-a-channel.md for a worked example.
15
15
  */
16
16
  import type OpenAI from "openai";
17
- import type { AnswerFn } from "../core/answer";
17
+ import type { AnswerFn, TransformReply } from "../core/answer";
18
18
  import type { BucketsFor } from "../core/buckets";
19
+ import type { Logger } from "../core/logger";
19
20
  import type { PromptLoader } from "../core/prompts";
20
21
  import type { VectorStore } from "../core/retrieval";
21
22
  import type { HistoryStore } from "../history/types";
@@ -33,8 +34,15 @@ export interface InboundPipelineDeps {
33
34
  prompts: PromptLoader;
34
35
  }
35
36
  export interface InboundPipelineConfig {
37
+ /**
38
+ * This channel's identity for `transformReply` — e.g. "whatsapp",
39
+ * "telegram". @default "channel"
40
+ */
41
+ channel?: string;
36
42
  answerFn?: AnswerFn;
37
43
  bucketsFor?: BucketsFor;
44
+ /** Modifies or vetoes the produced reply before delivery — see `ChatterConfig.transformReply`. */
45
+ transformReply?: TransformReply;
38
46
  model?: string;
39
47
  /** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. */
40
48
  channelHint?: string;
@@ -65,6 +73,8 @@ export interface InboundPipelineConfig {
65
73
  windowMs: number;
66
74
  };
67
75
  now?: () => number;
76
+ /** Logs a `transformReply` throw; the original reply still sends. */
77
+ logger?: Logger;
68
78
  }
69
79
  export interface InboundTurn {
70
80
  /** Delivers this turn's gate ack / chat answer — see {@link InboundReplySender}. */
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,KAAK,cAAc,EAIpB,MAAM,SAAS,CAAC;AAEjB,iNAAiN;AACjN,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,kFAAkF;IAClF,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kGAAkG;IAClG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sGAAsG;IACtG,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,gFAAgF;IAChF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,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,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACnD,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;CACpF;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CAAA;CAAE,GACzE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAsBrC;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,qBAAqB,GAC5B,eAAe,CAyGjB"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Telegram Bot API client — the whole transport layer of the Telegram
3
+ * channel, over plain `fetch`. No SDK, no optional peer dependency: the Bot
4
+ * API is JSON over HTTPS, so `./telegram` costs a consumer nothing beyond the
5
+ * package itself.
6
+ *
7
+ * Everything here is about the wire: envelopes, error mapping, the 4096-char
8
+ * message limit. Interpretation of a message (mentions, gates, replies) lives
9
+ * in `./updates` and `./channel`.
10
+ *
11
+ * The bot token is a credential that appears in every request URL, so it is
12
+ * never allowed into an error message or a log line — see {@link redactToken}.
13
+ */
14
+ /** Default Bot API origin. A self-hosted Bot API server sets its own via `baseUrl`. */
15
+ export declare const TELEGRAM_API_BASE = "https://api.telegram.org";
16
+ /** Bot API `sendMessage` hard limit, in UTF-16 code units. A longer text is rejected with HTTP 400. */
17
+ export declare const TELEGRAM_TEXT_LIMIT = 4096;
18
+ export interface TelegramUser {
19
+ id: number;
20
+ is_bot?: boolean;
21
+ username?: string;
22
+ first_name?: string;
23
+ }
24
+ export interface TelegramChat {
25
+ id: number;
26
+ type: "private" | "group" | "supergroup" | "channel";
27
+ }
28
+ /** Offsets/lengths are UTF-16 code units — the same units JS string indexing uses, so `text.slice(offset, offset + length)` is exact. */
29
+ export interface TelegramMessageEntity {
30
+ type: string;
31
+ offset: number;
32
+ length: number;
33
+ /** Present on `text_mention` entities: a mention of a user who has no username. */
34
+ user?: TelegramUser;
35
+ }
36
+ export interface TelegramMessage {
37
+ message_id: number;
38
+ from?: TelegramUser;
39
+ chat: TelegramChat;
40
+ text?: string;
41
+ /** Photos/videos/documents carry their text here, with entities on `caption_entities`. */
42
+ caption?: string;
43
+ entities?: TelegramMessageEntity[];
44
+ caption_entities?: TelegramMessageEntity[];
45
+ reply_to_message?: {
46
+ message_id?: number;
47
+ from?: TelegramUser;
48
+ };
49
+ }
50
+ export interface TelegramUpdate {
51
+ update_id: number;
52
+ message?: TelegramMessage;
53
+ }
54
+ /** A Bot API call that did not return `ok: true`, or never completed. `retryAfterMs` carries Telegram's own flood-wait instruction when it sent one. */
55
+ export declare class TelegramApiError extends Error {
56
+ readonly errorCode?: number;
57
+ readonly retryAfterMs?: number;
58
+ constructor(message: string, options?: {
59
+ errorCode?: number;
60
+ retryAfterMs?: number;
61
+ });
62
+ }
63
+ /**
64
+ * Replaces every occurrence of the bot token with `***`. The token is part of
65
+ * the request URL, and a fetch failure (or a proxy's error body) can echo that
66
+ * URL back — logging it verbatim would leak full control of the bot into the
67
+ * host's logs. An empty token is left alone: replacing "" would corrupt the
68
+ * message rather than protect anything.
69
+ */
70
+ export declare function redactToken(text: string, token: string): string;
71
+ /**
72
+ * Splits `text` into chunks no longer than `limit`, preferring paragraph, then
73
+ * line, then word boundaries — a model's answer regularly runs past Telegram's
74
+ * 4096-char cap, and the Bot API's response to that is to reject the whole
75
+ * message, so an unsplit send loses the answer entirely rather than truncating
76
+ * it. Empty/blank input yields no chunks (nothing to send).
77
+ */
78
+ export declare function splitTelegramText(text: string, limit?: number): string[];
79
+ /** What `ChannelSender.sendMedia` accepts for this channel. A bare string is shorthand for a photo URL. */
80
+ export interface TelegramMediaPayload {
81
+ /** @default "photo" */
82
+ kind?: "photo" | "document" | "video" | "audio";
83
+ /** An https URL or a Telegram `file_id` — the two forms the Bot API accepts without a multipart upload. */
84
+ url: string;
85
+ caption?: string;
86
+ }
87
+ /**
88
+ * Turns the registry's opaque `sendMedia` payload into a Bot API call.
89
+ * `ChannelSender.sendMedia` types its payload as `unknown` (payload shapes are
90
+ * transport-defined), so this is where that unknown is checked — a malformed
91
+ * payload throws here, which the sender registry reports as `false` rather
92
+ * than letting a caller's bad send crash it.
93
+ */
94
+ export declare function toTelegramMediaRequest(chatId: string, payload: unknown): {
95
+ method: string;
96
+ body: Record<string, unknown>;
97
+ };
98
+ export interface TelegramApiConfig {
99
+ botToken: string;
100
+ /** Overridable for tests and for hosts routing through a proxy; defaults to `globalThis.fetch`. */
101
+ fetch?: typeof fetch;
102
+ /** @default {@link TELEGRAM_API_BASE} */
103
+ baseUrl?: string;
104
+ }
105
+ export interface TelegramApi {
106
+ /** Raw call, for a method this interface does not wrap. Rejects with {@link TelegramApiError} on any non-`ok` result. */
107
+ call<T>(method: string, body?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
108
+ getMe(): Promise<TelegramUser>;
109
+ getUpdates(options: {
110
+ offset?: number;
111
+ timeoutSeconds: number;
112
+ signal?: AbortSignal;
113
+ }): Promise<TelegramUpdate[]>;
114
+ /** Splits at {@link TELEGRAM_TEXT_LIMIT}; only the first chunk threads onto `replyToMessageId`. */
115
+ sendMessage(chatId: string, text: string, options?: {
116
+ replyToMessageId?: number;
117
+ }): Promise<void>;
118
+ sendMedia(chatId: string, payload: unknown): Promise<void>;
119
+ setMessageReaction(chatId: string, messageId: number, emoji: string): Promise<void>;
120
+ }
121
+ export declare function createTelegramApi(config: TelegramApiConfig): TelegramApi;
122
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Telegram Bot API transport: a {@link Channel} that long-polls `getUpdates`,
3
+ * maps each update into a `ChannelMessage`, and answers through the shared
4
+ * `createInboundPipeline` — the same gates, persona, buckets, history and
5
+ * `answerFn` seams every other channel uses.
6
+ *
7
+ * This is the second transport on the channel SPI, and unlike WhatsApp it
8
+ * needs no optional peer dependency, no session persistence, and no deploy
9
+ * lease: the Bot API is JSON over HTTPS and Telegram itself queues updates
10
+ * per bot token. It is deliberately self-contained — `start(deps)` has
11
+ * everything it needs, so there is no `customRoutes` wiring step (contrast
12
+ * `./channels/whatsapp/inbound.ts`, whose transport and interpretation are
13
+ * separate modules).
14
+ *
15
+ * One caveat worth knowing before choosing this over a user-mode client: a bot
16
+ * cannot start a conversation (the user must message it first), and in groups
17
+ * Telegram's privacy mode means it only receives messages that address it —
18
+ * which is the same policy `decideChannelAction` applies anyway.
19
+ */
20
+ import type { AnswerFn, TransformReply } from "../../core/answer";
21
+ import type { BucketsFor } from "../../core/buckets";
22
+ import { type Logger } from "../../core/logger";
23
+ import type { HistoryStore } from "../../history/types";
24
+ import type { Channel } from "../index";
25
+ import { type TelegramApi } from "./api";
26
+ export interface TelegramChannelConfig {
27
+ /** From @BotFather. A credential — pass it from the environment, never commit it. */
28
+ botToken: string;
29
+ /** Channel and sender-registry name. Override to run more than one bot in one process. @default "telegram" */
30
+ name?: string;
31
+ /** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
32
+ allowedChats?: string[];
33
+ answerFn?: AnswerFn;
34
+ bucketsFor?: BucketsFor;
35
+ /** Modifies or vetoes the produced reply before delivery — see `ChatterConfig.transformReply`. Falls back to the server's own. */
36
+ transformReply?: TransformReply;
37
+ model?: string;
38
+ /** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. @default "Channel: Telegram." */
39
+ channelHint?: string;
40
+ /** A throw/rejection is treated as "no persona" for that turn. `sender` is the namespaced `tg:<id>` key. */
41
+ personaResolver?: (ctx: {
42
+ sender: string;
43
+ text: string;
44
+ }) => string | undefined | Promise<string | undefined>;
45
+ /** Off by default — the channel stays single-turn until a store is configured. */
46
+ history?: {
47
+ store: HistoryStore;
48
+ limit?: number;
49
+ };
50
+ muteRegex?: RegExp;
51
+ unmuteRegex?: RegExp;
52
+ /** Neutral, overridable acknowledgements — this module ships no bot personality; unset = silent mute/unmute. */
53
+ muteReply?: string;
54
+ unmuteReply?: string;
55
+ dmRateLimit?: {
56
+ max: number;
57
+ windowMs: number;
58
+ };
59
+ groupRateLimit?: {
60
+ max: number;
61
+ windowMs: number;
62
+ };
63
+ /** @default 30 */
64
+ pollTimeoutSeconds?: number;
65
+ /** Resume point for `getUpdates`. Omitted = whatever Telegram still has queued (see docs/telegram.md). */
66
+ initialOffset?: number;
67
+ /** Called with each acknowledged offset, for a host that wants to persist it across restarts. */
68
+ onOffset?: (offset: number) => void;
69
+ /** Overridable for tests and for hosts routing through a proxy; defaults to `globalThis.fetch`. */
70
+ fetch?: typeof fetch;
71
+ /** Self-hosted Bot API server origin. */
72
+ apiBaseUrl?: string;
73
+ /** Overridable for tests, which fake the Bot API instead of calling it; defaults to a `fetch` client over {@link TelegramChannelConfig.botToken}. */
74
+ api?: TelegramApi;
75
+ /** Overridable for tests; defaults to a `setTimeout`-based sleep. */
76
+ sleep?: (ms: number) => Promise<void>;
77
+ now?: () => number;
78
+ /** Logger for poll/gate diagnostics. Falls back to the host's `deps.logger`, then a console logger. */
79
+ logger?: Logger;
80
+ }
81
+ export declare function createTelegramChannel(config: TelegramChannelConfig): Channel;
82
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;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,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAGxC,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAOjF,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,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;QAAE,KAAK,EAAE,YAAY,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,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,CAkI5E"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Telegram Bot API transport — a {@link Channel} with NO optional peer
3
+ * dependency: the Bot API is JSON over HTTPS, reached with plain `fetch`, so
4
+ * this subpath costs a consumer nothing beyond the package itself.
5
+ *
6
+ * ```ts
7
+ * import { createTelegramChannel } from "@diegoaltoworks/chatter/telegram";
8
+ *
9
+ * await createServer({
10
+ * ...,
11
+ * channels: [createTelegramChannel({ botToken: process.env.TELEGRAM_BOT_TOKEN as string })],
12
+ * });
13
+ * ```
14
+ *
15
+ * Everything past turning an update into a `ChannelMessage` runs through
16
+ * `./channels`' `createInboundPipeline`, shared with every other channel — see
17
+ * docs/telegram.md for configuration and docs/build-a-channel.md for the SPI
18
+ * this implements.
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ 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
+ export { createTelegramChannel, type TelegramChannelConfig } from "./channel";
24
+ export { type LongPollDeps, pollBackoffMs, retryDelayMs, runLongPoll } from "./poll";
25
+ export { mentionsBot, messageEntities, messageText, nextOffset, type TelegramBotIdentity, telegramSenderKey, toChannelMessage, } from "./updates";
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;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,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"}