@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,55 +1,83 @@
1
- // @ts-nocheck
1
+ import { resolveDefaultAgentId } from "../agents/agent-scope.js";
2
2
  import { hasControlCommand } from "../auto-reply/command-detection.js";
3
3
  import { createInboundDebouncer, resolveInboundDebounceMs, } from "../auto-reply/inbound-debounce.js";
4
4
  import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js";
5
5
  import { buildModelsProviderData } from "../auto-reply/reply/commands-models.js";
6
6
  import { resolveStoredModelOverride } from "../auto-reply/reply/model-selection.js";
7
- import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
8
7
  import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
9
- import { resolveDefaultAgentId } from "../agents/agent-scope.js";
8
+ import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
9
+ import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
10
10
  import { loadConfig } from "../config/config.js";
11
+ import { writeConfigFile } from "../config/io.js";
11
12
  import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
13
+ import { danger, logVerbose, warn } from "../globals.js";
14
+ import { enqueueSystemEvent } from "../infra/system-events.js";
15
+ import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
12
16
  import { resolveAgentRoute } from "../routing/resolve-route.js";
13
17
  import { resolveThreadSessionKeys } from "../routing/session-key.js";
14
- import { writeConfigFile } from "../config/io.js";
15
- import { danger, logVerbose, warn } from "../globals.js";
16
- import { resolveMedia } from "./bot/delivery.js";
17
18
  import { withTelegramApiErrorLogging } from "./api-logging.js";
18
- import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, } from "./bot/helpers.js";
19
- import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
19
+ import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore, } from "./bot-access.js";
20
20
  import { MEDIA_GROUP_TIMEOUT_MS } from "./bot-updates.js";
21
+ import { resolveMedia } from "./bot/delivery.js";
22
+ import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramGroupAllowFromContext, } from "./bot/helpers.js";
23
+ import { evaluateTelegramGroupBaseAccess, evaluateTelegramGroupPolicyAccess, } from "./group-access.js";
21
24
  import { migrateTelegramGroupConfig } from "./group-migration.js";
22
25
  import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
23
- import { readTelegramAllowFromStore } from "./pairing-store.js";
24
- import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
25
26
  import { buildModelsKeyboard, buildProviderKeyboard, calculateTotalPages, getModelsPageSize, parseModelCallbackData, } from "./model-buttons.js";
26
27
  import { buildInlineKeyboard } from "./send.js";
28
+ import { wasSentByBot } from "./sent-message-cache.js";
27
29
  export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, mediaMaxBytes, telegramCfg, groupAllowFrom, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, processMessage, logger, }) => {
30
+ const DEFAULT_TEXT_FRAGMENT_MAX_GAP_MS = 1500;
28
31
  const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
29
- const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = 1500;
32
+ const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = typeof opts.testTimings?.textFragmentGapMs === "number" &&
33
+ Number.isFinite(opts.testTimings.textFragmentGapMs)
34
+ ? Math.max(10, Math.floor(opts.testTimings.textFragmentGapMs))
35
+ : DEFAULT_TEXT_FRAGMENT_MAX_GAP_MS;
30
36
  const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1;
31
37
  const TELEGRAM_TEXT_FRAGMENT_MAX_PARTS = 12;
32
38
  const TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS = 50_000;
39
+ const mediaGroupTimeoutMs = typeof opts.testTimings?.mediaGroupFlushMs === "number" &&
40
+ Number.isFinite(opts.testTimings.mediaGroupFlushMs)
41
+ ? Math.max(10, Math.floor(opts.testTimings.mediaGroupFlushMs))
42
+ : MEDIA_GROUP_TIMEOUT_MS;
33
43
  const mediaGroupBuffer = new Map();
34
44
  let mediaGroupProcessing = Promise.resolve();
35
45
  const textFragmentBuffer = new Map();
36
46
  let textFragmentProcessing = Promise.resolve();
37
47
  const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
48
+ const buildSyntheticTextMessage = (params) => ({
49
+ ...params.base,
50
+ ...(params.from ? { from: params.from } : {}),
51
+ text: params.text,
52
+ caption: undefined,
53
+ caption_entities: undefined,
54
+ entities: undefined,
55
+ ...(params.date != null ? { date: params.date } : {}),
56
+ });
57
+ const buildSyntheticContext = (ctx, message) => {
58
+ const getFile = typeof ctx.getFile === "function"
59
+ ? ctx.getFile.bind(ctx)
60
+ : async () => ({});
61
+ return { message, me: ctx.me, getFile };
62
+ };
38
63
  const inboundDebouncer = createInboundDebouncer({
39
64
  debounceMs,
40
65
  buildKey: (entry) => entry.debounceKey,
41
66
  shouldDebounce: (entry) => {
42
- if (entry.allMedia.length > 0)
67
+ if (entry.allMedia.length > 0) {
43
68
  return false;
69
+ }
44
70
  const text = entry.msg.text ?? entry.msg.caption ?? "";
45
- if (!text.trim())
71
+ if (!text.trim()) {
46
72
  return false;
73
+ }
47
74
  return !hasControlCommand(text, cfg, { botUsername: entry.botUsername });
48
75
  },
49
76
  onFlush: async (entries) => {
50
77
  const last = entries.at(-1);
51
- if (!last)
78
+ if (!last) {
52
79
  return;
80
+ }
53
81
  if (entries.length === 1) {
54
82
  await processMessage(last.ctx, last.allMedia, last.storeAllowFrom);
55
83
  return;
@@ -58,21 +86,18 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
58
86
  .map((entry) => entry.msg.text ?? entry.msg.caption ?? "")
59
87
  .filter(Boolean)
60
88
  .join("\n");
61
- if (!combinedText.trim())
89
+ if (!combinedText.trim()) {
62
90
  return;
91
+ }
63
92
  const first = entries[0];
64
93
  const baseCtx = first.ctx;
65
- const getFile = typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
66
- const syntheticMessage = {
67
- ...first.msg,
94
+ const syntheticMessage = buildSyntheticTextMessage({
95
+ base: first.msg,
68
96
  text: combinedText,
69
- caption: undefined,
70
- caption_entities: undefined,
71
- entities: undefined,
72
97
  date: last.msg.date ?? first.msg.date,
73
- };
98
+ });
74
99
  const messageIdOverride = last.msg.message_id ? String(last.msg.message_id) : undefined;
75
- await processMessage({ message: syntheticMessage, me: baseCtx.me, getFile }, [], first.storeAllowFrom, messageIdOverride ? { messageIdOverride } : undefined);
100
+ await processMessage(buildSyntheticContext(baseCtx, syntheticMessage), [], first.storeAllowFrom, messageIdOverride ? { messageIdOverride } : undefined);
76
101
  },
77
102
  onError: (err) => {
78
103
  runtime.error?.(danger(`telegram debounce flush failed: ${String(err)}`));
@@ -97,7 +122,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
97
122
  channel: "telegram",
98
123
  accountId,
99
124
  peer: {
100
- kind: params.isGroup ? "group" : "dm",
125
+ kind: params.isGroup ? "group" : "direct",
101
126
  id: peerId,
102
127
  },
103
128
  parentPeer,
@@ -145,7 +170,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
145
170
  });
146
171
  }
147
172
  }
148
- const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
173
+ const storeAllowFrom = await loadStoreAllowFrom();
149
174
  await processMessage(primaryEntry.ctx, allMedia, storeAllowFrom);
150
175
  }
