squidclaw 3.0.13 → 3.0.14
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-PhtBJ8mA.js → accounts-BjEXPlGc.js} +1 -1
- package/dist/{accounts-Xp5W2xrR.js → accounts-CyBVeR-N.js} +1 -1
- package/dist/{accounts-BFC1okn9.js → accounts-D095MOmG.js} +7 -7
- package/dist/{acp-cli-Dt95OPXW.js → acp-cli-Dd2joFFS.js} +8 -8
- package/dist/{agent-scope-tUxfsPYq.js → agent-scope-C1XMay0T.js} +17 -17
- package/dist/{agents.config-DNPisWCH.js → agents.config-CRKsD30n.js} +2 -2
- package/dist/{api-key-rotation-Y59kKrr0.js → api-key-rotation-BcKKu9kK.js} +1 -1
- package/dist/{audio-preflight-DeBM0nTy.js → audio-preflight-g9rsstMv.js} +34 -34
- package/dist/{audio-transcription-runner-B7oPsr3U.js → audio-transcription-runner-Bnl3Ubjo.js} +23 -23
- package/dist/{audit-Bq3iosCf.js → audit-B4s_5Gn1.js} +29 -29
- package/dist/{auth-HYiP0mxx.js → auth-DHSeaNcB.js} +1 -1
- package/dist/{auth-choice-Du_pIfBh.js → auth-choice-C-7c5Td_.js} +13 -13
- package/dist/{auth-choice-CX8TDXmp.js → auth-choice-D7LiN5Ju.js} +11 -11
- package/dist/{auth-choice.apply-helpers-DRfHu1d8.js → auth-choice.apply-helpers-CxO2Wbq-.js} +1 -1
- package/dist/{auth-token-BQRI3c6e.js → auth-token-BxYYHBlc.js} +1 -1
- package/dist/{bonjour-discovery-BenDpM0y.js → bonjour-discovery-mtNewKSx.js} +1 -1
- package/dist/{browser-cli-Et8PyJjA.js → browser-cli-I1fzIVmS.js} +12 -12
- package/dist/build-info.json +2 -2
- package/dist/{call-DbkLm3eP.js → call-DkR5OGhh.js} +10 -10
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{channel-account-context-C_WQRa6U.js → channel-account-context-e4ysObLI.js} +5 -5
- package/dist/{channel-activity-7kixHIgY.js → channel-activity-CCjlTlcN.js} +1 -1
- package/dist/{channel-options-D0TjqxeO.js → channel-options-BL5mHe-R.js} +3 -3
- package/dist/{channel-selection-BwbXcgj2.js → channel-selection-D_20zq3H.js} +1 -1
- package/dist/{channel-web-lfZ3VOOl.js → channel-web-C7Iij0H0.js} +16 -16
- package/dist/{channels-cli-Hr4vekE6.js → channels-cli-BMiEn81Y.js} +92 -92
- package/dist/{channels-status-issues-DeoqSoo-.js → channels-status-issues-N9kzQrD8.js} +1 -1
- package/dist/{chrome-gIqO4t7T.js → chrome-BNfSmFAM.js} +4 -4
- package/dist/{clawbot-cli-C-jGxnTq.js → clawbot-cli-B6-68gsT.js} +11 -11
- package/dist/cli/daemon-cli.js +1 -1
- package/dist/{cli-BH62lCfL.js → cli-ByaubO_B.js} +72 -72
- package/dist/{client-CeaUCJtL.js → client-BURktNyH.js} +2 -2
- package/dist/{command-secret-targets-BF4WzaME.js → command-secret-targets-WwcPUFAf.js} +4 -4
- package/dist/{commands-CD6EMOiw.js → commands-CiY9HjRt.js} +1 -1
- package/dist/{commands-registry-DCUj1QOi.js → commands-registry-0bpX7A_Q.js} +3 -3
- package/dist/{completion-cli-BIY_jjYY.js → completion-cli-8PdK97J-.js} +12 -12
- package/dist/{config-cli-DvNViMXW.js → config-cli-ClomIMw7.js} +7 -7
- package/dist/{config-guard-DwgnWjmj.js → config-guard-rLrk7eSS.js} +16 -16
- package/dist/{config-validation-CcqZ--gE.js → config-validation-BQEaXMk7.js} +3 -3
- package/dist/{configure-B5Zuhebc.js → configure-CoXGul3l.js} +17 -17
- package/dist/{control-ui-assets-x69xyZH-.js → control-ui-assets-Bz7TCLpb.js} +1 -1
- package/dist/{cron-cli-Bv-X4msQ.js → cron-cli-C3iKBccc.js} +11 -11
- package/dist/{daemon-cli-B-8-rGND.js → daemon-cli-BVcrlw5o.js} +15 -15
- package/dist/{daemon-install-2y4HHhYl.js → daemon-install-CXDWHEEO.js} +4 -4
- package/dist/{daemon-install-helpers-Cdo6Pryw.js → daemon-install-helpers-a4pGEsbA.js} +11 -11
- package/dist/{deliver-CVl43oM1.js → deliver-B1sllFkh.js} +7 -7
- package/dist/deliver-runtime-CaV7seKv.js +61 -0
- package/dist/deps-send-discord.runtime-DbS5pxta.js +36 -0
- package/dist/deps-send-imessage.runtime-Ji753_F7.js +35 -0
- package/dist/deps-send-signal.runtime-DLJqvPLf.js +34 -0
- package/dist/deps-send-slack.runtime-DrFxeZ1I.js +32 -0
- package/dist/{deps-send-telegram.runtime-C9lz8bf0.js → deps-send-telegram.runtime-CF-Ylrvr.js} +16 -16
- package/dist/deps-send-whatsapp.runtime-BOE0Ke6v.js +118 -0
- package/dist/{devices-cli-BT8K0hdG.js → devices-cli-Dn_norz3.js} +8 -8
- package/dist/{diagnostic-5bqrhGEp.js → diagnostic-CNl71AzW.js} +1 -1
- package/dist/{diagnostics-C0CazfQM.js → diagnostics-DZ99A0EW.js} +5 -5
- package/dist/{directory-cli-BZcT9ie1.js → directory-cli-ZzwXkW89.js} +7 -7
- package/dist/{dns-cli-CPYaeASu.js → dns-cli-DipYQjtY.js} +5 -5
- package/dist/{dock-CVTqzMqE.js → dock-C7N1CmcP.js} +4 -4
- package/dist/{docs-cli-C50vGyDh.js → docs-cli-C-Izm9Tb.js} +4 -4
- package/dist/{doctor-completion-Emprau8v.js → doctor-completion-CGmYWy0n.js} +2 -2
- package/dist/{doctor-config-flow-DSA-JE3n.js → doctor-config-flow-DDWA3Z2D.js} +15 -15
- package/dist/{enable-CAYj5OhY.js → enable-RJyJVP_B.js} +1 -1
- package/dist/{exec-approvals-allowlist-BO4EEQUI.js → exec-approvals-allowlist-Bu_oaDJC.js} +1 -1
- package/dist/{exec-approvals-cli-DSOHdWJR.js → exec-approvals-cli-Csducy9L.js} +16 -16
- package/dist/{exec-safe-bin-runtime-policy-CSnpe1AT.js → exec-safe-bin-runtime-policy-DRQwt8kj.js} +2 -2
- package/dist/{fetch-guard-VcWeni3c.js → fetch-guard-DNykKlIv.js} +1 -1
- package/dist/{fs-safe-CjHYKGbt.js → fs-safe-CKHYu4Vz.js} +24 -24
- package/dist/{gateway-cli-B2fScwuY.js → gateway-cli-CUph5Eqo.js} +152 -152
- package/dist/{gateway-rpc-DpqZnhyz.js → gateway-rpc-DoKw40aG.js} +1 -1
- package/dist/{health-BZcB9SMe.js → health-Cia3g9r1.js} +11 -11
- package/dist/{hooks-cli-BylHgZ52.js → hooks-cli-LDbU3XMU.js} +80 -80
- package/dist/{hooks-status-DqGd74DG.js → hooks-status-Dd_iWcBZ.js} +1 -1
- package/dist/{image-CulZw1up.js → image-D2a7rcK1.js} +5 -5
- package/dist/{image-ops-BpsIdA2j.js → image-ops-BIWika4g.js} +10 -10
- package/dist/image-runtime-DTxVPe7Z.js +55 -0
- package/dist/index.js +82 -82
- package/dist/{inspect-D5T8Bbzl.js → inspect-C3PHuODr.js} +4 -4
- package/dist/{install-safe-path-BoN-MLvX.js → install-safe-path-iCmVuopp.js} +25 -25
- package/dist/{installs-CPHMcmNj.js → installs-CyjlehlR.js} +9 -9
- package/dist/{ipv4-Bwl9ruCP.js → ipv4-CWVOQw5T.js} +1 -1
- package/dist/{ir-xnftdkOo.js → ir-BPM7rQYq.js} +8 -8
- package/dist/{issue-format-8t_ncgFx.js → issue-format-sA05c-6t.js} +1 -1
- package/dist/{json-files-DIEpaxUj.js → json-files-D6M304Qd.js} +8 -8
- package/dist/{lifecycle-core-DKiBjZdv.js → lifecycle-core-DsfZ6965.js} +5 -5
- package/dist/{local-roots-BrxGyJnb.js → local-roots-CMxJ8L4w.js} +3 -3
- package/dist/{login-CxDYXbgx.js → login-BjtBaVVG.js} +3 -3
- package/dist/{login-qr-zf4B1lpU.js → login-qr-JfRlpd7I.js} +6 -6
- package/dist/{logs-cli-CJAe1_dv.js → logs-cli-gv2Ql7GE.js} +9 -9
- package/dist/{manager-DBgRFvYb.js → manager-XeZQ6ws7.js} +14 -14
- package/dist/{manager-runtime-Dl31cGh-.js → manager-runtime-BvGqzQIY.js} +9 -9
- package/dist/{manifest-registry-Cx3mB9zS.js → manifest-registry-BaUPjNKb.js} +1 -1
- package/dist/{memory-cli-Bhoc-n_1.js → memory-cli-BUJHSsW6.js} +12 -12
- package/dist/{model-DYzjrxpA.js → model-Jy6fO59G.js} +2 -2
- package/dist/{model-catalog-kpDOW8eY.js → model-catalog-BfoWgIDd.js} +3 -3
- package/dist/{model-picker-BZwK54QT.js → model-picker-CxmHVCE1.js} +4 -4
- package/dist/{model-selection-ZV3PuYVR.js → model-selection-QgM_TOjC.js} +16 -16
- package/dist/{models-cli-BIxlIOFr.js → models-cli-DlbNL6it.js} +81 -81
- package/dist/{models-config-BLdDYoxi.js → models-config-DOD5jluc.js} +6 -6
- package/dist/{node-cli-6uYJccbd.js → node-cli-uJ5lFzxj.js} +33 -33
- package/dist/{node-command-policy-CKTXVa3D.js → node-command-policy-CwVo8Z1X.js} +1 -1
- package/dist/{node-service-DGPVb5ri.js → node-service-xGYkt3vb.js} +1 -1
- package/dist/{nodes-cli-CXLRfAYx.js → nodes-cli-eauCMbSr.js} +16 -16
- package/dist/{nodes-screen-C-tuRhA1.js → nodes-screen-iR_FzGNs.js} +7 -7
- package/dist/{npm-pack-install-DgSn7djE.js → npm-pack-install-s-mP9K0z.js} +18 -18
- package/dist/{npm-resolution-gw3vFNTB.js → npm-resolution-Ck7yFIM2.js} +4 -4
- package/dist/{onboard-CCaYDSO2.js → onboard-aISMp4Kt.js} +6 -6
- package/dist/{onboard-channels-DQnP5d3Y.js → onboard-channels-CELkEQUS.js} +21 -21
- package/dist/{onboard-custom-CJohrPzT.js → onboard-custom-BmAzWYbD.js} +4 -4
- package/dist/{onboard-helpers-Bcs_1b81.js → onboard-helpers-DRk4Y5uA.js} +10 -10
- package/dist/{onboard-hooks-A23nqe_3.js → onboard-hooks-lk1sMnDf.js} +4 -4
- package/dist/{onboard-remote-Hf9sTDHl.js → onboard-remote-Bj9cR8kF.js} +4 -4
- package/dist/{onboard-skills-Cpc8o6sG.js → onboard-skills-eRpyQ1la.js} +4 -4
- package/dist/{onboarding-wx00blu5.js → onboarding-Bl4RDn6W.js} +14 -14
- package/dist/{onboarding.finalize-B8wqvggG.js → onboarding.finalize-Bm7_INHb.js} +85 -85
- package/dist/{onboarding.gateway-config-CvWq5i_X.js → onboarding.gateway-config-BPyk6gwr.js} +18 -18
- package/dist/{onboarding.secret-input-CIN4p8mg.js → onboarding.secret-input-DlDt-I2O.js} +1 -1
- package/dist/{openai-model-default-BYfJa19m.js → openai-model-default-DnYP1Em1.js} +2 -2
- package/dist/{outbound-B3RZI-ai.js → outbound-BQtnr_z2.js} +3 -3
- package/dist/{outbound-attachment-BATDqOuj.js → outbound-attachment-WnXMTJC4.js} +2 -2
- package/dist/{pairing-cli-FMIw0yL6.js → pairing-cli-BsU-YnVT.js} +8 -8
- package/dist/{pairing-labels-MH31IXn_.js → pairing-labels-BXfoinTP.js} +1 -1
- package/dist/{pairing-store-KII9MIZX.js → pairing-store-C1FvWpDK.js} +3 -3
- package/dist/{path-alias-guards-B7H6jIIw.js → path-alias-guards-CCRXJArp.js} +3 -3
- package/dist/{path-safety-3wUzDqI9.js → path-safety-BWiXz6D4.js} +1 -1
- package/dist/{paths-BguvT84s.js → paths-Bg6h1q3v.js} +9 -9
- package/dist/{pi-embedded-helpers-D8WkqLZt.js → pi-embedded-helpers-DctimJJI.js} +6 -6
- package/dist/{pi-model-discovery-Bfyzj3Lq.js → pi-model-discovery-D63dINOn.js} +1 -1
- package/dist/{pi-model-discovery-runtime-nh-zh_Bp.js → pi-model-discovery-runtime-CcXGQcru.js} +5 -5
- package/dist/{pi-tools.before-tool-call.runtime-D4V6gUzq.js → pi-tools.before-tool-call.runtime-BMb_b90y.js} +5 -5
- package/dist/{pi-tools.policy-DCE9mhtw.js → pi-tools.policy-CXvZtJB7.js} +5 -5
- package/dist/{plugin-auto-enable-BJw5Rcbx.js → plugin-auto-enable-DMABTEO-.js} +3 -3
- package/dist/{plugin-registry-V0H8DaZf.js → plugin-registry-BSlUIHKX.js} +3 -3
- package/dist/plugin-sdk/accounts-B6gye1Jd.js +46 -0
- package/dist/plugin-sdk/accounts-C-BT6Po7.js +288 -0
- package/dist/plugin-sdk/accounts-DQDXV8eB.js +35 -0
- package/dist/plugin-sdk/active-listener-DZCohPuZ.js +50 -0
- package/dist/plugin-sdk/api-key-rotation-CrX0fvDj.js +181 -0
- package/dist/plugin-sdk/audio-preflight-xnWAFqH-.js +69 -0
- package/dist/plugin-sdk/audio-transcription-runner-BDmtq7-q.js +2176 -0
- package/dist/plugin-sdk/audit-membership-runtime-B9b-zRwg.js +58 -0
- package/dist/plugin-sdk/channel-activity-JjLRpUa_.js +94 -0
- package/dist/plugin-sdk/channel-web-DNWsxhYh.js +2256 -0
- package/dist/plugin-sdk/chrome-B5PWOUbr.js +2415 -0
- package/dist/plugin-sdk/commands-registry-BKeyJUxK.js +1125 -0
- package/dist/plugin-sdk/config-FhBFLsNm.js +17911 -0
- package/dist/plugin-sdk/deliver-DEbTlzFy.js +1694 -0
- package/dist/plugin-sdk/deliver-runtime-CO2uP-r9.js +32 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-DIPW0tR4.js +23 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-ByGjRa1H.js +22 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-Ca1awu4L.js +21 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-CRzWCVkC.js +19 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-BWyavKp9.js +24 -0
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-cC_XvHV8.js +57 -0
- package/dist/plugin-sdk/diagnostic-Dv9S12vm.js +319 -0
- package/dist/plugin-sdk/errors-B8oJXuCF.js +54 -0
- package/dist/plugin-sdk/fetch-guard-W_A4uSz2.js +156 -0
- package/dist/plugin-sdk/fs-safe-Dqmpk-Fr.js +352 -0
- package/dist/plugin-sdk/image-BSFy8d1M.js +2310 -0
- package/dist/plugin-sdk/image-ops-DN17S88I.js +584 -0
- package/dist/plugin-sdk/image-runtime-5YO31sjU.js +25 -0
- package/dist/plugin-sdk/imessage.js +2 -2
- package/dist/plugin-sdk/index.js +50 -50
- package/dist/plugin-sdk/ir-JaPZ0yKH.js +1296 -0
- package/dist/plugin-sdk/local-roots-BTW3ifco.js +186 -0
- package/dist/plugin-sdk/logger-DDdrdbDu.js +1163 -0
- package/dist/plugin-sdk/login-BXGRny-S.js +57 -0
- package/dist/plugin-sdk/login-qr-DTs92ap8.js +320 -0
- package/dist/plugin-sdk/manager-DzFj9oHX.js +3917 -0
- package/dist/plugin-sdk/manager-runtime-DrpyZvO0.js +15 -0
- package/dist/plugin-sdk/mattermost.js +3 -3
- package/dist/plugin-sdk/outbound-CQ7uBBML.js +212 -0
- package/dist/plugin-sdk/outbound-attachment-dTE6EVdX.js +19 -0
- package/dist/plugin-sdk/path-alias-guards-gBhrAn14.js +43 -0
- package/dist/plugin-sdk/paths-C6W4VHoa.js +166 -0
- package/dist/plugin-sdk/pi-embedded-helpers-CfzQPXDC.js +9627 -0
- package/dist/plugin-sdk/pi-model-discovery-Bt6B0MAj.js +134 -0
- package/dist/plugin-sdk/pi-model-discovery-runtime-BcgXpTmL.js +8 -0
- package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-DYJQxhuo.js +354 -0
- package/dist/plugin-sdk/plugins-6NxPd6TS.js +864 -0
- package/dist/plugin-sdk/proxy-fetch-ZPEvp58f.js +38 -0
- package/dist/plugin-sdk/pw-ai-BFK39pwE.js +1938 -0
- package/dist/plugin-sdk/qmd-manager-6bozlfFg.js +1448 -0
- package/dist/plugin-sdk/query-expansion-eeVz_aEm.js +1011 -0
- package/dist/plugin-sdk/redact-BoNEjbpF.js +319 -0
- package/dist/plugin-sdk/reply-DgcrQBKL.js +98828 -0
- package/dist/plugin-sdk/resolve-outbound-target-CbaJ-kc2.js +40 -0
- package/dist/plugin-sdk/run-with-concurrency-5DMu9szx.js +1994 -0
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-jkgyeVsN.js +10 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DdLJJ1YC.js +19 -0
- package/dist/plugin-sdk/send-BSoMbeqA.js +3135 -0
- package/dist/plugin-sdk/send-Byyfc20_.js +503 -0
- package/dist/plugin-sdk/send-CI-xWEs7.js +2587 -0
- package/dist/plugin-sdk/send-DzP9EJqK.js +540 -0
- package/dist/plugin-sdk/send-MlSZ82sA.js +414 -0
- package/dist/plugin-sdk/session-DFy97tfW.js +169 -0
- package/dist/plugin-sdk/signal.js +2 -2
- package/dist/plugin-sdk/skill-commands-yum46YuA.js +342 -0
- package/dist/plugin-sdk/skills-DUphJGKn.js +1428 -0
- package/dist/plugin-sdk/slash-commands.runtime-5UW5KLyR.js +13 -0
- package/dist/plugin-sdk/slash-dispatch.runtime-4oQ2P_qo.js +52 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-y_mOLyS7.js +16 -0
- package/dist/plugin-sdk/ssrf-B3XRWBsP.js +202 -0
- package/dist/plugin-sdk/store-DFvIhzWZ.js +81 -0
- package/dist/plugin-sdk/subagent-registry-runtime-DtKXhKtl.js +52 -0
- package/dist/plugin-sdk/tables-bDM_jlLP.js +55 -0
- package/dist/{target-errors-DlzVutaL.js → plugin-sdk/target-errors-BVBW25Y3.js} +4 -4
- package/dist/plugin-sdk/thinking-Bu-w5mW5.js +1206 -0
- package/dist/plugin-sdk/tokens-CTIYTLWu.js +52 -0
- package/dist/plugin-sdk/tool-images-D0G_giwP.js +274 -0
- package/dist/plugin-sdk/web-DSXk7XCb.js +56 -0
- package/dist/plugin-sdk/whatsapp-actions-BOyA0Uuj.js +80 -0
- package/dist/{plugins-Cl_3OCyK.js → plugins-H53_4Gxb.js} +2 -2
- package/dist/{plugins-cli-CN2fty5U.js → plugins-cli-CQkxWdnt.js} +82 -82
- package/dist/{ports-Bop51hz6.js → ports-CKXuQJST.js} +2 -2
- package/dist/{ports-6i8smH3e.js → ports-CXjhFS7T.js} +1 -1
- package/dist/{program-context-ehHvCw9L.js → program-context-tphS7xu7.js} +41 -41
- package/dist/{prompt-select-styled-CSMviLJY.js → prompt-select-styled-DL2p1pfi.js} +40 -40
- package/dist/{provider-auth-helpers-CgNWlsqs.js → provider-auth-helpers-BqWiy-r-.js} +5 -5
- package/dist/{proxy-env-CRD7fbqp.js → proxy-env-D1tz4Z6a.js} +1 -1
- package/dist/{push-apns-DoYzx3tH.js → push-apns-C-YdARdy.js} +5 -5
- package/dist/{pw-ai-yKJj32B4.js → pw-ai-SVeR5d2o.js} +18 -18
- package/dist/{qmd-manager-AYDUTXmc.js → qmd-manager-B-XXhWVw.js} +20 -20
- package/dist/{qr-cli-cikAHfYn.js → qr-cli-B4fjHvn1.js} +2 -2
- package/dist/{query-expansion-CV2Z4_mS.js → query-expansion-Bf60ekMj.js} +12 -12
- package/dist/{redact-snapshot-C9T1079O.js → redact-snapshot-DRqM8Vla.js} +1 -1
- package/dist/{register.agent-BlxoyQt0.js → register.agent-BdiM0qkl.js} +97 -97
- package/dist/register.configure-wANXDbzQ.js +164 -0
- package/dist/{register.maintenance-C-Yv2mHK.js → register.maintenance-CMAt3Nl8.js} +93 -93
- package/dist/{register.message-CUNXtFOj.js → register.message-CbxQ5lgi.js} +73 -73
- package/dist/{register.onboard-BKXm1mL7.js → register.onboard-CbiTdKQX.js} +18 -18
- package/dist/{register.setup-DaT9AIAz.js → register.setup-KhsHMx2v.js} +21 -21
- package/dist/{register.status-health-sessions-7yZmSvUL.js → register.status-health-sessions-Bw5YDa_s.js} +86 -86
- package/dist/{reply-CeUYZhWu.js → reply-BWXzPVSJ.js} +149 -149
- package/dist/{rpc-BDpuIniF.js → rpc-DlN_W_L1.js} +1 -1
- package/dist/{runtime-Dw7Yw4OJ.js → runtime-Ci7mtLvH.js} +3 -3
- package/dist/{runtime-config-collectors-jhc1wDBg.js → runtime-config-collectors-CKmGmTQ5.js} +1 -1
- package/dist/{runtime-whatsapp-login.runtime-f293Z_er.js → runtime-whatsapp-login.runtime-Bxqt5jiO.js} +7 -7
- package/dist/runtime-whatsapp-outbound.runtime-B-UsXwvb.js +32 -0
- package/dist/{sandbox-BgsD1lf7.js → sandbox-Cuk1IWYT.js} +18 -18
- package/dist/{sandbox-cli-BnvmhiN9.js → sandbox-cli-BxgFeoAD.js} +25 -25
- package/dist/{secrets-cli-D7-K7P82.js → secrets-cli-DODLX29x.js} +11 -11
- package/dist/{security-cli-BFJAgNYH.js → security-cli-Ch83VrLP.js} +42 -42
- package/dist/{send-BvlkshC1.js → send-BfVgGHK6.js} +6 -6
- package/dist/{send-tK0H9nwq.js → send-Bw8LNCit.js} +5 -5
- package/dist/{send-CDms2FQA.js → send-C9UJKBSM.js} +4 -4
- package/dist/{send-CPFNtAP8.js → send-DX_fR45p.js} +11 -11
- package/dist/{send-C3Aeswif.js → send-FTQaNJPj.js} +8 -8
- package/dist/{server-OfKJG6Bo.js → server-Q9nnn04D.js} +20 -20
- package/dist/{server-context-B-0KzcZE.js → server-context-CX28l04l.js} +12 -12
- package/dist/{server-lifecycle-D6VNKVvQ.js → server-lifecycle-yAUMTJhv.js} +2 -2
- package/dist/{server-middleware-7cXowO4W.js → server-middleware-DIc4WJOS.js} +1 -1
- package/dist/{server-node-events-DWQhNK-0.js → server-node-events-qU3NTncQ.js} +73 -73
- package/dist/{service-Dk-UMipf.js → service-D4y0_Q5Z.js} +15 -15
- package/dist/{session-Da18ilJ0.js → session-CZJ5Ux6-.js} +1 -1
- package/dist/{sessions-CmjcNTJ3.js → sessions-C5H_BZSr.js} +15 -15
- package/dist/{shared-BzY0v0tS.js → shared-CxdBWgym.js} +3 -3
- package/dist/{shared-BcB-feC8.js → shared-DwNtcoQg.js} +1 -1
- package/dist/{skill-commands-BEWkEml_.js → skill-commands-BSRPnzXp.js} +5 -5
- package/dist/{skill-scanner-Bb5SMbCz.js → skill-scanner-rdr9cQew.js} +6 -6
- package/dist/{skills-Rnr7zPen.js → skills-BnBOxX1c.js} +3 -3
- package/dist/{skills-cli-BowIIIzH.js → skills-cli-CUgeYV1y.js} +5 -5
- package/dist/{skills-install-BLNCKuex.js → skills-install-BE04CZ6e.js} +6 -6
- package/dist/{skills-status-BCU-5otB.js → skills-status-DQw98BkG.js} +1 -1
- package/dist/{slash-commands.runtime-BgVgQ-Eh.js → slash-commands.runtime-BPtL2Qev.js} +11 -11
- package/dist/slash-dispatch.runtime-Cf9dq1k6.js +113 -0
- package/dist/{slash-skill-commands.runtime-4dOiU6U0.js → slash-skill-commands.runtime-9BYoANpd.js} +15 -15
- package/dist/{squidclaw-root-BcB7vo9M.js → squidclaw-root-CnE19yKj.js} +8 -8
- package/dist/{status-COTRBaam.js → status-rb5Jz-VU.js} +26 -26
- package/dist/{status.update-BUql4yz-.js → status.update-BNODJGA9.js} +2 -2
- package/dist/{store-aa15VM42.js → store-PDMRmC5Z.js} +5 -5
- package/dist/subagent-registry-runtime-CTiA365B.js +113 -0
- package/dist/{system-cli-CgCUbH_M.js → system-cli-DJYyTO07.js} +9 -9
- package/dist/{system-run-command-Ny1SbbOD.js → system-run-command-BgnCyvrj.js} +1 -1
- package/dist/{systemd-zbKl2Q3E.js → systemd-fP8tz4aL.js} +9 -9
- package/dist/{systemd-hints-C9_7ouv7.js → systemd-hints-BG_t__ZD.js} +6 -6
- package/dist/{systemd-linger-BM6JyzAr.js → systemd-linger-DgK8uuKY.js} +1 -1
- package/dist/{tables-jZMI8hLl.js → tables-DUSFF9-W.js} +1 -1
- package/dist/{tailnet-gTCqUBfJ.js → tailnet-CLkKVwDq.js} +1 -1
- package/dist/target-errors-BxwxgIDk.js +195 -0
- package/dist/{tool-images-DTl_LHMa.js → tool-images-FhSCiY-o.js} +1 -1
- package/dist/{tui-kDK-MAOc.js → tui-BMOMT-ma.js} +6 -6
- package/dist/{tui-cli-Dxy6fKkU.js → tui-cli-kqZ_-2Mz.js} +32 -32
- package/dist/{update-ZUTn6Jsu.js → update-blK9j2ag.js} +3 -3
- package/dist/{update-cli-D_QEiKyz.js → update-cli-gLKrP8UQ.js} +102 -102
- package/dist/{update-runner-DOXPSA_-.js → update-runner-C2UrF4oZ.js} +16 -16
- package/dist/web-VmjeceHS.js +117 -0
- package/dist/{webhooks-cli-D1iehjkP.js → webhooks-cli-BEXPBILa.js} +6 -6
- package/dist/{whatsapp-actions-hN5bwjWU.js → whatsapp-actions-DhiV181U.js} +17 -17
- package/dist/{with-timeout-BjaANd4G.js → with-timeout-BCAfkt03.js} +3 -3
- package/dist/{workspace-BITWyKG4.js → workspace-B-k5DNiQ.js} +1 -1
- package/dist/{workspace-dirs-B7O9BAHp.js → workspace-dirs-B2dNahfe.js} +1 -1
- package/dist/{ws-Bx8lpC1N.js → ws-DKt5HoA5.js} +2 -2
- package/dist/{wsl-wYxPJ8EO.js → wsl-CgxzAzRe.js} +2 -2
- package/package.json +1 -1
- package/dist/deliver-runtime-uwleBPlq.js +0 -61
- package/dist/deps-send-discord.runtime-CuZGpA7H.js +0 -36
- package/dist/deps-send-imessage.runtime-ByVW2alP.js +0 -35
- package/dist/deps-send-signal.runtime-Dl4GaCbQ.js +0 -34
- package/dist/deps-send-slack.runtime-BEV10FHj.js +0 -32
- package/dist/deps-send-whatsapp.runtime-Di0SEPNK.js +0 -118
- package/dist/image-runtime-B5Q4J9w2.js +0 -55
- package/dist/register.configure-CGptmTVZ.js +0 -164
- package/dist/runtime-whatsapp-outbound.runtime-uDhEmWe1.js +0 -32
- package/dist/slash-dispatch.runtime-D83FVeU7.js +0 -113
- package/dist/subagent-registry-runtime-CrT5RSO9.js +0 -113
- package/dist/web-DtSq_aUB.js +0 -117
|
@@ -0,0 +1,3135 @@
|
|
|
1
|
+
import { at as DEFAULT_ACCOUNT_ID, ot as normalizeAccountId } from "./run-with-concurrency-5DMu9szx.js";
|
|
2
|
+
import { Un as resolveRetryConfig, Wn as retryAsync, n as loadConfig } from "./config-FhBFLsNm.js";
|
|
3
|
+
import { X as resolvePreferredSquidClawTmpDir } from "./logger-DDdrdbDu.js";
|
|
4
|
+
import { _ as maxBytesForKind, l as extensionForMime } from "./image-ops-DN17S88I.js";
|
|
5
|
+
import { H as resolveDiscordAccount, U as normalizeDiscordToken, _ as parseMentionPrefixOrAtUserTarget, g as buildMessagingTarget, v as requireTargetKind } from "./plugins-6NxPd6TS.js";
|
|
6
|
+
import { t as resolveFetch } from "./fetch-B_RcOnt9.js";
|
|
7
|
+
import { n as recordChannelActivity, r as createDiscordRetryRunner } from "./channel-activity-JjLRpUa_.js";
|
|
8
|
+
import { t as buildOutboundMediaLoadOptions } from "./load-options-DNSaiajj.js";
|
|
9
|
+
import { n as normalizePollInput, t as normalizePollDurationHours } from "./polls-3WJMd-G-.js";
|
|
10
|
+
import { c as chunkMarkdownTextWithMode, d as resolveChunkMode, i as resolveMarkdownTableMode, v as loadWebMedia, y as loadWebMediaRaw } from "./ir-JaPZ0yKH.js";
|
|
11
|
+
import { t as convertMarkdownTables } from "./tables-bDM_jlLP.js";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { ButtonStyle, ChannelType, MessageFlags, PermissionFlagsBits, Routes, TextInputStyle } from "discord-api-types/v10";
|
|
18
|
+
import { Button, ChannelSelectMenu, CheckboxGroup, Container, Embed, File, Label, LinkButton, MediaGallery, MentionableSelectMenu, Modal, RadioGroup, RateLimitError, RequestClient, RoleSelectMenu, Row, Section, Separator, StringSelectMenu, TextDisplay, TextInput, Thumbnail, UserSelectMenu, parseCustomId, serializePayload } from "@buape/carbon";
|
|
19
|
+
import { PollLayoutType } from "discord-api-types/payloads/v10";
|
|
20
|
+
|
|
21
|
+
//#region src/channels/channel-config.ts
|
|
22
|
+
function applyChannelMatchMeta(result, match) {
|
|
23
|
+
if (match.matchKey && match.matchSource) {
|
|
24
|
+
result.matchKey = match.matchKey;
|
|
25
|
+
result.matchSource = match.matchSource;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function resolveChannelMatchConfig(match, resolveEntry) {
|
|
30
|
+
if (!match.entry) return null;
|
|
31
|
+
return applyChannelMatchMeta(resolveEntry(match.entry), match);
|
|
32
|
+
}
|
|
33
|
+
function normalizeChannelSlug(value) {
|
|
34
|
+
return value.trim().toLowerCase().replace(/^#/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
35
|
+
}
|
|
36
|
+
function buildChannelKeyCandidates(...keys) {
|
|
37
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38
|
+
const candidates = [];
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if (typeof key !== "string") continue;
|
|
41
|
+
const trimmed = key.trim();
|
|
42
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
43
|
+
seen.add(trimmed);
|
|
44
|
+
candidates.push(trimmed);
|
|
45
|
+
}
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
function resolveChannelEntryMatch(params) {
|
|
49
|
+
const entries = params.entries ?? {};
|
|
50
|
+
const match = {};
|
|
51
|
+
for (const key of params.keys) {
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
|
|
53
|
+
match.entry = entries[key];
|
|
54
|
+
match.key = key;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (params.wildcardKey && Object.prototype.hasOwnProperty.call(entries, params.wildcardKey)) {
|
|
58
|
+
match.wildcardEntry = entries[params.wildcardKey];
|
|
59
|
+
match.wildcardKey = params.wildcardKey;
|
|
60
|
+
}
|
|
61
|
+
return match;
|
|
62
|
+
}
|
|
63
|
+
function resolveChannelEntryMatchWithFallback(params) {
|
|
64
|
+
const direct = resolveChannelEntryMatch({
|
|
65
|
+
entries: params.entries,
|
|
66
|
+
keys: params.keys,
|
|
67
|
+
wildcardKey: params.wildcardKey
|
|
68
|
+
});
|
|
69
|
+
if (direct.entry && direct.key) return {
|
|
70
|
+
...direct,
|
|
71
|
+
matchKey: direct.key,
|
|
72
|
+
matchSource: "direct"
|
|
73
|
+
};
|
|
74
|
+
const normalizeKey = params.normalizeKey;
|
|
75
|
+
if (normalizeKey) {
|
|
76
|
+
const normalizedKeys = params.keys.map((key) => normalizeKey(key)).filter(Boolean);
|
|
77
|
+
if (normalizedKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
|
|
78
|
+
const normalizedEntry = normalizeKey(entryKey);
|
|
79
|
+
if (normalizedEntry && normalizedKeys.includes(normalizedEntry)) return {
|
|
80
|
+
...direct,
|
|
81
|
+
entry,
|
|
82
|
+
key: entryKey,
|
|
83
|
+
matchKey: entryKey,
|
|
84
|
+
matchSource: "direct"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const parentKeys = params.parentKeys ?? [];
|
|
89
|
+
if (parentKeys.length > 0) {
|
|
90
|
+
const parent = resolveChannelEntryMatch({
|
|
91
|
+
entries: params.entries,
|
|
92
|
+
keys: parentKeys
|
|
93
|
+
});
|
|
94
|
+
if (parent.entry && parent.key) return {
|
|
95
|
+
...direct,
|
|
96
|
+
entry: parent.entry,
|
|
97
|
+
key: parent.key,
|
|
98
|
+
parentEntry: parent.entry,
|
|
99
|
+
parentKey: parent.key,
|
|
100
|
+
matchKey: parent.key,
|
|
101
|
+
matchSource: "parent"
|
|
102
|
+
};
|
|
103
|
+
if (normalizeKey) {
|
|
104
|
+
const normalizedParentKeys = parentKeys.map((key) => normalizeKey(key)).filter(Boolean);
|
|
105
|
+
if (normalizedParentKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
|
|
106
|
+
const normalizedEntry = normalizeKey(entryKey);
|
|
107
|
+
if (normalizedEntry && normalizedParentKeys.includes(normalizedEntry)) return {
|
|
108
|
+
...direct,
|
|
109
|
+
entry,
|
|
110
|
+
key: entryKey,
|
|
111
|
+
parentEntry: entry,
|
|
112
|
+
parentKey: entryKey,
|
|
113
|
+
matchKey: entryKey,
|
|
114
|
+
matchSource: "parent"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (direct.wildcardEntry && direct.wildcardKey) return {
|
|
120
|
+
...direct,
|
|
121
|
+
entry: direct.wildcardEntry,
|
|
122
|
+
key: direct.wildcardKey,
|
|
123
|
+
matchKey: direct.wildcardKey,
|
|
124
|
+
matchSource: "wildcard"
|
|
125
|
+
};
|
|
126
|
+
return direct;
|
|
127
|
+
}
|
|
128
|
+
function resolveNestedAllowlistDecision(params) {
|
|
129
|
+
if (!params.outerConfigured) return true;
|
|
130
|
+
if (!params.outerMatched) return false;
|
|
131
|
+
if (!params.innerConfigured) return true;
|
|
132
|
+
return params.innerMatched;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/discord/directory-cache.ts
|
|
137
|
+
const DISCORD_DIRECTORY_CACHE_MAX_ENTRIES = 4e3;
|
|
138
|
+
const DISCORD_DISCRIMINATOR_SUFFIX = /#\d{4}$/;
|
|
139
|
+
const DIRECTORY_HANDLE_CACHE = /* @__PURE__ */ new Map();
|
|
140
|
+
function normalizeAccountCacheKey(accountId) {
|
|
141
|
+
return normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID) || DEFAULT_ACCOUNT_ID;
|
|
142
|
+
}
|
|
143
|
+
function normalizeSnowflake$1(value) {
|
|
144
|
+
const text = String(value ?? "").trim();
|
|
145
|
+
if (!/^\d+$/.test(text)) return null;
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
function normalizeHandleKey(raw) {
|
|
149
|
+
let handle = raw.trim();
|
|
150
|
+
if (!handle) return null;
|
|
151
|
+
if (handle.startsWith("@")) handle = handle.slice(1).trim();
|
|
152
|
+
if (!handle || /\s/.test(handle)) return null;
|
|
153
|
+
return handle.toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
function ensureAccountCache(accountId) {
|
|
156
|
+
const cacheKey = normalizeAccountCacheKey(accountId);
|
|
157
|
+
const existing = DIRECTORY_HANDLE_CACHE.get(cacheKey);
|
|
158
|
+
if (existing) return existing;
|
|
159
|
+
const created = /* @__PURE__ */ new Map();
|
|
160
|
+
DIRECTORY_HANDLE_CACHE.set(cacheKey, created);
|
|
161
|
+
return created;
|
|
162
|
+
}
|
|
163
|
+
function setCacheEntry(cache, key, userId) {
|
|
164
|
+
if (cache.has(key)) cache.delete(key);
|
|
165
|
+
cache.set(key, userId);
|
|
166
|
+
if (cache.size <= DISCORD_DIRECTORY_CACHE_MAX_ENTRIES) return;
|
|
167
|
+
const oldest = cache.keys().next();
|
|
168
|
+
if (!oldest.done) cache.delete(oldest.value);
|
|
169
|
+
}
|
|
170
|
+
function rememberDiscordDirectoryUser(params) {
|
|
171
|
+
const userId = normalizeSnowflake$1(params.userId);
|
|
172
|
+
if (!userId) return;
|
|
173
|
+
const cache = ensureAccountCache(params.accountId);
|
|
174
|
+
for (const candidate of params.handles) {
|
|
175
|
+
if (typeof candidate !== "string") continue;
|
|
176
|
+
const handle = normalizeHandleKey(candidate);
|
|
177
|
+
if (!handle) continue;
|
|
178
|
+
setCacheEntry(cache, handle, userId);
|
|
179
|
+
const withoutDiscriminator = handle.replace(DISCORD_DISCRIMINATOR_SUFFIX, "");
|
|
180
|
+
if (withoutDiscriminator && withoutDiscriminator !== handle) setCacheEntry(cache, withoutDiscriminator, userId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function resolveDiscordDirectoryUserId(params) {
|
|
184
|
+
const cache = DIRECTORY_HANDLE_CACHE.get(normalizeAccountCacheKey(params.accountId));
|
|
185
|
+
if (!cache) return;
|
|
186
|
+
const handle = normalizeHandleKey(params.handle);
|
|
187
|
+
if (!handle) return;
|
|
188
|
+
const direct = cache.get(handle);
|
|
189
|
+
if (direct) return direct;
|
|
190
|
+
const withoutDiscriminator = handle.replace(DISCORD_DISCRIMINATOR_SUFFIX, "");
|
|
191
|
+
if (!withoutDiscriminator || withoutDiscriminator === handle) return;
|
|
192
|
+
return cache.get(withoutDiscriminator);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/discord/api.ts
|
|
197
|
+
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
198
|
+
const DISCORD_API_RETRY_DEFAULTS = {
|
|
199
|
+
attempts: 3,
|
|
200
|
+
minDelayMs: 500,
|
|
201
|
+
maxDelayMs: 3e4,
|
|
202
|
+
jitter: .1
|
|
203
|
+
};
|
|
204
|
+
function parseDiscordApiErrorPayload(text) {
|
|
205
|
+
const trimmed = text.trim();
|
|
206
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
207
|
+
try {
|
|
208
|
+
const payload = JSON.parse(trimmed);
|
|
209
|
+
if (payload && typeof payload === "object") return payload;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function parseRetryAfterSeconds(text, response) {
|
|
216
|
+
const payload = parseDiscordApiErrorPayload(text);
|
|
217
|
+
const retryAfter = payload && typeof payload.retry_after === "number" && Number.isFinite(payload.retry_after) ? payload.retry_after : void 0;
|
|
218
|
+
if (retryAfter !== void 0) return retryAfter;
|
|
219
|
+
const header = response.headers.get("Retry-After");
|
|
220
|
+
if (!header) return;
|
|
221
|
+
const parsed = Number(header);
|
|
222
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
223
|
+
}
|
|
224
|
+
function formatRetryAfterSeconds(value) {
|
|
225
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) return;
|
|
226
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value).toString()}s`;
|
|
227
|
+
}
|
|
228
|
+
function formatDiscordApiErrorText(text) {
|
|
229
|
+
const trimmed = text.trim();
|
|
230
|
+
if (!trimmed) return;
|
|
231
|
+
const payload = parseDiscordApiErrorPayload(trimmed);
|
|
232
|
+
if (!payload) return trimmed.startsWith("{") && trimmed.endsWith("}") ? "unknown error" : trimmed;
|
|
233
|
+
const message = typeof payload.message === "string" && payload.message.trim() ? payload.message.trim() : "unknown error";
|
|
234
|
+
const retryAfter = formatRetryAfterSeconds(typeof payload.retry_after === "number" ? payload.retry_after : void 0);
|
|
235
|
+
return retryAfter ? `${message} (retry after ${retryAfter})` : message;
|
|
236
|
+
}
|
|
237
|
+
var DiscordApiError = class extends Error {
|
|
238
|
+
constructor(message, status, retryAfter) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.status = status;
|
|
241
|
+
this.retryAfter = retryAfter;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
async function fetchDiscord(path, token, fetcher = fetch, options) {
|
|
245
|
+
const fetchImpl = resolveFetch(fetcher);
|
|
246
|
+
if (!fetchImpl) throw new Error("fetch is not available");
|
|
247
|
+
return retryAsync(async () => {
|
|
248
|
+
const res = await fetchImpl(`${DISCORD_API_BASE}${path}`, { headers: { Authorization: `Bot ${token}` } });
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
const text = await res.text().catch(() => "");
|
|
251
|
+
const detail = formatDiscordApiErrorText(text);
|
|
252
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
253
|
+
const retryAfter = res.status === 429 ? parseRetryAfterSeconds(text, res) : void 0;
|
|
254
|
+
throw new DiscordApiError(`Discord API ${path} failed (${res.status})${suffix}`, res.status, retryAfter);
|
|
255
|
+
}
|
|
256
|
+
return await res.json();
|
|
257
|
+
}, {
|
|
258
|
+
...resolveRetryConfig(DISCORD_API_RETRY_DEFAULTS, options?.retry),
|
|
259
|
+
label: options?.label ?? path,
|
|
260
|
+
shouldRetry: (err) => err instanceof DiscordApiError && err.status === 429,
|
|
261
|
+
retryAfterMs: (err) => err instanceof DiscordApiError && typeof err.retryAfter === "number" ? err.retryAfter * 1e3 : void 0
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/discord/monitor/format.ts
|
|
267
|
+
function resolveDiscordSystemLocation(params) {
|
|
268
|
+
const { isDirectMessage, isGroupDm, guild, channelName } = params;
|
|
269
|
+
if (isDirectMessage) return "DM";
|
|
270
|
+
if (isGroupDm) return `Group DM #${channelName}`;
|
|
271
|
+
return guild?.name ? `${guild.name} #${channelName}` : `#${channelName}`;
|
|
272
|
+
}
|
|
273
|
+
function formatDiscordReactionEmoji(emoji) {
|
|
274
|
+
if (emoji.id && emoji.name) return `<:${emoji.name}:${emoji.id}>`;
|
|
275
|
+
if (emoji.id) return `emoji:${emoji.id}`;
|
|
276
|
+
return emoji.name ?? "emoji";
|
|
277
|
+
}
|
|
278
|
+
function formatDiscordUserTag(user) {
|
|
279
|
+
const discriminator = (user.discriminator ?? "").trim();
|
|
280
|
+
if (discriminator && discriminator !== "0") return `${user.username}#${discriminator}`;
|
|
281
|
+
return user.username ?? user.id;
|
|
282
|
+
}
|
|
283
|
+
function resolveTimestampMs(timestamp) {
|
|
284
|
+
if (!timestamp) return;
|
|
285
|
+
const parsed = Date.parse(timestamp);
|
|
286
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/discord/monitor/allow-list.ts
|
|
291
|
+
const DISCORD_OWNER_ALLOWLIST_PREFIXES = [
|
|
292
|
+
"discord:",
|
|
293
|
+
"user:",
|
|
294
|
+
"pk:"
|
|
295
|
+
];
|
|
296
|
+
function normalizeDiscordAllowList(raw, prefixes) {
|
|
297
|
+
if (!raw || raw.length === 0) return null;
|
|
298
|
+
const ids = /* @__PURE__ */ new Set();
|
|
299
|
+
const names = /* @__PURE__ */ new Set();
|
|
300
|
+
const allowAll = raw.some((entry) => String(entry).trim() === "*");
|
|
301
|
+
for (const entry of raw) {
|
|
302
|
+
const text = String(entry).trim();
|
|
303
|
+
if (!text || text === "*") continue;
|
|
304
|
+
const normalized = normalizeDiscordSlug(text);
|
|
305
|
+
const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
|
306
|
+
if (/^\d+$/.test(maybeId)) {
|
|
307
|
+
ids.add(maybeId);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const prefix = prefixes.find((entry) => text.startsWith(entry));
|
|
311
|
+
if (prefix) {
|
|
312
|
+
const candidate = text.slice(prefix.length);
|
|
313
|
+
if (candidate) ids.add(candidate);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (normalized) names.add(normalized);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
allowAll,
|
|
320
|
+
ids,
|
|
321
|
+
names
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function normalizeDiscordSlug(value) {
|
|
325
|
+
return value.trim().toLowerCase().replace(/^#/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
326
|
+
}
|
|
327
|
+
function allowListMatches(list, candidate, params) {
|
|
328
|
+
if (list.allowAll) return true;
|
|
329
|
+
if (candidate.id && list.ids.has(candidate.id)) return true;
|
|
330
|
+
if (params?.allowNameMatching === true) {
|
|
331
|
+
const slug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
|
|
332
|
+
if (slug && list.names.has(slug)) return true;
|
|
333
|
+
if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag))) return true;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
function resolveDiscordAllowListMatch(params) {
|
|
338
|
+
const { allowList, candidate } = params;
|
|
339
|
+
if (allowList.allowAll) return {
|
|
340
|
+
allowed: true,
|
|
341
|
+
matchKey: "*",
|
|
342
|
+
matchSource: "wildcard"
|
|
343
|
+
};
|
|
344
|
+
if (candidate.id && allowList.ids.has(candidate.id)) return {
|
|
345
|
+
allowed: true,
|
|
346
|
+
matchKey: candidate.id,
|
|
347
|
+
matchSource: "id"
|
|
348
|
+
};
|
|
349
|
+
if (params.allowNameMatching === true) {
|
|
350
|
+
const nameSlug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
|
|
351
|
+
if (nameSlug && allowList.names.has(nameSlug)) return {
|
|
352
|
+
allowed: true,
|
|
353
|
+
matchKey: nameSlug,
|
|
354
|
+
matchSource: "name"
|
|
355
|
+
};
|
|
356
|
+
const tagSlug = candidate.tag ? normalizeDiscordSlug(candidate.tag) : "";
|
|
357
|
+
if (tagSlug && allowList.names.has(tagSlug)) return {
|
|
358
|
+
allowed: true,
|
|
359
|
+
matchKey: tagSlug,
|
|
360
|
+
matchSource: "tag"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return { allowed: false };
|
|
364
|
+
}
|
|
365
|
+
function resolveDiscordUserAllowed(params) {
|
|
366
|
+
const allowList = normalizeDiscordAllowList(params.allowList, [
|
|
367
|
+
"discord:",
|
|
368
|
+
"user:",
|
|
369
|
+
"pk:"
|
|
370
|
+
]);
|
|
371
|
+
if (!allowList) return true;
|
|
372
|
+
return allowListMatches(allowList, {
|
|
373
|
+
id: params.userId,
|
|
374
|
+
name: params.userName,
|
|
375
|
+
tag: params.userTag
|
|
376
|
+
}, { allowNameMatching: params.allowNameMatching });
|
|
377
|
+
}
|
|
378
|
+
function resolveDiscordRoleAllowed(params) {
|
|
379
|
+
const allowList = normalizeDiscordAllowList(params.allowList, ["role:"]);
|
|
380
|
+
if (!allowList) return true;
|
|
381
|
+
if (allowList.allowAll) return true;
|
|
382
|
+
return params.memberRoleIds.some((roleId) => allowList.ids.has(roleId));
|
|
383
|
+
}
|
|
384
|
+
function resolveDiscordMemberAllowed(params) {
|
|
385
|
+
const hasUserRestriction = Array.isArray(params.userAllowList) && params.userAllowList.length > 0;
|
|
386
|
+
const hasRoleRestriction = Array.isArray(params.roleAllowList) && params.roleAllowList.length > 0;
|
|
387
|
+
if (!hasUserRestriction && !hasRoleRestriction) return true;
|
|
388
|
+
const userOk = hasUserRestriction ? resolveDiscordUserAllowed({
|
|
389
|
+
allowList: params.userAllowList,
|
|
390
|
+
userId: params.userId,
|
|
391
|
+
userName: params.userName,
|
|
392
|
+
userTag: params.userTag,
|
|
393
|
+
allowNameMatching: params.allowNameMatching
|
|
394
|
+
}) : false;
|
|
395
|
+
const roleOk = hasRoleRestriction ? resolveDiscordRoleAllowed({
|
|
396
|
+
allowList: params.roleAllowList,
|
|
397
|
+
memberRoleIds: params.memberRoleIds
|
|
398
|
+
}) : false;
|
|
399
|
+
return userOk || roleOk;
|
|
400
|
+
}
|
|
401
|
+
function resolveDiscordMemberAccessState(params) {
|
|
402
|
+
const channelUsers = params.channelConfig?.users ?? params.guildInfo?.users;
|
|
403
|
+
const channelRoles = params.channelConfig?.roles ?? params.guildInfo?.roles;
|
|
404
|
+
return {
|
|
405
|
+
channelUsers,
|
|
406
|
+
channelRoles,
|
|
407
|
+
hasAccessRestrictions: Array.isArray(channelUsers) && channelUsers.length > 0 || Array.isArray(channelRoles) && channelRoles.length > 0,
|
|
408
|
+
memberAllowed: resolveDiscordMemberAllowed({
|
|
409
|
+
userAllowList: channelUsers,
|
|
410
|
+
roleAllowList: channelRoles,
|
|
411
|
+
memberRoleIds: params.memberRoleIds,
|
|
412
|
+
userId: params.sender.id,
|
|
413
|
+
userName: params.sender.name,
|
|
414
|
+
userTag: params.sender.tag,
|
|
415
|
+
allowNameMatching: params.allowNameMatching
|
|
416
|
+
})
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function resolveDiscordOwnerAllowFrom(params) {
|
|
420
|
+
const rawAllowList = params.channelConfig?.users ?? params.guildInfo?.users;
|
|
421
|
+
if (!Array.isArray(rawAllowList) || rawAllowList.length === 0) return;
|
|
422
|
+
const allowList = normalizeDiscordAllowList(rawAllowList, [
|
|
423
|
+
"discord:",
|
|
424
|
+
"user:",
|
|
425
|
+
"pk:"
|
|
426
|
+
]);
|
|
427
|
+
if (!allowList) return;
|
|
428
|
+
const match = resolveDiscordAllowListMatch({
|
|
429
|
+
allowList,
|
|
430
|
+
candidate: {
|
|
431
|
+
id: params.sender.id,
|
|
432
|
+
name: params.sender.name,
|
|
433
|
+
tag: params.sender.tag
|
|
434
|
+
},
|
|
435
|
+
allowNameMatching: params.allowNameMatching
|
|
436
|
+
});
|
|
437
|
+
if (!match.allowed || !match.matchKey || match.matchKey === "*") return;
|
|
438
|
+
return [match.matchKey];
|
|
439
|
+
}
|
|
440
|
+
function resolveDiscordOwnerAccess(params) {
|
|
441
|
+
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, DISCORD_OWNER_ALLOWLIST_PREFIXES);
|
|
442
|
+
return {
|
|
443
|
+
ownerAllowList,
|
|
444
|
+
ownerAllowed: ownerAllowList ? allowListMatches(ownerAllowList, {
|
|
445
|
+
id: params.sender.id,
|
|
446
|
+
name: params.sender.name,
|
|
447
|
+
tag: params.sender.tag
|
|
448
|
+
}, { allowNameMatching: params.allowNameMatching }) : false
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function resolveDiscordGuildEntry(params) {
|
|
452
|
+
const guild = params.guild;
|
|
453
|
+
const entries = params.guildEntries;
|
|
454
|
+
if (!guild || !entries) return null;
|
|
455
|
+
const byId = entries[guild.id];
|
|
456
|
+
if (byId) return {
|
|
457
|
+
...byId,
|
|
458
|
+
id: guild.id
|
|
459
|
+
};
|
|
460
|
+
const slug = normalizeDiscordSlug(guild.name ?? "");
|
|
461
|
+
const bySlug = entries[slug];
|
|
462
|
+
if (bySlug) return {
|
|
463
|
+
...bySlug,
|
|
464
|
+
id: guild.id,
|
|
465
|
+
slug: slug || bySlug.slug
|
|
466
|
+
};
|
|
467
|
+
const wildcard = entries["*"];
|
|
468
|
+
if (wildcard) return {
|
|
469
|
+
...wildcard,
|
|
470
|
+
id: guild.id,
|
|
471
|
+
slug: slug || wildcard.slug
|
|
472
|
+
};
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
function buildDiscordChannelKeys(params) {
|
|
476
|
+
const allowNameMatch = params.allowNameMatch !== false;
|
|
477
|
+
return buildChannelKeyCandidates(params.id, allowNameMatch ? params.slug : void 0, allowNameMatch ? params.name : void 0);
|
|
478
|
+
}
|
|
479
|
+
function resolveDiscordChannelEntryMatch(channels, params, parentParams) {
|
|
480
|
+
return resolveChannelEntryMatchWithFallback({
|
|
481
|
+
entries: channels,
|
|
482
|
+
keys: buildDiscordChannelKeys(params),
|
|
483
|
+
parentKeys: parentParams ? buildDiscordChannelKeys(parentParams) : void 0,
|
|
484
|
+
wildcardKey: "*"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function hasConfiguredDiscordChannels(channels) {
|
|
488
|
+
return Boolean(channels && Object.keys(channels).length > 0);
|
|
489
|
+
}
|
|
490
|
+
function resolveDiscordChannelConfigEntry(entry) {
|
|
491
|
+
return {
|
|
492
|
+
allowed: entry.allow !== false,
|
|
493
|
+
requireMention: entry.requireMention,
|
|
494
|
+
ignoreOtherMentions: entry.ignoreOtherMentions,
|
|
495
|
+
skills: entry.skills,
|
|
496
|
+
enabled: entry.enabled,
|
|
497
|
+
users: entry.users,
|
|
498
|
+
roles: entry.roles,
|
|
499
|
+
systemPrompt: entry.systemPrompt,
|
|
500
|
+
includeThreadStarter: entry.includeThreadStarter,
|
|
501
|
+
autoThread: entry.autoThread
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function resolveDiscordChannelConfigWithFallback(params) {
|
|
505
|
+
const { guildInfo, channelId, channelName, channelSlug, parentId, parentName, parentSlug, scope } = params;
|
|
506
|
+
const channels = guildInfo?.channels;
|
|
507
|
+
if (!hasConfiguredDiscordChannels(channels)) return null;
|
|
508
|
+
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
|
|
509
|
+
return resolveChannelMatchConfig(resolveDiscordChannelEntryMatch(channels, {
|
|
510
|
+
id: channelId,
|
|
511
|
+
name: channelName,
|
|
512
|
+
slug: channelSlug,
|
|
513
|
+
allowNameMatch: scope !== "thread"
|
|
514
|
+
}, parentId || parentName || parentSlug ? {
|
|
515
|
+
id: parentId ?? "",
|
|
516
|
+
name: parentName,
|
|
517
|
+
slug: resolvedParentSlug
|
|
518
|
+
} : void 0), resolveDiscordChannelConfigEntry) ?? { allowed: false };
|
|
519
|
+
}
|
|
520
|
+
function resolveDiscordShouldRequireMention(params) {
|
|
521
|
+
if (!params.isGuildMessage) return false;
|
|
522
|
+
if (params.isAutoThreadOwnedByBot ?? isDiscordAutoThreadOwnedByBot(params)) return false;
|
|
523
|
+
return params.channelConfig?.requireMention ?? params.guildInfo?.requireMention ?? true;
|
|
524
|
+
}
|
|
525
|
+
function isDiscordAutoThreadOwnedByBot(params) {
|
|
526
|
+
if (!params.isThread) return false;
|
|
527
|
+
if (!params.channelConfig?.autoThread) return false;
|
|
528
|
+
const botId = params.botId?.trim();
|
|
529
|
+
const threadOwnerId = params.threadOwnerId?.trim();
|
|
530
|
+
return Boolean(botId && threadOwnerId && botId === threadOwnerId);
|
|
531
|
+
}
|
|
532
|
+
function isDiscordGroupAllowedByPolicy(params) {
|
|
533
|
+
const { groupPolicy, guildAllowlisted, channelAllowlistConfigured, channelAllowed } = params;
|
|
534
|
+
if (groupPolicy === "disabled") return false;
|
|
535
|
+
if (groupPolicy === "open") return true;
|
|
536
|
+
if (!guildAllowlisted) return false;
|
|
537
|
+
if (!channelAllowlistConfigured) return true;
|
|
538
|
+
return channelAllowed;
|
|
539
|
+
}
|
|
540
|
+
function resolveGroupDmAllow(params) {
|
|
541
|
+
const { channels, channelId, channelName, channelSlug } = params;
|
|
542
|
+
if (!channels || channels.length === 0) return true;
|
|
543
|
+
const allowList = new Set(channels.map((entry) => normalizeDiscordSlug(String(entry))));
|
|
544
|
+
const candidates = [
|
|
545
|
+
normalizeDiscordSlug(channelId),
|
|
546
|
+
channelSlug,
|
|
547
|
+
channelName ? normalizeDiscordSlug(channelName) : ""
|
|
548
|
+
].filter(Boolean);
|
|
549
|
+
return allowList.has("*") || candidates.some((candidate) => allowList.has(candidate));
|
|
550
|
+
}
|
|
551
|
+
function shouldEmitDiscordReactionNotification(params) {
|
|
552
|
+
const mode = params.mode ?? "own";
|
|
553
|
+
if (mode === "off") return false;
|
|
554
|
+
if (mode === "all") return true;
|
|
555
|
+
if (mode === "own") return Boolean(params.botId && params.messageAuthorId === params.botId);
|
|
556
|
+
if (mode === "allowlist") {
|
|
557
|
+
const list = normalizeDiscordAllowList(params.allowlist, [
|
|
558
|
+
"discord:",
|
|
559
|
+
"user:",
|
|
560
|
+
"pk:"
|
|
561
|
+
]);
|
|
562
|
+
if (!list) return false;
|
|
563
|
+
return allowListMatches(list, {
|
|
564
|
+
id: params.userId,
|
|
565
|
+
name: params.userName,
|
|
566
|
+
tag: params.userTag
|
|
567
|
+
}, { allowNameMatching: params.allowNameMatching });
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/discord/directory-live.ts
|
|
574
|
+
function normalizeQuery(value) {
|
|
575
|
+
return value?.trim().toLowerCase() ?? "";
|
|
576
|
+
}
|
|
577
|
+
function buildUserRank(user) {
|
|
578
|
+
return user.bot ? 0 : 1;
|
|
579
|
+
}
|
|
580
|
+
function resolveDiscordDirectoryAccess(params) {
|
|
581
|
+
const token = normalizeDiscordToken(resolveDiscordAccount({
|
|
582
|
+
cfg: params.cfg,
|
|
583
|
+
accountId: params.accountId
|
|
584
|
+
}).token, "channels.discord.token");
|
|
585
|
+
if (!token) return null;
|
|
586
|
+
return {
|
|
587
|
+
token,
|
|
588
|
+
query: normalizeQuery(params.query)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function listDiscordGuilds(token) {
|
|
592
|
+
return (await fetchDiscord("/users/@me/guilds", token)).filter((guild) => guild.id && guild.name);
|
|
593
|
+
}
|
|
594
|
+
async function listDiscordDirectoryGroupsLive(params) {
|
|
595
|
+
const access = resolveDiscordDirectoryAccess(params);
|
|
596
|
+
if (!access) return [];
|
|
597
|
+
const { token, query } = access;
|
|
598
|
+
const guilds = await listDiscordGuilds(token);
|
|
599
|
+
const rows = [];
|
|
600
|
+
for (const guild of guilds) {
|
|
601
|
+
const channels = await fetchDiscord(`/guilds/${guild.id}/channels`, token);
|
|
602
|
+
for (const channel of channels) {
|
|
603
|
+
const name = channel.name?.trim();
|
|
604
|
+
if (!name) continue;
|
|
605
|
+
if (query && !normalizeDiscordSlug(name).includes(normalizeDiscordSlug(query))) continue;
|
|
606
|
+
rows.push({
|
|
607
|
+
kind: "group",
|
|
608
|
+
id: `channel:${channel.id}`,
|
|
609
|
+
name,
|
|
610
|
+
handle: `#${name}`,
|
|
611
|
+
raw: channel
|
|
612
|
+
});
|
|
613
|
+
if (typeof params.limit === "number" && params.limit > 0 && rows.length >= params.limit) return rows;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return rows;
|
|
617
|
+
}
|
|
618
|
+
async function listDiscordDirectoryPeersLive(params) {
|
|
619
|
+
const access = resolveDiscordDirectoryAccess(params);
|
|
620
|
+
if (!access) return [];
|
|
621
|
+
const { token, query } = access;
|
|
622
|
+
if (!query) return [];
|
|
623
|
+
const guilds = await listDiscordGuilds(token);
|
|
624
|
+
const rows = [];
|
|
625
|
+
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25;
|
|
626
|
+
for (const guild of guilds) {
|
|
627
|
+
const paramsObj = new URLSearchParams({
|
|
628
|
+
query,
|
|
629
|
+
limit: String(Math.min(limit, 100))
|
|
630
|
+
});
|
|
631
|
+
const members = await fetchDiscord(`/guilds/${guild.id}/members/search?${paramsObj.toString()}`, token);
|
|
632
|
+
for (const member of members) {
|
|
633
|
+
const user = member.user;
|
|
634
|
+
if (!user?.id) continue;
|
|
635
|
+
rememberDiscordDirectoryUser({
|
|
636
|
+
accountId: params.accountId,
|
|
637
|
+
userId: user.id,
|
|
638
|
+
handles: [
|
|
639
|
+
user.username,
|
|
640
|
+
user.global_name,
|
|
641
|
+
member.nick,
|
|
642
|
+
user.username ? `@${user.username}` : null
|
|
643
|
+
]
|
|
644
|
+
});
|
|
645
|
+
const name = member.nick?.trim() || user.global_name?.trim() || user.username?.trim();
|
|
646
|
+
rows.push({
|
|
647
|
+
kind: "user",
|
|
648
|
+
id: `user:${user.id}`,
|
|
649
|
+
name: name || void 0,
|
|
650
|
+
handle: user.username ? `@${user.username}` : void 0,
|
|
651
|
+
rank: buildUserRank(user),
|
|
652
|
+
raw: member
|
|
653
|
+
});
|
|
654
|
+
if (rows.length >= limit) return rows;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return rows;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/discord/targets.ts
|
|
662
|
+
function parseDiscordTarget(raw, options = {}) {
|
|
663
|
+
const trimmed = raw.trim();
|
|
664
|
+
if (!trimmed) return;
|
|
665
|
+
const userTarget = parseMentionPrefixOrAtUserTarget({
|
|
666
|
+
raw: trimmed,
|
|
667
|
+
mentionPattern: /^<@!?(\d+)>$/,
|
|
668
|
+
prefixes: [
|
|
669
|
+
{
|
|
670
|
+
prefix: "user:",
|
|
671
|
+
kind: "user"
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
prefix: "channel:",
|
|
675
|
+
kind: "channel"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
prefix: "discord:",
|
|
679
|
+
kind: "user"
|
|
680
|
+
}
|
|
681
|
+
],
|
|
682
|
+
atUserPattern: /^\d+$/,
|
|
683
|
+
atUserErrorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)"
|
|
684
|
+
});
|
|
685
|
+
if (userTarget) return userTarget;
|
|
686
|
+
if (/^\d+$/.test(trimmed)) {
|
|
687
|
+
if (options.defaultKind) return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
|
|
688
|
+
throw new Error(options.ambiguousMessage ?? `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`);
|
|
689
|
+
}
|
|
690
|
+
return buildMessagingTarget("channel", trimmed, trimmed);
|
|
691
|
+
}
|
|
692
|
+
function resolveDiscordChannelId(raw) {
|
|
693
|
+
return requireTargetKind({
|
|
694
|
+
platform: "Discord",
|
|
695
|
+
target: parseDiscordTarget(raw, { defaultKind: "channel" }),
|
|
696
|
+
kind: "channel"
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Resolve a Discord username to user ID using the directory lookup.
|
|
701
|
+
* This enables sending DMs by username instead of requiring explicit user IDs.
|
|
702
|
+
*
|
|
703
|
+
* @param raw - The username or raw target string (e.g., "john.doe")
|
|
704
|
+
* @param options - Directory configuration params (cfg, accountId, limit)
|
|
705
|
+
* @param parseOptions - Messaging target parsing options (defaults, ambiguity message)
|
|
706
|
+
* @returns Parsed MessagingTarget with user ID, or undefined if not found
|
|
707
|
+
*/
|
|
708
|
+
async function resolveDiscordTarget(raw, options, parseOptions = {}) {
|
|
709
|
+
const trimmed = raw.trim();
|
|
710
|
+
if (!trimmed) return;
|
|
711
|
+
const likelyUsername = isLikelyUsername(trimmed);
|
|
712
|
+
const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
|
|
713
|
+
const directParse = safeParseDiscordTarget(trimmed, parseOptions);
|
|
714
|
+
if (directParse && directParse.kind !== "channel" && !likelyUsername) return directParse;
|
|
715
|
+
if (!shouldLookup) return directParse ?? parseDiscordTarget(trimmed, parseOptions);
|
|
716
|
+
try {
|
|
717
|
+
const match = (await listDiscordDirectoryPeersLive({
|
|
718
|
+
...options,
|
|
719
|
+
query: trimmed,
|
|
720
|
+
limit: 1
|
|
721
|
+
}))[0];
|
|
722
|
+
if (match && match.kind === "user") {
|
|
723
|
+
const userId = match.id.replace(/^user:/, "");
|
|
724
|
+
rememberDiscordDirectoryUser({
|
|
725
|
+
accountId: options.accountId,
|
|
726
|
+
userId,
|
|
727
|
+
handles: [
|
|
728
|
+
trimmed,
|
|
729
|
+
match.name,
|
|
730
|
+
match.handle
|
|
731
|
+
]
|
|
732
|
+
});
|
|
733
|
+
return buildMessagingTarget("user", userId, trimmed);
|
|
734
|
+
}
|
|
735
|
+
} catch {}
|
|
736
|
+
return parseDiscordTarget(trimmed, parseOptions);
|
|
737
|
+
}
|
|
738
|
+
function safeParseDiscordTarget(input, options) {
|
|
739
|
+
try {
|
|
740
|
+
return parseDiscordTarget(input, options);
|
|
741
|
+
} catch {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function isExplicitUserLookup(input, options) {
|
|
746
|
+
if (/^<@!?(\d+)>$/.test(input)) return true;
|
|
747
|
+
if (/^(user:|discord:)/.test(input)) return true;
|
|
748
|
+
if (input.startsWith("@")) return true;
|
|
749
|
+
if (/^\d+$/.test(input)) return options.defaultKind === "user";
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Check if a string looks like a Discord username (not a mention, prefix, or ID).
|
|
754
|
+
* Usernames typically don't start with special characters except underscore.
|
|
755
|
+
*/
|
|
756
|
+
function isLikelyUsername(input) {
|
|
757
|
+
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) return false;
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/discord/client.ts
|
|
763
|
+
function resolveToken(params) {
|
|
764
|
+
const explicit = normalizeDiscordToken(params.explicit, "channels.discord.token");
|
|
765
|
+
if (explicit) return explicit;
|
|
766
|
+
const fallback = normalizeDiscordToken(params.fallbackToken, "channels.discord.token");
|
|
767
|
+
if (!fallback) throw new Error(`Discord bot token missing for account "${params.accountId}" (set discord.accounts.${params.accountId}.token or DISCORD_BOT_TOKEN for default).`);
|
|
768
|
+
return fallback;
|
|
769
|
+
}
|
|
770
|
+
function resolveRest(token, rest) {
|
|
771
|
+
return rest ?? new RequestClient(token);
|
|
772
|
+
}
|
|
773
|
+
function createDiscordRestClient(opts, cfg = loadConfig()) {
|
|
774
|
+
const account = resolveDiscordAccount({
|
|
775
|
+
cfg,
|
|
776
|
+
accountId: opts.accountId
|
|
777
|
+
});
|
|
778
|
+
const token = resolveToken({
|
|
779
|
+
explicit: opts.token,
|
|
780
|
+
accountId: account.accountId,
|
|
781
|
+
fallbackToken: account.token
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
token,
|
|
785
|
+
rest: resolveRest(token, opts.rest),
|
|
786
|
+
account
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function createDiscordClient(opts, cfg = loadConfig()) {
|
|
790
|
+
const { token, rest, account } = createDiscordRestClient(opts, cfg);
|
|
791
|
+
return {
|
|
792
|
+
token,
|
|
793
|
+
rest,
|
|
794
|
+
request: createDiscordRetryRunner({
|
|
795
|
+
retry: opts.retry,
|
|
796
|
+
configRetry: account.config.retry,
|
|
797
|
+
verbose: opts.verbose
|
|
798
|
+
})
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function resolveDiscordRest(opts) {
|
|
802
|
+
return createDiscordRestClient(opts).rest;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/discord/chunk.ts
|
|
807
|
+
const DEFAULT_MAX_CHARS = 2e3;
|
|
808
|
+
const DEFAULT_MAX_LINES = 17;
|
|
809
|
+
const FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
810
|
+
function countLines(text) {
|
|
811
|
+
if (!text) return 0;
|
|
812
|
+
return text.split("\n").length;
|
|
813
|
+
}
|
|
814
|
+
function parseFenceLine(line) {
|
|
815
|
+
const match = line.match(FENCE_RE);
|
|
816
|
+
if (!match) return null;
|
|
817
|
+
const indent = match[1] ?? "";
|
|
818
|
+
const marker = match[2] ?? "";
|
|
819
|
+
return {
|
|
820
|
+
indent,
|
|
821
|
+
markerChar: marker[0] ?? "`",
|
|
822
|
+
markerLen: marker.length,
|
|
823
|
+
openLine: line
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function closeFenceLine(openFence) {
|
|
827
|
+
return `${openFence.indent}${openFence.markerChar.repeat(openFence.markerLen)}`;
|
|
828
|
+
}
|
|
829
|
+
function closeFenceIfNeeded(text, openFence) {
|
|
830
|
+
if (!openFence) return text;
|
|
831
|
+
const closeLine = closeFenceLine(openFence);
|
|
832
|
+
if (!text) return closeLine;
|
|
833
|
+
if (!text.endsWith("\n")) return `${text}\n${closeLine}`;
|
|
834
|
+
return `${text}${closeLine}`;
|
|
835
|
+
}
|
|
836
|
+
function splitLongLine(line, maxChars, opts) {
|
|
837
|
+
const limit = Math.max(1, Math.floor(maxChars));
|
|
838
|
+
if (line.length <= limit) return [line];
|
|
839
|
+
const out = [];
|
|
840
|
+
let remaining = line;
|
|
841
|
+
while (remaining.length > limit) {
|
|
842
|
+
if (opts.preserveWhitespace) {
|
|
843
|
+
out.push(remaining.slice(0, limit));
|
|
844
|
+
remaining = remaining.slice(limit);
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const window = remaining.slice(0, limit);
|
|
848
|
+
let breakIdx = -1;
|
|
849
|
+
for (let i = window.length - 1; i >= 0; i--) if (/\s/.test(window[i])) {
|
|
850
|
+
breakIdx = i;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (breakIdx <= 0) breakIdx = limit;
|
|
854
|
+
out.push(remaining.slice(0, breakIdx));
|
|
855
|
+
remaining = remaining.slice(breakIdx);
|
|
856
|
+
}
|
|
857
|
+
if (remaining.length) out.push(remaining);
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Chunks outbound Discord text by both character count and (soft) line count,
|
|
862
|
+
* while keeping fenced code blocks balanced across chunks.
|
|
863
|
+
*/
|
|
864
|
+
function chunkDiscordText(text, opts = {}) {
|
|
865
|
+
const maxChars = Math.max(1, Math.floor(opts.maxChars ?? DEFAULT_MAX_CHARS));
|
|
866
|
+
const maxLines = Math.max(1, Math.floor(opts.maxLines ?? DEFAULT_MAX_LINES));
|
|
867
|
+
const body = text ?? "";
|
|
868
|
+
if (!body) return [];
|
|
869
|
+
if (body.length <= maxChars && countLines(body) <= maxLines) return [body];
|
|
870
|
+
const lines = body.split("\n");
|
|
871
|
+
const chunks = [];
|
|
872
|
+
let current = "";
|
|
873
|
+
let currentLines = 0;
|
|
874
|
+
let openFence = null;
|
|
875
|
+
const flush = () => {
|
|
876
|
+
if (!current) return;
|
|
877
|
+
const payload = closeFenceIfNeeded(current, openFence);
|
|
878
|
+
if (payload.trim().length) chunks.push(payload);
|
|
879
|
+
current = "";
|
|
880
|
+
currentLines = 0;
|
|
881
|
+
if (openFence) {
|
|
882
|
+
current = openFence.openLine;
|
|
883
|
+
currentLines = 1;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
for (const originalLine of lines) {
|
|
887
|
+
const fenceInfo = parseFenceLine(originalLine);
|
|
888
|
+
const wasInsideFence = openFence !== null;
|
|
889
|
+
let nextOpenFence = openFence;
|
|
890
|
+
if (fenceInfo) {
|
|
891
|
+
if (!openFence) nextOpenFence = fenceInfo;
|
|
892
|
+
else if (openFence.markerChar === fenceInfo.markerChar && fenceInfo.markerLen >= openFence.markerLen) nextOpenFence = null;
|
|
893
|
+
}
|
|
894
|
+
const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
|
|
895
|
+
const reserveLines = nextOpenFence ? 1 : 0;
|
|
896
|
+
const effectiveMaxChars = maxChars - reserveChars;
|
|
897
|
+
const effectiveMaxLines = maxLines - reserveLines;
|
|
898
|
+
const charLimit = effectiveMaxChars > 0 ? effectiveMaxChars : maxChars;
|
|
899
|
+
const lineLimit = effectiveMaxLines > 0 ? effectiveMaxLines : maxLines;
|
|
900
|
+
const prefixLen = current.length > 0 ? current.length + 1 : 0;
|
|
901
|
+
const segments = splitLongLine(originalLine, Math.max(1, charLimit - prefixLen), { preserveWhitespace: wasInsideFence });
|
|
902
|
+
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
|
|
903
|
+
const segment = segments[segIndex];
|
|
904
|
+
const isLineContinuation = segIndex > 0;
|
|
905
|
+
const addition = `${isLineContinuation ? "" : current.length > 0 ? "\n" : ""}${segment}`;
|
|
906
|
+
const nextLen = current.length + addition.length;
|
|
907
|
+
const nextLines = currentLines + (isLineContinuation ? 0 : 1);
|
|
908
|
+
if ((nextLen > charLimit || nextLines > lineLimit) && current.length > 0) flush();
|
|
909
|
+
if (current.length > 0) {
|
|
910
|
+
current += addition;
|
|
911
|
+
if (!isLineContinuation) currentLines += 1;
|
|
912
|
+
} else {
|
|
913
|
+
current = segment;
|
|
914
|
+
currentLines = 1;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
openFence = nextOpenFence;
|
|
918
|
+
}
|
|
919
|
+
if (current.length) {
|
|
920
|
+
const payload = closeFenceIfNeeded(current, openFence);
|
|
921
|
+
if (payload.trim().length) chunks.push(payload);
|
|
922
|
+
}
|
|
923
|
+
return rebalanceReasoningItalics(text, chunks);
|
|
924
|
+
}
|
|
925
|
+
function chunkDiscordTextWithMode(text, opts) {
|
|
926
|
+
if ((opts.chunkMode ?? "length") !== "newline") return chunkDiscordText(text, opts);
|
|
927
|
+
const lineChunks = chunkMarkdownTextWithMode(text, Math.max(1, Math.floor(opts.maxChars ?? DEFAULT_MAX_CHARS)), "newline");
|
|
928
|
+
const chunks = [];
|
|
929
|
+
for (const line of lineChunks) {
|
|
930
|
+
const nested = chunkDiscordText(line, opts);
|
|
931
|
+
if (!nested.length && line) {
|
|
932
|
+
chunks.push(line);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
chunks.push(...nested);
|
|
936
|
+
}
|
|
937
|
+
return chunks;
|
|
938
|
+
}
|
|
939
|
+
function rebalanceReasoningItalics(source, chunks) {
|
|
940
|
+
if (chunks.length <= 1) return chunks;
|
|
941
|
+
if (!(source.startsWith("Reasoning:\n_") && source.trimEnd().endsWith("_"))) return chunks;
|
|
942
|
+
const adjusted = [...chunks];
|
|
943
|
+
for (let i = 0; i < adjusted.length; i++) {
|
|
944
|
+
const isLast = i === adjusted.length - 1;
|
|
945
|
+
const current = adjusted[i];
|
|
946
|
+
if (!current.trimEnd().endsWith("_")) adjusted[i] = `${current}_`;
|
|
947
|
+
if (isLast) break;
|
|
948
|
+
const next = adjusted[i + 1];
|
|
949
|
+
const leadingWhitespaceLen = next.length - next.trimStart().length;
|
|
950
|
+
const leadingWhitespace = next.slice(0, leadingWhitespaceLen);
|
|
951
|
+
const nextBody = next.slice(leadingWhitespaceLen);
|
|
952
|
+
if (!nextBody.startsWith("_")) adjusted[i + 1] = `${leadingWhitespace}_${nextBody}`;
|
|
953
|
+
}
|
|
954
|
+
return adjusted;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/discord/send.permissions.ts
|
|
959
|
+
const PERMISSION_ENTRIES = Object.entries(PermissionFlagsBits).filter(([, value]) => typeof value === "bigint");
|
|
960
|
+
const ALL_PERMISSIONS = PERMISSION_ENTRIES.reduce((acc, [, value]) => acc | value, 0n);
|
|
961
|
+
const ADMINISTRATOR_BIT = PermissionFlagsBits.Administrator;
|
|
962
|
+
function addPermissionBits(base, add) {
|
|
963
|
+
if (!add) return base;
|
|
964
|
+
return base | BigInt(add);
|
|
965
|
+
}
|
|
966
|
+
function removePermissionBits(base, deny) {
|
|
967
|
+
if (!deny) return base;
|
|
968
|
+
return base & ~BigInt(deny);
|
|
969
|
+
}
|
|
970
|
+
function bitfieldToPermissions(bitfield) {
|
|
971
|
+
return PERMISSION_ENTRIES.filter(([, value]) => (bitfield & value) === value).map(([name]) => name).toSorted();
|
|
972
|
+
}
|
|
973
|
+
function hasAdministrator(bitfield) {
|
|
974
|
+
return (bitfield & ADMINISTRATOR_BIT) === ADMINISTRATOR_BIT;
|
|
975
|
+
}
|
|
976
|
+
function hasPermissionBit(bitfield, permission) {
|
|
977
|
+
return (bitfield & permission) === permission;
|
|
978
|
+
}
|
|
979
|
+
function isThreadChannelType(channelType) {
|
|
980
|
+
return channelType === ChannelType.GuildNewsThread || channelType === ChannelType.GuildPublicThread || channelType === ChannelType.GuildPrivateThread;
|
|
981
|
+
}
|
|
982
|
+
async function fetchBotUserId(rest) {
|
|
983
|
+
const me = await rest.get(Routes.user("@me"));
|
|
984
|
+
if (!me?.id) throw new Error("Failed to resolve bot user id");
|
|
985
|
+
return me.id;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Fetch guild-level permissions for a user. This does not include channel-specific overwrites.
|
|
989
|
+
*/
|
|
990
|
+
async function fetchMemberGuildPermissionsDiscord(guildId, userId, opts = {}) {
|
|
991
|
+
const rest = resolveDiscordRest(opts);
|
|
992
|
+
try {
|
|
993
|
+
const [guild, member] = await Promise.all([rest.get(Routes.guild(guildId)), rest.get(Routes.guildMember(guildId, userId))]);
|
|
994
|
+
const rolesById = new Map((guild.roles ?? []).map((role) => [role.id, role]));
|
|
995
|
+
const everyoneRole = rolesById.get(guildId);
|
|
996
|
+
let permissions = 0n;
|
|
997
|
+
if (everyoneRole?.permissions) permissions = addPermissionBits(permissions, everyoneRole.permissions);
|
|
998
|
+
for (const roleId of member.roles ?? []) {
|
|
999
|
+
const role = rolesById.get(roleId);
|
|
1000
|
+
if (role?.permissions) permissions = addPermissionBits(permissions, role.permissions);
|
|
1001
|
+
}
|
|
1002
|
+
return permissions;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Returns true when the user has ADMINISTRATOR or required permission bits
|
|
1009
|
+
* matching the provided predicate.
|
|
1010
|
+
*/
|
|
1011
|
+
async function hasGuildPermissionsDiscord(guildId, userId, requiredPermissions, check, opts = {}) {
|
|
1012
|
+
const permissions = await fetchMemberGuildPermissionsDiscord(guildId, userId, opts);
|
|
1013
|
+
if (permissions === null) return false;
|
|
1014
|
+
if (hasAdministrator(permissions)) return true;
|
|
1015
|
+
return check(permissions, requiredPermissions);
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Returns true when the user has ADMINISTRATOR or any required permission bit.
|
|
1019
|
+
*/
|
|
1020
|
+
async function hasAnyGuildPermissionDiscord(guildId, userId, requiredPermissions, opts = {}) {
|
|
1021
|
+
return await hasGuildPermissionsDiscord(guildId, userId, requiredPermissions, (permissions, required) => required.some((permission) => hasPermissionBit(permissions, permission)), opts);
|
|
1022
|
+
}
|
|
1023
|
+
async function fetchChannelPermissionsDiscord(channelId, opts = {}) {
|
|
1024
|
+
const rest = resolveDiscordRest(opts);
|
|
1025
|
+
const channel = await rest.get(Routes.channel(channelId));
|
|
1026
|
+
const channelType = "type" in channel ? channel.type : void 0;
|
|
1027
|
+
const guildId = "guild_id" in channel ? channel.guild_id : void 0;
|
|
1028
|
+
if (!guildId) return {
|
|
1029
|
+
channelId,
|
|
1030
|
+
permissions: [],
|
|
1031
|
+
raw: "0",
|
|
1032
|
+
isDm: true,
|
|
1033
|
+
channelType
|
|
1034
|
+
};
|
|
1035
|
+
const botId = await fetchBotUserId(rest);
|
|
1036
|
+
const [guild, member] = await Promise.all([rest.get(Routes.guild(guildId)), rest.get(Routes.guildMember(guildId, botId))]);
|
|
1037
|
+
const rolesById = new Map((guild.roles ?? []).map((role) => [role.id, role]));
|
|
1038
|
+
const everyoneRole = rolesById.get(guildId);
|
|
1039
|
+
let base = 0n;
|
|
1040
|
+
if (everyoneRole?.permissions) base = addPermissionBits(base, everyoneRole.permissions);
|
|
1041
|
+
for (const roleId of member.roles ?? []) {
|
|
1042
|
+
const role = rolesById.get(roleId);
|
|
1043
|
+
if (role?.permissions) base = addPermissionBits(base, role.permissions);
|
|
1044
|
+
}
|
|
1045
|
+
if (hasAdministrator(base)) return {
|
|
1046
|
+
channelId,
|
|
1047
|
+
guildId,
|
|
1048
|
+
permissions: bitfieldToPermissions(ALL_PERMISSIONS),
|
|
1049
|
+
raw: ALL_PERMISSIONS.toString(),
|
|
1050
|
+
isDm: false,
|
|
1051
|
+
channelType
|
|
1052
|
+
};
|
|
1053
|
+
let permissions = base;
|
|
1054
|
+
const overwrites = "permission_overwrites" in channel ? channel.permission_overwrites ?? [] : [];
|
|
1055
|
+
for (const overwrite of overwrites) if (overwrite.id === guildId) {
|
|
1056
|
+
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
|
1057
|
+
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
|
1058
|
+
}
|
|
1059
|
+
for (const overwrite of overwrites) if (member.roles?.includes(overwrite.id)) {
|
|
1060
|
+
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
|
1061
|
+
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
|
1062
|
+
}
|
|
1063
|
+
for (const overwrite of overwrites) if (overwrite.id === botId) {
|
|
1064
|
+
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
|
1065
|
+
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
channelId,
|
|
1069
|
+
guildId,
|
|
1070
|
+
permissions: bitfieldToPermissions(permissions),
|
|
1071
|
+
raw: permissions.toString(),
|
|
1072
|
+
isDm: false,
|
|
1073
|
+
channelType
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/discord/send.types.ts
|
|
1079
|
+
var DiscordSendError = class extends Error {
|
|
1080
|
+
constructor(message, opts) {
|
|
1081
|
+
super(message);
|
|
1082
|
+
this.name = "DiscordSendError";
|
|
1083
|
+
if (opts) Object.assign(this, opts);
|
|
1084
|
+
}
|
|
1085
|
+
toString() {
|
|
1086
|
+
return this.message;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
const DISCORD_MAX_EMOJI_BYTES = 256 * 1024;
|
|
1090
|
+
const DISCORD_MAX_STICKER_BYTES = 512 * 1024;
|
|
1091
|
+
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/discord/send.shared.ts
|
|
1094
|
+
const DISCORD_TEXT_LIMIT = 2e3;
|
|
1095
|
+
const DISCORD_MAX_STICKERS = 3;
|
|
1096
|
+
const DISCORD_POLL_MAX_ANSWERS = 10;
|
|
1097
|
+
const DISCORD_POLL_MAX_DURATION_HOURS = 768;
|
|
1098
|
+
const DISCORD_MISSING_PERMISSIONS = 50013;
|
|
1099
|
+
const DISCORD_CANNOT_DM = 50007;
|
|
1100
|
+
function normalizeReactionEmoji(raw) {
|
|
1101
|
+
const trimmed = raw.trim();
|
|
1102
|
+
if (!trimmed) throw new Error("emoji required");
|
|
1103
|
+
const customMatch = trimmed.match(/^<a?:([^:>]+):(\d+)>$/);
|
|
1104
|
+
const identifier = customMatch ? `${customMatch[1]}:${customMatch[2]}` : trimmed.replace(/[\uFE0E\uFE0F]/g, "");
|
|
1105
|
+
return encodeURIComponent(identifier);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Parse and resolve Discord recipient, including username lookup.
|
|
1109
|
+
* This enables sending DMs by username (e.g., "john.doe") by querying
|
|
1110
|
+
* the Discord directory to resolve usernames to user IDs.
|
|
1111
|
+
*
|
|
1112
|
+
* @param raw - The recipient string (username, ID, or known format)
|
|
1113
|
+
* @param accountId - Discord account ID to use for directory lookup
|
|
1114
|
+
* @returns Parsed DiscordRecipient with resolved user ID if applicable
|
|
1115
|
+
*/
|
|
1116
|
+
async function parseAndResolveRecipient(raw, accountId, cfg) {
|
|
1117
|
+
const resolvedCfg = cfg ?? loadConfig();
|
|
1118
|
+
const accountInfo = resolveDiscordAccount({
|
|
1119
|
+
cfg: resolvedCfg,
|
|
1120
|
+
accountId
|
|
1121
|
+
});
|
|
1122
|
+
const trimmed = raw.trim();
|
|
1123
|
+
const parseOptions = { ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.` };
|
|
1124
|
+
const resolved = await resolveDiscordTarget(raw, {
|
|
1125
|
+
cfg: resolvedCfg,
|
|
1126
|
+
accountId: accountInfo.accountId
|
|
1127
|
+
}, parseOptions);
|
|
1128
|
+
if (resolved) return {
|
|
1129
|
+
kind: resolved.kind,
|
|
1130
|
+
id: resolved.id
|
|
1131
|
+
};
|
|
1132
|
+
const parsed = parseDiscordTarget(raw, parseOptions);
|
|
1133
|
+
if (!parsed) throw new Error("Recipient is required for Discord sends");
|
|
1134
|
+
return {
|
|
1135
|
+
kind: parsed.kind,
|
|
1136
|
+
id: parsed.id
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function normalizeStickerIds(raw) {
|
|
1140
|
+
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
|
|
1141
|
+
if (ids.length === 0) throw new Error("At least one sticker id is required");
|
|
1142
|
+
if (ids.length > DISCORD_MAX_STICKERS) throw new Error("Discord supports up to 3 stickers per message");
|
|
1143
|
+
return ids;
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeEmojiName(raw, label) {
|
|
1146
|
+
const name = raw.trim();
|
|
1147
|
+
if (!name) throw new Error(`${label} is required`);
|
|
1148
|
+
return name;
|
|
1149
|
+
}
|
|
1150
|
+
function normalizeDiscordPollInput(input) {
|
|
1151
|
+
const poll = normalizePollInput(input, { maxOptions: DISCORD_POLL_MAX_ANSWERS });
|
|
1152
|
+
const duration = normalizePollDurationHours(poll.durationHours, {
|
|
1153
|
+
defaultHours: 24,
|
|
1154
|
+
maxHours: DISCORD_POLL_MAX_DURATION_HOURS
|
|
1155
|
+
});
|
|
1156
|
+
return {
|
|
1157
|
+
question: { text: poll.question },
|
|
1158
|
+
answers: poll.options.map((answer) => ({ poll_media: { text: answer } })),
|
|
1159
|
+
duration,
|
|
1160
|
+
allow_multiselect: poll.maxSelections > 1,
|
|
1161
|
+
layout_type: PollLayoutType.Default
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function getDiscordErrorCode(err) {
|
|
1165
|
+
if (!err || typeof err !== "object") return;
|
|
1166
|
+
const candidate = "code" in err && err.code !== void 0 ? err.code : "rawError" in err && err.rawError && typeof err.rawError === "object" ? err.rawError.code : void 0;
|
|
1167
|
+
if (typeof candidate === "number") return candidate;
|
|
1168
|
+
if (typeof candidate === "string" && /^\d+$/.test(candidate)) return Number(candidate);
|
|
1169
|
+
}
|
|
1170
|
+
async function buildDiscordSendError(err, ctx) {
|
|
1171
|
+
if (err instanceof DiscordSendError) return err;
|
|
1172
|
+
const code = getDiscordErrorCode(err);
|
|
1173
|
+
if (code === DISCORD_CANNOT_DM) return new DiscordSendError("discord dm failed: user blocks dms or privacy settings disallow it", { kind: "dm-blocked" });
|
|
1174
|
+
if (code !== DISCORD_MISSING_PERMISSIONS) return err;
|
|
1175
|
+
let missing = [];
|
|
1176
|
+
try {
|
|
1177
|
+
const permissions = await fetchChannelPermissionsDiscord(ctx.channelId, {
|
|
1178
|
+
rest: ctx.rest,
|
|
1179
|
+
token: ctx.token
|
|
1180
|
+
});
|
|
1181
|
+
const current = new Set(permissions.permissions);
|
|
1182
|
+
const required = ["ViewChannel", "SendMessages"];
|
|
1183
|
+
if (isThreadChannelType(permissions.channelType)) required.push("SendMessagesInThreads");
|
|
1184
|
+
if (ctx.hasMedia) required.push("AttachFiles");
|
|
1185
|
+
missing = required.filter((permission) => !current.has(permission));
|
|
1186
|
+
} catch {}
|
|
1187
|
+
return new DiscordSendError(`${missing.length ? `missing permissions in channel ${ctx.channelId}: ${missing.join(", ")}` : `missing permissions in channel ${ctx.channelId}`}. bot might be muted or blocked by role/channel overrides`, {
|
|
1188
|
+
kind: "missing-permissions",
|
|
1189
|
+
channelId: ctx.channelId,
|
|
1190
|
+
missingPermissions: missing
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
async function resolveChannelId(rest, recipient, request) {
|
|
1194
|
+
if (recipient.kind === "channel") return { channelId: recipient.id };
|
|
1195
|
+
const dmChannel = await request(() => rest.post(Routes.userChannels(), { body: { recipient_id: recipient.id } }), "dm-channel");
|
|
1196
|
+
if (!dmChannel?.id) throw new Error("Failed to create Discord DM channel");
|
|
1197
|
+
return {
|
|
1198
|
+
channelId: dmChannel.id,
|
|
1199
|
+
dm: true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function resolveDiscordChannelType(rest, channelId) {
|
|
1203
|
+
try {
|
|
1204
|
+
return (await rest.get(Routes.channel(channelId)))?.type;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const SUPPRESS_NOTIFICATIONS_FLAG$1 = 4096;
|
|
1210
|
+
function buildDiscordTextChunks(text, opts = {}) {
|
|
1211
|
+
if (!text) return [];
|
|
1212
|
+
const chunks = chunkDiscordTextWithMode(text, {
|
|
1213
|
+
maxChars: opts.maxChars ?? DISCORD_TEXT_LIMIT,
|
|
1214
|
+
maxLines: opts.maxLinesPerMessage,
|
|
1215
|
+
chunkMode: opts.chunkMode
|
|
1216
|
+
});
|
|
1217
|
+
if (!chunks.length && text) chunks.push(text);
|
|
1218
|
+
return chunks;
|
|
1219
|
+
}
|
|
1220
|
+
function hasV2Components(components) {
|
|
1221
|
+
return Boolean(components?.some((component) => "isV2" in component && component.isV2));
|
|
1222
|
+
}
|
|
1223
|
+
function resolveDiscordSendComponents(params) {
|
|
1224
|
+
if (!params.components || !params.isFirst) return;
|
|
1225
|
+
return typeof params.components === "function" ? params.components(params.text) : params.components;
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeDiscordEmbeds(embeds) {
|
|
1228
|
+
if (!embeds?.length) return;
|
|
1229
|
+
return embeds.map((embed) => embed instanceof Embed ? embed : new Embed(embed));
|
|
1230
|
+
}
|
|
1231
|
+
function resolveDiscordSendEmbeds(params) {
|
|
1232
|
+
if (!params.embeds || !params.isFirst) return;
|
|
1233
|
+
return normalizeDiscordEmbeds(params.embeds);
|
|
1234
|
+
}
|
|
1235
|
+
function buildDiscordMessagePayload(params) {
|
|
1236
|
+
const payload = {};
|
|
1237
|
+
const hasV2 = hasV2Components(params.components);
|
|
1238
|
+
const trimmed = params.text.trim();
|
|
1239
|
+
if (!hasV2 && trimmed) payload.content = params.text;
|
|
1240
|
+
if (params.components?.length) payload.components = params.components;
|
|
1241
|
+
if (!hasV2 && params.embeds?.length) payload.embeds = params.embeds;
|
|
1242
|
+
if (params.flags !== void 0) payload.flags = params.flags;
|
|
1243
|
+
if (params.files?.length) payload.files = params.files;
|
|
1244
|
+
return payload;
|
|
1245
|
+
}
|
|
1246
|
+
function stripUndefinedFields(value) {
|
|
1247
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
1248
|
+
}
|
|
1249
|
+
function toDiscordFileBlob(data) {
|
|
1250
|
+
if (data instanceof Blob) return data;
|
|
1251
|
+
const arrayBuffer = new ArrayBuffer(data.byteLength);
|
|
1252
|
+
new Uint8Array(arrayBuffer).set(data);
|
|
1253
|
+
return new Blob([arrayBuffer]);
|
|
1254
|
+
}
|
|
1255
|
+
async function sendDiscordText(rest, channelId, text, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
|
|
1256
|
+
if (!text.trim()) throw new Error("Message must be non-empty for Discord sends");
|
|
1257
|
+
const messageReference = replyTo ? {
|
|
1258
|
+
message_id: replyTo,
|
|
1259
|
+
fail_if_not_exists: false
|
|
1260
|
+
} : void 0;
|
|
1261
|
+
const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG$1 : void 0;
|
|
1262
|
+
const chunks = buildDiscordTextChunks(text, {
|
|
1263
|
+
maxLinesPerMessage,
|
|
1264
|
+
chunkMode
|
|
1265
|
+
});
|
|
1266
|
+
const sendChunk = async (chunk, isFirst) => {
|
|
1267
|
+
const body = stripUndefinedFields({
|
|
1268
|
+
...serializePayload(buildDiscordMessagePayload({
|
|
1269
|
+
text: chunk,
|
|
1270
|
+
components: resolveDiscordSendComponents({
|
|
1271
|
+
components,
|
|
1272
|
+
text: chunk,
|
|
1273
|
+
isFirst
|
|
1274
|
+
}),
|
|
1275
|
+
embeds: resolveDiscordSendEmbeds({
|
|
1276
|
+
embeds,
|
|
1277
|
+
isFirst
|
|
1278
|
+
}),
|
|
1279
|
+
flags
|
|
1280
|
+
})),
|
|
1281
|
+
...messageReference ? { message_reference: messageReference } : {}
|
|
1282
|
+
});
|
|
1283
|
+
return await request(() => rest.post(Routes.channelMessages(channelId), { body }), "text");
|
|
1284
|
+
};
|
|
1285
|
+
if (chunks.length === 1) return await sendChunk(chunks[0], true);
|
|
1286
|
+
let last = null;
|
|
1287
|
+
for (const [index, chunk] of chunks.entries()) last = await sendChunk(chunk, index === 0);
|
|
1288
|
+
if (!last) throw new Error("Discord send failed (empty chunk result)");
|
|
1289
|
+
return last;
|
|
1290
|
+
}
|
|
1291
|
+
async function sendDiscordMedia(rest, channelId, text, mediaUrl, mediaLocalRoots, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
|
|
1292
|
+
const media = await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({ mediaLocalRoots }));
|
|
1293
|
+
const chunks = text ? buildDiscordTextChunks(text, {
|
|
1294
|
+
maxLinesPerMessage,
|
|
1295
|
+
chunkMode
|
|
1296
|
+
}) : [];
|
|
1297
|
+
const caption = chunks[0] ?? "";
|
|
1298
|
+
const messageReference = replyTo ? {
|
|
1299
|
+
message_id: replyTo,
|
|
1300
|
+
fail_if_not_exists: false
|
|
1301
|
+
} : void 0;
|
|
1302
|
+
const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG$1 : void 0;
|
|
1303
|
+
const fileData = toDiscordFileBlob(media.buffer);
|
|
1304
|
+
const payload = buildDiscordMessagePayload({
|
|
1305
|
+
text: caption,
|
|
1306
|
+
components: resolveDiscordSendComponents({
|
|
1307
|
+
components,
|
|
1308
|
+
text: caption,
|
|
1309
|
+
isFirst: true
|
|
1310
|
+
}),
|
|
1311
|
+
embeds: resolveDiscordSendEmbeds({
|
|
1312
|
+
embeds,
|
|
1313
|
+
isFirst: true
|
|
1314
|
+
}),
|
|
1315
|
+
flags,
|
|
1316
|
+
files: [{
|
|
1317
|
+
data: fileData,
|
|
1318
|
+
name: media.fileName ?? "upload"
|
|
1319
|
+
}]
|
|
1320
|
+
});
|
|
1321
|
+
const res = await request(() => rest.post(Routes.channelMessages(channelId), { body: stripUndefinedFields({
|
|
1322
|
+
...serializePayload(payload),
|
|
1323
|
+
...messageReference ? { message_reference: messageReference } : {}
|
|
1324
|
+
}) }), "media");
|
|
1325
|
+
for (const chunk of chunks.slice(1)) {
|
|
1326
|
+
if (!chunk.trim()) continue;
|
|
1327
|
+
await sendDiscordText(rest, channelId, chunk, replyTo, request, maxLinesPerMessage, void 0, void 0, chunkMode, silent);
|
|
1328
|
+
}
|
|
1329
|
+
return res;
|
|
1330
|
+
}
|
|
1331
|
+
function buildReactionIdentifier(emoji) {
|
|
1332
|
+
if (emoji.id && emoji.name) return `${emoji.name}:${emoji.id}`;
|
|
1333
|
+
return emoji.name ?? "";
|
|
1334
|
+
}
|
|
1335
|
+
function formatReactionEmoji(emoji) {
|
|
1336
|
+
return buildReactionIdentifier(emoji);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/discord/send.channels.ts
|
|
1341
|
+
async function createChannelDiscord(payload, opts = {}) {
|
|
1342
|
+
const rest = resolveDiscordRest(opts);
|
|
1343
|
+
const body = { name: payload.name };
|
|
1344
|
+
if (payload.type !== void 0) body.type = payload.type;
|
|
1345
|
+
if (payload.parentId) body.parent_id = payload.parentId;
|
|
1346
|
+
if (payload.topic) body.topic = payload.topic;
|
|
1347
|
+
if (payload.position !== void 0) body.position = payload.position;
|
|
1348
|
+
if (payload.nsfw !== void 0) body.nsfw = payload.nsfw;
|
|
1349
|
+
return await rest.post(Routes.guildChannels(payload.guildId), { body });
|
|
1350
|
+
}
|
|
1351
|
+
async function editChannelDiscord(payload, opts = {}) {
|
|
1352
|
+
const rest = resolveDiscordRest(opts);
|
|
1353
|
+
const body = {};
|
|
1354
|
+
if (payload.name !== void 0) body.name = payload.name;
|
|
1355
|
+
if (payload.topic !== void 0) body.topic = payload.topic;
|
|
1356
|
+
if (payload.position !== void 0) body.position = payload.position;
|
|
1357
|
+
if (payload.parentId !== void 0) body.parent_id = payload.parentId;
|
|
1358
|
+
if (payload.nsfw !== void 0) body.nsfw = payload.nsfw;
|
|
1359
|
+
if (payload.rateLimitPerUser !== void 0) body.rate_limit_per_user = payload.rateLimitPerUser;
|
|
1360
|
+
if (payload.archived !== void 0) body.archived = payload.archived;
|
|
1361
|
+
if (payload.locked !== void 0) body.locked = payload.locked;
|
|
1362
|
+
if (payload.autoArchiveDuration !== void 0) body.auto_archive_duration = payload.autoArchiveDuration;
|
|
1363
|
+
if (payload.availableTags !== void 0) body.available_tags = payload.availableTags.map((t) => ({
|
|
1364
|
+
...t.id !== void 0 && { id: t.id },
|
|
1365
|
+
name: t.name,
|
|
1366
|
+
...t.moderated !== void 0 && { moderated: t.moderated },
|
|
1367
|
+
...t.emoji_id !== void 0 && { emoji_id: t.emoji_id },
|
|
1368
|
+
...t.emoji_name !== void 0 && { emoji_name: t.emoji_name }
|
|
1369
|
+
}));
|
|
1370
|
+
return await rest.patch(Routes.channel(payload.channelId), { body });
|
|
1371
|
+
}
|
|
1372
|
+
async function deleteChannelDiscord(channelId, opts = {}) {
|
|
1373
|
+
await resolveDiscordRest(opts).delete(Routes.channel(channelId));
|
|
1374
|
+
return {
|
|
1375
|
+
ok: true,
|
|
1376
|
+
channelId
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
async function moveChannelDiscord(payload, opts = {}) {
|
|
1380
|
+
const rest = resolveDiscordRest(opts);
|
|
1381
|
+
const body = [{
|
|
1382
|
+
id: payload.channelId,
|
|
1383
|
+
...payload.parentId !== void 0 && { parent_id: payload.parentId },
|
|
1384
|
+
...payload.position !== void 0 && { position: payload.position }
|
|
1385
|
+
}];
|
|
1386
|
+
await rest.patch(Routes.guildChannels(payload.guildId), { body });
|
|
1387
|
+
return { ok: true };
|
|
1388
|
+
}
|
|
1389
|
+
async function setChannelPermissionDiscord(payload, opts = {}) {
|
|
1390
|
+
const rest = resolveDiscordRest(opts);
|
|
1391
|
+
const body = { type: payload.targetType };
|
|
1392
|
+
if (payload.allow !== void 0) body.allow = payload.allow;
|
|
1393
|
+
if (payload.deny !== void 0) body.deny = payload.deny;
|
|
1394
|
+
await rest.put(`/channels/${payload.channelId}/permissions/${payload.targetId}`, { body });
|
|
1395
|
+
return { ok: true };
|
|
1396
|
+
}
|
|
1397
|
+
async function removeChannelPermissionDiscord(channelId, targetId, opts = {}) {
|
|
1398
|
+
await resolveDiscordRest(opts).delete(`/channels/${channelId}/permissions/${targetId}`);
|
|
1399
|
+
return { ok: true };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
//#endregion
|
|
1403
|
+
//#region src/discord/send.emojis-stickers.ts
|
|
1404
|
+
async function listGuildEmojisDiscord(guildId, opts = {}) {
|
|
1405
|
+
return await resolveDiscordRest(opts).get(Routes.guildEmojis(guildId));
|
|
1406
|
+
}
|
|
1407
|
+
async function uploadEmojiDiscord(payload, opts = {}) {
|
|
1408
|
+
const rest = resolveDiscordRest(opts);
|
|
1409
|
+
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES);
|
|
1410
|
+
const contentType = media.contentType?.toLowerCase();
|
|
1411
|
+
if (!contentType || ![
|
|
1412
|
+
"image/png",
|
|
1413
|
+
"image/jpeg",
|
|
1414
|
+
"image/jpg",
|
|
1415
|
+
"image/gif"
|
|
1416
|
+
].includes(contentType)) throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
|
|
1417
|
+
const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
|
|
1418
|
+
const roleIds = (payload.roleIds ?? []).map((id) => id.trim()).filter(Boolean);
|
|
1419
|
+
return await rest.post(Routes.guildEmojis(payload.guildId), { body: {
|
|
1420
|
+
name: normalizeEmojiName(payload.name, "Emoji name"),
|
|
1421
|
+
image,
|
|
1422
|
+
roles: roleIds.length ? roleIds : void 0
|
|
1423
|
+
} });
|
|
1424
|
+
}
|
|
1425
|
+
async function uploadStickerDiscord(payload, opts = {}) {
|
|
1426
|
+
const rest = resolveDiscordRest(opts);
|
|
1427
|
+
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES);
|
|
1428
|
+
const contentType = media.contentType?.toLowerCase();
|
|
1429
|
+
if (!contentType || ![
|
|
1430
|
+
"image/png",
|
|
1431
|
+
"image/apng",
|
|
1432
|
+
"application/json"
|
|
1433
|
+
].includes(contentType)) throw new Error("Discord sticker uploads require a PNG, APNG, or Lottie JSON file");
|
|
1434
|
+
return await rest.post(Routes.guildStickers(payload.guildId), { body: {
|
|
1435
|
+
name: normalizeEmojiName(payload.name, "Sticker name"),
|
|
1436
|
+
description: normalizeEmojiName(payload.description, "Sticker description"),
|
|
1437
|
+
tags: normalizeEmojiName(payload.tags, "Sticker tags"),
|
|
1438
|
+
files: [{
|
|
1439
|
+
data: media.buffer,
|
|
1440
|
+
name: media.fileName ?? "sticker",
|
|
1441
|
+
contentType
|
|
1442
|
+
}]
|
|
1443
|
+
} });
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/discord/send.guild.ts
|
|
1448
|
+
async function fetchMemberInfoDiscord(guildId, userId, opts = {}) {
|
|
1449
|
+
return await resolveDiscordRest(opts).get(Routes.guildMember(guildId, userId));
|
|
1450
|
+
}
|
|
1451
|
+
async function fetchRoleInfoDiscord(guildId, opts = {}) {
|
|
1452
|
+
return await resolveDiscordRest(opts).get(Routes.guildRoles(guildId));
|
|
1453
|
+
}
|
|
1454
|
+
async function addRoleDiscord(payload, opts = {}) {
|
|
1455
|
+
await resolveDiscordRest(opts).put(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
|
1456
|
+
return { ok: true };
|
|
1457
|
+
}
|
|
1458
|
+
async function removeRoleDiscord(payload, opts = {}) {
|
|
1459
|
+
await resolveDiscordRest(opts).delete(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
|
1460
|
+
return { ok: true };
|
|
1461
|
+
}
|
|
1462
|
+
async function fetchChannelInfoDiscord(channelId, opts = {}) {
|
|
1463
|
+
return await resolveDiscordRest(opts).get(Routes.channel(channelId));
|
|
1464
|
+
}
|
|
1465
|
+
async function listGuildChannelsDiscord(guildId, opts = {}) {
|
|
1466
|
+
return await resolveDiscordRest(opts).get(Routes.guildChannels(guildId));
|
|
1467
|
+
}
|
|
1468
|
+
async function fetchVoiceStatusDiscord(guildId, userId, opts = {}) {
|
|
1469
|
+
return await resolveDiscordRest(opts).get(Routes.guildVoiceState(guildId, userId));
|
|
1470
|
+
}
|
|
1471
|
+
async function listScheduledEventsDiscord(guildId, opts = {}) {
|
|
1472
|
+
return await resolveDiscordRest(opts).get(Routes.guildScheduledEvents(guildId));
|
|
1473
|
+
}
|
|
1474
|
+
async function createScheduledEventDiscord(guildId, payload, opts = {}) {
|
|
1475
|
+
return await resolveDiscordRest(opts).post(Routes.guildScheduledEvents(guildId), { body: payload });
|
|
1476
|
+
}
|
|
1477
|
+
async function timeoutMemberDiscord(payload, opts = {}) {
|
|
1478
|
+
const rest = resolveDiscordRest(opts);
|
|
1479
|
+
let until = payload.until;
|
|
1480
|
+
if (!until && payload.durationMinutes) {
|
|
1481
|
+
const ms = payload.durationMinutes * 60 * 1e3;
|
|
1482
|
+
until = new Date(Date.now() + ms).toISOString();
|
|
1483
|
+
}
|
|
1484
|
+
return await rest.patch(Routes.guildMember(payload.guildId, payload.userId), {
|
|
1485
|
+
body: { communication_disabled_until: until ?? null },
|
|
1486
|
+
headers: payload.reason ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) } : void 0
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
async function kickMemberDiscord(payload, opts = {}) {
|
|
1490
|
+
await resolveDiscordRest(opts).delete(Routes.guildMember(payload.guildId, payload.userId), { headers: payload.reason ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) } : void 0 });
|
|
1491
|
+
return { ok: true };
|
|
1492
|
+
}
|
|
1493
|
+
async function banMemberDiscord(payload, opts = {}) {
|
|
1494
|
+
const rest = resolveDiscordRest(opts);
|
|
1495
|
+
const deleteMessageDays = typeof payload.deleteMessageDays === "number" && Number.isFinite(payload.deleteMessageDays) ? Math.min(Math.max(Math.floor(payload.deleteMessageDays), 0), 7) : void 0;
|
|
1496
|
+
await rest.put(Routes.guildBan(payload.guildId, payload.userId), {
|
|
1497
|
+
body: deleteMessageDays !== void 0 ? { delete_message_days: deleteMessageDays } : void 0,
|
|
1498
|
+
headers: payload.reason ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) } : void 0
|
|
1499
|
+
});
|
|
1500
|
+
return { ok: true };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
//#endregion
|
|
1504
|
+
//#region src/discord/send.messages.ts
|
|
1505
|
+
async function readMessagesDiscord(channelId, query = {}, opts = {}) {
|
|
1506
|
+
const rest = resolveDiscordRest(opts);
|
|
1507
|
+
const limit = typeof query.limit === "number" && Number.isFinite(query.limit) ? Math.min(Math.max(Math.floor(query.limit), 1), 100) : void 0;
|
|
1508
|
+
const params = {};
|
|
1509
|
+
if (limit) params.limit = limit;
|
|
1510
|
+
if (query.before) params.before = query.before;
|
|
1511
|
+
if (query.after) params.after = query.after;
|
|
1512
|
+
if (query.around) params.around = query.around;
|
|
1513
|
+
return await rest.get(Routes.channelMessages(channelId), params);
|
|
1514
|
+
}
|
|
1515
|
+
async function fetchMessageDiscord(channelId, messageId, opts = {}) {
|
|
1516
|
+
return await resolveDiscordRest(opts).get(Routes.channelMessage(channelId, messageId));
|
|
1517
|
+
}
|
|
1518
|
+
async function editMessageDiscord(channelId, messageId, payload, opts = {}) {
|
|
1519
|
+
return await resolveDiscordRest(opts).patch(Routes.channelMessage(channelId, messageId), { body: { content: payload.content } });
|
|
1520
|
+
}
|
|
1521
|
+
async function deleteMessageDiscord(channelId, messageId, opts = {}) {
|
|
1522
|
+
await resolveDiscordRest(opts).delete(Routes.channelMessage(channelId, messageId));
|
|
1523
|
+
return { ok: true };
|
|
1524
|
+
}
|
|
1525
|
+
async function pinMessageDiscord(channelId, messageId, opts = {}) {
|
|
1526
|
+
await resolveDiscordRest(opts).put(Routes.channelPin(channelId, messageId));
|
|
1527
|
+
return { ok: true };
|
|
1528
|
+
}
|
|
1529
|
+
async function unpinMessageDiscord(channelId, messageId, opts = {}) {
|
|
1530
|
+
await resolveDiscordRest(opts).delete(Routes.channelPin(channelId, messageId));
|
|
1531
|
+
return { ok: true };
|
|
1532
|
+
}
|
|
1533
|
+
async function listPinsDiscord(channelId, opts = {}) {
|
|
1534
|
+
return await resolveDiscordRest(opts).get(Routes.channelPins(channelId));
|
|
1535
|
+
}
|
|
1536
|
+
async function createThreadDiscord(channelId, payload, opts = {}) {
|
|
1537
|
+
const rest = resolveDiscordRest(opts);
|
|
1538
|
+
const body = { name: payload.name };
|
|
1539
|
+
if (payload.autoArchiveMinutes) body.auto_archive_duration = payload.autoArchiveMinutes;
|
|
1540
|
+
if (!payload.messageId && payload.type !== void 0) body.type = payload.type;
|
|
1541
|
+
let channelType;
|
|
1542
|
+
if (!payload.messageId) try {
|
|
1543
|
+
channelType = (await rest.get(Routes.channel(channelId)))?.type;
|
|
1544
|
+
} catch {
|
|
1545
|
+
channelType = void 0;
|
|
1546
|
+
}
|
|
1547
|
+
const isForumLike = channelType === ChannelType.GuildForum || channelType === ChannelType.GuildMedia;
|
|
1548
|
+
if (isForumLike) {
|
|
1549
|
+
body.message = { content: payload.content?.trim() ? payload.content : payload.name };
|
|
1550
|
+
if (payload.appliedTags?.length) body.applied_tags = payload.appliedTags;
|
|
1551
|
+
}
|
|
1552
|
+
if (!payload.messageId && !isForumLike && body.type === void 0) body.type = ChannelType.PublicThread;
|
|
1553
|
+
const route = payload.messageId ? Routes.threads(channelId, payload.messageId) : Routes.threads(channelId);
|
|
1554
|
+
const thread = await rest.post(route, { body });
|
|
1555
|
+
if (!isForumLike && payload.content?.trim()) await rest.post(Routes.channelMessages(thread.id), { body: { content: payload.content } });
|
|
1556
|
+
return thread;
|
|
1557
|
+
}
|
|
1558
|
+
async function listThreadsDiscord(payload, opts = {}) {
|
|
1559
|
+
const rest = resolveDiscordRest(opts);
|
|
1560
|
+
if (payload.includeArchived) {
|
|
1561
|
+
if (!payload.channelId) throw new Error("channelId required to list archived threads");
|
|
1562
|
+
const params = {};
|
|
1563
|
+
if (payload.before) params.before = payload.before;
|
|
1564
|
+
if (payload.limit) params.limit = payload.limit;
|
|
1565
|
+
return await rest.get(Routes.channelThreads(payload.channelId, "public"), params);
|
|
1566
|
+
}
|
|
1567
|
+
return await rest.get(Routes.guildActiveThreads(payload.guildId));
|
|
1568
|
+
}
|
|
1569
|
+
async function searchMessagesDiscord(query, opts = {}) {
|
|
1570
|
+
const rest = resolveDiscordRest(opts);
|
|
1571
|
+
const params = new URLSearchParams();
|
|
1572
|
+
params.set("content", query.content);
|
|
1573
|
+
if (query.channelIds?.length) for (const channelId of query.channelIds) params.append("channel_id", channelId);
|
|
1574
|
+
if (query.authorIds?.length) for (const authorId of query.authorIds) params.append("author_id", authorId);
|
|
1575
|
+
if (query.limit) {
|
|
1576
|
+
const limit = Math.min(Math.max(Math.floor(query.limit), 1), 25);
|
|
1577
|
+
params.set("limit", String(limit));
|
|
1578
|
+
}
|
|
1579
|
+
return await rest.get(`/guilds/${query.guildId}/messages/search?${params.toString()}`);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
//#endregion
|
|
1583
|
+
//#region src/media/temp-files.ts
|
|
1584
|
+
async function unlinkIfExists(filePath) {
|
|
1585
|
+
if (!filePath) return;
|
|
1586
|
+
try {
|
|
1587
|
+
await fs.unlink(filePath);
|
|
1588
|
+
} catch {}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region src/discord/mentions.ts
|
|
1593
|
+
const MARKDOWN_CODE_SEGMENT_PATTERN = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
1594
|
+
const MENTION_CANDIDATE_PATTERN = /(^|[\s([{"'.,;:!?])@([a-z0-9_.-]{2,32}(?:#[0-9]{4})?)/gi;
|
|
1595
|
+
const DISCORD_RESERVED_MENTIONS = new Set(["everyone", "here"]);
|
|
1596
|
+
function normalizeSnowflake(value) {
|
|
1597
|
+
const text = String(value ?? "").trim();
|
|
1598
|
+
if (!/^\d+$/.test(text)) return null;
|
|
1599
|
+
return text;
|
|
1600
|
+
}
|
|
1601
|
+
function formatMention(params) {
|
|
1602
|
+
const userId = params.userId == null ? null : normalizeSnowflake(params.userId);
|
|
1603
|
+
const roleId = params.roleId == null ? null : normalizeSnowflake(params.roleId);
|
|
1604
|
+
const channelId = params.channelId == null ? null : normalizeSnowflake(params.channelId);
|
|
1605
|
+
const values = [
|
|
1606
|
+
userId ? {
|
|
1607
|
+
kind: "user",
|
|
1608
|
+
id: userId
|
|
1609
|
+
} : null,
|
|
1610
|
+
roleId ? {
|
|
1611
|
+
kind: "role",
|
|
1612
|
+
id: roleId
|
|
1613
|
+
} : null,
|
|
1614
|
+
channelId ? {
|
|
1615
|
+
kind: "channel",
|
|
1616
|
+
id: channelId
|
|
1617
|
+
} : null
|
|
1618
|
+
].filter((entry) => Boolean(entry));
|
|
1619
|
+
if (values.length !== 1) throw new Error("formatMention requires exactly one of userId, roleId, or channelId");
|
|
1620
|
+
const target = values[0];
|
|
1621
|
+
if (target.kind === "user") return `<@${target.id}>`;
|
|
1622
|
+
if (target.kind === "role") return `<@&${target.id}>`;
|
|
1623
|
+
return `<#${target.id}>`;
|
|
1624
|
+
}
|
|
1625
|
+
function rewritePlainTextMentions(text, accountId) {
|
|
1626
|
+
if (!text.includes("@")) return text;
|
|
1627
|
+
return text.replace(MENTION_CANDIDATE_PATTERN, (match, prefix, rawHandle) => {
|
|
1628
|
+
const handle = String(rawHandle ?? "").trim();
|
|
1629
|
+
if (!handle) return match;
|
|
1630
|
+
const lookup = handle.toLowerCase();
|
|
1631
|
+
if (DISCORD_RESERVED_MENTIONS.has(lookup)) return match;
|
|
1632
|
+
const userId = resolveDiscordDirectoryUserId({
|
|
1633
|
+
accountId,
|
|
1634
|
+
handle
|
|
1635
|
+
});
|
|
1636
|
+
if (!userId) return match;
|
|
1637
|
+
return `${String(prefix ?? "")}${formatMention({ userId })}`;
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
function rewriteDiscordKnownMentions(text, params) {
|
|
1641
|
+
if (!text.includes("@")) return text;
|
|
1642
|
+
let rewritten = "";
|
|
1643
|
+
let offset = 0;
|
|
1644
|
+
MARKDOWN_CODE_SEGMENT_PATTERN.lastIndex = 0;
|
|
1645
|
+
for (const match of text.matchAll(MARKDOWN_CODE_SEGMENT_PATTERN)) {
|
|
1646
|
+
const matchIndex = match.index ?? 0;
|
|
1647
|
+
rewritten += rewritePlainTextMentions(text.slice(offset, matchIndex), params.accountId);
|
|
1648
|
+
rewritten += match[0];
|
|
1649
|
+
offset = matchIndex + match[0].length;
|
|
1650
|
+
}
|
|
1651
|
+
rewritten += rewritePlainTextMentions(text.slice(offset), params.accountId);
|
|
1652
|
+
return rewritten;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
//#endregion
|
|
1656
|
+
//#region src/media/ffmpeg-limits.ts
|
|
1657
|
+
const MEDIA_FFMPEG_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
1658
|
+
const MEDIA_FFPROBE_TIMEOUT_MS = 1e4;
|
|
1659
|
+
const MEDIA_FFMPEG_TIMEOUT_MS = 45e3;
|
|
1660
|
+
const MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS = 1200;
|
|
1661
|
+
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region src/media/ffmpeg-exec.ts
|
|
1664
|
+
const execFileAsync = promisify(execFile);
|
|
1665
|
+
function resolveExecOptions(defaultTimeoutMs, options) {
|
|
1666
|
+
return {
|
|
1667
|
+
timeout: options?.timeoutMs ?? defaultTimeoutMs,
|
|
1668
|
+
maxBuffer: options?.maxBufferBytes ?? MEDIA_FFMPEG_MAX_BUFFER_BYTES
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
async function runFfprobe(args, options) {
|
|
1672
|
+
const { stdout } = await execFileAsync("ffprobe", args, resolveExecOptions(MEDIA_FFPROBE_TIMEOUT_MS, options));
|
|
1673
|
+
return stdout.toString();
|
|
1674
|
+
}
|
|
1675
|
+
async function runFfmpeg(args, options) {
|
|
1676
|
+
const { stdout } = await execFileAsync("ffmpeg", args, resolveExecOptions(MEDIA_FFMPEG_TIMEOUT_MS, options));
|
|
1677
|
+
return stdout.toString();
|
|
1678
|
+
}
|
|
1679
|
+
function parseFfprobeCsvFields(stdout, maxFields) {
|
|
1680
|
+
return stdout.trim().toLowerCase().split(/[,\r\n]+/, maxFields).map((field) => field.trim());
|
|
1681
|
+
}
|
|
1682
|
+
function parseFfprobeCodecAndSampleRate(stdout) {
|
|
1683
|
+
const [codecRaw, sampleRateRaw] = parseFfprobeCsvFields(stdout, 2);
|
|
1684
|
+
const codec = codecRaw ? codecRaw : null;
|
|
1685
|
+
const sampleRate = sampleRateRaw ? Number.parseInt(sampleRateRaw, 10) : NaN;
|
|
1686
|
+
return {
|
|
1687
|
+
codec,
|
|
1688
|
+
sampleRateHz: Number.isFinite(sampleRate) ? sampleRate : null
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/discord/voice-message.ts
|
|
1694
|
+
/**
|
|
1695
|
+
* Discord Voice Message Support
|
|
1696
|
+
*
|
|
1697
|
+
* Implements sending voice messages via Discord's API.
|
|
1698
|
+
* Voice messages require:
|
|
1699
|
+
* - OGG/Opus format audio
|
|
1700
|
+
* - Waveform data (base64 encoded, up to 256 samples, 0-255 values)
|
|
1701
|
+
* - Duration in seconds
|
|
1702
|
+
* - Message flag 8192 (IS_VOICE_MESSAGE)
|
|
1703
|
+
* - No other content (text, embeds, etc.)
|
|
1704
|
+
*/
|
|
1705
|
+
const DISCORD_VOICE_MESSAGE_FLAG = 8192;
|
|
1706
|
+
const SUPPRESS_NOTIFICATIONS_FLAG = 4096;
|
|
1707
|
+
const WAVEFORM_SAMPLES = 256;
|
|
1708
|
+
const DISCORD_OPUS_SAMPLE_RATE_HZ = 48e3;
|
|
1709
|
+
/**
|
|
1710
|
+
* Get audio duration using ffprobe
|
|
1711
|
+
*/
|
|
1712
|
+
async function getAudioDuration(filePath) {
|
|
1713
|
+
try {
|
|
1714
|
+
const stdout = await runFfprobe([
|
|
1715
|
+
"-v",
|
|
1716
|
+
"error",
|
|
1717
|
+
"-show_entries",
|
|
1718
|
+
"format=duration",
|
|
1719
|
+
"-of",
|
|
1720
|
+
"csv=p=0",
|
|
1721
|
+
filePath
|
|
1722
|
+
]);
|
|
1723
|
+
const duration = parseFloat(stdout.trim());
|
|
1724
|
+
if (isNaN(duration)) throw new Error("Could not parse duration");
|
|
1725
|
+
return Math.round(duration * 100) / 100;
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
1728
|
+
throw new Error(`Failed to get audio duration: ${errMessage}`, { cause: err });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Generate waveform data from audio file using ffmpeg
|
|
1733
|
+
* Returns base64 encoded byte array of amplitude samples (0-255)
|
|
1734
|
+
*/
|
|
1735
|
+
async function generateWaveform(filePath) {
|
|
1736
|
+
try {
|
|
1737
|
+
return await generateWaveformFromPcm(filePath);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return generatePlaceholderWaveform();
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Generate waveform by extracting raw PCM data and sampling amplitudes
|
|
1744
|
+
*/
|
|
1745
|
+
async function generateWaveformFromPcm(filePath) {
|
|
1746
|
+
const tempDir = resolvePreferredSquidClawTmpDir();
|
|
1747
|
+
const tempPcm = path.join(tempDir, `waveform-${crypto.randomUUID()}.raw`);
|
|
1748
|
+
try {
|
|
1749
|
+
await runFfmpeg([
|
|
1750
|
+
"-y",
|
|
1751
|
+
"-i",
|
|
1752
|
+
filePath,
|
|
1753
|
+
"-vn",
|
|
1754
|
+
"-sn",
|
|
1755
|
+
"-dn",
|
|
1756
|
+
"-t",
|
|
1757
|
+
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
|
|
1758
|
+
"-f",
|
|
1759
|
+
"s16le",
|
|
1760
|
+
"-acodec",
|
|
1761
|
+
"pcm_s16le",
|
|
1762
|
+
"-ac",
|
|
1763
|
+
"1",
|
|
1764
|
+
"-ar",
|
|
1765
|
+
"8000",
|
|
1766
|
+
tempPcm
|
|
1767
|
+
]);
|
|
1768
|
+
const pcmData = await fs.readFile(tempPcm);
|
|
1769
|
+
const samples = new Int16Array(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength / 2);
|
|
1770
|
+
const step = Math.max(1, Math.floor(samples.length / WAVEFORM_SAMPLES));
|
|
1771
|
+
const waveform = [];
|
|
1772
|
+
for (let i = 0; i < WAVEFORM_SAMPLES && i * step < samples.length; i++) {
|
|
1773
|
+
let sum = 0;
|
|
1774
|
+
let count = 0;
|
|
1775
|
+
for (let j = 0; j < step && i * step + j < samples.length; j++) {
|
|
1776
|
+
sum += Math.abs(samples[i * step + j]);
|
|
1777
|
+
count++;
|
|
1778
|
+
}
|
|
1779
|
+
const avg = count > 0 ? sum / count : 0;
|
|
1780
|
+
const normalized = Math.min(255, Math.round(avg / 32767 * 255));
|
|
1781
|
+
waveform.push(normalized);
|
|
1782
|
+
}
|
|
1783
|
+
while (waveform.length < WAVEFORM_SAMPLES) waveform.push(0);
|
|
1784
|
+
return Buffer.from(waveform).toString("base64");
|
|
1785
|
+
} finally {
|
|
1786
|
+
await unlinkIfExists(tempPcm);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Generate a placeholder waveform (for when audio processing fails)
|
|
1791
|
+
*/
|
|
1792
|
+
function generatePlaceholderWaveform() {
|
|
1793
|
+
const waveform = [];
|
|
1794
|
+
for (let i = 0; i < WAVEFORM_SAMPLES; i++) {
|
|
1795
|
+
const value = Math.round(128 + 64 * Math.sin(i / WAVEFORM_SAMPLES * Math.PI * 8));
|
|
1796
|
+
waveform.push(Math.min(255, Math.max(0, value)));
|
|
1797
|
+
}
|
|
1798
|
+
return Buffer.from(waveform).toString("base64");
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Convert audio file to OGG/Opus format if needed
|
|
1802
|
+
* Returns path to the OGG file (may be same as input if already OGG/Opus)
|
|
1803
|
+
*/
|
|
1804
|
+
async function ensureOggOpus(filePath) {
|
|
1805
|
+
const trimmed = filePath.trim();
|
|
1806
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) throw new Error(`Voice message conversion requires a local file path; received a URL/protocol source: ${trimmed}`);
|
|
1807
|
+
if (path.extname(filePath).toLowerCase() === ".ogg") try {
|
|
1808
|
+
const { codec, sampleRateHz } = parseFfprobeCodecAndSampleRate(await runFfprobe([
|
|
1809
|
+
"-v",
|
|
1810
|
+
"error",
|
|
1811
|
+
"-select_streams",
|
|
1812
|
+
"a:0",
|
|
1813
|
+
"-show_entries",
|
|
1814
|
+
"stream=codec_name,sample_rate",
|
|
1815
|
+
"-of",
|
|
1816
|
+
"csv=p=0",
|
|
1817
|
+
filePath
|
|
1818
|
+
]));
|
|
1819
|
+
if (codec === "opus" && sampleRateHz === DISCORD_OPUS_SAMPLE_RATE_HZ) return {
|
|
1820
|
+
path: filePath,
|
|
1821
|
+
cleanup: false
|
|
1822
|
+
};
|
|
1823
|
+
} catch {}
|
|
1824
|
+
const tempDir = resolvePreferredSquidClawTmpDir();
|
|
1825
|
+
const outputPath = path.join(tempDir, `voice-${crypto.randomUUID()}.ogg`);
|
|
1826
|
+
await runFfmpeg([
|
|
1827
|
+
"-y",
|
|
1828
|
+
"-i",
|
|
1829
|
+
filePath,
|
|
1830
|
+
"-vn",
|
|
1831
|
+
"-sn",
|
|
1832
|
+
"-dn",
|
|
1833
|
+
"-t",
|
|
1834
|
+
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
|
|
1835
|
+
"-ar",
|
|
1836
|
+
String(DISCORD_OPUS_SAMPLE_RATE_HZ),
|
|
1837
|
+
"-c:a",
|
|
1838
|
+
"libopus",
|
|
1839
|
+
"-b:a",
|
|
1840
|
+
"64k",
|
|
1841
|
+
outputPath
|
|
1842
|
+
]);
|
|
1843
|
+
return {
|
|
1844
|
+
path: outputPath,
|
|
1845
|
+
cleanup: true
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Get voice message metadata (duration and waveform)
|
|
1850
|
+
*/
|
|
1851
|
+
async function getVoiceMessageMetadata(filePath) {
|
|
1852
|
+
const [durationSecs, waveform] = await Promise.all([getAudioDuration(filePath), generateWaveform(filePath)]);
|
|
1853
|
+
return {
|
|
1854
|
+
durationSecs,
|
|
1855
|
+
waveform
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Send a voice message to Discord
|
|
1860
|
+
*
|
|
1861
|
+
* This follows Discord's voice message protocol:
|
|
1862
|
+
* 1. Request upload URL from Discord
|
|
1863
|
+
* 2. Upload the OGG file to the provided URL
|
|
1864
|
+
* 3. Send the message with flag 8192 and attachment metadata
|
|
1865
|
+
*/
|
|
1866
|
+
async function sendDiscordVoiceMessage(rest, channelId, audioBuffer, metadata, replyTo, request, silent, token) {
|
|
1867
|
+
const filename = "voice-message.ogg";
|
|
1868
|
+
const fileSize = audioBuffer.byteLength;
|
|
1869
|
+
const botToken = token;
|
|
1870
|
+
if (!botToken) throw new Error("Discord bot token is required for voice message upload");
|
|
1871
|
+
const uploadUrlResponse = await request(async () => {
|
|
1872
|
+
const url = `${rest.options?.baseUrl ?? "https://discord.com/api"}/channels/${channelId}/attachments`;
|
|
1873
|
+
const res = await fetch(url, {
|
|
1874
|
+
method: "POST",
|
|
1875
|
+
headers: {
|
|
1876
|
+
Authorization: `Bot ${botToken}`,
|
|
1877
|
+
"Content-Type": "application/json"
|
|
1878
|
+
},
|
|
1879
|
+
body: JSON.stringify({ files: [{
|
|
1880
|
+
filename,
|
|
1881
|
+
file_size: fileSize,
|
|
1882
|
+
id: "0"
|
|
1883
|
+
}] })
|
|
1884
|
+
});
|
|
1885
|
+
if (!res.ok) {
|
|
1886
|
+
if (res.status === 429) {
|
|
1887
|
+
const retryData = await res.json().catch(() => ({}));
|
|
1888
|
+
throw new RateLimitError(res, {
|
|
1889
|
+
message: retryData.message ?? "You are being rate limited.",
|
|
1890
|
+
retry_after: retryData.retry_after ?? 1,
|
|
1891
|
+
global: retryData.global ?? false
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const errorBody = await res.json().catch(() => null);
|
|
1895
|
+
const err = /* @__PURE__ */ new Error(`Upload URL request failed: ${res.status} ${errorBody?.message ?? ""}`);
|
|
1896
|
+
if (errorBody?.code !== void 0) err.code = errorBody.code;
|
|
1897
|
+
throw err;
|
|
1898
|
+
}
|
|
1899
|
+
return await res.json();
|
|
1900
|
+
}, "voice-upload-url");
|
|
1901
|
+
if (!uploadUrlResponse.attachments?.[0]) throw new Error("Failed to get upload URL for voice message");
|
|
1902
|
+
const { upload_url, upload_filename } = uploadUrlResponse.attachments[0];
|
|
1903
|
+
const uploadResponse = await fetch(upload_url, {
|
|
1904
|
+
method: "PUT",
|
|
1905
|
+
headers: { "Content-Type": "audio/ogg" },
|
|
1906
|
+
body: new Uint8Array(audioBuffer)
|
|
1907
|
+
});
|
|
1908
|
+
if (!uploadResponse.ok) throw new Error(`Failed to upload voice message: ${uploadResponse.status}`);
|
|
1909
|
+
const messagePayload = {
|
|
1910
|
+
flags: silent ? DISCORD_VOICE_MESSAGE_FLAG | SUPPRESS_NOTIFICATIONS_FLAG : DISCORD_VOICE_MESSAGE_FLAG,
|
|
1911
|
+
attachments: [{
|
|
1912
|
+
id: "0",
|
|
1913
|
+
filename,
|
|
1914
|
+
uploaded_filename: upload_filename,
|
|
1915
|
+
duration_secs: metadata.durationSecs,
|
|
1916
|
+
waveform: metadata.waveform
|
|
1917
|
+
}]
|
|
1918
|
+
};
|
|
1919
|
+
if (replyTo) messagePayload.message_reference = {
|
|
1920
|
+
message_id: replyTo,
|
|
1921
|
+
fail_if_not_exists: false
|
|
1922
|
+
};
|
|
1923
|
+
return await request(() => rest.post(`/channels/${channelId}/messages`, { body: messagePayload }), "voice-message");
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
//#endregion
|
|
1927
|
+
//#region src/discord/send.outbound.ts
|
|
1928
|
+
async function sendDiscordThreadTextChunks(params) {
|
|
1929
|
+
for (const chunk of params.chunks) await sendDiscordText(params.rest, params.threadId, chunk, void 0, params.request, params.maxLinesPerMessage, void 0, void 0, params.chunkMode, params.silent);
|
|
1930
|
+
}
|
|
1931
|
+
/** Discord thread names are capped at 100 characters. */
|
|
1932
|
+
const DISCORD_THREAD_NAME_LIMIT = 100;
|
|
1933
|
+
/** Derive a thread title from the first non-empty line of the message text. */
|
|
1934
|
+
function deriveForumThreadName(text) {
|
|
1935
|
+
return (text.split("\n").find((l) => l.trim())?.trim() ?? "").slice(0, DISCORD_THREAD_NAME_LIMIT) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 16);
|
|
1936
|
+
}
|
|
1937
|
+
/** Forum/Media channels cannot receive regular messages; detect them here. */
|
|
1938
|
+
function isForumLikeType(channelType) {
|
|
1939
|
+
return channelType === ChannelType.GuildForum || channelType === ChannelType.GuildMedia;
|
|
1940
|
+
}
|
|
1941
|
+
function toDiscordSendResult(result, fallbackChannelId) {
|
|
1942
|
+
return {
|
|
1943
|
+
messageId: result.id ? String(result.id) : "unknown",
|
|
1944
|
+
channelId: String(result.channel_id ?? fallbackChannelId)
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
async function resolveDiscordSendTarget(to, opts) {
|
|
1948
|
+
const cfg = opts.cfg ?? loadConfig();
|
|
1949
|
+
const { rest, request } = createDiscordClient(opts, cfg);
|
|
1950
|
+
const { channelId } = await resolveChannelId(rest, await parseAndResolveRecipient(to, opts.accountId, cfg), request);
|
|
1951
|
+
return {
|
|
1952
|
+
rest,
|
|
1953
|
+
request,
|
|
1954
|
+
channelId
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
async function sendMessageDiscord(to, text, opts = {}) {
|
|
1958
|
+
const cfg = opts.cfg ?? loadConfig();
|
|
1959
|
+
const accountInfo = resolveDiscordAccount({
|
|
1960
|
+
cfg,
|
|
1961
|
+
accountId: opts.accountId
|
|
1962
|
+
});
|
|
1963
|
+
const tableMode = resolveMarkdownTableMode({
|
|
1964
|
+
cfg,
|
|
1965
|
+
channel: "discord",
|
|
1966
|
+
accountId: accountInfo.accountId
|
|
1967
|
+
});
|
|
1968
|
+
const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
|
|
1969
|
+
const textWithTables = convertMarkdownTables(text ?? "", tableMode);
|
|
1970
|
+
const textWithMentions = rewriteDiscordKnownMentions(textWithTables, { accountId: accountInfo.accountId });
|
|
1971
|
+
const { token, rest, request } = createDiscordClient(opts, cfg);
|
|
1972
|
+
const { channelId } = await resolveChannelId(rest, await parseAndResolveRecipient(to, opts.accountId, cfg), request);
|
|
1973
|
+
if (isForumLikeType(await resolveDiscordChannelType(rest, channelId))) {
|
|
1974
|
+
const threadName = deriveForumThreadName(textWithTables);
|
|
1975
|
+
const chunks = buildDiscordTextChunks(textWithMentions, {
|
|
1976
|
+
maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
|
|
1977
|
+
chunkMode
|
|
1978
|
+
});
|
|
1979
|
+
const starterContent = chunks[0]?.trim() ? chunks[0] : threadName;
|
|
1980
|
+
const starterPayload = buildDiscordMessagePayload({
|
|
1981
|
+
text: starterContent,
|
|
1982
|
+
components: resolveDiscordSendComponents({
|
|
1983
|
+
components: opts.components,
|
|
1984
|
+
text: starterContent,
|
|
1985
|
+
isFirst: true
|
|
1986
|
+
}),
|
|
1987
|
+
embeds: resolveDiscordSendEmbeds({
|
|
1988
|
+
embeds: opts.embeds,
|
|
1989
|
+
isFirst: true
|
|
1990
|
+
}),
|
|
1991
|
+
flags: opts.silent ? 4096 : void 0
|
|
1992
|
+
});
|
|
1993
|
+
let threadRes;
|
|
1994
|
+
try {
|
|
1995
|
+
threadRes = await request(() => rest.post(Routes.threads(channelId), { body: {
|
|
1996
|
+
name: threadName,
|
|
1997
|
+
message: stripUndefinedFields(serializePayload(starterPayload))
|
|
1998
|
+
} }), "forum-thread");
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
throw await buildDiscordSendError(err, {
|
|
2001
|
+
channelId,
|
|
2002
|
+
rest,
|
|
2003
|
+
token,
|
|
2004
|
+
hasMedia: Boolean(opts.mediaUrl)
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
const threadId = threadRes.id;
|
|
2008
|
+
const messageId = threadRes.message?.id ?? threadId;
|
|
2009
|
+
const resultChannelId = threadRes.message?.channel_id ?? threadId;
|
|
2010
|
+
const remainingChunks = chunks.slice(1);
|
|
2011
|
+
try {
|
|
2012
|
+
if (opts.mediaUrl) {
|
|
2013
|
+
const [mediaCaption, ...afterMediaChunks] = remainingChunks;
|
|
2014
|
+
await sendDiscordMedia(rest, threadId, mediaCaption ?? "", opts.mediaUrl, opts.mediaLocalRoots, void 0, request, accountInfo.config.maxLinesPerMessage, void 0, void 0, chunkMode, opts.silent);
|
|
2015
|
+
await sendDiscordThreadTextChunks({
|
|
2016
|
+
rest,
|
|
2017
|
+
threadId,
|
|
2018
|
+
chunks: afterMediaChunks,
|
|
2019
|
+
request,
|
|
2020
|
+
maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
|
|
2021
|
+
chunkMode,
|
|
2022
|
+
silent: opts.silent
|
|
2023
|
+
});
|
|
2024
|
+
} else await sendDiscordThreadTextChunks({
|
|
2025
|
+
rest,
|
|
2026
|
+
threadId,
|
|
2027
|
+
chunks: remainingChunks,
|
|
2028
|
+
request,
|
|
2029
|
+
maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
|
|
2030
|
+
chunkMode,
|
|
2031
|
+
silent: opts.silent
|
|
2032
|
+
});
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
throw await buildDiscordSendError(err, {
|
|
2035
|
+
channelId: threadId,
|
|
2036
|
+
rest,
|
|
2037
|
+
token,
|
|
2038
|
+
hasMedia: Boolean(opts.mediaUrl)
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
recordChannelActivity({
|
|
2042
|
+
channel: "discord",
|
|
2043
|
+
accountId: accountInfo.accountId,
|
|
2044
|
+
direction: "outbound"
|
|
2045
|
+
});
|
|
2046
|
+
return toDiscordSendResult({
|
|
2047
|
+
id: messageId,
|
|
2048
|
+
channel_id: resultChannelId
|
|
2049
|
+
}, channelId);
|
|
2050
|
+
}
|
|
2051
|
+
let result;
|
|
2052
|
+
try {
|
|
2053
|
+
if (opts.mediaUrl) result = await sendDiscordMedia(rest, channelId, textWithMentions, opts.mediaUrl, opts.mediaLocalRoots, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.components, opts.embeds, chunkMode, opts.silent);
|
|
2054
|
+
else result = await sendDiscordText(rest, channelId, textWithMentions, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, opts.components, opts.embeds, chunkMode, opts.silent);
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
throw await buildDiscordSendError(err, {
|
|
2057
|
+
channelId,
|
|
2058
|
+
rest,
|
|
2059
|
+
token,
|
|
2060
|
+
hasMedia: Boolean(opts.mediaUrl)
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
recordChannelActivity({
|
|
2064
|
+
channel: "discord",
|
|
2065
|
+
accountId: accountInfo.accountId,
|
|
2066
|
+
direction: "outbound"
|
|
2067
|
+
});
|
|
2068
|
+
return toDiscordSendResult(result, channelId);
|
|
2069
|
+
}
|
|
2070
|
+
function resolveWebhookExecutionUrl(params) {
|
|
2071
|
+
const baseUrl = new URL(`https://discord.com/api/v10/webhooks/${encodeURIComponent(params.webhookId)}/${encodeURIComponent(params.webhookToken)}`);
|
|
2072
|
+
baseUrl.searchParams.set("wait", params.wait === false ? "false" : "true");
|
|
2073
|
+
if (params.threadId !== void 0 && params.threadId !== null && params.threadId !== "") baseUrl.searchParams.set("thread_id", String(params.threadId));
|
|
2074
|
+
return baseUrl.toString();
|
|
2075
|
+
}
|
|
2076
|
+
async function sendWebhookMessageDiscord(text, opts) {
|
|
2077
|
+
const webhookId = opts.webhookId.trim();
|
|
2078
|
+
const webhookToken = opts.webhookToken.trim();
|
|
2079
|
+
if (!webhookId || !webhookToken) throw new Error("Discord webhook id/token are required");
|
|
2080
|
+
const rewrittenText = rewriteDiscordKnownMentions(text, { accountId: opts.accountId });
|
|
2081
|
+
const replyTo = typeof opts.replyTo === "string" ? opts.replyTo.trim() : "";
|
|
2082
|
+
const messageReference = replyTo ? {
|
|
2083
|
+
message_id: replyTo,
|
|
2084
|
+
fail_if_not_exists: false
|
|
2085
|
+
} : void 0;
|
|
2086
|
+
const response = await fetch(resolveWebhookExecutionUrl({
|
|
2087
|
+
webhookId,
|
|
2088
|
+
webhookToken,
|
|
2089
|
+
threadId: opts.threadId,
|
|
2090
|
+
wait: opts.wait
|
|
2091
|
+
}), {
|
|
2092
|
+
method: "POST",
|
|
2093
|
+
headers: { "content-type": "application/json" },
|
|
2094
|
+
body: JSON.stringify({
|
|
2095
|
+
content: rewrittenText,
|
|
2096
|
+
username: opts.username?.trim() || void 0,
|
|
2097
|
+
avatar_url: opts.avatarUrl?.trim() || void 0,
|
|
2098
|
+
...messageReference ? { message_reference: messageReference } : {}
|
|
2099
|
+
})
|
|
2100
|
+
});
|
|
2101
|
+
if (!response.ok) {
|
|
2102
|
+
const raw = await response.text().catch(() => "");
|
|
2103
|
+
throw new Error(`Discord webhook send failed (${response.status}${raw ? `: ${raw.slice(0, 200)}` : ""})`);
|
|
2104
|
+
}
|
|
2105
|
+
const payload = await response.json().catch(() => ({}));
|
|
2106
|
+
try {
|
|
2107
|
+
recordChannelActivity({
|
|
2108
|
+
channel: "discord",
|
|
2109
|
+
accountId: resolveDiscordAccount({
|
|
2110
|
+
cfg: opts.cfg ?? loadConfig(),
|
|
2111
|
+
accountId: opts.accountId
|
|
2112
|
+
}).accountId,
|
|
2113
|
+
direction: "outbound"
|
|
2114
|
+
});
|
|
2115
|
+
} catch {}
|
|
2116
|
+
return {
|
|
2117
|
+
messageId: payload.id ? String(payload.id) : "unknown",
|
|
2118
|
+
channelId: payload.channel_id ? String(payload.channel_id) : opts.threadId ? String(opts.threadId) : ""
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
async function sendStickerDiscord(to, stickerIds, opts = {}) {
|
|
2122
|
+
const { rest, request, channelId } = await resolveDiscordSendTarget(to, opts);
|
|
2123
|
+
const content = opts.content?.trim();
|
|
2124
|
+
const rewrittenContent = content ? rewriteDiscordKnownMentions(content, { accountId: opts.accountId }) : void 0;
|
|
2125
|
+
const stickers = normalizeStickerIds(stickerIds);
|
|
2126
|
+
return toDiscordSendResult(await request(() => rest.post(Routes.channelMessages(channelId), { body: {
|
|
2127
|
+
content: rewrittenContent || void 0,
|
|
2128
|
+
sticker_ids: stickers
|
|
2129
|
+
} }), "sticker"), channelId);
|
|
2130
|
+
}
|
|
2131
|
+
async function sendPollDiscord(to, poll, opts = {}) {
|
|
2132
|
+
const { rest, request, channelId } = await resolveDiscordSendTarget(to, opts);
|
|
2133
|
+
const content = opts.content?.trim();
|
|
2134
|
+
const rewrittenContent = content ? rewriteDiscordKnownMentions(content, { accountId: opts.accountId }) : void 0;
|
|
2135
|
+
if (poll.durationSeconds !== void 0) throw new Error("Discord polls do not support durationSeconds; use durationHours");
|
|
2136
|
+
const payload = normalizeDiscordPollInput(poll);
|
|
2137
|
+
const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG$1 : void 0;
|
|
2138
|
+
return toDiscordSendResult(await request(() => rest.post(Routes.channelMessages(channelId), { body: {
|
|
2139
|
+
content: rewrittenContent || void 0,
|
|
2140
|
+
poll: payload,
|
|
2141
|
+
...flags ? { flags } : {}
|
|
2142
|
+
} }), "poll"), channelId);
|
|
2143
|
+
}
|
|
2144
|
+
async function materializeVoiceMessageInput(mediaUrl) {
|
|
2145
|
+
const media = await loadWebMediaRaw(mediaUrl, maxBytesForKind("audio"));
|
|
2146
|
+
const extFromName = media.fileName ? path.extname(media.fileName) : "";
|
|
2147
|
+
const extFromMime = media.contentType ? extensionForMime(media.contentType) : "";
|
|
2148
|
+
const ext = extFromName || extFromMime || ".bin";
|
|
2149
|
+
const tempDir = resolvePreferredSquidClawTmpDir();
|
|
2150
|
+
const filePath = path.join(tempDir, `voice-src-${crypto.randomUUID()}${ext}`);
|
|
2151
|
+
await fs.writeFile(filePath, media.buffer, { mode: 384 });
|
|
2152
|
+
return { filePath };
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Send a voice message to Discord.
|
|
2156
|
+
*
|
|
2157
|
+
* Voice messages are a special Discord feature that displays audio with a waveform
|
|
2158
|
+
* visualization. They require OGG/Opus format and cannot include text content.
|
|
2159
|
+
*
|
|
2160
|
+
* @param to - Recipient (user ID for DM or channel ID)
|
|
2161
|
+
* @param audioPath - Path to local audio file (will be converted to OGG/Opus if needed)
|
|
2162
|
+
* @param opts - Send options
|
|
2163
|
+
*/
|
|
2164
|
+
async function sendVoiceMessageDiscord(to, audioPath, opts = {}) {
|
|
2165
|
+
const { filePath: localInputPath } = await materializeVoiceMessageInput(audioPath);
|
|
2166
|
+
let oggPath = null;
|
|
2167
|
+
let oggCleanup = false;
|
|
2168
|
+
let token;
|
|
2169
|
+
let rest;
|
|
2170
|
+
let channelId;
|
|
2171
|
+
try {
|
|
2172
|
+
const cfg = opts.cfg ?? loadConfig();
|
|
2173
|
+
const accountInfo = resolveDiscordAccount({
|
|
2174
|
+
cfg,
|
|
2175
|
+
accountId: opts.accountId
|
|
2176
|
+
});
|
|
2177
|
+
const client = createDiscordClient(opts, cfg);
|
|
2178
|
+
token = client.token;
|
|
2179
|
+
rest = client.rest;
|
|
2180
|
+
const request = client.request;
|
|
2181
|
+
const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg);
|
|
2182
|
+
channelId = (await resolveChannelId(rest, recipient, request)).channelId;
|
|
2183
|
+
const ogg = await ensureOggOpus(localInputPath);
|
|
2184
|
+
oggPath = ogg.path;
|
|
2185
|
+
oggCleanup = ogg.cleanup;
|
|
2186
|
+
const metadata = await getVoiceMessageMetadata(oggPath);
|
|
2187
|
+
const audioBuffer = await fs.readFile(oggPath);
|
|
2188
|
+
const result = await sendDiscordVoiceMessage(rest, channelId, audioBuffer, metadata, opts.replyTo, request, opts.silent, token);
|
|
2189
|
+
recordChannelActivity({
|
|
2190
|
+
channel: "discord",
|
|
2191
|
+
accountId: accountInfo.accountId,
|
|
2192
|
+
direction: "outbound"
|
|
2193
|
+
});
|
|
2194
|
+
return toDiscordSendResult(result, channelId);
|
|
2195
|
+
} catch (err) {
|
|
2196
|
+
if (channelId && rest && token) throw await buildDiscordSendError(err, {
|
|
2197
|
+
channelId,
|
|
2198
|
+
rest,
|
|
2199
|
+
token,
|
|
2200
|
+
hasMedia: true
|
|
2201
|
+
});
|
|
2202
|
+
throw err;
|
|
2203
|
+
} finally {
|
|
2204
|
+
await unlinkIfExists(oggCleanup ? oggPath : null);
|
|
2205
|
+
await unlinkIfExists(localInputPath);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/discord/components-registry.ts
|
|
2211
|
+
const DEFAULT_COMPONENT_TTL_MS = 1800 * 1e3;
|
|
2212
|
+
const componentEntries = /* @__PURE__ */ new Map();
|
|
2213
|
+
const modalEntries = /* @__PURE__ */ new Map();
|
|
2214
|
+
function isExpired(entry, now) {
|
|
2215
|
+
return typeof entry.expiresAt === "number" && entry.expiresAt <= now;
|
|
2216
|
+
}
|
|
2217
|
+
function normalizeEntryTimestamps(entry, now, ttlMs) {
|
|
2218
|
+
const createdAt = entry.createdAt ?? now;
|
|
2219
|
+
const expiresAt = entry.expiresAt ?? createdAt + ttlMs;
|
|
2220
|
+
return {
|
|
2221
|
+
...entry,
|
|
2222
|
+
createdAt,
|
|
2223
|
+
expiresAt
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function registerDiscordComponentEntries(params) {
|
|
2227
|
+
const now = Date.now();
|
|
2228
|
+
const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
|
|
2229
|
+
for (const entry of params.entries) {
|
|
2230
|
+
const normalized = normalizeEntryTimestamps({
|
|
2231
|
+
...entry,
|
|
2232
|
+
messageId: params.messageId ?? entry.messageId
|
|
2233
|
+
}, now, ttlMs);
|
|
2234
|
+
componentEntries.set(entry.id, normalized);
|
|
2235
|
+
}
|
|
2236
|
+
for (const modal of params.modals) {
|
|
2237
|
+
const normalized = normalizeEntryTimestamps({
|
|
2238
|
+
...modal,
|
|
2239
|
+
messageId: params.messageId ?? modal.messageId
|
|
2240
|
+
}, now, ttlMs);
|
|
2241
|
+
modalEntries.set(modal.id, normalized);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function resolveDiscordComponentEntry(params) {
|
|
2245
|
+
const entry = componentEntries.get(params.id);
|
|
2246
|
+
if (!entry) return null;
|
|
2247
|
+
if (isExpired(entry, Date.now())) {
|
|
2248
|
+
componentEntries.delete(params.id);
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
if (params.consume !== false) componentEntries.delete(params.id);
|
|
2252
|
+
return entry;
|
|
2253
|
+
}
|
|
2254
|
+
function resolveDiscordModalEntry(params) {
|
|
2255
|
+
const entry = modalEntries.get(params.id);
|
|
2256
|
+
if (!entry) return null;
|
|
2257
|
+
if (isExpired(entry, Date.now())) {
|
|
2258
|
+
modalEntries.delete(params.id);
|
|
2259
|
+
return null;
|
|
2260
|
+
}
|
|
2261
|
+
if (params.consume !== false) modalEntries.delete(params.id);
|
|
2262
|
+
return entry;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
//#endregion
|
|
2266
|
+
//#region src/discord/components.ts
|
|
2267
|
+
const DISCORD_COMPONENT_CUSTOM_ID_KEY = "occomp";
|
|
2268
|
+
const DISCORD_MODAL_CUSTOM_ID_KEY = "ocmodal";
|
|
2269
|
+
const DISCORD_COMPONENT_ATTACHMENT_PREFIX = "attachment://";
|
|
2270
|
+
const BLOCK_ALIASES = new Map([["row", "actions"], ["action-row", "actions"]]);
|
|
2271
|
+
function createShortId(prefix) {
|
|
2272
|
+
return `${prefix}${crypto.randomBytes(6).toString("base64url")}`;
|
|
2273
|
+
}
|
|
2274
|
+
function requireObject(value, label) {
|
|
2275
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
2276
|
+
return value;
|
|
2277
|
+
}
|
|
2278
|
+
function readString(value, label, opts) {
|
|
2279
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
2280
|
+
const trimmed = value.trim();
|
|
2281
|
+
if (!opts?.allowEmpty && !trimmed) throw new Error(`${label} cannot be empty`);
|
|
2282
|
+
return opts?.allowEmpty ? value : trimmed;
|
|
2283
|
+
}
|
|
2284
|
+
function readOptionalString(value) {
|
|
2285
|
+
if (typeof value !== "string") return;
|
|
2286
|
+
const trimmed = value.trim();
|
|
2287
|
+
return trimmed ? trimmed : void 0;
|
|
2288
|
+
}
|
|
2289
|
+
function readOptionalStringArray(value, label) {
|
|
2290
|
+
if (value === void 0) return;
|
|
2291
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
2292
|
+
if (value.length === 0) return;
|
|
2293
|
+
return value.map((entry, index) => readString(entry, `${label}[${index}]`));
|
|
2294
|
+
}
|
|
2295
|
+
function readOptionalNumber(value) {
|
|
2296
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
2297
|
+
return value;
|
|
2298
|
+
}
|
|
2299
|
+
function normalizeModalFieldName(value, index) {
|
|
2300
|
+
const trimmed = value?.trim();
|
|
2301
|
+
if (trimmed) return trimmed;
|
|
2302
|
+
return `field_${index + 1}`;
|
|
2303
|
+
}
|
|
2304
|
+
function normalizeAttachmentRef(value, label) {
|
|
2305
|
+
const trimmed = value.trim();
|
|
2306
|
+
if (!trimmed.startsWith(DISCORD_COMPONENT_ATTACHMENT_PREFIX)) throw new Error(`${label} must start with "${DISCORD_COMPONENT_ATTACHMENT_PREFIX}"`);
|
|
2307
|
+
const attachmentName = trimmed.slice(13).trim();
|
|
2308
|
+
if (!attachmentName) throw new Error(`${label} must include an attachment filename`);
|
|
2309
|
+
return `${DISCORD_COMPONENT_ATTACHMENT_PREFIX}${attachmentName}`;
|
|
2310
|
+
}
|
|
2311
|
+
function resolveDiscordComponentAttachmentName(value) {
|
|
2312
|
+
const trimmed = value.trim();
|
|
2313
|
+
if (!trimmed.startsWith(DISCORD_COMPONENT_ATTACHMENT_PREFIX)) throw new Error(`Attachment reference must start with "${DISCORD_COMPONENT_ATTACHMENT_PREFIX}"`);
|
|
2314
|
+
const attachmentName = trimmed.slice(13).trim();
|
|
2315
|
+
if (!attachmentName) throw new Error("Attachment reference must include a filename");
|
|
2316
|
+
return attachmentName;
|
|
2317
|
+
}
|
|
2318
|
+
function mapButtonStyle(style) {
|
|
2319
|
+
switch ((style ?? "primary").toLowerCase()) {
|
|
2320
|
+
case "secondary": return ButtonStyle.Secondary;
|
|
2321
|
+
case "success": return ButtonStyle.Success;
|
|
2322
|
+
case "danger": return ButtonStyle.Danger;
|
|
2323
|
+
case "link": return ButtonStyle.Link;
|
|
2324
|
+
default: return ButtonStyle.Primary;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
function mapTextInputStyle(style) {
|
|
2328
|
+
return style === "paragraph" ? TextInputStyle.Paragraph : TextInputStyle.Short;
|
|
2329
|
+
}
|
|
2330
|
+
function normalizeBlockType(raw) {
|
|
2331
|
+
const lowered = raw.trim().toLowerCase();
|
|
2332
|
+
return BLOCK_ALIASES.get(lowered) ?? lowered;
|
|
2333
|
+
}
|
|
2334
|
+
function parseSelectOptions(raw, label) {
|
|
2335
|
+
if (raw === void 0) return;
|
|
2336
|
+
if (!Array.isArray(raw)) throw new Error(`${label} must be an array`);
|
|
2337
|
+
return raw.map((entry, index) => {
|
|
2338
|
+
const obj = requireObject(entry, `${label}[${index}]`);
|
|
2339
|
+
return {
|
|
2340
|
+
label: readString(obj.label, `${label}[${index}].label`),
|
|
2341
|
+
value: readString(obj.value, `${label}[${index}].value`),
|
|
2342
|
+
description: readOptionalString(obj.description),
|
|
2343
|
+
emoji: typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji) ? {
|
|
2344
|
+
name: readString(obj.emoji.name, `${label}[${index}].emoji.name`),
|
|
2345
|
+
id: readOptionalString(obj.emoji.id),
|
|
2346
|
+
animated: typeof obj.emoji.animated === "boolean" ? obj.emoji.animated : void 0
|
|
2347
|
+
} : void 0,
|
|
2348
|
+
default: typeof obj.default === "boolean" ? obj.default : void 0
|
|
2349
|
+
};
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
function parseButtonSpec(raw, label) {
|
|
2353
|
+
const obj = requireObject(raw, label);
|
|
2354
|
+
const style = readOptionalString(obj.style);
|
|
2355
|
+
const url = readOptionalString(obj.url);
|
|
2356
|
+
if ((style === "link" || url) && !url) throw new Error(`${label}.url is required for link buttons`);
|
|
2357
|
+
return {
|
|
2358
|
+
label: readString(obj.label, `${label}.label`),
|
|
2359
|
+
style,
|
|
2360
|
+
url,
|
|
2361
|
+
emoji: typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji) ? {
|
|
2362
|
+
name: readString(obj.emoji.name, `${label}.emoji.name`),
|
|
2363
|
+
id: readOptionalString(obj.emoji.id),
|
|
2364
|
+
animated: typeof obj.emoji.animated === "boolean" ? obj.emoji.animated : void 0
|
|
2365
|
+
} : void 0,
|
|
2366
|
+
disabled: typeof obj.disabled === "boolean" ? obj.disabled : void 0,
|
|
2367
|
+
allowedUsers: readOptionalStringArray(obj.allowedUsers, `${label}.allowedUsers`)
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function parseSelectSpec(raw, label) {
|
|
2371
|
+
const obj = requireObject(raw, label);
|
|
2372
|
+
const type = readOptionalString(obj.type);
|
|
2373
|
+
const allowedTypes = [
|
|
2374
|
+
"string",
|
|
2375
|
+
"user",
|
|
2376
|
+
"role",
|
|
2377
|
+
"mentionable",
|
|
2378
|
+
"channel"
|
|
2379
|
+
];
|
|
2380
|
+
if (type && !allowedTypes.includes(type)) throw new Error(`${label}.type must be one of ${allowedTypes.join(", ")}`);
|
|
2381
|
+
return {
|
|
2382
|
+
type,
|
|
2383
|
+
placeholder: readOptionalString(obj.placeholder),
|
|
2384
|
+
minValues: readOptionalNumber(obj.minValues),
|
|
2385
|
+
maxValues: readOptionalNumber(obj.maxValues),
|
|
2386
|
+
options: parseSelectOptions(obj.options, `${label}.options`)
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function parseModalField(raw, label, index) {
|
|
2390
|
+
const obj = requireObject(raw, label);
|
|
2391
|
+
const type = readString(obj.type, `${label}.type`).toLowerCase();
|
|
2392
|
+
const supported = [
|
|
2393
|
+
"text",
|
|
2394
|
+
"checkbox",
|
|
2395
|
+
"radio",
|
|
2396
|
+
"select",
|
|
2397
|
+
"role-select",
|
|
2398
|
+
"user-select"
|
|
2399
|
+
];
|
|
2400
|
+
if (!supported.includes(type)) throw new Error(`${label}.type must be one of ${supported.join(", ")}`);
|
|
2401
|
+
const options = parseSelectOptions(obj.options, `${label}.options`);
|
|
2402
|
+
if ([
|
|
2403
|
+
"checkbox",
|
|
2404
|
+
"radio",
|
|
2405
|
+
"select"
|
|
2406
|
+
].includes(type) && (!options || options.length === 0)) throw new Error(`${label}.options is required for ${type} fields`);
|
|
2407
|
+
return {
|
|
2408
|
+
type,
|
|
2409
|
+
name: normalizeModalFieldName(readOptionalString(obj.name), index),
|
|
2410
|
+
label: readString(obj.label, `${label}.label`),
|
|
2411
|
+
description: readOptionalString(obj.description),
|
|
2412
|
+
placeholder: readOptionalString(obj.placeholder),
|
|
2413
|
+
required: typeof obj.required === "boolean" ? obj.required : void 0,
|
|
2414
|
+
options,
|
|
2415
|
+
minValues: readOptionalNumber(obj.minValues),
|
|
2416
|
+
maxValues: readOptionalNumber(obj.maxValues),
|
|
2417
|
+
minLength: readOptionalNumber(obj.minLength),
|
|
2418
|
+
maxLength: readOptionalNumber(obj.maxLength),
|
|
2419
|
+
style: readOptionalString(obj.style)
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function parseComponentBlock(raw, label) {
|
|
2423
|
+
const obj = requireObject(raw, label);
|
|
2424
|
+
switch (normalizeBlockType(readString(obj.type, `${label}.type`).toLowerCase())) {
|
|
2425
|
+
case "text": return {
|
|
2426
|
+
type: "text",
|
|
2427
|
+
text: readString(obj.text, `${label}.text`)
|
|
2428
|
+
};
|
|
2429
|
+
case "section": {
|
|
2430
|
+
const text = readOptionalString(obj.text);
|
|
2431
|
+
const textsRaw = obj.texts;
|
|
2432
|
+
const texts = Array.isArray(textsRaw) ? textsRaw.map((entry, idx) => readString(entry, `${label}.texts[${idx}]`)) : void 0;
|
|
2433
|
+
if (!text && (!texts || texts.length === 0)) throw new Error(`${label}.text or ${label}.texts is required for section blocks`);
|
|
2434
|
+
let accessory;
|
|
2435
|
+
if (obj.accessory !== void 0) {
|
|
2436
|
+
const accessoryObj = requireObject(obj.accessory, `${label}.accessory`);
|
|
2437
|
+
const accessoryType = readString(accessoryObj.type, `${label}.accessory.type`).toLowerCase();
|
|
2438
|
+
if (accessoryType === "thumbnail") accessory = {
|
|
2439
|
+
type: "thumbnail",
|
|
2440
|
+
url: readString(accessoryObj.url, `${label}.accessory.url`)
|
|
2441
|
+
};
|
|
2442
|
+
else if (accessoryType === "button") accessory = {
|
|
2443
|
+
type: "button",
|
|
2444
|
+
button: parseButtonSpec(accessoryObj.button, `${label}.accessory.button`)
|
|
2445
|
+
};
|
|
2446
|
+
else throw new Error(`${label}.accessory.type must be "thumbnail" or "button"`);
|
|
2447
|
+
}
|
|
2448
|
+
return {
|
|
2449
|
+
type: "section",
|
|
2450
|
+
text,
|
|
2451
|
+
texts,
|
|
2452
|
+
accessory
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
case "separator": {
|
|
2456
|
+
const spacingRaw = obj.spacing;
|
|
2457
|
+
let spacing;
|
|
2458
|
+
if (spacingRaw === "small" || spacingRaw === "large") spacing = spacingRaw;
|
|
2459
|
+
else if (spacingRaw === 1 || spacingRaw === 2) spacing = spacingRaw;
|
|
2460
|
+
else if (spacingRaw !== void 0) throw new Error(`${label}.spacing must be "small", "large", 1, or 2`);
|
|
2461
|
+
const divider = typeof obj.divider === "boolean" ? obj.divider : void 0;
|
|
2462
|
+
return {
|
|
2463
|
+
type: "separator",
|
|
2464
|
+
spacing,
|
|
2465
|
+
divider
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
case "actions": {
|
|
2469
|
+
const buttonsRaw = obj.buttons;
|
|
2470
|
+
const buttons = Array.isArray(buttonsRaw) ? buttonsRaw.map((entry, idx) => parseButtonSpec(entry, `${label}.buttons[${idx}]`)) : void 0;
|
|
2471
|
+
const select = obj.select ? parseSelectSpec(obj.select, `${label}.select`) : void 0;
|
|
2472
|
+
if ((!buttons || buttons.length === 0) && !select) throw new Error(`${label} requires buttons or select`);
|
|
2473
|
+
if (buttons && select) throw new Error(`${label} cannot include both buttons and select`);
|
|
2474
|
+
return {
|
|
2475
|
+
type: "actions",
|
|
2476
|
+
buttons,
|
|
2477
|
+
select
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
case "media-gallery": {
|
|
2481
|
+
const itemsRaw = obj.items;
|
|
2482
|
+
if (!Array.isArray(itemsRaw) || itemsRaw.length === 0) throw new Error(`${label}.items must be a non-empty array`);
|
|
2483
|
+
return {
|
|
2484
|
+
type: "media-gallery",
|
|
2485
|
+
items: itemsRaw.map((entry, idx) => {
|
|
2486
|
+
const itemObj = requireObject(entry, `${label}.items[${idx}]`);
|
|
2487
|
+
return {
|
|
2488
|
+
url: readString(itemObj.url, `${label}.items[${idx}].url`),
|
|
2489
|
+
description: readOptionalString(itemObj.description),
|
|
2490
|
+
spoiler: typeof itemObj.spoiler === "boolean" ? itemObj.spoiler : void 0
|
|
2491
|
+
};
|
|
2492
|
+
})
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
case "file": return {
|
|
2496
|
+
type: "file",
|
|
2497
|
+
file: normalizeAttachmentRef(readString(obj.file, `${label}.file`), `${label}.file`),
|
|
2498
|
+
spoiler: typeof obj.spoiler === "boolean" ? obj.spoiler : void 0
|
|
2499
|
+
};
|
|
2500
|
+
default: throw new Error(`${label}.type must be a supported component block`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
function readDiscordComponentSpec(raw) {
|
|
2504
|
+
if (raw === void 0 || raw === null) return null;
|
|
2505
|
+
const obj = requireObject(raw, "components");
|
|
2506
|
+
const blocksRaw = obj.blocks;
|
|
2507
|
+
const blocks = Array.isArray(blocksRaw) ? blocksRaw.map((entry, idx) => parseComponentBlock(entry, `components.blocks[${idx}]`)) : void 0;
|
|
2508
|
+
const modalRaw = obj.modal;
|
|
2509
|
+
const reusable = typeof obj.reusable === "boolean" ? obj.reusable : void 0;
|
|
2510
|
+
let modal;
|
|
2511
|
+
if (modalRaw !== void 0) {
|
|
2512
|
+
const modalObj = requireObject(modalRaw, "components.modal");
|
|
2513
|
+
const fieldsRaw = modalObj.fields;
|
|
2514
|
+
if (!Array.isArray(fieldsRaw) || fieldsRaw.length === 0) throw new Error("components.modal.fields must be a non-empty array");
|
|
2515
|
+
if (fieldsRaw.length > 5) throw new Error("components.modal.fields supports up to 5 inputs");
|
|
2516
|
+
const fields = fieldsRaw.map((entry, idx) => parseModalField(entry, `components.modal.fields[${idx}]`, idx));
|
|
2517
|
+
modal = {
|
|
2518
|
+
title: readString(modalObj.title, "components.modal.title"),
|
|
2519
|
+
triggerLabel: readOptionalString(modalObj.triggerLabel),
|
|
2520
|
+
triggerStyle: readOptionalString(modalObj.triggerStyle),
|
|
2521
|
+
fields
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
return {
|
|
2525
|
+
text: readOptionalString(obj.text),
|
|
2526
|
+
reusable,
|
|
2527
|
+
container: typeof obj.container === "object" && obj.container && !Array.isArray(obj.container) ? {
|
|
2528
|
+
accentColor: obj.container.accentColor,
|
|
2529
|
+
spoiler: typeof obj.container.spoiler === "boolean" ? obj.container.spoiler : void 0
|
|
2530
|
+
} : void 0,
|
|
2531
|
+
blocks,
|
|
2532
|
+
modal
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
function buildDiscordComponentCustomId(params) {
|
|
2536
|
+
const base = `${DISCORD_COMPONENT_CUSTOM_ID_KEY}:cid=${params.componentId}`;
|
|
2537
|
+
return params.modalId ? `${base};mid=${params.modalId}` : base;
|
|
2538
|
+
}
|
|
2539
|
+
function buildDiscordModalCustomId(modalId) {
|
|
2540
|
+
return `${DISCORD_MODAL_CUSTOM_ID_KEY}:mid=${modalId}`;
|
|
2541
|
+
}
|
|
2542
|
+
function parseDiscordComponentCustomId(id) {
|
|
2543
|
+
const parsed = parseCustomId(id);
|
|
2544
|
+
if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) return null;
|
|
2545
|
+
const componentId = parsed.data.cid;
|
|
2546
|
+
if (typeof componentId !== "string" || !componentId.trim()) return null;
|
|
2547
|
+
const modalId = parsed.data.mid;
|
|
2548
|
+
return {
|
|
2549
|
+
componentId,
|
|
2550
|
+
modalId: typeof modalId === "string" && modalId.trim() ? modalId : void 0
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function parseDiscordModalCustomId(id) {
|
|
2554
|
+
const parsed = parseCustomId(id);
|
|
2555
|
+
if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) return null;
|
|
2556
|
+
const modalId = parsed.data.mid;
|
|
2557
|
+
if (typeof modalId !== "string" || !modalId.trim()) return null;
|
|
2558
|
+
return modalId;
|
|
2559
|
+
}
|
|
2560
|
+
function isDiscordComponentWildcardRegistrationId(id) {
|
|
2561
|
+
return /^__squidclaw_discord_component_[a-z_]+_wildcard__$/.test(id);
|
|
2562
|
+
}
|
|
2563
|
+
function parseDiscordComponentCustomIdForCarbon(id) {
|
|
2564
|
+
if (id === "*" || isDiscordComponentWildcardRegistrationId(id)) return {
|
|
2565
|
+
key: "*",
|
|
2566
|
+
data: {}
|
|
2567
|
+
};
|
|
2568
|
+
const parsed = parseCustomId(id);
|
|
2569
|
+
if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) return parsed;
|
|
2570
|
+
return {
|
|
2571
|
+
key: "*",
|
|
2572
|
+
data: parsed.data
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
function parseDiscordModalCustomIdForCarbon(id) {
|
|
2576
|
+
if (id === "*" || isDiscordComponentWildcardRegistrationId(id)) return {
|
|
2577
|
+
key: "*",
|
|
2578
|
+
data: {}
|
|
2579
|
+
};
|
|
2580
|
+
const parsed = parseCustomId(id);
|
|
2581
|
+
if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) return parsed;
|
|
2582
|
+
return {
|
|
2583
|
+
key: "*",
|
|
2584
|
+
data: parsed.data
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function buildTextDisplays(text, texts) {
|
|
2588
|
+
if (texts && texts.length > 0) return texts.map((entry) => new TextDisplay(entry));
|
|
2589
|
+
if (text) return [new TextDisplay(text)];
|
|
2590
|
+
return [];
|
|
2591
|
+
}
|
|
2592
|
+
function createButtonComponent(params) {
|
|
2593
|
+
const style = mapButtonStyle(params.spec.style);
|
|
2594
|
+
if (style === ButtonStyle.Link || Boolean(params.spec.url)) {
|
|
2595
|
+
if (!params.spec.url) throw new Error("Link buttons require a url");
|
|
2596
|
+
const linkUrl = params.spec.url;
|
|
2597
|
+
class DynamicLinkButton extends LinkButton {
|
|
2598
|
+
constructor(..._args) {
|
|
2599
|
+
super(..._args);
|
|
2600
|
+
this.label = params.spec.label;
|
|
2601
|
+
this.url = linkUrl;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return { component: new DynamicLinkButton() };
|
|
2605
|
+
}
|
|
2606
|
+
const componentId = params.componentId ?? createShortId("btn_");
|
|
2607
|
+
const customId = buildDiscordComponentCustomId({
|
|
2608
|
+
componentId,
|
|
2609
|
+
modalId: params.modalId
|
|
2610
|
+
});
|
|
2611
|
+
class DynamicButton extends Button {
|
|
2612
|
+
constructor(..._args2) {
|
|
2613
|
+
super(..._args2);
|
|
2614
|
+
this.label = params.spec.label;
|
|
2615
|
+
this.customId = customId;
|
|
2616
|
+
this.style = style;
|
|
2617
|
+
this.emoji = params.spec.emoji;
|
|
2618
|
+
this.disabled = params.spec.disabled ?? false;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return {
|
|
2622
|
+
component: new DynamicButton(),
|
|
2623
|
+
entry: {
|
|
2624
|
+
id: componentId,
|
|
2625
|
+
kind: params.modalId ? "modal-trigger" : "button",
|
|
2626
|
+
label: params.spec.label,
|
|
2627
|
+
modalId: params.modalId,
|
|
2628
|
+
allowedUsers: params.spec.allowedUsers
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
function createSelectComponent(params) {
|
|
2633
|
+
const type = (params.spec.type ?? "string").toLowerCase();
|
|
2634
|
+
const componentId = params.componentId ?? createShortId("sel_");
|
|
2635
|
+
const customId = buildDiscordComponentCustomId({ componentId });
|
|
2636
|
+
if (type === "string") {
|
|
2637
|
+
const options = params.spec.options ?? [];
|
|
2638
|
+
if (options.length === 0) throw new Error("String select menus require options");
|
|
2639
|
+
class DynamicStringSelect extends StringSelectMenu {
|
|
2640
|
+
constructor(..._args3) {
|
|
2641
|
+
super(..._args3);
|
|
2642
|
+
this.customId = customId;
|
|
2643
|
+
this.options = options;
|
|
2644
|
+
this.minValues = params.spec.minValues;
|
|
2645
|
+
this.maxValues = params.spec.maxValues;
|
|
2646
|
+
this.placeholder = params.spec.placeholder;
|
|
2647
|
+
this.disabled = false;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return {
|
|
2651
|
+
component: new DynamicStringSelect(),
|
|
2652
|
+
entry: {
|
|
2653
|
+
id: componentId,
|
|
2654
|
+
kind: "select",
|
|
2655
|
+
label: params.spec.placeholder ?? "select",
|
|
2656
|
+
selectType: "string",
|
|
2657
|
+
options: options.map((option) => ({
|
|
2658
|
+
value: option.value,
|
|
2659
|
+
label: option.label
|
|
2660
|
+
}))
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
if (type === "user") {
|
|
2665
|
+
class DynamicUserSelect extends UserSelectMenu {
|
|
2666
|
+
constructor(..._args4) {
|
|
2667
|
+
super(..._args4);
|
|
2668
|
+
this.customId = customId;
|
|
2669
|
+
this.minValues = params.spec.minValues;
|
|
2670
|
+
this.maxValues = params.spec.maxValues;
|
|
2671
|
+
this.placeholder = params.spec.placeholder;
|
|
2672
|
+
this.disabled = false;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return {
|
|
2676
|
+
component: new DynamicUserSelect(),
|
|
2677
|
+
entry: {
|
|
2678
|
+
id: componentId,
|
|
2679
|
+
kind: "select",
|
|
2680
|
+
label: params.spec.placeholder ?? "user select",
|
|
2681
|
+
selectType: "user"
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
if (type === "role") {
|
|
2686
|
+
class DynamicRoleSelect extends RoleSelectMenu {
|
|
2687
|
+
constructor(..._args5) {
|
|
2688
|
+
super(..._args5);
|
|
2689
|
+
this.customId = customId;
|
|
2690
|
+
this.minValues = params.spec.minValues;
|
|
2691
|
+
this.maxValues = params.spec.maxValues;
|
|
2692
|
+
this.placeholder = params.spec.placeholder;
|
|
2693
|
+
this.disabled = false;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return {
|
|
2697
|
+
component: new DynamicRoleSelect(),
|
|
2698
|
+
entry: {
|
|
2699
|
+
id: componentId,
|
|
2700
|
+
kind: "select",
|
|
2701
|
+
label: params.spec.placeholder ?? "role select",
|
|
2702
|
+
selectType: "role"
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
if (type === "mentionable") {
|
|
2707
|
+
class DynamicMentionableSelect extends MentionableSelectMenu {
|
|
2708
|
+
constructor(..._args6) {
|
|
2709
|
+
super(..._args6);
|
|
2710
|
+
this.customId = customId;
|
|
2711
|
+
this.minValues = params.spec.minValues;
|
|
2712
|
+
this.maxValues = params.spec.maxValues;
|
|
2713
|
+
this.placeholder = params.spec.placeholder;
|
|
2714
|
+
this.disabled = false;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return {
|
|
2718
|
+
component: new DynamicMentionableSelect(),
|
|
2719
|
+
entry: {
|
|
2720
|
+
id: componentId,
|
|
2721
|
+
kind: "select",
|
|
2722
|
+
label: params.spec.placeholder ?? "mentionable select",
|
|
2723
|
+
selectType: "mentionable"
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
class DynamicChannelSelect extends ChannelSelectMenu {
|
|
2728
|
+
constructor(..._args7) {
|
|
2729
|
+
super(..._args7);
|
|
2730
|
+
this.customId = customId;
|
|
2731
|
+
this.minValues = params.spec.minValues;
|
|
2732
|
+
this.maxValues = params.spec.maxValues;
|
|
2733
|
+
this.placeholder = params.spec.placeholder;
|
|
2734
|
+
this.disabled = false;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
return {
|
|
2738
|
+
component: new DynamicChannelSelect(),
|
|
2739
|
+
entry: {
|
|
2740
|
+
id: componentId,
|
|
2741
|
+
kind: "select",
|
|
2742
|
+
label: params.spec.placeholder ?? "channel select",
|
|
2743
|
+
selectType: "channel"
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function isSelectComponent(component) {
|
|
2748
|
+
return component instanceof StringSelectMenu || component instanceof UserSelectMenu || component instanceof RoleSelectMenu || component instanceof MentionableSelectMenu || component instanceof ChannelSelectMenu;
|
|
2749
|
+
}
|
|
2750
|
+
function createModalFieldComponent(field) {
|
|
2751
|
+
if (field.type === "text") {
|
|
2752
|
+
class DynamicTextInput extends TextInput {
|
|
2753
|
+
constructor(..._args8) {
|
|
2754
|
+
super(..._args8);
|
|
2755
|
+
this.customId = field.id;
|
|
2756
|
+
this.style = mapTextInputStyle(field.style);
|
|
2757
|
+
this.placeholder = field.placeholder;
|
|
2758
|
+
this.required = field.required;
|
|
2759
|
+
this.minLength = field.minLength;
|
|
2760
|
+
this.maxLength = field.maxLength;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return new DynamicTextInput();
|
|
2764
|
+
}
|
|
2765
|
+
if (field.type === "select") {
|
|
2766
|
+
const options = field.options ?? [];
|
|
2767
|
+
class DynamicModalSelect extends StringSelectMenu {
|
|
2768
|
+
constructor(..._args9) {
|
|
2769
|
+
super(..._args9);
|
|
2770
|
+
this.customId = field.id;
|
|
2771
|
+
this.options = options;
|
|
2772
|
+
this.required = field.required;
|
|
2773
|
+
this.minValues = field.minValues;
|
|
2774
|
+
this.maxValues = field.maxValues;
|
|
2775
|
+
this.placeholder = field.placeholder;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return new DynamicModalSelect();
|
|
2779
|
+
}
|
|
2780
|
+
if (field.type === "role-select") {
|
|
2781
|
+
class DynamicModalRoleSelect extends RoleSelectMenu {
|
|
2782
|
+
constructor(..._args10) {
|
|
2783
|
+
super(..._args10);
|
|
2784
|
+
this.customId = field.id;
|
|
2785
|
+
this.required = field.required;
|
|
2786
|
+
this.minValues = field.minValues;
|
|
2787
|
+
this.maxValues = field.maxValues;
|
|
2788
|
+
this.placeholder = field.placeholder;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
return new DynamicModalRoleSelect();
|
|
2792
|
+
}
|
|
2793
|
+
if (field.type === "user-select") {
|
|
2794
|
+
class DynamicModalUserSelect extends UserSelectMenu {
|
|
2795
|
+
constructor(..._args11) {
|
|
2796
|
+
super(..._args11);
|
|
2797
|
+
this.customId = field.id;
|
|
2798
|
+
this.required = field.required;
|
|
2799
|
+
this.minValues = field.minValues;
|
|
2800
|
+
this.maxValues = field.maxValues;
|
|
2801
|
+
this.placeholder = field.placeholder;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return new DynamicModalUserSelect();
|
|
2805
|
+
}
|
|
2806
|
+
if (field.type === "checkbox") {
|
|
2807
|
+
const options = field.options ?? [];
|
|
2808
|
+
class DynamicCheckboxGroup extends CheckboxGroup {
|
|
2809
|
+
constructor(..._args12) {
|
|
2810
|
+
super(..._args12);
|
|
2811
|
+
this.customId = field.id;
|
|
2812
|
+
this.options = options;
|
|
2813
|
+
this.required = field.required;
|
|
2814
|
+
this.minValues = field.minValues;
|
|
2815
|
+
this.maxValues = field.maxValues;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return new DynamicCheckboxGroup();
|
|
2819
|
+
}
|
|
2820
|
+
const options = field.options ?? [];
|
|
2821
|
+
class DynamicRadioGroup extends RadioGroup {
|
|
2822
|
+
constructor(..._args13) {
|
|
2823
|
+
super(..._args13);
|
|
2824
|
+
this.customId = field.id;
|
|
2825
|
+
this.options = options;
|
|
2826
|
+
this.required = field.required;
|
|
2827
|
+
this.minValues = field.minValues;
|
|
2828
|
+
this.maxValues = field.maxValues;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return new DynamicRadioGroup();
|
|
2832
|
+
}
|
|
2833
|
+
function buildDiscordComponentMessage(params) {
|
|
2834
|
+
const entries = [];
|
|
2835
|
+
const modals = [];
|
|
2836
|
+
const components = [];
|
|
2837
|
+
const containerChildren = [];
|
|
2838
|
+
const addEntry = (entry) => {
|
|
2839
|
+
entries.push({
|
|
2840
|
+
...entry,
|
|
2841
|
+
sessionKey: params.sessionKey,
|
|
2842
|
+
agentId: params.agentId,
|
|
2843
|
+
accountId: params.accountId,
|
|
2844
|
+
reusable: entry.reusable ?? params.spec.reusable
|
|
2845
|
+
});
|
|
2846
|
+
};
|
|
2847
|
+
const text = params.spec.text ?? params.fallbackText;
|
|
2848
|
+
if (text) containerChildren.push(new TextDisplay(text));
|
|
2849
|
+
for (const block of params.spec.blocks ?? []) {
|
|
2850
|
+
if (block.type === "text") {
|
|
2851
|
+
containerChildren.push(new TextDisplay(block.text));
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
if (block.type === "section") {
|
|
2855
|
+
const displays = buildTextDisplays(block.text, block.texts);
|
|
2856
|
+
if (displays.length > 3) throw new Error("Section blocks support up to 3 text displays");
|
|
2857
|
+
let accessory;
|
|
2858
|
+
if (block.accessory?.type === "thumbnail") accessory = new Thumbnail(block.accessory.url);
|
|
2859
|
+
else if (block.accessory?.type === "button") {
|
|
2860
|
+
const { component, entry } = createButtonComponent({ spec: block.accessory.button });
|
|
2861
|
+
accessory = component;
|
|
2862
|
+
if (entry) addEntry(entry);
|
|
2863
|
+
}
|
|
2864
|
+
containerChildren.push(new Section(displays, accessory));
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
if (block.type === "separator") {
|
|
2868
|
+
containerChildren.push(new Separator({
|
|
2869
|
+
spacing: block.spacing,
|
|
2870
|
+
divider: block.divider
|
|
2871
|
+
}));
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (block.type === "media-gallery") {
|
|
2875
|
+
containerChildren.push(new MediaGallery(block.items));
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
if (block.type === "file") {
|
|
2879
|
+
containerChildren.push(new File(block.file, block.spoiler));
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
if (block.type === "actions") {
|
|
2883
|
+
const rowComponents = [];
|
|
2884
|
+
if (block.buttons) {
|
|
2885
|
+
if (block.buttons.length > 5) throw new Error("Action rows support up to 5 buttons");
|
|
2886
|
+
for (const button of block.buttons) {
|
|
2887
|
+
const { component, entry } = createButtonComponent({ spec: button });
|
|
2888
|
+
rowComponents.push(component);
|
|
2889
|
+
if (entry) addEntry(entry);
|
|
2890
|
+
}
|
|
2891
|
+
} else if (block.select) {
|
|
2892
|
+
const { component, entry } = createSelectComponent({ spec: block.select });
|
|
2893
|
+
rowComponents.push(component);
|
|
2894
|
+
addEntry(entry);
|
|
2895
|
+
}
|
|
2896
|
+
containerChildren.push(new Row(rowComponents));
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (params.spec.modal) {
|
|
2900
|
+
const modalId = createShortId("mdl_");
|
|
2901
|
+
const fields = params.spec.modal.fields.map((field, index) => ({
|
|
2902
|
+
id: createShortId("fld_"),
|
|
2903
|
+
name: normalizeModalFieldName(field.name, index),
|
|
2904
|
+
label: field.label,
|
|
2905
|
+
type: field.type,
|
|
2906
|
+
description: field.description,
|
|
2907
|
+
placeholder: field.placeholder,
|
|
2908
|
+
required: field.required,
|
|
2909
|
+
options: field.options,
|
|
2910
|
+
minValues: field.minValues,
|
|
2911
|
+
maxValues: field.maxValues,
|
|
2912
|
+
minLength: field.minLength,
|
|
2913
|
+
maxLength: field.maxLength,
|
|
2914
|
+
style: field.style
|
|
2915
|
+
}));
|
|
2916
|
+
modals.push({
|
|
2917
|
+
id: modalId,
|
|
2918
|
+
title: params.spec.modal.title,
|
|
2919
|
+
fields,
|
|
2920
|
+
sessionKey: params.sessionKey,
|
|
2921
|
+
agentId: params.agentId,
|
|
2922
|
+
accountId: params.accountId,
|
|
2923
|
+
reusable: params.spec.reusable
|
|
2924
|
+
});
|
|
2925
|
+
const { component, entry } = createButtonComponent({
|
|
2926
|
+
spec: {
|
|
2927
|
+
label: params.spec.modal.triggerLabel ?? "Open form",
|
|
2928
|
+
style: params.spec.modal.triggerStyle ?? "primary"
|
|
2929
|
+
},
|
|
2930
|
+
modalId
|
|
2931
|
+
});
|
|
2932
|
+
if (entry) addEntry(entry);
|
|
2933
|
+
const lastChild = containerChildren.at(-1);
|
|
2934
|
+
if (lastChild instanceof Row) {
|
|
2935
|
+
const row = lastChild;
|
|
2936
|
+
const hasSelect = row.components.some((entry) => isSelectComponent(entry));
|
|
2937
|
+
if (row.components.length < 5 && !hasSelect) row.addComponent(component);
|
|
2938
|
+
else containerChildren.push(new Row([component]));
|
|
2939
|
+
} else containerChildren.push(new Row([component]));
|
|
2940
|
+
}
|
|
2941
|
+
if (containerChildren.length === 0) throw new Error("components must include at least one block, text, or modal trigger");
|
|
2942
|
+
const container = new Container(containerChildren, params.spec.container);
|
|
2943
|
+
components.push(container);
|
|
2944
|
+
return {
|
|
2945
|
+
components,
|
|
2946
|
+
entries,
|
|
2947
|
+
modals
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
function buildDiscordComponentMessageFlags(components) {
|
|
2951
|
+
return components.some((component) => component.isV2) ? MessageFlags.IsComponentsV2 : void 0;
|
|
2952
|
+
}
|
|
2953
|
+
var DiscordFormModal = class extends Modal {
|
|
2954
|
+
constructor(params) {
|
|
2955
|
+
super();
|
|
2956
|
+
this.customIdParser = parseDiscordModalCustomIdForCarbon;
|
|
2957
|
+
this.title = params.title;
|
|
2958
|
+
this.customId = buildDiscordModalCustomId(params.modalId);
|
|
2959
|
+
this.components = params.fields.map((field) => {
|
|
2960
|
+
const component = createModalFieldComponent(field);
|
|
2961
|
+
class DynamicLabel extends Label {
|
|
2962
|
+
constructor(..._args14) {
|
|
2963
|
+
super(..._args14);
|
|
2964
|
+
this.label = field.label;
|
|
2965
|
+
this.description = field.description;
|
|
2966
|
+
this.component = component;
|
|
2967
|
+
this.customId = field.id;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
return new DynamicLabel(component);
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
async run() {
|
|
2974
|
+
throw new Error("Modal handler is not registered for dynamic forms");
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
function createDiscordFormModal(entry) {
|
|
2978
|
+
return new DiscordFormModal({
|
|
2979
|
+
modalId: entry.id,
|
|
2980
|
+
title: entry.title,
|
|
2981
|
+
fields: entry.fields
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
function formatDiscordComponentEventText(params) {
|
|
2985
|
+
if (params.kind === "button") return `Clicked "${params.label}".`;
|
|
2986
|
+
const values = params.values ?? [];
|
|
2987
|
+
if (values.length === 0) return `Updated "${params.label}".`;
|
|
2988
|
+
return `Selected ${values.join(", ")} from "${params.label}".`;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
//#endregion
|
|
2992
|
+
//#region src/discord/send.components.ts
|
|
2993
|
+
const DISCORD_FORUM_LIKE_TYPES = new Set([ChannelType.GuildForum, ChannelType.GuildMedia]);
|
|
2994
|
+
function extractComponentAttachmentNames(spec) {
|
|
2995
|
+
const names = [];
|
|
2996
|
+
for (const block of spec.blocks ?? []) if (block.type === "file") names.push(resolveDiscordComponentAttachmentName(block.file));
|
|
2997
|
+
return names;
|
|
2998
|
+
}
|
|
2999
|
+
async function sendDiscordComponentMessage(to, spec, opts = {}) {
|
|
3000
|
+
const cfg = opts.cfg ?? loadConfig();
|
|
3001
|
+
const accountInfo = resolveDiscordAccount({
|
|
3002
|
+
cfg,
|
|
3003
|
+
accountId: opts.accountId
|
|
3004
|
+
});
|
|
3005
|
+
const { token, rest, request } = createDiscordClient(opts, cfg);
|
|
3006
|
+
const { channelId } = await resolveChannelId(rest, await parseAndResolveRecipient(to, opts.accountId, cfg), request);
|
|
3007
|
+
const channelType = await resolveDiscordChannelType(rest, channelId);
|
|
3008
|
+
if (channelType && DISCORD_FORUM_LIKE_TYPES.has(channelType)) throw new Error("Discord components are not supported in forum-style channels");
|
|
3009
|
+
const buildResult = buildDiscordComponentMessage({
|
|
3010
|
+
spec,
|
|
3011
|
+
sessionKey: opts.sessionKey,
|
|
3012
|
+
agentId: opts.agentId,
|
|
3013
|
+
accountId: accountInfo.accountId
|
|
3014
|
+
});
|
|
3015
|
+
const flags = buildDiscordComponentMessageFlags(buildResult.components);
|
|
3016
|
+
const finalFlags = opts.silent ? (flags ?? 0) | SUPPRESS_NOTIFICATIONS_FLAG$1 : flags ?? void 0;
|
|
3017
|
+
const messageReference = opts.replyTo ? {
|
|
3018
|
+
message_id: opts.replyTo,
|
|
3019
|
+
fail_if_not_exists: false
|
|
3020
|
+
} : void 0;
|
|
3021
|
+
const attachmentNames = extractComponentAttachmentNames(spec);
|
|
3022
|
+
const uniqueAttachmentNames = [...new Set(attachmentNames)];
|
|
3023
|
+
if (uniqueAttachmentNames.length > 1) throw new Error("Discord component attachments currently support a single file. Use media-gallery for multiple files.");
|
|
3024
|
+
const expectedAttachmentName = uniqueAttachmentNames[0];
|
|
3025
|
+
let files;
|
|
3026
|
+
if (opts.mediaUrl) {
|
|
3027
|
+
const media = await loadWebMedia(opts.mediaUrl, { localRoots: opts.mediaLocalRoots });
|
|
3028
|
+
const fileName = opts.filename?.trim() || media.fileName || "upload";
|
|
3029
|
+
if (expectedAttachmentName && expectedAttachmentName !== fileName) throw new Error(`Component file block expects attachment "${expectedAttachmentName}", but the uploaded file is "${fileName}". Update components.blocks[].file or provide a matching filename.`);
|
|
3030
|
+
files = [{
|
|
3031
|
+
data: toDiscordFileBlob(media.buffer),
|
|
3032
|
+
name: fileName
|
|
3033
|
+
}];
|
|
3034
|
+
} else if (expectedAttachmentName) throw new Error("Discord component file blocks require a media attachment (media/path/filePath).");
|
|
3035
|
+
const body = stripUndefinedFields({
|
|
3036
|
+
...serializePayload({
|
|
3037
|
+
components: buildResult.components,
|
|
3038
|
+
...finalFlags ? { flags: finalFlags } : {},
|
|
3039
|
+
...files ? { files } : {}
|
|
3040
|
+
}),
|
|
3041
|
+
...messageReference ? { message_reference: messageReference } : {}
|
|
3042
|
+
});
|
|
3043
|
+
let result;
|
|
3044
|
+
try {
|
|
3045
|
+
result = await request(() => rest.post(Routes.channelMessages(channelId), { body }), "components");
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
throw await buildDiscordSendError(err, {
|
|
3048
|
+
channelId,
|
|
3049
|
+
rest,
|
|
3050
|
+
token,
|
|
3051
|
+
hasMedia: Boolean(files?.length)
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
registerDiscordComponentEntries({
|
|
3055
|
+
entries: buildResult.entries,
|
|
3056
|
+
modals: buildResult.modals,
|
|
3057
|
+
messageId: result.id
|
|
3058
|
+
});
|
|
3059
|
+
recordChannelActivity({
|
|
3060
|
+
channel: "discord",
|
|
3061
|
+
accountId: accountInfo.accountId,
|
|
3062
|
+
direction: "outbound"
|
|
3063
|
+
});
|
|
3064
|
+
return {
|
|
3065
|
+
messageId: result.id ?? "unknown",
|
|
3066
|
+
channelId: result.channel_id ?? channelId
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
//#endregion
|
|
3071
|
+
//#region src/discord/send.reactions.ts
|
|
3072
|
+
async function reactMessageDiscord(channelId, messageId, emoji, opts = {}) {
|
|
3073
|
+
const { rest, request } = createDiscordClient(opts, opts.cfg ?? loadConfig());
|
|
3074
|
+
const encoded = normalizeReactionEmoji(emoji);
|
|
3075
|
+
await request(() => rest.put(Routes.channelMessageOwnReaction(channelId, messageId, encoded)), "react");
|
|
3076
|
+
return { ok: true };
|
|
3077
|
+
}
|
|
3078
|
+
async function removeReactionDiscord(channelId, messageId, emoji, opts = {}) {
|
|
3079
|
+
const { rest } = createDiscordClient(opts, opts.cfg ?? loadConfig());
|
|
3080
|
+
const encoded = normalizeReactionEmoji(emoji);
|
|
3081
|
+
await rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encoded));
|
|
3082
|
+
return { ok: true };
|
|
3083
|
+
}
|
|
3084
|
+
async function removeOwnReactionsDiscord(channelId, messageId, opts = {}) {
|
|
3085
|
+
const { rest } = createDiscordClient(opts, opts.cfg ?? loadConfig());
|
|
3086
|
+
const message = await rest.get(Routes.channelMessage(channelId, messageId));
|
|
3087
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
3088
|
+
for (const reaction of message.reactions ?? []) {
|
|
3089
|
+
const identifier = buildReactionIdentifier(reaction.emoji);
|
|
3090
|
+
if (identifier) identifiers.add(identifier);
|
|
3091
|
+
}
|
|
3092
|
+
if (identifiers.size === 0) return {
|
|
3093
|
+
ok: true,
|
|
3094
|
+
removed: []
|
|
3095
|
+
};
|
|
3096
|
+
const removed = [];
|
|
3097
|
+
await Promise.allSettled(Array.from(identifiers, (identifier) => {
|
|
3098
|
+
removed.push(identifier);
|
|
3099
|
+
return rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, normalizeReactionEmoji(identifier)));
|
|
3100
|
+
}));
|
|
3101
|
+
return {
|
|
3102
|
+
ok: true,
|
|
3103
|
+
removed
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
async function fetchReactionsDiscord(channelId, messageId, opts = {}) {
|
|
3107
|
+
const { rest } = createDiscordClient(opts, opts.cfg ?? loadConfig());
|
|
3108
|
+
const reactions = (await rest.get(Routes.channelMessage(channelId, messageId))).reactions ?? [];
|
|
3109
|
+
if (reactions.length === 0) return [];
|
|
3110
|
+
const limit = typeof opts.limit === "number" && Number.isFinite(opts.limit) ? Math.min(Math.max(Math.floor(opts.limit), 1), 100) : 100;
|
|
3111
|
+
const summaries = [];
|
|
3112
|
+
for (const reaction of reactions) {
|
|
3113
|
+
const identifier = buildReactionIdentifier(reaction.emoji);
|
|
3114
|
+
if (!identifier) continue;
|
|
3115
|
+
const encoded = encodeURIComponent(identifier);
|
|
3116
|
+
const users = await rest.get(Routes.channelMessageReaction(channelId, messageId, encoded), { limit });
|
|
3117
|
+
summaries.push({
|
|
3118
|
+
emoji: {
|
|
3119
|
+
id: reaction.emoji.id ?? null,
|
|
3120
|
+
name: reaction.emoji.name ?? null,
|
|
3121
|
+
raw: formatReactionEmoji(reaction.emoji)
|
|
3122
|
+
},
|
|
3123
|
+
count: reaction.count,
|
|
3124
|
+
users: users.map((user) => ({
|
|
3125
|
+
id: user.id,
|
|
3126
|
+
username: user.username,
|
|
3127
|
+
tag: user.username && user.discriminator ? `${user.username}#${user.discriminator}` : user.username
|
|
3128
|
+
}))
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
return summaries;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
//#endregion
|
|
3135
|
+
export { stripUndefinedFields as $, unpinMessageDiscord as A, resolveChannelEntryMatch as At, listScheduledEventsDiscord as B, editMessageDiscord as C, resolveDiscordSystemLocation as Ct, pinMessageDiscord as D, applyChannelMatchMeta as Dt, listThreadsDiscord as E, fetchDiscord as Et, fetchMemberInfoDiscord as F, uploadStickerDiscord as G, timeoutMemberDiscord as H, fetchRoleInfoDiscord as I, editChannelDiscord as J, createChannelDiscord as K, fetchVoiceStatusDiscord as L, banMemberDiscord as M, resolveNestedAllowlistDecision as Mt, createScheduledEventDiscord as N, readMessagesDiscord as O, buildChannelKeyCandidates as Ot, fetchChannelInfoDiscord as P, sendDiscordText as Q, kickMemberDiscord as R, deleteMessageDiscord as S, formatDiscordUserTag as St, listPinsDiscord as T, DiscordApiError as Tt, listGuildEmojisDiscord as U, removeRoleDiscord as V, uploadEmojiDiscord as W, removeChannelPermissionDiscord as X, moveChannelDiscord as Y, setChannelPermissionDiscord as Z, sendStickerDiscord as _, resolveDiscordOwnerAllowFrom as _t, sendDiscordComponentMessage as a, parseDiscordTarget as at, formatMention as b, shouldEmitDiscordReactionNotification as bt, parseDiscordComponentCustomId as c, listDiscordDirectoryPeersLive as ct, parseDiscordModalCustomIdForCarbon as d, normalizeDiscordSlug as dt, fetchChannelPermissionsDiscord as et, readDiscordComponentSpec as f, resolveDiscordAllowListMatch as ft, sendPollDiscord as g, resolveDiscordOwnerAccess as gt, sendMessageDiscord as h, resolveDiscordMemberAccessState as ht, removeReactionDiscord as i, createDiscordRestClient as it, addRoleDiscord as j, resolveChannelEntryMatchWithFallback as jt, searchMessagesDiscord as k, normalizeChannelSlug as kt, parseDiscordComponentCustomIdForCarbon as l, isDiscordGroupAllowedByPolicy as lt, resolveDiscordModalEntry as m, resolveDiscordGuildEntry as mt, reactMessageDiscord as n, chunkDiscordTextWithMode as nt, createDiscordFormModal as o, resolveDiscordChannelId as ot, resolveDiscordComponentEntry as p, resolveDiscordChannelConfigWithFallback as pt, deleteChannelDiscord as q, removeOwnReactionsDiscord as r, createDiscordClient as rt, formatDiscordComponentEventText as s, listDiscordDirectoryGroupsLive as st, fetchReactionsDiscord as t, hasAnyGuildPermissionDiscord as tt, parseDiscordModalCustomId as u, normalizeDiscordAllowList as ut, sendVoiceMessageDiscord as v, resolveDiscordShouldRequireMention as vt, fetchMessageDiscord as w, resolveTimestampMs as wt, createThreadDiscord as x, formatDiscordReactionEmoji as xt, sendWebhookMessageDiscord as y, resolveGroupDmAllow as yt, listGuildChannelsDiscord as z };
|