cclawd 1.0.3 → 1.0.4

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