151
176
  catch (err) {
@@ -157,82 +182,415 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
157
182
  entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id);
158
183
  const first = entry.messages[0];
159
184
  const last = entry.messages.at(-1);
160
- if (!first || !last)
185
+ if (!first || !last) {
161
186
  return;
187
+ }
162
188
  const combinedText = entry.messages.map((m) => m.msg.text ?? "").join("");
163
- if (!combinedText.trim())
189
+ if (!combinedText.trim()) {
164
190
  return;
165
- const syntheticMessage = {
166
- ...first.msg,
191
+ }
192
+ const syntheticMessage = buildSyntheticTextMessage({
193
+ base: first.msg,
167
194
  text: combinedText,
168
- caption: undefined,
169
- caption_entities: undefined,
170
- entities: undefined,
171
195
  date: last.msg.date ?? first.msg.date,
172
- };
173
- const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
196
+ });
197
+ const storeAllowFrom = await loadStoreAllowFrom();
174
198
  const baseCtx = first.ctx;
175
- const getFile = typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
176
- await processMessage({ message: syntheticMessage, me: baseCtx.me, getFile }, [], storeAllowFrom, { messageIdOverride: String(last.msg.message_id) });
199
+ await processMessage(buildSyntheticContext(baseCtx, syntheticMessage), [], storeAllowFrom, {
200
+ messageIdOverride: String(last.msg.message_id),
201
+ });
177
202
  }
178
203
  catch (err) {
179
204
  runtime.error?.(danger(`text fragment handler failed: ${String(err)}`));
180
205
  }
181
206
  };
207
+ const queueTextFragmentFlush = async (entry) => {
208
+ textFragmentProcessing = textFragmentProcessing
209
+ .then(async () => {
210
+ await flushTextFragments(entry);
211
+ })
212
+ .catch(() => undefined);
213
+ await textFragmentProcessing;
214
+ };
215
+ const runTextFragmentFlush = async (entry) => {
216
+ textFragmentBuffer.delete(entry.key);
217
+ await queueTextFragmentFlush(entry);
218
+ };
182
219
  const scheduleTextFragmentFlush = (entry) => {
183
220
  clearTimeout(entry.timer);
184
221
  entry.timer = setTimeout(async () => {
185
- textFragmentBuffer.delete(entry.key);
186
- textFragmentProcessing = textFragmentProcessing
187
- .then(async () => {
188
- await flushTextFragments(entry);
189
- })
190
- .catch(() => undefined);
191
- await textFragmentProcessing;
222
+ await runTextFragmentFlush(entry);
192
223
  }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS);
193
224
  };
