@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,536 @@
1
+ import { StickerFormatType, type APIAttachment, type APIStickerItem } from "discord-api-types/v10";
2
+ import { getFileExtension } from "autobot/plugin-sdk/media-mime";
3
+ import { saveRemoteMedia, type FetchLike } from "autobot/plugin-sdk/media-runtime";
4
+ import { buildMediaPayload } from "autobot/plugin-sdk/reply-payload";
5
+ import { logVerbose } from "autobot/plugin-sdk/runtime-env";
6
+ import type { SsrFPolicy } from "autobot/plugin-sdk/ssrf-runtime";
7
+ import {
8
+ normalizeLowercaseStringOrEmpty,
9
+ normalizeOptionalString,
10
+ } from "autobot/plugin-sdk/string-coerce-runtime";
11
+ import type { Message } from "../internal/discord.js";
12
+ import {
13
+ resolveDiscordMessageSnapshots,
14
+ resolveDiscordMessageStickers,
15
+ resolveDiscordReferencedForwardMessage,
16
+ resolveDiscordReferencedReplyMessage,
17
+ resolveDiscordSnapshotStickers,
18
+ } from "./message-forwarded.js";
19
+ import { mergeAbortSignals } from "./timeouts.js";
20
+
21
+ const DISCORD_CDN_HOSTNAMES = [
22
+ "cdn.discordapp.com",
23
+ "media.discordapp.net",
24
+ "*.discordapp.com",
25
+ "*.discordapp.net",
26
+ ];
27
+
28
+ // Allow Discord CDN downloads when VPN/proxy DNS resolves to RFC2544 benchmark ranges.
29
+ const DISCORD_MEDIA_SSRF_POLICY: SsrFPolicy = {
30
+ hostnameAllowlist: DISCORD_CDN_HOSTNAMES,
31
+ allowRfc2544BenchmarkRange: true,
32
+ };
33
+
34
+ const AUDIO_ATTACHMENT_EXTENSIONS = new Set([
35
+ ".aac",
36
+ ".caf",
37
+ ".flac",
38
+ ".m4a",
39
+ ".mp3",
40
+ ".oga",
41
+ ".ogg",
42
+ ".opus",
43
+ ".wav",
44
+ ]);
45
+
46
+ const DISCORD_STICKER_ASSET_BASE_URL = "https://media.discordapp.net/stickers";
47
+
48
+ export type DiscordMediaInfo = {
49
+ path: string;
50
+ contentType?: string;
51
+ placeholder: string;
52
+ };
53
+
54
+ export type DiscordMediaResolveOptions = {
55
+ fetchImpl?: FetchLike;
56
+ ssrfPolicy?: SsrFPolicy;
57
+ readIdleTimeoutMs?: number;
58
+ totalTimeoutMs?: number;
59
+ abortSignal?: AbortSignal;
60
+ };
61
+
62
+ type DiscordStickerAssetCandidate = {
63
+ url: string;
64
+ fileName: string;
65
+ };
66
+
67
+ function isDiscordAudioAttachmentFileName(fileName?: string | null): boolean {
68
+ const ext = getFileExtension(fileName);
69
+ return Boolean(ext && AUDIO_ATTACHMENT_EXTENSIONS.has(ext));
70
+ }
71
+
72
+ function hasDiscordVoiceAttachmentFields(attachment: APIAttachment): boolean {
73
+ return typeof attachment.duration_secs === "number" || typeof attachment.waveform === "string";
74
+ }
75
+
76
+ function mergeHostnameList(...lists: Array<string[] | undefined>): string[] | undefined {
77
+ const merged = lists
78
+ .flatMap((list) => list ?? [])
79
+ .map((value) => value.trim())
80
+ .filter((value) => value.length > 0);
81
+ if (merged.length === 0) {
82
+ return undefined;
83
+ }
84
+ return Array.from(new Set(merged));
85
+ }
86
+
87
+ function resolveDiscordMediaSsrFPolicy(policy?: SsrFPolicy): SsrFPolicy {
88
+ if (!policy) {
89
+ return DISCORD_MEDIA_SSRF_POLICY;
90
+ }
91
+ const hostnameAllowlist = mergeHostnameList(
92
+ DISCORD_MEDIA_SSRF_POLICY.hostnameAllowlist,
93
+ policy.hostnameAllowlist,
94
+ );
95
+ const allowedHostnames = mergeHostnameList(
96
+ DISCORD_MEDIA_SSRF_POLICY.allowedHostnames,
97
+ policy.allowedHostnames,
98
+ );
99
+ return {
100
+ ...DISCORD_MEDIA_SSRF_POLICY,
101
+ ...policy,
102
+ ...(allowedHostnames ? { allowedHostnames } : {}),
103
+ ...(hostnameAllowlist ? { hostnameAllowlist } : {}),
104
+ allowRfc2544BenchmarkRange:
105
+ Boolean(DISCORD_MEDIA_SSRF_POLICY.allowRfc2544BenchmarkRange) ||
106
+ Boolean(policy.allowRfc2544BenchmarkRange),
107
+ };
108
+ }
109
+
110
+ export async function resolveMediaList(
111
+ message: Message,
112
+ maxBytes: number,
113
+ options?: DiscordMediaResolveOptions,
114
+ ): Promise<DiscordMediaInfo[]> {
115
+ const out: DiscordMediaInfo[] = [];
116
+ const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(options?.ssrfPolicy);
117
+ await appendResolvedMediaFromAttachments({
118
+ attachments: message.attachments ?? [],
119
+ maxBytes,
120
+ out,
121
+ errorPrefix: "discord: failed to download attachment",
122
+ fetchImpl: options?.fetchImpl,
123
+ ssrfPolicy: resolvedSsrFPolicy,
124
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
125
+ totalTimeoutMs: options?.totalTimeoutMs,
126
+ abortSignal: options?.abortSignal,
127
+ });
128
+ await appendResolvedMediaFromStickers({
129
+ stickers: resolveDiscordMessageStickers(message),
130
+ maxBytes,
131
+ out,
132
+ errorPrefix: "discord: failed to download sticker",
133
+ fetchImpl: options?.fetchImpl,
134
+ ssrfPolicy: resolvedSsrFPolicy,
135
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
136
+ totalTimeoutMs: options?.totalTimeoutMs,
137
+ abortSignal: options?.abortSignal,
138
+ });
139
+ return out;
140
+ }
141
+
142
+ export async function resolveForwardedMediaList(
143
+ message: Message,
144
+ maxBytes: number,
145
+ options?: DiscordMediaResolveOptions,
146
+ ): Promise<DiscordMediaInfo[]> {
147
+ const snapshots = resolveDiscordMessageSnapshots(message);
148
+ const out: DiscordMediaInfo[] = [];
149
+ const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(options?.ssrfPolicy);
150
+ if (snapshots.length > 0) {
151
+ for (const snapshot of snapshots) {
152
+ await appendResolvedMediaFromAttachments({
153
+ attachments: snapshot.message?.attachments,
154
+ maxBytes,
155
+ out,
156
+ errorPrefix: "discord: failed to download forwarded attachment",
157
+ fetchImpl: options?.fetchImpl,
158
+ ssrfPolicy: resolvedSsrFPolicy,
159
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
160
+ totalTimeoutMs: options?.totalTimeoutMs,
161
+ abortSignal: options?.abortSignal,
162
+ });
163
+ await appendResolvedMediaFromStickers({
164
+ stickers: snapshot.message ? resolveDiscordSnapshotStickers(snapshot.message) : [],
165
+ maxBytes,
166
+ out,
167
+ errorPrefix: "discord: failed to download forwarded sticker",
168
+ fetchImpl: options?.fetchImpl,
169
+ ssrfPolicy: resolvedSsrFPolicy,
170
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
171
+ totalTimeoutMs: options?.totalTimeoutMs,
172
+ abortSignal: options?.abortSignal,
173
+ });
174
+ }
175
+ return out;
176
+ }
177
+ const referencedForward = resolveDiscordReferencedForwardMessage(message);
178
+ if (!referencedForward) {
179
+ return out;
180
+ }
181
+ await appendResolvedMediaFromAttachments({
182
+ attachments: referencedForward.attachments,
183
+ maxBytes,
184
+ out,
185
+ errorPrefix: "discord: failed to download forwarded attachment",
186
+ fetchImpl: options?.fetchImpl,
187
+ ssrfPolicy: resolvedSsrFPolicy,
188
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
189
+ totalTimeoutMs: options?.totalTimeoutMs,
190
+ abortSignal: options?.abortSignal,
191
+ });
192
+ await appendResolvedMediaFromStickers({
193
+ stickers: resolveDiscordMessageStickers(referencedForward),
194
+ maxBytes,
195
+ out,
196
+ errorPrefix: "discord: failed to download forwarded sticker",
197
+ fetchImpl: options?.fetchImpl,
198
+ ssrfPolicy: resolvedSsrFPolicy,
199
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
200
+ totalTimeoutMs: options?.totalTimeoutMs,
201
+ abortSignal: options?.abortSignal,
202
+ });
203
+ return out;
204
+ }
205
+
206
+ export async function resolveReferencedReplyMediaList(
207
+ message: Message,
208
+ maxBytes: number,
209
+ options?: DiscordMediaResolveOptions,
210
+ ): Promise<DiscordMediaInfo[]> {
211
+ const referencedReply = resolveDiscordReferencedReplyMessage(message);
212
+ const out: DiscordMediaInfo[] = [];
213
+ if (!referencedReply) {
214
+ return out;
215
+ }
216
+ const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(options?.ssrfPolicy);
217
+ await appendResolvedMediaFromAttachments({
218
+ attachments: referencedReply.attachments,
219
+ maxBytes,
220
+ out,
221
+ errorPrefix: "discord: failed to download referenced reply attachment",
222
+ fetchImpl: options?.fetchImpl,
223
+ ssrfPolicy: resolvedSsrFPolicy,
224
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
225
+ totalTimeoutMs: options?.totalTimeoutMs,
226
+ abortSignal: options?.abortSignal,
227
+ });
228
+ await appendResolvedMediaFromStickers({
229
+ stickers: resolveDiscordMessageStickers(referencedReply),
230
+ maxBytes,
231
+ out,
232
+ errorPrefix: "discord: failed to download referenced reply sticker",
233
+ fetchImpl: options?.fetchImpl,
234
+ ssrfPolicy: resolvedSsrFPolicy,
235
+ readIdleTimeoutMs: options?.readIdleTimeoutMs,
236
+ totalTimeoutMs: options?.totalTimeoutMs,
237
+ abortSignal: options?.abortSignal,
238
+ });
239
+ return out;
240
+ }
241
+
242
+ async function fetchDiscordMedia(params: {
243
+ url: string;
244
+ filePathHint: string;
245
+ maxBytes: number;
246
+ fetchImpl?: FetchLike;
247
+ ssrfPolicy?: SsrFPolicy;
248
+ readIdleTimeoutMs?: number;
249
+ totalTimeoutMs?: number;
250
+ abortSignal?: AbortSignal;
251
+ fallbackContentType?: string;
252
+ originalFilename?: string;
253
+ }) {
254
+ const timeoutAbortController = params.totalTimeoutMs ? new AbortController() : undefined;
255
+ const signal = mergeAbortSignals([params.abortSignal, timeoutAbortController?.signal]);
256
+ let timedOut = false;
257
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
258
+
259
+ const savePromise = saveRemoteMedia({
260
+ url: params.url,
261
+ filePathHint: params.filePathHint,
262
+ maxBytes: params.maxBytes,
263
+ fetchImpl: params.fetchImpl,
264
+ ssrfPolicy: params.ssrfPolicy,
265
+ readIdleTimeoutMs: params.readIdleTimeoutMs,
266
+ fallbackContentType: params.fallbackContentType,
267
+ originalFilename: params.originalFilename,
268
+ ...(signal ? { requestInit: { signal } } : {}),
269
+ }).catch((error) => {
270
+ if (timedOut) {
271
+ return new Promise<never>(() => {});
272
+ }
273
+ throw error;
274
+ });
275
+
276
+ try {
277
+ if (!params.totalTimeoutMs) {
278
+ return await savePromise;
279
+ }
280
+ const timeoutPromise = new Promise<never>((_, reject) => {
281
+ timeoutHandle = setTimeout(() => {
282
+ timedOut = true;
283
+ timeoutAbortController?.abort();
284
+ reject(new Error(`discord media download timed out after ${params.totalTimeoutMs}ms`));
285
+ }, params.totalTimeoutMs);
286
+ timeoutHandle.unref?.();
287
+ });
288
+ return await Promise.race([savePromise, timeoutPromise]);
289
+ } finally {
290
+ if (timeoutHandle) {
291
+ clearTimeout(timeoutHandle);
292
+ }
293
+ }
294
+ }
295
+
296
+ async function appendResolvedMediaFromAttachments(params: {
297
+ attachments?: APIAttachment[] | null;
298
+ maxBytes: number;
299
+ out: DiscordMediaInfo[];
300
+ errorPrefix: string;
301
+ fetchImpl?: FetchLike;
302
+ ssrfPolicy?: SsrFPolicy;
303
+ readIdleTimeoutMs?: number;
304
+ totalTimeoutMs?: number;
305
+ abortSignal?: AbortSignal;
306
+ }) {
307
+ const attachments = params.attachments;
308
+ if (!attachments || attachments.length === 0) {
309
+ return;
310
+ }
311
+ for (const attachment of attachments) {
312
+ const attachmentUrl = normalizeOptionalString(attachment.url);
313
+ if (!attachmentUrl) {
314
+ logVerbose(
315
+ `${params.errorPrefix} ${attachment.id ?? attachment.filename ?? "attachment"}: missing url`,
316
+ );
317
+ continue;
318
+ }
319
+ try {
320
+ const saved = await fetchDiscordMedia({
321
+ url: attachmentUrl,
322
+ filePathHint: attachment.filename ?? attachmentUrl,
323
+ maxBytes: params.maxBytes,
324
+ fetchImpl: params.fetchImpl,
325
+ ssrfPolicy: params.ssrfPolicy,
326
+ readIdleTimeoutMs: params.readIdleTimeoutMs,
327
+ totalTimeoutMs: params.totalTimeoutMs,
328
+ abortSignal: params.abortSignal,
329
+ fallbackContentType: attachment.content_type,
330
+ originalFilename: attachment.filename,
331
+ });
332
+ params.out.push({
333
+ path: saved.path,
334
+ contentType: saved.contentType,
335
+ placeholder: inferPlaceholder(attachment),
336
+ });
337
+ } catch (err) {
338
+ const id = attachment.id ?? attachmentUrl;
339
+ logVerbose(`${params.errorPrefix} ${id}: ${String(err)}`);
340
+ params.out.push({
341
+ path: attachmentUrl,
342
+ contentType: attachment.content_type,
343
+ placeholder: inferPlaceholder(attachment),
344
+ });
345
+ }
346
+ }
347
+ }
348
+
349
+ function resolveStickerAssetCandidates(sticker: APIStickerItem): DiscordStickerAssetCandidate[] {
350
+ const baseName = sticker.name?.trim() || `sticker-${sticker.id}`;
351
+ switch (sticker.format_type) {
352
+ case StickerFormatType.GIF:
353
+ return [
354
+ { url: `${DISCORD_STICKER_ASSET_BASE_URL}/${sticker.id}.gif`, fileName: `${baseName}.gif` },
355
+ ];
356
+ case StickerFormatType.Lottie:
357
+ return [
358
+ {
359
+ url: `${DISCORD_STICKER_ASSET_BASE_URL}/${sticker.id}.png?size=160`,
360
+ fileName: `${baseName}.png`,
361
+ },
362
+ {
363
+ url: `${DISCORD_STICKER_ASSET_BASE_URL}/${sticker.id}.json`,
364
+ fileName: `${baseName}.json`,
365
+ },
366
+ ];
367
+ case StickerFormatType.APNG:
368
+ case StickerFormatType.PNG:
369
+ default:
370
+ return [
371
+ { url: `${DISCORD_STICKER_ASSET_BASE_URL}/${sticker.id}.png`, fileName: `${baseName}.png` },
372
+ ];
373
+ }
374
+ }
375
+
376
+ function formatStickerError(err: unknown): string {
377
+ if (err instanceof Error) {
378
+ return err.message;
379
+ }
380
+ if (typeof err === "string") {
381
+ return err;
382
+ }
383
+ try {
384
+ return JSON.stringify(err) ?? "unknown error";
385
+ } catch {
386
+ return "unknown error";
387
+ }
388
+ }
389
+
390
+ function inferStickerContentType(sticker: APIStickerItem): string | undefined {
391
+ switch (sticker.format_type) {
392
+ case StickerFormatType.GIF:
393
+ return "image/gif";
394
+ case StickerFormatType.APNG:
395
+ case StickerFormatType.Lottie:
396
+ case StickerFormatType.PNG:
397
+ return "image/png";
398
+ default:
399
+ return undefined;
400
+ }
401
+ }
402
+
403
+ async function appendResolvedMediaFromStickers(params: {
404
+ stickers?: APIStickerItem[] | null;
405
+ maxBytes: number;
406
+ out: DiscordMediaInfo[];
407
+ errorPrefix: string;
408
+ fetchImpl?: FetchLike;
409
+ ssrfPolicy?: SsrFPolicy;
410
+ readIdleTimeoutMs?: number;
411
+ totalTimeoutMs?: number;
412
+ abortSignal?: AbortSignal;
413
+ }) {
414
+ const stickers = params.stickers;
415
+ if (!stickers || stickers.length === 0) {
416
+ return;
417
+ }
418
+ for (const sticker of stickers) {
419
+ const candidates = resolveStickerAssetCandidates(sticker);
420
+ let lastError: unknown;
421
+ for (const candidate of candidates) {
422
+ try {
423
+ const saved = await fetchDiscordMedia({
424
+ url: candidate.url,
425
+ filePathHint: candidate.fileName,
426
+ maxBytes: params.maxBytes,
427
+ fetchImpl: params.fetchImpl,
428
+ ssrfPolicy: params.ssrfPolicy,
429
+ readIdleTimeoutMs: params.readIdleTimeoutMs,
430
+ totalTimeoutMs: params.totalTimeoutMs,
431
+ abortSignal: params.abortSignal,
432
+ fallbackContentType: inferStickerContentType(sticker),
433
+ originalFilename: candidate.fileName,
434
+ });
435
+ params.out.push({
436
+ path: saved.path,
437
+ contentType: saved.contentType,
438
+ placeholder: "<media:sticker>",
439
+ });
440
+ lastError = null;
441
+ break;
442
+ } catch (err) {
443
+ lastError = err;
444
+ }
445
+ }
446
+ if (lastError) {
447
+ logVerbose(`${params.errorPrefix} ${sticker.id}: ${formatStickerError(lastError)}`);
448
+ const fallback = candidates[0];
449
+ if (fallback) {
450
+ params.out.push({
451
+ path: fallback.url,
452
+ contentType: inferStickerContentType(sticker),
453
+ placeholder: "<media:sticker>",
454
+ });
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ function inferPlaceholder(attachment: APIAttachment): string {
461
+ const mime = attachment.content_type ?? "";
462
+ if (mime.startsWith("image/")) {
463
+ return "<media:image>";
464
+ }
465
+ if (mime.startsWith("video/")) {
466
+ return "<media:video>";
467
+ }
468
+ if (mime.startsWith("audio/")) {
469
+ return "<media:audio>";
470
+ }
471
+ if (hasDiscordVoiceAttachmentFields(attachment)) {
472
+ return "<media:audio>";
473
+ }
474
+ if (isDiscordAudioAttachmentFileName(attachment.filename ?? attachment.url)) {
475
+ return "<media:audio>";
476
+ }
477
+ return "<media:document>";
478
+ }
479
+
480
+ function isImageAttachment(attachment: APIAttachment): boolean {
481
+ const mime = attachment.content_type ?? "";
482
+ if (mime.startsWith("image/")) {
483
+ return true;
484
+ }
485
+ const name = normalizeLowercaseStringOrEmpty(attachment.filename);
486
+ if (!name) {
487
+ return false;
488
+ }
489
+ return /\.(avif|bmp|gif|heic|heif|jpe?g|png|tiff?|webp)$/.test(name);
490
+ }
491
+
492
+ function buildDiscordAttachmentPlaceholder(attachments?: APIAttachment[]): string {
493
+ if (!attachments || attachments.length === 0) {
494
+ return "";
495
+ }
496
+ const count = attachments.length;
497
+ const allImages = attachments.every(isImageAttachment);
498
+ const label = allImages ? "image" : "file";
499
+ const suffix = count === 1 ? label : `${label}s`;
500
+ const tag = allImages ? "<media:image>" : "<media:document>";
501
+ return `${tag} (${count} ${suffix})`;
502
+ }
503
+
504
+ function buildDiscordStickerPlaceholder(stickers?: APIStickerItem[]): string {
505
+ if (!stickers || stickers.length === 0) {
506
+ return "";
507
+ }
508
+ const count = stickers.length;
509
+ const label = count === 1 ? "sticker" : "stickers";
510
+ return `<media:sticker> (${count} ${label})`;
511
+ }
512
+
513
+ export function buildDiscordMediaPlaceholder(params: {
514
+ attachments?: APIAttachment[];
515
+ stickers?: APIStickerItem[];
516
+ }): string {
517
+ const attachmentText = buildDiscordAttachmentPlaceholder(params.attachments);
518
+ const stickerText = buildDiscordStickerPlaceholder(params.stickers);
519
+ if (attachmentText && stickerText) {
520
+ return `${attachmentText}\n${stickerText}`;
521
+ }
522
+ return attachmentText || stickerText || "";
523
+ }
524
+
525
+ export function buildDiscordMediaPayload(
526
+ mediaList: Array<{ path: string; contentType?: string }>,
527
+ ): {
528
+ MediaPath?: string;
529
+ MediaType?: string;
530
+ MediaUrl?: string;
531
+ MediaPaths?: string[];
532
+ MediaUrls?: string[];
533
+ MediaTypes?: string[];
534
+ } {
535
+ return buildMediaPayload(mediaList);
536
+ }
@@ -0,0 +1,101 @@
1
+ import { createChannelRunQueue } from "autobot/plugin-sdk/channel-lifecycle";
2
+ import type { ClaimableDedupe } from "autobot/plugin-sdk/persistent-dedupe";
3
+ import { danger } from "autobot/plugin-sdk/runtime-env";
4
+ import {
5
+ commitDiscordInboundReplay,
6
+ createDiscordInboundReplayGuard,
7
+ DiscordRetryableInboundError,
8
+ releaseDiscordInboundReplay,
9
+ } from "./inbound-dedupe.js";
10
+ import { materializeDiscordInboundJob, type DiscordInboundJob } from "./inbound-job.js";
11
+ import type { RuntimeEnv } from "./message-handler.preflight.types.js";
12
+ import type { DiscordMonitorStatusSink } from "./status.js";
13
+ import { mergeAbortSignals } from "./timeouts.js";
14
+
15
+ type ProcessDiscordMessage = typeof import("./message-handler.process.js").processDiscordMessage;
16
+
17
+ type DiscordMessageRunQueueParams = {
18
+ runtime: RuntimeEnv;
19
+ setStatus?: DiscordMonitorStatusSink;
20
+ abortSignal?: AbortSignal;
21
+ replayGuard?: ClaimableDedupe;
22
+ testing?: DiscordMessageRunQueueTestingHooks;
23
+ };
24
+
25
+ type DiscordMessageRunQueue = {
26
+ enqueue: (job: DiscordInboundJob) => void;
27
+ deactivate: () => void;
28
+ };
29
+
30
+ export type DiscordMessageRunQueueTestingHooks = {
31
+ processDiscordMessage?: ProcessDiscordMessage;
32
+ };
33
+
34
+ let messageProcessRuntimePromise:
35
+ | Promise<typeof import("./message-handler.process.js")>
36
+ | undefined;
37
+
38
+ async function loadMessageProcessRuntime() {
39
+ messageProcessRuntimePromise ??= import("./message-handler.process.js");
40
+ return await messageProcessRuntimePromise;
41
+ }
42
+
43
+ async function processDiscordQueuedMessage(params: {
44
+ job: DiscordInboundJob;
45
+ lifecycleSignal?: AbortSignal;
46
+ replayGuard: ClaimableDedupe;
47
+ testing?: DiscordMessageRunQueueTestingHooks;
48
+ }) {
49
+ const processDiscordMessageImpl =
50
+ params.testing?.processDiscordMessage ??
51
+ (await loadMessageProcessRuntime()).processDiscordMessage;
52
+ const abortSignal = mergeAbortSignals([params.job.runtime.abortSignal, params.lifecycleSignal]);
53
+ try {
54
+ await processDiscordMessageImpl(materializeDiscordInboundJob(params.job, abortSignal));
55
+ await commitDiscordInboundReplay({
56
+ replayKeys: params.job.replayKeys,
57
+ replayGuard: params.replayGuard,
58
+ });
59
+ } catch (error) {
60
+ if (error instanceof DiscordRetryableInboundError) {
61
+ releaseDiscordInboundReplay({
62
+ replayKeys: params.job.replayKeys,
63
+ error,
64
+ replayGuard: params.replayGuard,
65
+ });
66
+ } else {
67
+ await commitDiscordInboundReplay({
68
+ replayKeys: params.job.replayKeys,
69
+ replayGuard: params.replayGuard,
70
+ });
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ export function createDiscordMessageRunQueue(
77
+ params: DiscordMessageRunQueueParams,
78
+ ): DiscordMessageRunQueue {
79
+ const replayGuard = params.replayGuard ?? createDiscordInboundReplayGuard();
80
+ const runQueue = createChannelRunQueue({
81
+ setStatus: params.setStatus,
82
+ abortSignal: params.abortSignal,
83
+ onError: (error) => {
84
+ params.runtime.error?.(danger(`discord message run failed: ${String(error)}`));
85
+ },
86
+ });
87
+
88
+ return {
89
+ enqueue(job) {
90
+ runQueue.enqueue(job.queueKey, async ({ lifecycleSignal }) => {
91
+ await processDiscordQueuedMessage({
92
+ job,
93
+ lifecycleSignal,
94
+ replayGuard,
95
+ testing: params.testing,
96
+ });
97
+ });
98
+ },
99
+ deactivate: runQueue.deactivate,
100
+ };
101
+ }