@nodetool-ai/telegram 0.8.1

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 (59) hide show
  1. package/README.md +49 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/bot-api.d.ts +115 -0
  4. package/dist/bot-api.d.ts.map +1 -0
  5. package/dist/bot-api.js +145 -0
  6. package/dist/bot-api.js.map +1 -0
  7. package/dist/chunk.d.ts +36 -0
  8. package/dist/chunk.d.ts.map +1 -0
  9. package/dist/chunk.js +280 -0
  10. package/dist/chunk.js.map +1 -0
  11. package/dist/cli.d.ts +10 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +62 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands.d.ts +83 -0
  16. package/dist/commands.d.ts.map +1 -0
  17. package/dist/commands.js +191 -0
  18. package/dist/commands.js.map +1 -0
  19. package/dist/config.d.ts +65 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +139 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/frame-renderer.d.ts +135 -0
  24. package/dist/frame-renderer.d.ts.map +1 -0
  25. package/dist/frame-renderer.js +407 -0
  26. package/dist/frame-renderer.js.map +1 -0
  27. package/dist/identity-client.d.ts +132 -0
  28. package/dist/identity-client.d.ts.map +1 -0
  29. package/dist/identity-client.js +204 -0
  30. package/dist/identity-client.js.map +1 -0
  31. package/dist/index.d.ts +56 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +90 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/markdown-html.d.ts +32 -0
  36. package/dist/markdown-html.d.ts.map +1 -0
  37. package/dist/markdown-html.js +251 -0
  38. package/dist/markdown-html.js.map +1 -0
  39. package/dist/nodetool-client.d.ts +32 -0
  40. package/dist/nodetool-client.d.ts.map +1 -0
  41. package/dist/nodetool-client.js +127 -0
  42. package/dist/nodetool-client.js.map +1 -0
  43. package/dist/register-commands.d.ts +24 -0
  44. package/dist/register-commands.d.ts.map +1 -0
  45. package/dist/register-commands.js +29 -0
  46. package/dist/register-commands.js.map +1 -0
  47. package/dist/strings.d.ts +7 -0
  48. package/dist/strings.d.ts.map +1 -0
  49. package/dist/strings.js +13 -0
  50. package/dist/strings.js.map +1 -0
  51. package/dist/telegram-adapter.d.ts +100 -0
  52. package/dist/telegram-adapter.d.ts.map +1 -0
  53. package/dist/telegram-adapter.js +366 -0
  54. package/dist/telegram-adapter.js.map +1 -0
  55. package/dist/turn-router.d.ts +167 -0
  56. package/dist/turn-router.d.ts.map +1 -0
  57. package/dist/turn-router.js +360 -0
  58. package/dist/turn-router.js.map +1 -0
  59. package/package.json +68 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * TelegramAdapter — the only module that knows Telegram (design §3).