225
+ const loadStoreAllowFrom = async () => readChannelAllowFromStore("telegram", process.env, accountId).catch(() => []);
226
+ const isAllowlistAuthorized = (allow, senderId, senderUsername) => allow.hasWildcard ||
227
+ (allow.hasEntries &&
228
+ isSenderAllowed({
229
+ allow,
230
+ senderId,
231
+ senderUsername,
232
+ }));
233
+ const shouldSkipGroupMessage = (params) => {
234
+ const { isGroup, chatId, chatTitle, resolvedThreadId, senderId, senderUsername, effectiveGroupAllow, hasGroupAllowOverride, groupConfig, topicConfig, } = params;
235
+ const baseAccess = evaluateTelegramGroupBaseAccess({
236
+ isGroup,
237
+ groupConfig,
238
+ topicConfig,
239
+ hasGroupAllowOverride,
240
+ effectiveGroupAllow,
241
+ senderId,
242
+ senderUsername,
243
+ enforceAllowOverride: true,
244
+ requireSenderForAllowOverride: true,
245
+ });
246
+ if (!baseAccess.allowed) {
247
+ if (baseAccess.reason === "group-disabled") {
248
+ logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
249
+ return true;
250
+ }
251
+ if (baseAccess.reason === "topic-disabled") {
252
+ logVerbose(`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`);
253
+ return true;
254
+ }
255
+ logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`);
256
+ return true;
257
+ }
258
+ if (!isGroup) {
259
+ return false;
260
+ }
261
+ const policyAccess = evaluateTelegramGroupPolicyAccess({
262
+ isGroup,
263
+ chatId,
264
+ cfg,
265
+ telegramCfg,
266
+ topicConfig,
267
+ groupConfig,
268
+ effectiveGroupAllow,
269
+ senderId,
270
+ senderUsername,
271
+ resolveGroupPolicy,
272
+ enforcePolicy: true,
273
+ useTopicAndGroupOverrides: true,
274
+ enforceAllowlistAuthorization: true,
275
+ allowEmptyAllowlistEntries: false,
276
+ requireSenderForAllowlistAuthorization: true,
277
+ checkChatAllowlist: true,
278
+ });
279
+ if (!policyAccess.allowed) {
280
+ if (policyAccess.reason === "group-policy-disabled") {
281
+ logVerbose("Blocked telegram group message (groupPolicy: disabled)");
282
+ return true;
283
+ }
284
+ if (policyAccess.reason === "group-policy-allowlist-no-sender") {
285
+ logVerbose("Blocked telegram group message (no sender ID, groupPolicy: allowlist)");
286
+ return true;
287
+ }
288
+ if (policyAccess.reason === "group-policy-allowlist-empty") {
289
+ logVerbose("Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)");
290
+ return true;
291
+ }
292
+ if (policyAccess.reason === "group-policy-allowlist-unauthorized") {
293
+ logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
294
+ return true;
295
+ }
296
+ logger.info({ chatId, title: chatTitle, reason: "not-allowed" }, "skipping group message");
297
+ return true;
298
+ }
299
+ return false;
300
+ };
301
+ // Handle emoji reactions to messages.
302
+ bot.on("message_reaction", async (ctx) => {
303
+ try {
304
+ const reaction = ctx.messageReaction;
305
+ if (!reaction) {
306
+ return;
307
+ }
308
+ if (shouldSkipUpdate(ctx)) {
309
+ return;
310
+ }
311
+ const chatId = reaction.chat.id;
312
+ const messageId = reaction.message_id;
313
+ const user = reaction.user;
314
+ // Resolve reaction notification mode (default: "own").
315
+ const reactionMode = telegramCfg.reactionNotifications ?? "own";
316
+ if (reactionMode === "off") {
317
+ return;
318
+ }
319
+ if (user?.is_bot) {
320
+ return;
321
+ }
322
+ if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) {
323
+ return;
324
+ }
325
+ // Detect added reactions.
326
+ const oldEmojis = new Set(reaction.old_reaction
327
+ .filter((r) => r.type === "emoji")
328
+ .map((r) => r.emoji));
329
+ const addedReactions = reaction.new_reaction
330
+ .filter((r) => r.type === "emoji")
331
+ .filter((r) => !oldEmojis.has(r.emoji));
332
+ if (addedReactions.length === 0) {
333
+ return;
334
+ }
335
+ // Build sender label.
336
+ const senderName = user
337
+ ? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username
338
+ : undefined;
339
+ const senderUsername = user?.username ? `@${user.username}` : undefined;
340
+ let senderLabel = senderName;
341
+ if (senderName && senderUsername) {
342
+ senderLabel = `${senderName} (${senderUsername})`;
343
+ }
344
+ else if (!senderName && senderUsername) {
345
+ senderLabel = senderUsername;
346
+ }
347
+ if (!senderLabel && user?.id) {
348
+ senderLabel = `id:${user.id}`;
349
+ }
350
+ senderLabel = senderLabel || "unknown";
351
+ // Reactions target a specific message_id; the Telegram Bot API does not include
352
+ // message_thread_id on MessageReactionUpdated, so we route to the chat-level
353
+ // session (forum topic routing is not available for reactions).
354
+ const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
355
+ const isForum = reaction.chat.is_forum === true;
356
+ const resolvedThreadId = isForum
357
+ ? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
358
+ : undefined;
359
+ const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
360
+ const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
361
+ // Fresh config for bindings lookup; other routing inputs are payload-derived.
362
+ const route = resolveAgentRoute({
363
+ cfg: loadConfig(),
364
+ channel: "telegram",
365
+ accountId,
366
+ peer: { kind: isGroup ? "group" : "direct", id: peerId },
367
+ parentPeer,
368
+ });
369
+ const sessionKey = route.sessionKey;
370
+ // Enqueue system event for each added reaction.
371
+ for (const r of addedReactions) {
372
+ const emoji = r.emoji;
373
+ const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`;
374
+ enqueueSystemEvent(text, {
375
+ sessionKey,
376
+ contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`,
377
+ });
378
+ logVerbose(`telegram: reaction event enqueued: ${text}`);
379
+ }
380
+ }
381
+ catch (err) {
382
+ runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`));
383
+ }
384
+ });
385
+ const processInboundMessage = async (params) => {
386
+ const { ctx, msg, chatId, resolvedThreadId, storeAllowFrom, sendOversizeWarning, oversizeLogMessage, } = params;
387
+ // Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
388
+ // We buffer “near-limit” messages and append immediately-following parts.
389
+ const text = typeof msg.text === "string" ? msg.text : undefined;
390
+ const isCommandLike = (text ?? "").trim().startsWith("/");
391
+ if (text && !isCommandLike) {
392
+ const nowMs = Date.now();
393
+ const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
394
+ const key = `text:${chatId}:${resolvedThreadId ?? "main"}:${senderId}`;
395
+ const existing = textFragmentBuffer.get(key);
396
+ if (existing) {
397
+ const last = existing.messages.at(-1);
398
+ const lastMsgId = last?.msg.message_id;
399
+ const lastReceivedAtMs = last?.receivedAtMs ?? nowMs;
400
+ const idGap = typeof lastMsgId === "number" ? msg.message_id - lastMsgId : Infinity;
401
+ const timeGapMs = nowMs - lastReceivedAtMs;
402
+ const canAppend = idGap > 0 &&
403
+ idGap <= TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP &&
404
+ timeGapMs >= 0 &&
405
+ timeGapMs <= TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS;
406
+ if (canAppend) {
407
+ const currentTotalChars = existing.messages.reduce((sum, m) => sum + (m.msg.text?.length ?? 0), 0);
408
+ const nextTotalChars = currentTotalChars + text.length;
409
+ if (existing.messages.length + 1 <= TELEGRAM_TEXT_FRAGMENT_MAX_PARTS &&
410
+ nextTotalChars <= TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS) {
411
+ existing.messages.push({ msg, ctx, receivedAtMs: nowMs });
412
+ scheduleTextFragmentFlush(existing);
413
+ return;
414
+ }
415
+ }
416
+ // Not appendable (or limits exceeded): flush buffered entry first, then continue normally.
417
+ clearTimeout(existing.timer);
418
+ textFragmentBuffer.delete(key);
419
+ textFragmentProcessing = textFragmentProcessing
420
+ .then(async () => {
421
+ await flushTextFragments(existing);
422
+ })
423
+ .catch(() => undefined);
424
+ await textFragmentProcessing;
425
+ }
426
+ const shouldStart = text.length >= TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS;
427
+ if (shouldStart) {
428
+ const entry = {
429
+ key,
430
+ messages: [{ msg, ctx, receivedAtMs: nowMs }],
431
+ timer: setTimeout(() => { }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS),
432
+ };
433
+ textFragmentBuffer.set(key, entry);
434
+ scheduleTextFragmentFlush(entry);
435
+ return;
436
+ }
437
+ }
438
+ // Media group handling - buffer multi-image messages
439
+ const mediaGroupId = msg.media_group_id;
440
+ if (mediaGroupId) {
441
+ const existing = mediaGroupBuffer.get(mediaGroupId);
442
+ if (existing) {
443
+ clearTimeout(existing.timer);
444
+ existing.messages.push({ msg, ctx });
445
+ existing.timer = setTimeout(async () => {
446
+ mediaGroupBuffer.delete(mediaGroupId);
447
+ mediaGroupProcessing = mediaGroupProcessing
448
+ .then(async () => {
449
+ await processMediaGroup(existing);
450
+ })
451
+ .catch(() => undefined);
452
+ await mediaGroupProcessing;
453
+ }, mediaGroupTimeoutMs);
454
+ }
455
+ else {
456
+ const entry = {
457
+ messages: [{ msg, ctx }],
458
+ timer: setTimeout(async () => {
459
+ mediaGroupBuffer.delete(mediaGroupId);
460
+ mediaGroupProcessing = mediaGroupProcessing
461
+ .then(async () => {
462
+ await processMediaGroup(entry);
463
+ })
464
+ .catch(() => undefined);
465
+ await mediaGroupProcessing;
466
+ }, mediaGroupTimeoutMs),
467
+ };
468
+ mediaGroupBuffer.set(mediaGroupId, entry);
469
+ }
470
+ return;
471
+ }
472
+ let media = null;
473
+ try {
474
+ media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
475
+ }
476
+ catch (mediaErr) {
477
+ const errMsg = String(mediaErr);
478
+ if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
479
+ if (sendOversizeWarning) {
480
+ const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
481
+ await withTelegramApiErrorLogging({
482
+ operation: "sendMessage",
483
+ runtime,
484
+ fn: () => bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
485
+ reply_to_message_id: msg.message_id,
486
+ }),
487
+ }).catch(() => { });
488
+ }
489
+ logger.warn({ chatId, error: errMsg }, oversizeLogMessage);
490
+ return;
491
+ }
492
+ throw mediaErr;
493
+ }
494
+ // Skip sticker-only messages where the sticker was skipped (animated/video)
495
+ // These have no media and no text content to process.
496
+ const hasText = Boolean((msg.text ?? msg.caption ?? "").trim());
497
+ if (msg.sticker && !media && !hasText) {
498
+ logVerbose("telegram: skipping sticker-only message (unsupported sticker type)");
499
+ return;
500
+ }
501
+ const allMedia = media
502
+ ? [
503
+ {
504
+ path: media.path,
505
+ contentType: media.contentType,
506
+ stickerMetadata: media.stickerMetadata,
507
+ },
508
+ ]
509
+ : [];
510
+ const senderId = msg.from?.id ? String(msg.from.id) : "";
511
+ const conversationKey = resolvedThreadId != null ? `${chatId}:topic:${resolvedThreadId}` : String(chatId);
512
+ const debounceKey = senderId
513
+ ? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}`
514
+ : null;
515
+ await inboundDebouncer.enqueue({
516
+ ctx,
517
+ msg,
518
+ allMedia,
519
+ storeAllowFrom,
520
+ debounceKey,
521
+ botUsername: ctx.me?.username,
522
+ });
523
+ };
194
524
  bot.on("callback_query", async (ctx) => {
195
525
  const callback = ctx.callbackQuery;
196
- if (!callback)
526
+ if (!callback) {
197
527
  return;
198
- if (shouldSkipUpdate(ctx))
528
+ }
529
+ if (shouldSkipUpdate(ctx)) {
199
530
  return;
531
+ }
532
+ const answerCallbackQuery = typeof ctx.answerCallbackQuery === "function"
533
+ ? () => ctx.answerCallbackQuery()
534
+ : () => bot.api.answerCallbackQuery(callback.id);
200
535
  // Answer immediately to prevent Telegram from retrying while we process
201
536
  await withTelegramApiErrorLogging({
202
537
  operation: "answerCallbackQuery",
203
538
  runtime,
204
- fn: () => bot.api.answerCallbackQuery(callback.id),
539
+ fn: answerCallbackQuery,
205
540
  }).catch(() => { });
206
541
  try {
207
542
  const data = (callback.data ?? "").trim();
208
543
  const callbackMessage = callback.message;
209
- if (!data || !callbackMessage)
544
+ if (!data || !callbackMessage) {
210
545
  return;
546
+ }
547
+ const editCallbackMessage = async (text, params) => {
548
+ const editTextFn = ctx.editMessageText;
549
+ if (typeof editTextFn === "function") {
550
+ return await ctx.editMessageText(text, params);
551
+ }
552
+ return await bot.api.editMessageText(callbackMessage.chat.id, callbackMessage.message_id, text, params);
553
+ };
554
+ const deleteCallbackMessage = async () => {
555
+ const deleteFn = ctx.deleteMessage;
556
+ if (typeof deleteFn === "function") {
557
+ return await ctx.deleteMessage();
558
+ }
559
+ return await bot.api.deleteMessage(callbackMessage.chat.id, callbackMessage.message_id);
560
+ };
561
+ const replyToCallbackChat = async (text, params) => {
562
+ const replyFn = ctx.reply;
563
+ if (typeof replyFn === "function") {
564
+ return await ctx.reply(text, params);
565
+ }
566
+ return await bot.api.sendMessage(callbackMessage.chat.id, text, params);
567
+ };
211
568
  const inlineButtonsScope = resolveTelegramInlineButtonsScope({
212
569
  cfg,
213
570
  accountId,
214
571
  });
215
- if (inlineButtonsScope === "off")
572
+ if (inlineButtonsScope === "off") {
216
573
  return;
574
+ }
217
575
  const chatId = callbackMessage.chat.id;
218
576
  const isGroup = callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";
219
- if (inlineButtonsScope === "dm" && isGroup)
577
+ if (inlineButtonsScope === "dm" && isGroup) {
220
578
  return;
221
- if (inlineButtonsScope === "group" && !isGroup)
579
+ }
580
+ if (inlineButtonsScope === "group" && !isGroup) {
222
581
  return;
582
+ }
223
583
  const messageThreadId = callbackMessage.message_thread_id;
224
584
  const isForum = callbackMessage.chat.is_forum === true;
225
- const resolvedThreadId = resolveTelegramForumThreadId({
585
+ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
586
+ chatId,
587
+ accountId,
226
588
  isForum,
227
589
  messageThreadId,
590
+ groupAllowFrom,
591
+ resolveTelegramGroupConfig,
228
592
  });
229
- const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
230
- const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
231
- const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
232
- const effectiveGroupAllow = normalizeAllowFromWithStore({
233
- allowFrom: groupAllowOverride ?? groupAllowFrom,
234
- storeAllowFrom,
235
- });
593
+ const { resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext;
236
594
  const effectiveDmAllow = normalizeAllowFromWithStore({
237
595
  allowFrom: telegramCfg.allowFrom,
238
596
  storeAllowFrom,
@@ -240,93 +598,49 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
240
598
  const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
241
599
  const senderId = callback.from?.id ? String(callback.from.id) : "";
242
600
  const senderUsername = callback.from?.username ?? "";
243
- if (isGroup) {
244
- if (groupConfig?.enabled === false) {
245
- logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
246
- return;
247
- }
248
- if (topicConfig?.enabled === false) {
249
- logVerbose(`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`);
250
- return;
251
- }
252
- if (typeof groupAllowOverride !== "undefined") {
253
- const allowed = senderId &&
254
- isSenderAllowed({
255
- allow: effectiveGroupAllow,
256
- senderId,
257
- senderUsername,
258
- });
259
- if (!allowed) {
260
- logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`);
261
- return;
262
- }
263
- }
264
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
265
- const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
266
- if (groupPolicy === "disabled") {
267
- logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
268
- return;
269
- }
270
- if (groupPolicy === "allowlist") {
271
- if (!senderId) {
272
- logVerbose(`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`);
273
- return;
274
- }
275
- if (!effectiveGroupAllow.hasEntries) {
276
- logVerbose("Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)");
277
- return;
278
- }
279
- if (!isSenderAllowed({
280
- allow: effectiveGroupAllow,
281
- senderId,
282
- senderUsername,
283
- })) {
284
- logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
285
- return;
286
- }
287
- }
288
- const groupAllowlist = resolveGroupPolicy(chatId);
289
- if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
290
- logger.info({ chatId, title: callbackMessage.chat.title, reason: "not-allowed" }, "skipping group message");
291
- return;
292
- }
601
+ if (shouldSkipGroupMessage({
602
+ isGroup,
603
+ chatId,
604
+ chatTitle: callbackMessage.chat.title,
605
+ resolvedThreadId,
606
+ senderId,
607
+ senderUsername,
608
+ effectiveGroupAllow,
609
+ hasGroupAllowOverride,
610
+ groupConfig,
611
+ topicConfig,
612
+ })) {
613
+ return;
293
614
  }
