squidclaw 3.0.19 → 3.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{accounts-s5e9zidf.js → accounts-C7pGQPcS.js} +2 -2
- package/dist/{accounts-Bkb-J8V6.js → accounts-DjhBQg_8.js} +2 -2
- package/dist/{accounts-B6F_XCgS.js → accounts-DwGoZKGm.js} +17 -17
- package/dist/{active-listener-6-Svu8Dx.js → active-listener-DPc_PGsA.js} +2 -2
- package/dist/{agents-DL8uYsUq.js → agents-DXKtU4Il.js} +4 -4
- package/dist/{agents.config-B4KAAVvR.js → agents.config-BM-2SLCh.js} +1 -1
- package/dist/{agents.config-D93fF1eJ.js → agents.config-eMSUW-iw.js} +1 -1
- package/dist/{plugin-sdk/api-key-rotation-DyjMI2n3.js → api-key-rotation-DVyYtQxC.js} +2 -2
- package/dist/{audio-preflight-DZ1LNMJp.js → audio-preflight-BUCBED7N.js} +32 -32
- package/dist/{audio-preflight-Bzo_zN4j.js → audio-preflight-lT9iRnYi.js} +4 -4
- package/dist/{audio-transcription-runner-B3u2x_ja.js → audio-transcription-runner-Cv0Q47cM.js} +12 -12
- package/dist/{audio-transcription-runner-Cp_lkLCv.js → audio-transcription-runner-YiR1ym3a.js} +1 -1
- package/dist/{audit-membership-runtime-w23FnNAN.js → audit-membership-runtime-DyLj-uhz.js} +4 -4
- package/dist/{auth-choice-Ba4AEm_k.js → auth-choice-CXepQc7c.js} +1 -1
- package/dist/{auth-choice-ziFj5bXd.js → auth-choice-D2JrMJn-.js} +1 -1
- package/dist/{banner-DDih2slD.js → banner-DMfuBV69.js} +1 -1
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +51 -51
- package/dist/bundled/bootstrap-extra-files/handler.js +6 -6
- package/dist/bundled/command-logger/handler.js +2 -2
- package/dist/bundled/session-memory/handler.js +687 -51
- package/dist/canvas-host/a2ui/a2ui.bundle.js +1 -17815
- package/dist/{channel-activity-CxJbx4sD.js → channel-activity-CPpt4XaL.js} +3 -3
- package/dist/{channel-options-DTDfU0R6.js → channel-options-CQ-gER0J.js} +1 -1
- package/dist/{channel-options-BBSsevyl.js → channel-options-SijIkAlT.js} +1 -1
- package/dist/{channel-web-BXl9jCKV.js → channel-web-7t1a2N13.js} +1 -1
- package/dist/{channel-web-SF18oYqo.js → channel-web-LLzBxZrT.js} +1 -1
- package/dist/{channels-cli-BiuWJpJu.js → channels-cli-D4xUEYYC.js} +6 -6
- package/dist/{channels-cli-BDLabpWH.js → channels-cli-DYaFNMme.js} +6 -6
- package/dist/{chrome-CZQnp4VH.js → chrome-CuBHInKC.js} +8 -8
- package/dist/{chrome-Dxm25ptH.js → chrome-OgCChbC_.js} +26 -26
- package/dist/{cli-CfgDCCuC.js → cli-CAID9zkL.js} +1 -1
- package/dist/{cli-DAZ1MKrJ.js → cli-Dlc9IFWZ.js} +1 -1
- package/dist/{command-registry-DkjNuW8w.js → command-registry-AHS8_9da.js} +9 -9
- package/dist/{commands-registry-B44COZFz.js → commands-registry-BJjv8_oR.js} +4 -4
- package/dist/{completion-cli-DbNma3J9.js → completion-cli-B8xjpHDs.js} +1 -1
- package/dist/{completion-cli-Du8H92bM.js → completion-cli-BR1r9_h4.js} +2 -2
- package/dist/{config-cli-CQhQ8Bcc.js → config-cli-D0A8Awoo.js} +1 -1
- package/dist/{config-cli-D16tlTWa.js → config-cli-D4Rxoozg.js} +1 -1
- package/dist/{configure-B9b3cQTC.js → configure-DOiI8_8c.js} +3 -3
- package/dist/{configure-B4TiK50K.js → configure-XmOqwwy9.js} +3 -3
- package/dist/control-ui/assets/{index-cM5P_3w7.js → index-BqxuPQOl.js} +2 -2
- package/dist/control-ui/assets/{index-cM5P_3w7.js.map → index-BqxuPQOl.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/{deliver-B4WWPQvt.js → deliver-6zfT7daI.js} +1 -1
- package/dist/{deliver-BcnsjbAB.js → deliver-DMTUTpTM.js} +21 -21
- package/dist/deliver-runtime-B80olQwJ.js +36 -0
- package/dist/{deliver-runtime-CeE1X9si.js → deliver-runtime-MBexxUuG.js} +3 -3
- package/dist/deps-send-discord.runtime-GIuvX7Xw.js +26 -0
- package/dist/deps-send-imessage.runtime-B3TC7G80.js +25 -0
- package/dist/deps-send-signal.runtime-CuVJyw7n.js +24 -0
- package/dist/deps-send-slack.runtime-cbfFoLZ4.js +22 -0
- package/dist/deps-send-telegram.runtime-CRyP-xDQ.js +27 -0
- package/dist/deps-send-whatsapp.runtime-4aOUBP2J.js +60 -0
- package/dist/{deps-send-whatsapp.runtime-B9uivfkM.js → deps-send-whatsapp.runtime-CETLGt-t.js} +3 -3
- package/dist/{deps-send-whatsapp.runtime-miTfQkK9.js → deps-send-whatsapp.runtime-CrxkfP31.js} +7 -7
- package/dist/{deps-send-whatsapp.runtime-CFnZsKW0.js → deps-send-whatsapp.runtime-__ogpmJj.js} +3 -3
- package/dist/{diagnostic-C2lklhkn.js → diagnostic-CnVwZNbm.js} +2 -2
- package/dist/{doctor-completion-ujZ_JdIz.js → doctor-completion-CZoEdMS-.js} +1 -1
- package/dist/{doctor-completion-CEiVFPte.js → doctor-completion-DhLEFUhN.js} +1 -1
- package/dist/entry.js +2 -2
- package/dist/{plugin-sdk/errors-B8oJXuCF.js → errors-kkRuS2Cs.js} +1 -1
- package/dist/extensionAPI.js +6 -6
- package/dist/{fetch-b8tSR7_e.js → fetch-DP-JjB6Z.js} +5 -5
- package/dist/{fetch-guard-D16tjNsZ.js → fetch-guard-BDy975wP.js} +2 -2
- package/dist/{frontmatter-CjKtFduT.js → frontmatter-Cq1TcIQ2.js} +3 -3
- package/dist/{fs-safe-CwHbZdFH.js → fs-safe-BoB4X3GD.js} +4 -4
- package/dist/{gateway-cli-NXeo-J8r.js → gateway-cli-BJHBChfI.js} +8 -8
- package/dist/{gateway-cli-2jaWM_Ci.js → gateway-cli-BpHskeDd.js} +8 -8
- package/dist/{github-copilot-token-Cw3tAXM9.js → github-copilot-token-B5cPlwaz.js} +7 -7
- package/dist/{health-BPZALM__.js → health-TmpUGSqu.js} +1 -1
- package/dist/{health-DFZKMrW2.js → health-XFKFZ7ZJ.js} +1 -1
- package/dist/{hooks-cli-B3rkhWcT.js → hooks-cli-CXsZK8H9.js} +2 -2
- package/dist/{hooks-cli-BO_gZJVD.js → hooks-cli-DsDV-Pxz.js} +2 -2
- package/dist/{image-DKkuLtZ4.js → image-VgwN31FZ.js} +1 -1
- package/dist/{image-C-8Kd2Mh.js → image-kKMG59st.js} +6 -6
- package/dist/{image-ops-BoN1E_WZ.js → image-ops-Dg8iraUV.js} +2 -2
- package/dist/image-runtime-BqIv7p_K.js +29 -0
- package/dist/{image-runtime-BMavqm9n.js → image-runtime-CwMuTYvd.js} +3 -3
- package/dist/index.js +6 -6
- package/dist/{ir-Dut0zXyS.js → ir-CKK03mBV.js} +8 -8
- package/dist/{legacy-names-B0wgIP0Q.js → legacy-names-aGJJuzM_.js} +1 -1
- package/dist/llm-slug-generator.js +51 -51
- package/dist/{logger-oGKcCLZ5.js → logger-CnTSBL7T.js} +7 -7
- package/dist/{login-DJ357UQV.js → login-CeKDrz6_.js} +5 -5
- package/dist/{login-qr-C0fDBnpM.js → login-qr-KbOpR0GQ.js} +10 -10
- package/dist/{manager-BPGhs30n.js → manager-DINhLnMi.js} +13 -13
- package/dist/manager-runtime-D6ckUNSs.js +18 -0
- package/dist/{model-selection-CHnojCCK.js → model-selection-DuNLFQPR.js} +43 -43
- package/dist/{models-Bb-GYqHr.js → models-5VXwJBU2.js} +2 -2
- package/dist/{models-cli-DffAz0ji.js → models-cli-Br56IHfy.js} +3 -3
- package/dist/{models-cli-CAIJM1Wh.js → models-cli-CrR1RN3j.js} +2 -2
- package/dist/{npm-resolution-D2_zGJM-.js → npm-resolution-CKtyq4FH.js} +1 -1
- package/dist/{npm-resolution-D5pBCkUw.js → npm-resolution-CPk7dS7F.js} +1 -1
- package/dist/{onboard-DcVsL9Jx.js → onboard-DUnBamC0.js} +2 -2
- package/dist/{onboard-channels-mNXKTvFs.js → onboard-channels-D45grihx.js} +1 -1
- package/dist/{onboard-channels-C-cQFUBH.js → onboard-channels-_kVo3Apf.js} +1 -1
- package/dist/{onboard-dhG2YBez.js → onboard-lFwpCpC3.js} +2 -2
- package/dist/{onboarding-B3-IwkNi.js → onboarding-CyCSQ__q.js} +3 -3
- package/dist/{onboarding-Cl6kW_iq.js → onboarding-EEd_g8Zg.js} +3 -3
- package/dist/{onboarding.finalize-CCU-ykTR.js → onboarding.finalize-CbMq7C4K.js} +5 -5
- package/dist/{onboarding.finalize-BZqsNG2H.js → onboarding.finalize-Cs1ukjFN.js} +6 -6
- package/dist/{outbound-qTioiTJg.js → outbound-C9svt6RH.js} +6 -6
- package/dist/{outbound-attachment-rlW7G5df.js → outbound-attachment-DwupUxYF.js} +2 -2
- package/dist/{path-alias-guards-Cpsiv2KL.js → path-alias-guards-DORgbZ1w.js} +1 -1
- package/dist/{paths-CSdAWKDO.js → paths-DA5srn0U.js} +5 -5
- package/dist/{paths-CXClY8zC.js → paths-DSd911Oe.js} +4 -4
- package/dist/{pi-embedded-B79nZERv.js → pi-embedded-BN8fghaF.js} +123 -26
- package/dist/{pi-embedded-C5U8Qxn6.js → pi-embedded-BiC4tIJ8.js} +266 -169
- package/dist/{pi-embedded-helpers-CEHpGDRs.js → pi-embedded-helpers-A9VYPVCH.js} +3 -3
- package/dist/{pi-embedded-helpers-DQ7IaeOi.js → pi-embedded-helpers-Di58J7Eh.js} +52 -52
- package/dist/{pi-model-discovery-o-WX5w2t.js → pi-model-discovery-V-InbjOM.js} +7 -7
- package/dist/pi-model-discovery-runtime--t6tAlar.js +11 -0
- package/dist/{pi-tools.before-tool-call.runtime-B_LUttg1.js → pi-tools.before-tool-call.runtime-Bpk4qTgV.js} +9 -9
- package/dist/{plugin-registry-CB47SSz1.js → plugin-registry-CXm125Uz.js} +1 -1
- package/dist/{plugin-registry-Gy_VByyf.js → plugin-registry-D3PnPE1D.js} +1 -1
- package/dist/plugin-sdk/{accounts-0kF5cxkn.js → accounts-C5PAuCTj.js} +2 -2
- package/dist/plugin-sdk/{accounts-u0-JAHzj.js → accounts-DC5cbH9r.js} +2 -2
- package/dist/plugin-sdk/{accounts-xZOA23tQ.js → accounts-DncG0Hx9.js} +3 -3
- package/dist/plugin-sdk/{active-listener-BO7eBEG_.js → active-listener-Bd3HH2km.js} +2 -2
- package/dist/plugin-sdk/agents/agent-tier.d.ts +22 -2
- package/dist/plugin-sdk/{api-key-rotation-C4C_mDsg.js → api-key-rotation-BAZ0GD26.js} +2 -2
- package/dist/plugin-sdk/{audio-preflight-C0q7lu6y.js → audio-preflight-tpVm-t9O.js} +26 -26
- package/dist/plugin-sdk/{audio-transcription-runner-Ced47O1H.js → audio-transcription-runner-DSScb434.js} +11 -11
- package/dist/plugin-sdk/{audit-membership-runtime-Xl20kCBe.js → audit-membership-runtime-DHQDvH4u.js} +2 -2
- package/dist/plugin-sdk/{channel-activity-CNffKOEQ.js → channel-activity-cYEaofTH.js} +3 -3
- package/dist/plugin-sdk/{channel-web-vB3Dcd8-.js → channel-web-CbeCrQ4C.js} +18 -18
- package/dist/plugin-sdk/{channel-web-BrhP6FQO.js → channel-web-CqiSEc52.js} +1 -1
- package/dist/plugin-sdk/{chrome-DJQWo149.js → chrome-DB2h0uN0.js} +6 -6
- package/dist/plugin-sdk/{commands-registry-_kBPE22q.js → commands-registry-DGZ1oFXJ.js} +4 -4
- package/dist/plugin-sdk/{common-J8vIST9Q.js → common-D5lLWoCW.js} +2 -2
- package/dist/plugin-sdk/compat.js +50 -50
- package/dist/plugin-sdk/{config-CnZ1TlEw.js → config-Bt-c7PWF.js} +7 -7
- package/dist/plugin-sdk/{deliver-Xh6voz9H.js → deliver-MvrkIeKJ.js} +10 -10
- package/dist/plugin-sdk/deliver-runtime-i50kjcNM.js +32 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-hOYq9ov0.js +23 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-D5n9DXyL.js +22 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-CwEaRyJU.js +21 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-DOZeLIaC.js +19 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-BW1hSPKh.js +24 -0
- package/dist/plugin-sdk/{deps-send-whatsapp.runtime-Ce4Gzz-J.js → deps-send-whatsapp.runtime-DvxhnHR_.js} +3 -3
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-JKmTtCFM.js +57 -0
- package/dist/plugin-sdk/{diagnostic-DKsyROPi.js → diagnostic-B6F3BtCX.js} +2 -2
- package/dist/plugin-sdk/{errors-CgRPdp3o.js → errors-9oVz7reJ.js} +1 -1
- package/dist/plugin-sdk/{fetch-guard-BBAT8G-1.js → fetch-guard-cfPCfkrw.js} +2 -2
- package/dist/plugin-sdk/{fs-safe-DqCO1D4C.js → fs-safe-DFbwq9CS.js} +3 -3
- package/dist/plugin-sdk/{image-4ay2cw7G.js → image-FK5xhY5u.js} +6 -6
- package/dist/plugin-sdk/{image-ops-G8KoEfY8.js → image-ops-BSYgrL7E.js} +2 -2
- package/dist/plugin-sdk/image-runtime-B95EPFpg.js +25 -0
- package/dist/plugin-sdk/index.js +2 -2
- package/dist/plugin-sdk/{ir-DXj1KGnL.js → ir-CWmryq5f.js} +7 -7
- package/dist/plugin-sdk/{local-roots-ovKHgVSP.js → local-roots-U25IdeDH.js} +4 -4
- package/dist/plugin-sdk/{logger-DIb2cGHp.js → logger-Bg4vIUJn.js} +2 -2
- package/dist/plugin-sdk/{login-Cgtm70by.js → login-8qzl2H7G.js} +4 -4
- package/dist/plugin-sdk/{login-qr-BrixqhMx.js → login-qr-kalCTJEw.js} +5 -5
- package/dist/plugin-sdk/{manager-PEQ_cPYF.js → manager-ijYHktIt.js} +8 -8
- package/dist/plugin-sdk/manager-runtime--JrLQE03.js +15 -0
- package/dist/plugin-sdk/{outbound-BK75h9CQ.js → outbound-BIVf0aPq.js} +5 -5
- package/dist/plugin-sdk/{outbound-attachment-BI1QngTS.js → outbound-attachment-F5tzeNUD.js} +2 -2
- package/dist/plugin-sdk/{path-alias-guards-TnxupPQC.js → path-alias-guards-DA0MhfkG.js} +1 -1
- package/dist/plugin-sdk/{paths-B7_75Pdr.js → paths-CP67O8eN.js} +1 -1
- package/dist/plugin-sdk/{pi-embedded-helpers-BioULNev.js → pi-embedded-helpers-CxMnPQ37.js} +16 -16
- package/dist/plugin-sdk/{pi-model-discovery-BMmAbnil.js → pi-model-discovery-8OL77kCh.js} +1 -1
- package/dist/plugin-sdk/pi-model-discovery-runtime-DGRFpUfd.js +8 -0
- package/dist/plugin-sdk/{pi-tools.before-tool-call.runtime-rdRqZ9lk.js → pi-tools.before-tool-call.runtime-B7FW36KX.js} +4 -4
- package/dist/plugin-sdk/{plugins-BqKpJkqt.js → plugins-D3mrhffi.js} +4 -4
- package/dist/plugin-sdk/{proxy-env-C4s12rr8.js → proxy-env-CUUXO6rv.js} +1 -1
- package/dist/plugin-sdk/{proxy-fetch-ZPEvp58f.js → proxy-fetch-Cf3IUSDw.js} +1 -1
- package/dist/plugin-sdk/{pw-ai-Cb0ZDSq4.js → pw-ai-dPmKdw_w.js} +9 -9
- package/dist/plugin-sdk/{qmd-manager-Bei6TaFq.js → qmd-manager-Ov9ElEfG.js} +7 -7
- package/dist/plugin-sdk/{query-expansion-POz2za8a.js → query-expansion-CzjwW461.js} +4 -4
- package/dist/plugin-sdk/{redact-9WsNyb7S.js → redact-DfACyt0X.js} +1 -1
- package/dist/plugin-sdk/{reply-DxvpiQBp.js → reply-BczXGzC_.js} +137 -40
- package/dist/plugin-sdk/{reply-sFKB3ofA.js → reply-Dfjh1YtV.js} +206 -109
- package/dist/plugin-sdk/{resolve-outbound-target-DL1adXpk.js → resolve-outbound-target-BeIB_jm0.js} +2 -2
- package/dist/plugin-sdk/{run-with-concurrency-DmTrN5JG.js → run-with-concurrency-kVooFCVo.js} +1 -1
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-B6EcC22F.js +10 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DH8J0Ie7.js +19 -0
- package/dist/plugin-sdk/{send-BCVXt-3e.js → send-CgkNzAhS.js} +8 -8
- package/dist/plugin-sdk/{send-CAG0Or0G.js → send-CuENGOhq.js} +7 -7
- package/dist/plugin-sdk/{send-X6QuS7x0.js → send-D6Uh2D1D.js} +5 -5
- package/dist/plugin-sdk/{send-BNePC8CO.js → send-DV5kR0Hg.js} +6 -6
- package/dist/plugin-sdk/{send-DhGDAZnT.js → send-DtA6ngBJ.js} +13 -13
- package/dist/plugin-sdk/{session-CAqYQVhe.js → session-CnlZn-bR.js} +3 -3
- package/dist/plugin-sdk/{skill-commands-Bk-IFyNw.js → skill-commands-Ckfii7h8.js} +4 -4
- package/dist/plugin-sdk/{skills-C5bT9-q4.js → skills-v0WQqKTa.js} +6 -6
- package/dist/plugin-sdk/slash-commands.runtime-BYoxsxkX.js +13 -0
- package/dist/plugin-sdk/{slash-dispatch.runtime-Dyb_EkUt.js → slash-dispatch.runtime-DOo1IzuY.js} +1 -1
- package/dist/plugin-sdk/slash-dispatch.runtime-JfFr7bNy.js +52 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-D0399tia.js +16 -0
- package/dist/plugin-sdk/{store-D3lgWnS2.js → store-C_UrNuM3.js} +2 -2
- package/dist/plugin-sdk/{subagent-registry-runtime-hdnabonI.js → subagent-registry-runtime-DLvup9ph.js} +1 -1
- package/dist/plugin-sdk/subagent-registry-runtime-DVl5xKOW.js +52 -0
- package/dist/plugin-sdk/{tables-Dmlu4_q_.js → tables-CJP05iMa.js} +1 -1
- package/dist/plugin-sdk/{thinking-DRNOh5Xx.js → thinking-BhP3vn1l.js} +7 -7
- package/dist/plugin-sdk/{tokens-CTIYTLWu.js → tokens-DAL_5WHL.js} +1 -1
- package/dist/plugin-sdk/{tool-images-GSlvf6RP.js → tool-images-ZE5G5UM0.js} +2 -2
- package/dist/plugin-sdk/{web-D9LAdsuU.js → web-C2Sj2WW0.js} +2 -2
- package/dist/plugin-sdk/web-CNY_ky8h.js +56 -0
- package/dist/plugin-sdk/{whatsapp-actions-BCJYmWCB.js → whatsapp-actions-DJmx6ihj.js} +17 -17
- package/dist/plugin-sdk/whatsapp.js +50 -50
- package/dist/{plugins-AqsVZZk3.js → plugins-DvejjZnJ.js} +11 -11
- package/dist/{plugins-cli-BYnoJwF1.js → plugins-cli-BA_2daJe.js} +2 -2
- package/dist/{plugins-cli-gtgkmDcv.js → plugins-cli-C0WKWrZo.js} +2 -2
- package/dist/{program-BGlgDpHI.js → program-D_xdLzmM.js} +7 -7
- package/dist/{program-context-M4FTWqqg.js → program-context-DfmCIQ91.js} +17 -17
- package/dist/{prompt-select-styled-AL5Tm3yO.js → prompt-select-styled-ApPSadr5.js} +4 -4
- package/dist/{prompt-select-styled-BVC2byQV.js → prompt-select-styled-BsheNEnh.js} +4 -4
- package/dist/{provider-auth-helpers-DDgrFku5.js → provider-auth-helpers-B5kD4Lt6.js} +1 -1
- package/dist/{provider-auth-helpers-Q7aIhDgv.js → provider-auth-helpers-DpOFkV28.js} +1 -1
- package/dist/{proxy-env-DXXfs2WL.js → proxy-env-D-fike7s.js} +1 -1
- package/dist/{plugin-sdk/proxy-fetch-Dt5BedH8.js → proxy-fetch-lH6RsRTE.js} +1 -1
- package/dist/{push-apns-Ga8xvhkh.js → push-apns-BbenjEX4.js} +1 -1
- package/dist/{push-apns-FyGbUyh2.js → push-apns-Ci2sdIKf.js} +1 -1
- package/dist/{pw-ai-GOBxzChI.js → pw-ai-B8ymIYub.js} +14 -14
- package/dist/{pw-ai-Du22SYoO.js → pw-ai-CM87tQwG.js} +1 -1
- package/dist/{qmd-manager-CO-shcLU.js → qmd-manager-BN0siR2Z.js} +10 -10
- package/dist/{query-expansion-DlQOkf-g.js → query-expansion-Dzxt6kXo.js} +6 -6
- package/dist/{redact-NmPEVjIo.js → redact-DvzicBMu.js} +1 -1
- package/dist/{register.agent-koAXw1ay.js → register.agent-CY6R-7fn.js} +6 -6
- package/dist/{register.agent-zP8a-sNB.js → register.agent-Dk_5V5oA.js} +7 -7
- package/dist/{register.configure-CUuP5J8W.js → register.configure-C8OFfGln.js} +7 -7
- package/dist/{register.configure-CYrmyjlf.js → register.configure-DT1RLeTR.js} +7 -7
- package/dist/{register.maintenance-BZuEljPU.js → register.maintenance-CluQOq8b.js} +8 -8
- package/dist/{register.maintenance-ByEQ2CVH.js → register.maintenance-YHXmOQzf.js} +7 -7
- package/dist/{register.message-BkSXhv9I.js → register.message-BCTXT5yK.js} +2 -2
- package/dist/{register.message-CgHfmDHm.js → register.message-BuoFjRqA.js} +2 -2
- package/dist/{register.onboard-HcQwPAEK.js → register.onboard-BJaRMtxU.js} +2 -2
- package/dist/{register.onboard-4ke40MNP.js → register.onboard-C64oXnvq.js} +2 -2
- package/dist/{register.setup-B41xZb1r.js → register.setup-Dl3nk2Ui.js} +2 -2
- package/dist/{register.setup-xAb7Cvzy.js → register.setup-Drc55K_w.js} +2 -2
- package/dist/{register.status-health-sessions-CcOVL0x8.js → register.status-health-sessions-BHtbPeCO.js} +3 -3
- package/dist/{register.status-health-sessions-BbHAAKwv.js → register.status-health-sessions-xgC_gtEM.js} +3 -3
- package/dist/{register.subclis-_uSASnfC.js → register.subclis-CDMFyaKR.js} +9 -9
- package/dist/{reply-BFTOdZcK.js → reply-DwjHsBuj.js} +137 -40
- package/dist/{run-main-C1eZrW-5.js → run-main-BcA22Ipv.js} +14 -14
- package/dist/{run-with-concurrency-FczpX8ng.js → run-with-concurrency-BFR3ReeF.js} +4 -4
- package/dist/runtime-whatsapp-login.runtime-DxV9iv6l.js +13 -0
- package/dist/runtime-whatsapp-outbound.runtime-DQqIlwhS.js +22 -0
- package/dist/{send-Bx9CqcZr.js → send-4nRsZJXH.js} +7 -7
- package/dist/{send-D5D0ZGht.js → send-BGlcHjUD.js} +26 -26
- package/dist/{send-DtHQ7_6Z.js → send-BTUU1jWM.js} +8 -8
- package/dist/{send-B9H0BkfO.js → send-DdBbRpTP.js} +6 -6
- package/dist/{send-B5QEmMJ4.js → send-dTQd-IyJ.js} +5 -5
- package/dist/{server-node-events-pYrJmi9w.js → server-node-events-Q0cT2ifj.js} +2 -2
- package/dist/{server-node-events-DbS1jJ5j.js → server-node-events-YN5eJeHa.js} +2 -2
- package/dist/{session-DLTCuoUD.js → session-CnCwDJke.js} +8 -8
- package/dist/{skill-commands-BwTLQRR8.js → skill-commands-Cz45_nme.js} +9 -9
- package/dist/{skills-B9N2bqKU.js → skills-CdCS1HeL.js} +22 -22
- package/dist/slash-commands.runtime-CZz6v6b3.js +16 -0
- package/dist/{slash-dispatch.runtime-PX9_08Hm.js → slash-dispatch.runtime-BOMEVFk0.js} +1 -1
- package/dist/{slash-dispatch.runtime-DmFVOi3w.js → slash-dispatch.runtime-DB1QsBRc.js} +1 -1
- package/dist/slash-dispatch.runtime-SO7HOe8q.js +56 -0
- package/dist/{slash-dispatch.runtime-BGXbtMX6.js → slash-dispatch.runtime-sXaUYn4v.js} +6 -6
- package/dist/slash-skill-commands.runtime-Bawt7j0r.js +20 -0
- package/dist/{status-DtiapFqq.js → status-Bh5x6DsW.js} +2 -2
- package/dist/{status-B4eVRJcO.js → status-cU9cJReo.js} +2 -2
- package/dist/{store-CvQ41zCV.js → store-D9z0dn7D.js} +2 -2
- package/dist/{subagent-registry-Ctrhw4X-.js → subagent-registry-CIgFD2oi.js} +103 -6
- package/dist/{subagent-registry-runtime-CezPaX9P.js → subagent-registry-runtime-232sNNT6.js} +6 -6
- package/dist/subagent-registry-runtime-D2rUxU5J.js +56 -0
- package/dist/{subagent-registry-runtime-Cy7dsGqF.js → subagent-registry-runtime-D7ubAnDZ.js} +1 -1
- package/dist/{subagent-registry-runtime-CTjx4TK5.js → subagent-registry-runtime-DGTjU9Lb.js} +1 -1
- package/dist/{subsystem-BaLYRf7D.js → subsystem-6v7sWnAD.js} +14 -14
- package/dist/{tables-BRYYxYs7.js → tables-BTFiZyRU.js} +1 -1
- package/dist/{target-errors-D0ZJUbbf.js → target-errors-DgNRx3Nr.js} +2 -2
- package/dist/{thinking-B-A99X3F.js → thinking-B75CXkTT.js} +7 -7
- package/dist/{tokens-D2XhLqIz.js → tokens-DfbMVF9y.js} +1 -1
- package/dist/{tool-images-Zn6jVmkX.js → tool-images-Dp5OiXeA.js} +2 -2
- package/dist/{update-cli-_FsTRdQZ.js → update-cli-CktBOXZF.js} +7 -7
- package/dist/{update-cli-DLOWRdjv.js → update-cli-DM_dUip_.js} +8 -8
- package/dist/{update-runner-BY3l7ytB.js → update-runner-BBJZmfZ4.js} +1 -1
- package/dist/{update-runner-BKWHnVYI.js → update-runner-BYGPkHXG.js} +1 -1
- package/dist/{web-BdZ3mX3p.js → web-CZhZC1EA.js} +2 -2
- package/dist/{web-B46aOGK0.js → web-Chw1dtKA.js} +6 -6
- package/dist/{web-BNfDYvlW.js → web-DWRZAXj9.js} +2 -2
- package/dist/{web-1D0d1RHD.js → web-QsxgXbNK.js} +55 -55
- package/dist/{whatsapp-actions-Cuy0qeQK.js → whatsapp-actions-CzqsuSGx.js} +21 -21
- package/dist/{workspace-TqfVSQuO.js → workspace-kVMIaBrV.js} +20 -20
- package/extensions/acpx/node_modules/.bin/acpx +2 -2
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +2 -2
- package/extensions/diffs/node_modules/.bin/playwright-core +2 -2
- package/extensions/googlechat/node_modules/.bin/squidclaw +2 -2
- package/extensions/matrix/node_modules/.bin/markdown-it +2 -2
- package/extensions/memory-core/node_modules/.bin/squidclaw +2 -2
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +2 -2
- package/extensions/memory-lancedb/node_modules/.bin/openai +2 -2
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/tlon/node_modules/.bin/tlon +2 -2
- package/package.json +1 -1
- package/dist/api-key-rotation-TRwuCWbu.js +0 -181
- package/dist/canvas-host/a2ui/.bundle.hash +0 -1
- package/dist/deliver-runtime-_mBfV_4S.js +0 -36
- package/dist/deps-send-discord.runtime-CUTAK2hy.js +0 -26
- package/dist/deps-send-imessage.runtime-CQOiEIuA.js +0 -25
- package/dist/deps-send-signal.runtime-wN7MkzLw.js +0 -24
- package/dist/deps-send-slack.runtime-HEEwW4uU.js +0 -22
- package/dist/deps-send-telegram.runtime-DHlcnjQO.js +0 -27
- package/dist/deps-send-whatsapp.runtime-Cw7U9orE.js +0 -60
- package/dist/errors-DfgAh2Ml.js +0 -54
- package/dist/export-html/vendor/highlight.min.js +0 -1213
- package/dist/export-html/vendor/marked.min.js +0 -6
- package/dist/image-runtime-D11wBIC8.js +0 -29
- package/dist/manager-runtime-BB9lcoFR.js +0 -18
- package/dist/pi-model-discovery-runtime-DBkQoIJw.js +0 -11
- package/dist/plugin-sdk/accounts-CUEuUR3C.js +0 -46
- package/dist/plugin-sdk/accounts-D0W2pELU.js +0 -288
- package/dist/plugin-sdk/accounts-ucj0odJq.js +0 -35
- package/dist/plugin-sdk/active-listener-C5xPUSTb.js +0 -50
- package/dist/plugin-sdk/audio-preflight-Cqdo0JKm.js +0 -69
- package/dist/plugin-sdk/audio-transcription-runner-DnEooIyE.js +0 -2176
- package/dist/plugin-sdk/audit-membership-runtime-B9b-zRwg.js +0 -58
- package/dist/plugin-sdk/channel-activity-BMWLw4o2.js +0 -94
- package/dist/plugin-sdk/channel-web-D9WdAFlv.js +0 -2256
- package/dist/plugin-sdk/chrome-CV-q0Lmc.js +0 -2415
- package/dist/plugin-sdk/commands-registry-e7YoqrbP.js +0 -1125
- package/dist/plugin-sdk/config-B2B64aX0.js +0 -17911
- package/dist/plugin-sdk/deliver-BkyBtcLR.js +0 -1694
- package/dist/plugin-sdk/deliver-runtime-5UVcSskg.js +0 -32
- package/dist/plugin-sdk/deliver-runtime-O4lwAWMw.js +0 -32
- package/dist/plugin-sdk/deps-send-discord.runtime-BAeeBldY.js +0 -23
- package/dist/plugin-sdk/deps-send-discord.runtime-DTspXSCt.js +0 -23
- package/dist/plugin-sdk/deps-send-imessage.runtime-EL-CfikZ.js +0 -22
- package/dist/plugin-sdk/deps-send-imessage.runtime-qThAwDEe.js +0 -22
- package/dist/plugin-sdk/deps-send-signal.runtime-BeemHeUu.js +0 -21
- package/dist/plugin-sdk/deps-send-signal.runtime-DnjnVzZF.js +0 -21
- package/dist/plugin-sdk/deps-send-slack.runtime-CbKevLnv.js +0 -19
- package/dist/plugin-sdk/deps-send-slack.runtime-DTttkC0N.js +0 -19
- package/dist/plugin-sdk/deps-send-telegram.runtime-C6y29O9E.js +0 -24
- package/dist/plugin-sdk/deps-send-telegram.runtime-Dsf9Cnka.js +0 -24
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-CCS71r77.js +0 -57
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-CuxKhIE0.js +0 -57
- package/dist/plugin-sdk/diagnostic-DPRVoKTk.js +0 -319
- package/dist/plugin-sdk/fetch-guard-F0Fnqisy.js +0 -156
- package/dist/plugin-sdk/fs-safe-Dqmpk-Fr.js +0 -352
- package/dist/plugin-sdk/image-cBW8L8pp.js +0 -2310
- package/dist/plugin-sdk/image-ops-BP8ix1GC.js +0 -584
- package/dist/plugin-sdk/image-runtime-9xkgSlNz.js +0 -25
- package/dist/plugin-sdk/image-runtime-CpfepTDc.js +0 -25
- package/dist/plugin-sdk/ir-DWEc6zOp.js +0 -1296
- package/dist/plugin-sdk/local-roots-BIPT8uAO.js +0 -186
- package/dist/plugin-sdk/logger-DDdrdbDu.js +0 -1163
- package/dist/plugin-sdk/login-BMTiGutN.js +0 -57
- package/dist/plugin-sdk/login-qr-BFxqYUkc.js +0 -320
- package/dist/plugin-sdk/manager-BD-aYaZ8.js +0 -3917
- package/dist/plugin-sdk/manager-runtime-BmgTeb5G.js +0 -15
- package/dist/plugin-sdk/manager-runtime-C5bRwUlz.js +0 -15
- package/dist/plugin-sdk/outbound-Bm07xvO6.js +0 -212
- package/dist/plugin-sdk/outbound-attachment-DLsaxDsc.js +0 -19
- package/dist/plugin-sdk/path-alias-guards-gBhrAn14.js +0 -43
- package/dist/plugin-sdk/paths-C6W4VHoa.js +0 -166
- package/dist/plugin-sdk/pi-embedded-helpers-BExwPvTh.js +0 -9627
- package/dist/plugin-sdk/pi-model-discovery-DdctvBeX.js +0 -134
- package/dist/plugin-sdk/pi-model-discovery-runtime-COnuGwZt.js +0 -8
- package/dist/plugin-sdk/pi-model-discovery-runtime-DrtpLJjk.js +0 -8
- package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-rgTz3FBl.js +0 -354
- package/dist/plugin-sdk/plugins-BN64HHZA.js +0 -864
- package/dist/plugin-sdk/pw-ai-DBm3RdBK.js +0 -1938
- package/dist/plugin-sdk/qmd-manager-6bozlfFg.js +0 -1448
- package/dist/plugin-sdk/query-expansion-eeVz_aEm.js +0 -1011
- package/dist/plugin-sdk/redact-BoNEjbpF.js +0 -319
- package/dist/plugin-sdk/reply-CqXMy3Yd.js +0 -98731
- package/dist/plugin-sdk/resolve-outbound-target-DXfjGlZQ.js +0 -40
- package/dist/plugin-sdk/run-with-concurrency-5DMu9szx.js +0 -1994
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-99sCh8RG.js +0 -10
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-D2hkJBa-.js +0 -10
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-C06I4adi.js +0 -19
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-dTkDaXHl.js +0 -19
- package/dist/plugin-sdk/send-B9xnwtQ-.js +0 -540
- package/dist/plugin-sdk/send-BxySs-Cu.js +0 -2587
- package/dist/plugin-sdk/send-D9THKp_J.js +0 -414
- package/dist/plugin-sdk/send-DCuaaP2w.js +0 -503
- package/dist/plugin-sdk/send-Dm_-_xje.js +0 -3135
- package/dist/plugin-sdk/session-DDzIZHxt.js +0 -169
- package/dist/plugin-sdk/skill-commands-DRvqtuFC.js +0 -342
- package/dist/plugin-sdk/skills-BWwlfbVj.js +0 -1428
- package/dist/plugin-sdk/slash-commands.runtime-BzYsaTST.js +0 -13
- package/dist/plugin-sdk/slash-commands.runtime-EAZKpRKq.js +0 -13
- package/dist/plugin-sdk/slash-dispatch.runtime-BC3IAF-I.js +0 -52
- package/dist/plugin-sdk/slash-dispatch.runtime-C7u7mlny.js +0 -52
- package/dist/plugin-sdk/slash-skill-commands.runtime-DGd_JSWX.js +0 -16
- package/dist/plugin-sdk/slash-skill-commands.runtime-sg98L8BK.js +0 -16
- package/dist/plugin-sdk/ssrf-DOBwjFow.js +0 -202
- package/dist/plugin-sdk/store-BKDR_-Qk.js +0 -81
- package/dist/plugin-sdk/subagent-registry-runtime-B6l6PxqP.js +0 -52
- package/dist/plugin-sdk/subagent-registry-runtime-Ceu7AzWi.js +0 -52
- package/dist/plugin-sdk/tables-GIj79us5.js +0 -55
- package/dist/plugin-sdk/target-errors-jlLHihbX.js +0 -195
- package/dist/plugin-sdk/thinking-BgdUnMZ2.js +0 -1206
- package/dist/plugin-sdk/tokens-CLE20fRI.js +0 -52
- package/dist/plugin-sdk/tool-images-BY1gsRyE.js +0 -274
- package/dist/plugin-sdk/web-BK2-uaOo.js +0 -56
- package/dist/plugin-sdk/web-Bnn82S4u.js +0 -56
- package/dist/plugin-sdk/whatsapp-actions-DQpK_5Ds.js +0 -80
- package/dist/proxy-fetch-C2v-Utgg.js +0 -38
- package/dist/runtime-whatsapp-login.runtime-DumUjKRz.js +0 -13
- package/dist/runtime-whatsapp-outbound.runtime-Cgu2MfqR.js +0 -22
- package/dist/slash-commands.runtime-CjBXruwG.js +0 -16
- package/dist/slash-dispatch.runtime-DqZVfX4H.js +0 -56
- package/dist/slash-skill-commands.runtime-DYK20Lxf.js +0 -20
- package/dist/subagent-registry-runtime-BIKGAzgI.js +0 -56
- package/docs/reference/templates/IDENTITY.md +0 -29
- package/docs/reference/templates/USER.md +0 -23
- package/docs/zh-CN/reference/templates/IDENTITY.md +0 -36
- package/docs/zh-CN/reference/templates/USER.md +0 -30
- package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +0 -178
|
@@ -1,2587 +0,0 @@
|
|
|
1
|
-
import { at as DEFAULT_ACCOUNT_ID, ot as normalizeAccountId } from "./run-with-concurrency-5DMu9szx.js";
|
|
2
|
-
import { c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, o as resolveOAuthDir } from "./paths-8xF5kDne.js";
|
|
3
|
-
import { Ar as withFileLock$1, bn as resolveTelegramPreviewStreamMode, ei as isTruthyEnvValue, i as readConfigFileSnapshotForWrite, n as loadConfig, o as writeConfigFile } from "./config-B2B64aX0.js";
|
|
4
|
-
import { D as safeParseJson, F as danger, L as logVerbose, a as createSubsystemLogger, f as CONFIG_DIR } from "./logger-DDdrdbDu.js";
|
|
5
|
-
import { h as normalizeMimeType, m as kindFromMime, p as isGifMedia, u as getFileExtension } from "./image-ops-BP8ix1GC.js";
|
|
6
|
-
import { C as resolveTelegramAccount, n as listChannelPlugins, t as getChannelPlugin } from "./plugins-BN64HHZA.js";
|
|
7
|
-
import { r as writeJsonAtomic } from "./json-files-DJe5SBCd.js";
|
|
8
|
-
import { t as resolveFetch } from "./fetch-B_RcOnt9.js";
|
|
9
|
-
import { t as redactSensitiveText } from "./redact-BoNEjbpF.js";
|
|
10
|
-
import { i as formatUncaughtError, n as extractErrorCode, o as readErrorName, r as formatErrorMessage, t as collectErrorGraphCandidates } from "./errors-B8oJXuCF.js";
|
|
11
|
-
import { i as createTelegramRetryRunner, n as recordChannelActivity } from "./channel-activity-BMWLw4o2.js";
|
|
12
|
-
import { t as buildOutboundMediaLoadOptions } from "./load-options-DNSaiajj.js";
|
|
13
|
-
import { n as normalizePollInput } from "./polls-3WJMd-G-.js";
|
|
14
|
-
import { u as hasProxyEnvConfigured } from "./ssrf-DOBwjFow.js";
|
|
15
|
-
import { i as resolveMarkdownTableMode, n as markdownToIR, t as chunkMarkdownIR, v as loadWebMedia } from "./ir-DWEc6zOp.js";
|
|
16
|
-
import { t as renderMarkdownWithMarkers } from "./render-HmipMDlP.js";
|
|
17
|
-
import { n as normalizeTelegramLookupTarget, r as parseTelegramTarget, t as normalizeTelegramChatId } from "./targets-D46Aqz9j.js";
|
|
18
|
-
import { t as makeProxyFetch } from "./proxy-fetch-ZPEvp58f.js";
|
|
19
|
-
import fs, { readFileSync } from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import os from "node:os";
|
|
22
|
-
import fs$1 from "node:fs/promises";
|
|
23
|
-
import crypto, { randomBytes } from "node:crypto";
|
|
24
|
-
import JSON5 from "json5";
|
|
25
|
-
import process$1 from "node:process";
|
|
26
|
-
import { EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
|
27
|
-
import * as dns from "node:dns";
|
|
28
|
-
import * as net$1 from "node:net";
|
|
29
|
-
import { Bot, HttpError, InputFile } from "grammy";
|
|
30
|
-
|
|
31
|
-
//#region src/channels/allow-from.ts
|
|
32
|
-
function mergeDmAllowFromSources(params) {
|
|
33
|
-
const storeEntries = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom ?? [];
|
|
34
|
-
return [...params.allowFrom ?? [], ...storeEntries].map((value) => String(value).trim()).filter(Boolean);
|
|
35
|
-
}
|
|
36
|
-
function resolveGroupAllowFromSources(params) {
|
|
37
|
-
const explicitGroupAllowFrom = Array.isArray(params.groupAllowFrom) && params.groupAllowFrom.length > 0 ? params.groupAllowFrom : void 0;
|
|
38
|
-
return (explicitGroupAllowFrom ? explicitGroupAllowFrom : params.fallbackToAllowFrom === false ? [] : params.allowFrom ?? []).map((value) => String(value).trim()).filter(Boolean);
|
|
39
|
-
}
|
|
40
|
-
function firstDefined(...values) {
|
|
41
|
-
for (const value of values) if (typeof value !== "undefined") return value;
|
|
42
|
-
}
|
|
43
|
-
function isSenderIdAllowed(allow, senderId, allowWhenEmpty) {
|
|
44
|
-
if (!allow.hasEntries) return allowWhenEmpty;
|
|
45
|
-
if (allow.hasWildcard) return true;
|
|
46
|
-
if (!senderId) return false;
|
|
47
|
-
return allow.entries.includes(senderId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/channels/plugins/pairing.ts
|
|
52
|
-
function listPairingChannels() {
|
|
53
|
-
return listChannelPlugins().filter((plugin) => plugin.pairing).map((plugin) => plugin.id);
|
|
54
|
-
}
|
|
55
|
-
function getPairingAdapter(channelId) {
|
|
56
|
-
return getChannelPlugin(channelId)?.pairing ?? null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/plugin-sdk/json-store.ts
|
|
61
|
-
async function readJsonFileWithFallback(filePath, fallback) {
|
|
62
|
-
try {
|
|
63
|
-
const parsed = safeParseJson(await fs.promises.readFile(filePath, "utf-8"));
|
|
64
|
-
if (parsed == null) return {
|
|
65
|
-
value: fallback,
|
|
66
|
-
exists: true
|
|
67
|
-
};
|
|
68
|
-
return {
|
|
69
|
-
value: parsed,
|
|
70
|
-
exists: true
|
|
71
|
-
};
|
|
72
|
-
} catch (err) {
|
|
73
|
-
if (err.code === "ENOENT") return {
|
|
74
|
-
value: fallback,
|
|
75
|
-
exists: false
|
|
76
|
-
};
|
|
77
|
-
return {
|
|
78
|
-
value: fallback,
|
|
79
|
-
exists: false
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async function writeJsonFileAtomically(filePath, value) {
|
|
84
|
-
await writeJsonAtomic(filePath, value, {
|
|
85
|
-
mode: 384,
|
|
86
|
-
trailingNewline: true,
|
|
87
|
-
ensureDirMode: 448
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
//#region src/pairing/pairing-store.ts
|
|
93
|
-
const PAIRING_CODE_LENGTH = 8;
|
|
94
|
-
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
95
|
-
const PAIRING_PENDING_TTL_MS = 3600 * 1e3;
|
|
96
|
-
const PAIRING_PENDING_MAX = 3;
|
|
97
|
-
const PAIRING_STORE_LOCK_OPTIONS = {
|
|
98
|
-
retries: {
|
|
99
|
-
retries: 10,
|
|
100
|
-
factor: 2,
|
|
101
|
-
minTimeout: 100,
|
|
102
|
-
maxTimeout: 1e4,
|
|
103
|
-
randomize: true
|
|
104
|
-
},
|
|
105
|
-
stale: 3e4
|
|
106
|
-
};
|
|
107
|
-
const allowFromReadCache = /* @__PURE__ */ new Map();
|
|
108
|
-
function resolveCredentialsDir(env = process.env) {
|
|
109
|
-
return resolveOAuthDir(env, resolveStateDir(env, () => resolveRequiredHomeDir(env, os.homedir)));
|
|
110
|
-
}
|
|
111
|
-
/** Sanitize channel ID for use in filenames (prevent path traversal). */
|
|
112
|
-
function safeChannelKey(channel) {
|
|
113
|
-
const raw = String(channel).trim().toLowerCase();
|
|
114
|
-
if (!raw) throw new Error("invalid pairing channel");
|
|
115
|
-
const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
|
|
116
|
-
if (!safe || safe === "_") throw new Error("invalid pairing channel");
|
|
117
|
-
return safe;
|
|
118
|
-
}
|
|
119
|
-
function resolvePairingPath(channel, env = process.env) {
|
|
120
|
-
return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-pairing.json`);
|
|
121
|
-
}
|
|
122
|
-
function safeAccountKey(accountId) {
|
|
123
|
-
const raw = String(accountId).trim().toLowerCase();
|
|
124
|
-
if (!raw) throw new Error("invalid pairing account id");
|
|
125
|
-
const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
|
|
126
|
-
if (!safe || safe === "_") throw new Error("invalid pairing account id");
|
|
127
|
-
return safe;
|
|
128
|
-
}
|
|
129
|
-
function resolveAllowFromPath(channel, env = process.env, accountId) {
|
|
130
|
-
const base = safeChannelKey(channel);
|
|
131
|
-
const normalizedAccountId = typeof accountId === "string" ? accountId.trim() : "";
|
|
132
|
-
if (!normalizedAccountId) return path.join(resolveCredentialsDir(env), `${base}-allowFrom.json`);
|
|
133
|
-
return path.join(resolveCredentialsDir(env), `${base}-${safeAccountKey(normalizedAccountId)}-allowFrom.json`);
|
|
134
|
-
}
|
|
135
|
-
async function readJsonFile(filePath, fallback) {
|
|
136
|
-
return await readJsonFileWithFallback(filePath, fallback);
|
|
137
|
-
}
|
|
138
|
-
async function writeJsonFile(filePath, value) {
|
|
139
|
-
await writeJsonFileAtomically(filePath, value);
|
|
140
|
-
}
|
|
141
|
-
async function readPairingRequests(filePath) {
|
|
142
|
-
const { value } = await readJsonFile(filePath, {
|
|
143
|
-
version: 1,
|
|
144
|
-
requests: []
|
|
145
|
-
});
|
|
146
|
-
return Array.isArray(value.requests) ? value.requests : [];
|
|
147
|
-
}
|
|
148
|
-
async function ensureJsonFile(filePath, fallback) {
|
|
149
|
-
try {
|
|
150
|
-
await fs.promises.access(filePath);
|
|
151
|
-
} catch {
|
|
152
|
-
await writeJsonFile(filePath, fallback);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async function withFileLock(filePath, fallback, fn) {
|
|
156
|
-
await ensureJsonFile(filePath, fallback);
|
|
157
|
-
return await withFileLock$1(filePath, PAIRING_STORE_LOCK_OPTIONS, async () => {
|
|
158
|
-
return await fn();
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
function parseTimestamp(value) {
|
|
162
|
-
if (!value) return null;
|
|
163
|
-
const parsed = Date.parse(value);
|
|
164
|
-
if (!Number.isFinite(parsed)) return null;
|
|
165
|
-
return parsed;
|
|
166
|
-
}
|
|
167
|
-
function isExpired(entry, nowMs) {
|
|
168
|
-
const createdAt = parseTimestamp(entry.createdAt);
|
|
169
|
-
if (!createdAt) return true;
|
|
170
|
-
return nowMs - createdAt > PAIRING_PENDING_TTL_MS;
|
|
171
|
-
}
|
|
172
|
-
function pruneExpiredRequests(reqs, nowMs) {
|
|
173
|
-
const kept = [];
|
|
174
|
-
let removed = false;
|
|
175
|
-
for (const req of reqs) {
|
|
176
|
-
if (isExpired(req, nowMs)) {
|
|
177
|
-
removed = true;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
kept.push(req);
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
requests: kept,
|
|
184
|
-
removed
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function resolveLastSeenAt(entry) {
|
|
188
|
-
return parseTimestamp(entry.lastSeenAt) ?? parseTimestamp(entry.createdAt) ?? 0;
|
|
189
|
-
}
|
|
190
|
-
function pruneExcessRequests(reqs, maxPending) {
|
|
191
|
-
if (maxPending <= 0 || reqs.length <= maxPending) return {
|
|
192
|
-
requests: reqs,
|
|
193
|
-
removed: false
|
|
194
|
-
};
|
|
195
|
-
return {
|
|
196
|
-
requests: reqs.slice().toSorted((a, b) => resolveLastSeenAt(a) - resolveLastSeenAt(b)).slice(-maxPending),
|
|
197
|
-
removed: true
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function randomCode() {
|
|
201
|
-
let out = "";
|
|
202
|
-
for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
|
|
203
|
-
const idx = crypto.randomInt(0, 32);
|
|
204
|
-
out += PAIRING_CODE_ALPHABET[idx];
|
|
205
|
-
}
|
|
206
|
-
return out;
|
|
207
|
-
}
|
|
208
|
-
function generateUniqueCode(existing) {
|
|
209
|
-
for (let attempt = 0; attempt < 500; attempt += 1) {
|
|
210
|
-
const code = randomCode();
|
|
211
|
-
if (!existing.has(code)) return code;
|
|
212
|
-
}
|
|
213
|
-
throw new Error("failed to generate unique pairing code");
|
|
214
|
-
}
|
|
215
|
-
function normalizePairingAccountId(accountId) {
|
|
216
|
-
return accountId?.trim().toLowerCase() || "";
|
|
217
|
-
}
|
|
218
|
-
function requestMatchesAccountId(entry, normalizedAccountId) {
|
|
219
|
-
if (!normalizedAccountId) return true;
|
|
220
|
-
return String(entry.meta?.accountId ?? "").trim().toLowerCase() === normalizedAccountId;
|
|
221
|
-
}
|
|
222
|
-
function shouldIncludeLegacyAllowFromEntries(normalizedAccountId) {
|
|
223
|
-
return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID;
|
|
224
|
-
}
|
|
225
|
-
function resolveAllowFromAccountId(accountId) {
|
|
226
|
-
return normalizePairingAccountId(accountId) || DEFAULT_ACCOUNT_ID;
|
|
227
|
-
}
|
|
228
|
-
function normalizeId(value) {
|
|
229
|
-
return String(value).trim();
|
|
230
|
-
}
|
|
231
|
-
function normalizeAllowEntry(channel, entry) {
|
|
232
|
-
const trimmed = entry.trim();
|
|
233
|
-
if (!trimmed) return "";
|
|
234
|
-
if (trimmed === "*") return "";
|
|
235
|
-
const adapter = getPairingAdapter(channel);
|
|
236
|
-
const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed;
|
|
237
|
-
return String(normalized).trim();
|
|
238
|
-
}
|
|
239
|
-
function normalizeAllowFromList(channel, store) {
|
|
240
|
-
return dedupePreserveOrder((Array.isArray(store.allowFrom) ? store.allowFrom : []).map((v) => normalizeAllowEntry(channel, String(v))).filter(Boolean));
|
|
241
|
-
}
|
|
242
|
-
function normalizeAllowFromInput(channel, entry) {
|
|
243
|
-
return normalizeAllowEntry(channel, normalizeId(entry));
|
|
244
|
-
}
|
|
245
|
-
function dedupePreserveOrder(entries) {
|
|
246
|
-
const seen = /* @__PURE__ */ new Set();
|
|
247
|
-
const out = [];
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
const normalized = String(entry).trim();
|
|
250
|
-
if (!normalized || seen.has(normalized)) continue;
|
|
251
|
-
seen.add(normalized);
|
|
252
|
-
out.push(normalized);
|
|
253
|
-
}
|
|
254
|
-
return out;
|
|
255
|
-
}
|
|
256
|
-
async function readAllowFromStateForPath(channel, filePath) {
|
|
257
|
-
return (await readAllowFromStateForPathWithExists(channel, filePath)).entries;
|
|
258
|
-
}
|
|
259
|
-
function cloneAllowFromCacheEntry(entry) {
|
|
260
|
-
return {
|
|
261
|
-
exists: entry.exists,
|
|
262
|
-
mtimeMs: entry.mtimeMs,
|
|
263
|
-
size: entry.size,
|
|
264
|
-
entries: entry.entries.slice()
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function setAllowFromReadCache(filePath, entry) {
|
|
268
|
-
allowFromReadCache.set(filePath, cloneAllowFromCacheEntry(entry));
|
|
269
|
-
}
|
|
270
|
-
function resolveAllowFromReadCacheHit(params) {
|
|
271
|
-
const cached = allowFromReadCache.get(params.filePath);
|
|
272
|
-
if (!cached) return null;
|
|
273
|
-
if (cached.exists !== params.exists) return null;
|
|
274
|
-
if (!params.exists) return cloneAllowFromCacheEntry(cached);
|
|
275
|
-
if (cached.mtimeMs !== params.mtimeMs || cached.size !== params.size) return null;
|
|
276
|
-
return cloneAllowFromCacheEntry(cached);
|
|
277
|
-
}
|
|
278
|
-
function resolveAllowFromReadCacheOrMissing(filePath, stat) {
|
|
279
|
-
const cached = resolveAllowFromReadCacheHit({
|
|
280
|
-
filePath,
|
|
281
|
-
exists: Boolean(stat),
|
|
282
|
-
mtimeMs: stat?.mtimeMs ?? null,
|
|
283
|
-
size: stat?.size ?? null
|
|
284
|
-
});
|
|
285
|
-
if (cached) return {
|
|
286
|
-
entries: cached.entries,
|
|
287
|
-
exists: cached.exists
|
|
288
|
-
};
|
|
289
|
-
if (!stat) {
|
|
290
|
-
setAllowFromReadCache(filePath, {
|
|
291
|
-
exists: false,
|
|
292
|
-
mtimeMs: null,
|
|
293
|
-
size: null,
|
|
294
|
-
entries: []
|
|
295
|
-
});
|
|
296
|
-
return {
|
|
297
|
-
entries: [],
|
|
298
|
-
exists: false
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
async function readAllowFromStateForPathWithExists(channel, filePath) {
|
|
304
|
-
let stat = null;
|
|
305
|
-
try {
|
|
306
|
-
stat = await fs.promises.stat(filePath);
|
|
307
|
-
} catch (err) {
|
|
308
|
-
if (err.code !== "ENOENT") throw err;
|
|
309
|
-
}
|
|
310
|
-
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
|
|
311
|
-
if (cachedOrMissing) return cachedOrMissing;
|
|
312
|
-
if (!stat) return {
|
|
313
|
-
entries: [],
|
|
314
|
-
exists: false
|
|
315
|
-
};
|
|
316
|
-
const { value, exists } = await readJsonFile(filePath, {
|
|
317
|
-
version: 1,
|
|
318
|
-
allowFrom: []
|
|
319
|
-
});
|
|
320
|
-
const entries = normalizeAllowFromList(channel, value);
|
|
321
|
-
setAllowFromReadCache(filePath, {
|
|
322
|
-
exists,
|
|
323
|
-
mtimeMs: stat.mtimeMs,
|
|
324
|
-
size: stat.size,
|
|
325
|
-
entries
|
|
326
|
-
});
|
|
327
|
-
return {
|
|
328
|
-
entries,
|
|
329
|
-
exists
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function readAllowFromStateForPathSync(channel, filePath) {
|
|
333
|
-
return readAllowFromStateForPathSyncWithExists(channel, filePath).entries;
|
|
334
|
-
}
|
|
335
|
-
function readAllowFromStateForPathSyncWithExists(channel, filePath) {
|
|
336
|
-
let stat = null;
|
|
337
|
-
try {
|
|
338
|
-
stat = fs.statSync(filePath);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
if (err.code !== "ENOENT") return {
|
|
341
|
-
entries: [],
|
|
342
|
-
exists: false
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
|
|
346
|
-
if (cachedOrMissing) return cachedOrMissing;
|
|
347
|
-
if (!stat) return {
|
|
348
|
-
entries: [],
|
|
349
|
-
exists: false
|
|
350
|
-
};
|
|
351
|
-
let raw = "";
|
|
352
|
-
try {
|
|
353
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
354
|
-
} catch (err) {
|
|
355
|
-
if (err.code === "ENOENT") return {
|
|
356
|
-
entries: [],
|
|
357
|
-
exists: false
|
|
358
|
-
};
|
|
359
|
-
return {
|
|
360
|
-
entries: [],
|
|
361
|
-
exists: false
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
const entries = normalizeAllowFromList(channel, JSON.parse(raw));
|
|
366
|
-
setAllowFromReadCache(filePath, {
|
|
367
|
-
exists: true,
|
|
368
|
-
mtimeMs: stat.mtimeMs,
|
|
369
|
-
size: stat.size,
|
|
370
|
-
entries
|
|
371
|
-
});
|
|
372
|
-
return {
|
|
373
|
-
entries,
|
|
374
|
-
exists: true
|
|
375
|
-
};
|
|
376
|
-
} catch {
|
|
377
|
-
setAllowFromReadCache(filePath, {
|
|
378
|
-
exists: true,
|
|
379
|
-
mtimeMs: stat.mtimeMs,
|
|
380
|
-
size: stat.size,
|
|
381
|
-
entries: []
|
|
382
|
-
});
|
|
383
|
-
return {
|
|
384
|
-
entries: [],
|
|
385
|
-
exists: true
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
async function readAllowFromState(params) {
|
|
390
|
-
const { value } = await readJsonFile(params.filePath, {
|
|
391
|
-
version: 1,
|
|
392
|
-
allowFrom: []
|
|
393
|
-
});
|
|
394
|
-
return {
|
|
395
|
-
current: normalizeAllowFromList(params.channel, value),
|
|
396
|
-
normalized: normalizeAllowFromInput(params.channel, params.entry) || null
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
async function writeAllowFromState(filePath, allowFrom) {
|
|
400
|
-
await writeJsonFile(filePath, {
|
|
401
|
-
version: 1,
|
|
402
|
-
allowFrom
|
|
403
|
-
});
|
|
404
|
-
let stat = null;
|
|
405
|
-
try {
|
|
406
|
-
stat = await fs.promises.stat(filePath);
|
|
407
|
-
} catch {}
|
|
408
|
-
setAllowFromReadCache(filePath, {
|
|
409
|
-
exists: true,
|
|
410
|
-
mtimeMs: stat?.mtimeMs ?? null,
|
|
411
|
-
size: stat?.size ?? null,
|
|
412
|
-
entries: allowFrom.slice()
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
async function readNonDefaultAccountAllowFrom(params) {
|
|
416
|
-
const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
|
|
417
|
-
return await readAllowFromStateForPath(params.channel, scopedPath);
|
|
418
|
-
}
|
|
419
|
-
function readNonDefaultAccountAllowFromSync(params) {
|
|
420
|
-
const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
|
|
421
|
-
return readAllowFromStateForPathSync(params.channel, scopedPath);
|
|
422
|
-
}
|
|
423
|
-
async function updateAllowFromStoreEntry(params) {
|
|
424
|
-
const env = params.env ?? process.env;
|
|
425
|
-
const filePath = resolveAllowFromPath(params.channel, env, params.accountId);
|
|
426
|
-
return await withFileLock(filePath, {
|
|
427
|
-
version: 1,
|
|
428
|
-
allowFrom: []
|
|
429
|
-
}, async () => {
|
|
430
|
-
const { current, normalized } = await readAllowFromState({
|
|
431
|
-
channel: params.channel,
|
|
432
|
-
entry: params.entry,
|
|
433
|
-
filePath
|
|
434
|
-
});
|
|
435
|
-
if (!normalized) return {
|
|
436
|
-
changed: false,
|
|
437
|
-
allowFrom: current
|
|
438
|
-
};
|
|
439
|
-
const next = params.apply(current, normalized);
|
|
440
|
-
if (!next) return {
|
|
441
|
-
changed: false,
|
|
442
|
-
allowFrom: current
|
|
443
|
-
};
|
|
444
|
-
await writeAllowFromState(filePath, next);
|
|
445
|
-
return {
|
|
446
|
-
changed: true,
|
|
447
|
-
allowFrom: next
|
|
448
|
-
};
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
async function readChannelAllowFromStore(channel, env = process.env, accountId) {
|
|
452
|
-
const resolvedAccountId = resolveAllowFromAccountId(accountId);
|
|
453
|
-
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return await readNonDefaultAccountAllowFrom({
|
|
454
|
-
channel,
|
|
455
|
-
env,
|
|
456
|
-
accountId: resolvedAccountId
|
|
457
|
-
});
|
|
458
|
-
const scopedEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
|
|
459
|
-
const legacyEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env));
|
|
460
|
-
return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
|
|
461
|
-
}
|
|
462
|
-
function readChannelAllowFromStoreSync(channel, env = process.env, accountId) {
|
|
463
|
-
const resolvedAccountId = resolveAllowFromAccountId(accountId);
|
|
464
|
-
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return readNonDefaultAccountAllowFromSync({
|
|
465
|
-
channel,
|
|
466
|
-
env,
|
|
467
|
-
accountId: resolvedAccountId
|
|
468
|
-
});
|
|
469
|
-
const scopedEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
|
|
470
|
-
const legacyEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env));
|
|
471
|
-
return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
|
|
472
|
-
}
|
|
473
|
-
async function updateChannelAllowFromStore(params) {
|
|
474
|
-
return await updateAllowFromStoreEntry({
|
|
475
|
-
channel: params.channel,
|
|
476
|
-
entry: params.entry,
|
|
477
|
-
accountId: params.accountId,
|
|
478
|
-
env: params.env,
|
|
479
|
-
apply: params.apply
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
async function mutateChannelAllowFromStoreEntry(params, apply) {
|
|
483
|
-
return await updateChannelAllowFromStore({
|
|
484
|
-
...params,
|
|
485
|
-
apply
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
async function addChannelAllowFromStoreEntry(params) {
|
|
489
|
-
return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
|
|
490
|
-
if (current.includes(normalized)) return null;
|
|
491
|
-
return [...current, normalized];
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
async function removeChannelAllowFromStoreEntry(params) {
|
|
495
|
-
return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
|
|
496
|
-
const next = current.filter((entry) => entry !== normalized);
|
|
497
|
-
if (next.length === current.length) return null;
|
|
498
|
-
return next;
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
async function upsertChannelPairingRequest(params) {
|
|
502
|
-
const env = params.env ?? process.env;
|
|
503
|
-
const filePath = resolvePairingPath(params.channel, env);
|
|
504
|
-
return await withFileLock(filePath, {
|
|
505
|
-
version: 1,
|
|
506
|
-
requests: []
|
|
507
|
-
}, async () => {
|
|
508
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
509
|
-
const nowMs = Date.now();
|
|
510
|
-
const id = normalizeId(params.id);
|
|
511
|
-
const normalizedAccountId = normalizePairingAccountId(params.accountId) || DEFAULT_ACCOUNT_ID;
|
|
512
|
-
const meta = {
|
|
513
|
-
...params.meta && typeof params.meta === "object" ? Object.fromEntries(Object.entries(params.meta).map(([k, v]) => [k, String(v ?? "").trim()]).filter(([_, v]) => Boolean(v))) : void 0,
|
|
514
|
-
accountId: normalizedAccountId
|
|
515
|
-
};
|
|
516
|
-
let reqs = await readPairingRequests(filePath);
|
|
517
|
-
const { requests: prunedExpired, removed: expiredRemoved } = pruneExpiredRequests(reqs, nowMs);
|
|
518
|
-
reqs = prunedExpired;
|
|
519
|
-
const normalizedMatchingAccountId = normalizedAccountId;
|
|
520
|
-
const existingIdx = reqs.findIndex((r) => {
|
|
521
|
-
if (r.id !== id) return false;
|
|
522
|
-
return requestMatchesAccountId(r, normalizedMatchingAccountId);
|
|
523
|
-
});
|
|
524
|
-
const existingCodes = new Set(reqs.map((req) => String(req.code ?? "").trim().toUpperCase()));
|
|
525
|
-
if (existingIdx >= 0) {
|
|
526
|
-
const existing = reqs[existingIdx];
|
|
527
|
-
const code = (existing && typeof existing.code === "string" ? existing.code.trim() : "") || generateUniqueCode(existingCodes);
|
|
528
|
-
const next = {
|
|
529
|
-
id,
|
|
530
|
-
code,
|
|
531
|
-
createdAt: existing?.createdAt ?? now,
|
|
532
|
-
lastSeenAt: now,
|
|
533
|
-
meta: meta ?? existing?.meta
|
|
534
|
-
};
|
|
535
|
-
reqs[existingIdx] = next;
|
|
536
|
-
const { requests: capped } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
|
|
537
|
-
await writeJsonFile(filePath, {
|
|
538
|
-
version: 1,
|
|
539
|
-
requests: capped
|
|
540
|
-
});
|
|
541
|
-
return {
|
|
542
|
-
code,
|
|
543
|
-
created: false
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
const { requests: capped, removed: cappedRemoved } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
|
|
547
|
-
reqs = capped;
|
|
548
|
-
if (PAIRING_PENDING_MAX > 0 && reqs.length >= PAIRING_PENDING_MAX) {
|
|
549
|
-
if (expiredRemoved || cappedRemoved) await writeJsonFile(filePath, {
|
|
550
|
-
version: 1,
|
|
551
|
-
requests: reqs
|
|
552
|
-
});
|
|
553
|
-
return {
|
|
554
|
-
code: "",
|
|
555
|
-
created: false
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
const code = generateUniqueCode(existingCodes);
|
|
559
|
-
const next = {
|
|
560
|
-
id,
|
|
561
|
-
code,
|
|
562
|
-
createdAt: now,
|
|
563
|
-
lastSeenAt: now,
|
|
564
|
-
...meta ? { meta } : {}
|
|
565
|
-
};
|
|
566
|
-
await writeJsonFile(filePath, {
|
|
567
|
-
version: 1,
|
|
568
|
-
requests: [...reqs, next]
|
|
569
|
-
});
|
|
570
|
-
return {
|
|
571
|
-
code,
|
|
572
|
-
created: true
|
|
573
|
-
};
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
//#endregion
|
|
578
|
-
//#region src/infra/wsl.ts
|
|
579
|
-
function isWSLEnv() {
|
|
580
|
-
if (process.env.WSL_INTEROP || process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
|
|
581
|
-
return false;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Synchronously check if running in WSL.
|
|
585
|
-
* Checks env vars first, then /proc/version.
|
|
586
|
-
*/
|
|
587
|
-
function isWSLSync() {
|
|
588
|
-
if (process.platform !== "linux") return false;
|
|
589
|
-
if (isWSLEnv()) return true;
|
|
590
|
-
try {
|
|
591
|
-
const release = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
592
|
-
return release.includes("microsoft") || release.includes("wsl");
|
|
593
|
-
} catch {
|
|
594
|
-
return false;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Synchronously check if running in WSL2.
|
|
599
|
-
*/
|
|
600
|
-
function isWSL2Sync() {
|
|
601
|
-
if (!isWSLSync()) return false;
|
|
602
|
-
try {
|
|
603
|
-
const version = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
604
|
-
return version.includes("wsl2") || version.includes("microsoft-standard");
|
|
605
|
-
} catch {
|
|
606
|
-
return false;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
//#endregion
|
|
611
|
-
//#region src/channels/location.ts
|
|
612
|
-
function resolveLocation(location) {
|
|
613
|
-
const source = location.source ?? (location.isLive ? "live" : location.name || location.address ? "place" : "pin");
|
|
614
|
-
const isLive = Boolean(location.isLive ?? source === "live");
|
|
615
|
-
return {
|
|
616
|
-
...location,
|
|
617
|
-
source,
|
|
618
|
-
isLive
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
function formatAccuracy(accuracy) {
|
|
622
|
-
if (!Number.isFinite(accuracy)) return "";
|
|
623
|
-
return ` ±${Math.round(accuracy ?? 0)}m`;
|
|
624
|
-
}
|
|
625
|
-
function formatCoords(latitude, longitude) {
|
|
626
|
-
return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
|
|
627
|
-
}
|
|
628
|
-
function formatLocationText(location) {
|
|
629
|
-
const resolved = resolveLocation(location);
|
|
630
|
-
const coords = formatCoords(resolved.latitude, resolved.longitude);
|
|
631
|
-
const accuracy = formatAccuracy(resolved.accuracy);
|
|
632
|
-
const caption = resolved.caption?.trim();
|
|
633
|
-
let header = "";
|
|
634
|
-
if (resolved.source === "live" || resolved.isLive) header = `🛰 Live location: ${coords}${accuracy}`;
|
|
635
|
-
else if (resolved.name || resolved.address) header = `📍 ${[resolved.name, resolved.address].filter(Boolean).join(" — ")} (${coords}${accuracy})`;
|
|
636
|
-
else header = `📍 ${coords}${accuracy}`;
|
|
637
|
-
return caption ? `${header}\n${caption}` : header;
|
|
638
|
-
}
|
|
639
|
-
function toLocationContext(location) {
|
|
640
|
-
const resolved = resolveLocation(location);
|
|
641
|
-
return {
|
|
642
|
-
LocationLat: resolved.latitude,
|
|
643
|
-
LocationLon: resolved.longitude,
|
|
644
|
-
LocationAccuracy: resolved.accuracy,
|
|
645
|
-
LocationName: resolved.name,
|
|
646
|
-
LocationAddress: resolved.address,
|
|
647
|
-
LocationSource: resolved.source,
|
|
648
|
-
LocationIsLive: resolved.isLive
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
//#endregion
|
|
653
|
-
//#region src/media/audio.ts
|
|
654
|
-
const TELEGRAM_VOICE_AUDIO_EXTENSIONS = new Set([
|
|
655
|
-
".oga",
|
|
656
|
-
".ogg",
|
|
657
|
-
".opus",
|
|
658
|
-
".mp3",
|
|
659
|
-
".m4a"
|
|
660
|
-
]);
|
|
661
|
-
/**
|
|
662
|
-
* MIME types compatible with voice messages.
|
|
663
|
-
* Telegram sendVoice supports OGG/Opus, MP3, and M4A.
|
|
664
|
-
* https://core.telegram.org/bots/api#sendvoice
|
|
665
|
-
*/
|
|
666
|
-
const TELEGRAM_VOICE_MIME_TYPES = new Set([
|
|
667
|
-
"audio/ogg",
|
|
668
|
-
"audio/opus",
|
|
669
|
-
"audio/mpeg",
|
|
670
|
-
"audio/mp3",
|
|
671
|
-
"audio/mp4",
|
|
672
|
-
"audio/x-m4a",
|
|
673
|
-
"audio/m4a"
|
|
674
|
-
]);
|
|
675
|
-
function isTelegramVoiceCompatibleAudio(opts) {
|
|
676
|
-
const mime = normalizeMimeType(opts.contentType);
|
|
677
|
-
if (mime && TELEGRAM_VOICE_MIME_TYPES.has(mime)) return true;
|
|
678
|
-
const fileName = opts.fileName?.trim();
|
|
679
|
-
if (!fileName) return false;
|
|
680
|
-
const ext = getFileExtension(fileName);
|
|
681
|
-
if (!ext) return false;
|
|
682
|
-
return TELEGRAM_VOICE_AUDIO_EXTENSIONS.has(ext);
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Backward-compatible alias used across plugin/runtime call sites.
|
|
686
|
-
* Keeps existing behavior while making Telegram-specific policy explicit.
|
|
687
|
-
*/
|
|
688
|
-
function isVoiceCompatibleAudio(opts) {
|
|
689
|
-
return isTelegramVoiceCompatibleAudio(opts);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
//#endregion
|
|
693
|
-
//#region src/telegram/bot-access.ts
|
|
694
|
-
const warnedInvalidEntries = /* @__PURE__ */ new Set();
|
|
695
|
-
const log$1 = createSubsystemLogger("telegram/bot-access");
|
|
696
|
-
function warnInvalidAllowFromEntries(entries) {
|
|
697
|
-
if (process.env.VITEST || false) return;
|
|
698
|
-
for (const entry of entries) {
|
|
699
|
-
if (warnedInvalidEntries.has(entry)) continue;
|
|
700
|
-
warnedInvalidEntries.add(entry);
|
|
701
|
-
log$1.warn([
|
|
702
|
-
"Invalid allowFrom entry:",
|
|
703
|
-
JSON.stringify(entry),
|
|
704
|
-
"- allowFrom/groupAllowFrom authorization requires numeric Telegram sender IDs only.",
|
|
705
|
-
"If you had \"@username\" entries, re-run onboarding (it resolves @username to IDs) or replace them manually."
|
|
706
|
-
].join(" "));
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
const normalizeAllowFrom = (list) => {
|
|
710
|
-
const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
|
|
711
|
-
const hasWildcard = entries.includes("*");
|
|
712
|
-
const normalized = entries.filter((value) => value !== "*").map((value) => value.replace(/^(telegram|tg):/i, ""));
|
|
713
|
-
const invalidEntries = normalized.filter((value) => !/^\d+$/.test(value));
|
|
714
|
-
if (invalidEntries.length > 0) warnInvalidAllowFromEntries([...new Set(invalidEntries)]);
|
|
715
|
-
return {
|
|
716
|
-
entries: normalized.filter((value) => /^\d+$/.test(value)),
|
|
717
|
-
hasWildcard,
|
|
718
|
-
hasEntries: entries.length > 0,
|
|
719
|
-
invalidEntries
|
|
720
|
-
};
|
|
721
|
-
};
|
|
722
|
-
const normalizeDmAllowFromWithStore = (params) => normalizeAllowFrom(mergeDmAllowFromSources(params));
|
|
723
|
-
const isSenderAllowed = (params) => {
|
|
724
|
-
const { allow, senderId } = params;
|
|
725
|
-
return isSenderIdAllowed(allow, senderId, true);
|
|
726
|
-
};
|
|
727
|
-
const resolveSenderAllowMatch = (params) => {
|
|
728
|
-
const { allow, senderId } = params;
|
|
729
|
-
if (allow.hasWildcard) return {
|
|
730
|
-
allowed: true,
|
|
731
|
-
matchKey: "*",
|
|
732
|
-
matchSource: "wildcard"
|
|
733
|
-
};
|
|
734
|
-
if (!allow.hasEntries) return { allowed: false };
|
|
735
|
-
if (senderId && allow.entries.includes(senderId)) return {
|
|
736
|
-
allowed: true,
|
|
737
|
-
matchKey: senderId,
|
|
738
|
-
matchSource: "id"
|
|
739
|
-
};
|
|
740
|
-
return { allowed: false };
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
//#endregion
|
|
744
|
-
//#region src/telegram/bot/helpers.ts
|
|
745
|
-
const TELEGRAM_GENERAL_TOPIC_ID = 1;
|
|
746
|
-
async function resolveTelegramGroupAllowFromContext(params) {
|
|
747
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
748
|
-
const threadSpec = resolveTelegramThreadSpec({
|
|
749
|
-
isGroup: params.isGroup ?? false,
|
|
750
|
-
isForum: params.isForum,
|
|
751
|
-
messageThreadId: params.messageThreadId
|
|
752
|
-
});
|
|
753
|
-
const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : void 0;
|
|
754
|
-
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : void 0;
|
|
755
|
-
const threadIdForConfig = resolvedThreadId ?? dmThreadId;
|
|
756
|
-
const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, accountId).catch(() => []);
|
|
757
|
-
const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig(params.chatId, threadIdForConfig);
|
|
758
|
-
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
|
759
|
-
return {
|
|
760
|
-
resolvedThreadId,
|
|
761
|
-
dmThreadId,
|
|
762
|
-
storeAllowFrom,
|
|
763
|
-
groupConfig,
|
|
764
|
-
topicConfig,
|
|
765
|
-
groupAllowOverride,
|
|
766
|
-
effectiveGroupAllow: normalizeAllowFrom(groupAllowOverride ?? params.groupAllowFrom),
|
|
767
|
-
hasGroupAllowOverride: typeof groupAllowOverride !== "undefined"
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Resolve the thread ID for Telegram forum topics.
|
|
772
|
-
* For non-forum groups, returns undefined even if messageThreadId is present
|
|
773
|
-
* (reply threads in regular groups should not create separate sessions).
|
|
774
|
-
* For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
|
|
775
|
-
*/
|
|
776
|
-
function resolveTelegramForumThreadId(params) {
|
|
777
|
-
if (!params.isForum) return;
|
|
778
|
-
if (params.messageThreadId == null) return TELEGRAM_GENERAL_TOPIC_ID;
|
|
779
|
-
return params.messageThreadId;
|
|
780
|
-
}
|
|
781
|
-
function resolveTelegramThreadSpec(params) {
|
|
782
|
-
if (params.isGroup) return {
|
|
783
|
-
id: resolveTelegramForumThreadId({
|
|
784
|
-
isForum: params.isForum,
|
|
785
|
-
messageThreadId: params.messageThreadId
|
|
786
|
-
}),
|
|
787
|
-
scope: params.isForum ? "forum" : "none"
|
|
788
|
-
};
|
|
789
|
-
if (params.messageThreadId == null) return { scope: "dm" };
|
|
790
|
-
return {
|
|
791
|
-
id: params.messageThreadId,
|
|
792
|
-
scope: "dm"
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Build thread params for Telegram API calls (messages, media).
|
|
797
|
-
*
|
|
798
|
-
* IMPORTANT: Thread IDs behave differently based on chat type:
|
|
799
|
-
* - DMs (private chats): Include message_thread_id when present (DM topics)
|
|
800
|
-
* - Forum topics: Skip thread_id=1 (General topic), include others
|
|
801
|
-
* - Regular groups: Thread IDs are ignored by Telegram
|
|
802
|
-
*
|
|
803
|
-
* General forum topic (id=1) must be treated like a regular supergroup send:
|
|
804
|
-
* Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
|
|
805
|
-
*
|
|
806
|
-
* @param thread - Thread specification with ID and scope
|
|
807
|
-
* @returns API params object or undefined if thread_id should be omitted
|
|
808
|
-
*/
|
|
809
|
-
function buildTelegramThreadParams(thread) {
|
|
810
|
-
if (thread?.id == null) return;
|
|
811
|
-
const normalized = Math.trunc(thread.id);
|
|
812
|
-
if (thread.scope === "dm") return normalized > 0 ? { message_thread_id: normalized } : void 0;
|
|
813
|
-
if (normalized === TELEGRAM_GENERAL_TOPIC_ID) return;
|
|
814
|
-
return { message_thread_id: normalized };
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Build thread params for typing indicators (sendChatAction).
|
|
818
|
-
* Empirically, General topic (id=1) needs message_thread_id for typing to appear.
|
|
819
|
-
*/
|
|
820
|
-
function buildTypingThreadParams(messageThreadId) {
|
|
821
|
-
if (messageThreadId == null) return;
|
|
822
|
-
return { message_thread_id: Math.trunc(messageThreadId) };
|
|
823
|
-
}
|
|
824
|
-
function resolveTelegramStreamMode(telegramCfg) {
|
|
825
|
-
return resolveTelegramPreviewStreamMode(telegramCfg);
|
|
826
|
-
}
|
|
827
|
-
function buildTelegramGroupPeerId(chatId, messageThreadId) {
|
|
828
|
-
return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
|
-
* Resolve the direct-message peer identifier for Telegram routing/session keys.
|
|
832
|
-
*
|
|
833
|
-
* In some Telegram DM deliveries (for example certain business/chat bridge flows),
|
|
834
|
-
* `chat.id` can differ from the actual sender user id. Prefer sender id when present
|
|
835
|
-
* so per-peer DM scopes isolate users correctly.
|
|
836
|
-
*/
|
|
837
|
-
function resolveTelegramDirectPeerId(params) {
|
|
838
|
-
const senderId = params.senderId != null ? String(params.senderId).trim() : "";
|
|
839
|
-
if (senderId) return senderId;
|
|
840
|
-
return String(params.chatId);
|
|
841
|
-
}
|
|
842
|
-
function buildTelegramGroupFrom(chatId, messageThreadId) {
|
|
843
|
-
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Build parentPeer for forum topic binding inheritance.
|
|
847
|
-
* When a message comes from a forum topic, the peer ID includes the topic suffix
|
|
848
|
-
* (e.g., `-1001234567890:topic:99`). To allow bindings configured for the base
|
|
849
|
-
* group ID to match, we provide the parent group as `parentPeer` so the routing
|
|
850
|
-
* layer can fall back to it when the exact peer doesn't match.
|
|
851
|
-
*/
|
|
852
|
-
function buildTelegramParentPeer(params) {
|
|
853
|
-
if (!params.isGroup || params.resolvedThreadId == null) return;
|
|
854
|
-
return {
|
|
855
|
-
kind: "group",
|
|
856
|
-
id: String(params.chatId)
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
function buildSenderName(msg) {
|
|
860
|
-
return [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || msg.from?.username || void 0;
|
|
861
|
-
}
|
|
862
|
-
function resolveTelegramMediaPlaceholder(msg) {
|
|
863
|
-
if (!msg) return;
|
|
864
|
-
if (msg.photo) return "<media:image>";
|
|
865
|
-
if (msg.video || msg.video_note) return "<media:video>";
|
|
866
|
-
if (msg.audio || msg.voice) return "<media:audio>";
|
|
867
|
-
if (msg.document) return "<media:document>";
|
|
868
|
-
if (msg.sticker) return "<media:sticker>";
|
|
869
|
-
}
|
|
870
|
-
function buildSenderLabel(msg, senderId) {
|
|
871
|
-
const name = buildSenderName(msg);
|
|
872
|
-
const username = msg.from?.username ? `@${msg.from.username}` : void 0;
|
|
873
|
-
let label = name;
|
|
874
|
-
if (name && username) label = `${name} (${username})`;
|
|
875
|
-
else if (!name && username) label = username;
|
|
876
|
-
const fallbackId = (senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : void 0) ?? (msg.from?.id != null ? String(msg.from.id) : void 0);
|
|
877
|
-
const idPart = fallbackId ? `id:${fallbackId}` : void 0;
|
|
878
|
-
if (label && idPart) return `${label} ${idPart}`;
|
|
879
|
-
if (label) return label;
|
|
880
|
-
return idPart ?? "id:unknown";
|
|
881
|
-
}
|
|
882
|
-
function buildGroupLabel(msg, chatId, messageThreadId) {
|
|
883
|
-
const title = msg.chat?.title;
|
|
884
|
-
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
|
885
|
-
if (title) return `${title} id:${chatId}${topicSuffix}`;
|
|
886
|
-
return `group:${chatId}${topicSuffix}`;
|
|
887
|
-
}
|
|
888
|
-
function hasBotMention(msg, botUsername) {
|
|
889
|
-
if ((msg.text ?? msg.caption ?? "").toLowerCase().includes(`@${botUsername}`)) return true;
|
|
890
|
-
const entities = msg.entities ?? msg.caption_entities ?? [];
|
|
891
|
-
for (const ent of entities) {
|
|
892
|
-
if (ent.type !== "mention") continue;
|
|
893
|
-
if ((msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length).toLowerCase() === `@${botUsername}`) return true;
|
|
894
|
-
}
|
|
895
|
-
return false;
|
|
896
|
-
}
|
|
897
|
-
function expandTextLinks(text, entities) {
|
|
898
|
-
if (!text || !entities?.length) return text;
|
|
899
|
-
const textLinks = entities.filter((entity) => entity.type === "text_link" && Boolean(entity.url)).toSorted((a, b) => b.offset - a.offset);
|
|
900
|
-
if (textLinks.length === 0) return text;
|
|
901
|
-
let result = text;
|
|
902
|
-
for (const entity of textLinks) {
|
|
903
|
-
const markdown = `[${text.slice(entity.offset, entity.offset + entity.length)}](${entity.url})`;
|
|
904
|
-
result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
|
|
905
|
-
}
|
|
906
|
-
return result;
|
|
907
|
-
}
|
|
908
|
-
function resolveTelegramReplyId(raw) {
|
|
909
|
-
if (!raw) return;
|
|
910
|
-
const parsed = Number(raw);
|
|
911
|
-
if (!Number.isFinite(parsed)) return;
|
|
912
|
-
return parsed;
|
|
913
|
-
}
|
|
914
|
-
function describeReplyTarget(msg) {
|
|
915
|
-
const reply = msg.reply_to_message;
|
|
916
|
-
const externalReply = msg.external_reply;
|
|
917
|
-
const quoteText = msg.quote?.text ?? externalReply?.quote?.text;
|
|
918
|
-
let body = "";
|
|
919
|
-
let kind = "reply";
|
|
920
|
-
if (typeof quoteText === "string") {
|
|
921
|
-
body = quoteText.trim();
|
|
922
|
-
if (body) kind = "quote";
|
|
923
|
-
}
|
|
924
|
-
const replyLike = reply ?? externalReply;
|
|
925
|
-
if (!body && replyLike) {
|
|
926
|
-
body = (replyLike.text ?? replyLike.caption ?? "").trim();
|
|
927
|
-
if (!body) {
|
|
928
|
-
body = resolveTelegramMediaPlaceholder(replyLike) ?? "";
|
|
929
|
-
if (!body) {
|
|
930
|
-
const locationData = extractTelegramLocation(replyLike);
|
|
931
|
-
if (locationData) body = formatLocationText(locationData);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
if (!body) return null;
|
|
936
|
-
const senderLabel = (replyLike ? buildSenderName(replyLike) : void 0) ?? "unknown sender";
|
|
937
|
-
const forwardedFrom = replyLike?.forward_origin ? resolveForwardOrigin(replyLike.forward_origin) ?? void 0 : void 0;
|
|
938
|
-
return {
|
|
939
|
-
id: replyLike?.message_id ? String(replyLike.message_id) : void 0,
|
|
940
|
-
sender: senderLabel,
|
|
941
|
-
body,
|
|
942
|
-
kind,
|
|
943
|
-
forwardedFrom
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
function normalizeForwardedUserLabel(user) {
|
|
947
|
-
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
|
948
|
-
const username = user.username?.trim() || void 0;
|
|
949
|
-
const id = String(user.id);
|
|
950
|
-
return {
|
|
951
|
-
display: (name && username ? `${name} (@${username})` : name || (username ? `@${username}` : void 0)) || `user:${id}`,
|
|
952
|
-
name: name || void 0,
|
|
953
|
-
username,
|
|
954
|
-
id
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
function normalizeForwardedChatLabel(chat, fallbackKind) {
|
|
958
|
-
const title = chat.title?.trim() || void 0;
|
|
959
|
-
const username = chat.username?.trim() || void 0;
|
|
960
|
-
const id = String(chat.id);
|
|
961
|
-
return {
|
|
962
|
-
display: title || (username ? `@${username}` : void 0) || `${fallbackKind}:${id}`,
|
|
963
|
-
title,
|
|
964
|
-
username,
|
|
965
|
-
id
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
function buildForwardedContextFromUser(params) {
|
|
969
|
-
const { display, name, username, id } = normalizeForwardedUserLabel(params.user);
|
|
970
|
-
if (!display) return null;
|
|
971
|
-
return {
|
|
972
|
-
from: display,
|
|
973
|
-
date: params.date,
|
|
974
|
-
fromType: params.type,
|
|
975
|
-
fromId: id,
|
|
976
|
-
fromUsername: username,
|
|
977
|
-
fromTitle: name
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
function buildForwardedContextFromHiddenName(params) {
|
|
981
|
-
const trimmed = params.name?.trim();
|
|
982
|
-
if (!trimmed) return null;
|
|
983
|
-
return {
|
|
984
|
-
from: trimmed,
|
|
985
|
-
date: params.date,
|
|
986
|
-
fromType: params.type,
|
|
987
|
-
fromTitle: trimmed
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
function buildForwardedContextFromChat(params) {
|
|
991
|
-
const fallbackKind = params.type === "channel" ? "channel" : "chat";
|
|
992
|
-
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
|
|
993
|
-
if (!display) return null;
|
|
994
|
-
const signature = params.signature?.trim() || void 0;
|
|
995
|
-
const from = signature ? `${display} (${signature})` : display;
|
|
996
|
-
const chatType = params.chat.type?.trim() || void 0;
|
|
997
|
-
return {
|
|
998
|
-
from,
|
|
999
|
-
date: params.date,
|
|
1000
|
-
fromType: params.type,
|
|
1001
|
-
fromId: id,
|
|
1002
|
-
fromUsername: username,
|
|
1003
|
-
fromTitle: title,
|
|
1004
|
-
fromSignature: signature,
|
|
1005
|
-
fromChatType: chatType,
|
|
1006
|
-
fromMessageId: params.messageId
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
function resolveForwardOrigin(origin) {
|
|
1010
|
-
switch (origin.type) {
|
|
1011
|
-
case "user": return buildForwardedContextFromUser({
|
|
1012
|
-
user: origin.sender_user,
|
|
1013
|
-
date: origin.date,
|
|
1014
|
-
type: "user"
|
|
1015
|
-
});
|
|
1016
|
-
case "hidden_user": return buildForwardedContextFromHiddenName({
|
|
1017
|
-
name: origin.sender_user_name,
|
|
1018
|
-
date: origin.date,
|
|
1019
|
-
type: "hidden_user"
|
|
1020
|
-
});
|
|
1021
|
-
case "chat": return buildForwardedContextFromChat({
|
|
1022
|
-
chat: origin.sender_chat,
|
|
1023
|
-
date: origin.date,
|
|
1024
|
-
type: "chat",
|
|
1025
|
-
signature: origin.author_signature
|
|
1026
|
-
});
|
|
1027
|
-
case "channel": return buildForwardedContextFromChat({
|
|
1028
|
-
chat: origin.chat,
|
|
1029
|
-
date: origin.date,
|
|
1030
|
-
type: "channel",
|
|
1031
|
-
signature: origin.author_signature,
|
|
1032
|
-
messageId: origin.message_id
|
|
1033
|
-
});
|
|
1034
|
-
default: return null;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
/** Extract forwarded message origin info from Telegram message. */
|
|
1038
|
-
function normalizeForwardedContext(msg) {
|
|
1039
|
-
if (!msg.forward_origin) return null;
|
|
1040
|
-
return resolveForwardOrigin(msg.forward_origin);
|
|
1041
|
-
}
|
|
1042
|
-
function extractTelegramLocation(msg) {
|
|
1043
|
-
const { venue, location } = msg;
|
|
1044
|
-
if (venue) return {
|
|
1045
|
-
latitude: venue.location.latitude,
|
|
1046
|
-
longitude: venue.location.longitude,
|
|
1047
|
-
accuracy: venue.location.horizontal_accuracy,
|
|
1048
|
-
name: venue.title,
|
|
1049
|
-
address: venue.address,
|
|
1050
|
-
source: "place",
|
|
1051
|
-
isLive: false
|
|
1052
|
-
};
|
|
1053
|
-
if (location) {
|
|
1054
|
-
const isLive = typeof location.live_period === "number" && location.live_period > 0;
|
|
1055
|
-
return {
|
|
1056
|
-
latitude: location.latitude,
|
|
1057
|
-
longitude: location.longitude,
|
|
1058
|
-
accuracy: location.horizontal_accuracy,
|
|
1059
|
-
source: isLive ? "live" : "pin",
|
|
1060
|
-
isLive
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
//#endregion
|
|
1067
|
-
//#region src/infra/diagnostic-flags.ts
|
|
1068
|
-
const DIAGNOSTICS_ENV = "SQUIDCLAW_DIAGNOSTICS";
|
|
1069
|
-
function normalizeFlag(value) {
|
|
1070
|
-
return value.trim().toLowerCase();
|
|
1071
|
-
}
|
|
1072
|
-
function parseEnvFlags(raw) {
|
|
1073
|
-
if (!raw) return [];
|
|
1074
|
-
const trimmed = raw.trim();
|
|
1075
|
-
if (!trimmed) return [];
|
|
1076
|
-
const lowered = trimmed.toLowerCase();
|
|
1077
|
-
if ([
|
|
1078
|
-
"0",
|
|
1079
|
-
"false",
|
|
1080
|
-
"off",
|
|
1081
|
-
"none"
|
|
1082
|
-
].includes(lowered)) return [];
|
|
1083
|
-
if ([
|
|
1084
|
-
"1",
|
|
1085
|
-
"true",
|
|
1086
|
-
"all",
|
|
1087
|
-
"*"
|
|
1088
|
-
].includes(lowered)) return ["*"];
|
|
1089
|
-
return trimmed.split(/[,\s]+/).map(normalizeFlag).filter(Boolean);
|
|
1090
|
-
}
|
|
1091
|
-
function uniqueFlags(flags) {
|
|
1092
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1093
|
-
const out = [];
|
|
1094
|
-
for (const flag of flags) {
|
|
1095
|
-
const normalized = normalizeFlag(flag);
|
|
1096
|
-
if (!normalized || seen.has(normalized)) continue;
|
|
1097
|
-
seen.add(normalized);
|
|
1098
|
-
out.push(normalized);
|
|
1099
|
-
}
|
|
1100
|
-
return out;
|
|
1101
|
-
}
|
|
1102
|
-
function resolveDiagnosticFlags(cfg, env = process.env) {
|
|
1103
|
-
const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : [];
|
|
1104
|
-
const envFlags = parseEnvFlags(env[DIAGNOSTICS_ENV]);
|
|
1105
|
-
return uniqueFlags([...configFlags, ...envFlags]);
|
|
1106
|
-
}
|
|
1107
|
-
function matchesDiagnosticFlag(flag, enabledFlags) {
|
|
1108
|
-
const target = normalizeFlag(flag);
|
|
1109
|
-
if (!target) return false;
|
|
1110
|
-
for (const raw of enabledFlags) {
|
|
1111
|
-
const enabled = normalizeFlag(raw);
|
|
1112
|
-
if (!enabled) continue;
|
|
1113
|
-
if (enabled === "*" || enabled === "all") return true;
|
|
1114
|
-
if (enabled.endsWith(".*")) {
|
|
1115
|
-
const prefix = enabled.slice(0, -2);
|
|
1116
|
-
if (target === prefix || target.startsWith(`${prefix}.`)) return true;
|
|
1117
|
-
}
|
|
1118
|
-
if (enabled.endsWith("*")) {
|
|
1119
|
-
const prefix = enabled.slice(0, -1);
|
|
1120
|
-
if (target.startsWith(prefix)) return true;
|
|
1121
|
-
}
|
|
1122
|
-
if (enabled === target) return true;
|
|
1123
|
-
}
|
|
1124
|
-
return false;
|
|
1125
|
-
}
|
|
1126
|
-
function isDiagnosticFlagEnabled(flag, cfg, env = process.env) {
|
|
1127
|
-
return matchesDiagnosticFlag(flag, resolveDiagnosticFlags(cfg, env));
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
//#endregion
|
|
1131
|
-
//#region src/telegram/api-logging.ts
|
|
1132
|
-
const fallbackLogger = createSubsystemLogger("telegram/api");
|
|
1133
|
-
function resolveTelegramApiLogger(runtime, logger) {
|
|
1134
|
-
if (logger) return logger;
|
|
1135
|
-
if (runtime?.error) return runtime.error;
|
|
1136
|
-
return (message) => fallbackLogger.error(message);
|
|
1137
|
-
}
|
|
1138
|
-
async function withTelegramApiErrorLogging({ operation, fn, runtime, logger, shouldLog }) {
|
|
1139
|
-
try {
|
|
1140
|
-
return await fn();
|
|
1141
|
-
} catch (err) {
|
|
1142
|
-
if (!shouldLog || shouldLog(err)) {
|
|
1143
|
-
const errText = formatErrorMessage(err);
|
|
1144
|
-
resolveTelegramApiLogger(runtime, logger)(danger(`telegram ${operation} failed: ${errText}`));
|
|
1145
|
-
}
|
|
1146
|
-
throw err;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
//#endregion
|
|
1151
|
-
//#region src/telegram/caption.ts
|
|
1152
|
-
const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
|
|
1153
|
-
function splitTelegramCaption(text) {
|
|
1154
|
-
const trimmed = text?.trim() ?? "";
|
|
1155
|
-
if (!trimmed) return {
|
|
1156
|
-
caption: void 0,
|
|
1157
|
-
followUpText: void 0
|
|
1158
|
-
};
|
|
1159
|
-
if (trimmed.length > TELEGRAM_MAX_CAPTION_LENGTH) return {
|
|
1160
|
-
caption: void 0,
|
|
1161
|
-
followUpText: trimmed
|
|
1162
|
-
};
|
|
1163
|
-
return {
|
|
1164
|
-
caption: trimmed,
|
|
1165
|
-
followUpText: void 0
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
//#endregion
|
|
1170
|
-
//#region src/telegram/network-config.ts
|
|
1171
|
-
const TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV = "SQUIDCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY";
|
|
1172
|
-
const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "SQUIDCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY";
|
|
1173
|
-
const TELEGRAM_DNS_RESULT_ORDER_ENV = "SQUIDCLAW_TELEGRAM_DNS_RESULT_ORDER";
|
|
1174
|
-
let wsl2SyncCache;
|
|
1175
|
-
function isWSL2SyncCached() {
|
|
1176
|
-
if (typeof wsl2SyncCache === "boolean") return wsl2SyncCache;
|
|
1177
|
-
wsl2SyncCache = isWSL2Sync();
|
|
1178
|
-
return wsl2SyncCache;
|
|
1179
|
-
}
|
|
1180
|
-
function resolveTelegramAutoSelectFamilyDecision(params) {
|
|
1181
|
-
const env = params?.env ?? process$1.env;
|
|
1182
|
-
const nodeMajor = typeof params?.nodeMajor === "number" ? params.nodeMajor : Number(process$1.versions.node.split(".")[0]);
|
|
1183
|
-
if (isTruthyEnvValue(env[TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV])) return {
|
|
1184
|
-
value: true,
|
|
1185
|
-
source: `env:${TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV}`
|
|
1186
|
-
};
|
|
1187
|
-
if (isTruthyEnvValue(env[TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV])) return {
|
|
1188
|
-
value: false,
|
|
1189
|
-
source: `env:${TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV}`
|
|
1190
|
-
};
|
|
1191
|
-
if (typeof params?.network?.autoSelectFamily === "boolean") return {
|
|
1192
|
-
value: params.network.autoSelectFamily,
|
|
1193
|
-
source: "config"
|
|
1194
|
-
};
|
|
1195
|
-
if (isWSL2SyncCached()) return {
|
|
1196
|
-
value: false,
|
|
1197
|
-
source: "default-wsl2"
|
|
1198
|
-
};
|
|
1199
|
-
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) return {
|
|
1200
|
-
value: true,
|
|
1201
|
-
source: "default-node22"
|
|
1202
|
-
};
|
|
1203
|
-
return { value: null };
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Resolve DNS result order setting for Telegram network requests.
|
|
1207
|
-
* Some networks/ISPs have issues with IPv6 causing fetch failures.
|
|
1208
|
-
* Setting "ipv4first" prioritizes IPv4 addresses in DNS resolution.
|
|
1209
|
-
*
|
|
1210
|
-
* Priority:
|
|
1211
|
-
* 1. Environment variable SQUIDCLAW_TELEGRAM_DNS_RESULT_ORDER
|
|
1212
|
-
* 2. Config: channels.telegram.network.dnsResultOrder
|
|
1213
|
-
* 3. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues)
|
|
1214
|
-
*/
|
|
1215
|
-
function resolveTelegramDnsResultOrderDecision(params) {
|
|
1216
|
-
const env = params?.env ?? process$1.env;
|
|
1217
|
-
const nodeMajor = typeof params?.nodeMajor === "number" ? params.nodeMajor : Number(process$1.versions.node.split(".")[0]);
|
|
1218
|
-
const envValue = env[TELEGRAM_DNS_RESULT_ORDER_ENV]?.trim().toLowerCase();
|
|
1219
|
-
if (envValue === "ipv4first" || envValue === "verbatim") return {
|
|
1220
|
-
value: envValue,
|
|
1221
|
-
source: `env:${TELEGRAM_DNS_RESULT_ORDER_ENV}`
|
|
1222
|
-
};
|
|
1223
|
-
const configValue = (params?.network)?.dnsResultOrder?.trim().toLowerCase();
|
|
1224
|
-
if (configValue === "ipv4first" || configValue === "verbatim") return {
|
|
1225
|
-
value: configValue,
|
|
1226
|
-
source: "config"
|
|
1227
|
-
};
|
|
1228
|
-
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) return {
|
|
1229
|
-
value: "ipv4first",
|
|
1230
|
-
source: "default-node22"
|
|
1231
|
-
};
|
|
1232
|
-
return { value: null };
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
//#endregion
|
|
1236
|
-
//#region src/telegram/fetch.ts
|
|
1237
|
-
let appliedAutoSelectFamily = null;
|
|
1238
|
-
let appliedDnsResultOrder = null;
|
|
1239
|
-
let appliedGlobalDispatcherAutoSelectFamily = null;
|
|
1240
|
-
const log = createSubsystemLogger("telegram/network");
|
|
1241
|
-
function isProxyLikeDispatcher(dispatcher) {
|
|
1242
|
-
const ctorName = dispatcher?.constructor?.name;
|
|
1243
|
-
return typeof ctorName === "string" && ctorName.includes("ProxyAgent");
|
|
1244
|
-
}
|
|
1245
|
-
const FALLBACK_RETRY_ERROR_CODES = [
|
|
1246
|
-
"ETIMEDOUT",
|
|
1247
|
-
"ENETUNREACH",
|
|
1248
|
-
"EHOSTUNREACH",
|
|
1249
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
1250
|
-
"UND_ERR_SOCKET"
|
|
1251
|
-
];
|
|
1252
|
-
const IPV4_FALLBACK_RULES = [{
|
|
1253
|
-
name: "fetch-failed-envelope",
|
|
1254
|
-
matches: ({ message }) => message.includes("fetch failed")
|
|
1255
|
-
}, {
|
|
1256
|
-
name: "known-network-code",
|
|
1257
|
-
matches: ({ codes }) => FALLBACK_RETRY_ERROR_CODES.some((code) => codes.has(code))
|
|
1258
|
-
}];
|
|
1259
|
-
function applyTelegramNetworkWorkarounds(network) {
|
|
1260
|
-
const autoSelectDecision = resolveTelegramAutoSelectFamilyDecision({ network });
|
|
1261
|
-
if (autoSelectDecision.value !== null && autoSelectDecision.value !== appliedAutoSelectFamily) {
|
|
1262
|
-
if (typeof net$1.setDefaultAutoSelectFamily === "function") try {
|
|
1263
|
-
net$1.setDefaultAutoSelectFamily(autoSelectDecision.value);
|
|
1264
|
-
appliedAutoSelectFamily = autoSelectDecision.value;
|
|
1265
|
-
const label = autoSelectDecision.source ? ` (${autoSelectDecision.source})` : "";
|
|
1266
|
-
log.info(`autoSelectFamily=${autoSelectDecision.value}${label}`);
|
|
1267
|
-
} catch {}
|
|
1268
|
-
}
|
|
1269
|
-
if (autoSelectDecision.value !== null && autoSelectDecision.value !== appliedGlobalDispatcherAutoSelectFamily) {
|
|
1270
|
-
if (!(isProxyLikeDispatcher(getGlobalDispatcher()) && !hasProxyEnvConfigured())) try {
|
|
1271
|
-
setGlobalDispatcher(new EnvHttpProxyAgent({ connect: {
|
|
1272
|
-
autoSelectFamily: autoSelectDecision.value,
|
|
1273
|
-
autoSelectFamilyAttemptTimeout: 300
|
|
1274
|
-
} }));
|
|
1275
|
-
appliedGlobalDispatcherAutoSelectFamily = autoSelectDecision.value;
|
|
1276
|
-
log.info(`global undici dispatcher autoSelectFamily=${autoSelectDecision.value}`);
|
|
1277
|
-
} catch {}
|
|
1278
|
-
}
|
|
1279
|
-
const dnsDecision = resolveTelegramDnsResultOrderDecision({ network });
|
|
1280
|
-
if (dnsDecision.value !== null && dnsDecision.value !== appliedDnsResultOrder) {
|
|
1281
|
-
if (typeof dns.setDefaultResultOrder === "function") try {
|
|
1282
|
-
dns.setDefaultResultOrder(dnsDecision.value);
|
|
1283
|
-
appliedDnsResultOrder = dnsDecision.value;
|
|
1284
|
-
const label = dnsDecision.source ? ` (${dnsDecision.source})` : "";
|
|
1285
|
-
log.info(`dnsResultOrder=${dnsDecision.value}${label}`);
|
|
1286
|
-
} catch {}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
function collectErrorCodes(err) {
|
|
1290
|
-
const codes = /* @__PURE__ */ new Set();
|
|
1291
|
-
const queue = [err];
|
|
1292
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1293
|
-
while (queue.length > 0) {
|
|
1294
|
-
const current = queue.shift();
|
|
1295
|
-
if (!current || seen.has(current)) continue;
|
|
1296
|
-
seen.add(current);
|
|
1297
|
-
if (typeof current === "object") {
|
|
1298
|
-
const code = current.code;
|
|
1299
|
-
if (typeof code === "string" && code.trim()) codes.add(code.trim().toUpperCase());
|
|
1300
|
-
const cause = current.cause;
|
|
1301
|
-
if (cause && !seen.has(cause)) queue.push(cause);
|
|
1302
|
-
const errors = current.errors;
|
|
1303
|
-
if (Array.isArray(errors)) {
|
|
1304
|
-
for (const nested of errors) if (nested && !seen.has(nested)) queue.push(nested);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
return codes;
|
|
1309
|
-
}
|
|
1310
|
-
function shouldRetryWithIpv4Fallback(err) {
|
|
1311
|
-
const ctx = {
|
|
1312
|
-
message: err && typeof err === "object" && "message" in err ? String(err.message).toLowerCase() : "",
|
|
1313
|
-
codes: collectErrorCodes(err)
|
|
1314
|
-
};
|
|
1315
|
-
for (const rule of IPV4_FALLBACK_RULES) if (!rule.matches(ctx)) return false;
|
|
1316
|
-
return true;
|
|
1317
|
-
}
|
|
1318
|
-
function applyTelegramIpv4Fallback() {
|
|
1319
|
-
applyTelegramNetworkWorkarounds({
|
|
1320
|
-
autoSelectFamily: false,
|
|
1321
|
-
dnsResultOrder: "ipv4first"
|
|
1322
|
-
});
|
|
1323
|
-
log.warn("fetch fallback: forcing autoSelectFamily=false + dnsResultOrder=ipv4first");
|
|
1324
|
-
}
|
|
1325
|
-
function resolveTelegramFetch(proxyFetch, options) {
|
|
1326
|
-
applyTelegramNetworkWorkarounds(options?.network);
|
|
1327
|
-
const sourceFetch = proxyFetch ? resolveFetch(proxyFetch) : resolveFetch();
|
|
1328
|
-
if (!sourceFetch) throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
|
1329
|
-
if (proxyFetch) return sourceFetch;
|
|
1330
|
-
return (async (input, init) => {
|
|
1331
|
-
try {
|
|
1332
|
-
return await sourceFetch(input, init);
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
if (shouldRetryWithIpv4Fallback(err)) {
|
|
1335
|
-
applyTelegramIpv4Fallback();
|
|
1336
|
-
return sourceFetch(input, init);
|
|
1337
|
-
}
|
|
1338
|
-
throw err;
|
|
1339
|
-
}
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
//#endregion
|
|
1344
|
-
//#region src/telegram/format.ts
|
|
1345
|
-
function escapeHtml(text) {
|
|
1346
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1347
|
-
}
|
|
1348
|
-
function escapeHtmlAttr(text) {
|
|
1349
|
-
return escapeHtml(text).replace(/"/g, """);
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* File extensions that share TLDs and commonly appear in code/documentation.
|
|
1353
|
-
* These are wrapped in <code> tags to prevent Telegram from generating
|
|
1354
|
-
* spurious domain registrar previews.
|
|
1355
|
-
*
|
|
1356
|
-
* Only includes extensions that are:
|
|
1357
|
-
* 1. Commonly used as file extensions in code/docs
|
|
1358
|
-
* 2. Rarely used as intentional domain references
|
|
1359
|
-
*
|
|
1360
|
-
* Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
|
|
1361
|
-
*/
|
|
1362
|
-
const FILE_EXTENSIONS_WITH_TLD = new Set([
|
|
1363
|
-
"md",
|
|
1364
|
-
"go",
|
|
1365
|
-
"py",
|
|
1366
|
-
"pl",
|
|
1367
|
-
"sh",
|
|
1368
|
-
"am",
|
|
1369
|
-
"at",
|
|
1370
|
-
"be",
|
|
1371
|
-
"cc"
|
|
1372
|
-
]);
|
|
1373
|
-
/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
|
|
1374
|
-
function isAutoLinkedFileRef(href, label) {
|
|
1375
|
-
if (href.replace(/^https?:\/\//i, "") !== label) return false;
|
|
1376
|
-
const dotIndex = label.lastIndexOf(".");
|
|
1377
|
-
if (dotIndex < 1) return false;
|
|
1378
|
-
const ext = label.slice(dotIndex + 1).toLowerCase();
|
|
1379
|
-
if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) return false;
|
|
1380
|
-
const segments = label.split("/");
|
|
1381
|
-
if (segments.length > 1) {
|
|
1382
|
-
for (let i = 0; i < segments.length - 1; i++) if (segments[i].includes(".")) return false;
|
|
1383
|
-
}
|
|
1384
|
-
return true;
|
|
1385
|
-
}
|
|
1386
|
-
function buildTelegramLink(link, text) {
|
|
1387
|
-
const href = link.href.trim();
|
|
1388
|
-
if (!href) return null;
|
|
1389
|
-
if (link.start === link.end) return null;
|
|
1390
|
-
if (isAutoLinkedFileRef(href, text.slice(link.start, link.end))) return null;
|
|
1391
|
-
const safeHref = escapeHtmlAttr(href);
|
|
1392
|
-
return {
|
|
1393
|
-
start: link.start,
|
|
1394
|
-
end: link.end,
|
|
1395
|
-
open: `<a href="${safeHref}">`,
|
|
1396
|
-
close: "</a>"
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
function renderTelegramHtml(ir) {
|
|
1400
|
-
return renderMarkdownWithMarkers(ir, {
|
|
1401
|
-
styleMarkers: {
|
|
1402
|
-
bold: {
|
|
1403
|
-
open: "<b>",
|
|
1404
|
-
close: "</b>"
|
|
1405
|
-
},
|
|
1406
|
-
italic: {
|
|
1407
|
-
open: "<i>",
|
|
1408
|
-
close: "</i>"
|
|
1409
|
-
},
|
|
1410
|
-
strikethrough: {
|
|
1411
|
-
open: "<s>",
|
|
1412
|
-
close: "</s>"
|
|
1413
|
-
},
|
|
1414
|
-
code: {
|
|
1415
|
-
open: "<code>",
|
|
1416
|
-
close: "</code>"
|
|
1417
|
-
},
|
|
1418
|
-
code_block: {
|
|
1419
|
-
open: "<pre><code>",
|
|
1420
|
-
close: "</code></pre>"
|
|
1421
|
-
},
|
|
1422
|
-
spoiler: {
|
|
1423
|
-
open: "<tg-spoiler>",
|
|
1424
|
-
close: "</tg-spoiler>"
|
|
1425
|
-
},
|
|
1426
|
-
blockquote: {
|
|
1427
|
-
open: "<blockquote>",
|
|
1428
|
-
close: "</blockquote>"
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
|
-
escapeText: escapeHtml,
|
|
1432
|
-
buildLink: buildTelegramLink
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
function markdownToTelegramHtml(markdown, options = {}) {
|
|
1436
|
-
const html = renderTelegramHtml(markdownToIR(markdown ?? "", {
|
|
1437
|
-
linkify: true,
|
|
1438
|
-
enableSpoilers: true,
|
|
1439
|
-
headingStyle: "none",
|
|
1440
|
-
blockquotePrefix: "",
|
|
1441
|
-
tableMode: options.tableMode
|
|
1442
|
-
}));
|
|
1443
|
-
if (options.wrapFileRefs !== false) return wrapFileReferencesInHtml(html);
|
|
1444
|
-
return html;
|
|
1445
|
-
}
|
|
1446
|
-
/**
|
|
1447
|
-
* Wraps standalone file references (with TLD extensions) in <code> tags.
|
|
1448
|
-
* This prevents Telegram from treating them as URLs and generating
|
|
1449
|
-
* irrelevant domain registrar previews.
|
|
1450
|
-
*
|
|
1451
|
-
* Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
|
|
1452
|
-
* Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
|
|
1453
|
-
*/
|
|
1454
|
-
/** Escape regex metacharacters in a string */
|
|
1455
|
-
function escapeRegex(str) {
|
|
1456
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1457
|
-
}
|
|
1458
|
-
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
|
|
1459
|
-
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
|
|
1460
|
-
const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
|
|
1461
|
-
const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
|
|
1462
|
-
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
|
|
1463
|
-
function wrapStandaloneFileRef(match, prefix, filename) {
|
|
1464
|
-
if (filename.startsWith("//")) return match;
|
|
1465
|
-
if (/https?:\/\/$/i.test(prefix)) return match;
|
|
1466
|
-
return `${prefix}<code>${escapeHtml(filename)}</code>`;
|
|
1467
|
-
}
|
|
1468
|
-
function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
|
|
1469
|
-
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) return text;
|
|
1470
|
-
return text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef).replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
|
|
1471
|
-
}
|
|
1472
|
-
function wrapFileReferencesInHtml(html) {
|
|
1473
|
-
AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
|
|
1474
|
-
const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
|
|
1475
|
-
if (!isAutoLinkedFileRef(`http://${label}`, label)) return _match;
|
|
1476
|
-
return `<code>${escapeHtml(label)}</code>`;
|
|
1477
|
-
});
|
|
1478
|
-
let codeDepth = 0;
|
|
1479
|
-
let preDepth = 0;
|
|
1480
|
-
let anchorDepth = 0;
|
|
1481
|
-
let result = "";
|
|
1482
|
-
let lastIndex = 0;
|
|
1483
|
-
HTML_TAG_PATTERN.lastIndex = 0;
|
|
1484
|
-
let match;
|
|
1485
|
-
while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
|
|
1486
|
-
const tagStart = match.index;
|
|
1487
|
-
const tagEnd = HTML_TAG_PATTERN.lastIndex;
|
|
1488
|
-
const isClosing = match[1] === "</";
|
|
1489
|
-
const tagName = match[2].toLowerCase();
|
|
1490
|
-
const textBefore = deLinkified.slice(lastIndex, tagStart);
|
|
1491
|
-
result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
|
|
1492
|
-
if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
|
|
1493
|
-
else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
|
|
1494
|
-
else if (tagName === "a") anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
|
|
1495
|
-
result += deLinkified.slice(tagStart, tagEnd);
|
|
1496
|
-
lastIndex = tagEnd;
|
|
1497
|
-
}
|
|
1498
|
-
const remainingText = deLinkified.slice(lastIndex);
|
|
1499
|
-
result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
|
|
1500
|
-
return result;
|
|
1501
|
-
}
|
|
1502
|
-
function renderTelegramHtmlText(text, options = {}) {
|
|
1503
|
-
if ((options.textMode ?? "markdown") === "html") return text;
|
|
1504
|
-
return markdownToTelegramHtml(text, { tableMode: options.tableMode });
|
|
1505
|
-
}
|
|
1506
|
-
function splitTelegramChunkByHtmlLimit(chunk, htmlLimit, renderedHtmlLength) {
|
|
1507
|
-
const currentTextLength = chunk.text.length;
|
|
1508
|
-
if (currentTextLength <= 1) return [chunk];
|
|
1509
|
-
const proportionalLimit = Math.floor(currentTextLength * htmlLimit / Math.max(renderedHtmlLength, 1));
|
|
1510
|
-
const candidateLimit = Math.min(currentTextLength - 1, proportionalLimit);
|
|
1511
|
-
const split = splitMarkdownIRPreserveWhitespace(chunk, Number.isFinite(candidateLimit) && candidateLimit > 0 ? candidateLimit : Math.max(1, Math.floor(currentTextLength / 2)));
|
|
1512
|
-
if (split.length > 1) return split;
|
|
1513
|
-
return splitMarkdownIRPreserveWhitespace(chunk, Math.max(1, Math.floor(currentTextLength / 2)));
|
|
1514
|
-
}
|
|
1515
|
-
function sliceStyleSpans(styles, start, end) {
|
|
1516
|
-
return styles.flatMap((span) => {
|
|
1517
|
-
if (span.end <= start || span.start >= end) return [];
|
|
1518
|
-
const nextStart = Math.max(span.start, start) - start;
|
|
1519
|
-
const nextEnd = Math.min(span.end, end) - start;
|
|
1520
|
-
if (nextEnd <= nextStart) return [];
|
|
1521
|
-
return [{
|
|
1522
|
-
...span,
|
|
1523
|
-
start: nextStart,
|
|
1524
|
-
end: nextEnd
|
|
1525
|
-
}];
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
function sliceLinkSpans(links, start, end) {
|
|
1529
|
-
return links.flatMap((link) => {
|
|
1530
|
-
if (link.end <= start || link.start >= end) return [];
|
|
1531
|
-
const nextStart = Math.max(link.start, start) - start;
|
|
1532
|
-
const nextEnd = Math.min(link.end, end) - start;
|
|
1533
|
-
if (nextEnd <= nextStart) return [];
|
|
1534
|
-
return [{
|
|
1535
|
-
...link,
|
|
1536
|
-
start: nextStart,
|
|
1537
|
-
end: nextEnd
|
|
1538
|
-
}];
|
|
1539
|
-
});
|
|
1540
|
-
}
|
|
1541
|
-
function splitMarkdownIRPreserveWhitespace(ir, limit) {
|
|
1542
|
-
if (!ir.text) return [];
|
|
1543
|
-
const normalizedLimit = Math.max(1, Math.floor(limit));
|
|
1544
|
-
if (normalizedLimit <= 0 || ir.text.length <= normalizedLimit) return [ir];
|
|
1545
|
-
const chunks = [];
|
|
1546
|
-
let cursor = 0;
|
|
1547
|
-
while (cursor < ir.text.length) {
|
|
1548
|
-
const end = Math.min(ir.text.length, cursor + normalizedLimit);
|
|
1549
|
-
chunks.push({
|
|
1550
|
-
text: ir.text.slice(cursor, end),
|
|
1551
|
-
styles: sliceStyleSpans(ir.styles, cursor, end),
|
|
1552
|
-
links: sliceLinkSpans(ir.links, cursor, end)
|
|
1553
|
-
});
|
|
1554
|
-
cursor = end;
|
|
1555
|
-
}
|
|
1556
|
-
return chunks;
|
|
1557
|
-
}
|
|
1558
|
-
function renderTelegramChunksWithinHtmlLimit(ir, limit) {
|
|
1559
|
-
const normalizedLimit = Math.max(1, Math.floor(limit));
|
|
1560
|
-
const pending = chunkMarkdownIR(ir, normalizedLimit);
|
|
1561
|
-
const rendered = [];
|
|
1562
|
-
while (pending.length > 0) {
|
|
1563
|
-
const chunk = pending.shift();
|
|
1564
|
-
if (!chunk) continue;
|
|
1565
|
-
const html = wrapFileReferencesInHtml(renderTelegramHtml(chunk));
|
|
1566
|
-
if (html.length <= normalizedLimit || chunk.text.length <= 1) {
|
|
1567
|
-
rendered.push({
|
|
1568
|
-
html,
|
|
1569
|
-
text: chunk.text
|
|
1570
|
-
});
|
|
1571
|
-
continue;
|
|
1572
|
-
}
|
|
1573
|
-
const split = splitTelegramChunkByHtmlLimit(chunk, normalizedLimit, html.length);
|
|
1574
|
-
if (split.length <= 1) {
|
|
1575
|
-
rendered.push({
|
|
1576
|
-
html,
|
|
1577
|
-
text: chunk.text
|
|
1578
|
-
});
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
pending.unshift(...split);
|
|
1582
|
-
}
|
|
1583
|
-
return rendered;
|
|
1584
|
-
}
|
|
1585
|
-
function markdownToTelegramChunks(markdown, limit, options = {}) {
|
|
1586
|
-
return renderTelegramChunksWithinHtmlLimit(markdownToIR(markdown ?? "", {
|
|
1587
|
-
linkify: true,
|
|
1588
|
-
enableSpoilers: true,
|
|
1589
|
-
headingStyle: "none",
|
|
1590
|
-
blockquotePrefix: "",
|
|
1591
|
-
tableMode: options.tableMode
|
|
1592
|
-
}), limit);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
//#endregion
|
|
1596
|
-
//#region src/telegram/network-errors.ts
|
|
1597
|
-
const RECOVERABLE_ERROR_CODES = new Set([
|
|
1598
|
-
"ECONNRESET",
|
|
1599
|
-
"ECONNREFUSED",
|
|
1600
|
-
"EPIPE",
|
|
1601
|
-
"ETIMEDOUT",
|
|
1602
|
-
"ESOCKETTIMEDOUT",
|
|
1603
|
-
"ENETUNREACH",
|
|
1604
|
-
"EHOSTUNREACH",
|
|
1605
|
-
"ENOTFOUND",
|
|
1606
|
-
"EAI_AGAIN",
|
|
1607
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
1608
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
1609
|
-
"UND_ERR_BODY_TIMEOUT",
|
|
1610
|
-
"UND_ERR_SOCKET",
|
|
1611
|
-
"UND_ERR_ABORTED",
|
|
1612
|
-
"ECONNABORTED",
|
|
1613
|
-
"ERR_NETWORK"
|
|
1614
|
-
]);
|
|
1615
|
-
const RECOVERABLE_ERROR_NAMES = new Set([
|
|
1616
|
-
"AbortError",
|
|
1617
|
-
"TimeoutError",
|
|
1618
|
-
"ConnectTimeoutError",
|
|
1619
|
-
"HeadersTimeoutError",
|
|
1620
|
-
"BodyTimeoutError"
|
|
1621
|
-
]);
|
|
1622
|
-
const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
|
|
1623
|
-
const RECOVERABLE_MESSAGE_SNIPPETS = [
|
|
1624
|
-
"undici",
|
|
1625
|
-
"network error",
|
|
1626
|
-
"network request",
|
|
1627
|
-
"client network socket disconnected",
|
|
1628
|
-
"socket hang up",
|
|
1629
|
-
"getaddrinfo",
|
|
1630
|
-
"timeout",
|
|
1631
|
-
"timed out"
|
|
1632
|
-
];
|
|
1633
|
-
function normalizeCode(code) {
|
|
1634
|
-
return code?.trim().toUpperCase() ?? "";
|
|
1635
|
-
}
|
|
1636
|
-
function getErrorCode(err) {
|
|
1637
|
-
const direct = extractErrorCode(err);
|
|
1638
|
-
if (direct) return direct;
|
|
1639
|
-
if (!err || typeof err !== "object") return;
|
|
1640
|
-
const errno = err.errno;
|
|
1641
|
-
if (typeof errno === "string") return errno;
|
|
1642
|
-
if (typeof errno === "number") return String(errno);
|
|
1643
|
-
}
|
|
1644
|
-
function isRecoverableTelegramNetworkError(err, options = {}) {
|
|
1645
|
-
if (!err) return false;
|
|
1646
|
-
const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch : options.context !== "send";
|
|
1647
|
-
for (const candidate of collectErrorGraphCandidates(err, (current) => {
|
|
1648
|
-
const nested = [current.cause, current.reason];
|
|
1649
|
-
if (Array.isArray(current.errors)) nested.push(...current.errors);
|
|
1650
|
-
if (readErrorName(current) === "HttpError") nested.push(current.error);
|
|
1651
|
-
return nested;
|
|
1652
|
-
})) {
|
|
1653
|
-
const code = normalizeCode(getErrorCode(candidate));
|
|
1654
|
-
if (code && RECOVERABLE_ERROR_CODES.has(code)) return true;
|
|
1655
|
-
const name = readErrorName(candidate);
|
|
1656
|
-
if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true;
|
|
1657
|
-
const message = formatErrorMessage(candidate).trim().toLowerCase();
|
|
1658
|
-
if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) return true;
|
|
1659
|
-
if (allowMessageMatch && message) {
|
|
1660
|
-
if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) return true;
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
return false;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
//#endregion
|
|
1667
|
-
//#region src/telegram/sent-message-cache.ts
|
|
1668
|
-
/**
|
|
1669
|
-
* In-memory cache of sent message IDs per chat.
|
|
1670
|
-
* Used to identify bot's own messages for reaction filtering ("own" mode).
|
|
1671
|
-
*/
|
|
1672
|
-
const TTL_MS = 1440 * 60 * 1e3;
|
|
1673
|
-
const sentMessages = /* @__PURE__ */ new Map();
|
|
1674
|
-
function getChatKey(chatId) {
|
|
1675
|
-
return String(chatId);
|
|
1676
|
-
}
|
|
1677
|
-
function cleanupExpired(entry) {
|
|
1678
|
-
const now = Date.now();
|
|
1679
|
-
for (const [msgId, timestamp] of entry.timestamps) if (now - timestamp > TTL_MS) entry.timestamps.delete(msgId);
|
|
1680
|
-
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Record a message ID as sent by the bot.
|
|
1683
|
-
*/
|
|
1684
|
-
function recordSentMessage(chatId, messageId) {
|
|
1685
|
-
const key = getChatKey(chatId);
|
|
1686
|
-
let entry = sentMessages.get(key);
|
|
1687
|
-
if (!entry) {
|
|
1688
|
-
entry = { timestamps: /* @__PURE__ */ new Map() };
|
|
1689
|
-
sentMessages.set(key, entry);
|
|
1690
|
-
}
|
|
1691
|
-
entry.timestamps.set(messageId, Date.now());
|
|
1692
|
-
if (entry.timestamps.size > 100) cleanupExpired(entry);
|
|
1693
|
-
}
|
|
1694
|
-
/**
|
|
1695
|
-
* Check if a message was sent by the bot.
|
|
1696
|
-
*/
|
|
1697
|
-
function wasSentByBot(chatId, messageId) {
|
|
1698
|
-
const key = getChatKey(chatId);
|
|
1699
|
-
const entry = sentMessages.get(key);
|
|
1700
|
-
if (!entry) return false;
|
|
1701
|
-
cleanupExpired(entry);
|
|
1702
|
-
return entry.timestamps.has(messageId);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
//#endregion
|
|
1706
|
-
//#region src/cron/store.ts
|
|
1707
|
-
const DEFAULT_CRON_DIR = path.join(CONFIG_DIR, "cron");
|
|
1708
|
-
const DEFAULT_CRON_STORE_PATH = path.join(DEFAULT_CRON_DIR, "jobs.json");
|
|
1709
|
-
const serializedStoreCache = /* @__PURE__ */ new Map();
|
|
1710
|
-
function resolveCronStorePath(storePath) {
|
|
1711
|
-
if (storePath?.trim()) {
|
|
1712
|
-
const raw = storePath.trim();
|
|
1713
|
-
if (raw.startsWith("~")) return path.resolve(expandHomePrefix(raw));
|
|
1714
|
-
return path.resolve(raw);
|
|
1715
|
-
}
|
|
1716
|
-
return DEFAULT_CRON_STORE_PATH;
|
|
1717
|
-
}
|
|
1718
|
-
async function loadCronStore(storePath) {
|
|
1719
|
-
try {
|
|
1720
|
-
const raw = await fs.promises.readFile(storePath, "utf-8");
|
|
1721
|
-
let parsed;
|
|
1722
|
-
try {
|
|
1723
|
-
parsed = JSON5.parse(raw);
|
|
1724
|
-
} catch (err) {
|
|
1725
|
-
throw new Error(`Failed to parse cron store at ${storePath}: ${String(err)}`, { cause: err });
|
|
1726
|
-
}
|
|
1727
|
-
const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1728
|
-
const store = {
|
|
1729
|
-
version: 1,
|
|
1730
|
-
jobs: (Array.isArray(parsedRecord.jobs) ? parsedRecord.jobs : []).filter(Boolean)
|
|
1731
|
-
};
|
|
1732
|
-
serializedStoreCache.set(storePath, JSON.stringify(store, null, 2));
|
|
1733
|
-
return store;
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
if (err?.code === "ENOENT") {
|
|
1736
|
-
serializedStoreCache.delete(storePath);
|
|
1737
|
-
return {
|
|
1738
|
-
version: 1,
|
|
1739
|
-
jobs: []
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
throw err;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
async function saveCronStore(storePath, store) {
|
|
1746
|
-
await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
|
|
1747
|
-
const json = JSON.stringify(store, null, 2);
|
|
1748
|
-
const cached = serializedStoreCache.get(storePath);
|
|
1749
|
-
if (cached === json) return;
|
|
1750
|
-
let previous = cached ?? null;
|
|
1751
|
-
if (previous === null) try {
|
|
1752
|
-
previous = await fs.promises.readFile(storePath, "utf-8");
|
|
1753
|
-
} catch (err) {
|
|
1754
|
-
if (err.code !== "ENOENT") throw err;
|
|
1755
|
-
}
|
|
1756
|
-
if (previous === json) {
|
|
1757
|
-
serializedStoreCache.set(storePath, json);
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
const tmp = `${storePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
1761
|
-
await fs.promises.writeFile(tmp, json, "utf-8");
|
|
1762
|
-
if (previous !== null) try {
|
|
1763
|
-
await fs.promises.copyFile(storePath, `${storePath}.bak`);
|
|
1764
|
-
} catch {}
|
|
1765
|
-
await renameWithRetry(tmp, storePath);
|
|
1766
|
-
serializedStoreCache.set(storePath, json);
|
|
1767
|
-
}
|
|
1768
|
-
const RENAME_MAX_RETRIES = 3;
|
|
1769
|
-
const RENAME_BASE_DELAY_MS = 50;
|
|
1770
|
-
async function renameWithRetry(src, dest) {
|
|
1771
|
-
for (let attempt = 0; attempt <= RENAME_MAX_RETRIES; attempt++) try {
|
|
1772
|
-
await fs.promises.rename(src, dest);
|
|
1773
|
-
return;
|
|
1774
|
-
} catch (err) {
|
|
1775
|
-
const code = err.code;
|
|
1776
|
-
if (code === "EBUSY" && attempt < RENAME_MAX_RETRIES) {
|
|
1777
|
-
await new Promise((resolve) => setTimeout(resolve, RENAME_BASE_DELAY_MS * 2 ** attempt));
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
if (code === "EPERM" || code === "EEXIST") {
|
|
1781
|
-
await fs.promises.copyFile(src, dest);
|
|
1782
|
-
await fs.promises.unlink(src).catch(() => {});
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
throw err;
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
//#endregion
|
|
1790
|
-
//#region src/telegram/target-writeback.ts
|
|
1791
|
-
const writebackLogger = createSubsystemLogger("telegram/target-writeback");
|
|
1792
|
-
function asObjectRecord(value) {
|
|
1793
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1794
|
-
return value;
|
|
1795
|
-
}
|
|
1796
|
-
function normalizeTelegramLookupTargetForMatch(raw) {
|
|
1797
|
-
const normalized = normalizeTelegramLookupTarget(raw);
|
|
1798
|
-
if (!normalized) return;
|
|
1799
|
-
return normalized.startsWith("@") ? normalized.toLowerCase() : normalized;
|
|
1800
|
-
}
|
|
1801
|
-
function normalizeTelegramTargetForMatch(raw) {
|
|
1802
|
-
const parsed = parseTelegramTarget(raw);
|
|
1803
|
-
const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
|
|
1804
|
-
if (!normalized) return;
|
|
1805
|
-
return `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`;
|
|
1806
|
-
}
|
|
1807
|
-
function buildResolvedTelegramTarget(params) {
|
|
1808
|
-
const { raw, parsed, resolvedChatId } = params;
|
|
1809
|
-
if (parsed.messageThreadId == null) return resolvedChatId;
|
|
1810
|
-
return raw.includes(":topic:") ? `${resolvedChatId}:topic:${parsed.messageThreadId}` : `${resolvedChatId}:${parsed.messageThreadId}`;
|
|
1811
|
-
}
|
|
1812
|
-
function resolveLegacyRewrite(params) {
|
|
1813
|
-
const parsed = parseTelegramTarget(params.raw);
|
|
1814
|
-
if (normalizeTelegramChatId(parsed.chatId)) return null;
|
|
1815
|
-
const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
|
|
1816
|
-
if (!normalized) return null;
|
|
1817
|
-
return {
|
|
1818
|
-
matchKey: `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`,
|
|
1819
|
-
resolvedTarget: buildResolvedTelegramTarget({
|
|
1820
|
-
raw: params.raw,
|
|
1821
|
-
parsed,
|
|
1822
|
-
resolvedChatId: params.resolvedChatId
|
|
1823
|
-
})
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
function rewriteTargetIfMatch(params) {
|
|
1827
|
-
if (typeof params.rawValue !== "string" && typeof params.rawValue !== "number") return null;
|
|
1828
|
-
const value = String(params.rawValue).trim();
|
|
1829
|
-
if (!value) return null;
|
|
1830
|
-
if (normalizeTelegramTargetForMatch(value) !== params.matchKey) return null;
|
|
1831
|
-
return params.resolvedTarget;
|
|
1832
|
-
}
|
|
1833
|
-
function replaceTelegramDefaultToTargets(params) {
|
|
1834
|
-
let changed = false;
|
|
1835
|
-
const telegram = asObjectRecord(params.cfg.channels?.telegram);
|
|
1836
|
-
if (!telegram) return changed;
|
|
1837
|
-
const maybeReplace = (holder, key) => {
|
|
1838
|
-
const nextTarget = rewriteTargetIfMatch({
|
|
1839
|
-
rawValue: holder[key],
|
|
1840
|
-
matchKey: params.matchKey,
|
|
1841
|
-
resolvedTarget: params.resolvedTarget
|
|
1842
|
-
});
|
|
1843
|
-
if (!nextTarget) return;
|
|
1844
|
-
holder[key] = nextTarget;
|
|
1845
|
-
changed = true;
|
|
1846
|
-
};
|
|
1847
|
-
maybeReplace(telegram, "defaultTo");
|
|
1848
|
-
const accounts = asObjectRecord(telegram.accounts);
|
|
1849
|
-
if (!accounts) return changed;
|
|
1850
|
-
for (const accountId of Object.keys(accounts)) {
|
|
1851
|
-
const account = asObjectRecord(accounts[accountId]);
|
|
1852
|
-
if (!account) continue;
|
|
1853
|
-
maybeReplace(account, "defaultTo");
|
|
1854
|
-
}
|
|
1855
|
-
return changed;
|
|
1856
|
-
}
|
|
1857
|
-
async function maybePersistResolvedTelegramTarget(params) {
|
|
1858
|
-
const raw = params.rawTarget.trim();
|
|
1859
|
-
if (!raw) return;
|
|
1860
|
-
const rewrite = resolveLegacyRewrite({
|
|
1861
|
-
raw,
|
|
1862
|
-
resolvedChatId: params.resolvedChatId
|
|
1863
|
-
});
|
|
1864
|
-
if (!rewrite) return;
|
|
1865
|
-
const { matchKey, resolvedTarget } = rewrite;
|
|
1866
|
-
try {
|
|
1867
|
-
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
|
|
1868
|
-
const nextConfig = structuredClone(snapshot.config ?? {});
|
|
1869
|
-
if (replaceTelegramDefaultToTargets({
|
|
1870
|
-
cfg: nextConfig,
|
|
1871
|
-
matchKey,
|
|
1872
|
-
resolvedTarget
|
|
1873
|
-
})) {
|
|
1874
|
-
await writeConfigFile(nextConfig, writeOptions);
|
|
1875
|
-
if (params.verbose) writebackLogger.warn(`resolved Telegram defaultTo target ${raw} -> ${resolvedTarget}`);
|
|
1876
|
-
}
|
|
1877
|
-
} catch (err) {
|
|
1878
|
-
if (params.verbose) writebackLogger.warn(`failed to persist Telegram defaultTo target ${raw}: ${String(err)}`);
|
|
1879
|
-
}
|
|
1880
|
-
try {
|
|
1881
|
-
const storePath = resolveCronStorePath(params.cfg.cron?.store);
|
|
1882
|
-
const store = await loadCronStore(storePath);
|
|
1883
|
-
let cronChanged = false;
|
|
1884
|
-
for (const job of store.jobs) {
|
|
1885
|
-
if (job.delivery?.channel !== "telegram") continue;
|
|
1886
|
-
const nextTarget = rewriteTargetIfMatch({
|
|
1887
|
-
rawValue: job.delivery.to,
|
|
1888
|
-
matchKey,
|
|
1889
|
-
resolvedTarget
|
|
1890
|
-
});
|
|
1891
|
-
if (!nextTarget) continue;
|
|
1892
|
-
job.delivery.to = nextTarget;
|
|
1893
|
-
cronChanged = true;
|
|
1894
|
-
}
|
|
1895
|
-
if (cronChanged) {
|
|
1896
|
-
await saveCronStore(storePath, store);
|
|
1897
|
-
if (params.verbose) writebackLogger.warn(`resolved Telegram cron delivery target ${raw} -> ${resolvedTarget}`);
|
|
1898
|
-
}
|
|
1899
|
-
} catch (err) {
|
|
1900
|
-
if (params.verbose) writebackLogger.warn(`failed to persist Telegram cron target ${raw}: ${String(err)}`);
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
//#endregion
|
|
1905
|
-
//#region src/telegram/voice.ts
|
|
1906
|
-
function resolveTelegramVoiceDecision(opts) {
|
|
1907
|
-
if (!opts.wantsVoice) return { useVoice: false };
|
|
1908
|
-
if (isTelegramVoiceCompatibleAudio(opts)) return { useVoice: true };
|
|
1909
|
-
return {
|
|
1910
|
-
useVoice: false,
|
|
1911
|
-
reason: `media is ${opts.contentType ?? "unknown"} (${opts.fileName ?? "unknown"})`
|
|
1912
|
-
};
|
|
1913
|
-
}
|
|
1914
|
-
function resolveTelegramVoiceSend(opts) {
|
|
1915
|
-
const decision = resolveTelegramVoiceDecision(opts);
|
|
1916
|
-
if (decision.reason && opts.logFallback) opts.logFallback(`Telegram voice requested but ${decision.reason}; sending as audio file instead.`);
|
|
1917
|
-
return { useVoice: decision.useVoice };
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
//#endregion
|
|
1921
|
-
//#region src/telegram/send.ts
|
|
1922
|
-
function resolveTelegramMessageIdOrThrow(result, context) {
|
|
1923
|
-
if (typeof result?.message_id === "number" && Number.isFinite(result.message_id)) return Math.trunc(result.message_id);
|
|
1924
|
-
throw new Error(`Telegram ${context} returned no message_id`);
|
|
1925
|
-
}
|
|
1926
|
-
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
|
1927
|
-
const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
|
|
1928
|
-
const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
|
|
1929
|
-
const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
|
|
1930
|
-
const sendLogger = createSubsystemLogger("telegram/send");
|
|
1931
|
-
const diagLogger = createSubsystemLogger("telegram/diagnostic");
|
|
1932
|
-
function createTelegramHttpLogger(cfg) {
|
|
1933
|
-
if (!isDiagnosticFlagEnabled("telegram.http", cfg)) return () => {};
|
|
1934
|
-
return (label, err) => {
|
|
1935
|
-
if (!(err instanceof HttpError)) return;
|
|
1936
|
-
const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
|
|
1937
|
-
diagLogger.warn(`telegram http error (${label}): ${detail}`);
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
function resolveTelegramClientOptions(account) {
|
|
1941
|
-
const proxyUrl = account.config.proxy?.trim();
|
|
1942
|
-
const fetchImpl = resolveTelegramFetch(proxyUrl ? makeProxyFetch(proxyUrl) : void 0, { network: account.config.network });
|
|
1943
|
-
const timeoutSeconds = typeof account.config.timeoutSeconds === "number" && Number.isFinite(account.config.timeoutSeconds) ? Math.max(1, Math.floor(account.config.timeoutSeconds)) : void 0;
|
|
1944
|
-
return fetchImpl || timeoutSeconds ? {
|
|
1945
|
-
...fetchImpl ? { fetch: fetchImpl } : {},
|
|
1946
|
-
...timeoutSeconds ? { timeoutSeconds } : {}
|
|
1947
|
-
} : void 0;
|
|
1948
|
-
}
|
|
1949
|
-
function resolveToken(explicit, params) {
|
|
1950
|
-
if (explicit?.trim()) return explicit.trim();
|
|
1951
|
-
if (!params.token) throw new Error(`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
|
|
1952
|
-
return params.token.trim();
|
|
1953
|
-
}
|
|
1954
|
-
async function resolveChatId(to, params) {
|
|
1955
|
-
const numericChatId = normalizeTelegramChatId(to);
|
|
1956
|
-
if (numericChatId) return numericChatId;
|
|
1957
|
-
const lookupTarget = normalizeTelegramLookupTarget(to);
|
|
1958
|
-
const getChat = params.api.getChat;
|
|
1959
|
-
if (!lookupTarget || typeof getChat !== "function") throw new Error("Telegram recipient must be a numeric chat ID");
|
|
1960
|
-
try {
|
|
1961
|
-
const chat = await getChat.call(params.api, lookupTarget);
|
|
1962
|
-
const resolved = normalizeTelegramChatId(String(chat?.id ?? ""));
|
|
1963
|
-
if (!resolved) throw new Error(`resolved chat id is not numeric (${String(chat?.id ?? "")})`);
|
|
1964
|
-
if (params.verbose) sendLogger.warn(`telegram recipient ${lookupTarget} resolved to numeric chat id ${resolved}`);
|
|
1965
|
-
return resolved;
|
|
1966
|
-
} catch (err) {
|
|
1967
|
-
const detail = formatErrorMessage(err);
|
|
1968
|
-
throw new Error(`Telegram recipient ${lookupTarget} could not be resolved to a numeric chat ID (${detail})`, { cause: err });
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
async function resolveAndPersistChatId(params) {
|
|
1972
|
-
const chatId = await resolveChatId(params.lookupTarget, {
|
|
1973
|
-
api: params.api,
|
|
1974
|
-
verbose: params.verbose
|
|
1975
|
-
});
|
|
1976
|
-
await maybePersistResolvedTelegramTarget({
|
|
1977
|
-
cfg: params.cfg,
|
|
1978
|
-
rawTarget: params.persistTarget,
|
|
1979
|
-
resolvedChatId: chatId,
|
|
1980
|
-
verbose: params.verbose
|
|
1981
|
-
});
|
|
1982
|
-
return chatId;
|
|
1983
|
-
}
|
|
1984
|
-
function normalizeMessageId(raw) {
|
|
1985
|
-
if (typeof raw === "number" && Number.isFinite(raw)) return Math.trunc(raw);
|
|
1986
|
-
if (typeof raw === "string") {
|
|
1987
|
-
const value = raw.trim();
|
|
1988
|
-
if (!value) throw new Error("Message id is required for Telegram actions");
|
|
1989
|
-
const parsed = Number.parseInt(value, 10);
|
|
1990
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
1991
|
-
}
|
|
1992
|
-
throw new Error("Message id is required for Telegram actions");
|
|
1993
|
-
}
|
|
1994
|
-
function isTelegramThreadNotFoundError(err) {
|
|
1995
|
-
return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
|
|
1996
|
-
}
|
|
1997
|
-
function isTelegramMessageNotModifiedError(err) {
|
|
1998
|
-
return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
|
|
1999
|
-
}
|
|
2000
|
-
function hasMessageThreadIdParam(params) {
|
|
2001
|
-
if (!params) return false;
|
|
2002
|
-
const value = params.message_thread_id;
|
|
2003
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
2004
|
-
if (typeof value === "string") return value.trim().length > 0;
|
|
2005
|
-
return false;
|
|
2006
|
-
}
|
|
2007
|
-
function removeMessageThreadIdParam(params) {
|
|
2008
|
-
if (!params || !hasMessageThreadIdParam(params)) return params;
|
|
2009
|
-
const next = { ...params };
|
|
2010
|
-
delete next.message_thread_id;
|
|
2011
|
-
return Object.keys(next).length > 0 ? next : void 0;
|
|
2012
|
-
}
|
|
2013
|
-
function isTelegramHtmlParseError(err) {
|
|
2014
|
-
return PARSE_ERR_RE.test(formatErrorMessage(err));
|
|
2015
|
-
}
|
|
2016
|
-
function buildTelegramThreadReplyParams(params) {
|
|
2017
|
-
const messageThreadId = params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
|
|
2018
|
-
const threadScope = params.chatType === "direct" ? "dm" : "forum";
|
|
2019
|
-
const threadIdParams = buildTelegramThreadParams(messageThreadId != null ? {
|
|
2020
|
-
id: messageThreadId,
|
|
2021
|
-
scope: threadScope
|
|
2022
|
-
} : void 0);
|
|
2023
|
-
const threadParams = threadIdParams ? { ...threadIdParams } : {};
|
|
2024
|
-
if (params.replyToMessageId != null) {
|
|
2025
|
-
const replyToMessageId = Math.trunc(params.replyToMessageId);
|
|
2026
|
-
if (params.quoteText?.trim()) threadParams.reply_parameters = {
|
|
2027
|
-
message_id: replyToMessageId,
|
|
2028
|
-
quote: params.quoteText.trim()
|
|
2029
|
-
};
|
|
2030
|
-
else threadParams.reply_to_message_id = replyToMessageId;
|
|
2031
|
-
}
|
|
2032
|
-
return threadParams;
|
|
2033
|
-
}
|
|
2034
|
-
async function withTelegramHtmlParseFallback(params) {
|
|
2035
|
-
try {
|
|
2036
|
-
return await params.requestHtml(params.label);
|
|
2037
|
-
} catch (err) {
|
|
2038
|
-
if (!isTelegramHtmlParseError(err)) throw err;
|
|
2039
|
-
if (params.verbose) sendLogger.warn(`telegram ${params.label} failed with HTML parse error, retrying as plain text: ${formatErrorMessage(err)}`);
|
|
2040
|
-
return await params.requestPlain(`${params.label}-plain`);
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
function resolveTelegramApiContext(opts) {
|
|
2044
|
-
const cfg = opts.cfg ?? loadConfig();
|
|
2045
|
-
const account = resolveTelegramAccount({
|
|
2046
|
-
cfg,
|
|
2047
|
-
accountId: opts.accountId
|
|
2048
|
-
});
|
|
2049
|
-
const token = resolveToken(opts.token, account);
|
|
2050
|
-
const client = resolveTelegramClientOptions(account);
|
|
2051
|
-
return {
|
|
2052
|
-
cfg,
|
|
2053
|
-
account,
|
|
2054
|
-
api: opts.api ?? new Bot(token, client ? { client } : void 0).api
|
|
2055
|
-
};
|
|
2056
|
-
}
|
|
2057
|
-
function createTelegramRequestWithDiag(params) {
|
|
2058
|
-
const request = createTelegramRetryRunner({
|
|
2059
|
-
retry: params.retry,
|
|
2060
|
-
configRetry: params.account.config.retry,
|
|
2061
|
-
verbose: params.verbose,
|
|
2062
|
-
...params.shouldRetry ? { shouldRetry: params.shouldRetry } : {}
|
|
2063
|
-
});
|
|
2064
|
-
const logHttpError = createTelegramHttpLogger(params.cfg);
|
|
2065
|
-
return (fn, label, options) => {
|
|
2066
|
-
const runRequest = () => request(fn, label);
|
|
2067
|
-
return (params.useApiErrorLogging === false ? runRequest() : withTelegramApiErrorLogging({
|
|
2068
|
-
operation: label ?? "request",
|
|
2069
|
-
fn: runRequest,
|
|
2070
|
-
...options?.shouldLog ? { shouldLog: options.shouldLog } : {}
|
|
2071
|
-
})).catch((err) => {
|
|
2072
|
-
logHttpError(label ?? "request", err);
|
|
2073
|
-
throw err;
|
|
2074
|
-
});
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
function wrapTelegramChatNotFoundError(err, params) {
|
|
2078
|
-
if (!CHAT_NOT_FOUND_RE.test(formatErrorMessage(err))) return err;
|
|
2079
|
-
return new Error([
|
|
2080
|
-
`Telegram send failed: chat not found (chat_id=${params.chatId}).`,
|
|
2081
|
-
"Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
|
|
2082
|
-
`Input was: ${JSON.stringify(params.input)}.`
|
|
2083
|
-
].join(" "));
|
|
2084
|
-
}
|
|
2085
|
-
async function withTelegramThreadFallback(params, label, verbose, attempt) {
|
|
2086
|
-
try {
|
|
2087
|
-
return await attempt(params, label);
|
|
2088
|
-
} catch (err) {
|
|
2089
|
-
if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) throw err;
|
|
2090
|
-
if (verbose) sendLogger.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
|
|
2091
|
-
return await attempt(removeMessageThreadIdParam(params), `${label}-threadless`);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
function createRequestWithChatNotFound(params) {
|
|
2095
|
-
return async (fn, label) => params.requestWithDiag(fn, label).catch((err) => {
|
|
2096
|
-
throw wrapTelegramChatNotFoundError(err, {
|
|
2097
|
-
chatId: params.chatId,
|
|
2098
|
-
input: params.input
|
|
2099
|
-
});
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
|
-
function buildInlineKeyboard(buttons) {
|
|
2103
|
-
if (!buttons?.length) return;
|
|
2104
|
-
const rows = buttons.map((row) => row.filter((button) => button?.text && button?.callback_data).map((button) => ({
|
|
2105
|
-
text: button.text,
|
|
2106
|
-
callback_data: button.callback_data,
|
|
2107
|
-
...button.style ? { style: button.style } : {}
|
|
2108
|
-
}))).filter((row) => row.length > 0);
|
|
2109
|
-
if (rows.length === 0) return;
|
|
2110
|
-
return { inline_keyboard: rows };
|
|
2111
|
-
}
|
|
2112
|
-
async function sendMessageTelegram(to, text, opts = {}) {
|
|
2113
|
-
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
|
2114
|
-
const target = parseTelegramTarget(to);
|
|
2115
|
-
const chatId = await resolveAndPersistChatId({
|
|
2116
|
-
cfg,
|
|
2117
|
-
api,
|
|
2118
|
-
lookupTarget: target.chatId,
|
|
2119
|
-
persistTarget: to,
|
|
2120
|
-
verbose: opts.verbose
|
|
2121
|
-
});
|
|
2122
|
-
const mediaUrl = opts.mediaUrl?.trim();
|
|
2123
|
-
const replyMarkup = buildInlineKeyboard(opts.buttons);
|
|
2124
|
-
const threadParams = buildTelegramThreadReplyParams({
|
|
2125
|
-
targetMessageThreadId: target.messageThreadId,
|
|
2126
|
-
messageThreadId: opts.messageThreadId,
|
|
2127
|
-
chatType: target.chatType,
|
|
2128
|
-
replyToMessageId: opts.replyToMessageId,
|
|
2129
|
-
quoteText: opts.quoteText
|
|
2130
|
-
});
|
|
2131
|
-
const hasThreadParams = Object.keys(threadParams).length > 0;
|
|
2132
|
-
const requestWithChatNotFound = createRequestWithChatNotFound({
|
|
2133
|
-
requestWithDiag: createTelegramRequestWithDiag({
|
|
2134
|
-
cfg,
|
|
2135
|
-
account,
|
|
2136
|
-
retry: opts.retry,
|
|
2137
|
-
verbose: opts.verbose,
|
|
2138
|
-
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
|
|
2139
|
-
}),
|
|
2140
|
-
chatId,
|
|
2141
|
-
input: to
|
|
2142
|
-
});
|
|
2143
|
-
const textMode = opts.textMode ?? "markdown";
|
|
2144
|
-
const tableMode = resolveMarkdownTableMode({
|
|
2145
|
-
cfg,
|
|
2146
|
-
channel: "telegram",
|
|
2147
|
-
accountId: account.accountId
|
|
2148
|
-
});
|
|
2149
|
-
const renderHtmlText = (value) => renderTelegramHtmlText(value, {
|
|
2150
|
-
textMode,
|
|
2151
|
-
tableMode
|
|
2152
|
-
});
|
|
2153
|
-
const linkPreviewOptions = account.config.linkPreview ?? true ? void 0 : { is_disabled: true };
|
|
2154
|
-
const sendTelegramText = async (rawText, params, fallbackText) => {
|
|
2155
|
-
return await withTelegramThreadFallback(params, "message", opts.verbose, async (effectiveParams, label) => {
|
|
2156
|
-
const htmlText = renderHtmlText(rawText);
|
|
2157
|
-
const baseParams = effectiveParams ? { ...effectiveParams } : {};
|
|
2158
|
-
if (linkPreviewOptions) baseParams.link_preview_options = linkPreviewOptions;
|
|
2159
|
-
const hasBaseParams = Object.keys(baseParams).length > 0;
|
|
2160
|
-
const sendParams = {
|
|
2161
|
-
parse_mode: "HTML",
|
|
2162
|
-
...baseParams,
|
|
2163
|
-
...opts.silent === true ? { disable_notification: true } : {}
|
|
2164
|
-
};
|
|
2165
|
-
return await withTelegramHtmlParseFallback({
|
|
2166
|
-
label,
|
|
2167
|
-
verbose: opts.verbose,
|
|
2168
|
-
requestHtml: (retryLabel) => requestWithChatNotFound(() => api.sendMessage(chatId, htmlText, sendParams), retryLabel),
|
|
2169
|
-
requestPlain: (retryLabel) => {
|
|
2170
|
-
const plainParams = hasBaseParams ? baseParams : void 0;
|
|
2171
|
-
return requestWithChatNotFound(() => plainParams ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) : api.sendMessage(chatId, fallbackText ?? rawText), retryLabel);
|
|
2172
|
-
}
|
|
2173
|
-
});
|
|
2174
|
-
});
|
|
2175
|
-
};
|
|
2176
|
-
if (mediaUrl) {
|
|
2177
|
-
const media = await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({
|
|
2178
|
-
maxBytes: opts.maxBytes,
|
|
2179
|
-
mediaLocalRoots: opts.mediaLocalRoots
|
|
2180
|
-
}));
|
|
2181
|
-
const kind = kindFromMime(media.contentType ?? void 0);
|
|
2182
|
-
const isGif = isGifMedia({
|
|
2183
|
-
contentType: media.contentType,
|
|
2184
|
-
fileName: media.fileName
|
|
2185
|
-
});
|
|
2186
|
-
const isVideoNote = kind === "video" && opts.asVideoNote === true;
|
|
2187
|
-
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
|
|
2188
|
-
const file = new InputFile(media.buffer, fileName);
|
|
2189
|
-
let caption;
|
|
2190
|
-
let followUpText;
|
|
2191
|
-
if (isVideoNote) {
|
|
2192
|
-
caption = void 0;
|
|
2193
|
-
followUpText = text.trim() ? text : void 0;
|
|
2194
|
-
} else {
|
|
2195
|
-
const split = splitTelegramCaption(text);
|
|
2196
|
-
caption = split.caption;
|
|
2197
|
-
followUpText = split.followUpText;
|
|
2198
|
-
}
|
|
2199
|
-
const htmlCaption = caption ? renderHtmlText(caption) : void 0;
|
|
2200
|
-
const needsSeparateText = Boolean(followUpText);
|
|
2201
|
-
const baseMediaParams = {
|
|
2202
|
-
...hasThreadParams ? threadParams : {},
|
|
2203
|
-
...!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}
|
|
2204
|
-
};
|
|
2205
|
-
const mediaParams = {
|
|
2206
|
-
...htmlCaption ? {
|
|
2207
|
-
caption: htmlCaption,
|
|
2208
|
-
parse_mode: "HTML"
|
|
2209
|
-
} : {},
|
|
2210
|
-
...baseMediaParams,
|
|
2211
|
-
...opts.silent === true ? { disable_notification: true } : {}
|
|
2212
|
-
};
|
|
2213
|
-
const sendMedia = async (label, sender) => await withTelegramThreadFallback(mediaParams, label, opts.verbose, async (effectiveParams, retryLabel) => requestWithChatNotFound(() => sender(effectiveParams), retryLabel));
|
|
2214
|
-
const mediaSender = (() => {
|
|
2215
|
-
if (isGif) return {
|
|
2216
|
-
label: "animation",
|
|
2217
|
-
sender: (effectiveParams) => api.sendAnimation(chatId, file, effectiveParams)
|
|
2218
|
-
};
|
|
2219
|
-
if (kind === "image") return {
|
|
2220
|
-
label: "photo",
|
|
2221
|
-
sender: (effectiveParams) => api.sendPhoto(chatId, file, effectiveParams)
|
|
2222
|
-
};
|
|
2223
|
-
if (kind === "video") {
|
|
2224
|
-
if (isVideoNote) return {
|
|
2225
|
-
label: "video_note",
|
|
2226
|
-
sender: (effectiveParams) => api.sendVideoNote(chatId, file, effectiveParams)
|
|
2227
|
-
};
|
|
2228
|
-
return {
|
|
2229
|
-
label: "video",
|
|
2230
|
-
sender: (effectiveParams) => api.sendVideo(chatId, file, effectiveParams)
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
if (kind === "audio") {
|
|
2234
|
-
const { useVoice } = resolveTelegramVoiceSend({
|
|
2235
|
-
wantsVoice: opts.asVoice === true,
|
|
2236
|
-
contentType: media.contentType,
|
|
2237
|
-
fileName,
|
|
2238
|
-
logFallback: logVerbose
|
|
2239
|
-
});
|
|
2240
|
-
if (useVoice) return {
|
|
2241
|
-
label: "voice",
|
|
2242
|
-
sender: (effectiveParams) => api.sendVoice(chatId, file, effectiveParams)
|
|
2243
|
-
};
|
|
2244
|
-
return {
|
|
2245
|
-
label: "audio",
|
|
2246
|
-
sender: (effectiveParams) => api.sendAudio(chatId, file, effectiveParams)
|
|
2247
|
-
};
|
|
2248
|
-
}
|
|
2249
|
-
return {
|
|
2250
|
-
label: "document",
|
|
2251
|
-
sender: (effectiveParams) => api.sendDocument(chatId, file, effectiveParams)
|
|
2252
|
-
};
|
|
2253
|
-
})();
|
|
2254
|
-
const result = await sendMedia(mediaSender.label, mediaSender.sender);
|
|
2255
|
-
const mediaMessageId = resolveTelegramMessageIdOrThrow(result, "media send");
|
|
2256
|
-
const resolvedChatId = String(result?.chat?.id ?? chatId);
|
|
2257
|
-
recordSentMessage(chatId, mediaMessageId);
|
|
2258
|
-
recordChannelActivity({
|
|
2259
|
-
channel: "telegram",
|
|
2260
|
-
accountId: account.accountId,
|
|
2261
|
-
direction: "outbound"
|
|
2262
|
-
});
|
|
2263
|
-
if (needsSeparateText && followUpText) {
|
|
2264
|
-
const textParams = hasThreadParams || replyMarkup ? {
|
|
2265
|
-
...threadParams,
|
|
2266
|
-
...replyMarkup ? { reply_markup: replyMarkup } : {}
|
|
2267
|
-
} : void 0;
|
|
2268
|
-
const textMessageId = resolveTelegramMessageIdOrThrow(await sendTelegramText(followUpText, textParams), "text follow-up send");
|
|
2269
|
-
recordSentMessage(chatId, textMessageId);
|
|
2270
|
-
return {
|
|
2271
|
-
messageId: String(textMessageId),
|
|
2272
|
-
chatId: resolvedChatId
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
return {
|
|
2276
|
-
messageId: String(mediaMessageId),
|
|
2277
|
-
chatId: resolvedChatId
|
|
2278
|
-
};
|
|
2279
|
-
}
|
|
2280
|
-
if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
|
|
2281
|
-
const res = await sendTelegramText(text, hasThreadParams || replyMarkup ? {
|
|
2282
|
-
...threadParams,
|
|
2283
|
-
...replyMarkup ? { reply_markup: replyMarkup } : {}
|
|
2284
|
-
} : void 0, opts.plainText);
|
|
2285
|
-
const messageId = resolveTelegramMessageIdOrThrow(res, "text send");
|
|
2286
|
-
recordSentMessage(chatId, messageId);
|
|
2287
|
-
recordChannelActivity({
|
|
2288
|
-
channel: "telegram",
|
|
2289
|
-
accountId: account.accountId,
|
|
2290
|
-
direction: "outbound"
|
|
2291
|
-
});
|
|
2292
|
-
return {
|
|
2293
|
-
messageId: String(messageId),
|
|
2294
|
-
chatId: String(res?.chat?.id ?? chatId)
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, opts = {}) {
|
|
2298
|
-
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
|
2299
|
-
const rawTarget = String(chatIdInput);
|
|
2300
|
-
const chatId = await resolveAndPersistChatId({
|
|
2301
|
-
cfg,
|
|
2302
|
-
api,
|
|
2303
|
-
lookupTarget: rawTarget,
|
|
2304
|
-
persistTarget: rawTarget,
|
|
2305
|
-
verbose: opts.verbose
|
|
2306
|
-
});
|
|
2307
|
-
const messageId = normalizeMessageId(messageIdInput);
|
|
2308
|
-
const requestWithDiag = createTelegramRequestWithDiag({
|
|
2309
|
-
cfg,
|
|
2310
|
-
account,
|
|
2311
|
-
retry: opts.retry,
|
|
2312
|
-
verbose: opts.verbose,
|
|
2313
|
-
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
|
|
2314
|
-
});
|
|
2315
|
-
const remove = opts.remove === true;
|
|
2316
|
-
const trimmedEmoji = emoji.trim();
|
|
2317
|
-
const reactions = remove || !trimmedEmoji ? [] : [{
|
|
2318
|
-
type: "emoji",
|
|
2319
|
-
emoji: trimmedEmoji
|
|
2320
|
-
}];
|
|
2321
|
-
if (typeof api.setMessageReaction !== "function") throw new Error("Telegram reactions are unavailable in this bot API.");
|
|
2322
|
-
try {
|
|
2323
|
-
await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
|
|
2324
|
-
} catch (err) {
|
|
2325
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2326
|
-
if (/REACTION_INVALID/i.test(msg)) return {
|
|
2327
|
-
ok: false,
|
|
2328
|
-
warning: `Reaction unavailable: ${trimmedEmoji}`
|
|
2329
|
-
};
|
|
2330
|
-
throw err;
|
|
2331
|
-
}
|
|
2332
|
-
return { ok: true };
|
|
2333
|
-
}
|
|
2334
|
-
async function deleteMessageTelegram(chatIdInput, messageIdInput, opts = {}) {
|
|
2335
|
-
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
|
2336
|
-
const rawTarget = String(chatIdInput);
|
|
2337
|
-
const chatId = await resolveAndPersistChatId({
|
|
2338
|
-
cfg,
|
|
2339
|
-
api,
|
|
2340
|
-
lookupTarget: rawTarget,
|
|
2341
|
-
persistTarget: rawTarget,
|
|
2342
|
-
verbose: opts.verbose
|
|
2343
|
-
});
|
|
2344
|
-
const messageId = normalizeMessageId(messageIdInput);
|
|
2345
|
-
await createTelegramRequestWithDiag({
|
|
2346
|
-
cfg,
|
|
2347
|
-
account,
|
|
2348
|
-
retry: opts.retry,
|
|
2349
|
-
verbose: opts.verbose,
|
|
2350
|
-
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
|
|
2351
|
-
})(() => api.deleteMessage(chatId, messageId), "deleteMessage");
|
|
2352
|
-
logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
|
|
2353
|
-
return { ok: true };
|
|
2354
|
-
}
|
|
2355
|
-
async function editMessageTelegram(chatIdInput, messageIdInput, text, opts = {}) {
|
|
2356
|
-
const { cfg, account, api } = resolveTelegramApiContext({
|
|
2357
|
-
...opts,
|
|
2358
|
-
cfg: opts.cfg
|
|
2359
|
-
});
|
|
2360
|
-
const rawTarget = String(chatIdInput);
|
|
2361
|
-
const chatId = await resolveAndPersistChatId({
|
|
2362
|
-
cfg,
|
|
2363
|
-
api,
|
|
2364
|
-
lookupTarget: rawTarget,
|
|
2365
|
-
persistTarget: rawTarget,
|
|
2366
|
-
verbose: opts.verbose
|
|
2367
|
-
});
|
|
2368
|
-
const messageId = normalizeMessageId(messageIdInput);
|
|
2369
|
-
const requestWithDiag = createTelegramRequestWithDiag({
|
|
2370
|
-
cfg,
|
|
2371
|
-
account,
|
|
2372
|
-
retry: opts.retry,
|
|
2373
|
-
verbose: opts.verbose
|
|
2374
|
-
});
|
|
2375
|
-
const requestWithEditShouldLog = (fn, label, shouldLog) => requestWithDiag(fn, label, shouldLog ? { shouldLog } : void 0);
|
|
2376
|
-
const htmlText = renderTelegramHtmlText(text, {
|
|
2377
|
-
textMode: opts.textMode ?? "markdown",
|
|
2378
|
-
tableMode: resolveMarkdownTableMode({
|
|
2379
|
-
cfg,
|
|
2380
|
-
channel: "telegram",
|
|
2381
|
-
accountId: account.accountId
|
|
2382
|
-
})
|
|
2383
|
-
});
|
|
2384
|
-
const shouldTouchButtons = opts.buttons !== void 0;
|
|
2385
|
-
const builtKeyboard = shouldTouchButtons ? buildInlineKeyboard(opts.buttons) : void 0;
|
|
2386
|
-
const replyMarkup = shouldTouchButtons ? builtKeyboard ?? { inline_keyboard: [] } : void 0;
|
|
2387
|
-
const editParams = { parse_mode: "HTML" };
|
|
2388
|
-
if (opts.linkPreview === false) editParams.link_preview_options = { is_disabled: true };
|
|
2389
|
-
if (replyMarkup !== void 0) editParams.reply_markup = replyMarkup;
|
|
2390
|
-
const plainParams = {};
|
|
2391
|
-
if (opts.linkPreview === false) plainParams.link_preview_options = { is_disabled: true };
|
|
2392
|
-
if (replyMarkup !== void 0) plainParams.reply_markup = replyMarkup;
|
|
2393
|
-
try {
|
|
2394
|
-
await withTelegramHtmlParseFallback({
|
|
2395
|
-
label: "editMessage",
|
|
2396
|
-
verbose: opts.verbose,
|
|
2397
|
-
requestHtml: (retryLabel) => requestWithEditShouldLog(() => api.editMessageText(chatId, messageId, htmlText, editParams), retryLabel, (err) => !isTelegramMessageNotModifiedError(err)),
|
|
2398
|
-
requestPlain: (retryLabel) => requestWithEditShouldLog(() => Object.keys(plainParams).length > 0 ? api.editMessageText(chatId, messageId, text, plainParams) : api.editMessageText(chatId, messageId, text), retryLabel, (plainErr) => !isTelegramMessageNotModifiedError(plainErr))
|
|
2399
|
-
});
|
|
2400
|
-
} catch (err) {
|
|
2401
|
-
if (isTelegramMessageNotModifiedError(err)) {} else throw err;
|
|
2402
|
-
}
|
|
2403
|
-
logVerbose(`[telegram] Edited message ${messageId} in chat ${chatId}`);
|
|
2404
|
-
return {
|
|
2405
|
-
ok: true,
|
|
2406
|
-
messageId: String(messageId),
|
|
2407
|
-
chatId
|
|
2408
|
-
};
|
|
2409
|
-
}
|
|
2410
|
-
function inferFilename(kind) {
|
|
2411
|
-
switch (kind) {
|
|
2412
|
-
case "image": return "image.jpg";
|
|
2413
|
-
case "video": return "video.mp4";
|
|
2414
|
-
case "audio": return "audio.ogg";
|
|
2415
|
-
default: return "file.bin";
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
/**
|
|
2419
|
-
* Send a sticker to a Telegram chat by file_id.
|
|
2420
|
-
* @param to - Chat ID or username (e.g., "123456789" or "@username")
|
|
2421
|
-
* @param fileId - Telegram file_id of the sticker to send
|
|
2422
|
-
* @param opts - Optional configuration
|
|
2423
|
-
*/
|
|
2424
|
-
async function sendStickerTelegram(to, fileId, opts = {}) {
|
|
2425
|
-
if (!fileId?.trim()) throw new Error("Telegram sticker file_id is required");
|
|
2426
|
-
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
|
2427
|
-
const target = parseTelegramTarget(to);
|
|
2428
|
-
const chatId = await resolveAndPersistChatId({
|
|
2429
|
-
cfg,
|
|
2430
|
-
api,
|
|
2431
|
-
lookupTarget: target.chatId,
|
|
2432
|
-
persistTarget: to,
|
|
2433
|
-
verbose: opts.verbose
|
|
2434
|
-
});
|
|
2435
|
-
const threadParams = buildTelegramThreadReplyParams({
|
|
2436
|
-
targetMessageThreadId: target.messageThreadId,
|
|
2437
|
-
messageThreadId: opts.messageThreadId,
|
|
2438
|
-
chatType: target.chatType,
|
|
2439
|
-
replyToMessageId: opts.replyToMessageId
|
|
2440
|
-
});
|
|
2441
|
-
const hasThreadParams = Object.keys(threadParams).length > 0;
|
|
2442
|
-
const requestWithChatNotFound = createRequestWithChatNotFound({
|
|
2443
|
-
requestWithDiag: createTelegramRequestWithDiag({
|
|
2444
|
-
cfg,
|
|
2445
|
-
account,
|
|
2446
|
-
retry: opts.retry,
|
|
2447
|
-
verbose: opts.verbose,
|
|
2448
|
-
useApiErrorLogging: false
|
|
2449
|
-
}),
|
|
2450
|
-
chatId,
|
|
2451
|
-
input: to
|
|
2452
|
-
});
|
|
2453
|
-
const result = await withTelegramThreadFallback(hasThreadParams ? threadParams : void 0, "sticker", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label));
|
|
2454
|
-
const messageId = resolveTelegramMessageIdOrThrow(result, "sticker send");
|
|
2455
|
-
const resolvedChatId = String(result?.chat?.id ?? chatId);
|
|
2456
|
-
recordSentMessage(chatId, messageId);
|
|
2457
|
-
recordChannelActivity({
|
|
2458
|
-
channel: "telegram",
|
|
2459
|
-
accountId: account.accountId,
|
|
2460
|
-
direction: "outbound"
|
|
2461
|
-
});
|
|
2462
|
-
return {
|
|
2463
|
-
messageId: String(messageId),
|
|
2464
|
-
chatId: resolvedChatId
|
|
2465
|
-
};
|
|
2466
|
-
}
|
|
2467
|
-
/**
|
|
2468
|
-
* Send a poll to a Telegram chat.
|
|
2469
|
-
* @param to - Chat ID or username (e.g., "123456789" or "@username")
|
|
2470
|
-
* @param poll - Poll input with question, options, maxSelections, and optional durationHours
|
|
2471
|
-
* @param opts - Optional configuration
|
|
2472
|
-
*/
|
|
2473
|
-
async function sendPollTelegram(to, poll, opts = {}) {
|
|
2474
|
-
const { cfg, account, api } = resolveTelegramApiContext(opts);
|
|
2475
|
-
const target = parseTelegramTarget(to);
|
|
2476
|
-
const chatId = await resolveAndPersistChatId({
|
|
2477
|
-
cfg,
|
|
2478
|
-
api,
|
|
2479
|
-
lookupTarget: target.chatId,
|
|
2480
|
-
persistTarget: to,
|
|
2481
|
-
verbose: opts.verbose
|
|
2482
|
-
});
|
|
2483
|
-
const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
|
|
2484
|
-
const threadParams = buildTelegramThreadReplyParams({
|
|
2485
|
-
targetMessageThreadId: target.messageThreadId,
|
|
2486
|
-
messageThreadId: opts.messageThreadId,
|
|
2487
|
-
chatType: target.chatType,
|
|
2488
|
-
replyToMessageId: opts.replyToMessageId
|
|
2489
|
-
});
|
|
2490
|
-
const pollOptions = normalizedPoll.options;
|
|
2491
|
-
const requestWithChatNotFound = createRequestWithChatNotFound({
|
|
2492
|
-
requestWithDiag: createTelegramRequestWithDiag({
|
|
2493
|
-
cfg,
|
|
2494
|
-
account,
|
|
2495
|
-
retry: opts.retry,
|
|
2496
|
-
verbose: opts.verbose,
|
|
2497
|
-
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
|
|
2498
|
-
}),
|
|
2499
|
-
chatId,
|
|
2500
|
-
input: to
|
|
2501
|
-
});
|
|
2502
|
-
const durationSeconds = normalizedPoll.durationSeconds;
|
|
2503
|
-
if (durationSeconds === void 0 && normalizedPoll.durationHours !== void 0) throw new Error("Telegram poll durationHours is not supported. Use durationSeconds (5-600) instead.");
|
|
2504
|
-
if (durationSeconds !== void 0 && (durationSeconds < 5 || durationSeconds > 600)) throw new Error("Telegram poll durationSeconds must be between 5 and 600");
|
|
2505
|
-
const result = await withTelegramThreadFallback({
|
|
2506
|
-
allows_multiple_answers: normalizedPoll.maxSelections > 1,
|
|
2507
|
-
is_anonymous: opts.isAnonymous ?? true,
|
|
2508
|
-
...durationSeconds !== void 0 ? { open_period: durationSeconds } : {},
|
|
2509
|
-
...Object.keys(threadParams).length > 0 ? threadParams : {},
|
|
2510
|
-
...opts.silent === true ? { disable_notification: true } : {}
|
|
2511
|
-
}, "poll", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendPoll(chatId, normalizedPoll.question, pollOptions, effectiveParams), label));
|
|
2512
|
-
const messageId = resolveTelegramMessageIdOrThrow(result, "poll send");
|
|
2513
|
-
const resolvedChatId = String(result?.chat?.id ?? chatId);
|
|
2514
|
-
const pollId = result?.poll?.id;
|
|
2515
|
-
recordSentMessage(chatId, messageId);
|
|
2516
|
-
recordChannelActivity({
|
|
2517
|
-
channel: "telegram",
|
|
2518
|
-
accountId: account.accountId,
|
|
2519
|
-
direction: "outbound"
|
|
2520
|
-
});
|
|
2521
|
-
return {
|
|
2522
|
-
messageId: String(messageId),
|
|
2523
|
-
chatId: resolvedChatId,
|
|
2524
|
-
pollId
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
/**
|
|
2528
|
-
* Create a forum topic in a Telegram supergroup.
|
|
2529
|
-
* Requires the bot to have `can_manage_topics` permission.
|
|
2530
|
-
*
|
|
2531
|
-
* @param chatId - Supergroup chat ID
|
|
2532
|
-
* @param name - Topic name (1-128 characters)
|
|
2533
|
-
* @param opts - Optional configuration
|
|
2534
|
-
*/
|
|
2535
|
-
async function createForumTopicTelegram(chatId, name, opts = {}) {
|
|
2536
|
-
if (!name?.trim()) throw new Error("Forum topic name is required");
|
|
2537
|
-
const trimmedName = name.trim();
|
|
2538
|
-
if (trimmedName.length > 128) throw new Error("Forum topic name must be 128 characters or fewer");
|
|
2539
|
-
const cfg = loadConfig();
|
|
2540
|
-
const account = resolveTelegramAccount({
|
|
2541
|
-
cfg,
|
|
2542
|
-
accountId: opts.accountId
|
|
2543
|
-
});
|
|
2544
|
-
const token = resolveToken(opts.token, account);
|
|
2545
|
-
const client = resolveTelegramClientOptions(account);
|
|
2546
|
-
const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
|
|
2547
|
-
const normalizedChatId = await resolveAndPersistChatId({
|
|
2548
|
-
cfg,
|
|
2549
|
-
api,
|
|
2550
|
-
lookupTarget: parseTelegramTarget(chatId).chatId,
|
|
2551
|
-
persistTarget: chatId,
|
|
2552
|
-
verbose: opts.verbose
|
|
2553
|
-
});
|
|
2554
|
-
const request = createTelegramRetryRunner({
|
|
2555
|
-
retry: opts.retry,
|
|
2556
|
-
configRetry: account.config.retry,
|
|
2557
|
-
verbose: opts.verbose,
|
|
2558
|
-
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
|
|
2559
|
-
});
|
|
2560
|
-
const logHttpError = createTelegramHttpLogger(cfg);
|
|
2561
|
-
const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
|
|
2562
|
-
operation: label ?? "request",
|
|
2563
|
-
fn: () => request(fn, label)
|
|
2564
|
-
}).catch((err) => {
|
|
2565
|
-
logHttpError(label ?? "request", err);
|
|
2566
|
-
throw err;
|
|
2567
|
-
});
|
|
2568
|
-
const extra = {};
|
|
2569
|
-
if (opts.iconColor != null) extra.icon_color = opts.iconColor;
|
|
2570
|
-
if (opts.iconCustomEmojiId?.trim()) extra.icon_custom_emoji_id = opts.iconCustomEmojiId.trim();
|
|
2571
|
-
const hasExtra = Object.keys(extra).length > 0;
|
|
2572
|
-
const result = await requestWithDiag(() => api.createForumTopic(normalizedChatId, trimmedName, hasExtra ? extra : void 0), "createForumTopic");
|
|
2573
|
-
const topicId = result.message_thread_id;
|
|
2574
|
-
recordChannelActivity({
|
|
2575
|
-
channel: "telegram",
|
|
2576
|
-
accountId: account.accountId,
|
|
2577
|
-
direction: "outbound"
|
|
2578
|
-
});
|
|
2579
|
-
return {
|
|
2580
|
-
topicId,
|
|
2581
|
-
name: result.name ?? trimmedName,
|
|
2582
|
-
chatId: normalizedChatId
|
|
2583
|
-
};
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
//#endregion
|
|
2587
|
-
export { readChannelAllowFromStoreSync as $, expandTextLinks as A, resolveTelegramThreadSpec as B, buildSenderName as C, buildTelegramThreadParams as D, buildTelegramParentPeer as E, resolveTelegramForumThreadId as F, isVoiceCompatibleAudio as G, normalizeAllowFrom as H, resolveTelegramGroupAllowFromContext as I, isWSL2Sync as J, formatLocationText as K, resolveTelegramMediaPlaceholder as L, hasBotMention as M, normalizeForwardedContext as N, buildTypingThreadParams as O, resolveTelegramDirectPeerId as P, readChannelAllowFromStore as Q, resolveTelegramReplyId as R, buildSenderLabel as S, buildTelegramGroupPeerId as T, normalizeDmAllowFromWithStore as U, isSenderAllowed as V, resolveSenderAllowMatch as W, isWSLSync as X, isWSLEnv as Y, addChannelAllowFromStoreEntry as Z, wrapFileReferencesInHtml as _, reactMessageTelegram as a, listPairingChannels as at, withTelegramApiErrorLogging as b, sendStickerTelegram as c, mergeDmAllowFromSources as ct, resolveCronStorePath as d, removeChannelAllowFromStoreEntry as et, wasSentByBot as f, renderTelegramHtmlText as g, markdownToTelegramHtml as h, editMessageTelegram as i, getPairingAdapter as it, extractTelegramLocation as j, describeReplyTarget as k, resolveTelegramVoiceSend as l, resolveGroupAllowFromSources as lt, markdownToTelegramChunks as m, createForumTopicTelegram as n, readJsonFileWithFallback as nt, sendMessageTelegram as o, firstDefined as ot, isRecoverableTelegramNetworkError as p, toLocationContext as q, deleteMessageTelegram as r, writeJsonFileAtomically as rt, sendPollTelegram as s, isSenderIdAllowed as st, buildInlineKeyboard as t, upsertChannelPairingRequest as tt, loadCronStore as u, resolveTelegramFetch as v, buildTelegramGroupFrom as w, buildGroupLabel as x, splitTelegramCaption as y, resolveTelegramStreamMode as z };
|