@gakr-gakr/discord 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +130 -0
  4. package/autobot.plugin.json +15 -0
  5. package/channel-config-api.ts +1 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/config-api.ts +4 -0
  8. package/configured-state.ts +6 -0
  9. package/contract-api.ts +21 -0
  10. package/directory-contract-api.ts +4 -0
  11. package/doctor-contract-api.ts +1 -0
  12. package/index.ts +24 -0
  13. package/package.json +79 -0
  14. package/runtime-api.actions.ts +15 -0
  15. package/runtime-api.lookup.ts +22 -0
  16. package/runtime-api.monitor.ts +50 -0
  17. package/runtime-api.send.ts +79 -0
  18. package/runtime-api.threads.ts +31 -0
  19. package/runtime-api.ts +181 -0
  20. package/runtime-setter-api.ts +3 -0
  21. package/secret-contract-api.ts +4 -0
  22. package/security-audit-contract-api.ts +1 -0
  23. package/security-contract-api.ts +4 -0
  24. package/session-key-api.ts +1 -0
  25. package/setup-entry.ts +9 -0
  26. package/setup-plugin-api.ts +3 -0
  27. package/src/account-inspect.ts +131 -0
  28. package/src/accounts.ts +205 -0
  29. package/src/actions/handle-action.guild-admin.ts +421 -0
  30. package/src/actions/handle-action.ts +402 -0
  31. package/src/actions/runtime.guild.ts +446 -0
  32. package/src/actions/runtime.messaging.messages.ts +226 -0
  33. package/src/actions/runtime.messaging.reactions.ts +67 -0
  34. package/src/actions/runtime.messaging.runtime.ts +73 -0
  35. package/src/actions/runtime.messaging.send.ts +336 -0
  36. package/src/actions/runtime.messaging.shared.ts +97 -0
  37. package/src/actions/runtime.messaging.ts +37 -0
  38. package/src/actions/runtime.moderation-shared.ts +48 -0
  39. package/src/actions/runtime.moderation.ts +116 -0
  40. package/src/actions/runtime.presence.ts +117 -0
  41. package/src/actions/runtime.shared.ts +86 -0
  42. package/src/actions/runtime.ts +87 -0
  43. package/src/api.ts +219 -0
  44. package/src/approval-handler.runtime.ts +636 -0
  45. package/src/approval-native.ts +219 -0
  46. package/src/approval-runtime.ts +14 -0
  47. package/src/approval-shared.ts +56 -0
  48. package/src/audit-core.ts +178 -0
  49. package/src/audit.ts +32 -0
  50. package/src/channel-actions.runtime.ts +1 -0
  51. package/src/channel-actions.ts +254 -0
  52. package/src/channel-api.ts +29 -0
  53. package/src/channel.conversation.ts +159 -0
  54. package/src/channel.loaders.ts +50 -0
  55. package/src/channel.runtime.ts +1 -0
  56. package/src/channel.setup.ts +12 -0
  57. package/src/channel.ts +728 -0
  58. package/src/chunk.ts +321 -0
  59. package/src/client.ts +143 -0
  60. package/src/component-custom-id.ts +72 -0
  61. package/src/components-registry.ts +356 -0
  62. package/src/components.builders.ts +410 -0
  63. package/src/components.modal.ts +124 -0
  64. package/src/components.parse.ts +407 -0
  65. package/src/components.ts +54 -0
  66. package/src/components.types.ts +187 -0
  67. package/src/config-schema.ts +6 -0
  68. package/src/config-ui-hints.ts +354 -0
  69. package/src/conversation-identity.ts +58 -0
  70. package/src/delivery-retry.ts +56 -0
  71. package/src/directory-cache.ts +116 -0
  72. package/src/directory-config.ts +58 -0
  73. package/src/directory-live.ts +135 -0
  74. package/src/doctor-contract.ts +477 -0
  75. package/src/doctor-shared.ts +5 -0
  76. package/src/doctor.ts +340 -0
  77. package/src/draft-chunking.ts +43 -0
  78. package/src/draft-stream.ts +162 -0
  79. package/src/error-body.ts +38 -0
  80. package/src/exec-approvals.ts +110 -0
  81. package/src/gateway-logging.ts +67 -0
  82. package/src/group-policy.ts +113 -0
  83. package/src/guilds.ts +29 -0
  84. package/src/inbound-event-delivery.ts +135 -0
  85. package/src/interactive-dispatch.ts +104 -0
  86. package/src/internal/api.commands.ts +51 -0
  87. package/src/internal/api.guild.ts +164 -0
  88. package/src/internal/api.interactions.ts +53 -0
  89. package/src/internal/api.messages.ts +113 -0
  90. package/src/internal/api.reactions.ts +38 -0
  91. package/src/internal/api.ts +61 -0
  92. package/src/internal/api.users.ts +19 -0
  93. package/src/internal/api.webhooks.ts +13 -0
  94. package/src/internal/client.ts +310 -0
  95. package/src/internal/command-deploy.ts +352 -0
  96. package/src/internal/commands.ts +188 -0
  97. package/src/internal/components.base.ts +65 -0
  98. package/src/internal/components.message.ts +279 -0
  99. package/src/internal/components.modal.ts +95 -0
  100. package/src/internal/components.ts +31 -0
  101. package/src/internal/discord.ts +11 -0
  102. package/src/internal/embeds.ts +35 -0
  103. package/src/internal/entity-cache.ts +98 -0
  104. package/src/internal/event-queue.ts +185 -0
  105. package/src/internal/gateway-close-codes.ts +25 -0
  106. package/src/internal/gateway-dispatch.ts +96 -0
  107. package/src/internal/gateway-identify-limiter.ts +26 -0
  108. package/src/internal/gateway-lifecycle.ts +75 -0
  109. package/src/internal/gateway-rate-limit.ts +104 -0
  110. package/src/internal/gateway.ts +479 -0
  111. package/src/internal/interaction-dispatch.ts +162 -0
  112. package/src/internal/interaction-options.ts +98 -0
  113. package/src/internal/interaction-response.ts +53 -0
  114. package/src/internal/interactions.ts +378 -0
  115. package/src/internal/listeners.ts +91 -0
  116. package/src/internal/modal-fields.ts +95 -0
  117. package/src/internal/payload.ts +69 -0
  118. package/src/internal/rest-body.ts +115 -0
  119. package/src/internal/rest-errors.ts +88 -0
  120. package/src/internal/rest-routes.ts +50 -0
  121. package/src/internal/rest-scheduler.ts +557 -0
  122. package/src/internal/rest.ts +322 -0
  123. package/src/internal/schemas.ts +36 -0
  124. package/src/internal/structures.ts +280 -0
  125. package/src/internal/test-builders.test-support.ts +167 -0
  126. package/src/internal/voice.ts +49 -0
  127. package/src/media-detection.ts +28 -0
  128. package/src/mentions.ts +147 -0
  129. package/src/monitor/ack-reactions.ts +70 -0
  130. package/src/monitor/agent-components-auth.ts +7 -0
  131. package/src/monitor/agent-components-context.ts +154 -0
  132. package/src/monitor/agent-components-data.ts +224 -0
  133. package/src/monitor/agent-components-dm-auth.ts +177 -0
  134. package/src/monitor/agent-components-guild-auth.ts +322 -0
  135. package/src/monitor/agent-components-helpers.runtime.ts +3 -0
  136. package/src/monitor/agent-components-helpers.ts +34 -0
  137. package/src/monitor/agent-components-reply.ts +10 -0
  138. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  139. package/src/monitor/agent-components.dispatch.ts +359 -0
  140. package/src/monitor/agent-components.handlers.ts +303 -0
  141. package/src/monitor/agent-components.modal.ts +160 -0
  142. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  143. package/src/monitor/agent-components.runtime.ts +14 -0
  144. package/src/monitor/agent-components.system-controls.ts +215 -0
  145. package/src/monitor/agent-components.ts +70 -0
  146. package/src/monitor/agent-components.types.ts +58 -0
  147. package/src/monitor/agent-components.wildcard-controls.ts +171 -0
  148. package/src/monitor/allow-list.ts +631 -0
  149. package/src/monitor/auto-presence.ts +356 -0
  150. package/src/monitor/channel-access.ts +102 -0
  151. package/src/monitor/commands.ts +9 -0
  152. package/src/monitor/dm-command-auth.ts +259 -0
  153. package/src/monitor/dm-command-decision.ts +49 -0
  154. package/src/monitor/exec-approvals.ts +161 -0
  155. package/src/monitor/format.ts +45 -0
  156. package/src/monitor/gateway-handle.ts +34 -0
  157. package/src/monitor/gateway-metadata.ts +298 -0
  158. package/src/monitor/gateway-plugin.ts +302 -0
  159. package/src/monitor/gateway-registry.ts +37 -0
  160. package/src/monitor/gateway-supervisor.ts +206 -0
  161. package/src/monitor/inbound-context.ts +95 -0
  162. package/src/monitor/inbound-dedupe.ts +79 -0
  163. package/src/monitor/inbound-job.ts +118 -0
  164. package/src/monitor/listeners.queue.ts +91 -0
  165. package/src/monitor/listeners.reactions.ts +594 -0
  166. package/src/monitor/listeners.ts +150 -0
  167. package/src/monitor/message-channel-info.ts +96 -0
  168. package/src/monitor/message-forwarded.ts +114 -0
  169. package/src/monitor/message-handler.batch-gate.ts +19 -0
  170. package/src/monitor/message-handler.context.ts +492 -0
  171. package/src/monitor/message-handler.dm-preflight.ts +119 -0
  172. package/src/monitor/message-handler.draft-preview.ts +436 -0
  173. package/src/monitor/message-handler.hydration.ts +198 -0
  174. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  175. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  176. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  177. package/src/monitor/message-handler.preflight-context.ts +54 -0
  178. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  179. package/src/monitor/message-handler.preflight-history.ts +23 -0
  180. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  181. package/src/monitor/message-handler.preflight-pluralkit.ts +28 -0
  182. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  183. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  184. package/src/monitor/message-handler.preflight.ts +822 -0
  185. package/src/monitor/message-handler.preflight.types.ts +115 -0
  186. package/src/monitor/message-handler.process.ts +1033 -0
  187. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  188. package/src/monitor/message-handler.ts +309 -0
  189. package/src/monitor/message-media.ts +536 -0
  190. package/src/monitor/message-run-queue.ts +101 -0
  191. package/src/monitor/message-text.ts +171 -0
  192. package/src/monitor/message-utils.ts +34 -0
  193. package/src/monitor/model-picker-preferences.ts +184 -0
  194. package/src/monitor/model-picker.state.ts +364 -0
  195. package/src/monitor/model-picker.test-utils.ts +26 -0
  196. package/src/monitor/model-picker.ts +38 -0
  197. package/src/monitor/model-picker.view.ts +722 -0
  198. package/src/monitor/native-command-agent-reply.ts +125 -0
  199. package/src/monitor/native-command-arg-ui.ts +233 -0
  200. package/src/monitor/native-command-auth.ts +309 -0
  201. package/src/monitor/native-command-bypass.ts +13 -0
  202. package/src/monitor/native-command-context.ts +109 -0
  203. package/src/monitor/native-command-dispatch.ts +35 -0
  204. package/src/monitor/native-command-model-picker-apply.ts +209 -0
  205. package/src/monitor/native-command-model-picker-interaction.ts +516 -0
  206. package/src/monitor/native-command-model-picker-ui.ts +357 -0
  207. package/src/monitor/native-command-reply.ts +185 -0
  208. package/src/monitor/native-command-route.ts +91 -0
  209. package/src/monitor/native-command-status.ts +76 -0
  210. package/src/monitor/native-command-ui.ts +26 -0
  211. package/src/monitor/native-command-ui.types.ts +20 -0
  212. package/src/monitor/native-command.args.ts +45 -0
  213. package/src/monitor/native-command.options.ts +153 -0
  214. package/src/monitor/native-command.runtime.ts +51 -0
  215. package/src/monitor/native-command.ts +747 -0
  216. package/src/monitor/native-command.types.ts +9 -0
  217. package/src/monitor/native-interaction-channel-context.ts +50 -0
  218. package/src/monitor/preflight-audio.runtime.ts +9 -0
  219. package/src/monitor/preflight-audio.ts +130 -0
  220. package/src/monitor/presence-cache.ts +61 -0
  221. package/src/monitor/presence.ts +50 -0
  222. package/src/monitor/provider-session.runtime.ts +12 -0
  223. package/src/monitor/provider.acp.ts +89 -0
  224. package/src/monitor/provider.allowlist.ts +398 -0
  225. package/src/monitor/provider.cleanup.ts +41 -0
  226. package/src/monitor/provider.commands.ts +129 -0
  227. package/src/monitor/provider.config-log.ts +45 -0
  228. package/src/monitor/provider.deploy-errors.ts +362 -0
  229. package/src/monitor/provider.deploy.ts +221 -0
  230. package/src/monitor/provider.interactions.ts +160 -0
  231. package/src/monitor/provider.lifecycle.ts +562 -0
  232. package/src/monitor/provider.runtime.ts +1 -0
  233. package/src/monitor/provider.startup-log.ts +32 -0
  234. package/src/monitor/provider.startup.ts +323 -0
  235. package/src/monitor/provider.ts +688 -0
  236. package/src/monitor/reply-context.ts +64 -0
  237. package/src/monitor/reply-delivery.ts +216 -0
  238. package/src/monitor/reply-safety.ts +96 -0
  239. package/src/monitor/rest-fetch.ts +97 -0
  240. package/src/monitor/route-resolution.ts +140 -0
  241. package/src/monitor/sender-identity.ts +81 -0
  242. package/src/monitor/startup-status.ts +10 -0
  243. package/src/monitor/status.ts +22 -0
  244. package/src/monitor/system-events.ts +55 -0
  245. package/src/monitor/thread-bindings.config.ts +35 -0
  246. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  247. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  248. package/src/monitor/thread-bindings.manager.ts +554 -0
  249. package/src/monitor/thread-bindings.messages.ts +6 -0
  250. package/src/monitor/thread-bindings.persona.ts +25 -0
  251. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  252. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  253. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  254. package/src/monitor/thread-bindings.state.ts +540 -0
  255. package/src/monitor/thread-bindings.ts +48 -0
  256. package/src/monitor/thread-bindings.types.ts +83 -0
  257. package/src/monitor/thread-channel-context.ts +112 -0
  258. package/src/monitor/thread-session-close.ts +63 -0
  259. package/src/monitor/thread-title.ts +181 -0
  260. package/src/monitor/threading.auto-thread.ts +287 -0
  261. package/src/monitor/threading.cache.ts +45 -0
  262. package/src/monitor/threading.starter.ts +288 -0
  263. package/src/monitor/threading.ts +20 -0
  264. package/src/monitor/threading.types.ts +102 -0
  265. package/src/monitor/timeouts.ts +84 -0
  266. package/src/monitor/typing.ts +17 -0
  267. package/src/monitor.gateway.ts +75 -0
  268. package/src/monitor.ts +28 -0
  269. package/src/network-config.ts +79 -0
  270. package/src/normalize.ts +86 -0
  271. package/src/outbound-adapter.ts +327 -0
  272. package/src/outbound-approval.ts +29 -0
  273. package/src/outbound-components.ts +86 -0
  274. package/src/outbound-payload.ts +208 -0
  275. package/src/outbound-send-context.ts +92 -0
  276. package/src/outbound-session-route.ts +72 -0
  277. package/src/pluralkit.ts +58 -0
  278. package/src/preview-streaming.ts +18 -0
  279. package/src/probe.runtime.ts +1 -0
  280. package/src/probe.ts +237 -0
  281. package/src/proxy-fetch.ts +92 -0
  282. package/src/proxy-request-client.ts +21 -0
  283. package/src/recipient-resolution.ts +39 -0
  284. package/src/resolve-allowlist-common.ts +39 -0
  285. package/src/resolve-channels.ts +369 -0
  286. package/src/resolve-users.ts +184 -0
  287. package/src/retry.ts +98 -0
  288. package/src/runtime-api.ts +64 -0
  289. package/src/runtime-config.ts +16 -0
  290. package/src/runtime.ts +23 -0
  291. package/src/secret-config-contract.ts +140 -0
  292. package/src/security-audit.runtime.ts +1 -0
  293. package/src/security-audit.ts +208 -0
  294. package/src/security-contract.ts +47 -0
  295. package/src/security-doctor.ts +20 -0
  296. package/src/security.ts +60 -0
  297. package/src/send-target-parsing.ts +14 -0
  298. package/src/send.channels.ts +139 -0
  299. package/src/send.components.ts +391 -0
  300. package/src/send.emojis-stickers.ts +57 -0
  301. package/src/send.guild.ts +170 -0
  302. package/src/send.message-request.ts +112 -0
  303. package/src/send.messages.ts +229 -0
  304. package/src/send.outbound.ts +459 -0
  305. package/src/send.permissions.ts +283 -0
  306. package/src/send.reactions.ts +155 -0
  307. package/src/send.receipt.ts +69 -0
  308. package/src/send.shared.ts +469 -0
  309. package/src/send.ts +82 -0
  310. package/src/send.types.ts +191 -0
  311. package/src/send.typing.ts +9 -0
  312. package/src/send.voice.ts +140 -0
  313. package/src/send.webhook.ts +137 -0
  314. package/src/session-contract.ts +3 -0
  315. package/src/session-key-normalization.ts +47 -0
  316. package/src/setup-account-state.ts +144 -0
  317. package/src/setup-adapter.ts +14 -0
  318. package/src/setup-core.ts +215 -0
  319. package/src/setup-runtime-helpers.ts +10 -0
  320. package/src/setup-surface.ts +132 -0
  321. package/src/shared-interactive.ts +167 -0
  322. package/src/shared.ts +197 -0
  323. package/src/status-issues.ts +201 -0
  324. package/src/subagent-hooks.ts +232 -0
  325. package/src/target-parsing.ts +70 -0
  326. package/src/target-resolver.ts +129 -0
  327. package/src/targets.ts +12 -0
  328. package/src/token.ts +107 -0
  329. package/src/ui-colors.ts +27 -0
  330. package/src/ui.ts +20 -0
  331. package/src/voice/access.ts +126 -0
  332. package/src/voice/audio.ts +249 -0
  333. package/src/voice/capture-state.ts +120 -0
  334. package/src/voice/command.ts +284 -0
  335. package/src/voice/config.ts +8 -0
  336. package/src/voice/ingress.ts +164 -0
  337. package/src/voice/manager.runtime.ts +14 -0
  338. package/src/voice/manager.ts +1155 -0
  339. package/src/voice/prompt.ts +22 -0
  340. package/src/voice/realtime.ts +1370 -0
  341. package/src/voice/receive-recovery.ts +159 -0
  342. package/src/voice/sanitize.ts +29 -0
  343. package/src/voice/sdk-runtime.ts +14 -0
  344. package/src/voice/segment.ts +160 -0
  345. package/src/voice/session.ts +81 -0
  346. package/src/voice/speaker-context.ts +127 -0
  347. package/src/voice/tts.ts +151 -0
  348. package/src/voice-message.ts +474 -0
  349. package/subagent-hooks-api.ts +27 -0
  350. package/test-api.ts +4 -0
  351. package/thread-binding-api.ts +1 -0
  352. package/timeouts.ts +6 -0
  353. package/tsconfig.json +16 -0