294
615
  if (inlineButtonsScope === "allowlist") {
295
616
  if (!isGroup) {
296
- if (dmPolicy === "disabled")
617
+ if (dmPolicy === "disabled") {
297
618
  return;
619
+ }
298
620
  if (dmPolicy !== "open") {
299
- const allowed = effectiveDmAllow.hasWildcard ||
300
- (effectiveDmAllow.hasEntries &&
301
- isSenderAllowed({
302
- allow: effectiveDmAllow,
303
- senderId,
304
- senderUsername,
305
- }));
306
- if (!allowed)
621
+ const allowed = isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername);
622
+ if (!allowed) {
307
623
  return;
624
+ }
308
625
  }
309
626
  }
310
627
  else {
311
- const allowed = effectiveGroupAllow.hasWildcard ||
312
- (effectiveGroupAllow.hasEntries &&
313
- isSenderAllowed({
314
- allow: effectiveGroupAllow,
315
- senderId,
316
- senderUsername,
317
- }));
318
- if (!allowed)
628
+ const allowed = isAllowlistAuthorized(effectiveGroupAllow, senderId, senderUsername);
629
+ if (!allowed) {
319
630
  return;
631
+ }
320
632
  }
321
633
  }
322
634
  const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/);
323
635
  if (paginationMatch) {
324
636
  const pageValue = paginationMatch[1];
325
- if (pageValue === "noop")
637
+ if (pageValue === "noop") {
326
638
  return;
639
+ }
327
640
  const page = Number.parseInt(pageValue, 10);
328
- if (Number.isNaN(page) || page < 1)
641
+ if (Number.isNaN(page) || page < 1) {
329
642
  return;
643
+ }
330
644
  const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined;
331
645
  const skillCommands = listSkillCommandsForAgents({
332
646
  cfg,
@@ -340,7 +654,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
340
654
  ? buildInlineKeyboard(buildCommandsPaginationKeyboard(result.currentPage, result.totalPages, agentId))
341
655
  : undefined;
342
656
  try {
343
- await bot.api.editMessageText(callbackMessage.chat.id, callbackMessage.message_id, result.text, keyboard ? { reply_markup: keyboard } : undefined);
657
+ await editCallbackMessage(result.text, keyboard ? { reply_markup: keyboard } : undefined);
344
658
  }
345
659
  catch (editErr) {
346
660
  const errStr = String(editErr);
@@ -358,11 +672,18 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
358
672
  const editMessageWithButtons = async (text, buttons) => {
359
673
  const keyboard = buildInlineKeyboard(buttons);
360
674
  try {
361
- await bot.api.editMessageText(callbackMessage.chat.id, callbackMessage.message_id, text, keyboard ? { reply_markup: keyboard } : undefined);
675
+ await editCallbackMessage(text, keyboard ? { reply_markup: keyboard } : undefined);
362
676
  }
363
677
  catch (editErr) {
364
678
  const errStr = String(editErr);
365
- if (!errStr.includes("message is not modified")) {
679
+ if (errStr.includes("no text in the message")) {
680
+ try {
681
+ await deleteCallbackMessage();
682
+ }
683
+ catch { }
684
+ await replyToCallbackChat(text, keyboard ? { reply_markup: keyboard } : undefined);
685
+ }
686
+ else if (!errStr.includes("message is not modified")) {
366
687
  throw editErr;
367
688
  }
368
689
  }
@@ -384,6 +705,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
384
705
  const { provider, page } = modelCallback;
385
706
  const modelSet = byProvider.get(provider);
386
707
  if (!modelSet || modelSet.size === 0) {
708
+ // Provider not found or no models - show providers list
387
709
  const providerInfos = providers.map((p) => ({
388
710
  id: p,
389
711
  count: byProvider.get(p)?.size ?? 0,
@@ -396,6 +718,7 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
396
718
  const pageSize = getModelsPageSize();
397
719
  const totalPages = calculateTotalPages(models.length, pageSize);
398
720
  const safePage = Math.max(1, Math.min(page, totalPages));
721
+ // Resolve current model from session (prefer overrides)
399
722
  const currentModel = resolveTelegramSessionModel({
400
723
  chatId,
401
724
  isGroup,
@@ -417,16 +740,13 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
417
740
  }
418
741
  if (modelCallback.type === "select") {
419
742
  const { provider, model } = modelCallback;
420
- const syntheticMessage = {
421
- ...callbackMessage,
743
+ // Process model selection as a synthetic message with /model command
744
+ const syntheticMessage = buildSyntheticTextMessage({
745
+ base: callbackMessage,
422
746
  from: callback.from,
423
747
  text: `/model ${provider}/${model}`,
424
- caption: undefined,
425
- caption_entities: undefined,
426
- entities: undefined,
427
- };
428
- const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
429
- await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
748
+ });
749
+ await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
430
750
  forceWasMentioned: true,
431
751
  messageIdOverride: callback.id,
432
752
  });
@@ -434,16 +754,12 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
434
754
  }
435
755
  return;
436
756
  }
437
- const syntheticMessage = {
438
- ...callbackMessage,
757
+ const syntheticMessage = buildSyntheticTextMessage({
758
+ base: callbackMessage,
439
759
  from: callback.from,
440
760
  text: data,
441
- caption: undefined,
442
- caption_entities: undefined,
443
- entities: undefined,
444
- };
445
- const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({});
446
- await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
761
+ });
762
+ await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
447
763
  forceWasMentioned: true,
448
764
  messageIdOverride: callback.id,
449
765
  });
@@ -456,10 +772,12 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
456
772
  bot.on("message:migrate_to_chat_id", async (ctx) => {
457
773
  try {
458
774
  const msg = ctx.message;
459
- if (!msg?.migrate_to_chat_id)
775
+ if (!msg?.migrate_to_chat_id) {
460
776
  return;
461
- if (shouldSkipUpdate(ctx))
777
+ }
778
+ if (shouldSkipUpdate(ctx)) {
462
779
  return;
780
+ }
463
781
  const oldChatId = String(msg.chat.id);
464
782
  const newChatId = String(msg.migrate_to_chat_id);
465
783
  const chatTitle = msg.chat.title ?? "Unknown";
@@ -496,224 +814,167 @@ export const registerTelegramHandlers = ({ cfg, accountId, bot, opts, runtime, m
496
814
  bot.on("message", async (ctx) => {
497
815
  try {
498
816
  const msg = ctx.message;
499
- if (!msg)
817
+ if (!msg) {
500
818
  return;
501
- if (shouldSkipUpdate(ctx))
819
+ }
820
+ if (shouldSkipUpdate(ctx)) {
502
821
  return;
822
+ }
503
823
  const chatId = msg.chat.id;
504
824
  const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
505
825
  const messageThreadId = msg.message_thread_id;
506
826
  const isForum = msg.chat.is_forum === true;
507
- const resolvedThreadId = resolveTelegramForumThreadId({
827
+ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
828
+ chatId,
829
+ accountId,
508
830
  isForum,
509
831
  messageThreadId,
832
+ groupAllowFrom,
833
+ resolveTelegramGroupConfig,
510
834
  });
511
- const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
512
- const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
513
- const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
514
- const effectiveGroupAllow = normalizeAllowFromWithStore({
515
- allowFrom: groupAllowOverride ?? groupAllowFrom,
835
+ const { resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext;
836
+ const senderId = msg.from?.id != null ? String(msg.from.id) : "";
837
+ const senderUsername = msg.from?.username ?? "";
838
+ if (shouldSkipGroupMessage({
839
+ isGroup,
840
+ chatId,
841
+ chatTitle: msg.chat.title,
842
+ resolvedThreadId,
843
+ senderId,
844
+ senderUsername,
845
+ effectiveGroupAllow,
846
+ hasGroupAllowOverride,
847
+ groupConfig,
848
+ topicConfig,
849
+ })) {
850
+ return;
851
+ }
852
+ await processInboundMessage({
853
+ ctx,
854
+ msg,
855
+ chatId,
856
+ resolvedThreadId,
516
857
  storeAllowFrom,
858
+ sendOversizeWarning: true,
859
+ oversizeLogMessage: "media exceeds size limit",
517
860
  });
518
- const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
519
- if (isGroup) {
520
- if (groupConfig?.enabled === false) {
521
- logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
522
- return;
523
- }
524
- if (topicConfig?.enabled === false) {
525
- logVerbose(`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`);
526
- return;
527
- }
528
- if (hasGroupAllowOverride) {
529
- const senderId = msg.from?.id;
530
- const senderUsername = msg.from?.username ?? "";
531
- const allowed = senderId != null &&
532
- isSenderAllowed({
533
- allow: effectiveGroupAllow,
534
- senderId: String(senderId),
535
- senderUsername,
536
- });
537
- if (!allowed) {
538
- logVerbose(`Blocked telegram group sender ${senderId ?? "unknown"} (group allowFrom override)`);
539
- return;
540
- }
541
- }
542
- // Group policy filtering: controls how group messages are handled
543
- // - "open": groups bypass allowFrom, only mention-gating applies
544
- // - "disabled": block all group messages entirely
545
- // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
546
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
547
- const groupPolicy = firstDefined(topicConfig?.groupPolicy, groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
548
- if (groupPolicy === "disabled") {
549
- logVerbose(`Blocked telegram group message (groupPolicy: disabled)`);
550
- return;
551
- }
552
- if (groupPolicy === "allowlist") {
553
- // For allowlist mode, the sender (msg.from.id) must be in allowFrom
554
- const senderId = msg.from?.id;
555
- if (senderId == null) {
556
- logVerbose(`Blocked telegram group message (no sender ID, groupPolicy: allowlist)`);
557
- return;
558
- }
559
- if (!effectiveGroupAllow.hasEntries) {
560
- logVerbose("Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)");
561
- return;
562
- }
563
- const senderUsername = msg.from?.username ?? "";
564
- if (!isSenderAllowed({
861
+ }
862
+ catch (err) {
863
+ runtime.error?.(danger(`handler failed: ${String(err)}`));
864
+ }
865
+ });
866
+ // Handle channel posts — enables bot-to-bot communication via Telegram channels.
867
+ // Telegram bots cannot see other bot messages in groups, but CAN in channels.
868
+ // This handler normalizes channel_post updates into the standard message pipeline.
869
+ bot.on("channel_post", async (ctx) => {
870
+ try {
871
+ const post = ctx.channelPost;
872
+ if (!post) {
873
+ return;
874
+ }
875
+ // Deduplication check — same as the regular message handler
876
+ if (shouldSkipUpdate(ctx)) {
877
+ return;
878
+ }
879
+ const chatId = post.chat.id;
880
+ // Use the full group allow-from context for access control (same as message handler)
881
+ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
882
+ chatId,
883
+ accountId,
884
+ isForum: false,
885
+ messageThreadId: undefined,
886
+ groupAllowFrom,
887
+ resolveTelegramGroupConfig,
888
+ });
889
+ const { storeAllowFrom, groupConfig, effectiveGroupAllow, hasGroupAllowOverride } = groupAllowContext;
890
+ // Check group allowlist (channels use the same groups config)
891
+ const groupAllowlist = resolveGroupPolicy(chatId);
892
+ if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
893
+ return;
894
+ }
895
+ if (!groupConfig || groupConfig.enabled === false) {
896
+ logVerbose(`Blocked telegram channel ${chatId} (channel disabled)`);
897
+ return;
898
+ }
899
+ // Group policy filtering (same as message handler)
900
+ const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
901
+ const groupPolicy = firstDefined(groupConfig?.groupPolicy, telegramCfg.groupPolicy, defaultGroupPolicy, "open");
902
+ if (groupPolicy === "disabled") {
903
+ logVerbose(`Blocked telegram channel message (groupPolicy: disabled)`);
904
+ return;
905
+ }
906
+ if (hasGroupAllowOverride) {
907
+ const senderId = post.sender_chat?.id ?? post.from?.id;
908
+ const senderUsername = post.sender_chat?.username ?? post.from?.username ?? "";
909
+ const allowed = senderId != null &&
910
+ isSenderAllowed({
565
911
  allow: effectiveGroupAllow,
566
912
  senderId: String(senderId),
567
913
  senderUsername,
568
- })) {
569
- logVerbose(`Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`);
570
- return;
571
- }
572
- }
573
- // Group allowlist based on configured group IDs.
574
- const groupAllowlist = resolveGroupPolicy(chatId);
575
- if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
576
- logger.info({ chatId, title: msg.chat.title, reason: "not-allowed" }, "skipping group message");
914
+ });
915
+ if (!allowed) {
916
+ logVerbose(`Blocked telegram channel sender ${senderId ?? "unknown"} (group allowFrom override)`);
577
917
  return;
578
918
  }
579
919
  }
580
- // Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
581
- // We buffer “near-limit” messages and append immediately-following parts.
582
- const text = typeof msg.text === "string" ? msg.text : undefined;
583
- const isCommandLike = (text ?? "").trim().startsWith("/");
584
- if (text && !isCommandLike) {
585
- const nowMs = Date.now();
586
- const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
587
- const key = `text:${chatId}:${resolvedThreadId ?? "main"}:${senderId}`;
588
- const existing = textFragmentBuffer.get(key);
589
- if (existing) {
590
- const last = existing.messages.at(-1);
591
- const lastMsgId = last?.msg.message_id;
592
- const lastReceivedAtMs = last?.receivedAtMs ?? nowMs;
593
- const idGap = typeof lastMsgId === "number" ? msg.message_id - lastMsgId : Infinity;
594
- const timeGapMs = nowMs - lastReceivedAtMs;
595
- const canAppend = idGap > 0 &&
596
- idGap <= TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP &&
597
- timeGapMs >= 0 &&
598
- timeGapMs <= TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS;
599
- if (canAppend) {
600
- const currentTotalChars = existing.messages.reduce((sum, m) => sum + (m.msg.text?.length ?? 0), 0);
601
- const nextTotalChars = currentTotalChars + text.length;
602
- if (existing.messages.length + 1 <= TELEGRAM_TEXT_FRAGMENT_MAX_PARTS &&
603
- nextTotalChars <= TELEGRAM_TEXT_FRAGMENT_MAX_TOTAL_CHARS) {
604
- existing.messages.push({ msg, ctx, receivedAtMs: nowMs });
605
- scheduleTextFragmentFlush(existing);
606
- return;
607
- }
608
- }
609
- // Not appendable (or limits exceeded): flush buffered entry first, then continue normally.
610
- clearTimeout(existing.timer);
611
- textFragmentBuffer.delete(key);
612
- textFragmentProcessing = textFragmentProcessing
613
- .then(async () => {
614
- await flushTextFragments(existing);
615
- })
616
- .catch(() => undefined);
617
- await textFragmentProcessing;
618
- }
619
- const shouldStart = text.length >= TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS;
620
- if (shouldStart) {
621
- const entry = {
622
- key,
623
- messages: [{ msg, ctx, receivedAtMs: nowMs }],
624
- timer: setTimeout(() => { }, TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS),
625
- };
626
- textFragmentBuffer.set(key, entry);
627
- scheduleTextFragmentFlush(entry);
920
+ if (groupPolicy === "allowlist") {
921
+ const senderId = post.sender_chat?.id ?? post.from?.id;
922
+ if (senderId == null) {
923
+ logVerbose(`Blocked telegram channel message (no sender ID, groupPolicy: allowlist)`);
628
924
  return;
629
925
  }
630
- }
631
- // Media group handling - buffer multi-image messages
632
- const mediaGroupId = msg.media_group_id;
633
- if (mediaGroupId) {
634
- const existing = mediaGroupBuffer.get(mediaGroupId);
635
- if (existing) {
636
- clearTimeout(existing.timer);
637
- existing.messages.push({ msg, ctx });
638
- existing.timer = setTimeout(async () => {
639
- mediaGroupBuffer.delete(mediaGroupId);
640
- mediaGroupProcessing = mediaGroupProcessing
641
- .then(async () => {
642
- await processMediaGroup(existing);
643
- })
644
- .catch(() => undefined);
645
- await mediaGroupProcessing;
646
- }, MEDIA_GROUP_TIMEOUT_MS);
647
- }
648
- else {
649
- const entry = {
650
- messages: [{ msg, ctx }],
651
- timer: setTimeout(async () => {
652
- mediaGroupBuffer.delete(mediaGroupId);
653
- mediaGroupProcessing = mediaGroupProcessing
654
- .then(async () => {
655
- await processMediaGroup(entry);
656
- })
657
- .catch(() => undefined);
658
- await mediaGroupProcessing;
659
- }, MEDIA_GROUP_TIMEOUT_MS),
660
- };
661
- mediaGroupBuffer.set(mediaGroupId, entry);
926
+ if (!effectiveGroupAllow.hasEntries) {
927
+ logVerbose("Blocked telegram channel message (groupPolicy: allowlist, no allowlist entries)");
928
+ return;
662
929
  }
663
- return;
664
- }
665
- let media = null;
666
- try {
667
- media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
668
- }
669
- catch (mediaErr) {
670
- const errMsg = String(mediaErr);
671
- if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
672
- const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
673
- await withTelegramApiErrorLogging({
674
- operation: "sendMessage",
675
- runtime,
676
- fn: () => bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
677
- reply_to_message_id: msg.message_id,
678
- }),
679
- }).catch(() => { });
680
- logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
930
+ const senderUsername = post.sender_chat?.username ?? post.from?.username ?? "";
931
+ if (!isSenderAllowed({
932
+ allow: effectiveGroupAllow,
933
+ senderId: String(senderId),
934
+ senderUsername,
935
+ })) {
936
+ logVerbose(`Blocked telegram channel message from ${senderId} (groupPolicy: allowlist)`);
681
937
  return;
682
938
  }
683
- throw mediaErr;
684
- }
685
- // Skip sticker-only messages where the sticker was skipped (animated/video)
686
- // These have no media and no text content to process.
687
- const hasText = Boolean((msg.text ?? msg.caption ?? "").trim());
688
- if (msg.sticker && !media && !hasText) {
689
- logVerbose("telegram: skipping sticker-only message (unsupported sticker type)");
690
- return;
691
939
  }
692
- const allMedia = media
693
- ? [
694
- {
695
- path: media.path,
696
- contentType: media.contentType,
697
- stickerMetadata: media.stickerMetadata,
698
- },
699
- ]
700
- : [];
701
- const senderId = msg.from?.id ? String(msg.from.id) : "";
702
- const conversationKey = resolvedThreadId != null ? `${chatId}:topic:${resolvedThreadId}` : String(chatId);
703
- const debounceKey = senderId
704
- ? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}`
705
- : null;
706
- await inboundDebouncer.enqueue({
707
- ctx,
708
- msg,
709
- allMedia,
940
+ // Build a synthetic `from` field since channel posts may not have one.
941
+ // Use sender_chat (the bot/user that posted) if available.
942
+ const syntheticFrom = post.sender_chat
943
+ ? {
944
+ id: post.sender_chat.id,
945
+ is_bot: true,
946
+ first_name: post.sender_chat.title || "Channel",
947
+ username: post.sender_chat.username,
948
+ }
949
+ : {
950
+ id: chatId,
951
+ is_bot: true,
952
+ first_name: post.chat.title || "Channel",
953
+ username: post.chat.username,
954
+ };
955
+ const syntheticMsg = {
956
+ ...post,
957
+ from: post.from ?? syntheticFrom,
958
+ chat: {
959
+ ...post.chat,
960
+ type: "supergroup",
961
+ },
962
+ };
963
+ const syntheticCtx = Object.create(ctx, {
964
+ message: { value: syntheticMsg, writable: true, enumerable: true },
965
+ });
966
+ await processInboundMessage({
967
+ ctx: syntheticCtx,
968
+ msg: syntheticMsg,
969
+ chatId,
970
+ resolvedThreadId: undefined,
710
971
  storeAllowFrom,
711
- debounceKey,
712
- botUsername: ctx.me?.username,
972
+ sendOversizeWarning: false,
973
+ oversizeLogMessage: "channel post media exceeds size limit",
713
974
  });
714
975
  }
715
976
  catch (err) {
716
- runtime.error?.(danger(`handler failed: ${String(err)}`));
977
+ runtime.error?.(danger(`channel_post handler failed: ${String(err)}`));
717
978
  }
718
979
  });
719
980
  };