@desplega.ai/agent-swarm 1.173.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-kr4abrsk.js} +17 -16
- package/dist/{actions-sgeyv08k.js → actions-eh4g6w2r.js} +39 -34
- package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-7h7wdt8q.js} +8 -7
- package/dist/{app-km9qnv2f.js → app-7k8s9w5p.js} +18 -14
- package/dist/{assistant-5r8tcvvd.js → assistant-wswt2y7p.js} +40 -35
- package/dist/{boot-reembed-16rv9843.js → boot-reembed-9apm9qw6.js} +10 -9
- package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-whzbdthj.js} +10 -9
- package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-ndzmvmgt.js} +11 -10
- package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-e9sta0yt.js} +16 -13
- package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-4mqd3r6r.js} +19 -18
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-9wswb52m.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-tpe1effx.js} +6 -4
- package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-q49ghmh9.js} +12402 -11850
- package/dist/{cli-ne7smj1b.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-nwrdx840.js → cli-035vekcw.js} +3 -3
- package/dist/{cli-9xf7fdtp.js → cli-0pd8hzhc.js} +29 -6
- package/dist/{cli-tmhsxhb8.js → cli-0veb92a3.js} +28 -6
- package/dist/{cli-c3rk1v0x.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-6thk4jz3.js → cli-1y7yywmj.js} +6 -6
- package/dist/{cli-6r2f7edk.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-rfs4vddv.js → cli-29rja109.js} +39 -20
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-na2pxe4f.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-7a93cv8c.js → cli-4mengw46.js} +441 -43
- package/dist/{cli-94b48jhk.js → cli-5twdke5x.js} +16 -14
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-w1apjecb.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-r4fqfpwf.js → cli-78vv7446.js} +50 -27
- package/dist/{cli-srqq3gc5.js → cli-7n6s5man.js} +5 -5
- package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
- package/dist/{cli-2bw3w47j.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-ywm1azg9.js → cli-9jzdj90z.js} +8 -4
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/cli-cvv9k0hh.js +10143 -0
- package/dist/{cli-anzdk0z4.js → cli-d9286m9h.js} +2 -2
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-gmdajfw5.js → cli-gxzkg1de.js} +5020 -1729
- package/dist/{cli-451ks6aw.js → cli-h1arqn31.js} +1 -1
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-jpc4r224.js → cli-hqr88ew6.js} +19 -3
- package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
- package/dist/{cli-cd0hv9nh.js → cli-j67mh2vg.js} +2 -2
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-d603qf86.js → cli-ksphgh48.js} +10 -8
- package/dist/{cli-3nxdmjrc.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-saqyage0.js → cli-mz5as5ha.js} +179 -27
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-ppy4nnnh.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1w0vj3gw.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-0a7mm3zp.js → cli-nm1p8jfr.js} +25 -21
- package/dist/{cli-wrh08zjd.js → cli-p3zbc1rw.js} +5 -18
- package/dist/{cli-hsem16hb.js → cli-pe05f8yk.js} +29 -6
- package/dist/{cli-5wpw4rab.js → cli-qmzsbkvy.js} +5 -5
- package/dist/{cli-bv2etw2y.js → cli-qnm4e9e4.js} +99 -590
- package/dist/{cli-1x7da0de.js → cli-qqy4q48b.js} +8 -8
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-5er5gjbm.js → cli-r35rw5de.js} +15 -15
- package/dist/{cli-v3zhnxsb.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-7jykv7ef.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-x119kdba.js → cli-rdxm77k3.js} +1 -1
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-0p3enk35.js → cli-s69nfyeh.js} +9 -1
- package/dist/{cli-0kxf81jj.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-e3pmyt39.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-d161g69q.js → cli-shk7r5cs.js} +57 -6
- package/dist/{cli-7pz3b7x5.js → cli-sksetb2q.js} +36 -1
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-db3m6zdt.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
- package/dist/{cli-s8z6x4b4.js → cli-v7g551te.js} +225 -113
- package/dist/{cli-rfz2ve55.js → cli-vgqzp4em.js} +3341 -955
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-qm1448f9.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-tadfgd0t.js → cli-x1r545x8.js} +33 -4
- package/dist/{cli-xc7p7s3g.js → cli-xczsgshn.js} +6 -6
- package/dist/{cli-fmhj1ehj.js → cli-y3167wt1.js} +882 -271
- package/dist/{cli-r0byd61h.js → cli-yjmqpxt3.js} +17 -16
- package/dist/{cli-81r1wjyv.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-phcqp1b9.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +34 -49
- package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-4z16v9n0.js} +15 -14
- package/dist/{codex-login-841kv5fk.js → codex-login-pkepy2j1.js} +3 -2
- package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-rvms9y2x.js} +15 -14
- package/dist/{commands-z8fq9qna.js → commands-2a9rtyat.js} +11 -10
- package/dist/{controller-hq5ggpcp.js → controller-3k3madbn.js} +5 -4
- package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-v9yn8zb9.js} +42 -37
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-98gbhyzn.js} +7 -6
- package/dist/{db-qfpq4bt7.js → db-eywwsnjw.js} +30 -9
- package/dist/{db-yyhqqrbf.js → db-ncypk9np.js} +12 -11
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-zeq8b9gz.js} +3 -2
- package/dist/{dispatcher-x0x7qfcq.js → dispatcher-w94kc575.js} +38 -33
- package/dist/{docker-f503k9p0.js → docker-b1e4cqab.js} +3 -2
- package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-47ztej36.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-bbt8ey45.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-dp804sen.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-j2jdg3qb.js} +3 -2
- package/dist/{external-channel-h1fjecwe.js → external-channel-zrkg8ar9.js} +11 -10
- package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
- package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
- package/dist/{grok-adapter-e7mr287e.js → grok-adapter-s9ncp9vy.js} +18 -17
- package/dist/{handlers-znb9n7d4.js → handlers-kqftvjgw.js} +41 -36
- package/dist/{heartbeat-bm4qkn8d.js → heartbeat-9q96p8v9.js} +43 -38
- package/dist/{hook-254r4mza.js → hook-56qe10dp.js} +11 -23
- package/dist/{hook-xaa3q80s.js → hook-yy6rw42k.js} +12 -11
- package/dist/{http-3dgrgp8y.js → http-r2dnb8f8.js} +2411 -275
- package/dist/{index-fx3jqq1e.js → index-3gtmkhc6.js} +38 -33
- package/dist/{index-bdsqa799.js → index-3k5hkz7r.js} +43 -38
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-g2byveez.js → index-5e101240.js} +38 -33
- package/dist/{index-2ybk7khy.js → index-5wyfjc2c.js} +845 -683
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-jd7sejm2.js → index-vxvhxxbn.js} +10 -9
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{keepalive-6ywhpja1.js → keepalive-psj6j54c.js} +12 -11
- package/dist/{lead-cw45n21s.js → lead-hajz4688.js} +61 -56
- package/dist/{maintenance-ss1a1ge9.js → maintenance-ps02sb5m.js} +10 -9
- package/dist/{node-fkajaq96.js → node-mwb5r697.js} +2 -1
- package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-0284h93b.js} +11 -10
- package/dist/{onboard-8hzdhc1k.js → onboard-rbf6bg01.js} +11 -3
- package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-13dcatgq.js} +18 -13
- package/dist/{otel-impl-e6mnvc4v.js → otel-impl-zvqcbhp7.js} +242 -868
- package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-xxy47q4v.js} +244 -106
- package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-kefx9v3t.js} +12 -11
- package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-3m36rxm2.js} +11 -10
- package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-z76xv0hq.js} +12 -11
- package/dist/{render-v2-907cy6sa.js → render-v2-wz73bs25.js} +18 -14
- package/dist/{resume-42stcaz0.js → resume-k4wzge8w.js} +25 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-adbc1c10.js → setup-2fp2pypp.js} +2 -2
- package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
- package/dist/{work-objects-wpj8ck95.js → work-objects-6k0ywz3m.js} +41 -36
- package/dist/{worker-bzdra9rf.js → worker-5v3rxsdh.js} +61 -56
- package/dist/{x-qfmygqqv.js → x-4mtdeym1.js} +3 -2
- package/openapi.json +1279 -329
- package/package.json +15 -15
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +237 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +87 -36
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +155 -10
- package/src/be/model-validation.ts +12 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +22 -8
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +0 -18
- package/src/be/seed-skills/index.ts +104 -3
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +193 -71
- package/src/e2b/env.ts +5 -0
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/loader.ts +10 -5
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -1
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +62 -19
- package/src/http/core.ts +8 -0
- package/src/http/index.ts +25 -3
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp.ts +15 -5
- package/src/http/model-profiles.ts +303 -0
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +13 -7
- package/src/http/pages.ts +3 -13
- package/src/http/poll.ts +42 -71
- package/src/http/requested-by.ts +54 -0
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +26 -2
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +79 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +160 -3
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/{artifact-sdk → pages}/feedback-overlay.ts +65 -6
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +1 -1
- package/src/providers/claude-flag-settings.ts +17 -5
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +243 -26
- package/src/providers/opencode-swarm-plugin.ts +31 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/pricing-sources.md +33 -2
- package/src/providers/reasoning-effort.ts +37 -0
- package/src/providers/types.ts +6 -2
- package/src/rbac/legacy-policy.ts +16 -2
- package/src/rbac/permissions.ts +26 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +11 -3
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +12 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +39 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +66 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +39 -15
- package/src/server.ts +62 -19
- package/src/slack/blocks.ts +4 -2
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +60 -1
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +15 -33
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +9 -4
- package/src/tests/claude-flag-settings.test.ts +11 -3
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +22 -0
- package/src/tests/db-task-write-contract.test.ts +11 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +216 -2
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +4 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +32 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +217 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +96 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +46 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +48 -1
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +14 -6
- package/src/types.ts +48 -3
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +86 -10
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +8 -1
- package/templates/skills/apps/content.md +8 -1
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-42qhyvwx.js +0 -33
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/cli-tcc4wd5d.js +0 -56471
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
|
@@ -0,0 +1,3465 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdapterRateLimitError,
|
|
3
|
+
AuthenticationError,
|
|
4
|
+
NetworkError,
|
|
5
|
+
PermissionError,
|
|
6
|
+
ResourceNotFoundError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
cardToFallbackText,
|
|
9
|
+
extractCard,
|
|
10
|
+
extractFiles,
|
|
11
|
+
extractPostableAttachments
|
|
12
|
+
} from "./cli-9traj3dn.js";
|
|
13
|
+
import {
|
|
14
|
+
BaseFormatConverter,
|
|
15
|
+
ConsoleLogger,
|
|
16
|
+
Message,
|
|
17
|
+
NotImplementedError,
|
|
18
|
+
StreamingMarkdownRenderer,
|
|
19
|
+
convertEmojiPlaceholders,
|
|
20
|
+
defaultEmojiResolver,
|
|
21
|
+
getEmoji,
|
|
22
|
+
isTableNode,
|
|
23
|
+
markdownToPlainText,
|
|
24
|
+
parseMarkdown,
|
|
25
|
+
stringifyMarkdown,
|
|
26
|
+
tableToAscii,
|
|
27
|
+
toPlainText,
|
|
28
|
+
walkAst
|
|
29
|
+
} from "./cli-2m1ktfzz.js";
|
|
30
|
+
import"./cli-kceah1eh.js";
|
|
31
|
+
import"./cli-f527ce74.js";
|
|
32
|
+
import"./cli-bc05qbss.js";
|
|
33
|
+
|
|
34
|
+
// node_modules/@chat-adapter/telegram/dist/index.js
|
|
35
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
36
|
+
var CALLBACK_DATA_PREFIX = "chat:";
|
|
37
|
+
var TELEGRAM_CALLBACK_DATA_LIMIT_BYTES = 64;
|
|
38
|
+
function convertLabel(label) {
|
|
39
|
+
return convertEmojiPlaceholders(label, "gchat");
|
|
40
|
+
}
|
|
41
|
+
function toInlineKeyboardRow(actions) {
|
|
42
|
+
const row = [];
|
|
43
|
+
for (const action of actions.children) {
|
|
44
|
+
if (action.type === "button") {
|
|
45
|
+
row.push({
|
|
46
|
+
text: convertLabel(action.label),
|
|
47
|
+
callback_data: encodeTelegramCallbackData(action.id, action.value)
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (action.type === "link-button") {
|
|
52
|
+
row.push({
|
|
53
|
+
text: convertLabel(action.label),
|
|
54
|
+
url: action.url
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return row;
|
|
59
|
+
}
|
|
60
|
+
function collectInlineKeyboardRows(children, rows) {
|
|
61
|
+
for (const child of children) {
|
|
62
|
+
if (child.type === "actions") {
|
|
63
|
+
const row = toInlineKeyboardRow(child);
|
|
64
|
+
if (row.length > 0) {
|
|
65
|
+
rows.push(row);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (child.type === "section") {
|
|
70
|
+
collectInlineKeyboardRows(child.children, rows);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function cardToTelegramInlineKeyboard(card) {
|
|
75
|
+
const rows = [];
|
|
76
|
+
collectInlineKeyboardRows(card.children, rows);
|
|
77
|
+
if (rows.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
inline_keyboard: rows
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function emptyTelegramInlineKeyboard() {
|
|
85
|
+
return { inline_keyboard: [] };
|
|
86
|
+
}
|
|
87
|
+
function encodeTelegramCallbackData(actionId, value) {
|
|
88
|
+
const payload = { a: actionId };
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
payload.v = value;
|
|
91
|
+
}
|
|
92
|
+
const callbackData = `${CALLBACK_DATA_PREFIX}${JSON.stringify(payload)}`;
|
|
93
|
+
if (Buffer.byteLength(callbackData, "utf8") > TELEGRAM_CALLBACK_DATA_LIMIT_BYTES) {
|
|
94
|
+
throw new ValidationError("telegram", `Callback payload too large for Telegram (max ${TELEGRAM_CALLBACK_DATA_LIMIT_BYTES} bytes).`);
|
|
95
|
+
}
|
|
96
|
+
return callbackData;
|
|
97
|
+
}
|
|
98
|
+
function decodeTelegramCallbackData(data) {
|
|
99
|
+
if (!data) {
|
|
100
|
+
return { actionId: "telegram_callback", value: undefined };
|
|
101
|
+
}
|
|
102
|
+
if (!data.startsWith(CALLBACK_DATA_PREFIX)) {
|
|
103
|
+
return { actionId: data, value: data };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const decoded = JSON.parse(data.slice(CALLBACK_DATA_PREFIX.length));
|
|
107
|
+
if (typeof decoded.a === "string" && decoded.a) {
|
|
108
|
+
return {
|
|
109
|
+
actionId: decoded.a,
|
|
110
|
+
value: typeof decoded.v === "string" ? decoded.v : undefined
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
return { actionId: data, value: data };
|
|
115
|
+
}
|
|
116
|
+
var MARKDOWNV2_SPECIAL_CHARS = /([_*[\]()~`>#+\-=|{}.!\\])/g;
|
|
117
|
+
var CODE_BLOCK_SPECIAL_CHARS = /([`\\])/g;
|
|
118
|
+
var LINK_URL_SPECIAL_CHARS = /([)\\])/g;
|
|
119
|
+
function toBotApiParseMode(mode) {
|
|
120
|
+
return mode === "MarkdownV2" ? "MarkdownV2" : undefined;
|
|
121
|
+
}
|
|
122
|
+
var TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
123
|
+
var TELEGRAM_CAPTION_LIMIT = 1024;
|
|
124
|
+
var MARKDOWN_V2_ENTITY_MARKERS = ["*", "_", "__", "~", "`"];
|
|
125
|
+
var MARKDOWN_V2_ELLIPSIS = "\\.\\.\\.";
|
|
126
|
+
var PLAIN_ELLIPSIS = "...";
|
|
127
|
+
function escapeMarkdownV2(text2) {
|
|
128
|
+
return text2.replace(MARKDOWNV2_SPECIAL_CHARS, "\\$1");
|
|
129
|
+
}
|
|
130
|
+
function scanDelimiters(text2) {
|
|
131
|
+
const markers = {
|
|
132
|
+
"*": [],
|
|
133
|
+
_: [],
|
|
134
|
+
__: [],
|
|
135
|
+
"~": [],
|
|
136
|
+
"`": []
|
|
137
|
+
};
|
|
138
|
+
const openBrackets = [];
|
|
139
|
+
let closeBrackets = 0;
|
|
140
|
+
let inFence = false;
|
|
141
|
+
let inInline = false;
|
|
142
|
+
let inLinkUrl = false;
|
|
143
|
+
let backslashes = 0;
|
|
144
|
+
const lastIndex = text2.length - 1;
|
|
145
|
+
for (let i = 0;i < text2.length; i++) {
|
|
146
|
+
const ch = text2[i];
|
|
147
|
+
if (ch === "\\") {
|
|
148
|
+
backslashes++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const escaped = backslashes % 2 === 1;
|
|
152
|
+
backslashes = 0;
|
|
153
|
+
if (escaped) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (inLinkUrl) {
|
|
157
|
+
if (ch === ")") {
|
|
158
|
+
inLinkUrl = false;
|
|
159
|
+
closeBrackets++;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "`") {
|
|
164
|
+
if (!inInline && text2.startsWith("```", i)) {
|
|
165
|
+
markers["`"].push(i);
|
|
166
|
+
inFence = !inFence;
|
|
167
|
+
i += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (inFence) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
markers["`"].push(i);
|
|
174
|
+
if (!inInline && i + 2 === text2.length && text2[i + 1] === "`") {
|
|
175
|
+
i += 1;
|
|
176
|
+
}
|
|
177
|
+
inInline = !inInline;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (inFence || inInline) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === "[") {
|
|
184
|
+
openBrackets.push(i);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "]") {
|
|
188
|
+
if (text2[i + 1] === "(") {
|
|
189
|
+
inLinkUrl = true;
|
|
190
|
+
i += 1;
|
|
191
|
+
} else if (i < lastIndex) {
|
|
192
|
+
closeBrackets++;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === "_") {
|
|
197
|
+
if (text2[i + 1] === "_") {
|
|
198
|
+
markers.__.push(i);
|
|
199
|
+
i += 1;
|
|
200
|
+
} else {
|
|
201
|
+
markers._.push(i);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === "*" || ch === "~") {
|
|
206
|
+
markers[ch].push(i);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { markers, openBrackets, closeBrackets };
|
|
210
|
+
}
|
|
211
|
+
function endsWithOrphanBackslash(text2) {
|
|
212
|
+
let trailing = 0;
|
|
213
|
+
for (let i = text2.length - 1;i >= 0 && text2[i] === "\\"; i--) {
|
|
214
|
+
trailing++;
|
|
215
|
+
}
|
|
216
|
+
return trailing % 2 === 1;
|
|
217
|
+
}
|
|
218
|
+
function trimToMarkdownV2SafeBoundary(text2) {
|
|
219
|
+
let current = text2;
|
|
220
|
+
const maxIterations = current.length + 1;
|
|
221
|
+
for (let i = 0;i < maxIterations; i++) {
|
|
222
|
+
if (endsWithOrphanBackslash(current)) {
|
|
223
|
+
current = current.slice(0, -1);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const scan = scanDelimiters(current);
|
|
227
|
+
let cut = current.length;
|
|
228
|
+
for (const marker of MARKDOWN_V2_ENTITY_MARKERS) {
|
|
229
|
+
const positions = scan.markers[marker];
|
|
230
|
+
if (positions.length % 2 === 1) {
|
|
231
|
+
cut = Math.min(cut, positions.at(-1) ?? cut);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (scan.openBrackets.length > scan.closeBrackets) {
|
|
235
|
+
cut = Math.min(cut, scan.openBrackets.at(-1) ?? cut);
|
|
236
|
+
}
|
|
237
|
+
if (cut >= current.length) {
|
|
238
|
+
return current;
|
|
239
|
+
}
|
|
240
|
+
current = current.slice(0, cut);
|
|
241
|
+
}
|
|
242
|
+
return current;
|
|
243
|
+
}
|
|
244
|
+
function truncateForTelegram(text2, limit, parseMode) {
|
|
245
|
+
if (text2.length <= limit) {
|
|
246
|
+
return text2;
|
|
247
|
+
}
|
|
248
|
+
const isMarkdownV2 = parseMode === "MarkdownV2";
|
|
249
|
+
const ellipsis = isMarkdownV2 ? MARKDOWN_V2_ELLIPSIS : PLAIN_ELLIPSIS;
|
|
250
|
+
let slice = text2.slice(0, limit - ellipsis.length);
|
|
251
|
+
if (isMarkdownV2) {
|
|
252
|
+
slice = trimToMarkdownV2SafeBoundary(slice);
|
|
253
|
+
}
|
|
254
|
+
return `${slice}${ellipsis}`;
|
|
255
|
+
}
|
|
256
|
+
function escapeCodeBlock(text2) {
|
|
257
|
+
return text2.replace(CODE_BLOCK_SPECIAL_CHARS, "\\$1");
|
|
258
|
+
}
|
|
259
|
+
function escapeLinkUrl(text2) {
|
|
260
|
+
return text2.replace(LINK_URL_SPECIAL_CHARS, "\\$1");
|
|
261
|
+
}
|
|
262
|
+
function renderMarkdownV2(node) {
|
|
263
|
+
switch (node.type) {
|
|
264
|
+
case "root":
|
|
265
|
+
return node.children.map(renderMarkdownV2).join(`
|
|
266
|
+
|
|
267
|
+
`);
|
|
268
|
+
case "paragraph":
|
|
269
|
+
return node.children.map(renderMarkdownV2).join("");
|
|
270
|
+
case "text":
|
|
271
|
+
return escapeMarkdownV2(node.value);
|
|
272
|
+
case "strong":
|
|
273
|
+
return `*${node.children.map(renderMarkdownV2).join("")}*`;
|
|
274
|
+
case "emphasis":
|
|
275
|
+
return `_${node.children.map(renderMarkdownV2).join("")}_`;
|
|
276
|
+
case "delete":
|
|
277
|
+
return `~${node.children.map(renderMarkdownV2).join("")}~`;
|
|
278
|
+
case "inlineCode":
|
|
279
|
+
return `\`${escapeCodeBlock(node.value)}\``;
|
|
280
|
+
case "code": {
|
|
281
|
+
const lang = node.lang ?? "";
|
|
282
|
+
const val = escapeCodeBlock(node.value);
|
|
283
|
+
return `\`\`\`${lang}
|
|
284
|
+
${val}
|
|
285
|
+
\`\`\``;
|
|
286
|
+
}
|
|
287
|
+
case "link": {
|
|
288
|
+
const linkText = node.children.map(renderMarkdownV2).join("");
|
|
289
|
+
const url = escapeLinkUrl(node.url);
|
|
290
|
+
return `[${linkText}](${url})`;
|
|
291
|
+
}
|
|
292
|
+
case "blockquote": {
|
|
293
|
+
const inner = node.children.map(renderMarkdownV2).join(`
|
|
294
|
+
`);
|
|
295
|
+
return inner.split(`
|
|
296
|
+
`).map((line) => `>${line}`).join(`
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
case "list":
|
|
300
|
+
return node.children.map((item2, i) => {
|
|
301
|
+
const content = item2.children.map(renderMarkdownV2).join(`
|
|
302
|
+
`);
|
|
303
|
+
if (node.ordered) {
|
|
304
|
+
return `${escapeMarkdownV2(`${i + 1}.`)} ${content}`;
|
|
305
|
+
}
|
|
306
|
+
return `\\- ${content}`;
|
|
307
|
+
}).join(`
|
|
308
|
+
`);
|
|
309
|
+
case "listItem":
|
|
310
|
+
return node.children.map(renderMarkdownV2).join(`
|
|
311
|
+
`);
|
|
312
|
+
case "heading": {
|
|
313
|
+
const text2 = node.children.map(renderMarkdownV2).join("");
|
|
314
|
+
return `*${text2}*`;
|
|
315
|
+
}
|
|
316
|
+
case "thematicBreak":
|
|
317
|
+
return escapeMarkdownV2("———");
|
|
318
|
+
case "break":
|
|
319
|
+
return `
|
|
320
|
+
`;
|
|
321
|
+
case "image": {
|
|
322
|
+
const alt = escapeMarkdownV2(node.alt ?? "");
|
|
323
|
+
const url = escapeLinkUrl(node.url);
|
|
324
|
+
return `[${alt}](${url})`;
|
|
325
|
+
}
|
|
326
|
+
case "html":
|
|
327
|
+
return escapeMarkdownV2(node.value);
|
|
328
|
+
case "linkReference":
|
|
329
|
+
case "imageReference":
|
|
330
|
+
if ("children" in node && node.children.length > 0) {
|
|
331
|
+
return node.children.map(renderMarkdownV2).join("");
|
|
332
|
+
}
|
|
333
|
+
return escapeMarkdownV2(node.label ?? node.identifier);
|
|
334
|
+
case "definition":
|
|
335
|
+
return "";
|
|
336
|
+
case "footnoteDefinition":
|
|
337
|
+
return "";
|
|
338
|
+
case "footnoteReference":
|
|
339
|
+
return escapeMarkdownV2(`[^${node.label ?? node.identifier}]`);
|
|
340
|
+
case "yaml":
|
|
341
|
+
return "";
|
|
342
|
+
case "table":
|
|
343
|
+
case "tableRow":
|
|
344
|
+
case "tableCell":
|
|
345
|
+
throw new Error(`Telegram MarkdownV2 renderer received a ${node.type} node; fromAst should have preprocessed it into a code block.`);
|
|
346
|
+
default: {
|
|
347
|
+
throw new Error(`Unhandled case: ${node}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
var TelegramFormatConverter = class extends BaseFormatConverter {
|
|
352
|
+
fromAst(ast) {
|
|
353
|
+
const transformed = walkAst(structuredClone(ast), (node) => {
|
|
354
|
+
if (isTableNode(node)) {
|
|
355
|
+
return {
|
|
356
|
+
type: "code",
|
|
357
|
+
value: tableToAscii(node),
|
|
358
|
+
lang: undefined
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return node;
|
|
362
|
+
});
|
|
363
|
+
return renderMarkdownV2(transformed).trim();
|
|
364
|
+
}
|
|
365
|
+
toAst(text2) {
|
|
366
|
+
return parseMarkdown(text2);
|
|
367
|
+
}
|
|
368
|
+
renderPostable(message) {
|
|
369
|
+
if (typeof message === "string") {
|
|
370
|
+
return message;
|
|
371
|
+
}
|
|
372
|
+
if ("raw" in message) {
|
|
373
|
+
return message.raw;
|
|
374
|
+
}
|
|
375
|
+
if ("markdown" in message) {
|
|
376
|
+
return this.fromMarkdown(message.markdown);
|
|
377
|
+
}
|
|
378
|
+
if ("ast" in message) {
|
|
379
|
+
return this.fromAst(message.ast);
|
|
380
|
+
}
|
|
381
|
+
return super.renderPostable(message);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var TELEGRAM_RICH_MESSAGE_LIMIT = 32768;
|
|
385
|
+
var MARKDOWN_PUNCTUATION = /[!-/:-@[-`{-~]/g;
|
|
386
|
+
var LINE_BREAKS = /[\r\n]/g;
|
|
387
|
+
var BACKTICKS = /`+/g;
|
|
388
|
+
function truncateRichMarkdown(markdown) {
|
|
389
|
+
const characters = Array.from(markdown);
|
|
390
|
+
if (characters.length <= TELEGRAM_RICH_MESSAGE_LIMIT) {
|
|
391
|
+
return markdown;
|
|
392
|
+
}
|
|
393
|
+
let end = TELEGRAM_RICH_MESSAGE_LIMIT - 3;
|
|
394
|
+
while (end > 0) {
|
|
395
|
+
const renderer = new StreamingMarkdownRenderer;
|
|
396
|
+
renderer.push(characters.slice(0, end).join(""));
|
|
397
|
+
const rendered = `${renderer.finish()}...`;
|
|
398
|
+
if (Array.from(rendered).length <= TELEGRAM_RICH_MESSAGE_LIMIT) {
|
|
399
|
+
return rendered;
|
|
400
|
+
}
|
|
401
|
+
end -= Array.from(rendered).length - TELEGRAM_RICH_MESSAGE_LIMIT;
|
|
402
|
+
}
|
|
403
|
+
return "...";
|
|
404
|
+
}
|
|
405
|
+
function escapeText(value) {
|
|
406
|
+
return value.replace(MARKDOWN_PUNCTUATION, "\\$&");
|
|
407
|
+
}
|
|
408
|
+
function inlineCode(value) {
|
|
409
|
+
if (!value) {
|
|
410
|
+
return "";
|
|
411
|
+
}
|
|
412
|
+
const runs = value.match(BACKTICKS) ?? [];
|
|
413
|
+
const size = Math.max(1, ...runs.map((run) => run.length + 1));
|
|
414
|
+
const marker = "`".repeat(size);
|
|
415
|
+
const hasBoundarySpace = value.startsWith(" ") && value.endsWith(" ") && value.trim().length > 0;
|
|
416
|
+
const padding = value.startsWith("`") || value.endsWith("`") || hasBoundarySpace ? " " : "";
|
|
417
|
+
return `${marker}${padding}${value}${padding}${marker}`;
|
|
418
|
+
}
|
|
419
|
+
function codeBlock(value, language) {
|
|
420
|
+
const runs = value.match(BACKTICKS) ?? [];
|
|
421
|
+
const size = Math.max(3, ...runs.map((run) => run.length + 1));
|
|
422
|
+
const marker = "`".repeat(size);
|
|
423
|
+
const info = language?.replace(LINE_BREAKS, " ").replaceAll("`", "") ?? "";
|
|
424
|
+
return `${marker}${info}
|
|
425
|
+
${value}
|
|
426
|
+
${marker}`;
|
|
427
|
+
}
|
|
428
|
+
function linkDestination(value) {
|
|
429
|
+
return `<${value.replaceAll("\\", "%5C").replaceAll("<", "%3C").replaceAll(">", "%3E").replaceAll("\r", "%0D").replaceAll(`
|
|
430
|
+
`, "%0A")}>`;
|
|
431
|
+
}
|
|
432
|
+
function text(markdown) {
|
|
433
|
+
if (typeof markdown === "string") {
|
|
434
|
+
return escapeText(markdown);
|
|
435
|
+
}
|
|
436
|
+
if (Array.isArray(markdown)) {
|
|
437
|
+
return markdown.map(text).join("");
|
|
438
|
+
}
|
|
439
|
+
switch (markdown.type) {
|
|
440
|
+
case "bold":
|
|
441
|
+
return `**${text(markdown.text)}**`;
|
|
442
|
+
case "italic":
|
|
443
|
+
return `_${text(markdown.text)}_`;
|
|
444
|
+
case "underline":
|
|
445
|
+
return `<u>${text(markdown.text)}</u>`;
|
|
446
|
+
case "strikethrough":
|
|
447
|
+
return `~~${text(markdown.text)}~~`;
|
|
448
|
+
case "spoiler":
|
|
449
|
+
return `||${text(markdown.text)}||`;
|
|
450
|
+
case "subscript":
|
|
451
|
+
return `<sub>${text(markdown.text)}</sub>`;
|
|
452
|
+
case "superscript":
|
|
453
|
+
return `<sup>${text(markdown.text)}</sup>`;
|
|
454
|
+
case "marked":
|
|
455
|
+
return `==${text(markdown.text)}==`;
|
|
456
|
+
case "code":
|
|
457
|
+
return inlineCode(plain(markdown.text));
|
|
458
|
+
case "date_time":
|
|
459
|
+
case "text_mention":
|
|
460
|
+
return text(markdown.text);
|
|
461
|
+
case "bank_card_number":
|
|
462
|
+
case "bot_command":
|
|
463
|
+
case "cashtag":
|
|
464
|
+
case "hashtag":
|
|
465
|
+
case "mention":
|
|
466
|
+
return text(markdown.text);
|
|
467
|
+
case "custom_emoji":
|
|
468
|
+
return markdown.alternative_text;
|
|
469
|
+
case "mathematical_expression":
|
|
470
|
+
return `$${markdown.expression}$`;
|
|
471
|
+
case "url":
|
|
472
|
+
return `[${text(markdown.text)}](${linkDestination(markdown.url)})`;
|
|
473
|
+
case "email_address":
|
|
474
|
+
return `[${text(markdown.text)}](${linkDestination(`mailto:${markdown.email_address}`)})`;
|
|
475
|
+
case "phone_number":
|
|
476
|
+
return `[${text(markdown.text)}](${linkDestination(`tel:${markdown.phone_number}`)})`;
|
|
477
|
+
case "anchor":
|
|
478
|
+
return "";
|
|
479
|
+
case "anchor_link":
|
|
480
|
+
case "reference":
|
|
481
|
+
case "reference_link":
|
|
482
|
+
return text(markdown.text);
|
|
483
|
+
default:
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function plain(markdown) {
|
|
488
|
+
if (typeof markdown === "string") {
|
|
489
|
+
return markdown;
|
|
490
|
+
}
|
|
491
|
+
if (Array.isArray(markdown)) {
|
|
492
|
+
return markdown.map(plain).join("");
|
|
493
|
+
}
|
|
494
|
+
switch (markdown.type) {
|
|
495
|
+
case "bold":
|
|
496
|
+
case "italic":
|
|
497
|
+
case "underline":
|
|
498
|
+
case "strikethrough":
|
|
499
|
+
case "spoiler":
|
|
500
|
+
case "subscript":
|
|
501
|
+
case "superscript":
|
|
502
|
+
case "marked":
|
|
503
|
+
case "code":
|
|
504
|
+
case "date_time":
|
|
505
|
+
case "text_mention":
|
|
506
|
+
case "url":
|
|
507
|
+
case "email_address":
|
|
508
|
+
case "phone_number":
|
|
509
|
+
case "bank_card_number":
|
|
510
|
+
case "mention":
|
|
511
|
+
case "hashtag":
|
|
512
|
+
case "cashtag":
|
|
513
|
+
case "bot_command":
|
|
514
|
+
case "anchor_link":
|
|
515
|
+
case "reference":
|
|
516
|
+
case "reference_link":
|
|
517
|
+
return plain(markdown.text);
|
|
518
|
+
case "custom_emoji":
|
|
519
|
+
return markdown.alternative_text;
|
|
520
|
+
case "mathematical_expression":
|
|
521
|
+
return markdown.expression;
|
|
522
|
+
case "anchor":
|
|
523
|
+
return "";
|
|
524
|
+
default:
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function caption(value) {
|
|
529
|
+
if (!value) {
|
|
530
|
+
return "";
|
|
531
|
+
}
|
|
532
|
+
const credit = value.credit ? `
|
|
533
|
+
${text(value.credit)}` : "";
|
|
534
|
+
return `${text(value.text)}${credit}`;
|
|
535
|
+
}
|
|
536
|
+
function plainCaption(value) {
|
|
537
|
+
if (!value) {
|
|
538
|
+
return "";
|
|
539
|
+
}
|
|
540
|
+
const credit = value.credit ? `
|
|
541
|
+
${plain(value.credit)}` : "";
|
|
542
|
+
return `${plain(value.text)}${credit}`;
|
|
543
|
+
}
|
|
544
|
+
function cell(value) {
|
|
545
|
+
return value.text ? text(value.text) : "";
|
|
546
|
+
}
|
|
547
|
+
function item(value) {
|
|
548
|
+
let checked = "";
|
|
549
|
+
if (value.has_checkbox) {
|
|
550
|
+
checked = value.is_checked ? "[x] " : "[ ] ";
|
|
551
|
+
}
|
|
552
|
+
const content = value.blocks.map(block).join(`
|
|
553
|
+
|
|
554
|
+
`).replaceAll(`
|
|
555
|
+
`, `
|
|
556
|
+
`);
|
|
557
|
+
return `${value.label} ${checked}${content}`.trimEnd();
|
|
558
|
+
}
|
|
559
|
+
function plainItem(value) {
|
|
560
|
+
let checked = "";
|
|
561
|
+
if (value.has_checkbox) {
|
|
562
|
+
checked = value.is_checked ? "[x] " : "[ ] ";
|
|
563
|
+
}
|
|
564
|
+
const content = value.blocks.map(plainBlock).filter(Boolean).join(`
|
|
565
|
+
`).replaceAll(`
|
|
566
|
+
`, `
|
|
567
|
+
`);
|
|
568
|
+
return `${value.label} ${checked}${content}`.trimEnd();
|
|
569
|
+
}
|
|
570
|
+
function table(value) {
|
|
571
|
+
const rows = value.cells.map((row) => `| ${row.map(cell).join(" | ")} |`);
|
|
572
|
+
if (rows.length === 0) {
|
|
573
|
+
return value.caption ? text(value.caption) : "";
|
|
574
|
+
}
|
|
575
|
+
const columns = Math.max(...value.cells.map((row) => row.length));
|
|
576
|
+
const separator = `| ${Array.from({ length: columns }, () => "---").join(" | ")} |`;
|
|
577
|
+
const content = [rows[0], separator, ...rows.slice(1)].join(`
|
|
578
|
+
`);
|
|
579
|
+
return value.caption ? `${text(value.caption)}
|
|
580
|
+
|
|
581
|
+
${content}` : content;
|
|
582
|
+
}
|
|
583
|
+
function quote(value) {
|
|
584
|
+
return value.split(`
|
|
585
|
+
`).map((line) => `> ${line}`).join(`
|
|
586
|
+
`);
|
|
587
|
+
}
|
|
588
|
+
function block(value) {
|
|
589
|
+
switch (value.type) {
|
|
590
|
+
case "paragraph":
|
|
591
|
+
case "footer":
|
|
592
|
+
case "thinking":
|
|
593
|
+
return text(value.text);
|
|
594
|
+
case "heading":
|
|
595
|
+
return `${"#".repeat(Math.min(6, Math.max(1, value.size)))} ${text(value.text)}`;
|
|
596
|
+
case "pre":
|
|
597
|
+
return codeBlock(plain(value.text), value.language);
|
|
598
|
+
case "divider":
|
|
599
|
+
return "---";
|
|
600
|
+
case "mathematical_expression":
|
|
601
|
+
return `$$${value.expression}$$`;
|
|
602
|
+
case "anchor":
|
|
603
|
+
return "";
|
|
604
|
+
case "list":
|
|
605
|
+
return value.items.map(item).join(`
|
|
606
|
+
`);
|
|
607
|
+
case "blockquote": {
|
|
608
|
+
const content = value.blocks.map(block).join(`
|
|
609
|
+
|
|
610
|
+
`);
|
|
611
|
+
const credit = value.credit ? `
|
|
612
|
+
|
|
613
|
+
${text(value.credit)}` : "";
|
|
614
|
+
return quote(`${content}${credit}`);
|
|
615
|
+
}
|
|
616
|
+
case "pullquote": {
|
|
617
|
+
const credit = value.credit ? `
|
|
618
|
+
|
|
619
|
+
${text(value.credit)}` : "";
|
|
620
|
+
return quote(`${text(value.text)}${credit}`);
|
|
621
|
+
}
|
|
622
|
+
case "collage":
|
|
623
|
+
case "slideshow": {
|
|
624
|
+
const content = value.blocks.map(block).filter(Boolean).join(`
|
|
625
|
+
|
|
626
|
+
`);
|
|
627
|
+
const description = caption(value.caption);
|
|
628
|
+
return [content, description].filter(Boolean).join(`
|
|
629
|
+
|
|
630
|
+
`);
|
|
631
|
+
}
|
|
632
|
+
case "table":
|
|
633
|
+
return table(value);
|
|
634
|
+
case "details":
|
|
635
|
+
return `${text(value.summary)}
|
|
636
|
+
|
|
637
|
+
${value.blocks.map(block).join(`
|
|
638
|
+
|
|
639
|
+
`)}`;
|
|
640
|
+
case "map":
|
|
641
|
+
return caption(value.caption);
|
|
642
|
+
case "animation":
|
|
643
|
+
case "audio":
|
|
644
|
+
case "photo":
|
|
645
|
+
case "video":
|
|
646
|
+
case "voice_note":
|
|
647
|
+
return caption(value.caption);
|
|
648
|
+
default:
|
|
649
|
+
return "";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function plainBlock(value) {
|
|
653
|
+
switch (value.type) {
|
|
654
|
+
case "paragraph":
|
|
655
|
+
case "footer":
|
|
656
|
+
case "thinking":
|
|
657
|
+
case "heading":
|
|
658
|
+
case "pre":
|
|
659
|
+
return plain(value.text);
|
|
660
|
+
case "divider":
|
|
661
|
+
case "anchor":
|
|
662
|
+
return "";
|
|
663
|
+
case "mathematical_expression":
|
|
664
|
+
return value.expression;
|
|
665
|
+
case "list":
|
|
666
|
+
return value.items.map(plainItem).join(`
|
|
667
|
+
`);
|
|
668
|
+
case "blockquote": {
|
|
669
|
+
const content = value.blocks.map(plainBlock).filter(Boolean).join(`
|
|
670
|
+
|
|
671
|
+
`);
|
|
672
|
+
const credit = value.credit ? `
|
|
673
|
+
|
|
674
|
+
${plain(value.credit)}` : "";
|
|
675
|
+
return `${content}${credit}`;
|
|
676
|
+
}
|
|
677
|
+
case "pullquote": {
|
|
678
|
+
const credit = value.credit ? `
|
|
679
|
+
|
|
680
|
+
${plain(value.credit)}` : "";
|
|
681
|
+
return `${plain(value.text)}${credit}`;
|
|
682
|
+
}
|
|
683
|
+
case "collage":
|
|
684
|
+
case "slideshow": {
|
|
685
|
+
const content = value.blocks.map(plainBlock).filter(Boolean).join(`
|
|
686
|
+
|
|
687
|
+
`);
|
|
688
|
+
const description = plainCaption(value.caption);
|
|
689
|
+
return [content, description].filter(Boolean).join(`
|
|
690
|
+
|
|
691
|
+
`);
|
|
692
|
+
}
|
|
693
|
+
case "table": {
|
|
694
|
+
const rows = value.cells.map((row) => row.map((entry) => entry.text ? plain(entry.text) : "").join("\t"));
|
|
695
|
+
const content = rows.join(`
|
|
696
|
+
`);
|
|
697
|
+
return value.caption ? `${plain(value.caption)}
|
|
698
|
+
|
|
699
|
+
${content}` : content;
|
|
700
|
+
}
|
|
701
|
+
case "details":
|
|
702
|
+
return `${plain(value.summary)}
|
|
703
|
+
|
|
704
|
+
${value.blocks.map(plainBlock).filter(Boolean).join(`
|
|
705
|
+
|
|
706
|
+
`)}`;
|
|
707
|
+
case "map":
|
|
708
|
+
return plainCaption(value.caption);
|
|
709
|
+
case "animation":
|
|
710
|
+
case "audio":
|
|
711
|
+
case "photo":
|
|
712
|
+
case "video":
|
|
713
|
+
case "voice_note":
|
|
714
|
+
return plainCaption(value.caption);
|
|
715
|
+
default:
|
|
716
|
+
return "";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function media(blocks, result) {
|
|
720
|
+
for (const value of blocks) {
|
|
721
|
+
switch (value.type) {
|
|
722
|
+
case "list":
|
|
723
|
+
for (const entry of value.items) {
|
|
724
|
+
media(entry.blocks, result);
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
case "blockquote":
|
|
728
|
+
case "collage":
|
|
729
|
+
case "slideshow":
|
|
730
|
+
case "details":
|
|
731
|
+
media(value.blocks, result);
|
|
732
|
+
break;
|
|
733
|
+
case "animation":
|
|
734
|
+
result.push({
|
|
735
|
+
file: value.animation,
|
|
736
|
+
height: value.animation.height,
|
|
737
|
+
mimeType: value.animation.mime_type,
|
|
738
|
+
name: value.animation.file_name,
|
|
739
|
+
type: value.animation.mime_type?.startsWith("image/") ? "image" : "video",
|
|
740
|
+
width: value.animation.width
|
|
741
|
+
});
|
|
742
|
+
break;
|
|
743
|
+
case "audio":
|
|
744
|
+
result.push({
|
|
745
|
+
file: value.audio,
|
|
746
|
+
mimeType: value.audio.mime_type,
|
|
747
|
+
name: value.audio.file_name,
|
|
748
|
+
type: "audio"
|
|
749
|
+
});
|
|
750
|
+
break;
|
|
751
|
+
case "photo": {
|
|
752
|
+
const photo = value.photo.at(-1);
|
|
753
|
+
if (photo) {
|
|
754
|
+
result.push({
|
|
755
|
+
file: photo,
|
|
756
|
+
height: photo.height,
|
|
757
|
+
mimeType: "image/jpeg",
|
|
758
|
+
type: "image",
|
|
759
|
+
width: photo.width
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "video":
|
|
765
|
+
result.push({
|
|
766
|
+
file: value.video,
|
|
767
|
+
height: value.video.height,
|
|
768
|
+
mimeType: value.video.mime_type,
|
|
769
|
+
name: value.video.file_name,
|
|
770
|
+
type: "video",
|
|
771
|
+
width: value.video.width
|
|
772
|
+
});
|
|
773
|
+
break;
|
|
774
|
+
case "voice_note":
|
|
775
|
+
result.push({
|
|
776
|
+
file: value.voice_note,
|
|
777
|
+
mimeType: value.voice_note.mime_type,
|
|
778
|
+
type: "audio"
|
|
779
|
+
});
|
|
780
|
+
break;
|
|
781
|
+
default:
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function richMessageToMarkdown(message) {
|
|
787
|
+
return message.blocks.map(block).filter(Boolean).join(`
|
|
788
|
+
|
|
789
|
+
`).trim();
|
|
790
|
+
}
|
|
791
|
+
function richMessageToText(message) {
|
|
792
|
+
return message.blocks.map(plainBlock).filter(Boolean).join(`
|
|
793
|
+
|
|
794
|
+
`).trim();
|
|
795
|
+
}
|
|
796
|
+
function richMessageMedia(message) {
|
|
797
|
+
const result = [];
|
|
798
|
+
media(message.blocks, result);
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
var TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
802
|
+
var TELEGRAM_FILE_LIMIT = 25 * 1024 * 1024;
|
|
803
|
+
var TELEGRAM_FILE_TIMEOUT_MS = 30000;
|
|
804
|
+
async function readTelegramFile(response, fileId) {
|
|
805
|
+
const declared = Number(response.headers.get("content-length"));
|
|
806
|
+
if (Number.isFinite(declared) && declared > TELEGRAM_FILE_LIMIT) {
|
|
807
|
+
await response.body?.cancel();
|
|
808
|
+
throw new NetworkError("telegram", `Telegram file ${fileId} exceeds the download limit`);
|
|
809
|
+
}
|
|
810
|
+
const reader = response.body?.getReader();
|
|
811
|
+
if (!reader) {
|
|
812
|
+
return await response.arrayBuffer();
|
|
813
|
+
}
|
|
814
|
+
const chunks = [];
|
|
815
|
+
let size = 0;
|
|
816
|
+
for (;; ) {
|
|
817
|
+
const { done, value } = await reader.read();
|
|
818
|
+
if (done) {
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
size += value.length;
|
|
822
|
+
if (size > TELEGRAM_FILE_LIMIT) {
|
|
823
|
+
await reader.cancel();
|
|
824
|
+
throw new NetworkError("telegram", `Telegram file ${fileId} exceeds the download limit`);
|
|
825
|
+
}
|
|
826
|
+
chunks.push(value);
|
|
827
|
+
}
|
|
828
|
+
const data = new Uint8Array(size);
|
|
829
|
+
let offset = 0;
|
|
830
|
+
for (const chunk of chunks) {
|
|
831
|
+
data.set(chunk, offset);
|
|
832
|
+
offset += chunk.length;
|
|
833
|
+
}
|
|
834
|
+
return data.buffer;
|
|
835
|
+
}
|
|
836
|
+
var TELEGRAM_SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
|
|
837
|
+
var TELEGRAM_WEBHOOK_VERIFICATION_ERROR = "secretToken is required in webhook mode. Set TELEGRAM_WEBHOOK_SECRET_TOKEN or provide secretToken. To accept unverified webhooks, set allowUnverifiedWebhooks: true or TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS=true.";
|
|
838
|
+
var MESSAGE_ID_PATTERN = /^([^:]+):(\d+)$/;
|
|
839
|
+
var BARE_MESSAGE_ID_PATTERN = /^\d+$/;
|
|
840
|
+
var trimTrailingSlashes = (url) => {
|
|
841
|
+
let end = url.length;
|
|
842
|
+
while (end > 0 && url[end - 1] === "/") {
|
|
843
|
+
end--;
|
|
844
|
+
}
|
|
845
|
+
return url.slice(0, end);
|
|
846
|
+
};
|
|
847
|
+
var MESSAGE_SEQUENCE_PATTERN = /:(\d+)$/;
|
|
848
|
+
var ATTACHMENT_UPLOADS = {
|
|
849
|
+
audio: { field: "audio", method: "sendAudio" },
|
|
850
|
+
file: { field: "document", method: "sendDocument" },
|
|
851
|
+
image: { field: "photo", method: "sendPhoto" },
|
|
852
|
+
video: { field: "video", method: "sendVideo" }
|
|
853
|
+
};
|
|
854
|
+
var ATTACHMENT_MEDIA_GROUP_TYPES = {
|
|
855
|
+
audio: "audio",
|
|
856
|
+
file: "document",
|
|
857
|
+
image: "photo",
|
|
858
|
+
video: "video"
|
|
859
|
+
};
|
|
860
|
+
var LEADING_AT_PATTERN = /^@+/;
|
|
861
|
+
var EMOJI_PLACEHOLDER_PATTERN = /^\{\{emoji:([a-z0-9_]+)\}\}$/i;
|
|
862
|
+
var EMOJI_NAME_PATTERN = /^[a-z0-9_+-]+$/i;
|
|
863
|
+
var TELEGRAM_DEFAULT_POLLING_TIMEOUT_SECONDS = 30;
|
|
864
|
+
var TELEGRAM_DEFAULT_POLLING_LIMIT = 100;
|
|
865
|
+
var TELEGRAM_DEFAULT_POLLING_RETRY_DELAY_MS = 1000;
|
|
866
|
+
var TELEGRAM_DEFAULT_STREAM_UPDATE_INTERVAL_MS = 250;
|
|
867
|
+
var TELEGRAM_DEFAULT_PRIVATE_STREAMING_EDIT_INTERVAL_MS = 1100;
|
|
868
|
+
var TELEGRAM_DEFAULT_NON_PRIVATE_STREAMING_EDIT_INTERVAL_MS = 3100;
|
|
869
|
+
var TELEGRAM_STREAM_FINAL_EDIT_RETRY_CAP_MS = 5000;
|
|
870
|
+
var TELEGRAM_STREAM_PLACEHOLDER_TEXT = "...";
|
|
871
|
+
var TELEGRAM_INCOMING_MEDIA_GROUP_BUFFER_TTL_MS = 30000;
|
|
872
|
+
var TELEGRAM_INCOMING_MEDIA_GROUP_LOCK_TTL_MS = 5000;
|
|
873
|
+
var TELEGRAM_INCOMING_MEDIA_GROUP_RETRY_MS = 50;
|
|
874
|
+
var TELEGRAM_INCOMING_MEDIA_GROUP_SETTLE_MS = 1000;
|
|
875
|
+
var TELEGRAM_WEBHOOK_UPDATE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
876
|
+
var TELEGRAM_MEDIA_GROUP_MIN = 2;
|
|
877
|
+
var TELEGRAM_MEDIA_GROUP_MAX = 10;
|
|
878
|
+
var TELEGRAM_MARKDOWN_PARSE_ERROR_PATTERN = /can't parse (?:caption )?entities/i;
|
|
879
|
+
var TELEGRAM_MAX_POLLING_LIMIT = 100;
|
|
880
|
+
var TELEGRAM_MIN_POLLING_LIMIT = 1;
|
|
881
|
+
var TELEGRAM_MIN_POLLING_TIMEOUT_SECONDS = 0;
|
|
882
|
+
var TELEGRAM_MAX_POLLING_TIMEOUT_SECONDS = 300;
|
|
883
|
+
var TELEGRAM_BUSINESS_ALLOWED_UPDATES = [
|
|
884
|
+
"business_connection",
|
|
885
|
+
"business_message",
|
|
886
|
+
"edited_business_message"
|
|
887
|
+
];
|
|
888
|
+
var TELEGRAM_DEFAULT_ALLOWED_UPDATES = [
|
|
889
|
+
"message",
|
|
890
|
+
"edited_message",
|
|
891
|
+
"channel_post",
|
|
892
|
+
"edited_channel_post",
|
|
893
|
+
"callback_query"
|
|
894
|
+
];
|
|
895
|
+
var TELEGRAM_BUSINESS_CONNECTION_TTL_MS = 60 * 60 * 1000;
|
|
896
|
+
function normalizeBotTokenProvider(botToken) {
|
|
897
|
+
if (typeof botToken === "function") {
|
|
898
|
+
return async () => await botToken();
|
|
899
|
+
}
|
|
900
|
+
return () => Promise.resolve(botToken);
|
|
901
|
+
}
|
|
902
|
+
var escapeMarkdownInEntity = (text2) => text2.replace(/([[\]()\\])/g, "\\$1");
|
|
903
|
+
function applyTelegramEntities(text2, entities) {
|
|
904
|
+
if (entities.length === 0) {
|
|
905
|
+
return text2;
|
|
906
|
+
}
|
|
907
|
+
const sorted = [...entities].sort((a, b) => {
|
|
908
|
+
const offsetDiff = b.offset - a.offset;
|
|
909
|
+
if (offsetDiff !== 0) {
|
|
910
|
+
return offsetDiff;
|
|
911
|
+
}
|
|
912
|
+
return a.length - b.length;
|
|
913
|
+
});
|
|
914
|
+
let result = text2;
|
|
915
|
+
for (const entity of sorted) {
|
|
916
|
+
const start = entity.offset;
|
|
917
|
+
const end = entity.offset + entity.length;
|
|
918
|
+
const entityText = result.slice(start, end);
|
|
919
|
+
let replacement;
|
|
920
|
+
switch (entity.type) {
|
|
921
|
+
case "text_link": {
|
|
922
|
+
if (entity.url) {
|
|
923
|
+
replacement = `[${escapeMarkdownInEntity(entityText)}](${entity.url})`;
|
|
924
|
+
}
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
case "bold": {
|
|
928
|
+
replacement = `**${entityText}**`;
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
case "italic": {
|
|
932
|
+
replacement = `*${entityText}*`;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "code": {
|
|
936
|
+
replacement = `\`${entityText}\``;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
case "pre": {
|
|
940
|
+
const lang = entity.language ?? "";
|
|
941
|
+
replacement = `\`\`\`${lang}
|
|
942
|
+
${entityText}
|
|
943
|
+
\`\`\``;
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
case "strikethrough": {
|
|
947
|
+
replacement = `~~${entityText}~~`;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
default:
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (replacement !== undefined) {
|
|
954
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
function describeNonFileContent(raw) {
|
|
960
|
+
if (raw.venue) {
|
|
961
|
+
return `\uD83D\uDCCD ${raw.venue.title}, ${raw.venue.address}`;
|
|
962
|
+
}
|
|
963
|
+
if (raw.location) {
|
|
964
|
+
return `\uD83D\uDCCD ${raw.location.latitude}, ${raw.location.longitude}`;
|
|
965
|
+
}
|
|
966
|
+
if (raw.contact) {
|
|
967
|
+
const name = [raw.contact.first_name, raw.contact.last_name].filter(Boolean).join(" ");
|
|
968
|
+
return `\uD83D\uDC64 ${name} ${raw.contact.phone_number}`.trim();
|
|
969
|
+
}
|
|
970
|
+
if (raw.poll) {
|
|
971
|
+
return `\uD83D\uDCCA ${raw.poll.question}`;
|
|
972
|
+
}
|
|
973
|
+
if (raw.dice) {
|
|
974
|
+
return `${raw.dice.emoji} ${raw.dice.value}`;
|
|
975
|
+
}
|
|
976
|
+
if (raw.game) {
|
|
977
|
+
return `\uD83C\uDFAE ${raw.game.title}`;
|
|
978
|
+
}
|
|
979
|
+
if (raw.invoice) {
|
|
980
|
+
const amount = formatInvoiceAmount(raw.invoice.total_amount, raw.invoice.currency);
|
|
981
|
+
return `\uD83E\uDDFE ${raw.invoice.title} — ${amount} ${raw.invoice.currency}`;
|
|
982
|
+
}
|
|
983
|
+
if (raw.story) {
|
|
984
|
+
return "\uD83D\uDCD6 Story";
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
var ZERO_EXPONENT_CURRENCIES = /* @__PURE__ */ new Set([
|
|
989
|
+
"CLP",
|
|
990
|
+
"ISK",
|
|
991
|
+
"JPY",
|
|
992
|
+
"KRW",
|
|
993
|
+
"PYG",
|
|
994
|
+
"UGX",
|
|
995
|
+
"VND",
|
|
996
|
+
"XTR"
|
|
997
|
+
]);
|
|
998
|
+
var THREE_EXPONENT_CURRENCIES = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD"]);
|
|
999
|
+
function formatInvoiceAmount(totalAmount, currency) {
|
|
1000
|
+
let exponent = 2;
|
|
1001
|
+
if (ZERO_EXPONENT_CURRENCIES.has(currency)) {
|
|
1002
|
+
exponent = 0;
|
|
1003
|
+
} else if (THREE_EXPONENT_CURRENCIES.has(currency)) {
|
|
1004
|
+
exponent = 3;
|
|
1005
|
+
}
|
|
1006
|
+
return (totalAmount / 10 ** exponent).toFixed(exponent);
|
|
1007
|
+
}
|
|
1008
|
+
function stickerAttachmentFormat(sticker) {
|
|
1009
|
+
if (sticker.is_video) {
|
|
1010
|
+
return { type: "video", mimeType: "video/webm" };
|
|
1011
|
+
}
|
|
1012
|
+
if (sticker.is_animated) {
|
|
1013
|
+
return { type: "file", mimeType: "application/x-tgsticker" };
|
|
1014
|
+
}
|
|
1015
|
+
return { type: "image", mimeType: "image/webp" };
|
|
1016
|
+
}
|
|
1017
|
+
var TelegramAdapter = class {
|
|
1018
|
+
name = "telegram";
|
|
1019
|
+
lockScope = "channel";
|
|
1020
|
+
persistThreadHistory = true;
|
|
1021
|
+
allowedUserIds;
|
|
1022
|
+
allowUnverifiedWebhooks;
|
|
1023
|
+
botTokenProvider;
|
|
1024
|
+
staticBotToken;
|
|
1025
|
+
apiBaseUrl;
|
|
1026
|
+
secretToken;
|
|
1027
|
+
mentionOnReply;
|
|
1028
|
+
botIdentityPromise = null;
|
|
1029
|
+
webhookScope;
|
|
1030
|
+
warnedNoVerification = false;
|
|
1031
|
+
logger;
|
|
1032
|
+
formatConverter = new TelegramFormatConverter;
|
|
1033
|
+
messageCache = /* @__PURE__ */ new Map;
|
|
1034
|
+
chat = null;
|
|
1035
|
+
_botUserId;
|
|
1036
|
+
_userName;
|
|
1037
|
+
_mentionRegexUsername;
|
|
1038
|
+
_mentionRegex;
|
|
1039
|
+
hasExplicitUserName;
|
|
1040
|
+
mode;
|
|
1041
|
+
nativeStreaming;
|
|
1042
|
+
streamingEditIntervalMs;
|
|
1043
|
+
longPolling;
|
|
1044
|
+
businessMode;
|
|
1045
|
+
_runtimeMode = "webhook";
|
|
1046
|
+
pollingAbortController = null;
|
|
1047
|
+
pollingTask = null;
|
|
1048
|
+
pollingActive = false;
|
|
1049
|
+
nextDraftId = Math.max(1, Date.now() % 2147483647);
|
|
1050
|
+
richMessagesAvailable = true;
|
|
1051
|
+
get botUserId() {
|
|
1052
|
+
return this._botUserId;
|
|
1053
|
+
}
|
|
1054
|
+
get userName() {
|
|
1055
|
+
return this._userName;
|
|
1056
|
+
}
|
|
1057
|
+
get isPolling() {
|
|
1058
|
+
return this.pollingActive;
|
|
1059
|
+
}
|
|
1060
|
+
get runtimeMode() {
|
|
1061
|
+
return this._runtimeMode;
|
|
1062
|
+
}
|
|
1063
|
+
constructor(config = {}) {
|
|
1064
|
+
const botToken = config.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
1065
|
+
if (!botToken) {
|
|
1066
|
+
throw new ValidationError("telegram", "botToken is required. Set TELEGRAM_BOT_TOKEN or provide it in config.");
|
|
1067
|
+
}
|
|
1068
|
+
this.botTokenProvider = normalizeBotTokenProvider(botToken);
|
|
1069
|
+
this.staticBotToken = typeof botToken === "string" ? botToken : undefined;
|
|
1070
|
+
this.apiBaseUrl = trimTrailingSlashes(config.apiUrl ?? config.apiBaseUrl ?? process.env.TELEGRAM_API_BASE_URL ?? TELEGRAM_API_BASE);
|
|
1071
|
+
this.secretToken = config.secretToken ?? process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN;
|
|
1072
|
+
this.allowUnverifiedWebhooks = config.allowUnverifiedWebhooks ?? process.env.TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS === "true";
|
|
1073
|
+
this.mentionOnReply = config.mentionOnReply ?? process.env.TELEGRAM_MENTION_ON_REPLY === "true";
|
|
1074
|
+
const allowedUserIds = config.allowedUserIds ?? process.env.TELEGRAM_ALLOWED_USER_IDS?.split(",");
|
|
1075
|
+
const normalizedAllowedUserIds = allowedUserIds?.map((id) => String(id).trim()).filter(Boolean);
|
|
1076
|
+
this.allowedUserIds = normalizedAllowedUserIds?.length ? new Set(normalizedAllowedUserIds) : undefined;
|
|
1077
|
+
this.logger = config.logger ?? new ConsoleLogger("info").child("telegram");
|
|
1078
|
+
const userName = config.userName ?? process.env.TELEGRAM_BOT_USERNAME;
|
|
1079
|
+
this._userName = this.normalizeUserName(userName ?? "bot");
|
|
1080
|
+
this.hasExplicitUserName = Boolean(userName);
|
|
1081
|
+
this.mode = config.mode ?? "auto";
|
|
1082
|
+
this.nativeStreaming = config.nativeStreaming ?? false;
|
|
1083
|
+
this.streamingEditIntervalMs = typeof config.streamingEditIntervalMs !== "number" || !Number.isFinite(config.streamingEditIntervalMs) ? undefined : this.clampInteger(config.streamingEditIntervalMs, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
1084
|
+
this.longPolling = config.longPolling;
|
|
1085
|
+
this.businessMode = config.businessMode ?? false;
|
|
1086
|
+
if (!["auto", "webhook", "polling"].includes(this.mode)) {
|
|
1087
|
+
throw new ValidationError("telegram", `Invalid mode: ${this.mode}. Expected "auto", "webhook", or "polling".`);
|
|
1088
|
+
}
|
|
1089
|
+
if (this.mode === "webhook" && !(this.secretToken || this.allowUnverifiedWebhooks)) {
|
|
1090
|
+
throw new ValidationError("telegram", TELEGRAM_WEBHOOK_VERIFICATION_ERROR);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async resolveBotToken() {
|
|
1094
|
+
const botToken = await this.botTokenProvider();
|
|
1095
|
+
if (!botToken) {
|
|
1096
|
+
throw new AuthenticationError("telegram", "Telegram bot token resolver returned an empty token. Check TELEGRAM_BOT_TOKEN or the Vercel Connect connector.");
|
|
1097
|
+
}
|
|
1098
|
+
return botToken;
|
|
1099
|
+
}
|
|
1100
|
+
async ensureBotIdentity() {
|
|
1101
|
+
if (this.webhookScope) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (!this.botIdentityPromise) {
|
|
1105
|
+
this.botIdentityPromise = this.telegramFetch("getMe").then((me) => {
|
|
1106
|
+
this._botUserId = String(me.id);
|
|
1107
|
+
this.webhookScope = createHash("sha256").update(this._botUserId).digest("hex");
|
|
1108
|
+
if (!this.hasExplicitUserName && me.username) {
|
|
1109
|
+
this._userName = this.normalizeUserName(me.username);
|
|
1110
|
+
}
|
|
1111
|
+
this.logger.info("Telegram bot identity resolved", {
|
|
1112
|
+
botUserId: this._botUserId,
|
|
1113
|
+
userName: this._userName
|
|
1114
|
+
});
|
|
1115
|
+
}).finally(() => {
|
|
1116
|
+
this.botIdentityPromise = null;
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
await this.botIdentityPromise;
|
|
1120
|
+
}
|
|
1121
|
+
async initialize(chat) {
|
|
1122
|
+
this.chat = chat;
|
|
1123
|
+
if (!this.hasExplicitUserName) {
|
|
1124
|
+
const chatUserName = chat.getUserName?.();
|
|
1125
|
+
if (typeof chatUserName === "string" && chatUserName.trim()) {
|
|
1126
|
+
this._userName = this.normalizeUserName(chatUserName);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
try {
|
|
1130
|
+
await this.ensureBotIdentity();
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
this.logger.warn("Failed to fetch Telegram bot identity", {
|
|
1133
|
+
error: String(error)
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const runtimeMode = await this.resolveRuntimeMode();
|
|
1137
|
+
this._runtimeMode = runtimeMode;
|
|
1138
|
+
if (runtimeMode === "webhook" && !(this.secretToken || this.allowUnverifiedWebhooks)) {
|
|
1139
|
+
throw new ValidationError("telegram", TELEGRAM_WEBHOOK_VERIFICATION_ERROR);
|
|
1140
|
+
}
|
|
1141
|
+
if (runtimeMode === "polling") {
|
|
1142
|
+
const pollingConfig = this.longPolling;
|
|
1143
|
+
if (this.mode === "auto") {
|
|
1144
|
+
await this.startPolling(pollingConfig ? { ...pollingConfig, deleteWebhook: false } : { deleteWebhook: false });
|
|
1145
|
+
} else {
|
|
1146
|
+
await this.startPolling(pollingConfig);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async getUser(userId) {
|
|
1151
|
+
try {
|
|
1152
|
+
const chat = await this.telegramFetch("getChat", {
|
|
1153
|
+
chat_id: userId
|
|
1154
|
+
});
|
|
1155
|
+
if (chat.type !== "private") {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
const fullName = [chat.first_name, chat.last_name].filter(Boolean).join(" ");
|
|
1159
|
+
return {
|
|
1160
|
+
email: undefined,
|
|
1161
|
+
fullName: fullName || String(chat.id),
|
|
1162
|
+
isBot: false,
|
|
1163
|
+
userId: String(chat.id),
|
|
1164
|
+
userName: chat.username || chat.first_name || String(chat.id)
|
|
1165
|
+
};
|
|
1166
|
+
} catch {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async handleWebhook(request, options) {
|
|
1171
|
+
if (!(this.secretToken || this.allowUnverifiedWebhooks)) {
|
|
1172
|
+
this.logger.warn("Telegram webhook rejected because verification is not configured");
|
|
1173
|
+
return new Response("Webhook verification required", { status: 401 });
|
|
1174
|
+
}
|
|
1175
|
+
if (this.secretToken) {
|
|
1176
|
+
const headerToken = request.headers.get(TELEGRAM_SECRET_TOKEN_HEADER);
|
|
1177
|
+
let valid = false;
|
|
1178
|
+
try {
|
|
1179
|
+
valid = !!headerToken && timingSafeEqual(Buffer.from(headerToken), Buffer.from(this.secretToken));
|
|
1180
|
+
} catch {}
|
|
1181
|
+
if (!valid) {
|
|
1182
|
+
this.logger.warn("Telegram webhook rejected due to invalid secret token");
|
|
1183
|
+
return new Response("Invalid secret token", { status: 401 });
|
|
1184
|
+
}
|
|
1185
|
+
} else if (!this.warnedNoVerification) {
|
|
1186
|
+
this.warnedNoVerification = true;
|
|
1187
|
+
this.logger.warn("Telegram webhook verification is explicitly disabled");
|
|
1188
|
+
}
|
|
1189
|
+
let update;
|
|
1190
|
+
try {
|
|
1191
|
+
update = await request.json();
|
|
1192
|
+
} catch {
|
|
1193
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
1194
|
+
}
|
|
1195
|
+
if (!this.chat) {
|
|
1196
|
+
this.logger.warn("Chat instance not initialized, ignoring Telegram webhook");
|
|
1197
|
+
return new Response("OK", { status: 200 });
|
|
1198
|
+
}
|
|
1199
|
+
if (Number.isInteger(update.update_id)) {
|
|
1200
|
+
let webhookScope = this.webhookScope;
|
|
1201
|
+
if (!webhookScope) {
|
|
1202
|
+
try {
|
|
1203
|
+
await this.ensureBotIdentity();
|
|
1204
|
+
webhookScope = this.webhookScope;
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
this.logger.warn("Telegram webhook update could not resolve bot identity", { error: String(error) });
|
|
1207
|
+
return new Response("Service unavailable", { status: 503 });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (!webhookScope) {
|
|
1211
|
+
return new Response("Service unavailable", { status: 503 });
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const claimed = await this.chat.getState().setIfNotExists(`${this.name}:webhook-update:${webhookScope}:${update.update_id}`, true, TELEGRAM_WEBHOOK_UPDATE_TTL_MS);
|
|
1215
|
+
if (!claimed) {
|
|
1216
|
+
this.logger.debug("Ignoring duplicate Telegram webhook update", {
|
|
1217
|
+
updateId: update.update_id
|
|
1218
|
+
});
|
|
1219
|
+
return new Response("OK", { status: 200 });
|
|
1220
|
+
}
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
this.logger.warn("Failed to claim Telegram webhook update", {
|
|
1223
|
+
error: String(error),
|
|
1224
|
+
updateId: update.update_id
|
|
1225
|
+
});
|
|
1226
|
+
return new Response("Service unavailable", { status: 503 });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
this.processUpdate(update, options);
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
this.logger.warn("Failed to process Telegram webhook update", {
|
|
1233
|
+
error: String(error),
|
|
1234
|
+
updateId: update.update_id
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
return new Response("OK", { status: 200 });
|
|
1238
|
+
}
|
|
1239
|
+
async startPolling(config) {
|
|
1240
|
+
if (!this.chat) {
|
|
1241
|
+
throw new ValidationError("telegram", "Cannot start polling before initialize()");
|
|
1242
|
+
}
|
|
1243
|
+
if (this.pollingActive) {
|
|
1244
|
+
this.logger.debug("Telegram polling already active");
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const resolvedConfig = this.resolvePollingConfig(config);
|
|
1248
|
+
const previousRuntimeMode = this._runtimeMode;
|
|
1249
|
+
this.pollingActive = true;
|
|
1250
|
+
try {
|
|
1251
|
+
if (resolvedConfig.deleteWebhook) {
|
|
1252
|
+
await this.resetWebhook(resolvedConfig.dropPendingUpdates);
|
|
1253
|
+
}
|
|
1254
|
+
this._runtimeMode = "polling";
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
this.pollingActive = false;
|
|
1257
|
+
this._runtimeMode = previousRuntimeMode;
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
this.logger.info("Telegram polling started", {
|
|
1261
|
+
limit: resolvedConfig.limit,
|
|
1262
|
+
timeout: resolvedConfig.timeout,
|
|
1263
|
+
allowedUpdates: resolvedConfig.allowedUpdates
|
|
1264
|
+
});
|
|
1265
|
+
this.pollingTask = this.pollingLoop(resolvedConfig).finally(() => {
|
|
1266
|
+
this.pollingActive = false;
|
|
1267
|
+
this.pollingAbortController = null;
|
|
1268
|
+
this.pollingTask = null;
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
async stopPolling() {
|
|
1272
|
+
if (!this.pollingActive) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
this.pollingActive = false;
|
|
1276
|
+
this.pollingAbortController?.abort();
|
|
1277
|
+
if (this.pollingTask) {
|
|
1278
|
+
await this.pollingTask;
|
|
1279
|
+
}
|
|
1280
|
+
this.logger.info("Telegram polling stopped");
|
|
1281
|
+
}
|
|
1282
|
+
async disconnect() {
|
|
1283
|
+
await this.stopPolling();
|
|
1284
|
+
}
|
|
1285
|
+
async resetWebhook(dropPendingUpdates = false) {
|
|
1286
|
+
await this.telegramFetch("deleteWebhook", {
|
|
1287
|
+
drop_pending_updates: dropPendingUpdates
|
|
1288
|
+
});
|
|
1289
|
+
this.logger.info("Telegram webhook reset", {
|
|
1290
|
+
dropPendingUpdates
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
async resolveRuntimeMode() {
|
|
1294
|
+
if (this.mode === "webhook") {
|
|
1295
|
+
return "webhook";
|
|
1296
|
+
}
|
|
1297
|
+
if (this.mode === "polling") {
|
|
1298
|
+
return "polling";
|
|
1299
|
+
}
|
|
1300
|
+
const webhookInfo = await this.fetchWebhookInfo();
|
|
1301
|
+
if (!webhookInfo) {
|
|
1302
|
+
this.logger.warn("Telegram auto mode could not verify webhook status; keeping webhook mode");
|
|
1303
|
+
return "webhook";
|
|
1304
|
+
}
|
|
1305
|
+
if (typeof webhookInfo.url === "string" && webhookInfo.url.trim()) {
|
|
1306
|
+
this.logger.debug("Telegram auto mode selected webhook mode", {
|
|
1307
|
+
webhookUrl: webhookInfo.url
|
|
1308
|
+
});
|
|
1309
|
+
return "webhook";
|
|
1310
|
+
}
|
|
1311
|
+
if (this.isLikelyServerlessRuntime()) {
|
|
1312
|
+
this.logger.warn("Telegram auto mode detected serverless runtime without webhook URL; keeping webhook mode");
|
|
1313
|
+
return "webhook";
|
|
1314
|
+
}
|
|
1315
|
+
this.logger.info("Telegram auto mode selected polling mode");
|
|
1316
|
+
return "polling";
|
|
1317
|
+
}
|
|
1318
|
+
async fetchWebhookInfo() {
|
|
1319
|
+
try {
|
|
1320
|
+
return await this.telegramFetch("getWebhookInfo");
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
this.logger.warn("Failed to fetch Telegram webhook info", {
|
|
1323
|
+
error: String(error)
|
|
1324
|
+
});
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
isLikelyServerlessRuntime() {
|
|
1329
|
+
if (typeof process === "undefined" || !process.env) {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
return Boolean(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") || process.env.FUNCTIONS_WORKER_RUNTIME || process.env.NETLIFY || process.env.K_SERVICE);
|
|
1333
|
+
}
|
|
1334
|
+
processUpdate(update, options) {
|
|
1335
|
+
return this.collectUpdateTasks(update, options);
|
|
1336
|
+
}
|
|
1337
|
+
collectUpdateTasks(update, options) {
|
|
1338
|
+
const tasks = [];
|
|
1339
|
+
if (this.businessMode && update.business_connection) {
|
|
1340
|
+
const connection = update.business_connection;
|
|
1341
|
+
const task = this.cacheBusinessConnection(connection);
|
|
1342
|
+
const tracked = task.catch((error) => {
|
|
1343
|
+
this.logger.warn("Failed to cache Telegram business connection", {
|
|
1344
|
+
error: String(error),
|
|
1345
|
+
connectionId: connection.id
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
options?.waitUntil?.(tracked);
|
|
1349
|
+
tasks.push(task);
|
|
1350
|
+
}
|
|
1351
|
+
const messageUpdate = update.message ?? update.edited_message ?? update.channel_post ?? update.edited_channel_post;
|
|
1352
|
+
const businessMessageUpdate = update.business_message ?? update.edited_business_message;
|
|
1353
|
+
const userId = update.callback_query?.from.id ?? update.message_reaction?.user?.id ?? messageUpdate?.from?.id ?? businessMessageUpdate?.from?.id;
|
|
1354
|
+
if (this.allowedUserIds && (userId === undefined || !this.allowedUserIds.has(String(userId)))) {
|
|
1355
|
+
return tasks;
|
|
1356
|
+
}
|
|
1357
|
+
const handledSlashCommand = update.message !== undefined && !update.message.media_group_id && this.handleSlashCommandUpdate(update.message, this.encodeThreadId({
|
|
1358
|
+
chatId: String(update.message.chat.id),
|
|
1359
|
+
messageThreadId: update.message.message_thread_id
|
|
1360
|
+
}), options);
|
|
1361
|
+
if (handledSlashCommand && typeof handledSlashCommand !== "boolean") {
|
|
1362
|
+
tasks.push(handledSlashCommand);
|
|
1363
|
+
}
|
|
1364
|
+
if (messageUpdate && !handledSlashCommand) {
|
|
1365
|
+
const task = this.handleIncomingMessageUpdate(messageUpdate, options);
|
|
1366
|
+
if (task) {
|
|
1367
|
+
tasks.push(task);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (update.callback_query) {
|
|
1371
|
+
const task = this.handleCallbackQuery(update.callback_query, options);
|
|
1372
|
+
if (task) {
|
|
1373
|
+
tasks.push(task);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (update.message_reaction) {
|
|
1377
|
+
tasks.push(...this.handleMessageReactionUpdate(update.message_reaction, options) ?? []);
|
|
1378
|
+
}
|
|
1379
|
+
if (this.businessMode && businessMessageUpdate) {
|
|
1380
|
+
const task = this.handleBusinessMessageUpdate(businessMessageUpdate, { isEdit: update.business_message === undefined }, options);
|
|
1381
|
+
const tracked = task.catch((error) => {
|
|
1382
|
+
this.logger.warn("Failed to process Telegram business message", {
|
|
1383
|
+
error: String(error),
|
|
1384
|
+
connectionId: businessMessageUpdate.business_connection_id
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
options?.waitUntil?.(tracked);
|
|
1388
|
+
tasks.push(task);
|
|
1389
|
+
}
|
|
1390
|
+
return tasks;
|
|
1391
|
+
}
|
|
1392
|
+
businessConnectionCacheKey(connectionId) {
|
|
1393
|
+
return `${this.name}:business-connection:${connectionId}`;
|
|
1394
|
+
}
|
|
1395
|
+
async cacheBusinessConnection(connection) {
|
|
1396
|
+
if (!this.chat) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await this.chat.getState().set(this.businessConnectionCacheKey(connection.id), connection, TELEGRAM_BUSINESS_CONNECTION_TTL_MS);
|
|
1400
|
+
}
|
|
1401
|
+
async resolveBusinessConnection(connectionId) {
|
|
1402
|
+
const state = this.chat?.getState();
|
|
1403
|
+
const key = this.businessConnectionCacheKey(connectionId);
|
|
1404
|
+
const cached = await state?.get(key);
|
|
1405
|
+
if (cached) {
|
|
1406
|
+
return cached;
|
|
1407
|
+
}
|
|
1408
|
+
const connection = await this.telegramFetch("getBusinessConnection", {
|
|
1409
|
+
business_connection_id: connectionId
|
|
1410
|
+
});
|
|
1411
|
+
await state?.set(key, connection, TELEGRAM_BUSINESS_CONNECTION_TTL_MS);
|
|
1412
|
+
return connection;
|
|
1413
|
+
}
|
|
1414
|
+
isBusinessConnectionUsable(connection) {
|
|
1415
|
+
return connection.is_enabled && (connection.rights?.can_reply ?? false);
|
|
1416
|
+
}
|
|
1417
|
+
async handleBusinessMessageUpdate(telegramMessage, routing, options) {
|
|
1418
|
+
if (!this.chat) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const connectionId = telegramMessage.business_connection_id;
|
|
1422
|
+
if (!connectionId) {
|
|
1423
|
+
this.logger.warn("Telegram business message missing connection id", {
|
|
1424
|
+
messageId: telegramMessage.message_id
|
|
1425
|
+
});
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const connection = await this.resolveBusinessConnection(connectionId);
|
|
1429
|
+
if (!this.isBusinessConnectionUsable(connection)) {
|
|
1430
|
+
this.logger.debug("Ignoring Telegram business message: connection cannot reply", {
|
|
1431
|
+
connectionId,
|
|
1432
|
+
isEnabled: connection.is_enabled,
|
|
1433
|
+
canReply: connection.rights?.can_reply ?? false
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (telegramMessage.sender_business_bot) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (telegramMessage.from?.id === connection.user.id) {
|
|
1441
|
+
this.logger.debug("Ignoring Telegram business message typed by the account owner", { connectionId, messageId: telegramMessage.message_id });
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const threadId = this.encodeThreadId({
|
|
1445
|
+
chatId: String(telegramMessage.chat.id),
|
|
1446
|
+
businessConnectionId: connectionId
|
|
1447
|
+
});
|
|
1448
|
+
if (telegramMessage.media_group_id) {
|
|
1449
|
+
const task = this.processIncomingMediaGroup(telegramMessage, threadId, options);
|
|
1450
|
+
const tracked = task.catch((error) => {
|
|
1451
|
+
this.logger.warn("Failed to process incoming Telegram media group", {
|
|
1452
|
+
error: String(error),
|
|
1453
|
+
mediaGroupId: telegramMessage.media_group_id,
|
|
1454
|
+
threadId
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
options?.waitUntil?.(tracked);
|
|
1458
|
+
await task;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const command = !routing.isEdit && this.handleSlashCommandUpdate(telegramMessage, threadId, options);
|
|
1462
|
+
if (command) {
|
|
1463
|
+
await command;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
this.startTypingForPrivateMessage(telegramMessage, threadId, options);
|
|
1467
|
+
const parsedMessage = this.parseTelegramMessage(telegramMessage, threadId);
|
|
1468
|
+
this.cacheMessage(parsedMessage);
|
|
1469
|
+
await this.chat.processMessage(this, threadId, parsedMessage, options);
|
|
1470
|
+
}
|
|
1471
|
+
handleIncomingMessageUpdate(telegramMessage, options) {
|
|
1472
|
+
if (!this.chat) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const threadId = this.encodeThreadId({
|
|
1476
|
+
chatId: String(telegramMessage.chat.id),
|
|
1477
|
+
messageThreadId: telegramMessage.message_thread_id
|
|
1478
|
+
});
|
|
1479
|
+
if (telegramMessage.media_group_id) {
|
|
1480
|
+
const task = this.processIncomingMediaGroup(telegramMessage, threadId, options);
|
|
1481
|
+
const tracked = task.catch((error) => {
|
|
1482
|
+
this.logger.warn("Failed to process incoming Telegram media group", {
|
|
1483
|
+
error: String(error),
|
|
1484
|
+
mediaGroupId: telegramMessage.media_group_id,
|
|
1485
|
+
threadId
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
options?.waitUntil?.(tracked);
|
|
1489
|
+
return task;
|
|
1490
|
+
}
|
|
1491
|
+
this.startTypingForPrivateMessage(telegramMessage, threadId, options);
|
|
1492
|
+
const parsedMessage = this.parseTelegramMessage(telegramMessage, threadId);
|
|
1493
|
+
this.cacheMessage(parsedMessage);
|
|
1494
|
+
return this.chat.processMessage(this, threadId, parsedMessage, options);
|
|
1495
|
+
}
|
|
1496
|
+
async processIncomingMediaGroup(telegramMessage, threadId, options) {
|
|
1497
|
+
if (!(this.chat && telegramMessage.media_group_id)) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const state = this.chat.getState();
|
|
1501
|
+
const mediaGroupKey = `${this.name}:incoming-media-group:${threadId}:${telegramMessage.media_group_id}`;
|
|
1502
|
+
const lockKey = `${mediaGroupKey}:lock`;
|
|
1503
|
+
let appended = false;
|
|
1504
|
+
for (;; ) {
|
|
1505
|
+
const lock = await state.acquireLock(lockKey, TELEGRAM_INCOMING_MEDIA_GROUP_LOCK_TTL_MS);
|
|
1506
|
+
if (!lock) {
|
|
1507
|
+
await this.sleep(TELEGRAM_INCOMING_MEDIA_GROUP_RETRY_MS);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
let entries = [];
|
|
1511
|
+
let remainingSettleMs = 0;
|
|
1512
|
+
try {
|
|
1513
|
+
entries = await state.get(mediaGroupKey) ?? [];
|
|
1514
|
+
if (!appended) {
|
|
1515
|
+
entries = entries.filter((entry) => entry.message.message_id !== telegramMessage.message_id);
|
|
1516
|
+
entries.push({ message: telegramMessage, receivedAt: Date.now() });
|
|
1517
|
+
await state.set(mediaGroupKey, entries.slice(-TELEGRAM_MEDIA_GROUP_MAX), TELEGRAM_INCOMING_MEDIA_GROUP_BUFFER_TTL_MS);
|
|
1518
|
+
appended = true;
|
|
1519
|
+
remainingSettleMs = TELEGRAM_INCOMING_MEDIA_GROUP_SETTLE_MS;
|
|
1520
|
+
} else if (entries.length === 0) {
|
|
1521
|
+
return;
|
|
1522
|
+
} else {
|
|
1523
|
+
const newestReceivedAt = Math.max(...entries.map((entry) => entry.receivedAt));
|
|
1524
|
+
remainingSettleMs = Math.max(0, TELEGRAM_INCOMING_MEDIA_GROUP_SETTLE_MS - (Date.now() - newestReceivedAt));
|
|
1525
|
+
if (remainingSettleMs === 0) {
|
|
1526
|
+
await state.delete(mediaGroupKey);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
} finally {
|
|
1530
|
+
await state.releaseLock(lock);
|
|
1531
|
+
}
|
|
1532
|
+
if (remainingSettleMs > 0) {
|
|
1533
|
+
await this.sleep(remainingSettleMs);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
const orderedMessages = entries.map((entry) => entry.message).sort((left, right) => left.message_id - right.message_id);
|
|
1537
|
+
const parsedMessages = orderedMessages.map((message) => this.parseTelegramMessage(message, threadId));
|
|
1538
|
+
const latestMessage = parsedMessages.at(-1);
|
|
1539
|
+
if (!latestMessage) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const contentMessage = parsedMessages.find((message) => message.text.length > 0) ?? latestMessage;
|
|
1543
|
+
const combinedMessage = new Message({
|
|
1544
|
+
id: latestMessage.id,
|
|
1545
|
+
threadId,
|
|
1546
|
+
text: contentMessage.text,
|
|
1547
|
+
formatted: contentMessage.formatted,
|
|
1548
|
+
raw: latestMessage.raw,
|
|
1549
|
+
author: latestMessage.author,
|
|
1550
|
+
metadata: latestMessage.metadata,
|
|
1551
|
+
attachments: parsedMessages.flatMap((message) => message.attachments),
|
|
1552
|
+
replyTo: parsedMessages.find((message) => message.replyTo)?.replyTo,
|
|
1553
|
+
isMention: parsedMessages.some((message) => message.isMention),
|
|
1554
|
+
links: parsedMessages.flatMap((message) => message.links)
|
|
1555
|
+
});
|
|
1556
|
+
this.startTypingForPrivateMessage(orderedMessages[0], threadId);
|
|
1557
|
+
this.cacheMessage(combinedMessage);
|
|
1558
|
+
await this.chat.processMessage(this, threadId, combinedMessage, options);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
handleSlashCommandUpdate(telegramMessage, threadId, options) {
|
|
1563
|
+
if (!this.chat) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
const slashCommand = this.parseSlashCommand(telegramMessage);
|
|
1567
|
+
if (!slashCommand) {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
this.startTypingForPrivateMessage(telegramMessage, threadId, options);
|
|
1571
|
+
const parsedMessage = this.parseTelegramMessage(telegramMessage, threadId);
|
|
1572
|
+
this.cacheMessage(parsedMessage);
|
|
1573
|
+
return Promise.resolve(this.chat.processSlashCommand({
|
|
1574
|
+
adapter: this,
|
|
1575
|
+
channelId: threadId,
|
|
1576
|
+
command: slashCommand.command,
|
|
1577
|
+
text: slashCommand.text,
|
|
1578
|
+
user: parsedMessage.author,
|
|
1579
|
+
raw: telegramMessage
|
|
1580
|
+
}, options));
|
|
1581
|
+
}
|
|
1582
|
+
startTypingForPrivateMessage(telegramMessage, threadId, options) {
|
|
1583
|
+
if (telegramMessage.chat.type !== "private" || telegramMessage.from?.is_bot) {
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const typingTask = this.startTyping(threadId).catch((error) => {
|
|
1587
|
+
this.logger.warn("Failed to send Telegram typing action", {
|
|
1588
|
+
error: String(error),
|
|
1589
|
+
threadId
|
|
1590
|
+
});
|
|
1591
|
+
});
|
|
1592
|
+
options?.waitUntil?.(typingTask);
|
|
1593
|
+
}
|
|
1594
|
+
parseSlashCommand(telegramMessage) {
|
|
1595
|
+
const hasText = telegramMessage.text !== undefined;
|
|
1596
|
+
const text2 = hasText ? telegramMessage.text : telegramMessage.caption;
|
|
1597
|
+
const entities = hasText ? telegramMessage.entities ?? [] : telegramMessage.caption_entities ?? [];
|
|
1598
|
+
if (!text2) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
const commandEntity = entities.find((entity) => entity.type === "bot_command" && entity.offset === 0);
|
|
1602
|
+
if (!commandEntity) {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
const rawCommand = this.entityText(text2, commandEntity);
|
|
1606
|
+
if (!rawCommand.startsWith("/")) {
|
|
1607
|
+
return null;
|
|
1608
|
+
}
|
|
1609
|
+
const commandWithoutSlash = rawCommand.slice(1);
|
|
1610
|
+
const atIndex = commandWithoutSlash.indexOf("@");
|
|
1611
|
+
const commandName = atIndex === -1 ? commandWithoutSlash : commandWithoutSlash.slice(0, atIndex);
|
|
1612
|
+
const targetBot = atIndex === -1 ? undefined : commandWithoutSlash.slice(atIndex + 1);
|
|
1613
|
+
if (!commandName) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
if (targetBot && targetBot.toLowerCase() !== this.userName.toLowerCase()) {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
command: `/${commandName}`,
|
|
1621
|
+
text: text2.slice(commandEntity.offset + commandEntity.length).trimStart()
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
handleCallbackQuery(callbackQuery, options) {
|
|
1625
|
+
if (!(this.chat && callbackQuery.message)) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const threadId = this.encodeThreadId({
|
|
1629
|
+
chatId: String(callbackQuery.message.chat.id),
|
|
1630
|
+
messageThreadId: callbackQuery.message.message_thread_id,
|
|
1631
|
+
businessConnectionId: callbackQuery.message.business_connection_id
|
|
1632
|
+
});
|
|
1633
|
+
const messageId = this.encodeMessageId(String(callbackQuery.message.chat.id), callbackQuery.message.message_id);
|
|
1634
|
+
const { actionId, value } = decodeTelegramCallbackData(callbackQuery.data);
|
|
1635
|
+
const task = this.chat.processAction({
|
|
1636
|
+
adapter: this,
|
|
1637
|
+
actionId,
|
|
1638
|
+
value,
|
|
1639
|
+
messageId,
|
|
1640
|
+
threadId,
|
|
1641
|
+
user: this.toAuthor(callbackQuery.from),
|
|
1642
|
+
raw: callbackQuery
|
|
1643
|
+
}, options);
|
|
1644
|
+
const ackTask = this.telegramFetch("answerCallbackQuery", {
|
|
1645
|
+
callback_query_id: callbackQuery.id
|
|
1646
|
+
}).catch((error) => {
|
|
1647
|
+
this.logger.warn("Failed to acknowledge Telegram callback query", {
|
|
1648
|
+
callbackQueryId: callbackQuery.id,
|
|
1649
|
+
error: String(error)
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
if (options?.waitUntil) {
|
|
1653
|
+
options.waitUntil(ackTask);
|
|
1654
|
+
}
|
|
1655
|
+
return task;
|
|
1656
|
+
}
|
|
1657
|
+
handleMessageReactionUpdate(reactionUpdate, options) {
|
|
1658
|
+
const tasks = [];
|
|
1659
|
+
if (!this.chat) {
|
|
1660
|
+
return tasks;
|
|
1661
|
+
}
|
|
1662
|
+
const threadId = this.encodeThreadId({
|
|
1663
|
+
chatId: String(reactionUpdate.chat.id),
|
|
1664
|
+
messageThreadId: reactionUpdate.message_thread_id
|
|
1665
|
+
});
|
|
1666
|
+
const messageId = this.encodeMessageId(String(reactionUpdate.chat.id), reactionUpdate.message_id);
|
|
1667
|
+
const oldReactions = new Set(reactionUpdate.old_reaction.map((reaction) => this.reactionKey(reaction)));
|
|
1668
|
+
const newReactions = new Set(reactionUpdate.new_reaction.map((reaction) => this.reactionKey(reaction)));
|
|
1669
|
+
const actor = reactionUpdate.user ? this.toAuthor(reactionUpdate.user) : this.toReactionActorAuthor(reactionUpdate.chat);
|
|
1670
|
+
for (const reaction of reactionUpdate.new_reaction) {
|
|
1671
|
+
const key = this.reactionKey(reaction);
|
|
1672
|
+
if (!oldReactions.has(key)) {
|
|
1673
|
+
const task = this.chat.processReaction({
|
|
1674
|
+
adapter: this,
|
|
1675
|
+
threadId,
|
|
1676
|
+
messageId,
|
|
1677
|
+
emoji: this.reactionToEmojiValue(reaction),
|
|
1678
|
+
rawEmoji: key,
|
|
1679
|
+
added: true,
|
|
1680
|
+
user: actor,
|
|
1681
|
+
raw: reactionUpdate
|
|
1682
|
+
}, options);
|
|
1683
|
+
if (task) {
|
|
1684
|
+
tasks.push(task);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
for (const reaction of reactionUpdate.old_reaction) {
|
|
1689
|
+
const key = this.reactionKey(reaction);
|
|
1690
|
+
if (!newReactions.has(key)) {
|
|
1691
|
+
const task = this.chat.processReaction({
|
|
1692
|
+
adapter: this,
|
|
1693
|
+
threadId,
|
|
1694
|
+
messageId,
|
|
1695
|
+
emoji: this.reactionToEmojiValue(reaction),
|
|
1696
|
+
rawEmoji: key,
|
|
1697
|
+
added: false,
|
|
1698
|
+
user: actor,
|
|
1699
|
+
raw: reactionUpdate
|
|
1700
|
+
}, options);
|
|
1701
|
+
if (task) {
|
|
1702
|
+
tasks.push(task);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return tasks;
|
|
1707
|
+
}
|
|
1708
|
+
async postMessage(threadId, message, replyToMessageId) {
|
|
1709
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1710
|
+
const replyParameters = this.buildReplyParameters(replyToMessageId, parsedThread.chatId);
|
|
1711
|
+
const card = extractCard(message);
|
|
1712
|
+
const replyMarkup = card ? cardToTelegramInlineKeyboard(card) : undefined;
|
|
1713
|
+
const parseMode = this.resolveParseMode(message, card);
|
|
1714
|
+
const plainText = truncateForTelegram(convertEmojiPlaceholders(this.renderPlainTextMessage(message, card), "gchat"), TELEGRAM_MESSAGE_LIMIT, "plain");
|
|
1715
|
+
const text2 = truncateForTelegram(convertEmojiPlaceholders(card ? this.formatConverter.fromMarkdown(cardToFallbackText(card, { boldFormat: "**" })) : this.formatConverter.renderPostable(message), "gchat"), TELEGRAM_MESSAGE_LIMIT, parseMode);
|
|
1716
|
+
const files = extractFiles(message);
|
|
1717
|
+
const attachments = extractPostableAttachments(message);
|
|
1718
|
+
if (files.length > 0 && attachments.length > 0) {
|
|
1719
|
+
throw new ValidationError("telegram", "Telegram adapter does not support mixing file uploads and attachments in one message");
|
|
1720
|
+
}
|
|
1721
|
+
const rich = this.resolveRichMessage(message, card, files.length, attachments.length);
|
|
1722
|
+
let rawMessages;
|
|
1723
|
+
if (files.length > 0) {
|
|
1724
|
+
const [file] = files;
|
|
1725
|
+
if (!file) {
|
|
1726
|
+
throw new ValidationError("telegram", "File upload payload is empty");
|
|
1727
|
+
}
|
|
1728
|
+
rawMessages = files.length === 1 ? [
|
|
1729
|
+
await this.sendDocument(parsedThread, file, text2, plainText, replyMarkup, parseMode, replyParameters)
|
|
1730
|
+
] : await this.sendDocumentMediaGroup(parsedThread, files, text2, plainText, replyMarkup, parseMode, replyParameters);
|
|
1731
|
+
} else if (attachments.length > 0) {
|
|
1732
|
+
const [attachment] = attachments;
|
|
1733
|
+
if (!attachment) {
|
|
1734
|
+
throw new ValidationError("telegram", "Attachment upload payload is empty");
|
|
1735
|
+
}
|
|
1736
|
+
rawMessages = attachments.length === 1 ? [
|
|
1737
|
+
await this.sendAttachment(parsedThread, attachment, text2, plainText, replyMarkup, parseMode, replyParameters)
|
|
1738
|
+
] : await this.sendAttachmentMediaGroup(parsedThread, attachments, text2, plainText, replyMarkup, parseMode, replyParameters);
|
|
1739
|
+
} else {
|
|
1740
|
+
if (!text2.trim()) {
|
|
1741
|
+
throw new ValidationError("telegram", "Message text cannot be empty");
|
|
1742
|
+
}
|
|
1743
|
+
const sendRegular = () => this.sendRegularMessage(parsedThread, text2, plainText, parseMode, replyMarkup, threadId, replyParameters);
|
|
1744
|
+
rawMessages = [
|
|
1745
|
+
rich ? await this.withTelegramRichFallback(() => this.telegramFetch("sendRichMessage", {
|
|
1746
|
+
...this.buildChatTargetParams(parsedThread),
|
|
1747
|
+
rich_message: {
|
|
1748
|
+
markdown: rich.markdown
|
|
1749
|
+
},
|
|
1750
|
+
reply_markup: replyMarkup,
|
|
1751
|
+
reply_parameters: replyParameters
|
|
1752
|
+
}), sendRegular, {
|
|
1753
|
+
method: "sendRichMessage",
|
|
1754
|
+
threadId
|
|
1755
|
+
}) : await sendRegular()
|
|
1756
|
+
];
|
|
1757
|
+
}
|
|
1758
|
+
const parsedMessages = rawMessages.map((rawMessage) => this.parseTelegramMessage(rawMessage, this.encodeThreadId({
|
|
1759
|
+
chatId: String(rawMessage.chat.id),
|
|
1760
|
+
messageThreadId: rawMessage.message_thread_id ?? parsedThread.messageThreadId,
|
|
1761
|
+
businessConnectionId: parsedThread.businessConnectionId
|
|
1762
|
+
}), rich ? {
|
|
1763
|
+
formatted: rich.formatted,
|
|
1764
|
+
text: rich.text
|
|
1765
|
+
} : undefined));
|
|
1766
|
+
for (const parsedMessage of parsedMessages) {
|
|
1767
|
+
this.cacheMessage(parsedMessage);
|
|
1768
|
+
}
|
|
1769
|
+
const sentMessage = parsedMessages.at(-1);
|
|
1770
|
+
if (!sentMessage) {
|
|
1771
|
+
throw new NetworkError("telegram", "Telegram postMessage did not return any sent messages");
|
|
1772
|
+
}
|
|
1773
|
+
return {
|
|
1774
|
+
id: sentMessage.id,
|
|
1775
|
+
threadId: sentMessage.threadId,
|
|
1776
|
+
raw: sentMessage.raw
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
async postChannelMessage(channelId, message) {
|
|
1780
|
+
return this.postMessage(channelId, message);
|
|
1781
|
+
}
|
|
1782
|
+
async reply(threadId, messageId, message) {
|
|
1783
|
+
return this.postMessage(threadId, message, messageId);
|
|
1784
|
+
}
|
|
1785
|
+
async editMessage(threadId, messageId, message) {
|
|
1786
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1787
|
+
const { messageId: telegramMessageId, compositeId } = this.decodeCompositeMessageId(messageId, parsedThread.chatId);
|
|
1788
|
+
const card = extractCard(message);
|
|
1789
|
+
const replyMarkup = card ? cardToTelegramInlineKeyboard(card) : undefined;
|
|
1790
|
+
const parseMode = this.resolveParseMode(message, card);
|
|
1791
|
+
const plainText = truncateForTelegram(convertEmojiPlaceholders(this.renderPlainTextMessage(message, card), "gchat"), TELEGRAM_MESSAGE_LIMIT, "plain");
|
|
1792
|
+
const text2 = truncateForTelegram(convertEmojiPlaceholders(card ? this.formatConverter.fromMarkdown(cardToFallbackText(card, { boldFormat: "**" })) : this.formatConverter.renderPostable(message), "gchat"), TELEGRAM_MESSAGE_LIMIT, parseMode);
|
|
1793
|
+
const rich = this.resolveRichMessage(message, card, 0, 0);
|
|
1794
|
+
if (!text2.trim()) {
|
|
1795
|
+
throw new ValidationError("telegram", "Message text cannot be empty");
|
|
1796
|
+
}
|
|
1797
|
+
const editRegular = () => this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => this.telegramFetch("editMessageText", {
|
|
1798
|
+
...this.buildChatTargetParams(parsedThread),
|
|
1799
|
+
message_id: telegramMessageId,
|
|
1800
|
+
text: resolvedText,
|
|
1801
|
+
reply_markup: replyMarkup ?? emptyTelegramInlineKeyboard(),
|
|
1802
|
+
parse_mode: toBotApiParseMode(resolvedParseMode)
|
|
1803
|
+
}), {
|
|
1804
|
+
initialText: text2,
|
|
1805
|
+
fallbackText: plainText,
|
|
1806
|
+
messageId,
|
|
1807
|
+
method: "editMessageText",
|
|
1808
|
+
threadId
|
|
1809
|
+
});
|
|
1810
|
+
const result = rich ? await this.withTelegramRichFallback(() => this.telegramFetch("editMessageText", {
|
|
1811
|
+
...this.buildChatTargetParams(parsedThread),
|
|
1812
|
+
message_id: telegramMessageId,
|
|
1813
|
+
rich_message: {
|
|
1814
|
+
markdown: rich.markdown
|
|
1815
|
+
},
|
|
1816
|
+
reply_markup: replyMarkup ?? emptyTelegramInlineKeyboard()
|
|
1817
|
+
}), editRegular, {
|
|
1818
|
+
messageId,
|
|
1819
|
+
method: "editMessageText",
|
|
1820
|
+
threadId
|
|
1821
|
+
}) : await editRegular();
|
|
1822
|
+
if (result === true) {
|
|
1823
|
+
const existing = this.findCachedMessage(compositeId);
|
|
1824
|
+
if (!existing) {
|
|
1825
|
+
throw new NotImplementedError("Telegram returned a non-message edit result and no cached message was found", "editMessage");
|
|
1826
|
+
}
|
|
1827
|
+
const updated = new Message({
|
|
1828
|
+
...existing,
|
|
1829
|
+
text: rich?.text ?? text2,
|
|
1830
|
+
formatted: rich?.formatted ?? this.formatConverter.toAst(text2),
|
|
1831
|
+
metadata: {
|
|
1832
|
+
...existing.metadata,
|
|
1833
|
+
edited: true,
|
|
1834
|
+
editedAt: /* @__PURE__ */ new Date
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
this.cacheMessage(updated);
|
|
1838
|
+
return {
|
|
1839
|
+
id: updated.id,
|
|
1840
|
+
threadId: updated.threadId,
|
|
1841
|
+
raw: updated.raw
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
const resultingThreadId = this.encodeThreadId({
|
|
1845
|
+
chatId: String(result.chat.id),
|
|
1846
|
+
messageThreadId: result.message_thread_id ?? parsedThread.messageThreadId,
|
|
1847
|
+
businessConnectionId: parsedThread.businessConnectionId
|
|
1848
|
+
});
|
|
1849
|
+
const parsedMessage = this.parseTelegramMessage(result, resultingThreadId, rich ? {
|
|
1850
|
+
formatted: rich.formatted,
|
|
1851
|
+
text: rich.text
|
|
1852
|
+
} : undefined);
|
|
1853
|
+
this.cacheMessage(parsedMessage);
|
|
1854
|
+
return {
|
|
1855
|
+
id: parsedMessage.id,
|
|
1856
|
+
threadId: parsedMessage.threadId,
|
|
1857
|
+
raw: result
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
async deleteMessage(threadId, messageId) {
|
|
1861
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1862
|
+
const { messageId: telegramMessageId, compositeId } = this.decodeCompositeMessageId(messageId, parsedThread.chatId);
|
|
1863
|
+
if (parsedThread.businessConnectionId) {
|
|
1864
|
+
await this.telegramFetch("deleteBusinessMessages", {
|
|
1865
|
+
business_connection_id: parsedThread.businessConnectionId,
|
|
1866
|
+
message_ids: [telegramMessageId]
|
|
1867
|
+
});
|
|
1868
|
+
} else {
|
|
1869
|
+
await this.telegramFetch("deleteMessage", {
|
|
1870
|
+
chat_id: parsedThread.chatId,
|
|
1871
|
+
message_id: telegramMessageId
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
this.deleteCachedMessage(compositeId);
|
|
1875
|
+
}
|
|
1876
|
+
async addReaction(threadId, messageId, emoji) {
|
|
1877
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1878
|
+
this.assertReactionsSupported(parsedThread, "addReaction");
|
|
1879
|
+
const { messageId: telegramMessageId } = this.decodeCompositeMessageId(messageId, parsedThread.chatId);
|
|
1880
|
+
await this.telegramFetch("setMessageReaction", {
|
|
1881
|
+
chat_id: parsedThread.chatId,
|
|
1882
|
+
message_id: telegramMessageId,
|
|
1883
|
+
reaction: [this.toTelegramReaction(emoji)]
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
async removeReaction(threadId, messageId, _emoji) {
|
|
1887
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1888
|
+
this.assertReactionsSupported(parsedThread, "removeReaction");
|
|
1889
|
+
const { messageId: telegramMessageId } = this.decodeCompositeMessageId(messageId, parsedThread.chatId);
|
|
1890
|
+
await this.telegramFetch("setMessageReaction", {
|
|
1891
|
+
chat_id: parsedThread.chatId,
|
|
1892
|
+
message_id: telegramMessageId,
|
|
1893
|
+
reaction: []
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
assertReactionsSupported(thread, method) {
|
|
1897
|
+
if (thread.businessConnectionId) {
|
|
1898
|
+
throw new NotImplementedError("Telegram Bot API does not support reactions on behalf of a business account", method);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async startTyping(threadId) {
|
|
1902
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
1903
|
+
await this.telegramFetch("sendChatAction", {
|
|
1904
|
+
...this.buildChatTargetParams(parsedThread),
|
|
1905
|
+
action: "typing"
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
async stream(threadId, textStream, options) {
|
|
1909
|
+
if (this.nativeStreaming && this.isDM(threadId)) {
|
|
1910
|
+
return await this.nativeDraftStream(threadId, textStream, options);
|
|
1911
|
+
}
|
|
1912
|
+
return await this.postAndEditStream(threadId, textStream, options);
|
|
1913
|
+
}
|
|
1914
|
+
async postAndEditStream(threadId, textStream, options) {
|
|
1915
|
+
const defaultIntervalMs = this.isDM(threadId) ? TELEGRAM_DEFAULT_PRIVATE_STREAMING_EDIT_INTERVAL_MS : TELEGRAM_DEFAULT_NON_PRIVATE_STREAMING_EDIT_INTERVAL_MS;
|
|
1916
|
+
const intervalMs = Math.max(this.clampInteger(options?.updateIntervalMs, 0, 0, Number.MAX_SAFE_INTEGER), this.streamingEditIntervalMs ?? defaultIntervalMs);
|
|
1917
|
+
const placeholderText = options?.fallbackStreamingPlaceholderText === undefined ? TELEGRAM_STREAM_PLACEHOLDER_TEXT : options.fallbackStreamingPlaceholderText;
|
|
1918
|
+
const renderer = new StreamingMarkdownRenderer;
|
|
1919
|
+
let accumulated = "";
|
|
1920
|
+
let posted = null;
|
|
1921
|
+
let editThreadId = threadId;
|
|
1922
|
+
let lastEditContent = "";
|
|
1923
|
+
let lastEditAt = 0;
|
|
1924
|
+
let blockedUntil = 0;
|
|
1925
|
+
let rateLimitError = null;
|
|
1926
|
+
if (placeholderText !== null) {
|
|
1927
|
+
posted = await this.postMessage(threadId, placeholderText);
|
|
1928
|
+
editThreadId = posted.threadId || threadId;
|
|
1929
|
+
lastEditContent = placeholderText;
|
|
1930
|
+
lastEditAt = Date.now();
|
|
1931
|
+
}
|
|
1932
|
+
const applyEdit = async (content) => {
|
|
1933
|
+
if (!posted) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
posted = await this.editMessage(editThreadId, posted.id, {
|
|
1937
|
+
markdown: content
|
|
1938
|
+
});
|
|
1939
|
+
editThreadId = posted.threadId || editThreadId;
|
|
1940
|
+
lastEditContent = content;
|
|
1941
|
+
lastEditAt = Date.now();
|
|
1942
|
+
blockedUntil = 0;
|
|
1943
|
+
rateLimitError = null;
|
|
1944
|
+
};
|
|
1945
|
+
const rememberRateLimit = (error) => {
|
|
1946
|
+
const retryAfterMs = Math.max(0, error.retryAfter ?? 1) * 1000;
|
|
1947
|
+
blockedUntil = Math.max(blockedUntil, Date.now() + retryAfterMs);
|
|
1948
|
+
rateLimitError = error;
|
|
1949
|
+
};
|
|
1950
|
+
const shouldEdit = (content) => Boolean(posted) && content.trim().length > 0 && content !== lastEditContent;
|
|
1951
|
+
const flushEdit = async (content) => {
|
|
1952
|
+
if (!shouldEdit(content)) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
await applyEdit(content);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
if (error instanceof AdapterRateLimitError) {
|
|
1959
|
+
rememberRateLimit(error);
|
|
1960
|
+
}
|
|
1961
|
+
this.logger.warn("Telegram stream edit failed", {
|
|
1962
|
+
error: String(error),
|
|
1963
|
+
threadId
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
const flushFinalEdit = async (content) => {
|
|
1968
|
+
if (!shouldEdit(content)) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const blockedMs = Math.max(0, blockedUntil - Date.now());
|
|
1972
|
+
const pacingMs = Math.max(0, intervalMs - (Date.now() - lastEditAt));
|
|
1973
|
+
if (blockedMs > TELEGRAM_STREAM_FINAL_EDIT_RETRY_CAP_MS && rateLimitError) {
|
|
1974
|
+
throw rateLimitError;
|
|
1975
|
+
}
|
|
1976
|
+
await this.sleep(Math.max(blockedMs, pacingMs));
|
|
1977
|
+
try {
|
|
1978
|
+
await applyEdit(content);
|
|
1979
|
+
return;
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
if (!(error instanceof AdapterRateLimitError)) {
|
|
1982
|
+
throw error;
|
|
1983
|
+
}
|
|
1984
|
+
rememberRateLimit(error);
|
|
1985
|
+
const retryAfterMs = (error.retryAfter ?? 1) * 1000;
|
|
1986
|
+
if (retryAfterMs > TELEGRAM_STREAM_FINAL_EDIT_RETRY_CAP_MS) {
|
|
1987
|
+
throw error;
|
|
1988
|
+
}
|
|
1989
|
+
await this.sleep(retryAfterMs);
|
|
1990
|
+
}
|
|
1991
|
+
await applyEdit(content);
|
|
1992
|
+
};
|
|
1993
|
+
for await (const chunk of textStream) {
|
|
1994
|
+
let text2 = null;
|
|
1995
|
+
if (typeof chunk === "string") {
|
|
1996
|
+
text2 = chunk;
|
|
1997
|
+
} else if (chunk.type === "markdown_text") {
|
|
1998
|
+
text2 = chunk.text;
|
|
1999
|
+
}
|
|
2000
|
+
if (text2 === null) {
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
accumulated += text2;
|
|
2004
|
+
renderer.push(text2);
|
|
2005
|
+
if (posted) {
|
|
2006
|
+
if (Date.now() >= blockedUntil && Date.now() - lastEditAt >= intervalMs) {
|
|
2007
|
+
await flushEdit(renderer.render());
|
|
2008
|
+
}
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const initial = renderer.render();
|
|
2012
|
+
if (initial.trim()) {
|
|
2013
|
+
posted = await this.postMessage(threadId, { markdown: initial });
|
|
2014
|
+
editThreadId = posted.threadId || threadId;
|
|
2015
|
+
lastEditContent = initial;
|
|
2016
|
+
lastEditAt = Date.now();
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const finalContent = renderer.finish();
|
|
2020
|
+
if (posted) {
|
|
2021
|
+
await flushFinalEdit(finalContent);
|
|
2022
|
+
return posted;
|
|
2023
|
+
}
|
|
2024
|
+
if (!accumulated.trim()) {
|
|
2025
|
+
throw new ValidationError("telegram", "Telegram streaming requires text content");
|
|
2026
|
+
}
|
|
2027
|
+
return await this.postMessage(threadId, { markdown: accumulated });
|
|
2028
|
+
}
|
|
2029
|
+
async nativeDraftStream(threadId, textStream, options) {
|
|
2030
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
2031
|
+
const updateIntervalMs = this.clampInteger(options?.updateIntervalMs, TELEGRAM_DEFAULT_STREAM_UPDATE_INTERVAL_MS, 0, Number.MAX_SAFE_INTEGER);
|
|
2032
|
+
const renderer = new StreamingMarkdownRenderer;
|
|
2033
|
+
const draftId = this.createDraftId();
|
|
2034
|
+
let accumulated = "";
|
|
2035
|
+
let lastDraftText = null;
|
|
2036
|
+
let lastFlushAt = 0;
|
|
2037
|
+
let draftStreamingEnabled = true;
|
|
2038
|
+
let streamUsesMarkdown = true;
|
|
2039
|
+
let streamUsesRich = this.richMessagesAvailable;
|
|
2040
|
+
const renderMarkdownForTelegram = (text2) => convertEmojiPlaceholders(this.formatConverter.fromMarkdown(text2), "gchat");
|
|
2041
|
+
const renderMarkdownText = (text2) => truncateForTelegram(renderMarkdownForTelegram(text2), TELEGRAM_MESSAGE_LIMIT, "MarkdownV2");
|
|
2042
|
+
const renderPlainText = (text2) => truncateForTelegram(this.resolveTelegramFallbackText(text2, markdownToPlainText(text2)), TELEGRAM_MESSAGE_LIMIT, "plain");
|
|
2043
|
+
const sendDraft = async (text2, useMarkdown) => {
|
|
2044
|
+
if (!draftStreamingEnabled || text2 === lastDraftText) {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
let draftText = text2;
|
|
2048
|
+
if (streamUsesRich) {
|
|
2049
|
+
try {
|
|
2050
|
+
await this.telegramFetch("sendRichMessageDraft", {
|
|
2051
|
+
...this.buildChatTargetParams(parsedThread),
|
|
2052
|
+
draft_id: draftId,
|
|
2053
|
+
rich_message: {
|
|
2054
|
+
markdown: text2
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
lastDraftText = text2;
|
|
2058
|
+
lastFlushAt = Date.now();
|
|
2059
|
+
return;
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
if (!this.canFallbackFromRichMessage(error, "sendRichMessageDraft")) {
|
|
2062
|
+
draftStreamingEnabled = false;
|
|
2063
|
+
this.logger.warn("Telegram rich draft streaming update failed", {
|
|
2064
|
+
error: String(error),
|
|
2065
|
+
threadId
|
|
2066
|
+
});
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
this.rememberRichMessageFailure(error, "sendRichMessageDraft");
|
|
2070
|
+
this.logger.warn("Telegram rich draft failed; retrying with a regular draft", {
|
|
2071
|
+
error: String(error),
|
|
2072
|
+
threadId
|
|
2073
|
+
});
|
|
2074
|
+
streamUsesRich = false;
|
|
2075
|
+
draftText = useMarkdown ? renderMarkdownText(renderer.render()) : renderPlainText(accumulated);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
if (useMarkdown) {
|
|
2080
|
+
await this.telegramFetch("sendMessageDraft", {
|
|
2081
|
+
...this.buildChatTargetParams(parsedThread),
|
|
2082
|
+
draft_id: draftId,
|
|
2083
|
+
text: draftText,
|
|
2084
|
+
parse_mode: toBotApiParseMode("MarkdownV2")
|
|
2085
|
+
});
|
|
2086
|
+
} else {
|
|
2087
|
+
await this.telegramFetch("sendMessageDraft", {
|
|
2088
|
+
...this.buildChatTargetParams(parsedThread),
|
|
2089
|
+
draft_id: draftId,
|
|
2090
|
+
text: draftText
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
lastDraftText = draftText;
|
|
2094
|
+
lastFlushAt = Date.now();
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
if (useMarkdown && this.isTelegramMarkdownParseError(error)) {
|
|
2097
|
+
streamUsesMarkdown = false;
|
|
2098
|
+
const plainDraftText = renderPlainText(accumulated);
|
|
2099
|
+
try {
|
|
2100
|
+
await this.telegramFetch("sendMessageDraft", {
|
|
2101
|
+
...this.buildChatTargetParams(parsedThread),
|
|
2102
|
+
draft_id: draftId,
|
|
2103
|
+
text: plainDraftText
|
|
2104
|
+
});
|
|
2105
|
+
lastDraftText = plainDraftText;
|
|
2106
|
+
lastFlushAt = Date.now();
|
|
2107
|
+
} catch (retryError) {
|
|
2108
|
+
draftStreamingEnabled = false;
|
|
2109
|
+
this.logger.warn("Telegram draft streaming update failed", {
|
|
2110
|
+
error: String(retryError),
|
|
2111
|
+
threadId
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
draftStreamingEnabled = false;
|
|
2117
|
+
this.logger.warn("Telegram draft streaming update failed", {
|
|
2118
|
+
error: String(error),
|
|
2119
|
+
threadId
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
const flushDraft = async () => {
|
|
2124
|
+
if (!draftStreamingEnabled) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
let draftText;
|
|
2128
|
+
if (streamUsesRich) {
|
|
2129
|
+
draftText = truncateRichMarkdown(renderer.render());
|
|
2130
|
+
} else if (streamUsesMarkdown) {
|
|
2131
|
+
draftText = renderMarkdownText(renderer.render());
|
|
2132
|
+
} else {
|
|
2133
|
+
draftText = renderPlainText(accumulated);
|
|
2134
|
+
}
|
|
2135
|
+
await sendDraft(draftText, streamUsesMarkdown);
|
|
2136
|
+
};
|
|
2137
|
+
for await (const chunk of textStream) {
|
|
2138
|
+
let text2 = null;
|
|
2139
|
+
if (typeof chunk === "string") {
|
|
2140
|
+
text2 = chunk;
|
|
2141
|
+
} else if (chunk.type === "markdown_text") {
|
|
2142
|
+
text2 = chunk.text;
|
|
2143
|
+
}
|
|
2144
|
+
if (text2 === null) {
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
accumulated += text2;
|
|
2148
|
+
renderer.push(text2);
|
|
2149
|
+
if (Date.now() - lastFlushAt >= updateIntervalMs) {
|
|
2150
|
+
await flushDraft();
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (!accumulated.trim()) {
|
|
2154
|
+
throw new ValidationError("telegram", "Telegram streaming requires text content");
|
|
2155
|
+
}
|
|
2156
|
+
const finalMarkdown = renderer.finish();
|
|
2157
|
+
await flushDraft();
|
|
2158
|
+
if (streamUsesRich) {
|
|
2159
|
+
const markdown = truncateRichMarkdown(finalMarkdown);
|
|
2160
|
+
try {
|
|
2161
|
+
const raw2 = await this.telegramFetch("sendRichMessage", {
|
|
2162
|
+
...this.buildChatTargetParams(parsedThread),
|
|
2163
|
+
rich_message: {
|
|
2164
|
+
markdown
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
const resultingThreadId2 = this.encodeThreadId({
|
|
2168
|
+
chatId: String(raw2.chat.id),
|
|
2169
|
+
messageThreadId: raw2.message_thread_id ?? parsedThread.messageThreadId,
|
|
2170
|
+
businessConnectionId: parsedThread.businessConnectionId
|
|
2171
|
+
});
|
|
2172
|
+
const formatted = this.formatConverter.toAst(accumulated);
|
|
2173
|
+
const message2 = this.parseTelegramMessage(raw2, resultingThreadId2, {
|
|
2174
|
+
formatted,
|
|
2175
|
+
text: toPlainText(formatted)
|
|
2176
|
+
});
|
|
2177
|
+
this.cacheMessage(message2);
|
|
2178
|
+
return {
|
|
2179
|
+
id: message2.id,
|
|
2180
|
+
threadId: message2.threadId,
|
|
2181
|
+
raw: raw2
|
|
2182
|
+
};
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
if (!this.canFallbackFromRichMessage(error, "sendRichMessage")) {
|
|
2185
|
+
throw error;
|
|
2186
|
+
}
|
|
2187
|
+
this.rememberRichMessageFailure(error, "sendRichMessage");
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const regularText = streamUsesMarkdown ? renderMarkdownText(finalMarkdown) : renderPlainText(accumulated);
|
|
2191
|
+
const plainText = renderPlainText(accumulated);
|
|
2192
|
+
const raw = await this.sendRegularMessage(parsedThread, regularText, plainText, streamUsesMarkdown ? "MarkdownV2" : "plain", undefined, threadId);
|
|
2193
|
+
const resultingThreadId = this.encodeThreadId({
|
|
2194
|
+
chatId: String(raw.chat.id),
|
|
2195
|
+
messageThreadId: raw.message_thread_id ?? parsedThread.messageThreadId,
|
|
2196
|
+
businessConnectionId: parsedThread.businessConnectionId
|
|
2197
|
+
});
|
|
2198
|
+
const message = this.parseTelegramMessage(raw, resultingThreadId);
|
|
2199
|
+
this.cacheMessage(message);
|
|
2200
|
+
return {
|
|
2201
|
+
id: message.id,
|
|
2202
|
+
threadId: message.threadId,
|
|
2203
|
+
raw
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
async fetchMessages(threadId, options = {}) {
|
|
2207
|
+
const messages = [...this.messageCache.get(threadId) ?? []].sort((a, b) => this.compareMessages(a, b));
|
|
2208
|
+
return this.paginateMessages(messages, options);
|
|
2209
|
+
}
|
|
2210
|
+
async fetchChannelMessages(channelId, options = {}) {
|
|
2211
|
+
const byId = /* @__PURE__ */ new Map;
|
|
2212
|
+
for (const [threadId, messages] of this.messageCache.entries()) {
|
|
2213
|
+
let decoded;
|
|
2214
|
+
try {
|
|
2215
|
+
decoded = this.decodeThreadId(threadId);
|
|
2216
|
+
} catch {
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
if (decoded.chatId !== channelId) {
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
for (const message of messages) {
|
|
2223
|
+
byId.set(message.id, message);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const allMessages = [...byId.values()].sort((a, b) => this.compareMessages(a, b));
|
|
2227
|
+
return this.paginateMessages(allMessages, options);
|
|
2228
|
+
}
|
|
2229
|
+
async fetchMessage(_threadId, messageId) {
|
|
2230
|
+
return this.findCachedMessage(messageId) ?? null;
|
|
2231
|
+
}
|
|
2232
|
+
async fetchThread(threadId) {
|
|
2233
|
+
const parsedThread = this.resolveThreadId(threadId);
|
|
2234
|
+
const encodedThreadId = this.encodeThreadId(parsedThread);
|
|
2235
|
+
if (parsedThread.businessConnectionId) {
|
|
2236
|
+
const chat2 = await this.fetchBusinessChat(parsedThread, encodedThreadId);
|
|
2237
|
+
return {
|
|
2238
|
+
id: encodedThreadId,
|
|
2239
|
+
channelId: parsedThread.chatId,
|
|
2240
|
+
channelName: chat2 ? this.chatDisplayName(chat2) : undefined,
|
|
2241
|
+
isDM: true,
|
|
2242
|
+
metadata: {
|
|
2243
|
+
chat: chat2,
|
|
2244
|
+
businessConnectionId: parsedThread.businessConnectionId
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
const chat = await this.telegramFetch("getChat", {
|
|
2249
|
+
chat_id: parsedThread.chatId
|
|
2250
|
+
});
|
|
2251
|
+
return {
|
|
2252
|
+
id: encodedThreadId,
|
|
2253
|
+
channelId: String(chat.id),
|
|
2254
|
+
channelName: this.chatDisplayName(chat),
|
|
2255
|
+
isDM: chat.type === "private",
|
|
2256
|
+
metadata: {
|
|
2257
|
+
chat,
|
|
2258
|
+
messageThreadId: parsedThread.messageThreadId
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
async fetchBusinessChat(thread, threadId) {
|
|
2263
|
+
const cachedChat = this.messageCache.get(threadId)?.at(-1)?.raw.chat;
|
|
2264
|
+
try {
|
|
2265
|
+
return await this.telegramFetch("getChat", {
|
|
2266
|
+
chat_id: thread.chatId
|
|
2267
|
+
});
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
if (!cachedChat) {
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
this.logger.debug("Telegram getChat failed for business thread, using cached chat", { error: String(error), threadId });
|
|
2273
|
+
return cachedChat;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async fetchChannelInfo(channelId) {
|
|
2277
|
+
const chat = await this.telegramFetch("getChat", {
|
|
2278
|
+
chat_id: channelId
|
|
2279
|
+
});
|
|
2280
|
+
let memberCount;
|
|
2281
|
+
try {
|
|
2282
|
+
memberCount = await this.telegramFetch("getChatMemberCount", {
|
|
2283
|
+
chat_id: channelId
|
|
2284
|
+
});
|
|
2285
|
+
} catch {
|
|
2286
|
+
memberCount = undefined;
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
id: String(chat.id),
|
|
2290
|
+
name: this.chatDisplayName(chat),
|
|
2291
|
+
isDM: chat.type === "private",
|
|
2292
|
+
memberCount,
|
|
2293
|
+
metadata: {
|
|
2294
|
+
chat
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
channelIdFromThreadId(threadId) {
|
|
2299
|
+
const { chatId, businessConnectionId } = this.resolveThreadId(threadId);
|
|
2300
|
+
if (businessConnectionId) {
|
|
2301
|
+
return this.encodeThreadId({ chatId, businessConnectionId });
|
|
2302
|
+
}
|
|
2303
|
+
return `telegram:${chatId}`;
|
|
2304
|
+
}
|
|
2305
|
+
async openDM(userId) {
|
|
2306
|
+
return this.encodeThreadId({ chatId: userId });
|
|
2307
|
+
}
|
|
2308
|
+
isDM(threadId) {
|
|
2309
|
+
const { chatId } = this.resolveThreadId(threadId);
|
|
2310
|
+
return !chatId.startsWith("-");
|
|
2311
|
+
}
|
|
2312
|
+
encodeThreadId(platformData) {
|
|
2313
|
+
if (platformData.businessConnectionId) {
|
|
2314
|
+
return `telegram:biz:${platformData.businessConnectionId}:${platformData.chatId}`;
|
|
2315
|
+
}
|
|
2316
|
+
if (typeof platformData.messageThreadId === "number") {
|
|
2317
|
+
return `telegram:${platformData.chatId}:${platformData.messageThreadId}`;
|
|
2318
|
+
}
|
|
2319
|
+
return `telegram:${platformData.chatId}`;
|
|
2320
|
+
}
|
|
2321
|
+
decodeThreadId(threadId) {
|
|
2322
|
+
const parts = threadId.split(":");
|
|
2323
|
+
if (parts[0] !== "telegram") {
|
|
2324
|
+
throw new ValidationError("telegram", `Invalid Telegram thread ID: ${threadId}`);
|
|
2325
|
+
}
|
|
2326
|
+
if (parts[1] === "biz") {
|
|
2327
|
+
if (parts.length !== 4) {
|
|
2328
|
+
throw new ValidationError("telegram", `Invalid Telegram business thread ID: ${threadId}`);
|
|
2329
|
+
}
|
|
2330
|
+
const businessConnectionId = parts[2];
|
|
2331
|
+
const chatId2 = parts[3];
|
|
2332
|
+
if (!(businessConnectionId && chatId2)) {
|
|
2333
|
+
throw new ValidationError("telegram", `Invalid Telegram business thread ID: ${threadId}`);
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
chatId: chatId2,
|
|
2337
|
+
businessConnectionId
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
2341
|
+
throw new ValidationError("telegram", `Invalid Telegram thread ID: ${threadId}`);
|
|
2342
|
+
}
|
|
2343
|
+
const chatId = parts[1];
|
|
2344
|
+
if (!chatId) {
|
|
2345
|
+
throw new ValidationError("telegram", `Invalid Telegram thread ID: ${threadId}`);
|
|
2346
|
+
}
|
|
2347
|
+
const messageThreadPart = parts[2];
|
|
2348
|
+
if (!messageThreadPart) {
|
|
2349
|
+
return { chatId };
|
|
2350
|
+
}
|
|
2351
|
+
const messageThreadId = Number.parseInt(messageThreadPart, 10);
|
|
2352
|
+
if (!Number.isFinite(messageThreadId)) {
|
|
2353
|
+
throw new ValidationError("telegram", `Invalid Telegram thread topic ID in thread ID: ${threadId}`);
|
|
2354
|
+
}
|
|
2355
|
+
return {
|
|
2356
|
+
chatId,
|
|
2357
|
+
messageThreadId
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
parseMessage(raw) {
|
|
2361
|
+
const threadId = this.encodeThreadId({
|
|
2362
|
+
chatId: String(raw.chat.id),
|
|
2363
|
+
messageThreadId: raw.message_thread_id
|
|
2364
|
+
});
|
|
2365
|
+
const message = this.parseTelegramMessage(raw, threadId);
|
|
2366
|
+
this.cacheMessage(message);
|
|
2367
|
+
return message;
|
|
2368
|
+
}
|
|
2369
|
+
renderFormatted(content) {
|
|
2370
|
+
return this.formatConverter.fromAst(content);
|
|
2371
|
+
}
|
|
2372
|
+
parseTelegramMessage(raw, threadId, content) {
|
|
2373
|
+
const richMarkdown = raw.rich_message ? richMessageToMarkdown(raw.rich_message) : "";
|
|
2374
|
+
const plainText = content?.text ?? raw.text ?? raw.caption ?? (raw.sticker ? raw.sticker.emoji ?? raw.sticker.set_name ?? "sticker" : undefined) ?? describeNonFileContent(raw) ?? (raw.rich_message ? richMessageToText(raw.rich_message) : "");
|
|
2375
|
+
const entities = raw.entities ?? raw.caption_entities ?? [];
|
|
2376
|
+
const text2 = content?.text ? content.text : applyTelegramEntities(plainText, entities);
|
|
2377
|
+
let author;
|
|
2378
|
+
if (raw.sender_business_bot) {
|
|
2379
|
+
author = this.toAuthor(raw.sender_business_bot);
|
|
2380
|
+
} else if (raw.from) {
|
|
2381
|
+
author = this.toAuthor(raw.from);
|
|
2382
|
+
} else if (raw.sender_chat) {
|
|
2383
|
+
author = this.toReactionActorAuthor(raw.sender_chat);
|
|
2384
|
+
} else {
|
|
2385
|
+
const fallbackName = this.chatDisplayName(raw.chat) ?? String(raw.chat.id);
|
|
2386
|
+
author = {
|
|
2387
|
+
userId: String(raw.chat.id),
|
|
2388
|
+
userName: fallbackName,
|
|
2389
|
+
fullName: fallbackName,
|
|
2390
|
+
isBot: "unknown",
|
|
2391
|
+
isMe: false
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
const message = new Message({
|
|
2395
|
+
id: this.encodeMessageId(String(raw.chat.id), raw.message_id),
|
|
2396
|
+
threadId,
|
|
2397
|
+
text: text2,
|
|
2398
|
+
formatted: content?.formatted ?? this.formatConverter.toAst(richMarkdown || text2),
|
|
2399
|
+
raw,
|
|
2400
|
+
author,
|
|
2401
|
+
metadata: {
|
|
2402
|
+
dateSent: new Date(raw.date * 1000),
|
|
2403
|
+
edited: raw.edit_date !== undefined,
|
|
2404
|
+
editedAt: raw.edit_date !== undefined ? new Date(raw.edit_date * 1000) : undefined
|
|
2405
|
+
},
|
|
2406
|
+
attachments: this.extractAttachments(raw),
|
|
2407
|
+
replyTo: raw.reply_to_message ? this.parseTelegramMessage(raw.reply_to_message, threadId) : undefined,
|
|
2408
|
+
isMention: this.isBotMentioned(raw, plainText)
|
|
2409
|
+
});
|
|
2410
|
+
return message;
|
|
2411
|
+
}
|
|
2412
|
+
extractAttachments(raw) {
|
|
2413
|
+
const attachments = [];
|
|
2414
|
+
const photo = raw.photo?.at(-1);
|
|
2415
|
+
if (photo) {
|
|
2416
|
+
attachments.push(this.createAttachment("image", photo.file_id, {
|
|
2417
|
+
size: photo.file_size,
|
|
2418
|
+
width: photo.width,
|
|
2419
|
+
height: photo.height,
|
|
2420
|
+
fileUniqueId: photo.file_unique_id,
|
|
2421
|
+
mimeType: "image/jpeg"
|
|
2422
|
+
}));
|
|
2423
|
+
}
|
|
2424
|
+
if (raw.video) {
|
|
2425
|
+
attachments.push(this.createAttachment("video", raw.video.file_id, {
|
|
2426
|
+
size: raw.video.file_size,
|
|
2427
|
+
width: raw.video.width,
|
|
2428
|
+
height: raw.video.height,
|
|
2429
|
+
name: raw.video.file_name,
|
|
2430
|
+
mimeType: raw.video.mime_type,
|
|
2431
|
+
fileUniqueId: raw.video.file_unique_id
|
|
2432
|
+
}));
|
|
2433
|
+
}
|
|
2434
|
+
if (raw.audio) {
|
|
2435
|
+
attachments.push(this.createAttachment("audio", raw.audio.file_id, {
|
|
2436
|
+
size: raw.audio.file_size,
|
|
2437
|
+
name: raw.audio.file_name,
|
|
2438
|
+
mimeType: raw.audio.mime_type,
|
|
2439
|
+
fileUniqueId: raw.audio.file_unique_id
|
|
2440
|
+
}));
|
|
2441
|
+
}
|
|
2442
|
+
if (raw.voice) {
|
|
2443
|
+
attachments.push(this.createAttachment("audio", raw.voice.file_id, {
|
|
2444
|
+
size: raw.voice.file_size,
|
|
2445
|
+
mimeType: raw.voice.mime_type,
|
|
2446
|
+
fileUniqueId: raw.voice.file_unique_id
|
|
2447
|
+
}));
|
|
2448
|
+
}
|
|
2449
|
+
if (raw.document && !raw.animation) {
|
|
2450
|
+
attachments.push(this.createAttachment("file", raw.document.file_id, {
|
|
2451
|
+
size: raw.document.file_size,
|
|
2452
|
+
name: raw.document.file_name,
|
|
2453
|
+
mimeType: raw.document.mime_type,
|
|
2454
|
+
fileUniqueId: raw.document.file_unique_id
|
|
2455
|
+
}));
|
|
2456
|
+
}
|
|
2457
|
+
if (raw.video_note) {
|
|
2458
|
+
attachments.push(this.createAttachment("video", raw.video_note.file_id, {
|
|
2459
|
+
size: raw.video_note.file_size,
|
|
2460
|
+
width: raw.video_note.length,
|
|
2461
|
+
height: raw.video_note.length,
|
|
2462
|
+
fileUniqueId: raw.video_note.file_unique_id
|
|
2463
|
+
}));
|
|
2464
|
+
}
|
|
2465
|
+
if (raw.animation) {
|
|
2466
|
+
attachments.push(this.createAttachment("video", raw.animation.file_id, {
|
|
2467
|
+
size: raw.animation.file_size,
|
|
2468
|
+
width: raw.animation.width,
|
|
2469
|
+
height: raw.animation.height,
|
|
2470
|
+
name: raw.animation.file_name,
|
|
2471
|
+
mimeType: raw.animation.mime_type,
|
|
2472
|
+
fileUniqueId: raw.animation.file_unique_id
|
|
2473
|
+
}));
|
|
2474
|
+
}
|
|
2475
|
+
if (raw.sticker) {
|
|
2476
|
+
const format = stickerAttachmentFormat(raw.sticker);
|
|
2477
|
+
attachments.push(this.createAttachment(format.type, raw.sticker.file_id, {
|
|
2478
|
+
size: raw.sticker.file_size,
|
|
2479
|
+
width: raw.sticker.width,
|
|
2480
|
+
height: raw.sticker.height,
|
|
2481
|
+
mimeType: format.mimeType,
|
|
2482
|
+
fileUniqueId: raw.sticker.file_unique_id
|
|
2483
|
+
}));
|
|
2484
|
+
}
|
|
2485
|
+
if (raw.rich_message) {
|
|
2486
|
+
for (const media2 of richMessageMedia(raw.rich_message)) {
|
|
2487
|
+
attachments.push(this.createAttachment(media2.type, media2.file.file_id, {
|
|
2488
|
+
size: media2.file.file_size,
|
|
2489
|
+
width: media2.width,
|
|
2490
|
+
height: media2.height,
|
|
2491
|
+
name: media2.name,
|
|
2492
|
+
mimeType: media2.mimeType,
|
|
2493
|
+
fileUniqueId: media2.file.file_unique_id
|
|
2494
|
+
}));
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return attachments;
|
|
2498
|
+
}
|
|
2499
|
+
createAttachment(type, fileId, metadata) {
|
|
2500
|
+
return {
|
|
2501
|
+
type,
|
|
2502
|
+
size: metadata?.size,
|
|
2503
|
+
width: metadata?.width,
|
|
2504
|
+
height: metadata?.height,
|
|
2505
|
+
name: metadata?.name,
|
|
2506
|
+
mimeType: metadata?.mimeType,
|
|
2507
|
+
fetchMetadata: {
|
|
2508
|
+
fileId,
|
|
2509
|
+
...metadata?.fileUniqueId ? { fileUniqueId: metadata.fileUniqueId } : {}
|
|
2510
|
+
},
|
|
2511
|
+
fetchData: async () => this.downloadFile(fileId)
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
rehydrateAttachment(attachment) {
|
|
2515
|
+
const fileId = attachment.fetchMetadata?.fileId;
|
|
2516
|
+
if (!fileId) {
|
|
2517
|
+
return attachment;
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
...attachment,
|
|
2521
|
+
fetchData: async () => this.downloadFile(fileId)
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
async downloadFile(fileId) {
|
|
2525
|
+
const file = await this.telegramFetch("getFile", {
|
|
2526
|
+
file_id: fileId
|
|
2527
|
+
});
|
|
2528
|
+
if (!file.file_path) {
|
|
2529
|
+
throw new ResourceNotFoundError("telegram", "file", fileId);
|
|
2530
|
+
}
|
|
2531
|
+
const botToken = this.staticBotToken ?? await this.resolveBotToken();
|
|
2532
|
+
const fileUrl = `${this.apiBaseUrl}/file/bot${botToken}/${file.file_path}`;
|
|
2533
|
+
let response;
|
|
2534
|
+
try {
|
|
2535
|
+
response = await fetch(fileUrl, {
|
|
2536
|
+
signal: AbortSignal.timeout(TELEGRAM_FILE_TIMEOUT_MS)
|
|
2537
|
+
});
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
throw new NetworkError("telegram", `Failed to download Telegram file ${fileId}`, error instanceof Error ? error : undefined);
|
|
2540
|
+
}
|
|
2541
|
+
if (!response.ok) {
|
|
2542
|
+
throw new NetworkError("telegram", `Failed to download Telegram file ${fileId}: ${response.status}`);
|
|
2543
|
+
}
|
|
2544
|
+
try {
|
|
2545
|
+
return await readTelegramFile(response, fileId);
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
if (error instanceof NetworkError) {
|
|
2548
|
+
throw error;
|
|
2549
|
+
}
|
|
2550
|
+
throw new NetworkError("telegram", `Failed to download Telegram file ${fileId}`, error instanceof Error ? error : undefined);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
async sendDocument(thread, file, text2, plainText, replyMarkup, parseMode = "plain", replyParameters) {
|
|
2554
|
+
const buffer = await this.toTelegramBuffer(file.data);
|
|
2555
|
+
return this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => this.telegramFetch("sendDocument", this.createTelegramDocumentFormData(thread, file, buffer, resolvedText, replyMarkup, resolvedParseMode, replyParameters)), {
|
|
2556
|
+
initialText: text2,
|
|
2557
|
+
fallbackText: plainText,
|
|
2558
|
+
method: "sendDocument",
|
|
2559
|
+
threadId: this.encodeThreadId(thread)
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
createTelegramDocumentFormData(thread, file, buffer, text2, replyMarkup, parseMode = "plain", replyParameters) {
|
|
2563
|
+
const formData = new FormData;
|
|
2564
|
+
this.appendChatTargetToFormData(formData, thread);
|
|
2565
|
+
this.appendReplyParameters(formData, replyParameters);
|
|
2566
|
+
if (text2.trim()) {
|
|
2567
|
+
formData.append("caption", truncateForTelegram(text2, TELEGRAM_CAPTION_LIMIT, parseMode));
|
|
2568
|
+
const botApiParseMode = toBotApiParseMode(parseMode);
|
|
2569
|
+
if (botApiParseMode) {
|
|
2570
|
+
formData.append("parse_mode", botApiParseMode);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
const blob = new Blob([new Uint8Array(buffer)], {
|
|
2574
|
+
type: file.mimeType ?? "application/octet-stream"
|
|
2575
|
+
});
|
|
2576
|
+
formData.append("document", blob, file.filename);
|
|
2577
|
+
if (replyMarkup) {
|
|
2578
|
+
formData.append("reply_markup", JSON.stringify(replyMarkup));
|
|
2579
|
+
}
|
|
2580
|
+
return formData;
|
|
2581
|
+
}
|
|
2582
|
+
async sendAttachment(thread, attachment, text2, plainText, replyMarkup, parseMode = "plain", replyParameters) {
|
|
2583
|
+
const upload = ATTACHMENT_UPLOADS[attachment.type];
|
|
2584
|
+
const data = attachment.data ?? (attachment.fetchData ? await attachment.fetchData() : undefined);
|
|
2585
|
+
if (!(data || attachment.url)) {
|
|
2586
|
+
throw new ValidationError("telegram", `Attachment data or URL required for ${attachment.type}`);
|
|
2587
|
+
}
|
|
2588
|
+
const buffer = data ? await this.toTelegramBuffer(data) : undefined;
|
|
2589
|
+
return this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => {
|
|
2590
|
+
if (!buffer) {
|
|
2591
|
+
const payload = {
|
|
2592
|
+
...this.buildChatTargetParams(thread),
|
|
2593
|
+
[upload.field]: attachment.url,
|
|
2594
|
+
reply_parameters: replyParameters
|
|
2595
|
+
};
|
|
2596
|
+
if (resolvedText.trim()) {
|
|
2597
|
+
payload.caption = truncateForTelegram(resolvedText, TELEGRAM_CAPTION_LIMIT, resolvedParseMode);
|
|
2598
|
+
const botApiParseMode = toBotApiParseMode(resolvedParseMode);
|
|
2599
|
+
if (botApiParseMode) {
|
|
2600
|
+
payload.parse_mode = botApiParseMode;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (attachment.type === "video") {
|
|
2604
|
+
if (Number.isInteger(attachment.width)) {
|
|
2605
|
+
payload.width = attachment.width;
|
|
2606
|
+
}
|
|
2607
|
+
if (Number.isInteger(attachment.height)) {
|
|
2608
|
+
payload.height = attachment.height;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (replyMarkup) {
|
|
2612
|
+
payload.reply_markup = replyMarkup;
|
|
2613
|
+
}
|
|
2614
|
+
return this.telegramFetch(upload.method, payload);
|
|
2615
|
+
}
|
|
2616
|
+
const formData = new FormData;
|
|
2617
|
+
this.appendChatTargetToFormData(formData, thread);
|
|
2618
|
+
if (resolvedText.trim()) {
|
|
2619
|
+
formData.append("caption", truncateForTelegram(resolvedText, TELEGRAM_CAPTION_LIMIT, resolvedParseMode));
|
|
2620
|
+
const botApiParseMode = toBotApiParseMode(resolvedParseMode);
|
|
2621
|
+
if (botApiParseMode) {
|
|
2622
|
+
formData.append("parse_mode", botApiParseMode);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (attachment.type === "video") {
|
|
2626
|
+
if (Number.isInteger(attachment.width)) {
|
|
2627
|
+
formData.append("width", String(attachment.width));
|
|
2628
|
+
}
|
|
2629
|
+
if (Number.isInteger(attachment.height)) {
|
|
2630
|
+
formData.append("height", String(attachment.height));
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
const blob = new Blob([new Uint8Array(buffer)], {
|
|
2634
|
+
type: attachment.mimeType ?? "application/octet-stream"
|
|
2635
|
+
});
|
|
2636
|
+
formData.append(upload.field, blob, attachment.name ?? "attachment");
|
|
2637
|
+
if (replyMarkup) {
|
|
2638
|
+
formData.append("reply_markup", JSON.stringify(replyMarkup));
|
|
2639
|
+
}
|
|
2640
|
+
this.appendReplyParameters(formData, replyParameters);
|
|
2641
|
+
return this.telegramFetch(upload.method, formData);
|
|
2642
|
+
}, {
|
|
2643
|
+
initialText: text2,
|
|
2644
|
+
fallbackText: plainText,
|
|
2645
|
+
method: upload.method,
|
|
2646
|
+
threadId: this.encodeThreadId(thread)
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
async sendDocumentMediaGroup(thread, files, text2, plainText, replyMarkup, parseMode = "plain", replyParameters) {
|
|
2650
|
+
this.validateMediaGroupLength(files.length);
|
|
2651
|
+
if (replyMarkup) {
|
|
2652
|
+
throw new ValidationError("telegram", "Telegram media groups do not support inline keyboards");
|
|
2653
|
+
}
|
|
2654
|
+
const parts = [];
|
|
2655
|
+
for (const [index, file] of files.entries()) {
|
|
2656
|
+
parts.push({
|
|
2657
|
+
buffer: await this.toTelegramBuffer(file.data),
|
|
2658
|
+
filename: file.filename,
|
|
2659
|
+
media: `attach://media${index}`,
|
|
2660
|
+
mimeType: file.mimeType,
|
|
2661
|
+
type: "document"
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
return this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => this.telegramFetch("sendMediaGroup", this.createTelegramMediaGroupFormData(thread, parts, resolvedText, resolvedParseMode, replyParameters)), {
|
|
2665
|
+
initialText: text2,
|
|
2666
|
+
fallbackText: plainText,
|
|
2667
|
+
method: "sendMediaGroup",
|
|
2668
|
+
threadId: this.encodeThreadId(thread)
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
async sendAttachmentMediaGroup(thread, attachments, text2, plainText, replyMarkup, parseMode = "plain", replyParameters) {
|
|
2672
|
+
this.validateMediaGroupLength(attachments.length);
|
|
2673
|
+
this.validateAttachmentMediaGroupTypes(attachments);
|
|
2674
|
+
if (replyMarkup) {
|
|
2675
|
+
throw new ValidationError("telegram", "Telegram media groups do not support inline keyboards");
|
|
2676
|
+
}
|
|
2677
|
+
const parts = [];
|
|
2678
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
2679
|
+
const data = attachment.data ?? (attachment.fetchData ? await attachment.fetchData() : undefined);
|
|
2680
|
+
if (!(data || attachment.url)) {
|
|
2681
|
+
throw new ValidationError("telegram", `Attachment data or URL required for ${attachment.type}`);
|
|
2682
|
+
}
|
|
2683
|
+
const buffer = data ? await this.toTelegramBuffer(data) : undefined;
|
|
2684
|
+
const media2 = buffer ? `attach://media${index}` : attachment.url;
|
|
2685
|
+
if (!media2) {
|
|
2686
|
+
throw new ValidationError("telegram", `Attachment data or URL required for ${attachment.type}`);
|
|
2687
|
+
}
|
|
2688
|
+
parts.push({
|
|
2689
|
+
buffer,
|
|
2690
|
+
filename: attachment.name ?? `attachment-${index}`,
|
|
2691
|
+
height: attachment.height,
|
|
2692
|
+
media: media2,
|
|
2693
|
+
mimeType: attachment.mimeType,
|
|
2694
|
+
type: ATTACHMENT_MEDIA_GROUP_TYPES[attachment.type],
|
|
2695
|
+
width: attachment.width
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
return this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => this.telegramFetch("sendMediaGroup", this.createTelegramMediaGroupFormData(thread, parts, resolvedText, resolvedParseMode, replyParameters)), {
|
|
2699
|
+
initialText: text2,
|
|
2700
|
+
fallbackText: plainText,
|
|
2701
|
+
method: "sendMediaGroup",
|
|
2702
|
+
threadId: this.encodeThreadId(thread)
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
createTelegramMediaGroupFormData(thread, parts, text2, parseMode, replyParameters) {
|
|
2706
|
+
const formData = new FormData;
|
|
2707
|
+
this.appendChatTargetToFormData(formData, thread);
|
|
2708
|
+
this.appendReplyParameters(formData, replyParameters);
|
|
2709
|
+
const captionIndex = 0;
|
|
2710
|
+
const botApiParseMode = toBotApiParseMode(parseMode);
|
|
2711
|
+
const media2 = parts.map((part, index) => {
|
|
2712
|
+
const item2 = {
|
|
2713
|
+
media: part.media,
|
|
2714
|
+
type: part.type
|
|
2715
|
+
};
|
|
2716
|
+
if (index === captionIndex && text2.trim()) {
|
|
2717
|
+
item2.caption = truncateForTelegram(text2, TELEGRAM_CAPTION_LIMIT, parseMode);
|
|
2718
|
+
if (botApiParseMode) {
|
|
2719
|
+
item2.parse_mode = botApiParseMode;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (part.type === "video") {
|
|
2723
|
+
if (Number.isInteger(part.width)) {
|
|
2724
|
+
item2.width = part.width;
|
|
2725
|
+
}
|
|
2726
|
+
if (Number.isInteger(part.height)) {
|
|
2727
|
+
item2.height = part.height;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return item2;
|
|
2731
|
+
});
|
|
2732
|
+
formData.append("media", JSON.stringify(media2));
|
|
2733
|
+
for (const [index, part] of parts.entries()) {
|
|
2734
|
+
if (!part.buffer) {
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
const blob = new Blob([new Uint8Array(part.buffer)], {
|
|
2738
|
+
type: part.mimeType ?? "application/octet-stream"
|
|
2739
|
+
});
|
|
2740
|
+
formData.append(`media${index}`, blob, part.filename ?? `media-${index}`);
|
|
2741
|
+
}
|
|
2742
|
+
return formData;
|
|
2743
|
+
}
|
|
2744
|
+
validateMediaGroupLength(count) {
|
|
2745
|
+
if (count < TELEGRAM_MEDIA_GROUP_MIN || count > TELEGRAM_MEDIA_GROUP_MAX) {
|
|
2746
|
+
throw new ValidationError("telegram", "Telegram media groups support 2-10 files");
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
validateAttachmentMediaGroupTypes(attachments) {
|
|
2750
|
+
const categories = new Set(attachments.map((attachment) => {
|
|
2751
|
+
if (attachment.type === "file") {
|
|
2752
|
+
return "document";
|
|
2753
|
+
}
|
|
2754
|
+
if (attachment.type === "audio") {
|
|
2755
|
+
return "audio";
|
|
2756
|
+
}
|
|
2757
|
+
return "visual";
|
|
2758
|
+
}));
|
|
2759
|
+
if (categories.size > 1) {
|
|
2760
|
+
throw new ValidationError("telegram", "Telegram media groups can mix photos and videos, but documents and audio files must be grouped only with the same type");
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
async toTelegramBuffer(data) {
|
|
2764
|
+
if (Buffer.isBuffer(data)) {
|
|
2765
|
+
return data;
|
|
2766
|
+
}
|
|
2767
|
+
if (data instanceof ArrayBuffer) {
|
|
2768
|
+
return Buffer.from(data);
|
|
2769
|
+
}
|
|
2770
|
+
if (data instanceof Blob) {
|
|
2771
|
+
return Buffer.from(await data.arrayBuffer());
|
|
2772
|
+
}
|
|
2773
|
+
throw new ValidationError("telegram", "Unsupported file data type");
|
|
2774
|
+
}
|
|
2775
|
+
paginateMessages(messages, options) {
|
|
2776
|
+
const limit = Math.max(1, Math.min(options.limit ?? 50, 100));
|
|
2777
|
+
const direction = options.direction ?? "backward";
|
|
2778
|
+
if (messages.length === 0) {
|
|
2779
|
+
return { messages: [] };
|
|
2780
|
+
}
|
|
2781
|
+
const messageIndexById = new Map(messages.map((message, index) => [message.id, index]));
|
|
2782
|
+
if (direction === "backward") {
|
|
2783
|
+
const end2 = options.cursor && messageIndexById.has(options.cursor) ? messageIndexById.get(options.cursor) ?? messages.length : messages.length;
|
|
2784
|
+
const start2 = Math.max(0, end2 - limit);
|
|
2785
|
+
const page2 = messages.slice(start2, end2);
|
|
2786
|
+
return {
|
|
2787
|
+
messages: page2,
|
|
2788
|
+
nextCursor: start2 > 0 ? page2[0]?.id : undefined
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
const start = options.cursor && messageIndexById.has(options.cursor) ? (messageIndexById.get(options.cursor) ?? -1) + 1 : 0;
|
|
2792
|
+
const end = Math.min(messages.length, start + limit);
|
|
2793
|
+
const page = messages.slice(start, end);
|
|
2794
|
+
return {
|
|
2795
|
+
messages: page,
|
|
2796
|
+
nextCursor: end < messages.length ? page.at(-1)?.id : undefined
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
cacheMessage(message) {
|
|
2800
|
+
const existing = this.messageCache.get(message.threadId) ?? [];
|
|
2801
|
+
const index = existing.findIndex((item2) => item2.id === message.id);
|
|
2802
|
+
if (index >= 0) {
|
|
2803
|
+
existing[index] = message;
|
|
2804
|
+
} else {
|
|
2805
|
+
existing.push(message);
|
|
2806
|
+
}
|
|
2807
|
+
existing.sort((a, b) => this.compareMessages(a, b));
|
|
2808
|
+
this.messageCache.set(message.threadId, existing);
|
|
2809
|
+
}
|
|
2810
|
+
findCachedMessage(messageId) {
|
|
2811
|
+
for (const messages of this.messageCache.values()) {
|
|
2812
|
+
const found = messages.find((message) => message.id === messageId);
|
|
2813
|
+
if (found) {
|
|
2814
|
+
return found;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
deleteCachedMessage(messageId) {
|
|
2820
|
+
for (const [threadId, messages] of this.messageCache.entries()) {
|
|
2821
|
+
const filtered = messages.filter((message) => message.id !== messageId);
|
|
2822
|
+
if (filtered.length === 0) {
|
|
2823
|
+
this.messageCache.delete(threadId);
|
|
2824
|
+
} else if (filtered.length !== messages.length) {
|
|
2825
|
+
this.messageCache.set(threadId, filtered);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
compareMessages(a, b) {
|
|
2830
|
+
const timeDiff = a.metadata.dateSent.getTime() - b.metadata.dateSent.getTime();
|
|
2831
|
+
if (timeDiff !== 0) {
|
|
2832
|
+
return timeDiff;
|
|
2833
|
+
}
|
|
2834
|
+
return this.messageSequence(a.id) - this.messageSequence(b.id);
|
|
2835
|
+
}
|
|
2836
|
+
messageSequence(messageId) {
|
|
2837
|
+
const match = messageId.match(MESSAGE_SEQUENCE_PATTERN);
|
|
2838
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
2839
|
+
}
|
|
2840
|
+
createDraftId() {
|
|
2841
|
+
this.nextDraftId = this.nextDraftId >= 2147483647 ? 1 : this.nextDraftId + 1;
|
|
2842
|
+
return this.nextDraftId;
|
|
2843
|
+
}
|
|
2844
|
+
resolveThreadId(value) {
|
|
2845
|
+
if (value.startsWith("telegram:")) {
|
|
2846
|
+
return this.decodeThreadId(value);
|
|
2847
|
+
}
|
|
2848
|
+
return { chatId: value };
|
|
2849
|
+
}
|
|
2850
|
+
buildChatTargetParams(thread) {
|
|
2851
|
+
const params = {
|
|
2852
|
+
chat_id: thread.chatId
|
|
2853
|
+
};
|
|
2854
|
+
if (typeof thread.messageThreadId === "number") {
|
|
2855
|
+
params.message_thread_id = thread.messageThreadId;
|
|
2856
|
+
}
|
|
2857
|
+
if (thread.businessConnectionId) {
|
|
2858
|
+
params.business_connection_id = thread.businessConnectionId;
|
|
2859
|
+
}
|
|
2860
|
+
return params;
|
|
2861
|
+
}
|
|
2862
|
+
appendChatTargetToFormData(formData, thread) {
|
|
2863
|
+
formData.append("chat_id", thread.chatId);
|
|
2864
|
+
if (typeof thread.messageThreadId === "number") {
|
|
2865
|
+
formData.append("message_thread_id", String(thread.messageThreadId));
|
|
2866
|
+
}
|
|
2867
|
+
if (thread.businessConnectionId) {
|
|
2868
|
+
formData.append("business_connection_id", thread.businessConnectionId);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
encodeMessageId(chatId, messageId) {
|
|
2872
|
+
return `${chatId}:${messageId}`;
|
|
2873
|
+
}
|
|
2874
|
+
buildReplyParameters(replyToMessageId, expectedChatId) {
|
|
2875
|
+
if (!replyToMessageId) {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const { messageId } = this.decodeCompositeMessageId(replyToMessageId, expectedChatId);
|
|
2879
|
+
return { message_id: messageId, allow_sending_without_reply: true };
|
|
2880
|
+
}
|
|
2881
|
+
appendReplyParameters(formData, replyParameters) {
|
|
2882
|
+
if (replyParameters) {
|
|
2883
|
+
formData.append("reply_parameters", JSON.stringify(replyParameters));
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
decodeCompositeMessageId(messageId, expectedChatId) {
|
|
2887
|
+
const compositeMatch = messageId.match(MESSAGE_ID_PATTERN);
|
|
2888
|
+
if (compositeMatch) {
|
|
2889
|
+
const [, chatId, rawMessageId] = compositeMatch;
|
|
2890
|
+
const parsedMessageId2 = Number.parseInt(rawMessageId, 10);
|
|
2891
|
+
if (expectedChatId && chatId !== expectedChatId) {
|
|
2892
|
+
throw new ValidationError("telegram", `Message ID chat mismatch: expected ${expectedChatId}, got ${chatId}`);
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
chatId,
|
|
2896
|
+
messageId: parsedMessageId2,
|
|
2897
|
+
compositeId: `${chatId}:${parsedMessageId2}`
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
if (!expectedChatId) {
|
|
2901
|
+
throw new ValidationError("telegram", `Telegram message ID must be in <chatId>:<messageId> format, got: ${messageId}`);
|
|
2902
|
+
}
|
|
2903
|
+
if (!BARE_MESSAGE_ID_PATTERN.test(messageId)) {
|
|
2904
|
+
throw new ValidationError("telegram", `Invalid Telegram message ID: ${messageId}`);
|
|
2905
|
+
}
|
|
2906
|
+
const parsedMessageId = Number.parseInt(messageId, 10);
|
|
2907
|
+
return {
|
|
2908
|
+
chatId: expectedChatId,
|
|
2909
|
+
messageId: parsedMessageId,
|
|
2910
|
+
compositeId: `${expectedChatId}:${parsedMessageId}`
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
toAuthor(user) {
|
|
2914
|
+
const fullName = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
|
2915
|
+
return {
|
|
2916
|
+
userId: String(user.id),
|
|
2917
|
+
userName: user.username ?? user.first_name ?? String(user.id),
|
|
2918
|
+
fullName: fullName || user.username || String(user.id),
|
|
2919
|
+
isBot: user.is_bot,
|
|
2920
|
+
isMe: String(user.id) === this._botUserId
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
toReactionActorAuthor(chat) {
|
|
2924
|
+
const name = this.chatDisplayName(chat) ?? String(chat.id);
|
|
2925
|
+
return {
|
|
2926
|
+
userId: `chat:${chat.id}`,
|
|
2927
|
+
userName: name,
|
|
2928
|
+
fullName: name,
|
|
2929
|
+
isBot: "unknown",
|
|
2930
|
+
isMe: false
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
chatDisplayName(chat) {
|
|
2934
|
+
if (chat.title) {
|
|
2935
|
+
return chat.title;
|
|
2936
|
+
}
|
|
2937
|
+
const privateName = [chat.first_name, chat.last_name].filter(Boolean).join(" ").trim();
|
|
2938
|
+
if (privateName) {
|
|
2939
|
+
return privateName;
|
|
2940
|
+
}
|
|
2941
|
+
return chat.username;
|
|
2942
|
+
}
|
|
2943
|
+
isBotMentioned(message, text2) {
|
|
2944
|
+
if (this.mentionOnReply && this._botUserId && message.reply_to_message?.from && String(message.reply_to_message.from.id) === this._botUserId && message.reply_to_message.message_id !== message.message_thread_id && !(message.from && String(message.from.id) === this._botUserId)) {
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
if (!text2) {
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
const username = this._userName;
|
|
2951
|
+
const entities = message.entities ?? message.caption_entities ?? [];
|
|
2952
|
+
for (const entity of entities) {
|
|
2953
|
+
if (entity.type === "mention") {
|
|
2954
|
+
const mentionText = this.entityText(text2, entity);
|
|
2955
|
+
if (mentionText.toLowerCase() === `@${username.toLowerCase()}`) {
|
|
2956
|
+
return true;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (entity.type === "text_mention" && entity.user && this._botUserId && String(entity.user.id) === this._botUserId) {
|
|
2960
|
+
return true;
|
|
2961
|
+
}
|
|
2962
|
+
if (entity.type === "bot_command") {
|
|
2963
|
+
const commandText = this.entityText(text2, entity);
|
|
2964
|
+
if (commandText.toLowerCase().endsWith(`@${username.toLowerCase()}`)) {
|
|
2965
|
+
return true;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
const mentionRegex = this.getMentionRegex(username);
|
|
2970
|
+
return mentionRegex.test(text2);
|
|
2971
|
+
}
|
|
2972
|
+
getMentionRegex(username) {
|
|
2973
|
+
if (!this._mentionRegex || this._mentionRegexUsername !== username) {
|
|
2974
|
+
this._mentionRegexUsername = username;
|
|
2975
|
+
this._mentionRegex = new RegExp(`@${this.escapeRegex(username)}(?![\\w-])`, "i");
|
|
2976
|
+
}
|
|
2977
|
+
return this._mentionRegex;
|
|
2978
|
+
}
|
|
2979
|
+
entityText(text2, entity) {
|
|
2980
|
+
return text2.slice(entity.offset, entity.offset + entity.length);
|
|
2981
|
+
}
|
|
2982
|
+
escapeRegex(input) {
|
|
2983
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2984
|
+
}
|
|
2985
|
+
normalizeUserName(value) {
|
|
2986
|
+
if (typeof value !== "string") {
|
|
2987
|
+
return "bot";
|
|
2988
|
+
}
|
|
2989
|
+
return value.replace(LEADING_AT_PATTERN, "").trim() || "bot";
|
|
2990
|
+
}
|
|
2991
|
+
resolveParseMode(message, card) {
|
|
2992
|
+
if (card) {
|
|
2993
|
+
return "MarkdownV2";
|
|
2994
|
+
}
|
|
2995
|
+
if (typeof message === "string") {
|
|
2996
|
+
return "plain";
|
|
2997
|
+
}
|
|
2998
|
+
if (typeof message === "object" && message !== null && "raw" in message) {
|
|
2999
|
+
return "plain";
|
|
3000
|
+
}
|
|
3001
|
+
return "MarkdownV2";
|
|
3002
|
+
}
|
|
3003
|
+
resolveRichMessage(message, card, fileCount, attachmentCount) {
|
|
3004
|
+
if (!this.richMessagesAvailable || card || fileCount > 0 || attachmentCount > 0 || typeof message === "string" || "raw" in message) {
|
|
3005
|
+
return null;
|
|
3006
|
+
}
|
|
3007
|
+
if ("markdown" in message) {
|
|
3008
|
+
const formatted = this.formatConverter.toAst(message.markdown);
|
|
3009
|
+
return {
|
|
3010
|
+
formatted,
|
|
3011
|
+
markdown: truncateRichMarkdown(convertEmojiPlaceholders(message.markdown, "gchat")),
|
|
3012
|
+
text: toPlainText(formatted)
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
if ("ast" in message) {
|
|
3016
|
+
return {
|
|
3017
|
+
formatted: message.ast,
|
|
3018
|
+
markdown: truncateRichMarkdown(convertEmojiPlaceholders(stringifyMarkdown(message.ast), "gchat")),
|
|
3019
|
+
text: toPlainText(message.ast)
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
renderPlainTextMessage(message, card) {
|
|
3025
|
+
if (card) {
|
|
3026
|
+
return cardToFallbackText(card);
|
|
3027
|
+
}
|
|
3028
|
+
if (typeof message === "string") {
|
|
3029
|
+
return message;
|
|
3030
|
+
}
|
|
3031
|
+
if ("raw" in message) {
|
|
3032
|
+
return message.raw;
|
|
3033
|
+
}
|
|
3034
|
+
if ("markdown" in message) {
|
|
3035
|
+
return this.resolveTelegramFallbackText(message.markdown, markdownToPlainText(message.markdown));
|
|
3036
|
+
}
|
|
3037
|
+
if ("ast" in message) {
|
|
3038
|
+
return toPlainText(message.ast);
|
|
3039
|
+
}
|
|
3040
|
+
return this.formatConverter.renderPostable(message);
|
|
3041
|
+
}
|
|
3042
|
+
resolveTelegramFallbackText(originalText, fallbackText) {
|
|
3043
|
+
return fallbackText.trim() ? fallbackText : originalText;
|
|
3044
|
+
}
|
|
3045
|
+
async sendRegularMessage(thread, text2, plainText, parseMode, replyMarkup, threadId, replyParameters) {
|
|
3046
|
+
return this.withTelegramMarkdownFallback(parseMode, (resolvedParseMode, resolvedText) => this.telegramFetch("sendMessage", {
|
|
3047
|
+
...this.buildChatTargetParams(thread),
|
|
3048
|
+
text: resolvedText,
|
|
3049
|
+
reply_markup: replyMarkup,
|
|
3050
|
+
parse_mode: toBotApiParseMode(resolvedParseMode),
|
|
3051
|
+
reply_parameters: replyParameters
|
|
3052
|
+
}), {
|
|
3053
|
+
initialText: text2,
|
|
3054
|
+
fallbackText: plainText,
|
|
3055
|
+
method: "sendMessage",
|
|
3056
|
+
threadId
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
canFallbackFromRichMessage(error, method) {
|
|
3060
|
+
const message = error instanceof ValidationError ? error.message.toLowerCase() : "";
|
|
3061
|
+
const missingMethod = message.includes("method") && message.includes("not found");
|
|
3062
|
+
const unsupportedRich = message.includes("rich message") && message.includes("unsupported");
|
|
3063
|
+
const rejectedReplyParameters = message.includes("reply_parameters");
|
|
3064
|
+
return method.startsWith("sendRichMessage") && error instanceof ResourceNotFoundError || error instanceof ValidationError && (message.includes("can't parse") || missingMethod || unsupportedRich || rejectedReplyParameters);
|
|
3065
|
+
}
|
|
3066
|
+
rememberRichMessageFailure(error, method) {
|
|
3067
|
+
const message = error instanceof ValidationError ? error.message.toLowerCase() : "";
|
|
3068
|
+
const missingMethod = message.includes("method") && message.includes("not found");
|
|
3069
|
+
const unsupportedRich = message.includes("rich message") && message.includes("unsupported");
|
|
3070
|
+
if (method.startsWith("sendRichMessage") && error instanceof ResourceNotFoundError || error instanceof ValidationError && (missingMethod || unsupportedRich)) {
|
|
3071
|
+
this.richMessagesAvailable = false;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
toTelegramReaction(emoji) {
|
|
3075
|
+
if (typeof emoji !== "string") {
|
|
3076
|
+
return {
|
|
3077
|
+
type: "emoji",
|
|
3078
|
+
emoji: defaultEmojiResolver.toGChat(emoji.name)
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
if (emoji.startsWith("custom:")) {
|
|
3082
|
+
return {
|
|
3083
|
+
type: "custom_emoji",
|
|
3084
|
+
custom_emoji_id: emoji.slice("custom:".length)
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
const placeholderMatch = emoji.match(EMOJI_PLACEHOLDER_PATTERN);
|
|
3088
|
+
if (placeholderMatch) {
|
|
3089
|
+
return {
|
|
3090
|
+
type: "emoji",
|
|
3091
|
+
emoji: defaultEmojiResolver.toGChat(placeholderMatch[1])
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
if (EMOJI_NAME_PATTERN.test(emoji)) {
|
|
3095
|
+
return {
|
|
3096
|
+
type: "emoji",
|
|
3097
|
+
emoji: defaultEmojiResolver.toGChat(emoji.toLowerCase())
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
return {
|
|
3101
|
+
type: "emoji",
|
|
3102
|
+
emoji
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
reactionKey(reaction) {
|
|
3106
|
+
if (reaction.type === "emoji") {
|
|
3107
|
+
return reaction.emoji;
|
|
3108
|
+
}
|
|
3109
|
+
return `custom:${reaction.custom_emoji_id}`;
|
|
3110
|
+
}
|
|
3111
|
+
reactionToEmojiValue(reaction) {
|
|
3112
|
+
if (reaction.type === "emoji") {
|
|
3113
|
+
return defaultEmojiResolver.fromGChat(reaction.emoji);
|
|
3114
|
+
}
|
|
3115
|
+
return getEmoji(`custom:${reaction.custom_emoji_id}`);
|
|
3116
|
+
}
|
|
3117
|
+
pollingGroup(update) {
|
|
3118
|
+
const message = update.message ?? update.edited_message ?? update.channel_post ?? update.edited_channel_post ?? update.business_message ?? update.edited_business_message;
|
|
3119
|
+
if (this.allowedUserIds && (message?.from === undefined || !this.allowedUserIds.has(String(message.from.id)))) {
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
return message?.media_group_id ? JSON.stringify([
|
|
3123
|
+
message.chat.id,
|
|
3124
|
+
message.message_thread_id,
|
|
3125
|
+
message.business_connection_id,
|
|
3126
|
+
message.media_group_id
|
|
3127
|
+
]) : undefined;
|
|
3128
|
+
}
|
|
3129
|
+
async processPollingUpdates(updates, completed) {
|
|
3130
|
+
const groups = /* @__PURE__ */ new Map;
|
|
3131
|
+
for (const update of updates) {
|
|
3132
|
+
const key = this.pollingGroup(update) ?? String(update.update_id);
|
|
3133
|
+
const group = groups.get(key) ?? [];
|
|
3134
|
+
group.push(update);
|
|
3135
|
+
groups.set(key, group);
|
|
3136
|
+
}
|
|
3137
|
+
const pending = [...groups.values()].map(async (group) => {
|
|
3138
|
+
try {
|
|
3139
|
+
const results = await Promise.allSettled(group.map(async (update) => {
|
|
3140
|
+
if (!completed.has(update.update_id)) {
|
|
3141
|
+
const tasks = await Promise.allSettled(this.processUpdate(update, { deduplicate: false }) ?? []);
|
|
3142
|
+
for (const task of tasks) {
|
|
3143
|
+
if (task.status === "rejected") {
|
|
3144
|
+
throw task.reason;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
}));
|
|
3149
|
+
for (const result of results) {
|
|
3150
|
+
if (result.status === "rejected") {
|
|
3151
|
+
throw result.reason;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return group.map((update) => {
|
|
3155
|
+
completed.add(update.update_id);
|
|
3156
|
+
return { update };
|
|
3157
|
+
});
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
return group.map((update) => ({ error, update }));
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
return (await Promise.all(pending)).flat().sort((left, right) => left.update.update_id - right.update.update_id);
|
|
3163
|
+
}
|
|
3164
|
+
async pollingLoop(config) {
|
|
3165
|
+
const state = this.chat?.getState();
|
|
3166
|
+
if (!state) {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
let checkpoint;
|
|
3170
|
+
let consecutiveFailures = 0;
|
|
3171
|
+
let drained = false;
|
|
3172
|
+
const MAX_BACKOFF_MS = 30000;
|
|
3173
|
+
const completed = /* @__PURE__ */ new Set;
|
|
3174
|
+
const retry = (entries, error) => {
|
|
3175
|
+
const attempts = Math.max(...entries.map((entry) => entry.attempts ?? 0)) + 1;
|
|
3176
|
+
const delay = Math.max(Math.min(Math.max(config.retryDelayMs, TELEGRAM_DEFAULT_POLLING_RETRY_DELAY_MS) * 2 ** Math.min(attempts - 1, 30), MAX_BACKOFF_MS), error instanceof AdapterRateLimitError ? (error.retryAfter ?? 0) * 1000 : 0);
|
|
3177
|
+
const retryAt = Date.now() + delay;
|
|
3178
|
+
this.logger.warn("Telegram polling update processing failed", {
|
|
3179
|
+
error: String(error),
|
|
3180
|
+
updateId: entries[0].update.update_id,
|
|
3181
|
+
attempts,
|
|
3182
|
+
retryAt
|
|
3183
|
+
});
|
|
3184
|
+
return { attempts, retryAt };
|
|
3185
|
+
};
|
|
3186
|
+
while (this.pollingActive) {
|
|
3187
|
+
this.pollingAbortController = new AbortController;
|
|
3188
|
+
let timer;
|
|
3189
|
+
try {
|
|
3190
|
+
await this.ensureBotIdentity();
|
|
3191
|
+
const key = `${this.name}:polling:${this.webhookScope}`;
|
|
3192
|
+
checkpoint ??= await state.get(key) ?? {
|
|
3193
|
+
pending: []
|
|
3194
|
+
};
|
|
3195
|
+
const groups = /* @__PURE__ */ new Map;
|
|
3196
|
+
for (const entry of checkpoint.pending) {
|
|
3197
|
+
const group = this.pollingGroup(entry.update) ?? String(entry.update.update_id);
|
|
3198
|
+
const entries = groups.get(group) ?? [];
|
|
3199
|
+
entries.push(entry);
|
|
3200
|
+
groups.set(group, entries);
|
|
3201
|
+
}
|
|
3202
|
+
const eligible = [...groups.values()].filter((entries) => entries.every((entry) => entry.update.update_id < (checkpoint?.offset ?? 0)));
|
|
3203
|
+
const deadline = (entries) => Math.max(...entries.map((entry) => Math.max(entry.receivedAt + (this.pollingGroup(entry.update) ? TELEGRAM_INCOMING_MEDIA_GROUP_SETTLE_MS : 0), entry.retryAt ?? 0)));
|
|
3204
|
+
const ready = eligible.filter((entries) => deadline(entries) <= Date.now());
|
|
3205
|
+
if (ready.length > 0 && !drained) {
|
|
3206
|
+
const results2 = await this.processPollingUpdates(ready.flatMap((entries) => entries.map((entry) => entry.update)), completed);
|
|
3207
|
+
const failures = new Map(results2.flatMap((result) => ("error" in result) ? [[result.update.update_id, result.error]] : []));
|
|
3208
|
+
const retries = /* @__PURE__ */ new Map;
|
|
3209
|
+
for (const entries of ready) {
|
|
3210
|
+
const failed = entries.find((entry) => failures.has(entry.update.update_id));
|
|
3211
|
+
if (!failed) {
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
const scheduled = retry(entries, failures.get(failed.update.update_id));
|
|
3215
|
+
for (const entry of entries) {
|
|
3216
|
+
retries.set(entry.update.update_id, scheduled);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
const pending2 = checkpoint.pending.filter((entry) => !completed.has(entry.update.update_id)).map((entry) => ({
|
|
3220
|
+
...entry,
|
|
3221
|
+
...retries.get(entry.update.update_id)
|
|
3222
|
+
}));
|
|
3223
|
+
const next2 = { ...checkpoint, pending: pending2 };
|
|
3224
|
+
if (pending2.length > 0) {
|
|
3225
|
+
await state.set(key, next2);
|
|
3226
|
+
} else {
|
|
3227
|
+
await state.delete(key);
|
|
3228
|
+
}
|
|
3229
|
+
checkpoint = next2;
|
|
3230
|
+
for (const result of results2) {
|
|
3231
|
+
completed.delete(result.update.update_id);
|
|
3232
|
+
}
|
|
3233
|
+
consecutiveFailures = 0;
|
|
3234
|
+
drained = true;
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
const remaining = eligible.length > 0 ? Math.max(0, Math.min(...eligible.map(deadline)) - Date.now()) : undefined;
|
|
3238
|
+
let collecting = false;
|
|
3239
|
+
if (remaining !== undefined && remaining > 0) {
|
|
3240
|
+
timer = setTimeout(() => {
|
|
3241
|
+
collecting = true;
|
|
3242
|
+
this.pollingAbortController?.abort();
|
|
3243
|
+
}, remaining);
|
|
3244
|
+
}
|
|
3245
|
+
let updates;
|
|
3246
|
+
try {
|
|
3247
|
+
updates = await this.telegramFetch("getUpdates", {
|
|
3248
|
+
allowed_updates: config.allowedUpdates,
|
|
3249
|
+
limit: config.limit,
|
|
3250
|
+
offset: checkpoint.offset,
|
|
3251
|
+
timeout: remaining === 0 ? 0 : config.timeout
|
|
3252
|
+
}, { signal: this.pollingAbortController.signal });
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
if (collecting && this.pollingActive && this.isAbortError(error)) {
|
|
3255
|
+
updates = [];
|
|
3256
|
+
} else {
|
|
3257
|
+
throw error;
|
|
3258
|
+
}
|
|
3259
|
+
} finally {
|
|
3260
|
+
clearTimeout(timer);
|
|
3261
|
+
}
|
|
3262
|
+
drained = false;
|
|
3263
|
+
if (!this.pollingActive) {
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
const results = await this.processPollingUpdates(updates.filter((update) => !this.pollingGroup(update)), completed);
|
|
3267
|
+
const pending = new Map(checkpoint.pending.map((entry) => [entry.update.update_id, entry]));
|
|
3268
|
+
for (const result of results) {
|
|
3269
|
+
if ("error" in result) {
|
|
3270
|
+
const entry = { update: result.update, receivedAt: Date.now() };
|
|
3271
|
+
pending.set(result.update.update_id, {
|
|
3272
|
+
...entry,
|
|
3273
|
+
...retry([entry], result.error)
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
for (const update of updates) {
|
|
3278
|
+
if (this.pollingGroup(update) && !completed.has(update.update_id) && !pending.has(update.update_id)) {
|
|
3279
|
+
pending.set(update.update_id, { update, receivedAt: Date.now() });
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
let offset = checkpoint.offset;
|
|
3283
|
+
for (const update of updates) {
|
|
3284
|
+
offset = update.update_id + 1;
|
|
3285
|
+
}
|
|
3286
|
+
const next = { offset, pending: [...pending.values()] };
|
|
3287
|
+
if (next.pending.length > 0 || checkpoint.pending.length > 0) {
|
|
3288
|
+
await state.set(key, next);
|
|
3289
|
+
}
|
|
3290
|
+
checkpoint = next;
|
|
3291
|
+
for (const id of completed) {
|
|
3292
|
+
if (id < (offset ?? 0)) {
|
|
3293
|
+
completed.delete(id);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (updates.length === 0 && remaining !== undefined) {
|
|
3297
|
+
await this.sleep(Math.min(remaining, TELEGRAM_INCOMING_MEDIA_GROUP_RETRY_MS), this.pollingAbortController.signal);
|
|
3298
|
+
}
|
|
3299
|
+
consecutiveFailures = 0;
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
if (this.isAbortError(error) && !this.pollingActive) {
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
consecutiveFailures++;
|
|
3305
|
+
const backoffMs = Math.min(config.retryDelayMs * 2 ** (consecutiveFailures - 1), MAX_BACKOFF_MS);
|
|
3306
|
+
this.logger.warn("Telegram polling request failed", {
|
|
3307
|
+
error: String(error),
|
|
3308
|
+
retryDelayMs: backoffMs,
|
|
3309
|
+
consecutiveFailures
|
|
3310
|
+
});
|
|
3311
|
+
if (!this.pollingActive) {
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
await this.sleep(backoffMs, this.pollingAbortController?.signal);
|
|
3315
|
+
} finally {
|
|
3316
|
+
clearTimeout(timer);
|
|
3317
|
+
this.pollingAbortController = null;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
resolvePollingConfig(override) {
|
|
3322
|
+
const baseConfig = this.longPolling ?? {};
|
|
3323
|
+
const merged = {
|
|
3324
|
+
...baseConfig,
|
|
3325
|
+
...override
|
|
3326
|
+
};
|
|
3327
|
+
return {
|
|
3328
|
+
allowedUpdates: this.resolveAllowedUpdates(merged.allowedUpdates),
|
|
3329
|
+
deleteWebhook: merged.deleteWebhook ?? true,
|
|
3330
|
+
dropPendingUpdates: merged.dropPendingUpdates ?? false,
|
|
3331
|
+
limit: this.clampInteger(merged.limit, TELEGRAM_DEFAULT_POLLING_LIMIT, TELEGRAM_MIN_POLLING_LIMIT, TELEGRAM_MAX_POLLING_LIMIT),
|
|
3332
|
+
retryDelayMs: this.clampInteger(merged.retryDelayMs, TELEGRAM_DEFAULT_POLLING_RETRY_DELAY_MS, 0, Number.MAX_SAFE_INTEGER),
|
|
3333
|
+
timeout: this.clampInteger(merged.timeout, TELEGRAM_DEFAULT_POLLING_TIMEOUT_SECONDS, TELEGRAM_MIN_POLLING_TIMEOUT_SECONDS, TELEGRAM_MAX_POLLING_TIMEOUT_SECONDS)
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
resolveAllowedUpdates(allowedUpdates) {
|
|
3337
|
+
if (!this.businessMode) {
|
|
3338
|
+
return allowedUpdates && allowedUpdates.length > 0 ? [...allowedUpdates] : undefined;
|
|
3339
|
+
}
|
|
3340
|
+
const base = allowedUpdates && allowedUpdates.length > 0 ? allowedUpdates : TELEGRAM_DEFAULT_ALLOWED_UPDATES;
|
|
3341
|
+
return [.../* @__PURE__ */ new Set([...base, ...TELEGRAM_BUSINESS_ALLOWED_UPDATES])];
|
|
3342
|
+
}
|
|
3343
|
+
clampInteger(value, fallback, min, max) {
|
|
3344
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3345
|
+
return fallback;
|
|
3346
|
+
}
|
|
3347
|
+
const parsed = Math.trunc(value);
|
|
3348
|
+
return Math.max(min, Math.min(max, parsed));
|
|
3349
|
+
}
|
|
3350
|
+
isAbortError(error) {
|
|
3351
|
+
return error instanceof Error && error.name === "AbortError";
|
|
3352
|
+
}
|
|
3353
|
+
async sleep(delayMs, signal) {
|
|
3354
|
+
if (delayMs <= 0 || signal?.aborted) {
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
await new Promise((resolve) => {
|
|
3358
|
+
const timeoutId = setTimeout(() => {
|
|
3359
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3360
|
+
resolve();
|
|
3361
|
+
}, delayMs);
|
|
3362
|
+
const onAbort = () => {
|
|
3363
|
+
clearTimeout(timeoutId);
|
|
3364
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3365
|
+
resolve();
|
|
3366
|
+
};
|
|
3367
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
async telegramFetch(method, payload, request) {
|
|
3371
|
+
const botToken = this.staticBotToken ?? await this.resolveBotToken();
|
|
3372
|
+
const url = `${this.apiBaseUrl}/bot${botToken}/${method}`;
|
|
3373
|
+
let response;
|
|
3374
|
+
try {
|
|
3375
|
+
response = await fetch(url, {
|
|
3376
|
+
method: "POST",
|
|
3377
|
+
headers: payload instanceof FormData ? undefined : {
|
|
3378
|
+
"Content-Type": "application/json"
|
|
3379
|
+
},
|
|
3380
|
+
body: payload instanceof FormData ? payload : JSON.stringify(payload ?? {}),
|
|
3381
|
+
signal: request?.signal
|
|
3382
|
+
});
|
|
3383
|
+
} catch (error) {
|
|
3384
|
+
if (this.isAbortError(error)) {
|
|
3385
|
+
throw error;
|
|
3386
|
+
}
|
|
3387
|
+
throw new NetworkError("telegram", `Network error calling Telegram ${method}`, error instanceof Error ? error : undefined);
|
|
3388
|
+
}
|
|
3389
|
+
let data;
|
|
3390
|
+
try {
|
|
3391
|
+
data = await response.json();
|
|
3392
|
+
} catch {
|
|
3393
|
+
throw new NetworkError("telegram", `Failed to parse Telegram API response for ${method}`);
|
|
3394
|
+
}
|
|
3395
|
+
if (!(response.ok && data.ok)) {
|
|
3396
|
+
this.throwTelegramApiError(method, response.status, data);
|
|
3397
|
+
}
|
|
3398
|
+
if (typeof data.result === "undefined") {
|
|
3399
|
+
throw new NetworkError("telegram", `Telegram API ${method} returned no result`);
|
|
3400
|
+
}
|
|
3401
|
+
return data.result;
|
|
3402
|
+
}
|
|
3403
|
+
throwTelegramApiError(method, status, data) {
|
|
3404
|
+
const errorCode = data.error_code ?? status;
|
|
3405
|
+
const description = data.description ?? `Telegram API ${method} failed`;
|
|
3406
|
+
if (errorCode === 429) {
|
|
3407
|
+
throw new AdapterRateLimitError("telegram", data.parameters?.retry_after);
|
|
3408
|
+
}
|
|
3409
|
+
if (errorCode === 401) {
|
|
3410
|
+
throw new AuthenticationError("telegram", description);
|
|
3411
|
+
}
|
|
3412
|
+
if (errorCode === 403) {
|
|
3413
|
+
throw new PermissionError("telegram", method);
|
|
3414
|
+
}
|
|
3415
|
+
if (errorCode === 404) {
|
|
3416
|
+
throw new ResourceNotFoundError("telegram", method);
|
|
3417
|
+
}
|
|
3418
|
+
if (errorCode >= 400 && errorCode < 500) {
|
|
3419
|
+
throw new ValidationError("telegram", description);
|
|
3420
|
+
}
|
|
3421
|
+
throw new NetworkError("telegram", `${description} (status ${status}, error ${errorCode})`);
|
|
3422
|
+
}
|
|
3423
|
+
async withTelegramMarkdownFallback(parseMode, operation, context) {
|
|
3424
|
+
try {
|
|
3425
|
+
return await operation(parseMode, context.initialText);
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
if (parseMode !== "MarkdownV2" || !this.isTelegramMarkdownParseError(error)) {
|
|
3428
|
+
throw error;
|
|
3429
|
+
}
|
|
3430
|
+
this.logger.warn("Telegram markdown parse failed; retrying without parse mode", {
|
|
3431
|
+
error: String(error),
|
|
3432
|
+
...context
|
|
3433
|
+
});
|
|
3434
|
+
return operation("plain", this.resolveTelegramFallbackText(context.initialText, context.fallbackText));
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
isTelegramMarkdownParseError(error) {
|
|
3438
|
+
return error instanceof ValidationError && error.adapter === "telegram" && TELEGRAM_MARKDOWN_PARSE_ERROR_PATTERN.test(error.message);
|
|
3439
|
+
}
|
|
3440
|
+
async withTelegramRichFallback(operation, fallback, context) {
|
|
3441
|
+
try {
|
|
3442
|
+
return await operation();
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
if (!this.canFallbackFromRichMessage(error, context.method)) {
|
|
3445
|
+
throw error;
|
|
3446
|
+
}
|
|
3447
|
+
this.rememberRichMessageFailure(error, context.method);
|
|
3448
|
+
this.logger.warn("Telegram rich message failed; retrying with a regular message", {
|
|
3449
|
+
error: String(error),
|
|
3450
|
+
...context
|
|
3451
|
+
});
|
|
3452
|
+
return fallback();
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
function createTelegramAdapter(config) {
|
|
3457
|
+
return new TelegramAdapter(config ?? {});
|
|
3458
|
+
}
|
|
3459
|
+
export {
|
|
3460
|
+
TelegramAdapter,
|
|
3461
|
+
TelegramFormatConverter,
|
|
3462
|
+
applyTelegramEntities,
|
|
3463
|
+
createTelegramAdapter,
|
|
3464
|
+
escapeMarkdownV2
|
|
3465
|
+
};
|