@@ -0,0 +1,391 @@
1
+ import { ChannelType } from "discord-api-types/v10";
2
+ import { recordChannelActivity } from "autobot/plugin-sdk/channel-activity-runtime";
3
+ import type { MarkdownTableMode, AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
4
+ import type { OutboundMediaAccess } from "autobot/plugin-sdk/media-runtime";
5
+ import { requireRuntimeConfig } from "autobot/plugin-sdk/plugin-config-runtime";
6
+ import type { ChunkMode } from "autobot/plugin-sdk/reply-chunking";
7
+ import { resolveDiscordAccount } from "./accounts.js";
8
+ import { registerDiscordComponentEntries } from "./components-registry.js";
9
+ import {
10
+ buildDiscordComponentMessage,
11
+ buildDiscordComponentMessageFlags,
12
+ resolveDiscordComponentAttachmentName,
13
+ type DiscordComponentBuildResult,
14
+ type DiscordComponentMessageSpec,
15
+ } from "./components.js";
16
+ import {
17
+ createChannelMessage,
18
+ editChannelMessage,
19
+ serializePayload,
20
+ type MessagePayloadFile,
21
+ type MessagePayloadObject,
22
+ type RequestClient,
23
+ } from "./internal/discord.js";
24
+ import { parseAndResolveRecipient } from "./recipient-resolution.js";
25
+ import { loadOutboundMediaFromUrl } from "./runtime-api.js";
26
+ import { sendMessageDiscord } from "./send.outbound.js";
27
+ import { createDiscordSendResult } from "./send.receipt.js";
28
+ import {
29
+ buildDiscordSendError,
30
+ createDiscordClient,
31
+ resolveChannelId,
32
+ resolveDiscordChannelType,
33
+ toDiscordFileBlob,
34
+ stripUndefinedFields,
35
+ SUPPRESS_NOTIFICATIONS_FLAG,
36
+ } from "./send.shared.js";
37
+ import type { DiscordSendResult } from "./send.types.js";
38
+
39
+ const DISCORD_FORUM_LIKE_TYPES = new Set<number>([ChannelType.GuildForum, ChannelType.GuildMedia]);
40
+
41
+ function extractComponentAttachmentNames(spec: DiscordComponentMessageSpec): string[] {
42
+ const names: string[] = [];
43
+ for (const block of spec.blocks ?? []) {
44
+ if (block.type === "file") {
45
+ names.push(resolveDiscordComponentAttachmentName(block.file));
46
+ }
47
+ }
48
+ return names;
49
+ }
50
+
51
+ function hasComponentAttachmentBlock(spec: DiscordComponentMessageSpec): boolean {
52
+ return (spec.blocks ?? []).some((block) => block.type === "file");
53
+ }
54
+
55
+ function withImplicitComponentAttachmentBlock(
56
+ spec: DiscordComponentMessageSpec,
57
+ attachmentName: string | undefined,
58
+ ): DiscordComponentMessageSpec {
59
+ if (!attachmentName || hasComponentAttachmentBlock(spec)) {
60
+ return spec;
61
+ }
62
+ // Discord File components must point at the uploaded attachment name. Add the
63
+ // matching file block automatically so callers do not have to duplicate it.
64
+ return {
65
+ ...spec,
66
+ blocks: [
67
+ ...(spec.blocks ?? []),
68
+ {
69
+ type: "file",
70
+ file: `attachment://${attachmentName}`,
71
+ },
72
+ ],
73
+ };
74
+ }
75
+
76
+ function hasClassicOnlyBlocks(spec: DiscordComponentMessageSpec): boolean {
77
+ return (spec.blocks ?? []).every((block) => block.type === "text" || block.type === "file");
78
+ }
79
+
80
+ function hasUnsupportedClassicFeatures(spec: DiscordComponentMessageSpec): boolean {
81
+ return Boolean(spec.modal || spec.container);
82
+ }
83
+
84
+ function hasAtMostOneNonSpoilerFile(spec: DiscordComponentMessageSpec): boolean {
85
+ let fileBlockCount = 0;
86
+ for (const block of spec.blocks ?? []) {
87
+ if (block.type !== "file") {
88
+ continue;
89
+ }
90
+ fileBlockCount += 1;
91
+ if (block.spoiler) {
92
+ return false;
93
+ }
94
+ }
95
+ return fileBlockCount <= 1;
96
+ }
97
+
98
+ type ClassicDiscordMessageDecision =
99
+ | {
100
+ mode: "classic";
101
+ reason: "plain-text-single-file";
102
+ }
103
+ | {
104
+ mode: "components";
105
+ reason: "unsupported-feature" | "unsupported-block" | "multiple-or-spoiler-files";
106
+ };
107
+
108
+ /**
109
+ * Keep the downgrade rules explicit because this path is only safe when the
110
+ * spec means exactly what a plain Discord message can represent.
111
+ */
112
+ function getClassicDiscordMessageDecision(
113
+ spec: DiscordComponentMessageSpec,
114
+ ): ClassicDiscordMessageDecision {
115
+ if (hasUnsupportedClassicFeatures(spec)) {
116
+ return { mode: "components", reason: "unsupported-feature" };
117
+ }
118
+ if (!hasClassicOnlyBlocks(spec)) {
119
+ return { mode: "components", reason: "unsupported-block" };
120
+ }
121
+ if (!hasAtMostOneNonSpoilerFile(spec)) {
122
+ return { mode: "components", reason: "multiple-or-spoiler-files" };
123
+ }
124
+ return { mode: "classic", reason: "plain-text-single-file" };
125
+ }
126
+
127
+ function collapseClassicComponentText(spec: DiscordComponentMessageSpec): string {
128
+ const parts: string[] = [];
129
+ const addPart = (value: string | undefined) => {
130
+ if (typeof value !== "string") {
131
+ return;
132
+ }
133
+ const trimmed = value.trim();
134
+ if (!trimmed || parts.includes(trimmed)) {
135
+ return;
136
+ }
137
+ parts.push(trimmed);
138
+ };
139
+
140
+ addPart(spec.text);
141
+ for (const block of spec.blocks ?? []) {
142
+ if (block.type === "text") {
143
+ addPart(block.text);
144
+ }
145
+ }
146
+ return parts.join("\n\n");
147
+ }
148
+
149
+ type DiscordComponentSendOpts = {
150
+ cfg: AutoBotConfig;
151
+ accountId?: string;
152
+ token?: string;
153
+ rest?: RequestClient;
154
+ silent?: boolean;
155
+ replyTo?: string;
156
+ sessionKey?: string;
157
+ agentId?: string;
158
+ mediaUrl?: string;
159
+ mediaAccess?: OutboundMediaAccess;
160
+ mediaLocalRoots?: readonly string[];
161
+ mediaReadFile?: (filePath: string) => Promise<Buffer>;
162
+ filename?: string;
163
+ textLimit?: number;
164
+ maxLinesPerMessage?: number;
165
+ tableMode?: MarkdownTableMode;
166
+ chunkMode?: ChunkMode;
167
+ suppressEmbeds?: boolean;
168
+ };
169
+
170
+ export function registerBuiltDiscordComponentMessage(params: {
171
+ buildResult: DiscordComponentBuildResult;
172
+ messageId: string;
173
+ }): void {
174
+ registerDiscordComponentEntries({
175
+ entries: params.buildResult.entries,
176
+ modals: params.buildResult.modals,
177
+ messageId: params.messageId,
178
+ });
179
+ }
180
+
181
+ async function buildDiscordComponentPayload(params: {
182
+ spec: DiscordComponentMessageSpec;
183
+ opts: DiscordComponentSendOpts;
184
+ accountId: string;
185
+ }): Promise<{
186
+ body: ReturnType<typeof stripUndefinedFields>;
187
+ buildResult: ReturnType<typeof buildDiscordComponentMessage>;
188
+ }> {
189
+ const messageReference = params.opts.replyTo
190
+ ? { message_id: params.opts.replyTo, fail_if_not_exists: false }
191
+ : undefined;
192
+
193
+ let spec = params.spec;
194
+ let resolvedFileName: string | undefined;
195
+ let files: MessagePayloadFile[] | undefined;
196
+ if (params.opts.mediaUrl) {
197
+ const media = await loadOutboundMediaFromUrl(params.opts.mediaUrl, {
198
+ mediaAccess: params.opts.mediaAccess,
199
+ mediaLocalRoots: params.opts.mediaLocalRoots,
200
+ mediaReadFile: params.opts.mediaReadFile,
201
+ });
202
+ const filenameOverride = params.opts.filename?.trim();
203
+ resolvedFileName = filenameOverride || media.fileName || "upload";
204
+ spec = withImplicitComponentAttachmentBlock(spec, resolvedFileName);
205
+ const fileData = toDiscordFileBlob(media.buffer);
206
+ files = [{ data: fileData, name: resolvedFileName }];
207
+ }
208
+
209
+ const attachmentNames = extractComponentAttachmentNames(spec);
210
+ const uniqueAttachmentNames = [...new Set(attachmentNames)];
211
+ if (uniqueAttachmentNames.length > 1) {
212
+ throw new Error(
213
+ "Discord component attachments currently support a single file. Use media-gallery for multiple files.",
214
+ );
215
+ }
216
+ const expectedAttachmentName = uniqueAttachmentNames[0];
217
+ if (expectedAttachmentName && resolvedFileName && expectedAttachmentName !== resolvedFileName) {
218
+ throw new Error(
219
+ `Component file block expects attachment "${expectedAttachmentName}", but the uploaded file is "${resolvedFileName}". Update components.blocks[].file or provide a matching filename.`,
220
+ );
221
+ }
222
+ if (!params.opts.mediaUrl && expectedAttachmentName) {
223
+ throw new Error(
224
+ "Discord component file blocks require a media attachment (media/path/filePath).",
225
+ );
226
+ }
227
+
228
+ const buildResult = buildDiscordComponentMessage({
229
+ spec,
230
+ sessionKey: params.opts.sessionKey,
231
+ agentId: params.opts.agentId,
232
+ accountId: params.accountId,
233
+ });
234
+ const flags = buildDiscordComponentMessageFlags(buildResult.components);
235
+ const finalFlags = params.opts.silent
236
+ ? (flags ?? 0) | SUPPRESS_NOTIFICATIONS_FLAG
237
+ : (flags ?? undefined);
238
+
239
+ const payload: MessagePayloadObject = {
240
+ components: buildResult.components,
241
+ ...(finalFlags ? { flags: finalFlags } : {}),
242
+ ...(files ? { files } : {}),
243
+ };
244
+ const body = stripUndefinedFields({
245
+ ...serializePayload(payload),
246
+ ...(messageReference ? { message_reference: messageReference } : {}),
247
+ });
248
+
249
+ return { body, buildResult };
250
+ }
251
+
252
+ export async function sendDiscordComponentMessage(
253
+ to: string,
254
+ spec: DiscordComponentMessageSpec,
255
+ opts: DiscordComponentSendOpts,
256
+ ): Promise<DiscordSendResult> {
257
+ const classicDecision = getClassicDiscordMessageDecision(spec);
258
+ if (opts.mediaUrl && classicDecision.mode === "classic") {
259
+ return await sendMessageDiscord(to, collapseClassicComponentText(spec), {
260
+ cfg: opts.cfg,
261
+ accountId: opts.accountId,
262
+ token: opts.token,
263
+ rest: opts.rest,
264
+ mediaUrl: opts.mediaUrl,
265
+ filename: opts.filename,
266
+ mediaLocalRoots: opts.mediaLocalRoots,
267
+ mediaReadFile: opts.mediaReadFile,
268
+ mediaAccess: opts.mediaAccess,
269
+ replyTo: opts.replyTo,
270
+ silent: opts.silent,
271
+ textLimit: opts.textLimit,
272
+ maxLinesPerMessage: opts.maxLinesPerMessage,
273
+ tableMode: opts.tableMode,
274
+ chunkMode: opts.chunkMode,
275
+ ...(opts.suppressEmbeds === undefined ? {} : { suppressEmbeds: opts.suppressEmbeds }),
276
+ });
277
+ }
278
+
279
+ const cfg = requireRuntimeConfig(opts.cfg, "Discord component send");
280
+ const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId });
281
+ const { token, rest, request } = createDiscordClient({ ...opts, cfg });
282
+ const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
283
+ const { channelId } = await resolveChannelId(rest, recipient, request);
284
+
285
+ const channelType = await resolveDiscordChannelType(rest, channelId);
286
+
287
+ if (channelType && DISCORD_FORUM_LIKE_TYPES.has(channelType)) {
288
+ throw new Error("Discord components are not supported in forum-style channels");
289
+ }
290
+
291
+ const { body, buildResult } = await buildDiscordComponentPayload({
292
+ spec,
293
+ opts,
294
+ accountId: accountInfo.accountId,
295
+ });
296
+
297
+ let result: { id: string; channel_id: string };
298
+ try {
299
+ result = (await request(
300
+ () =>
301
+ createChannelMessage<{ id: string; channel_id: string }>(rest, channelId, {
302
+ body,
303
+ }),
304
+ "components",
305
+ )) as { id: string; channel_id: string };
306
+ } catch (err) {
307
+ throw await buildDiscordSendError(err, {
308
+ channelId,
309
+ cfg,
310
+ rest,
311
+ token,
312
+ hasMedia: Boolean(opts.mediaUrl),
313
+ });
314
+ }
315
+
316
+ registerBuiltDiscordComponentMessage({
317
+ buildResult,
318
+ messageId: result.id,
319
+ });
320
+
321
+ recordChannelActivity({
322
+ channel: "discord",
323
+ accountId: accountInfo.accountId,
324
+ direction: "outbound",
325
+ });
326
+
327
+ return createDiscordSendResult({
328
+ result,
329
+ fallbackChannelId: channelId,
330
+ kind: "card",
331
+ ...(opts.replyTo ? { replyToId: opts.replyTo } : {}),
332
+ });
333
+ }
334
+
335
+ export async function editDiscordComponentMessage(
336
+ to: string,
337
+ messageId: string,
338
+ spec: DiscordComponentMessageSpec,
339
+ opts: DiscordComponentSendOpts,
340
+ ): Promise<DiscordSendResult> {
341
+ const cfg = requireRuntimeConfig(opts.cfg, "Discord component edit");
342
+ const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId });
343
+ const { token, rest, request } = createDiscordClient({ ...opts, cfg });
344
+ const recipient = await parseAndResolveRecipient(to, cfg, opts.accountId);
345
+ const { channelId } = await resolveChannelId(rest, recipient, request);
346
+ const { body, buildResult } = await buildDiscordComponentPayload({
347
+ spec,
348
+ opts,
349
+ accountId: accountInfo.accountId,
350
+ });
351
+
352
+ let result: { id: string; channel_id: string };
353
+ try {
354
+ result = (await request(
355
+ () =>
356
+ editChannelMessage(rest, channelId, messageId, {
357
+ body,
358
+ }) as Promise<{ id: string; channel_id: string }>,
359
+ "components",
360
+ )) as { id: string; channel_id: string };
361
+ } catch (err) {
362
+ throw await buildDiscordSendError(err, {
363
+ channelId,
364
+ cfg,
365
+ rest,
366
+ token,
367
+ hasMedia: Boolean(opts.mediaUrl),
368
+ });
369
+ }
370
+
371
+ registerBuiltDiscordComponentMessage({
372
+ buildResult,
373
+ messageId: result.id ?? messageId,
374
+ });
375
+
376
+ recordChannelActivity({
377
+ channel: "discord",
378
+ accountId: accountInfo.accountId,
379
+ direction: "outbound",
380
+ });
381
+
382
+ return createDiscordSendResult({
383
+ result: {
384
+ id: result.id ?? messageId,
385
+ channel_id: result.channel_id,
386
+ },
387
+ fallbackChannelId: channelId,
388
+ kind: "card",
389
+ ...(opts.replyTo ? { replyToId: opts.replyTo } : {}),
390
+ });
391
+ }
@@ -0,0 +1,57 @@
1
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
2
+ import { loadWebMediaRaw } from "autobot/plugin-sdk/web-media";
3
+ import { createGuildEmoji, createGuildSticker, listGuildEmojis } from "./internal/discord.js";
4
+ import { normalizeEmojiName, resolveDiscordRest } from "./send.shared.js";
5
+ import type { DiscordEmojiUpload, DiscordReactOpts, DiscordStickerUpload } from "./send.types.js";
6
+ import { DISCORD_MAX_EMOJI_BYTES, DISCORD_MAX_STICKER_BYTES } from "./send.types.js";
7
+
8
+ export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts) {
9
+ const rest = resolveDiscordRest(opts);
10
+ return await listGuildEmojis(rest, guildId);
11
+ }
12
+
13
+ export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts) {
14
+ const rest = resolveDiscordRest(opts);
15
+ const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES);
16
+ const contentType = normalizeOptionalLowercaseString(media.contentType);
17
+ if (
18
+ !contentType ||
19
+ !["image/png", "image/jpeg", "image/jpg", "image/gif"].includes(contentType)
20
+ ) {
21
+ throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
22
+ }
23
+ const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
24
+ const roleIds = (payload.roleIds ?? []).map((id) => id.trim()).filter(Boolean);
25
+ return await createGuildEmoji(rest, payload.guildId, {
26
+ body: {
27
+ name: normalizeEmojiName(payload.name, "Emoji name"),
28
+ image,
29
+ roles: roleIds.length ? roleIds : undefined,
30
+ },
31
+ });
32
+ }
33
+
34
+ export async function uploadStickerDiscord(payload: DiscordStickerUpload, opts: DiscordReactOpts) {
35
+ const rest = resolveDiscordRest(opts);
36
+ const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES);
37
+ const contentType = normalizeOptionalLowercaseString(media.contentType);
38
+ if (!contentType || !["image/png", "image/apng", "application/json"].includes(contentType)) {
39
+ throw new Error("Discord sticker uploads require a PNG, APNG, or Lottie JSON file");
40
+ }
41
+ return await createGuildSticker(rest, payload.guildId, {
42
+ multipartStyle: "form",
43
+ body: {
44
+ name: normalizeEmojiName(payload.name, "Sticker name"),
45
+ description: normalizeEmojiName(payload.description, "Sticker description"),
46
+ tags: normalizeEmojiName(payload.tags, "Sticker tags"),
47
+ files: [
48
+ {
49
+ data: media.buffer,
50
+ fieldName: "file",
51
+ name: media.fileName ?? "sticker",
52
+ contentType,
53
+ },
54
+ ],
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,170 @@
1
+ import type {
2
+ APIGuildMember,
3
+ APIGuildScheduledEvent,
4
+ APIRole,
5
+ APIVoiceState,
6
+ RESTPostAPIGuildScheduledEventJSONBody,
7
+ } from "discord-api-types/v10";
8
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
9
+ import { loadWebMediaRaw } from "autobot/plugin-sdk/web-media";
10
+ import {
11
+ addGuildMemberRole,
12
+ createGuildBan,
13
+ createGuildScheduledEvent,
14
+ getChannel,
15
+ getGuildMember,
16
+ getGuildVoiceState,
17
+ listGuildChannels,
18
+ listGuildRoles,
19
+ listGuildScheduledEvents,
20
+ removeGuildMember,
21
+ removeGuildMemberRole,
22
+ timeoutGuildMember,
23
+ type APIChannel,
24
+ } from "./internal/discord.js";
25
+ import { resolveDiscordRest } from "./send.shared.js";
26
+ import type {
27
+ DiscordModerationTarget,
28
+ DiscordReactOpts,
29
+ DiscordRoleChange,
30
+ DiscordTimeoutTarget,
31
+ } from "./send.types.js";
32
+ import { DISCORD_MAX_EVENT_COVER_BYTES } from "./send.types.js";
33
+
34
+ export async function fetchMemberInfoDiscord(
35
+ guildId: string,
36
+ userId: string,
37
+ opts: DiscordReactOpts,
38
+ ): Promise<APIGuildMember> {
39
+ const rest = resolveDiscordRest(opts);
40
+ return await getGuildMember(rest, guildId, userId);
41
+ }
42
+
43
+ export async function fetchRoleInfoDiscord(
44
+ guildId: string,
45
+ opts: DiscordReactOpts,
46
+ ): Promise<APIRole[]> {
47
+ const rest = resolveDiscordRest(opts);
48
+ return await listGuildRoles(rest, guildId);
49
+ }
50
+
51
+ export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) {
52
+ const rest = resolveDiscordRest(opts);
53
+ await addGuildMemberRole(rest, payload.guildId, payload.userId, payload.roleId);
54
+ return { ok: true };
55
+ }
56
+
57
+ export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts) {
58
+ const rest = resolveDiscordRest(opts);
59
+ await removeGuildMemberRole(rest, payload.guildId, payload.userId, payload.roleId);
60
+ return { ok: true };
61
+ }
62
+
63
+ export async function fetchChannelInfoDiscord(
64
+ channelId: string,
65
+ opts: DiscordReactOpts,
66
+ ): Promise<APIChannel> {
67
+ const rest = resolveDiscordRest(opts);
68
+ return await getChannel(rest, channelId);
69
+ }
70
+
71
+ export async function listGuildChannelsDiscord(
72
+ guildId: string,
73
+ opts: DiscordReactOpts,
74
+ ): Promise<APIChannel[]> {
75
+ const rest = resolveDiscordRest(opts);
76
+ return await listGuildChannels(rest, guildId);
77
+ }
78
+
79
+ export async function fetchVoiceStatusDiscord(
80
+ guildId: string,
81
+ userId: string,
82
+ opts: DiscordReactOpts,
83
+ ): Promise<APIVoiceState> {
84
+ const rest = resolveDiscordRest(opts);
85
+ return await getGuildVoiceState(rest, guildId, userId);
86
+ }
87
+
88
+ export async function listScheduledEventsDiscord(
89
+ guildId: string,
90
+ opts: DiscordReactOpts,
91
+ ): Promise<APIGuildScheduledEvent[]> {
92
+ const rest = resolveDiscordRest(opts);
93
+ return await listGuildScheduledEvents(rest, guildId);
94
+ }
95
+
96
+ const ALLOWED_EVENT_COVER_TYPES = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]);
97
+
98
+ // Loads an image from a URL or path and returns a data URI suitable for the Discord API.
99
+ export async function resolveEventCoverImage(
100
+ imageUrl: string,
101
+ opts?: { localRoots?: readonly string[] },
102
+ ): Promise<string> {
103
+ const media = await loadWebMediaRaw(imageUrl, DISCORD_MAX_EVENT_COVER_BYTES, {
104
+ localRoots: opts?.localRoots,
105
+ });
106
+ const contentType = normalizeOptionalLowercaseString(media.contentType);
107
+ if (!contentType || !ALLOWED_EVENT_COVER_TYPES.has(contentType)) {
108
+ throw new Error(
109
+ `Discord event cover images must be PNG, JPG, or GIF (got ${contentType ?? "unknown"})`,
110
+ );
111
+ }
112
+ return `data:${contentType};base64,${media.buffer.toString("base64")}`;
113
+ }
114
+
115
+ export async function createScheduledEventDiscord(
116
+ guildId: string,
117
+ payload: RESTPostAPIGuildScheduledEventJSONBody,
118
+ opts: DiscordReactOpts,
119
+ ): Promise<APIGuildScheduledEvent> {
120
+ const rest = resolveDiscordRest(opts);
121
+ return await createGuildScheduledEvent(rest, guildId, payload);
122
+ }
123
+
124
+ export async function timeoutMemberDiscord(
125
+ payload: DiscordTimeoutTarget,
126
+ opts: DiscordReactOpts,
127
+ ): Promise<APIGuildMember> {
128
+ const rest = resolveDiscordRest(opts);
129
+ let until = payload.until;
130
+ if (!until && payload.durationMinutes) {
131
+ const ms = payload.durationMinutes * 60 * 1000;
132
+ until = new Date(Date.now() + ms).toISOString();
133
+ }
134
+ return await timeoutGuildMember(rest, payload.guildId, payload.userId, {
135
+ body: { communication_disabled_until: until ?? null },
136
+ headers: payload.reason
137
+ ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
138
+ : undefined,
139
+ });
140
+ }
141
+
142
+ export async function kickMemberDiscord(payload: DiscordModerationTarget, opts: DiscordReactOpts) {
143
+ const rest = resolveDiscordRest(opts);
144
+ await removeGuildMember(rest, payload.guildId, payload.userId, {
145
+ headers: payload.reason
146
+ ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
147
+ : undefined,
148
+ });
149
+ return { ok: true };
150
+ }
151
+
152
+ export async function banMemberDiscord(
153
+ payload: DiscordModerationTarget & { deleteMessageDays?: number },
154
+ opts: DiscordReactOpts,
155
+ ) {
156
+ const rest = resolveDiscordRest(opts);
157
+ const deleteMessageDays =
158
+ typeof payload.deleteMessageDays === "number" && Number.isFinite(payload.deleteMessageDays)
159
+ ? Math.min(Math.max(Math.floor(payload.deleteMessageDays), 0), 7)
160
+ : undefined;
161
+ await createGuildBan(rest, payload.guildId, payload.userId, {
162
+ body: deleteMessageDays !== undefined ? { delete_message_days: deleteMessageDays } : undefined,
163
+ headers: payload.reason
164
+ ? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
165
+ : undefined,
166
+ });
167
+ return { ok: true };
168
+ }
169
+
170
+ // Channel management functions