@dreb/telegram 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge-lifecycle.d.ts.map +1 -1
- package/dist/bridge-lifecycle.js +2 -3
- package/dist/bridge-lifecycle.js.map +1 -1
- package/dist/commands/buddy.d.ts.map +1 -1
- package/dist/commands/buddy.js +1 -0
- package/dist/commands/buddy.js.map +1 -1
- package/dist/commands/core.d.ts.map +1 -1
- package/dist/commands/core.js +6 -2
- package/dist/commands/core.js.map +1 -1
- package/dist/util/telegram.d.ts.map +1 -1
- package/dist/util/telegram.js +2 -0
- package/dist/util/telegram.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-lifecycle.d.ts","sourceRoot":"","sources":["../src/bridge-lifecycle.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;GAGG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CA2B7F;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"bridge-lifecycle.d.ts","sourceRoot":"","sources":["../src/bridge-lifecycle.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;GAGG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CA2B7F;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAkDxG","sourcesContent":["/**\n * Bridge lifecycle helpers — extracted to avoid circular imports between bot.ts and commands.\n */\n\nimport { AgentBridge } from \"./agent-bridge.js\";\nimport type { Config } from \"./config.js\";\nimport type { UserState } from \"./types.js\";\n\n/**\n * Ensure the user has an active agent bridge, starting one if needed.\n * Does NOT handle session selection — that's up to the caller.\n */\nexport async function ensureBridge(config: Config, userState: UserState): Promise<AgentBridge> {\n\tif (!userState.bridge || !userState.bridge.isAlive) {\n\t\t// Use effectiveCwd if set — preserves custom cwd across bridge crashes\n\t\tconst effectiveConfig =\n\t\t\tuserState.effectiveCwd && userState.effectiveCwd !== config.workingDir\n\t\t\t\t? { ...config, workingDir: userState.effectiveCwd }\n\t\t\t\t: config;\n\t\tconst bridge = new AgentBridge(effectiveConfig);\n\t\tawait bridge.start();\n\t\tuserState.bridge = bridge;\n\n\t\t// Wire up background agent tracking\n\t\tbridge.onEvent((event: any) => {\n\t\t\tif (event.type === \"background_agent_start\") {\n\t\t\t\tuserState.backgroundAgents.set(event.agentId, {\n\t\t\t\t\tagentId: event.agentId,\n\t\t\t\t\tagentType: event.agentType,\n\t\t\t\t\ttaskSummary: event.taskSummary,\n\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t});\n\t\t\t} else if (event.type === \"background_agent_end\") {\n\t\t\t\tuserState.backgroundAgents.delete(event.agentId);\n\t\t\t}\n\t\t});\n\t}\n\n\treturn userState.bridge;\n}\n\n/**\n * Ensure bridge is alive AND a session is selected.\n * Used by message/file handlers and skill commands before prompting.\n */\nexport async function ensureBridgeWithSession(config: Config, userState: UserState): Promise<AgentBridge> {\n\t// Handle new session — always kill and recreate the bridge for clean state.\n\t// cmdNew always sets newSessionCwd to a concrete string (never null).\n\tif (userState.newSessionFlag) {\n\t\tconst cwd = userState.newSessionCwd!;\n\t\tuserState.newSessionFlag = false;\n\t\tuserState.newSessionCwd = null;\n\n\t\t// Kill existing bridge and start a new one with the resolved cwd\n\t\tif (userState.bridge?.isAlive) {\n\t\t\tawait userState.bridge.stop();\n\t\t}\n\t\tuserState.bridge = null;\n\n\t\tconst customConfig = { ...config, workingDir: cwd };\n\t\t// Set effectiveCwd BEFORE ensureBridge so the stale-cwd override\n\t\t// in ensureBridge doesn't clobber the resolved directory\n\t\tuserState.effectiveCwd = cwd;\n\t\tconst bridge = await ensureBridge(customConfig, userState);\n\t\tawait bridge.newSession();\n\t\treturn bridge;\n\t}\n\n\tconst hadBridge = !!userState.bridge?.isAlive;\n\tconst bridge = await ensureBridge(config, userState);\n\tconst freshBridge = !hadBridge;\n\n\t// Track effective cwd (default from config on first bridge creation)\n\tif (!userState.effectiveCwd) {\n\t\tuserState.effectiveCwd = config.workingDir;\n\t}\n\n\t// No session yet — try to resume latest\n\tif (!bridge.sessionId) {\n\t\tawait bridge.resumeLatest();\n\t}\n\n\t// Check for model fallback warning on fresh bridge (e.g. after crash)\n\tif (freshBridge) {\n\t\ttry {\n\t\t\tconst state = await bridge.getState();\n\t\t\tif (state?.modelFallbackMessage) {\n\t\t\t\tuserState.pendingModelFallbackWarning = state.modelFallbackMessage;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Non-critical — the warning is best-effort\n\t\t}\n\t}\n\n\treturn bridge;\n}\n"]}
|
package/dist/bridge-lifecycle.js
CHANGED
|
@@ -38,10 +38,9 @@ export async function ensureBridge(config, userState) {
|
|
|
38
38
|
*/
|
|
39
39
|
export async function ensureBridgeWithSession(config, userState) {
|
|
40
40
|
// Handle new session — always kill and recreate the bridge for clean state.
|
|
41
|
-
//
|
|
42
|
-
// For /new (bare): preserves the current effectiveCwd.
|
|
41
|
+
// cmdNew always sets newSessionCwd to a concrete string (never null).
|
|
43
42
|
if (userState.newSessionFlag) {
|
|
44
|
-
const cwd = userState.newSessionCwd
|
|
43
|
+
const cwd = userState.newSessionCwd;
|
|
45
44
|
userState.newSessionFlag = false;
|
|
46
45
|
userState.newSessionCwd = null;
|
|
47
46
|
// Kill existing bridge and start a new one with the resolved cwd
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-lifecycle.js","sourceRoot":"","sources":["../src/bridge-lifecycle.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,SAAoB,EAAwB;IAC9F,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpD,yEAAuE;QACvE,MAAM,eAAe,GACpB,SAAS,CAAC,YAAY,IAAI,SAAS,CAAC,YAAY,KAAK,MAAM,CAAC,UAAU;YACrE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,YAAY,EAAE;YACnD,CAAC,CAAC,MAAM,CAAC;QACX,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,eAAe,CAAC,CAAC;QAChD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC;QAE1B,oCAAoC;QACpC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB,EAAE,CAAC;gBAC7C,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE;oBAC7C,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACrB,CAAC,CAAC;YACJ,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;gBAClD,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAClD,CAAC;QAAA,CACD,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC;AAAA,CACxB;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,SAAoB,EAAwB;IACzG,8EAA4E;IAC5E,
|
|
1
|
+
{"version":3,"file":"bridge-lifecycle.js","sourceRoot":"","sources":["../src/bridge-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bridge lifecycle helpers — extracted to avoid circular imports between bot.ts and commands.\n */\n\nimport { AgentBridge } from \"./agent-bridge.js\";\nimport type { Config } from \"./config.js\";\nimport type { UserState } from \"./types.js\";\n\n/**\n * Ensure the user has an active agent bridge, starting one if needed.\n * Does NOT handle session selection — that's up to the caller.\n */\nexport async function ensureBridge(config: Config, userState: UserState): Promise<AgentBridge> {\n\tif (!userState.bridge || !userState.bridge.isAlive) {\n\t\t// Use effectiveCwd if set — preserves custom cwd across bridge crashes\n\t\tconst effectiveConfig =\n\t\t\tuserState.effectiveCwd && userState.effectiveCwd !== config.workingDir\n\t\t\t\t? { ...config, workingDir: userState.effectiveCwd }\n\t\t\t\t: config;\n\t\tconst bridge = new AgentBridge(effectiveConfig);\n\t\tawait bridge.start();\n\t\tuserState.bridge = bridge;\n\n\t\t// Wire up background agent tracking\n\t\tbridge.onEvent((event: any) => {\n\t\t\tif (event.type === \"background_agent_start\") {\n\t\t\t\tuserState.backgroundAgents.set(event.agentId, {\n\t\t\t\t\tagentId: event.agentId,\n\t\t\t\t\tagentType: event.agentType,\n\t\t\t\t\ttaskSummary: event.taskSummary,\n\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t});\n\t\t\t} else if (event.type === \"background_agent_end\") {\n\t\t\t\tuserState.backgroundAgents.delete(event.agentId);\n\t\t\t}\n\t\t});\n\t}\n\n\treturn userState.bridge;\n}\n\n/**\n * Ensure bridge is alive AND a session is selected.\n * Used by message/file handlers and skill commands before prompting.\n */\nexport async function ensureBridgeWithSession(config: Config, userState: UserState): Promise<AgentBridge> {\n\t// Handle new session — always kill and recreate the bridge for clean state.\n\t// cmdNew always sets newSessionCwd to a concrete string (never null).\n\tif (userState.newSessionFlag) {\n\t\tconst cwd = userState.newSessionCwd!;\n\t\tuserState.newSessionFlag = false;\n\t\tuserState.newSessionCwd = null;\n\n\t\t// Kill existing bridge and start a new one with the resolved cwd\n\t\tif (userState.bridge?.isAlive) {\n\t\t\tawait userState.bridge.stop();\n\t\t}\n\t\tuserState.bridge = null;\n\n\t\tconst customConfig = { ...config, workingDir: cwd };\n\t\t// Set effectiveCwd BEFORE ensureBridge so the stale-cwd override\n\t\t// in ensureBridge doesn't clobber the resolved directory\n\t\tuserState.effectiveCwd = cwd;\n\t\tconst bridge = await ensureBridge(customConfig, userState);\n\t\tawait bridge.newSession();\n\t\treturn bridge;\n\t}\n\n\tconst hadBridge = !!userState.bridge?.isAlive;\n\tconst bridge = await ensureBridge(config, userState);\n\tconst freshBridge = !hadBridge;\n\n\t// Track effective cwd (default from config on first bridge creation)\n\tif (!userState.effectiveCwd) {\n\t\tuserState.effectiveCwd = config.workingDir;\n\t}\n\n\t// No session yet — try to resume latest\n\tif (!bridge.sessionId) {\n\t\tawait bridge.resumeLatest();\n\t}\n\n\t// Check for model fallback warning on fresh bridge (e.g. after crash)\n\tif (freshBridge) {\n\t\ttry {\n\t\t\tconst state = await bridge.getState();\n\t\t\tif (state?.modelFallbackMessage) {\n\t\t\t\tuserState.pendingModelFallbackWarning = state.modelFallbackMessage;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Non-critical — the warning is best-effort\n\t\t}\n\t}\n\n\treturn bridge;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy.d.ts","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1G;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"buddy.d.ts","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1G;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DhG","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* setMessageReaction unavailable or forbidden — send heart as plain text */\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
package/dist/commands/buddy.js
CHANGED
|
@@ -55,6 +55,7 @@ export async function cmdBuddy(ctx, config, userState) {
|
|
|
55
55
|
await ctx.api.setMessageReaction(chatId, ctx.message.message_id, [{ type: "emoji", emoji: "❤" }]);
|
|
56
56
|
}
|
|
57
57
|
catch {
|
|
58
|
+
/* setMessageReaction unavailable or forbidden — send heart as plain text */
|
|
58
59
|
await safeSend(ctx.api, chatId, "❤️");
|
|
59
60
|
}
|
|
60
61
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy.js","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy.js","sourceRoot":"","sources":["../../src/commands/buddy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /buddy command — manage the buddy companion in Telegram.\n */\n\nimport type { Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport { createTelegramBuddyController, formatBuddyStats } from \"../handlers/buddy.js\";\nimport { enqueueSend } from \"../handlers/message.js\";\nimport type { UserState } from \"../types.js\";\nimport { safeSend } from \"../util/telegram.js\";\n\n/**\n * Ensure the user has a buddy controller, creating one if needed.\n *\n * The controller auto-loads from the shared buddy.json on creation,\n * so it picks up any buddy hatched in the TUI (and vice versa).\n *\n * @param api — grammy Api for chat actions\n * @param userState — per-user state (controller stored here)\n * @param chatId — Telegram chat ID (private chat = user ID)\n * @param config — bot config (for bridge resolution in hatch/reroll)\n */\nexport function ensureBuddyController(api: any, userState: UserState, chatId: number, config: Config): void {\n\tif (userState.buddyController) return;\n\n\tconst send = (text: string, long?: boolean) => {\n\t\tenqueueSend(api, userState, chatId, text, long);\n\t};\n\tuserState.buddyController = createTelegramBuddyController(send, api, chatId, config, userState);\n}\n\nexport async function cmdBuddy(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst args = (ctx.match as string)?.trim() ?? \"\";\n\tconst subcommand = args.split(/\\s+/)[0]?.toLowerCase() ?? \"\";\n\n\ttry {\n\t\t// Ensure buddy controller exists\n\t\tensureBuddyController(ctx.api, userState, chatId, config);\n\n\t\tconst controller = userState.buddyController;\n\t\tconst result = await controller.handleCommand(subcommand);\n\n\t\tswitch (result.type) {\n\t\t\tcase \"hatch\":\n\t\t\tcase \"reroll\": {\n\t\t\t\t// Reload manager state after RPC hatch/reroll changed buddy.json\n\t\t\t\tcontroller.manager.load();\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* Reactions not available in all chats */\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"show\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"pet\": {\n\t\t\t\ttry {\n\t\t\t\t\tawait ctx.api.setMessageReaction(chatId, ctx.message!.message_id, [{ type: \"emoji\", emoji: \"❤\" }]);\n\t\t\t\t} catch {\n\t\t\t\t\t/* setMessageReaction unavailable or forbidden — send heart as plain text */\n\t\t\t\t\tawait safeSend(ctx.api, chatId, \"❤️\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, formatBuddyStats(result.state));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, \"🐣 Buddy hidden. Use /buddy to bring them back.\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"warning\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `⚠️ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"error\": {\n\t\t\t\tawait safeSend(ctx.api, chatId, `❌ ${result.message}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tawait safeSend(\n\t\t\tctx.api,\n\t\t\tchatId,\n\t\t\t`❌ Failed to initialize buddy: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/commands/core.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB1D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BjG;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9F;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/commands/core.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB1D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BjG;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9F;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B5F;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAY1F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5E","sourcesContent":["/**\n * Core slash commands: /start, /status, /cwd, /new, /stop, /restart\n */\n\nimport { execSync } from \"node:child_process\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { resolve } from \"node:path\";\nimport type { Api, Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport type { UserState } from \"../types.js\";\nimport { log, safeSend } from \"../util/telegram.js\";\n\nexport async function cmdStart(ctx: Context): Promise<void> {\n\tawait ctx.reply(\n\t\t\"🤖 *dreb Telegram*\\n\\n\" +\n\t\t\t\"Send me a message and I'll forward it to dreb.\\n\\n\" +\n\t\t\t\"*Session:*\\n\" +\n\t\t\t\"/new — Start a fresh session (keeps current directory)\\n\" +\n\t\t\t\"/new <path> — Start a fresh session in a different directory\\n\" +\n\t\t\t\"/sessions — List recent sessions\\n\" +\n\t\t\t\"/resume <id> — Resume a session\\n\" +\n\t\t\t\"/recent \\\\[N\\\\] — Resend last N messages\\n\\n\" +\n\t\t\t\"*Agent:*\\n\" +\n\t\t\t\"/status — Connection info\\n\" +\n\t\t\t\"/cwd — Working directory\\n\" +\n\t\t\t\"/stats — Token usage & cost\\n\" +\n\t\t\t\"/compact — Compact context\\n\" +\n\t\t\t\"/agents — Background subagents\\n\" +\n\t\t\t\"/model \\\\[pattern\\\\] — View/switch model\\n\" +\n\t\t\t\"/thinking \\\\[level\\\\] — View/set thinking\\n\" +\n\t\t\t\"/skills — List available skills\\n\\n\" +\n\t\t\t\"*Control:*\\n\" +\n\t\t\t\"/stop — Interrupt current task\\n\" +\n\t\t\t\"/restart — Restart the bot\",\n\t\t{ parse_mode: \"Markdown\" },\n\t);\n}\n\nexport async function cmdStatus(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst bridge = userState.bridge;\n\n\tlet version = \"unknown\";\n\tlet model = \"none\";\n\ttry {\n\t\tif (bridge?.isAlive) {\n\t\t\tversion = await bridge.getVersion();\n\t\t\tconst state = await bridge.getState();\n\t\t\tif (state?.model) model = `${state.model.provider}/${state.model.id}`;\n\t\t}\n\t} catch (e) {\n\t\tlog(`[CMD] /status error: ${e}`);\n\t}\n\n\tconst lines = [\n\t\tbridge?.isAlive ? \"✅ Connected\" : \"⚠️ Not connected (will start on next message)\",\n\t\t`📁 Working dir: \\`${config.workingDir}\\``,\n\t\t`🔧 dreb ${version}`,\n\t\t`🧠 Model: ${model}`,\n\t];\n\n\tif (bridge?.sessionId) {\n\t\tlines.push(`📂 Session: \\`${bridge.sessionId.slice(0, 8)}...\\``);\n\t}\n\n\tawait safeSend(ctx.api, chatId, lines.join(\"\\n\"));\n}\n\nexport async function cmdCwd(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst cwd = userState.effectiveCwd ?? config.workingDir;\n\tawait safeSend(ctx.api, ctx.chat!.id, `📁 Working directory: \\`${cwd}\\``);\n}\n\nexport async function cmdNew(ctx: Context, userState: UserState, args: string): Promise<void> {\n\tconst pathArg = args.trim();\n\n\tif (pathArg) {\n\t\t// Resolve path (expand ~ and make absolute)\n\t\tconst expanded = pathArg.startsWith(\"~\") ? pathArg.replace(\"~\", homedir()) : pathArg;\n\t\tconst resolved = resolve(expanded);\n\n\t\tif (!existsSync(resolved)) {\n\t\t\tawait safeSend(ctx.api, ctx.chat!.id, `❌ Directory not found: \\`${resolved}\\``);\n\t\t\treturn;\n\t\t}\n\t\tif (!statSync(resolved).isDirectory()) {\n\t\t\tawait safeSend(ctx.api, ctx.chat!.id, `❌ Not a directory: \\`${resolved}\\``);\n\t\t\treturn;\n\t\t}\n\n\t\tuserState.newSessionFlag = true;\n\t\tuserState.newSessionCwd = resolved;\n\t\tawait ctx.reply(`🆕 Next message will start a fresh session in \\`${resolved}\\``);\n\t} else {\n\t\t// Eagerly resolve CWD — never store null as a sentinel.\n\t\t// After /restart, effectiveCwd is null (in-memory state is lost),\n\t\t// so this falls back to config.workingDir deterministically.\n\t\tconst cwd = userState.effectiveCwd ?? userState.config.workingDir;\n\t\tuserState.newSessionFlag = true;\n\t\tuserState.newSessionCwd = cwd;\n\t\tawait ctx.reply(`🆕 Next message will start a fresh session in \\`${cwd}\\``);\n\t}\n}\n\nexport async function cmdStop(ctx: Context, _api: Api, userState: UserState): Promise<void> {\n\tuserState.stopRequested = true;\n\n\t// Abort current agent activity — like pressing Esc in the TUI.\n\t// This stops the agent, not the bridge. Session stays connected.\n\tif (userState.bridge?.isAlive) {\n\t\tawait userState.bridge.abort();\n\t}\n\n\tconst parts: string[] = [];\n\tif (userState.bridge?.isStreaming || userState.promptInFlight) parts.push(\"interrupted current task\");\n\tawait ctx.reply(parts.length > 0 ? `🛑 Stopped — ${parts.join(\", \")}.` : \"🛑 Stopped.\");\n}\n\nexport async function cmdRestart(ctx: Context, config: Config): Promise<void> {\n\tawait ctx.reply(\"🔄 Restarting...\");\n\tlog(\"[CMD] /restart — triggering systemctl restart\");\n\ttry {\n\t\texecSync(`systemctl --user restart ${config.serviceName}`, { timeout: 5000 });\n\t} catch {\n\t\t// Process will be killed by systemd restart\n\t}\n}\n"]}
|
package/dist/commands/core.js
CHANGED
|
@@ -78,9 +78,13 @@ export async function cmdNew(ctx, userState, args) {
|
|
|
78
78
|
await ctx.reply(`🆕 Next message will start a fresh session in \`${resolved}\``);
|
|
79
79
|
}
|
|
80
80
|
else {
|
|
81
|
+
// Eagerly resolve CWD — never store null as a sentinel.
|
|
82
|
+
// After /restart, effectiveCwd is null (in-memory state is lost),
|
|
83
|
+
// so this falls back to config.workingDir deterministically.
|
|
84
|
+
const cwd = userState.effectiveCwd ?? userState.config.workingDir;
|
|
81
85
|
userState.newSessionFlag = true;
|
|
82
|
-
userState.newSessionCwd =
|
|
83
|
-
await ctx.reply(
|
|
86
|
+
userState.newSessionCwd = cwd;
|
|
87
|
+
await ctx.reply(`🆕 Next message will start a fresh session in \`${cwd}\``);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
export async function cmdStop(ctx, _api, userState) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/commands/core.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/commands/core.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Core slash commands: /start, /status, /cwd, /new, /stop, /restart\n */\n\nimport { execSync } from \"node:child_process\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { resolve } from \"node:path\";\nimport type { Api, Context } from \"grammy\";\nimport type { Config } from \"../config.js\";\nimport type { UserState } from \"../types.js\";\nimport { log, safeSend } from \"../util/telegram.js\";\n\nexport async function cmdStart(ctx: Context): Promise<void> {\n\tawait ctx.reply(\n\t\t\"🤖 *dreb Telegram*\\n\\n\" +\n\t\t\t\"Send me a message and I'll forward it to dreb.\\n\\n\" +\n\t\t\t\"*Session:*\\n\" +\n\t\t\t\"/new — Start a fresh session (keeps current directory)\\n\" +\n\t\t\t\"/new <path> — Start a fresh session in a different directory\\n\" +\n\t\t\t\"/sessions — List recent sessions\\n\" +\n\t\t\t\"/resume <id> — Resume a session\\n\" +\n\t\t\t\"/recent \\\\[N\\\\] — Resend last N messages\\n\\n\" +\n\t\t\t\"*Agent:*\\n\" +\n\t\t\t\"/status — Connection info\\n\" +\n\t\t\t\"/cwd — Working directory\\n\" +\n\t\t\t\"/stats — Token usage & cost\\n\" +\n\t\t\t\"/compact — Compact context\\n\" +\n\t\t\t\"/agents — Background subagents\\n\" +\n\t\t\t\"/model \\\\[pattern\\\\] — View/switch model\\n\" +\n\t\t\t\"/thinking \\\\[level\\\\] — View/set thinking\\n\" +\n\t\t\t\"/skills — List available skills\\n\\n\" +\n\t\t\t\"*Control:*\\n\" +\n\t\t\t\"/stop — Interrupt current task\\n\" +\n\t\t\t\"/restart — Restart the bot\",\n\t\t{ parse_mode: \"Markdown\" },\n\t);\n}\n\nexport async function cmdStatus(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst chatId = ctx.chat!.id;\n\tconst bridge = userState.bridge;\n\n\tlet version = \"unknown\";\n\tlet model = \"none\";\n\ttry {\n\t\tif (bridge?.isAlive) {\n\t\t\tversion = await bridge.getVersion();\n\t\t\tconst state = await bridge.getState();\n\t\t\tif (state?.model) model = `${state.model.provider}/${state.model.id}`;\n\t\t}\n\t} catch (e) {\n\t\tlog(`[CMD] /status error: ${e}`);\n\t}\n\n\tconst lines = [\n\t\tbridge?.isAlive ? \"✅ Connected\" : \"⚠️ Not connected (will start on next message)\",\n\t\t`📁 Working dir: \\`${config.workingDir}\\``,\n\t\t`🔧 dreb ${version}`,\n\t\t`🧠 Model: ${model}`,\n\t];\n\n\tif (bridge?.sessionId) {\n\t\tlines.push(`📂 Session: \\`${bridge.sessionId.slice(0, 8)}...\\``);\n\t}\n\n\tawait safeSend(ctx.api, chatId, lines.join(\"\\n\"));\n}\n\nexport async function cmdCwd(ctx: Context, config: Config, userState: UserState): Promise<void> {\n\tconst cwd = userState.effectiveCwd ?? config.workingDir;\n\tawait safeSend(ctx.api, ctx.chat!.id, `📁 Working directory: \\`${cwd}\\``);\n}\n\nexport async function cmdNew(ctx: Context, userState: UserState, args: string): Promise<void> {\n\tconst pathArg = args.trim();\n\n\tif (pathArg) {\n\t\t// Resolve path (expand ~ and make absolute)\n\t\tconst expanded = pathArg.startsWith(\"~\") ? pathArg.replace(\"~\", homedir()) : pathArg;\n\t\tconst resolved = resolve(expanded);\n\n\t\tif (!existsSync(resolved)) {\n\t\t\tawait safeSend(ctx.api, ctx.chat!.id, `❌ Directory not found: \\`${resolved}\\``);\n\t\t\treturn;\n\t\t}\n\t\tif (!statSync(resolved).isDirectory()) {\n\t\t\tawait safeSend(ctx.api, ctx.chat!.id, `❌ Not a directory: \\`${resolved}\\``);\n\t\t\treturn;\n\t\t}\n\n\t\tuserState.newSessionFlag = true;\n\t\tuserState.newSessionCwd = resolved;\n\t\tawait ctx.reply(`🆕 Next message will start a fresh session in \\`${resolved}\\``);\n\t} else {\n\t\t// Eagerly resolve CWD — never store null as a sentinel.\n\t\t// After /restart, effectiveCwd is null (in-memory state is lost),\n\t\t// so this falls back to config.workingDir deterministically.\n\t\tconst cwd = userState.effectiveCwd ?? userState.config.workingDir;\n\t\tuserState.newSessionFlag = true;\n\t\tuserState.newSessionCwd = cwd;\n\t\tawait ctx.reply(`🆕 Next message will start a fresh session in \\`${cwd}\\``);\n\t}\n}\n\nexport async function cmdStop(ctx: Context, _api: Api, userState: UserState): Promise<void> {\n\tuserState.stopRequested = true;\n\n\t// Abort current agent activity — like pressing Esc in the TUI.\n\t// This stops the agent, not the bridge. Session stays connected.\n\tif (userState.bridge?.isAlive) {\n\t\tawait userState.bridge.abort();\n\t}\n\n\tconst parts: string[] = [];\n\tif (userState.bridge?.isStreaming || userState.promptInFlight) parts.push(\"interrupted current task\");\n\tawait ctx.reply(parts.length > 0 ? `🛑 Stopped — ${parts.join(\", \")}.` : \"🛑 Stopped.\");\n}\n\nexport async function cmdRestart(ctx: Context, config: Config): Promise<void> {\n\tawait ctx.reply(\"🔄 Restarting...\");\n\tlog(\"[CMD] /restart — triggering systemctl restart\");\n\ttry {\n\t\texecSync(`systemctl --user restart ${config.serviceName}`, { timeout: 5000 });\n\t} catch {\n\t\t// Process will be killed by systemd restart\n\t}\n}\n"]}
|
|
@@ -1 +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,
|
|
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,CAe1G;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\t/* Markdown parse rejected by Telegram — retry as plaintext */\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\t/* Plaintext edit also failed (message deleted, permissions revoked) */\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/dist/util/telegram.js
CHANGED
|
@@ -107,11 +107,13 @@ export async function safeEdit(api, chatId, messageId, text) {
|
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
|
+
/* Markdown parse rejected by Telegram — retry as plaintext */
|
|
110
111
|
try {
|
|
111
112
|
await withTimeout(api.editMessageText(chatId, messageId, text), API_TIMEOUT);
|
|
112
113
|
return true;
|
|
113
114
|
}
|
|
114
115
|
catch {
|
|
116
|
+
/* Plaintext edit also failed (message deleted, permissions revoked) */
|
|
115
117
|
return false;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../src/util/telegram.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,0DAA0D;AAEpF;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAI,OAAmB,EAAE,EAAU,EAAc;IAC3E,IAAI,KAAoC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;QACjD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,8BAA8B,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAAA,CACtF,CAAC,CAAC;IACH,yEAAyE;IACzE,8EAA8E;IAC9E,8CAA8C;IAC9C,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;IACxB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAM,CAAC,CAAC,CAAC;AAAA,CAC5E;AAED,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,4BAA4B;AAExD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY,EAAE,SAAkB,EAAmB;IAC3G,4FAA0F;IAC1F,IAAI,IAAI,CAAC,MAAM,GAAG,WAAW;QAAE,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAClE,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,WAAW,CAC5B,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE;YAC7B,UAAU,EAAE,UAAU;YACtB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC,EACF,WAAW,CACX,CAAC;QACF,OAAO,GAAG,CAAC,UAAU,CAAC;IACvB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,qEAAqE;QACrE,+EAA6E;QAC7E,sEAAsE;QACtE,mCAAmC;QACnC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,GAAG,CAAC,kCAAkC,CAAC,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,WAAW,CAC5B,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE;gBAC7B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACrE,CAAC,EACF,WAAW,CACX,CAAC;YACF,OAAO,GAAG,CAAC,UAAU,CAAC;QACvB,CAAC;QAAC,OAAO,EAAE,EAAE,CAAC;YACb,GAAG,CAAC,wDAAwD,EAAE,EAAE,CAAC,CAAC;YAClE,OAAO,CAAC,CAAC;QACV,CAAC;IACF,CAAC;AAAA,CACD;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,CAAU,EAAW;IAClD,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9C,kCAAkC;IAClC,MAAM,IAAI,GAAI,CAAS,CAAC,UAAU,IAAK,CAAS,CAAC,MAAM,IAAK,CAAS,CAAC,UAAU,CAAC;IACjF,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,MAAM,GAAG,GAAI,CAAS,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;AAAA,CACvE;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY,EAAE,SAAkB,EAAmB;IAC3G,OAAO,IAAI,EAAE,CAAC;QACb,IAAI,IAAI,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YAC3D,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAChC,CAAC;QACD,IAAI,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAClD,IAAI,OAAO,GAAG,IAAI;YAAE,OAAO,GAAG,WAAW,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;QAC7E,IAAI,KAAK,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,kEAAgE;QAC9F,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/C,SAAS,GAAG,SAAS,CAAC,CAAC,gCAAgC;IACxD,CAAC;IACD,OAAO,EAAE,CAAC;AAAA,CACV;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAQ,EAAE,MAAc,EAAE,SAAiB,EAAE,IAAY,EAAoB;IAC3G,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC;QACJ,MAAM,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;QACzG,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,IAAI,CAAC;YACJ,MAAM,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,WAAW,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;AAAA,CACD;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAQ,EAAE,MAAc,EAAE,SAAiB,EAAiB;IAC5F,IAAI,CAAC;QACJ,MAAM,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACR,uDAAqD;IACtD,CAAC;AAAA,CACD;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAM,GAAG,WAAW,EAAU;IACpE,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,mBAAmB,CAAC;AAAA,CACxD;AAED;;;GAGG;AACH,MAAM,OAAO,eAAe;IAKP,GAAG;IAJf,OAAO,GAAwE,IAAI,GAAG,EAAE,CAAC;IACzF,QAAQ,GAAwB,IAAI,GAAG,EAAE,CAAC;IACjC,WAAW,GAAG,IAAI,CAAC,CAAC,mBAAmB;IAExD,YAAoB,GAAQ,EAAE;mBAAV,GAAG;IAAQ,CAAC;IAEhC;;;OAGG;IACH,IAAI,CAAC,MAAc,EAAE,SAAiB,EAAE,IAAY,EAAQ;QAC3D,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,SAAS,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,CAAC;QAEtD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACnC,KAAK,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QAAA,CACjD,EAAE,KAAK,CAAC,CAAC;QAEV,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAAA,CACvC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc,EAAE,SAAiB,EAAiB;QAC7D,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,SAAS,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,QAAQ,EAAE,CAAC;YACd,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACnC,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5D,CAAC;IAAA,CACD;IAED,gCAAgC;IAChC,KAAK,GAAS;QACb,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACnE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAAA,CACrB;CACD;AAED,MAAM,UAAU,GAAG,CAAC,GAAW,EAAQ;IACtC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAAA,CACnB","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"]}
|
|
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\t/* Markdown parse rejected by Telegram — retry as plaintext */\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\t/* Plaintext edit also failed (message deleted, permissions revoked) */\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"]}
|