@poolzin/pool-bot 2026.2.21 → 2026.2.23

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 (378) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/device-pair/index.ts +2 -2
  326. package/extensions/diagnostics-otel/package.json +1 -1
  327. package/extensions/discord/package.json +1 -1
  328. package/extensions/feishu/package.json +1 -1
  329. package/extensions/google-antigravity-auth/package.json +1 -1
  330. package/extensions/google-gemini-cli-auth/package.json +1 -1
  331. package/extensions/googlechat/package.json +1 -1
  332. package/extensions/imessage/package.json +1 -1
  333. package/extensions/irc/package.json +1 -1
  334. package/extensions/irc/src/accounts.ts +1 -1
  335. package/extensions/irc/src/onboarding.ts +4 -4
  336. package/extensions/line/package.json +1 -1
  337. package/extensions/llm-task/package.json +1 -1
  338. package/extensions/lobster/package.json +1 -1
  339. package/extensions/matrix/CHANGELOG.md +10 -0
  340. package/extensions/matrix/package.json +1 -1
  341. package/extensions/mattermost/package.json +1 -1
  342. package/extensions/memory-core/package.json +1 -1
  343. package/extensions/memory-lancedb/package.json +1 -1
  344. package/extensions/minimax-portal-auth/package.json +1 -1
  345. package/extensions/msteams/CHANGELOG.md +10 -0
  346. package/extensions/msteams/package.json +1 -1
  347. package/extensions/nextcloud-talk/package.json +1 -1
  348. package/extensions/nostr/CHANGELOG.md +10 -0
  349. package/extensions/nostr/package.json +1 -1
  350. package/extensions/open-prose/package.json +1 -1
  351. package/extensions/openai-codex-auth/package.json +1 -1
  352. package/extensions/signal/package.json +1 -1
  353. package/extensions/slack/package.json +1 -1
  354. package/extensions/telegram/package.json +1 -1
  355. package/extensions/tlon/package.json +1 -1
  356. package/extensions/twitch/CHANGELOG.md +10 -0
  357. package/extensions/twitch/package.json +1 -1
  358. package/extensions/voice-call/CHANGELOG.md +10 -0
  359. package/extensions/voice-call/package.json +1 -1
  360. package/extensions/whatsapp/package.json +1 -1
  361. package/extensions/zalo/CHANGELOG.md +10 -0
  362. package/extensions/zalo/package.json +1 -1
  363. package/extensions/zalouser/CHANGELOG.md +10 -0
  364. package/extensions/zalouser/package.json +1 -1
  365. package/package.json +1 -1
  366. package/skills/apple-reminders/SKILL.md +100 -49
  367. package/skills/coding-agent/SKILL.md +34 -28
  368. package/skills/github/SKILL.md +131 -16
  369. package/skills/imsg/SKILL.md +112 -15
  370. package/skills/openhue/SKILL.md +101 -19
  371. package/skills/tmux/SKILL.md +111 -79
  372. package/skills/weather/SKILL.md +88 -25
  373. package/dist/agents/openclaw-tools.js +0 -151
  374. package/dist/agents/tool-security.js +0 -96
  375. package/dist/gateway/url-validation.js +0 -94
  376. package/dist/infra/openclaw-root.js +0 -109
  377. package/dist/infra/tmp-openclaw-dir.js +0 -81
  378. package/dist/media/path-sanitization.js +0 -78
@@ -1,29 +1,32 @@
1
1
  import { Bot, HttpError, InputFile } from "grammy";
2
2
  import { loadConfig } from "../config/config.js";
3
+ import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
3
4
  import { logVerbose } from "../globals.js";
4
5
  import { recordChannelActivity } from "../infra/channel-activity.js";
5
- import { withTelegramApiErrorLogging } from "./api-logging.js";
6
- import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
7
6
  import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js";
7
+ import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
8
8
  import { createTelegramRetryRunner } from "../infra/retry-policy.js";
9
9
  import { redactSensitiveText } from "../logging/redact.js";
10
10
  import { createSubsystemLogger } from "../logging/subsystem.js";
11
11
  import { mediaKindFromMime } from "../media/constants.js";
12
12
  import { isGifMedia } from "../media/mime.js";
13
+ import { normalizePollInput } from "../polls.js";
13
14
  import { loadWebMedia } from "../web/media.js";
14
15
  import { resolveTelegramAccount } from "./accounts.js";
16
+ import { withTelegramApiErrorLogging } from "./api-logging.js";
17
+ import { buildTelegramThreadParams } from "./bot/helpers.js";
18
+ import { splitTelegramCaption } from "./caption.js";
15
19
  import { resolveTelegramFetch } from "./fetch.js";
16
- import { makeProxyFetch } from "./proxy.js";
17
20
  import { renderTelegramHtmlText } from "./format.js";
18
- import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
19
21
  import { isRecoverableTelegramNetworkError } from "./network-errors.js";
20
- import { splitTelegramCaption } from "./caption.js";
22
+ import { makeProxyFetch } from "./proxy.js";
21
23
  import { recordSentMessage } from "./sent-message-cache.js";
22
24
  import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
23
25
  import { resolveTelegramVoiceSend } from "./voice.js";
24
- import { buildTelegramThreadParams } from "./bot/helpers.js";
25
26
  const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
26
27
  const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
28
+ const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
29
+ const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
27
30
  const diagLogger = createSubsystemLogger("telegram/diagnostic");