3
+ *
4
+ * Two directions meet here. Inbound: `getUpdates` long polling, private-chat
5
+ * and command routing, the unlinked-user prompt, group declines. Outbound: the
6
+ * renderer's ops become Bot API calls, with the platform's two facts of life
7
+ * handled rather than hoped away — 429 `retry_after`, and the parse mode
8
+ * rejecting text, which falls back to plain rather than failing the turn.
9
+ *
10
+ * Ops are executed off a per-chat queue. That queue is what makes superseded
11
+ * edits droppable: while a backoff holds the head of the queue, later edits to
12
+ * the same message pile up behind it, and only the last one is worth sending.
13
+ */
14
+ import { BotApiError, TELEGRAM_UPLOAD_LIMIT_BYTES } from "./bot-api.js";
15
+ import { MESSAGES, handleCommand, isAllowedUser, parseCommand } from "./commands.js";
16
+ /** Long-poll seconds asked of `getUpdates`. */
17
+ export const POLL_TIMEOUT_SECONDS = 50;
18
+ /** Callback data on the inline stop button. */
19
+ export const STOP_CALLBACK_DATA = "stop";
20
+ function defaultWait(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+ function isPhoto(contentType, filename) {
24
+ if (contentType !== null) {
25
+ return contentType.startsWith("image/") && !contentType.includes("svg");
26
+ }
27
+ return /\.(jpe?g|png|webp)$/i.test(filename);
28
+ }
29
+ export class TelegramAdapter {
30
+ api;
31
+ identity;
32
+ router;
33
+ config;
34
+ fetchImpl;
35
+ wait;
36
+ resolveAsset;
37
+ log;
38
+ offset = 0;
39
+ running = false;
40
+ messages = new Map();
41
+ queues = new Map();
42
+ declinedGroups = new Set();
43
+ constructor(options) {
44
+ this.api = options.api;
45
+ this.identity = options.identity;
46
+ this.router = options.router;
47
+ this.config = options.config;
48
+ this.fetchImpl = options.fetch;
49
+ this.wait = options.wait ?? defaultWait;
50
+ this.resolveAsset = options.resolveAsset ?? null;
51
+ this.log = options.log ?? ((message, detail) => console.error(message, detail ?? ""));
52
+ }
53
+ /**
54
+ * Long-poll until {@link stop} is called.
55
+ *
56
+ * A 409 means another process holds this bot token's update stream — two
57
+ * consumers silently steal each other's updates, so the loop ends and says
58
+ * so instead of fighting for them.
59
+ */
60
+ async poll() {
61
+ this.running = true;
62
+ while (this.running) {
63
+ let updates;
64
+ try {
65
+ updates = await this.api.getUpdates(this.offset, POLL_TIMEOUT_SECONDS);
66
+ }
67
+ catch (err) {
68
+ if (err instanceof BotApiError && err.isConflict) {
69
+ this.log("[telegram] another getUpdates consumer holds this bot token — " +
70
+ "stop the other bridge instance (or delete the webhook) and start again.");
71
+ this.running = false;
72
+ return "conflict";
73
+ }
74
+ if (err instanceof BotApiError && err.retryAfterSeconds !== null) {
75
+ await this.wait(err.retryAfterSeconds * 1000);
76
+ continue;
77
+ }
78
+ if (err instanceof BotApiError && err.status === 401) {
79
+ this.log(`[telegram] ${err.message}`);
80
+ this.running = false;
81
+ return "fatal";
82
+ }
83
+ this.log("[telegram] getUpdates failed:", err);
84
+ await this.wait(1000);
85
+ continue;
86
+ }
87
+ for (const update of updates) {
88
+ this.offset = Math.max(this.offset, update.update_id + 1);
89
+ try {
90
+ await this.handleUpdate(update);
91
+ }
92
+ catch (err) {
93
+ this.log("[telegram] failed to handle update:", err);
94
+ }
95
+ }
96
+ }
97
+ return "stopped";
98
+ }
99
+ /** Ask the poll loop to end after the in-flight request. */
100
+ stop() {
101
+ this.running = false;
102
+ }
103
+ /** Wait until every queued op has been executed. */
104
+ async flush() {
105
+ for (const queue of this.queues.values()) {
106
+ await queue.draining;
107
+ }
108
+ }
109
+ // -------------------------------------------------------------------------
110
+ // Inbound
111
+ // -------------------------------------------------------------------------
112
+ async handleUpdate(update) {
113
+ if (update.callback_query) {
114
+ await this.handleCallbackQuery(update.callback_query);
115
+ return;
116
+ }
117
+ const message = update.message;
118
+ if (message) {
119
+ await this.handleMessage(message);
120
+ }
121
+ }
122
+ async handleCallbackQuery(query) {
123
+ const chatId = query.message ? String(query.message.chat.id) : null;
124
+ if (query.data !== STOP_CALLBACK_DATA || chatId === null) {
125
+ await this.api.answerCallbackQuery(query.id);
126
+ return;
127
+ }
128
+ const stopped = this.router.stop(chatId);
129
+ await this.api.answerCallbackQuery(query.id, stopped ? MESSAGES.stopping : MESSAGES.nothingRunning);
130
+ }
131
+ async handleMessage(message) {
132
+ const chatId = String(message.chat.id);
133
+ const telegramUserId = message.from ? String(message.from.id) : null;
134
+ if (telegramUserId === null) {
135
+ return;
136
+ }
137
+ if (message.chat.type !== "private") {
138
+ if (!this.declinedGroups.has(chatId)) {
139
+ this.declinedGroups.add(chatId);
140
+ await this.sendPlain(chatId, MESSAGES.groupDecline);
141
+ }
142
+ return;
143
+ }
144
+ if (!isAllowedUser(this.config.allowUsers, telegramUserId)) {
145
+ await this.sendPlain(chatId, MESSAGES.notAllowed);
146
+ return;
147
+ }
148
+ const text = message.text ?? message.caption ?? "";
149
+ const command = text.length > 0 ? parseCommand(text, message.entities) : null;
150
+ if (command !== null) {
151
+ const outcome = await handleCommand(this.commandDeps(), {
152
+ command: command.command,
153
+ args: command.args,
154
+ chatId,
155
+ telegramUserId
156
+ });
157
+ for (const reply of outcome.replies) {
158
+ await this.sendPlain(chatId, reply);
159
+ }
160
+ return;
161
+ }
162
+ if (text.length === 0) {
163
+ await this.sendPlain(chatId, MESSAGES.unsupportedMedia);
164
+ return;
165
+ }
166
+ const result = await this.router.submit({ telegramUserId, chatId, text });
167
+ if (result.status === "unlinked") {
168
+ await this.sendPlain(chatId, result.reason === "local-mode" ? result.message : MESSAGES.linkPrompt);
169
+ return;
170
+ }
171
+ if (result.status === "busy") {
172
+ await this.sendPlain(chatId, MESSAGES.busy);
173
+ }
174
+ }
175
+ commandDeps() {
176
+ return {
177
+ identity: this.identity,
178
+ router: this.router,
179
+ apiUrl: this.config.apiUrl,
180
+ fetch: this.fetchImpl,
181
+ allowUsers: this.config.allowUsers
182
+ };
183
+ }
184
+ // -------------------------------------------------------------------------
185
+ // Outbound
186
+ // -------------------------------------------------------------------------
187
+ /**
188
+ * Accept a batch of render ops for delivery. Returns once the ops are
189
+ * queued; the per-chat drain loop executes them in order.
190
+ */
191
+ executeOps = (context, ops) => {
192
+ const queue = this.queueFor(context.chatId);
193
+ for (const op of ops) {
194
+ queue.ops.push({ op, context });
195
+ }
196
+ if (queue.draining === null) {
197
+ queue.draining = this.drain(context.chatId, queue).finally(() => {
198
+ queue.draining = null;
199
+ });
200
+ }
201
+ };
202
+ queueFor(chatId) {
203
+ let queue = this.queues.get(chatId);
204
+ if (!queue) {
205
+ queue = { ops: [], draining: null };
206
+ this.queues.set(chatId, queue);
207
+ }
208
+ return queue;
209
+ }
210
+ async drain(chatId, queue) {
211
+ while (queue.ops.length > 0) {
212
+ const entry = queue.ops.shift();
213
+ if (entry === undefined) {
214
+ return;
215
+ }
216
+ if (this.isSuperseded(entry.op, queue)) {
217
+ continue;
218
+ }
219
+ try {
220
+ await this.executeOne(chatId, entry);
221
+ }
222
+ catch (err) {
223
+ if (err instanceof BotApiError && err.retryAfterSeconds !== null) {
224
+ await this.wait(err.retryAfterSeconds * 1000);
225
+ // Re-queue at the head; the supersede check runs again, so a newer
226
+ // edit of the same message wins over this one.
227
+ queue.ops.unshift(entry);
228
+ continue;
229
+ }
230
+ this.log("[telegram] render op failed:", err);
231
+ }
232
+ }
233
+ }
234
+ /** An intermediate edit nothing will see, because a later one is queued. */
235
+ isSuperseded(op, queue) {
236
+ if (op.kind !== "edit") {
237
+ return false;
238
+ }
239
+ return queue.ops.some((entry) => (entry.op.kind === "edit" || entry.op.kind === "finalize") &&
240
+ entry.op.target === op.target);
241
+ }
242
+ async executeOne(chatId, entry) {
243
+ const { op, context } = entry;
244
+ const state = this.messagesFor(chatId);
245
+ switch (op.kind) {
246
+ case "typing":
247
+ await this.api.sendChatAction(chatId, "typing");
248
+ return;
249
+ case "send": {
250
+ const sent = await this.send(chatId, op.text, op.parseMode, op.target === "status");
251
+ state[op.target] = sent;
252
+ return;
253
+ }
254
+ case "edit": {
255
+ const messageId = state[op.target];
256
+ if (messageId === null) {
257
+ state[op.target] = await this.send(chatId, op.text, op.parseMode, op.target === "status");
258
+ return;
259
+ }
260
+ await this.edit(chatId, messageId, op.text, op.parseMode);
261
+ return;
262
+ }
263
+ case "finalize": {
264
+ const messageId = state[op.target];
265
+ if (op.create || messageId === null) {
266
+ await this.send(chatId, op.text, op.parseMode, false);
267
+ }
268
+ else {
269
+ await this.edit(chatId, messageId, op.text, op.parseMode);
270
+ }
271
+ state[op.target] = null;
272
+ return;
273
+ }
274
+ case "stop-note":
275
+ await this.sendPlain(chatId, op.text);
276
+ return;
277
+ case "attach":
278
+ await this.attach(chatId, op.asset, context);
279
+ return;
280
+ default:
281
+ return;
282
+ }
283
+ }
284
+ messagesFor(chatId) {
285
+ let state = this.messages.get(chatId);
286
+ if (!state) {
287
+ state = { status: null, stream: null };
288
+ this.messages.set(chatId, state);
289
+ }
290
+ return state;
291
+ }
292
+ async send(chatId, text, parseMode, withStopButton) {
293
+ const payload = { chat_id: chatId, text };
294
+ if (parseMode === "html") {
295
+ payload.parse_mode = "HTML";
296
+ }
297
+ if (withStopButton) {
298
+ payload.reply_markup = {
299
+ inline_keyboard: [[{ text: "⏹ Stop", callback_data: STOP_CALLBACK_DATA }]]
300
+ };
301
+ }
302
+ try {
303
+ const sent = await this.api.sendMessage(payload);
304
+ return sent.message_id;
305
+ }
306
+ catch (err) {
307
+ if (err instanceof BotApiError && err.isParseFailure) {
308
+ delete payload.parse_mode;
309
+ const sent = await this.api.sendMessage(payload);
310
+ return sent.message_id;
311
+ }
312
+ throw err;
313
+ }
314
+ }
315
+ async edit(chatId, messageId, text, parseMode) {
316
+ const payload = {
317
+ chat_id: chatId,
318
+ message_id: messageId,
319
+ text
320
+ };
321
+ if (parseMode === "html") {
322
+ payload.parse_mode = "HTML";
323
+ }
324
+ try {
325
+ await this.api.editMessageText(payload);
326
+ }
327
+ catch (err) {
328
+ if (err instanceof BotApiError && err.isUnmodified) {
329
+ // The message already shows this text; nothing to do.
330
+ return;
331
+ }
332
+ if (err instanceof BotApiError && err.isParseFailure) {
333
+ delete payload.parse_mode;
334
+ await this.api.editMessageText(payload);
335
+ return;
336
+ }
337
+ throw err;
338
+ }
339
+ }
340
+ async sendPlain(chatId, text) {
341
+ await this.api.sendMessage({ chat_id: chatId, text });
342
+ }
343
+ async attach(chatId, asset, context) {
344
+ if (this.resolveAsset === null) {
345
+ await this.sendPlain(chatId, `📎 ${asset.name ?? asset.uri}`);
346
+ return;
347
+ }
348
+ const resolved = await this.resolveAsset(asset, context);
349
+ if (resolved === null) {
350
+ await this.sendPlain(chatId, `📎 ${asset.name ?? asset.uri}`);
351
+ return;
352
+ }
353
+ if (resolved.bytes.byteLength > TELEGRAM_UPLOAD_LIMIT_BYTES) {
354
+ await this.sendPlain(chatId, `📎 too large for Telegram: ${asset.uri}`);
355
+ return;
356
+ }
357
+ const photo = isPhoto(resolved.contentType, resolved.filename);
358
+ const form = new FormData();
359
+ form.append("chat_id", chatId);
360
+ form.append(photo ? "photo" : "document", new Blob([resolved.bytes], {
361
+ type: resolved.contentType ?? "application/octet-stream"
362
+ }), resolved.filename);
363
+ await this.api.callMultipart(photo ? "sendPhoto" : "sendDocument", form);
364
+ }
365
+ }
366
+ //# sourceMappingURL=telegram-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram-adapter.js","sourceRoot":"","sources":["../src/telegram-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * TurnRouter — one turn at a time per conversation, one socket per user
3
+ * (design §6).
4
+ *
5
+ * The router is where identity, the chat socket and the pure frame renderer
6
+ * meet. It owns three things the other modules deliberately do not:
7
+ *
8
+ * - **Whose socket.** A Telegram user's turns run on a `ChatSocket` opened
9
+ * with that user's delegated token, so every turn is the server's own
10
+ * tenant-isolated path. Two users never share a socket.
11
+ * - **Turn order.** A private chat is one conversation: a second message while
12
+ * a turn runs queues behind it (capped), mirroring the server's per-thread
13
+ * turn lock instead of racing it.
14
+ * - **The clock.** `foldFrame` is clockless by construction; this layer is
15
+ * where `Date.now()` enters, which keeps the renderer's throttling rules
16
+ * exercisable by scripted tests.
17
+ *
18
+ * Nothing here imports the Telegram API or the NodeTool SDK: the socket, the
19
+ * thread listing and the delivery of render ops are all injected.
20
+ */
21
+ import type { RenderFrame, RenderOp } from "./frame-renderer.js";
22
+ import type { IdentityResolution, UnlinkedReason } from "./identity-client.js";
23
+ /** The identity surface the router needs — a subset of `IdentityClient`. */
24
+ export interface IdentityResolver {
25
+ resolve(externalId: string): Promise<IdentityResolution>;
26
+ invalidate(externalId: string): void;
27
+ }
28
+ /** One outbound turn, in the shape the server's `chat_message` command takes. */
29
+ export interface BridgeSendOptions {
30
+ readonly threadId: string;
31
+ readonly text: string;
32
+ readonly provider?: string | null;
33
+ readonly model?: string | null;
34
+ readonly agentMode?: boolean;
35
+ readonly permissionMode?: "plan" | "default" | "auto" | null;
36
+ }
37
+ /**
38
+ * The chat socket, narrowed to what the bridge uses. `@nodetool-ai/sdk`'s
39
+ * `ChatSocket` is wrapped into this shape in `nodetool-client.ts`, so the
40
+ * router stays free of the SDK — and of `ws`.
41
+ */
42
+ export interface BridgeChatSocket {
43
+ connect(): void;
44
+ disconnect(): void;
45
+ send(options: BridgeSendOptions): void;
46
+ stop(threadId: string): void;
47
+ /** Replay everything after `lastSeq` on this thread (`resume_chat`). */
48
+ resume(threadId: string, lastSeq: number): void;
49
+ /** Every inbound frame, including types without a typed handler. */
50
+ onFrame(listener: (frame: RenderFrame) => void): () => void;
51
+ /** Connection-state transitions; `connected` after a drop triggers resume. */
52
+ onStateChange(listener: (state: string) => void): () => void;
53
+ }
54
+ /** A connected NodeTool client bound to one user's delegated token. */
55
+ export interface BridgeClient {
56
+ readonly socket: BridgeChatSocket;
57
+ /** Thread ids owned by this user, for `n` recovery after a restart. */
58
+ listThreadIds(): Promise<readonly string[]>;
59
+ }
60
+ export type MakeBridgeClient = (session: {
61
+ readonly token: string;
62
+ readonly userId: string;
63
+ }) => BridgeClient;
64
+ /** Everything the adapter needs to place a turn's ops in the right chat. */
65
+ export interface DeliveryContext {
66
+ readonly chatId: string;
67
+ readonly telegramUserId: string;
68
+ /** NodeTool user the turn runs as. */
69
+ readonly userId: string;
70
+ readonly threadId: string;
71
+ /** Delegated token, so the adapter can fetch the user's own assets. */
72
+ readonly token: string;
73
+ }
74
+ export type ExecuteOps = (context: DeliveryContext, ops: readonly RenderOp[]) => void | Promise<void>;
75
+ export interface TurnRouterOptions {
76
+ readonly identity: IdentityResolver;
77
+ readonly makeClient: MakeBridgeClient;
78
+ readonly config: TurnRouterConfig;
79
+ readonly executeOps: ExecuteOps;
80
+ /** Clock, in ms. Defaults to `Date.now`. */
81
+ readonly nowMs?: () => number;
82
+ }
83
+ export interface TurnRouterConfig {
84
+ readonly editThrottleMs: number;
85
+ readonly maxQueuedTurns: number;
86
+ /** Provider override for every turn; null leaves the server's default. */
87
+ readonly provider?: string | null;
88
+ readonly model?: string | null;
89
+ /**
90
+ * There is no interactive approver on this surface, so `auto` is the mode
91
+ * that makes a turn complete rather than park forever (design §9).
92
+ */
93
+ readonly permissionMode?: "plan" | "default" | "auto";
94
+ }
95
+ export type SubmitResult = {
96
+ readonly status: "started";
97
+ readonly threadId: string;
98
+ } | {
99
+ readonly status: "queued";
100
+ readonly threadId: string;
101
+ readonly depth: number;
102
+ } | {
103
+ readonly status: "busy";
104
+ readonly depth: number;
105
+ } | {
106
+ readonly status: "unlinked";
107
+ readonly reason: UnlinkedReason;
108
+ readonly message: string;
109
+ };
110
+ export interface SubmitInput {
111
+ readonly telegramUserId: string;
112
+ readonly chatId: string;
113
+ readonly text: string;
114
+ }
115
+ /** `telegram-<chatId>-<uid8>-<n>`. */
116
+ export declare function deriveThreadId(chatId: string, uid8: string, index: number): string;
117
+ /** The short user hash that keeps two tenants' derived ids disjoint. */
118
+ export declare function userHash8(userId: string): string;
119
+ /** Highest `n` among ids already in this chat's series, or 0 when there is none. */
120
+ export declare function highestThreadIndex(threadIds: readonly string[], chatId: string, uid8: string): number;
121
+ export declare class TurnRouter {
122
+ private readonly identity;
123
+ private readonly makeClient;
124
+ private readonly config;
125
+ private readonly executeOps;
126
+ private readonly now;
127
+ private readonly sessions;
128
+ private readonly conversations;
129
+ private readonly byThread;
130
+ constructor(options: TurnRouterOptions);
131
+ /**
132
+ * Take one inbound message. Starts a turn when the conversation is idle,
133
+ * queues it when a turn is running, and answers busy when the queue is full.
134
+ */
135
+ submit(input: SubmitInput): Promise<SubmitResult>;
136
+ /** Cancel the in-flight turn in this chat. False when nothing was running. */
137
+ stop(chatId: string): boolean;
138
+ /**
139
+ * Rotate this chat onto a fresh thread (`/new`). Anything queued behind the
140
+ * current turn is dropped: the user asked for a clean slate, and delivering
141
+ * old messages into a new thread would be the opposite.
142
+ */
143
+ newThread(chatId: string): string | null;
144
+ /** Current thread id for a chat, when one has been established. */
145
+ currentThreadId(chatId: string): string | null;
146
+ /** Messages waiting behind the in-flight turn, for `/status`. */
147
+ queueDepth(chatId: string): number;
148
+ /** Whether a turn is in flight in this chat. */
149
+ isRunning(chatId: string): boolean;
150
+ /**
151
+ * Drop a user's session and cached token after the server rejects it. The
152
+ * next turn mints a fresh token and opens a new socket, rather than retrying
153
+ * a credential the server has already refused.
154
+ */
155
+ invalidateSession(telegramUserId: string): void;
156
+ /** Close every socket. */
157
+ close(): void;
158
+ private ensureSession;
159
+ private ensureConversation;
160
+ private startTurn;
161
+ private resumeRunningTurns;
162
+ private handleFrame;
163
+ private locateConversation;
164
+ private deliver;
165
+ private finishTurn;
166
+ }
167
+ //# sourceMappingURL=turn-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-router.d.ts","sourceRoot":"","sources":["../src/turn-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAiB,MAAM,qBAAqB,CAAC;AAEhF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAM/E,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzD,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,iFAAiF;AACjF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9D;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,IAAI,CAAC;IAChB,UAAU,IAAI,IAAI,CAAC;IACnB,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACvC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wEAAwE;IACxE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,oEAAoE;IACpE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC9D;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,uEAAuE;IACvE,aAAa,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CAC7C;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,KAAK,YAAY,CAAC;AAEnB,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,GAAG,CACvB,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,SAAS,QAAQ,EAAE,KACrB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;CACvD;AAMD,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChF;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IACE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAwDD,sCAAsC;AACtC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,oFAAoF;AACpF,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,MAAM,CAaR;AAMD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAE5D,YAAY,OAAO,EAAE,iBAAiB,EAMrC;IAED;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA2BtD;IAED,8EAA8E;IAC9E,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAW5B;IAED;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAavC;IAED,mEAAmE;IACnE,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO7C;IAED,iEAAiE;IACjE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjC;IAED,gDAAgD;IAChD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAS9C;IAED,0BAA0B;IAC1B,KAAK,IAAI,IAAI,CAQZ;IAID,OAAO,CAAC,aAAa;YAyCP,kBAAkB;IA8BhC,OAAO,CAAC,SAAS;IAuBjB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,WAAW;IAiCnB,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,UAAU;CAanB"}