@dreb/telegram 1.16.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 (82) hide show
  1. package/README.md +91 -0
  2. package/dist/agent-bridge.d.ts +146 -0
  3. package/dist/agent-bridge.d.ts.map +1 -0
  4. package/dist/agent-bridge.js +466 -0
  5. package/dist/agent-bridge.js.map +1 -0
  6. package/dist/bot.d.ts +11 -0
  7. package/dist/bot.d.ts.map +1 -0
  8. package/dist/bot.js +112 -0
  9. package/dist/bot.js.map +1 -0
  10. package/dist/bridge-lifecycle.d.ts +17 -0
  11. package/dist/bridge-lifecycle.d.ts.map +1 -0
  12. package/dist/bridge-lifecycle.js +71 -0
  13. package/dist/bridge-lifecycle.js.map +1 -0
  14. package/dist/commands/agent.d.ts +11 -0
  15. package/dist/commands/agent.d.ts.map +1 -0
  16. package/dist/commands/agent.js +171 -0
  17. package/dist/commands/agent.js.map +1 -0
  18. package/dist/commands/buddy.d.ts +20 -0
  19. package/dist/commands/buddy.d.ts.map +1 -0
  20. package/dist/commands/buddy.js +84 -0
  21. package/dist/commands/buddy.js.map +1 -0
  22. package/dist/commands/core.d.ts +13 -0
  23. package/dist/commands/core.d.ts.map +1 -0
  24. package/dist/commands/core.js +107 -0
  25. package/dist/commands/core.js.map +1 -0
  26. package/dist/commands/index.d.ts +16 -0
  27. package/dist/commands/index.d.ts.map +1 -0
  28. package/dist/commands/index.js +132 -0
  29. package/dist/commands/index.js.map +1 -0
  30. package/dist/commands/refresh.d.ts +18 -0
  31. package/dist/commands/refresh.d.ts.map +1 -0
  32. package/dist/commands/refresh.js +55 -0
  33. package/dist/commands/refresh.js.map +1 -0
  34. package/dist/commands/sessions.d.ts +10 -0
  35. package/dist/commands/sessions.d.ts.map +1 -0
  36. package/dist/commands/sessions.js +125 -0
  37. package/dist/commands/sessions.js.map +1 -0
  38. package/dist/commands/skills.d.ts +10 -0
  39. package/dist/commands/skills.d.ts.map +1 -0
  40. package/dist/commands/skills.js +48 -0
  41. package/dist/commands/skills.js.map +1 -0
  42. package/dist/config.d.ts +30 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +77 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/handlers/buddy.d.ts +31 -0
  47. package/dist/handlers/buddy.d.ts.map +1 -0
  48. package/dist/handlers/buddy.js +126 -0
  49. package/dist/handlers/buddy.js.map +1 -0
  50. package/dist/handlers/events.d.ts +65 -0
  51. package/dist/handlers/events.d.ts.map +1 -0
  52. package/dist/handlers/events.js +381 -0
  53. package/dist/handlers/events.js.map +1 -0
  54. package/dist/handlers/file.d.ts +11 -0
  55. package/dist/handlers/file.d.ts.map +1 -0
  56. package/dist/handlers/file.js +138 -0
  57. package/dist/handlers/file.js.map +1 -0
  58. package/dist/handlers/message.d.ts +34 -0
  59. package/dist/handlers/message.d.ts.map +1 -0
  60. package/dist/handlers/message.js +262 -0
  61. package/dist/handlers/message.js.map +1 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +82 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/state.d.ts +11 -0
  67. package/dist/state.d.ts.map +1 -0
  68. package/dist/state.js +47 -0
  69. package/dist/state.js.map +1 -0
  70. package/dist/types.d.ts +50 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +5 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/util/files.d.ts +27 -0
  75. package/dist/util/files.d.ts.map +1 -0
  76. package/dist/util/files.js +75 -0
  77. package/dist/util/files.js.map +1 -0
  78. package/dist/util/telegram.d.ts +60 -0
  79. package/dist/util/telegram.d.ts.map +1 -0
  80. package/dist/util/telegram.js +192 -0
  81. package/dist/util/telegram.js.map +1 -0
  82. package/package.json +49 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * File handling utilities — temp dir management, telegram:send pattern.