28
31
  function createTelegramHttpLogger(cfg) {
29
32
  const enabled = isDiagnosticFlagEnabled("telegram.http", cfg);
@@ -31,8 +34,9 @@ function createTelegramHttpLogger(cfg) {
31
34
  return () => { };
32
35
  }
33
36
  return (label, err) => {
34
- if (!(err instanceof HttpError))
37
+ if (!(err instanceof HttpError)) {
35
38
  return;
39
+ }
36
40
  const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
37
41
  diagLogger.warn(`telegram http error (${label}): ${detail}`);
38
42
  };
@@ -55,8 +59,9 @@ function resolveTelegramClientOptions(account) {
55
59
  : undefined;
56
60
  }
57
61
  function resolveToken(explicit, params) {
58
- if (explicit?.trim())
62
+ if (explicit?.trim()) {
59
63
  return explicit.trim();
64
+ }
60
65
  if (!params.token) {
61
66
  throw new Error(`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
62
67
  }
@@ -64,8 +69,9 @@ function resolveToken(explicit, params) {
64
69
  }
65
70
  function normalizeChatId(to) {
66
71
  const trimmed = to.trim();
67
- if (!trimmed)
72
+ if (!trimmed) {
68
73
  throw new Error("Recipient is required for Telegram sends");
74
+ }
69
75
  // Common internal prefixes that sometimes leak into outbound sends.
70
76
  // - ctx.To uses `telegram:<id>`
71
77
  // - group sessions often use `telegram:group:<id>`
@@ -74,17 +80,22 @@ function normalizeChatId(to) {
74
80
  // (Invite links like `t.me/+...` are not resolvable via Bot API.)
75
81
  const m = /^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
76
82
  /^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
77
- if (m?.[1])
83
+ if (m?.[1]) {
78
84
  normalized = `@${m[1]}`;
79
- if (!normalized)
85
+ }
86
+ if (!normalized) {
80
87
  throw new Error("Recipient is required for Telegram sends");
81
- if (normalized.startsWith("@"))
88
+ }
89
+ if (normalized.startsWith("@")) {
82
90
  return normalized;
83
- if (/^-?\d+$/.test(normalized))
91
+ }
92
+ if (/^-?\d+$/.test(normalized)) {
84
93
  return normalized;
94
+ }
85
95
  // If the user passed a username without `@`, assume they meant a public chat/channel.
86
- if (/^[A-Za-z0-9_]{5,}$/i.test(normalized))
96
+ if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) {
87
97
  return `@${normalized}`;
98
+ }
88
99
  return normalized;
89
100
  }
90
101
  function normalizeMessageId(raw) {
@@ -97,117 +108,192 @@ function normalizeMessageId(raw) {
97
108
  throw new Error("Message id is required for Telegram actions");
98
109
  }
99
110
  const parsed = Number.parseInt(value, 10);
100
- if (Number.isFinite(parsed))
111
+ if (Number.isFinite(parsed)) {
101
112
  return parsed;
113
+ }
102
114
  }
103
115
  throw new Error("Message id is required for Telegram actions");
104
116
  }
105
117
  function isTelegramThreadNotFoundError(err) {
106
118
  return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
107
119
  }
120
+ function isTelegramMessageNotModifiedError(err) {
121
+ return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
122
+ }
108
123
  function hasMessageThreadIdParam(params) {
109
- if (!params)
124
+ if (!params) {
110
125
  return false;
126
+ }
111
127
  const value = params.message_thread_id;
112
- if (typeof value === "number")
128
+ if (typeof value === "number") {
113
129
  return Number.isFinite(value);
114
- if (typeof value === "string")
130
+ }
131
+ if (typeof value === "string") {
115
132
  return value.trim().length > 0;
133
+ }
116
134
  return false;
117
135
  }
118
136
  function removeMessageThreadIdParam(params) {
119
- if (!params || !hasMessageThreadIdParam(params))
137
+ if (!params || !hasMessageThreadIdParam(params)) {
120
138
  return params;
139
+ }
121
140
  const next = { ...params };
122
141
  delete next.message_thread_id;
123
142
  return Object.keys(next).length > 0 ? next : undefined;
124
143
  }
144
+ function isTelegramHtmlParseError(err) {
145
+ return PARSE_ERR_RE.test(formatErrorMessage(err));
146
+ }
147
+ function buildTelegramThreadReplyParams(params) {
148
+ const messageThreadId = params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
149
+ const threadScope = params.chatType === "direct" ? "dm" : "forum";
150
+ // Never blanket-strip DM message_thread_id by chat-id sign.
151
+ // Telegram supports DM topics; stripping silently misroutes topic replies.
152
+ // Keep thread id and rely on thread-not-found retry fallback for plain DMs.
153
+ const threadSpec = messageThreadId != null ? { id: messageThreadId, scope: threadScope } : undefined;
154
+ const threadIdParams = buildTelegramThreadParams(threadSpec);
155
+ const threadParams = threadIdParams ? { ...threadIdParams } : {};
156
+ if (params.replyToMessageId != null) {
157
+ const replyToMessageId = Math.trunc(params.replyToMessageId);
158
+ if (params.quoteText?.trim()) {
159
+ threadParams.reply_parameters = {
160
+ message_id: replyToMessageId,
161
+ quote: params.quoteText.trim(),
162
+ };
163
+ }
164
+ else {
165
+ threadParams.reply_to_message_id = replyToMessageId;
166
+ }
167
+ }
168
+ return threadParams;
169
+ }
170
+ async function withTelegramHtmlParseFallback(params) {
171
+ try {
172
+ return await params.requestHtml(params.label);
173
+ }
174
+ catch (err) {
175
+ if (!isTelegramHtmlParseError(err)) {
176
+ throw err;
177
+ }
178
+ if (params.verbose) {
179
+ console.warn(`telegram ${params.label} failed with HTML parse error, retrying as plain text: ${formatErrorMessage(err)}`);
180
+ }
181
+ return await params.requestPlain(`${params.label}-plain`);
182
+ }
183
+ }
184
+ function resolveTelegramApiContext(opts) {
185
+ const cfg = opts.cfg ?? loadConfig();
186
+ const account = resolveTelegramAccount({
187
+ cfg,
188
+ accountId: opts.accountId,
189
+ });
190
+ const token = resolveToken(opts.token, account);
191
+ const client = resolveTelegramClientOptions(account);
192
+ const api = (opts.api ?? new Bot(token, client ? { client } : undefined).api);
193
+ return { cfg, account, api };
194
+ }
195
+ function createTelegramRequestWithDiag(params) {
196
+ const request = createTelegramRetryRunner({
197
+ retry: params.retry,
198
+ configRetry: params.account.config.retry,
199
+ verbose: params.verbose,
200
+ ...(params.shouldRetry ? { shouldRetry: params.shouldRetry } : {}),
201
+ });
202
+ const logHttpError = createTelegramHttpLogger(params.cfg);
203
+ return (fn, label, options) => {
204
+ const runRequest = () => request(fn, label);
205
+ const call = params.useApiErrorLogging === false
206
+ ? runRequest()
207
+ : withTelegramApiErrorLogging({
208
+ operation: label ?? "request",
209
+ fn: runRequest,
210
+ ...(options?.shouldLog ? { shouldLog: options.shouldLog } : {}),
211
+ });
212
+ return call.catch((err) => {
213
+ logHttpError(label ?? "request", err);
214
+ throw err;
215
+ });
216
+ };
217
+ }
218
+ function wrapTelegramChatNotFoundError(err, params) {
219
+ if (!CHAT_NOT_FOUND_RE.test(formatErrorMessage(err))) {
220
+ return err;
221
+ }
222
+ return new Error([
223
+ `Telegram send failed: chat not found (chat_id=${params.chatId}).`,
224
+ "Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
225
+ `Input was: ${JSON.stringify(params.input)}.`,
226
+ ].join(" "));
227
+ }
228
+ async function withTelegramThreadFallback(params, label, verbose, attempt) {
229
+ try {
230
+ return await attempt(params, label);
231
+ }
232
+ catch (err) {
233
+ // Do not widen this fallback to cover "chat not found".
234
+ // chat-not-found is routing/auth/membership/token; stripping thread IDs hides root cause.
235
+ if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
236
+ throw err;
237
+ }
238
+ if (verbose) {
239
+ console.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
240
+ }
241
+ const retriedParams = removeMessageThreadIdParam(params);
242
+ return await attempt(retriedParams, `${label}-threadless`);
243
+ }
244
+ }
245
+ function createRequestWithChatNotFound(params) {
246
+ return async (fn, label) => params.requestWithDiag(fn, label).catch((err) => {
247
+ throw wrapTelegramChatNotFoundError(err, {
248
+ chatId: params.chatId,
249
+ input: params.input,
250
+ });
251
+ });
252
+ }
125
253
  export function buildInlineKeyboard(buttons) {
126
- if (!buttons?.length)
254
+ if (!buttons?.length) {
127
255
  return undefined;
256
+ }
128
257
  const rows = buttons
129
258
  .map((row) => row
130
259
  .filter((button) => button?.text && button?.callback_data)
131
260
  .map((button) => ({
132
261
  text: button.text,
133
262
  callback_data: button.callback_data,
263
+ ...(button.style ? { style: button.style } : {}),
134
264
  })))
135
265
  .filter((row) => row.length > 0);
136
- if (rows.length === 0)
266
+ if (rows.length === 0) {
137
267
  return undefined;
268
+ }
138
269
  return { inline_keyboard: rows };
139
270
  }
140
271
  export async function sendMessageTelegram(to, text, opts = {}) {
141
- const cfg = loadConfig();
142
- const account = resolveTelegramAccount({
143
- cfg,
144
- accountId: opts.accountId,
145
- });
146
- const token = resolveToken(opts.token, account);
272
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
147
273
  const target = parseTelegramTarget(to);
148
274
  const chatId = normalizeChatId(target.chatId);
149
- // Use provided api or create a new Bot instance. The nullish coalescing
150
- // operator ensures api is always defined (Bot.api is always non-null).
151
- const client = resolveTelegramClientOptions(account);
152
- const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
153
275
  const mediaUrl = opts.mediaUrl?.trim();
154
276
  const replyMarkup = buildInlineKeyboard(opts.buttons);
155
- // Build optional params for forum topics and reply threading.
156
- // Only include these if actually provided to keep API calls clean.
157
- const messageThreadId = opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
158
- const threadIdParams = buildTelegramThreadParams(messageThreadId);
159
- const threadParams = threadIdParams ? { ...threadIdParams } : {};
160
- const quoteText = opts.quoteText?.trim();
161
- if (opts.replyToMessageId != null) {
162
- if (quoteText) {
163
- threadParams.reply_parameters = {
164
- message_id: Math.trunc(opts.replyToMessageId),
165
- quote: quoteText,
166
- };
167
- }
168
- else {
169
- threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
170
- }
171
- }
277
+ const threadParams = buildTelegramThreadReplyParams({
278
+ targetMessageThreadId: target.messageThreadId,
279
+ messageThreadId: opts.messageThreadId,
280
+ chatType: target.chatType,
281
+ replyToMessageId: opts.replyToMessageId,
282
+ quoteText: opts.quoteText,
283
+ });
172
284
  const hasThreadParams = Object.keys(threadParams).length > 0;
173
- const request = createTelegramRetryRunner({
285
+ const requestWithDiag = createTelegramRequestWithDiag({
286
+ cfg,
287
+ account,
174
288
  retry: opts.retry,
175
- configRetry: account.config.retry,
176
289
  verbose: opts.verbose,
177
290
  shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
178
291
  });
179
- const logHttpError = createTelegramHttpLogger(cfg);
180
- const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
181
- operation: label ?? "request",
182
- fn: () => request(fn, label),
183
- }).catch((err) => {
184
- logHttpError(label ?? "request", err);
185
- throw err;
292
+ const requestWithChatNotFound = createRequestWithChatNotFound({
293
+ requestWithDiag,
294
+ chatId,
295
+ input: to,
186
296
  });
187
- const wrapChatNotFound = (err) => {
188
- if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err)))
189
- return err;
190
- return new Error([
191
- `Telegram send failed: chat not found (chat_id=${chatId}).`,
192
- "Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
193
- `Input was: ${JSON.stringify(to)}.`,
194
- ].join(" "));
195
- };
196
- const sendWithThreadFallback = async (params, label, attempt) => {
197
- try {
198
- return await attempt(params, label);
199
- }
200
- catch (err) {
201
- if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
202
- throw err;
203
- }
204
- if (opts.verbose) {
205
- console.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
206
- }
207
- const retriedParams = removeMessageThreadIdParam(params);
208
- return await attempt(retriedParams, `${label}-threadless`);
209
- }
210
- };
211
297
  const textMode = opts.textMode ?? "markdown";
212
298
  const tableMode = resolveMarkdownTableMode({
213
299
  cfg,
@@ -219,7 +305,7 @@ export async function sendMessageTelegram(to, text, opts = {}) {
219
305
  const linkPreviewEnabled = account.config.linkPreview ?? true;
220
306
  const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
221
307
  const sendTelegramText = async (rawText, params, fallbackText) => {
222
- return await sendWithThreadFallback(params, "message", async (effectiveParams, label) => {
308
+ return await withTelegramThreadFallback(params, "message", opts.verbose, async (effectiveParams, label) => {
223
309
  const htmlText = renderHtmlText(rawText);
224
310
  const baseParams = effectiveParams ? { ...effectiveParams } : {};
225
311
  if (linkPreviewOptions) {
@@ -231,31 +317,26 @@ export async function sendMessageTelegram(to, text, opts = {}) {
231
317
  ...baseParams,
232
318
  ...(opts.silent === true ? { disable_notification: true } : {}),
233
319
  };
234
- const res = await requestWithDiag(() => api.sendMessage(chatId, htmlText, sendParams), label).catch(async (err) => {
235
- // Telegram rejects malformed HTML (e.g., unsupported tags or entities).
236
- // When that happens, fall back to plain text so the message still delivers.
237
- const errText = formatErrorMessage(err);
238
- if (PARSE_ERR_RE.test(errText)) {
239
- if (opts.verbose) {
240
- console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
241
- }
242
- const fallback = fallbackText ?? rawText;
320
+ return await withTelegramHtmlParseFallback({
321
+ label,
322
+ verbose: opts.verbose,
323
+ requestHtml: (retryLabel) => requestWithChatNotFound(() => api.sendMessage(chatId, htmlText, sendParams), retryLabel),
324
+ requestPlain: (retryLabel) => {
243
325
  const plainParams = hasBaseParams
244
326
  ? baseParams
245
327
  : undefined;
246
- return await requestWithDiag(() => plainParams
247
- ? api.sendMessage(chatId, fallback, plainParams)
248
- : api.sendMessage(chatId, fallback), `${label}-plain`).catch((err2) => {
249
- throw wrapChatNotFound(err2);
250
- });
251
- }
252
- throw wrapChatNotFound(err);
328
+ return requestWithChatNotFound(() => plainParams
329
+ ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams)
330
+ : api.sendMessage(chatId, fallbackText ?? rawText), retryLabel);
331
+ },
253
332
  });
254
- return res;
255
333
  });
256
334
  };
257
335
  if (mediaUrl) {
258
- const media = await loadWebMedia(mediaUrl, opts.maxBytes);
336
+ const media = await loadWebMedia(mediaUrl, {
337
+ maxBytes: opts.maxBytes,
338
+ localRoots: opts.mediaLocalRoots,
339
+ });
259
340
  const kind = mediaKindFromMime(media.contentType ?? undefined);
260
341
  const isGif = isGifMedia({
261
342
  contentType: media.contentType,
@@ -267,7 +348,6 @@ export async function sendMessageTelegram(to, text, opts = {}) {
267
348
  let caption;
268
349
  let followUpText;
269
350
  if (isVideoNote) {
270
- // Video notes don't support captions; send any text as follow-up.
271
351
  caption = undefined;
272
352
  followUpText = text.trim() ? text : undefined;
273
353
  }
@@ -291,52 +371,56 @@ export async function sendMessageTelegram(to, text, opts = {}) {
291
371
  ...baseMediaParams,
292
372
  ...(opts.silent === true ? { disable_notification: true } : {}),
293
373
  };
294
- let result;
295
- if (isGif) {
296
- result = await sendWithThreadFallback(mediaParams, "animation", async (effectiveParams, label) => requestWithDiag(() => api.sendAnimation(chatId, file, effectiveParams), label).catch((err) => {
297
- throw wrapChatNotFound(err);
298
- }));
299
- }
300
- else if (kind === "image") {
301
- result = await sendWithThreadFallback(mediaParams, "photo", async (effectiveParams, label) => requestWithDiag(() => api.sendPhoto(chatId, file, effectiveParams), label).catch((err) => {
302
- throw wrapChatNotFound(err);
303
- }));
304
- }
305
- else if (kind === "video") {
306
- if (isVideoNote) {
307
- result = await sendWithThreadFallback(mediaParams, "video_note", async (effectiveParams, label) => requestWithDiag(() => api.sendVideoNote(chatId, file, effectiveParams), label).catch((err) => {
308
- throw wrapChatNotFound(err);
309
- }));
374
+ const sendMedia = async (label, sender) => await withTelegramThreadFallback(mediaParams, label, opts.verbose, async (effectiveParams, retryLabel) => requestWithChatNotFound(() => sender(effectiveParams), retryLabel));
375
+ const mediaSender = (() => {
376
+ if (isGif) {
377
+ return {
378
+ label: "animation",
379
+ sender: (effectiveParams) => api.sendAnimation(chatId, file, effectiveParams),
380
+ };
310
381
  }
311
- else {
312
- result = await sendWithThreadFallback(mediaParams, "video", async (effectiveParams, label) => requestWithDiag(() => api.sendVideo(chatId, file, effectiveParams), label).catch((err) => {
313
- throw wrapChatNotFound(err);
314
- }));
382
+ if (kind === "image") {
383
+ return {
384
+ label: "photo",
385
+ sender: (effectiveParams) => api.sendPhoto(chatId, file, effectiveParams),
386
+ };
315
387
  }
316
- }
317
- else if (kind === "audio") {
318
- const { useVoice } = resolveTelegramVoiceSend({
319
- wantsVoice: opts.asVoice === true, // default false (backward compatible)
320
- contentType: media.contentType,
321
- fileName,
322
- logFallback: logVerbose,
323
- });
324
- if (useVoice) {
325
- result = await sendWithThreadFallback(mediaParams, "voice", async (effectiveParams, label) => requestWithDiag(() => api.sendVoice(chatId, file, effectiveParams), label).catch((err) => {
326
- throw wrapChatNotFound(err);
327
- }));
388
+ if (kind === "video") {
389
+ if (isVideoNote) {
390
+ return {
391
+ label: "video_note",
392
+ sender: (effectiveParams) => api.sendVideoNote(chatId, file, effectiveParams),
393
+ };
394
+ }
395
+ return {
396
+ label: "video",
397
+ sender: (effectiveParams) => api.sendVideo(chatId, file, effectiveParams),
398
+ };
328
399
  }
329
- else {
330
- result = await sendWithThreadFallback(mediaParams, "audio", async (effectiveParams, label) => requestWithDiag(() => api.sendAudio(chatId, file, effectiveParams), label).catch((err) => {
331
- throw wrapChatNotFound(err);
332
- }));
400
+ if (kind === "audio") {
401
+ const { useVoice } = resolveTelegramVoiceSend({
402
+ wantsVoice: opts.asVoice === true, // default false (backward compatible)
403
+ contentType: media.contentType,
404
+ fileName,
405
+ logFallback: logVerbose,
406
+ });
407
+ if (useVoice) {
408
+ return {
409
+ label: "voice",
410
+ sender: (effectiveParams) => api.sendVoice(chatId, file, effectiveParams),
411
+ };
412
+ }
413
+ return {
414
+ label: "audio",
415
+ sender: (effectiveParams) => api.sendAudio(chatId, file, effectiveParams),
416
+ };
333
417
  }
334
- }
335
- else {
336
- result = await sendWithThreadFallback(mediaParams, "document", async (effectiveParams, label) => requestWithDiag(() => api.sendDocument(chatId, file, effectiveParams), label).catch((err) => {
337
- throw wrapChatNotFound(err);
338
- }));
339
- }
418
+ return {
419
+ label: "document",
420
+ sender: (effectiveParams) => api.sendDocument(chatId, file, effectiveParams),
421
+ };
422
+ })();
423
+ const result = await sendMedia(mediaSender.label, mediaSender.sender);
340
424
  const mediaMessageId = String(result?.message_id ?? "unknown");
341
425
  const resolvedChatId = String(result?.chat?.id ?? chatId);
342
426
  if (result?.message_id) {
@@ -387,30 +471,16 @@ export async function sendMessageTelegram(to, text, opts = {}) {
387
471
  return { messageId, chatId: String(res?.chat?.id ?? chatId) };
388
472
  }
389
473
  export async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, opts = {}) {
390
- const cfg = loadConfig();
391
- const account = resolveTelegramAccount({
392
- cfg,
393
- accountId: opts.accountId,
394
- });
395
- const token = resolveToken(opts.token, account);
474
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
396
475
  const chatId = normalizeChatId(String(chatIdInput));
397
476
  const messageId = normalizeMessageId(messageIdInput);
398
- const client = resolveTelegramClientOptions(account);
399
- const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
400
- const request = createTelegramRetryRunner({
477
+ const requestWithDiag = createTelegramRequestWithDiag({
478
+ cfg,
479
+ account,
401
480
  retry: opts.retry,
402
- configRetry: account.config.retry,
403
481
  verbose: opts.verbose,
404
482
  shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
405
483
  });
406
- const logHttpError = createTelegramHttpLogger(cfg);
407
- const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
408
- operation: label ?? "request",
409
- fn: () => request(fn, label),
410
- }).catch((err) => {
411
- logHttpError(label ?? "request", err);
412
- throw err;
413
- });
414
484
  const remove = opts.remove === true;
415
485
  const trimmedEmoji = emoji.trim();
416
486
  // Build the reaction array. We cast emoji to the grammY union type since
@@ -421,62 +491,47 @@ export async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, o
421
491
  if (typeof api.setMessageReaction !== "function") {
422
492
  throw new Error("Telegram reactions are unavailable in this bot API.");
423
493
  }
424
- await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
494
+ try {
495
+ await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
496
+ }
497
+ catch (err) {
498
+ const msg = err instanceof Error ? err.message : String(err);
499
+ if (/REACTION_INVALID/i.test(msg)) {
500
+ return { ok: false, warning: `Reaction unavailable: ${trimmedEmoji}` };
501
+ }
502
+ throw err;
503
+ }
425
504
  return { ok: true };
426
505
  }
427
506
  export async function deleteMessageTelegram(chatIdInput, messageIdInput, opts = {}) {
428
- const cfg = loadConfig();
429
- const account = resolveTelegramAccount({
430
- cfg,
431
- accountId: opts.accountId,
432
- });
433
- const token = resolveToken(opts.token, account);
507
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
434
508
  const chatId = normalizeChatId(String(chatIdInput));
435
509
  const messageId = normalizeMessageId(messageIdInput);
436
- const client = resolveTelegramClientOptions(account);
437
- const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
438
- const request = createTelegramRetryRunner({
510
+ const requestWithDiag = createTelegramRequestWithDiag({
511
+ cfg,
512
+ account,
439
513
  retry: opts.retry,
440
- configRetry: account.config.retry,
441
514
  verbose: opts.verbose,
442
515
  shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
443
516
  });
444
- const logHttpError = createTelegramHttpLogger(cfg);
445
- const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
446
- operation: label ?? "request",
447
- fn: () => request(fn, label),
448
- }).catch((err) => {
449
- logHttpError(label ?? "request", err);
450
- throw err;
451
- });
452
517
  await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
453
518
  logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
454
519
  return { ok: true };
455
520
  }
456
521
  export async function editMessageTelegram(chatIdInput, messageIdInput, text, opts = {}) {
457
- const cfg = opts.cfg ?? loadConfig();
458
- const account = resolveTelegramAccount({
459
- cfg,
460
- accountId: opts.accountId,
522
+ const { cfg, account, api } = resolveTelegramApiContext({
523
+ ...opts,
524
+ cfg: opts.cfg,
461
525
  });
462
- const token = resolveToken(opts.token, account);
463
526
  const chatId = normalizeChatId(String(chatIdInput));
464
527
  const messageId = normalizeMessageId(messageIdInput);
465
- const client = resolveTelegramClientOptions(account);
466
- const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
467
- const request = createTelegramRetryRunner({
528
+ const requestWithDiag = createTelegramRequestWithDiag({
529
+ cfg,
530
+ account,
468
531
  retry: opts.retry,
469
- configRetry: account.config.retry,
470
532
  verbose: opts.verbose,
471
533
  });
472
- const logHttpError = createTelegramHttpLogger(cfg);
473
- const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
474
- operation: label ?? "request",
475
- fn: () => request(fn, label),
476
- }).catch((err) => {
477
- logHttpError(label ?? "request", err);
478
- throw err;
479
- });
534
+ const requestWithEditShouldLog = (fn, label, shouldLog) => requestWithDiag(fn, label, shouldLog ? { shouldLog } : undefined);
480
535
  const textMode = opts.textMode ?? "markdown";
481
536
  const tableMode = resolveMarkdownTableMode({
482
537
  cfg,
@@ -494,26 +549,37 @@ export async function editMessageTelegram(chatIdInput, messageIdInput, text, opt
494
549
  const editParams = {
495
550
  parse_mode: "HTML",
496
551
  };
552
+ if (opts.linkPreview === false) {
553
+ editParams.link_preview_options = { is_disabled: true };
554
+ }
497
555
  if (replyMarkup !== undefined) {
498
556
  editParams.reply_markup = replyMarkup;
499
557
  }
500
- await requestWithDiag(() => api.editMessageText(chatId, messageId, htmlText, editParams), "editMessage").catch(async (err) => {
501
- // Telegram rejects malformed HTML. Fall back to plain text.
502
- const errText = formatErrorMessage(err);
503
- if (PARSE_ERR_RE.test(errText)) {
504
- if (opts.verbose) {
505
- console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
506
- }
507
- const plainParams = {};
508
- if (replyMarkup !== undefined) {
509
- plainParams.reply_markup = replyMarkup;
510
- }
511
- return await requestWithDiag(() => Object.keys(plainParams).length > 0
558
+ const plainParams = {};
559
+ if (opts.linkPreview === false) {
560
+ plainParams.link_preview_options = { is_disabled: true };
561
+ }
562
+ if (replyMarkup !== undefined) {
563
+ plainParams.reply_markup = replyMarkup;
564
+ }
565
+ try {
566
+ await withTelegramHtmlParseFallback({
567
+ label: "editMessage",
568
+ verbose: opts.verbose,
569
+ requestHtml: (retryLabel) => requestWithEditShouldLog(() => api.editMessageText(chatId, messageId, htmlText, editParams), retryLabel, (err) => !isTelegramMessageNotModifiedError(err)),
570
+ requestPlain: (retryLabel) => requestWithEditShouldLog(() => Object.keys(plainParams).length > 0
512
571
  ? api.editMessageText(chatId, messageId, text, plainParams)
513
- : api.editMessageText(chatId, messageId, text), "editMessage-plain");
572
+ : api.editMessageText(chatId, messageId, text), retryLabel, (plainErr) => !isTelegramMessageNotModifiedError(plainErr)),
573
+ });
574
+ }
575
+ catch (err) {
576
+ if (isTelegramMessageNotModifiedError(err)) {
577
+ // no-op: Telegram reports message content unchanged, treat as success
514
578
  }
515
- throw err;
516
- });
579
+ else {
580
+ throw err;
581
+ }
582
+ }
517
583
  logVerbose(`[telegram] Edited message ${messageId} in chat ${chatId}`);
518
584
  return { ok: true, messageId: String(messageId), chatId };
519
585
  }
@@ -539,70 +605,164 @@ export async function sendStickerTelegram(to, fileId, opts = {}) {
539
605
  if (!fileId?.trim()) {
540
606
  throw new Error("Telegram sticker file_id is required");
541
607
  }
608
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
609
+ const target = parseTelegramTarget(to);
610
+ const chatId = normalizeChatId(target.chatId);
611
+ const threadParams = buildTelegramThreadReplyParams({
612
+ targetMessageThreadId: target.messageThreadId,
613
+ messageThreadId: opts.messageThreadId,
614
+ chatType: target.chatType,
615
+ replyToMessageId: opts.replyToMessageId,
616
+ });
617
+ const hasThreadParams = Object.keys(threadParams).length > 0;
618
+ const requestWithDiag = createTelegramRequestWithDiag({
619
+ cfg,
620
+ account,
621
+ retry: opts.retry,
622
+ verbose: opts.verbose,
623
+ useApiErrorLogging: false,
624
+ });
625
+ const requestWithChatNotFound = createRequestWithChatNotFound({
626
+ requestWithDiag,
627
+ chatId,
628
+ input: to,
629
+ });
630
+ const stickerParams = hasThreadParams ? threadParams : undefined;
631
+ const result = await withTelegramThreadFallback(stickerParams, "sticker", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label));
632
+ const messageId = String(result?.message_id ?? "unknown");
633
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
634
+ if (result?.message_id) {
635
+ recordSentMessage(chatId, result.message_id);
636
+ }
637
+ recordChannelActivity({
638
+ channel: "telegram",
639
+ accountId: account.accountId,
640
+ direction: "outbound",
641
+ });
642
+ return { messageId, chatId: resolvedChatId };
643
+ }
644
+ /**
645
+ * Send a poll to a Telegram chat.
646
+ * @param to - Chat ID or username (e.g., "123456789" or "@username")
647
+ * @param poll - Poll input with question, options, maxSelections, and optional durationHours
648
+ * @param opts - Optional configuration
649
+ */
650
+ export async function sendPollTelegram(to, poll, opts = {}) {
651
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
652
+ const target = parseTelegramTarget(to);
653
+ const chatId = normalizeChatId(target.chatId);
654
+ // Normalize the poll input (validates question, options, maxSelections)
655
+ const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
656
+ const threadParams = buildTelegramThreadReplyParams({
657
+ targetMessageThreadId: target.messageThreadId,
658
+ messageThreadId: opts.messageThreadId,
659
+ chatType: target.chatType,
660
+ replyToMessageId: opts.replyToMessageId,
661
+ });
662
+ // Build poll options as simple strings (Grammy accepts string[] or InputPollOption[])
663
+ const pollOptions = normalizedPoll.options;
664
+ const requestWithDiag = createTelegramRequestWithDiag({
665
+ cfg,
666
+ account,
667
+ retry: opts.retry,
668
+ verbose: opts.verbose,
669
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
670
+ });
671
+ const requestWithChatNotFound = createRequestWithChatNotFound({
672
+ requestWithDiag,
673
+ chatId,
674
+ input: to,
675
+ });
676
+ const durationSeconds = normalizedPoll.durationSeconds;
677
+ if (durationSeconds === undefined && normalizedPoll.durationHours !== undefined) {
678
+ throw new Error("Telegram poll durationHours is not supported. Use durationSeconds (5-600) instead.");
679
+ }
680
+ if (durationSeconds !== undefined && (durationSeconds < 5 || durationSeconds > 600)) {
681
+ throw new Error("Telegram poll durationSeconds must be between 5 and 600");
682
+ }
683
+ // Build poll parameters following Grammy's api.sendPoll signature
684
+ // sendPoll(chat_id, question, options, other?, signal?)
685
+ const pollParams = {
686
+ allows_multiple_answers: normalizedPoll.maxSelections > 1,
687
+ is_anonymous: opts.isAnonymous ?? true,
688
+ ...(durationSeconds !== undefined ? { open_period: durationSeconds } : {}),
689
+ ...(Object.keys(threadParams).length > 0 ? threadParams : {}),
690
+ ...(opts.silent === true ? { disable_notification: true } : {}),
691
+ };
692
+ const result = await withTelegramThreadFallback(pollParams, "poll", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendPoll(chatId, normalizedPoll.question, pollOptions, effectiveParams), label));
693
+ const messageId = String(result?.message_id ?? "unknown");
694
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
695
+ const pollId = result?.poll?.id;
696
+ if (result?.message_id) {
697
+ recordSentMessage(chatId, result.message_id);
698
+ }
699
+ recordChannelActivity({
700
+ channel: "telegram",
701
+ accountId: account.accountId,
702
+ direction: "outbound",
703
+ });
704
+ return { messageId, chatId: resolvedChatId, pollId };
705
+ }
706
+ /**
707
+ * Create a forum topic in a Telegram supergroup.
708
+ * Requires the bot to have `can_manage_topics` permission.
709
+ *
710
+ * @param chatId - Supergroup chat ID
711
+ * @param name - Topic name (1-128 characters)
712
+ * @param opts - Optional configuration
713
+ */
714
+ export async function createForumTopicTelegram(chatId, name, opts = {}) {
715
+ if (!name?.trim()) {
716
+ throw new Error("Forum topic name is required");
717
+ }
718
+ const trimmedName = name.trim();
719
+ if (trimmedName.length > 128) {
720
+ throw new Error("Forum topic name must be 128 characters or fewer");
721
+ }
542
722
  const cfg = loadConfig();
543
723
  const account = resolveTelegramAccount({
544
724
  cfg,
545
725
  accountId: opts.accountId,
546
726
  });
547
727
  const token = resolveToken(opts.token, account);
548
- const target = parseTelegramTarget(to);
549
- const chatId = normalizeChatId(target.chatId);
728
+ // Accept topic-qualified targets (e.g. telegram:group:<id>:topic:<thread>)
729
+ // but createForumTopic must always target the base supergroup chat id.
730
+ const target = parseTelegramTarget(chatId);
731
+ const normalizedChatId = normalizeChatId(target.chatId);
550
732
  const client = resolveTelegramClientOptions(account);
551
733
  const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
552
- const messageThreadId = opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
553
- const threadIdParams = buildTelegramThreadParams(messageThreadId);
554
- const threadParams = threadIdParams ? { ...threadIdParams } : {};
555
- if (opts.replyToMessageId != null) {
556
- threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
557
- }
558
- const hasThreadParams = Object.keys(threadParams).length > 0;
559
734
  const request = createTelegramRetryRunner({
560
735
  retry: opts.retry,
561
736
  configRetry: account.config.retry,
562
737
  verbose: opts.verbose,
738
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
563
739
  });
564
740
  const logHttpError = createTelegramHttpLogger(cfg);
565
- const requestWithDiag = (fn, label) => request(fn, label).catch((err) => {
741
+ const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
742
+ operation: label ?? "request",
743
+ fn: () => request(fn, label),
744
+ }).catch((err) => {
566
745
  logHttpError(label ?? "request", err);
567
746
  throw err;
568
747
  });
569
- const wrapChatNotFound = (err) => {
570
- if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err)))
571
- return err;
572
- return new Error([
573
- `Telegram send failed: chat not found (chat_id=${chatId}).`,
574
- "Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
575
- `Input was: ${JSON.stringify(to)}.`,
576
- ].join(" "));
577
- };
578
- const sendWithStickerThreadFallback = async (params, label, attempt) => {
579
- try {
580
- return await attempt(params, label);
581
- }
582
- catch (err) {
583
- if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
584
- throw err;
585
- }
586
- if (opts.verbose) {
587
- console.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
588
- }
589
- const retriedParams = removeMessageThreadIdParam(params);
590
- return await attempt(retriedParams, `${label}-threadless`);
591
- }
592
- };
593
- const stickerParams = hasThreadParams ? threadParams : undefined;
594
- const result = await sendWithStickerThreadFallback(stickerParams, "sticker", async (effectiveParams, label) => requestWithDiag(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label).catch((err) => {
595
- throw wrapChatNotFound(err);
596
- }));
597
- const messageId = String(result?.message_id ?? "unknown");
598
- const resolvedChatId = String(result?.chat?.id ?? chatId);
599
- if (result?.message_id) {
600
- recordSentMessage(chatId, result.message_id);
748
+ const extra = {};
749
+ if (opts.iconColor != null) {
750
+ extra.icon_color = opts.iconColor;
751
+ }
752
+ if (opts.iconCustomEmojiId?.trim()) {
753
+ extra.icon_custom_emoji_id = opts.iconCustomEmojiId.trim();
601
754
  }
755
+ const hasExtra = Object.keys(extra).length > 0;
756
+ const result = await requestWithDiag(() => api.createForumTopic(normalizedChatId, trimmedName, hasExtra ? extra : undefined), "createForumTopic");
757
+ const topicId = result.message_thread_id;
602
758
  recordChannelActivity({
603
759
  channel: "telegram",
604
760
  accountId: account.accountId,
605
761
  direction: "outbound",
606
762
  });
607
- return { messageId, chatId: resolvedChatId };
763
+ return {
764
+ topicId,
765
+ name: result.name ?? trimmedName,
766
+ chatId: normalizedChatId,
767
+ };
608
768
  }