3
+ */
4
+ /** Pattern for file send requests embedded in assistant text */
5
+ export declare const SEND_FILE_PATTERN: RegExp;
6
+ /**
7
+ * Ensure the upload directory exists.
8
+ */
9
+ export declare function ensureUploadDir(): string;
10
+ /**
11
+ * Save a buffer to the upload directory.
12
+ */
13
+ export declare function saveUpload(filename: string, data: Buffer): string;
14
+ /**
15
+ * Extract [[telegram:send:path]] markers from text.
16
+ * Returns [cleanedText, filePaths].
17
+ */
18
+ export declare function extractSendFiles(text: string): [string, string[]];
19
+ /**
20
+ * Clean up the upload directory. Skips cleanup if there are pending file
21
+ * batches (3-second debounce timers) to avoid deleting files that haven't
22
+ * been consumed yet.
23
+ */
24
+ export declare function cleanupUploads(): void;
25
+ export declare function setPendingBatches(count: number): void;
26
+ export declare function hasPendingBatches(): boolean;
27
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/util/files.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,QAAoC,CAAC;AAEnE;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAOjE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAerC;AAOD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAErD;AACD,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C","sourcesContent":["/**\n * File handling utilities — temp dir management, telegram:send pattern.\n */\n\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { log } from \"./telegram.js\";\n\nconst UPLOAD_DIR = join(tmpdir(), \"dreb-telegram-uploads\");\n\n/** Pattern for file send requests embedded in assistant text */\nexport const SEND_FILE_PATTERN = /\\[\\[telegram:send:([^\\]]+)\\]\\]/g;\n\n/**\n * Ensure the upload directory exists.\n */\nexport function ensureUploadDir(): string {\n\tif (!existsSync(UPLOAD_DIR)) {\n\t\tmkdirSync(UPLOAD_DIR, { recursive: true });\n\t}\n\treturn UPLOAD_DIR;\n}\n\n/**\n * Save a buffer to the upload directory.\n */\nexport function saveUpload(filename: string, data: Buffer): string {\n\tconst dir = ensureUploadDir();\n\tconst safe = basename(filename);\n\tconst path = join(dir, `${Date.now()}_${safe}`);\n\twriteFileSync(path, data);\n\treturn path;\n}\n\n/**\n * Extract [[telegram:send:path]] markers from text.\n * Returns [cleanedText, filePaths].\n */\nexport function extractSendFiles(text: string): [string, string[]] {\n\tconst paths: string[] = [];\n\tconst cleaned = text.replace(SEND_FILE_PATTERN, (_match, path) => {\n\t\tpaths.push(path.trim());\n\t\treturn \"\";\n\t});\n\treturn [cleaned.trim(), paths];\n}\n\n/**\n * Clean up the upload directory. Skips cleanup if there are pending file\n * batches (3-second debounce timers) to avoid deleting files that haven't\n * been consumed yet.\n */\nexport function cleanupUploads(): void {\n\t// Check if any file batches are still pending — their debounce timers\n\t// haven't fired yet, so their files haven't been consumed by the queue\n\tif (hasPendingBatches()) {\n\t\tlog(\"[FILES] Skipping cleanup — pending file batches\");\n\t\treturn;\n\t}\n\ttry {\n\t\tif (existsSync(UPLOAD_DIR)) {\n\t\t\trmSync(UPLOAD_DIR, { recursive: true, force: true });\n\t\t\tlog(\"[FILES] Cleaned up upload directory\");\n\t\t}\n\t} catch (e) {\n\t\tlog(`[FILES] Cleanup failed: ${e}`);\n\t}\n}\n\n/**\n * Track whether file batches are pending so cleanupUploads can skip\n * when files haven't been consumed yet.\n */\nlet _pendingBatchCount = 0;\nexport function setPendingBatches(count: number): void {\n\t_pendingBatchCount = count;\n}\nexport function hasPendingBatches(): boolean {\n\treturn _pendingBatchCount > 0;\n}\n"]}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * File handling utilities — temp dir management, telegram:send pattern.
3
+ */
4
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, join } from "node:path";
7
+ import { log } from "./telegram.js";
8
+ const UPLOAD_DIR = join(tmpdir(), "dreb-telegram-uploads");
9
+ /** Pattern for file send requests embedded in assistant text */
10
+ export const SEND_FILE_PATTERN = /\[\[telegram:send:([^\]]+)\]\]/g;
11
+ /**
12
+ * Ensure the upload directory exists.
13
+ */
14
+ export function ensureUploadDir() {
15
+ if (!existsSync(UPLOAD_DIR)) {
16
+ mkdirSync(UPLOAD_DIR, { recursive: true });
17
+ }
18
+ return UPLOAD_DIR;
19
+ }
20
+ /**
21
+ * Save a buffer to the upload directory.
22
+ */
23
+ export function saveUpload(filename, data) {
24
+ const dir = ensureUploadDir();
25
+ const safe = basename(filename);
26
+ const path = join(dir, `${Date.now()}_${safe}`);
27
+ writeFileSync(path, data);
28
+ return path;
29
+ }
30
+ /**
31
+ * Extract [[telegram:send:path]] markers from text.
32
+ * Returns [cleanedText, filePaths].
33
+ */
34
+ export function extractSendFiles(text) {
35
+ const paths = [];
36
+ const cleaned = text.replace(SEND_FILE_PATTERN, (_match, path) => {
37
+ paths.push(path.trim());
38
+ return "";
39
+ });
40
+ return [cleaned.trim(), paths];
41
+ }
42
+ /**
43
+ * Clean up the upload directory. Skips cleanup if there are pending file
44
+ * batches (3-second debounce timers) to avoid deleting files that haven't
45
+ * been consumed yet.
46
+ */
47
+ export function cleanupUploads() {
48
+ // Check if any file batches are still pending — their debounce timers
49
+ // haven't fired yet, so their files haven't been consumed by the queue
50
+ if (hasPendingBatches()) {
51
+ log("[FILES] Skipping cleanup — pending file batches");
52
+ return;
53
+ }
54
+ try {
55
+ if (existsSync(UPLOAD_DIR)) {
56
+ rmSync(UPLOAD_DIR, { recursive: true, force: true });
57
+ log("[FILES] Cleaned up upload directory");
58
+ }
59
+ }
60
+ catch (e) {
61
+ log(`[FILES] Cleanup failed: ${e}`);
62
+ }
63
+ }
64
+ /**
65
+ * Track whether file batches are pending so cleanupUploads can skip
66
+ * when files haven't been consumed yet.
67
+ */
68
+ let _pendingBatchCount = 0;
69
+ export function setPendingBatches(count) {
70
+ _pendingBatchCount = count;
71
+ }
72
+ export function hasPendingBatches() {
73
+ return _pendingBatchCount > 0;
74
+ }
75
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/util/files.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEpC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,uBAAuB,CAAC,CAAC;AAE3D,gEAAgE;AAChE,MAAM,CAAC,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AAEnE;;GAEG;AACH,MAAM,UAAU,eAAe,GAAW;IACzC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,UAAU,CAAC;AAAA,CAClB;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,IAAY,EAAU;IAClE,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IAChD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAsB;IAClE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QACjE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACxB,OAAO,EAAE,CAAC;IAAA,CACV,CAAC,CAAC;IACH,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;AAAA,CAC/B;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,GAAS;IACtC,wEAAsE;IACtE,uEAAuE;IACvE,IAAI,iBAAiB,EAAE,EAAE,CAAC;QACzB,GAAG,CAAC,mDAAiD,CAAC,CAAC;QACvD,OAAO;IACR,CAAC;IACD,IAAI,CAAC;QACJ,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,qCAAqC,CAAC,CAAC;QAC5C,CAAC;IACF,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,GAAG,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,IAAI,kBAAkB,GAAG,CAAC,CAAC;AAC3B,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAQ;IACtD,kBAAkB,GAAG,KAAK,CAAC;AAAA,CAC3B;AACD,MAAM,UAAU,iBAAiB,GAAY;IAC5C,OAAO,kBAAkB,GAAG,CAAC,CAAC;AAAA,CAC9B","sourcesContent":["/**\n * File handling utilities — temp dir management, telegram:send pattern.\n */\n\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { log } from \"./telegram.js\";\n\nconst UPLOAD_DIR = join(tmpdir(), \"dreb-telegram-uploads\");\n\n/** Pattern for file send requests embedded in assistant text */\nexport const SEND_FILE_PATTERN = /\\[\\[telegram:send:([^\\]]+)\\]\\]/g;\n\n/**\n * Ensure the upload directory exists.\n */\nexport function ensureUploadDir(): string {\n\tif (!existsSync(UPLOAD_DIR)) {\n\t\tmkdirSync(UPLOAD_DIR, { recursive: true });\n\t}\n\treturn UPLOAD_DIR;\n}\n\n/**\n * Save a buffer to the upload directory.\n */\nexport function saveUpload(filename: string, data: Buffer): string {\n\tconst dir = ensureUploadDir();\n\tconst safe = basename(filename);\n\tconst path = join(dir, `${Date.now()}_${safe}`);\n\twriteFileSync(path, data);\n\treturn path;\n}\n\n/**\n * Extract [[telegram:send:path]] markers from text.\n * Returns [cleanedText, filePaths].\n */\nexport function extractSendFiles(text: string): [string, string[]] {\n\tconst paths: string[] = [];\n\tconst cleaned = text.replace(SEND_FILE_PATTERN, (_match, path) => {\n\t\tpaths.push(path.trim());\n\t\treturn \"\";\n\t});\n\treturn [cleaned.trim(), paths];\n}\n\n/**\n * Clean up the upload directory. Skips cleanup if there are pending file\n * batches (3-second debounce timers) to avoid deleting files that haven't\n * been consumed yet.\n */\nexport function cleanupUploads(): void {\n\t// Check if any file batches are still pending — their debounce timers\n\t// haven't fired yet, so their files haven't been consumed by the queue\n\tif (hasPendingBatches()) {\n\t\tlog(\"[FILES] Skipping cleanup — pending file batches\");\n\t\treturn;\n\t}\n\ttry {\n\t\tif (existsSync(UPLOAD_DIR)) {\n\t\t\trmSync(UPLOAD_DIR, { recursive: true, force: true });\n\t\t\tlog(\"[FILES] Cleaned up upload directory\");\n\t\t}\n\t} catch (e) {\n\t\tlog(`[FILES] Cleanup failed: ${e}`);\n\t}\n}\n\n/**\n * Track whether file batches are pending so cleanupUploads can skip\n * when files haven't been consumed yet.\n */\nlet _pendingBatchCount = 0;\nexport function setPendingBatches(count: number): void {\n\t_pendingBatchCount = count;\n}\nexport function hasPendingBatches(): boolean {\n\treturn _pendingBatchCount > 0;\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.
3
+ */
4
+ import type { Api } from "grammy";
5
+ /**
6
+ * Wrap a promise with a timeout. Rejects with an error if the promise
7
+ * doesn't settle within `ms` milliseconds. Used to prevent Telegram API
8
+ * calls from hanging the event chain indefinitely.
9
+ *
10
+ * The timer is cleared when the promise settles to avoid accumulating
11
+ * stale timers in the Node.js timer heap during heavy message runs.
12
+ */
13
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T>;
14
+ /**
15
+ * Send a message, falling back to plain text if Markdown fails.
16
+ */
17
+ export declare function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number>;
18
+ /**
19
+ * Send a long message, splitting at newline boundaries.
20
+ * Stops on first chunk failure to avoid resending already-delivered chunks
21
+ * on retry. Returns the remaining (undelivered) text, or empty string if
22
+ * everything was delivered.
23
+ */
24
+ export declare function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string>;
25
+ /**
26
+ * Edit a message safely, falling back to plain text.
27
+ */
28
+ export declare function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean>;
29
+ /**
30
+ * Delete a message, ignoring errors.
31
+ */
32
+ export declare function safeDelete(api: Api, chatId: number, messageId: number): Promise<void>;
33
+ /**
34
+ * Truncate text to fit Telegram's limit.
35
+ */
36
+ export declare function truncate(text: string, maxLen?: number): string;
37
+ /**
38
+ * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.
39
+ * Minimum 2 seconds between edits to the same message.
40
+ */
41
+ export declare class DebouncedEditor {
42
+ private api;
43
+ private pending;
44
+ private lastEdit;
45
+ private readonly minInterval;
46
+ constructor(api: Api);
47
+ /**
48
+ * Schedule an edit. If another edit comes in before the debounce fires,
49
+ * the previous one is replaced.
50
+ */
51
+ edit(chatId: number, messageId: number, text: string): void;
52
+ /**
53
+ * Force flush a pending edit immediately (e.g., before deleting the message).
54
+ */
55
+ flush(chatId: number, messageId: number): Promise<void>;
56
+ /** Cancel all pending edits. */
57
+ clear(): void;
58
+ }
59
+ export declare function log(msg: string): void;
60
+ //# sourceMappingURL=telegram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAIlC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAU1E;AAID;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC1G;AAkBD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1G;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa1G;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3F;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAc,GAAG,MAAM,CAGnE;AAED;;;GAGG;AACH,qBAAa,eAAe;IAKf,OAAO,CAAC,GAAG;IAJvB,OAAO,CAAC,OAAO,CAAkF;IACjG,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,YAAoB,GAAG,EAAE,GAAG,EAAI;IAEhC;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAgB1D;IAED;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS5D;IAED,gCAAgC;IAChC,KAAK,IAAI,IAAI,CAGZ;CACD;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAErC","sourcesContent":["/**\n * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.\n */\n\nimport type { Api } from \"grammy\";\n\nconst SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)\n\n/**\n * Wrap a promise with a timeout. Rejects with an error if the promise\n * doesn't settle within `ms` milliseconds. Used to prevent Telegram API\n * calls from hanging the event chain indefinitely.\n *\n * The timer is cleared when the promise settles to avoid accumulating\n * stale timers in the Node.js timer heap during heavy message runs.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout>;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);\n\t});\n\t// Prevent unhandled rejection from the slow promise if the timeout wins.\n\t// Without this, a late rejection after timeout becomes an unhandled rejection\n\t// which crashes the process in Node.js >= 15.\n\tpromise.catch(() => {});\n\treturn Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));\n}\n\nconst API_TIMEOUT = 15_000; // 15s per Telegram API call\n\n/**\n * Send a message, falling back to plain text if Markdown fails.\n */\nexport async function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number> {\n\t// Truncate to Telegram's limit — callers sending long content should use sendLong instead\n\tif (text.length > SAFE_LENGTH) text = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tconst msg = await withTimeout(\n\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t}),\n\t\t\tAPI_TIMEOUT,\n\t\t);\n\t\treturn msg.message_id;\n\t} catch (e) {\n\t\t// Only retry as plain text for Markdown parse errors (Telegram 400).\n\t\t// For timeouts/network errors, the original request may still be in-flight —\n\t\t// retrying would risk delivering duplicate messages. Return 0 and let\n\t\t// the outbox retry loop handle it.\n\t\tif (!isTelegramParseError(e)) {\n\t\t\tlog(`[WARN] Failed to send message: ${e}`);\n\t\t\treturn 0;\n\t\t}\n\t\ttry {\n\t\t\tconst msg = await withTimeout(\n\t\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t\t}),\n\t\t\t\tAPI_TIMEOUT,\n\t\t\t);\n\t\t\treturn msg.message_id;\n\t\t} catch (e2) {\n\t\t\tlog(`[WARN] Failed to send message (plain text fallback): ${e2}`);\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\n/**\n * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).\n * These are safe to retry as plain text because the original request definitively\n * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe\n * to retry immediately because the in-flight request may still succeed.\n */\nfunction isTelegramParseError(e: unknown): boolean {\n\tif (!e || typeof e !== \"object\") return false;\n\t// grammy HttpError has error_code\n\tconst code = (e as any).error_code ?? (e as any).status ?? (e as any).statusCode;\n\tif (code === 400) return true;\n\t// Fallback: check message for common Telegram parse error text\n\tconst msg = (e as any).message ?? String(e);\n\treturn typeof msg === \"string\" && msg.includes(\"can't parse entities\");\n}\n\n/**\n * Send a long message, splitting at newline boundaries.\n * Stops on first chunk failure to avoid resending already-delivered chunks\n * on retry. Returns the remaining (undelivered) text, or empty string if\n * everything was delivered.\n */\nexport async function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string> {\n\twhile (text) {\n\t\tif (text.length <= SAFE_LENGTH) {\n\t\t\tconst msgId = await safeSend(api, chatId, text, replyToId);\n\t\t\treturn msgId === 0 ? text : \"\";\n\t\t}\n\t\tlet splitAt = text.lastIndexOf(\"\\n\", SAFE_LENGTH);\n\t\tif (splitAt < 2000) splitAt = SAFE_LENGTH;\n\t\tconst msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);\n\t\tif (msgId === 0) return text; // Stop — return full remaining text including this failed chunk\n\t\ttext = text.slice(splitAt).replace(/^\\n+/, \"\");\n\t\treplyToId = undefined; // Only reply to the first chunk\n\t}\n\treturn \"\";\n}\n\n/**\n * Edit a message safely, falling back to plain text.\n */\nexport async function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean> {\n\ttext = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tawait withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: \"Markdown\" }), API_TIMEOUT);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tawait withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Delete a message, ignoring errors.\n */\nexport async function safeDelete(api: Api, chatId: number, messageId: number): Promise<void> {\n\ttry {\n\t\tawait withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);\n\t} catch {\n\t\t// Ignore — message may already be deleted or too old\n\t}\n}\n\n/**\n * Truncate text to fit Telegram's limit.\n */\nexport function truncate(text: string, maxLen = SAFE_LENGTH): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.slice(0, maxLen - 20)}\\n\\n_(truncated)_`;\n}\n\n/**\n * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.\n * Minimum 2 seconds between edits to the same message.\n */\nexport class DebouncedEditor {\n\tprivate pending: Map<string, { text: string; timer: ReturnType<typeof setTimeout> }> = new Map();\n\tprivate lastEdit: Map<string, number> = new Map();\n\tprivate readonly minInterval = 2000; // 2s between edits\n\n\tconstructor(private api: Api) {}\n\n\t/**\n\t * Schedule an edit. If another edit comes in before the debounce fires,\n\t * the previous one is replaced.\n\t */\n\tedit(chatId: number, messageId: number, text: string): void {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) clearTimeout(existing.timer);\n\n\t\tconst lastTime = this.lastEdit.get(key) || 0;\n\t\tconst elapsed = Date.now() - lastTime;\n\t\tconst delay = Math.max(0, this.minInterval - elapsed);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tvoid safeEdit(this.api, chatId, messageId, text);\n\t\t}, delay);\n\n\t\tthis.pending.set(key, { text, timer });\n\t}\n\n\t/**\n\t * Force flush a pending edit immediately (e.g., before deleting the message).\n\t */\n\tasync flush(chatId: number, messageId: number): Promise<void> {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing.timer);\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tawait safeEdit(this.api, chatId, messageId, existing.text);\n\t\t}\n\t}\n\n\t/** Cancel all pending edits. */\n\tclear(): void {\n\t\tfor (const { timer } of this.pending.values()) clearTimeout(timer);\n\t\tthis.pending.clear();\n\t}\n}\n\nexport function log(msg: string): void {\n\tconsole.error(msg);\n}\n"]}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.
3
+ */
4
+ const SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)
5
+ /**
6
+ * Wrap a promise with a timeout. Rejects with an error if the promise
7
+ * doesn't settle within `ms` milliseconds. Used to prevent Telegram API
8
+ * calls from hanging the event chain indefinitely.
9
+ *
10
+ * The timer is cleared when the promise settles to avoid accumulating
11
+ * stale timers in the Node.js timer heap during heavy message runs.
12
+ */
13
+ export function withTimeout(promise, ms) {
14
+ let timer;
15
+ const timeout = new Promise((_, reject) => {
16
+ timer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);
17
+ });
18
+ // Prevent unhandled rejection from the slow promise if the timeout wins.
19
+ // Without this, a late rejection after timeout becomes an unhandled rejection
20
+ // which crashes the process in Node.js >= 15.
21
+ promise.catch(() => { });
22
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
23
+ }
24
+ const API_TIMEOUT = 15_000; // 15s per Telegram API call
25
+ /**
26
+ * Send a message, falling back to plain text if Markdown fails.
27
+ */
28
+ export async function safeSend(api, chatId, text, replyToId) {
29
+ // Truncate to Telegram's limit — callers sending long content should use sendLong instead
30
+ if (text.length > SAFE_LENGTH)
31
+ text = truncate(text, SAFE_LENGTH);
32
+ try {
33
+ const msg = await withTimeout(api.sendMessage(chatId, text, {
34
+ parse_mode: "Markdown",
35
+ ...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),
36
+ }), API_TIMEOUT);
37
+ return msg.message_id;
38
+ }
39
+ catch (e) {
40
+ // Only retry as plain text for Markdown parse errors (Telegram 400).
41
+ // For timeouts/network errors, the original request may still be in-flight —
42
+ // retrying would risk delivering duplicate messages. Return 0 and let
43
+ // the outbox retry loop handle it.
44
+ if (!isTelegramParseError(e)) {
45
+ log(`[WARN] Failed to send message: ${e}`);
46
+ return 0;
47
+ }
48
+ try {
49
+ const msg = await withTimeout(api.sendMessage(chatId, text, {
50
+ ...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),
51
+ }), API_TIMEOUT);
52
+ return msg.message_id;
53
+ }
54
+ catch (e2) {
55
+ log(`[WARN] Failed to send message (plain text fallback): ${e2}`);
56
+ return 0;
57
+ }
58
+ }
59
+ }
60
+ /**
61
+ * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).
62
+ * These are safe to retry as plain text because the original request definitively
63
+ * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe
64
+ * to retry immediately because the in-flight request may still succeed.
65
+ */
66
+ function isTelegramParseError(e) {
67
+ if (!e || typeof e !== "object")
68
+ return false;
69
+ // grammy HttpError has error_code
70
+ const code = e.error_code ?? e.status ?? e.statusCode;
71
+ if (code === 400)
72
+ return true;
73
+ // Fallback: check message for common Telegram parse error text
74
+ const msg = e.message ?? String(e);
75
+ return typeof msg === "string" && msg.includes("can't parse entities");
76
+ }
77
+ /**
78
+ * Send a long message, splitting at newline boundaries.
79
+ * Stops on first chunk failure to avoid resending already-delivered chunks
80
+ * on retry. Returns the remaining (undelivered) text, or empty string if
81
+ * everything was delivered.
82
+ */
83
+ export async function sendLong(api, chatId, text, replyToId) {
84
+ while (text) {
85
+ if (text.length <= SAFE_LENGTH) {
86
+ const msgId = await safeSend(api, chatId, text, replyToId);
87
+ return msgId === 0 ? text : "";
88
+ }
89
+ let splitAt = text.lastIndexOf("\n", SAFE_LENGTH);
90
+ if (splitAt < 2000)
91
+ splitAt = SAFE_LENGTH;
92
+ const msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);
93
+ if (msgId === 0)
94
+ return text; // Stop — return full remaining text including this failed chunk
95
+ text = text.slice(splitAt).replace(/^\n+/, "");
96
+ replyToId = undefined; // Only reply to the first chunk
97
+ }
98
+ return "";
99
+ }
100
+ /**
101
+ * Edit a message safely, falling back to plain text.
102
+ */
103
+ export async function safeEdit(api, chatId, messageId, text) {
104
+ text = truncate(text, SAFE_LENGTH);
105
+ try {
106
+ await withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: "Markdown" }), API_TIMEOUT);
107
+ return true;
108
+ }
109
+ catch {
110
+ try {
111
+ await withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Delete a message, ignoring errors.
121
+ */
122
+ export async function safeDelete(api, chatId, messageId) {
123
+ try {
124
+ await withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);
125
+ }
126
+ catch {
127
+ // Ignore — message may already be deleted or too old
128
+ }
129
+ }
130
+ /**
131
+ * Truncate text to fit Telegram's limit.
132
+ */
133
+ export function truncate(text, maxLen = SAFE_LENGTH) {
134
+ if (text.length <= maxLen)
135
+ return text;
136
+ return `${text.slice(0, maxLen - 20)}\n\n_(truncated)_`;
137
+ }
138
+ /**
139
+ * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.
140
+ * Minimum 2 seconds between edits to the same message.
141
+ */
142
+ export class DebouncedEditor {
143
+ api;
144
+ pending = new Map();
145
+ lastEdit = new Map();
146
+ minInterval = 2000; // 2s between edits
147
+ constructor(api) {
148
+ this.api = api;
149
+ }
150
+ /**
151
+ * Schedule an edit. If another edit comes in before the debounce fires,
152
+ * the previous one is replaced.
153
+ */
154
+ edit(chatId, messageId, text) {
155
+ const key = `${chatId}:${messageId}`;
156
+ const existing = this.pending.get(key);
157
+ if (existing)
158
+ clearTimeout(existing.timer);
159
+ const lastTime = this.lastEdit.get(key) || 0;
160
+ const elapsed = Date.now() - lastTime;
161
+ const delay = Math.max(0, this.minInterval - elapsed);
162
+ const timer = setTimeout(() => {
163
+ this.pending.delete(key);
164
+ this.lastEdit.set(key, Date.now());
165
+ void safeEdit(this.api, chatId, messageId, text);
166
+ }, delay);
167
+ this.pending.set(key, { text, timer });
168
+ }
169
+ /**
170
+ * Force flush a pending edit immediately (e.g., before deleting the message).
171
+ */
172
+ async flush(chatId, messageId) {
173
+ const key = `${chatId}:${messageId}`;
174
+ const existing = this.pending.get(key);
175
+ if (existing) {
176
+ clearTimeout(existing.timer);
177
+ this.pending.delete(key);
178
+ this.lastEdit.set(key, Date.now());
179
+ await safeEdit(this.api, chatId, messageId, existing.text);
180
+ }
181
+ }
182
+ /** Cancel all pending edits. */
183
+ clear() {
184
+ for (const { timer } of this.pending.values())
185
+ clearTimeout(timer);
186
+ this.pending.clear();
187
+ }
188
+ }
189
+ export function log(msg) {
190
+ console.error(msg);
191
+ }
192
+ //# sourceMappingURL=telegram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Telegram message utilities — splitting, markdown fallback, rate-limit debounce.\n */\n\nimport type { Api } from \"grammy\";\n\nconst SAFE_LENGTH = 4000; // Leave room for markdown overhead (Telegram max is 4096)\n\n/**\n * Wrap a promise with a timeout. Rejects with an error if the promise\n * doesn't settle within `ms` milliseconds. Used to prevent Telegram API\n * calls from hanging the event chain indefinitely.\n *\n * The timer is cleared when the promise settles to avoid accumulating\n * stale timers in the Node.js timer heap during heavy message runs.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout>;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`Telegram API timeout after ${ms}ms`)), ms);\n\t});\n\t// Prevent unhandled rejection from the slow promise if the timeout wins.\n\t// Without this, a late rejection after timeout becomes an unhandled rejection\n\t// which crashes the process in Node.js >= 15.\n\tpromise.catch(() => {});\n\treturn Promise.race([promise, timeout]).finally(() => clearTimeout(timer!));\n}\n\nconst API_TIMEOUT = 15_000; // 15s per Telegram API call\n\n/**\n * Send a message, falling back to plain text if Markdown fails.\n */\nexport async function safeSend(api: Api, chatId: number, text: string, replyToId?: number): Promise<number> {\n\t// Truncate to Telegram's limit — callers sending long content should use sendLong instead\n\tif (text.length > SAFE_LENGTH) text = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tconst msg = await withTimeout(\n\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t}),\n\t\t\tAPI_TIMEOUT,\n\t\t);\n\t\treturn msg.message_id;\n\t} catch (e) {\n\t\t// Only retry as plain text for Markdown parse errors (Telegram 400).\n\t\t// For timeouts/network errors, the original request may still be in-flight —\n\t\t// retrying would risk delivering duplicate messages. Return 0 and let\n\t\t// the outbox retry loop handle it.\n\t\tif (!isTelegramParseError(e)) {\n\t\t\tlog(`[WARN] Failed to send message: ${e}`);\n\t\t\treturn 0;\n\t\t}\n\t\ttry {\n\t\t\tconst msg = await withTimeout(\n\t\t\t\tapi.sendMessage(chatId, text, {\n\t\t\t\t\t...(replyToId ? { reply_parameters: { message_id: replyToId } } : {}),\n\t\t\t\t}),\n\t\t\t\tAPI_TIMEOUT,\n\t\t\t);\n\t\t\treturn msg.message_id;\n\t\t} catch (e2) {\n\t\t\tlog(`[WARN] Failed to send message (plain text fallback): ${e2}`);\n\t\t\treturn 0;\n\t\t}\n\t}\n}\n\n/**\n * Check if an error is a Telegram API parse error (HTTP 400 for bad Markdown).\n * These are safe to retry as plain text because the original request definitively\n * failed — Telegram won't deliver it. Timeouts and network errors are NOT safe\n * to retry immediately because the in-flight request may still succeed.\n */\nfunction isTelegramParseError(e: unknown): boolean {\n\tif (!e || typeof e !== \"object\") return false;\n\t// grammy HttpError has error_code\n\tconst code = (e as any).error_code ?? (e as any).status ?? (e as any).statusCode;\n\tif (code === 400) return true;\n\t// Fallback: check message for common Telegram parse error text\n\tconst msg = (e as any).message ?? String(e);\n\treturn typeof msg === \"string\" && msg.includes(\"can't parse entities\");\n}\n\n/**\n * Send a long message, splitting at newline boundaries.\n * Stops on first chunk failure to avoid resending already-delivered chunks\n * on retry. Returns the remaining (undelivered) text, or empty string if\n * everything was delivered.\n */\nexport async function sendLong(api: Api, chatId: number, text: string, replyToId?: number): Promise<string> {\n\twhile (text) {\n\t\tif (text.length <= SAFE_LENGTH) {\n\t\t\tconst msgId = await safeSend(api, chatId, text, replyToId);\n\t\t\treturn msgId === 0 ? text : \"\";\n\t\t}\n\t\tlet splitAt = text.lastIndexOf(\"\\n\", SAFE_LENGTH);\n\t\tif (splitAt < 2000) splitAt = SAFE_LENGTH;\n\t\tconst msgId = await safeSend(api, chatId, text.slice(0, splitAt), replyToId);\n\t\tif (msgId === 0) return text; // Stop — return full remaining text including this failed chunk\n\t\ttext = text.slice(splitAt).replace(/^\\n+/, \"\");\n\t\treplyToId = undefined; // Only reply to the first chunk\n\t}\n\treturn \"\";\n}\n\n/**\n * Edit a message safely, falling back to plain text.\n */\nexport async function safeEdit(api: Api, chatId: number, messageId: number, text: string): Promise<boolean> {\n\ttext = truncate(text, SAFE_LENGTH);\n\ttry {\n\t\tawait withTimeout(api.editMessageText(chatId, messageId, text, { parse_mode: \"Markdown\" }), API_TIMEOUT);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tawait withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Delete a message, ignoring errors.\n */\nexport async function safeDelete(api: Api, chatId: number, messageId: number): Promise<void> {\n\ttry {\n\t\tawait withTimeout(api.deleteMessage(chatId, messageId), API_TIMEOUT);\n\t} catch {\n\t\t// Ignore — message may already be deleted or too old\n\t}\n}\n\n/**\n * Truncate text to fit Telegram's limit.\n */\nexport function truncate(text: string, maxLen = SAFE_LENGTH): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.slice(0, maxLen - 20)}\\n\\n_(truncated)_`;\n}\n\n/**\n * Rate-limited message editor — debounces rapid edits to avoid Telegram 429s.\n * Minimum 2 seconds between edits to the same message.\n */\nexport class DebouncedEditor {\n\tprivate pending: Map<string, { text: string; timer: ReturnType<typeof setTimeout> }> = new Map();\n\tprivate lastEdit: Map<string, number> = new Map();\n\tprivate readonly minInterval = 2000; // 2s between edits\n\n\tconstructor(private api: Api) {}\n\n\t/**\n\t * Schedule an edit. If another edit comes in before the debounce fires,\n\t * the previous one is replaced.\n\t */\n\tedit(chatId: number, messageId: number, text: string): void {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) clearTimeout(existing.timer);\n\n\t\tconst lastTime = this.lastEdit.get(key) || 0;\n\t\tconst elapsed = Date.now() - lastTime;\n\t\tconst delay = Math.max(0, this.minInterval - elapsed);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tvoid safeEdit(this.api, chatId, messageId, text);\n\t\t}, delay);\n\n\t\tthis.pending.set(key, { text, timer });\n\t}\n\n\t/**\n\t * Force flush a pending edit immediately (e.g., before deleting the message).\n\t */\n\tasync flush(chatId: number, messageId: number): Promise<void> {\n\t\tconst key = `${chatId}:${messageId}`;\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing.timer);\n\t\t\tthis.pending.delete(key);\n\t\t\tthis.lastEdit.set(key, Date.now());\n\t\t\tawait safeEdit(this.api, chatId, messageId, existing.text);\n\t\t}\n\t}\n\n\t/** Cancel all pending edits. */\n\tclear(): void {\n\t\tfor (const { timer } of this.pending.values()) clearTimeout(timer);\n\t\tthis.pending.clear();\n\t}\n}\n\nexport function log(msg: string): void {\n\tconsole.error(msg);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@dreb/telegram",
3
+ "version": "1.16.0",
4
+ "description": "Telegram bot frontend for dreb coding agent",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "dreb-telegram": "./dist/index.js"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "scripts": {
26
+ "clean": "shx rm -rf dist",
27
+ "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/index.js",
28
+ "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
29
+ "test": "vitest --run",
30
+ "prepublishOnly": "npm run clean && npm run build"
31
+ },
32
+ "dependencies": {
33
+ "@dreb/coding-agent": "^1.0.0",
34
+ "grammy": "^1.35.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.3.0",
38
+ "typescript": "^5.9.2",
39
+ "vitest": "^3.2.4"
40
+ },
41
+ "engines": {
42
+ "node": ">=20.0.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/aebrer/dreb.git",
47
+ "directory": "packages/telegram"
48
+ }
49
+ }