@azurioh/discord-kernel 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 (373) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authz/authorizer.d.ts +45 -0
  4. package/dist/authz/authorizer.d.ts.map +1 -0
  5. package/dist/authz/authorizer.js +31 -0
  6. package/dist/authz/authorizer.js.map +1 -0
  7. package/dist/authz/require-level-guard.d.ts +23 -0
  8. package/dist/authz/require-level-guard.d.ts.map +1 -0
  9. package/dist/authz/require-level-guard.js +37 -0
  10. package/dist/authz/require-level-guard.js.map +1 -0
  11. package/dist/clock.d.ts +13 -0
  12. package/dist/clock.d.ts.map +1 -0
  13. package/dist/clock.js +13 -0
  14. package/dist/clock.js.map +1 -0
  15. package/dist/concurrency/keyed-queue.d.ts +18 -0
  16. package/dist/concurrency/keyed-queue.d.ts.map +1 -0
  17. package/dist/concurrency/keyed-queue.js +37 -0
  18. package/dist/concurrency/keyed-queue.js.map +1 -0
  19. package/dist/concurrency/run-with-concurrency.d.ts +15 -0
  20. package/dist/concurrency/run-with-concurrency.d.ts.map +1 -0
  21. package/dist/concurrency/run-with-concurrency.js +40 -0
  22. package/dist/concurrency/run-with-concurrency.js.map +1 -0
  23. package/dist/config/env.d.ts +17 -0
  24. package/dist/config/env.d.ts.map +1 -0
  25. package/dist/config/env.js +49 -0
  26. package/dist/config/env.js.map +1 -0
  27. package/dist/config/errors.d.ts +14 -0
  28. package/dist/config/errors.d.ts.map +1 -0
  29. package/dist/config/errors.js +25 -0
  30. package/dist/config/errors.js.map +1 -0
  31. package/dist/datetime.d.ts +34 -0
  32. package/dist/datetime.d.ts.map +1 -0
  33. package/dist/datetime.js +188 -0
  34. package/dist/datetime.js.map +1 -0
  35. package/dist/discord/api-errors.d.ts +20 -0
  36. package/dist/discord/api-errors.d.ts.map +1 -0
  37. package/dist/discord/api-errors.js +90 -0
  38. package/dist/discord/api-errors.js.map +1 -0
  39. package/dist/discord/channel-exporter.d.ts +42 -0
  40. package/dist/discord/channel-exporter.d.ts.map +1 -0
  41. package/dist/discord/channel-exporter.js +3 -0
  42. package/dist/discord/channel-exporter.js.map +1 -0
  43. package/dist/discord/command/autocomplete-dispatcher.d.ts +26 -0
  44. package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -0
  45. package/dist/discord/command/autocomplete-dispatcher.js +41 -0
  46. package/dist/discord/command/autocomplete-dispatcher.js.map +1 -0
  47. package/dist/discord/command/autocomplete.d.ts +18 -0
  48. package/dist/discord/command/autocomplete.d.ts.map +1 -0
  49. package/dist/discord/command/autocomplete.js +65 -0
  50. package/dist/discord/command/autocomplete.js.map +1 -0
  51. package/dist/discord/command/context.d.ts +33 -0
  52. package/dist/discord/command/context.d.ts.map +1 -0
  53. package/dist/discord/command/context.js +48 -0
  54. package/dist/discord/command/context.js.map +1 -0
  55. package/dist/discord/command/cooldown-guard.d.ts +25 -0
  56. package/dist/discord/command/cooldown-guard.d.ts.map +1 -0
  57. package/dist/discord/command/cooldown-guard.js +60 -0
  58. package/dist/discord/command/cooldown-guard.js.map +1 -0
  59. package/dist/discord/command/create-command.d.ts +100 -0
  60. package/dist/discord/command/create-command.d.ts.map +1 -0
  61. package/dist/discord/command/create-command.js +245 -0
  62. package/dist/discord/command/create-command.js.map +1 -0
  63. package/dist/discord/command/create-context-menu-command.d.ts +51 -0
  64. package/dist/discord/command/create-context-menu-command.d.ts.map +1 -0
  65. package/dist/discord/command/create-context-menu-command.js +128 -0
  66. package/dist/discord/command/create-context-menu-command.js.map +1 -0
  67. package/dist/discord/command/errors.d.ts +19 -0
  68. package/dist/discord/command/errors.d.ts.map +1 -0
  69. package/dist/discord/command/errors.js +27 -0
  70. package/dist/discord/command/errors.js.map +1 -0
  71. package/dist/discord/command/guard.d.ts +30 -0
  72. package/dist/discord/command/guard.d.ts.map +1 -0
  73. package/dist/discord/command/guard.js +34 -0
  74. package/dist/discord/command/guard.js.map +1 -0
  75. package/dist/discord/command/localization.d.ts +9 -0
  76. package/dist/discord/command/localization.d.ts.map +1 -0
  77. package/dist/discord/command/localization.js +13 -0
  78. package/dist/discord/command/localization.js.map +1 -0
  79. package/dist/discord/command/options.d.ts +113 -0
  80. package/dist/discord/command/options.d.ts.map +1 -0
  81. package/dist/discord/command/options.js +509 -0
  82. package/dist/discord/command/options.js.map +1 -0
  83. package/dist/discord/command/permission-guard.d.ts +21 -0
  84. package/dist/discord/command/permission-guard.d.ts.map +1 -0
  85. package/dist/discord/command/permission-guard.js +69 -0
  86. package/dist/discord/command/permission-guard.js.map +1 -0
  87. package/dist/discord/command/router.d.ts +48 -0
  88. package/dist/discord/command/router.d.ts.map +1 -0
  89. package/dist/discord/command/router.js +118 -0
  90. package/dist/discord/command/router.js.map +1 -0
  91. package/dist/discord/command/types.d.ts +51 -0
  92. package/dist/discord/command/types.d.ts.map +1 -0
  93. package/dist/discord/command/types.js +3 -0
  94. package/dist/discord/command/types.js.map +1 -0
  95. package/dist/discord/components/component-router.d.ts +116 -0
  96. package/dist/discord/components/component-router.d.ts.map +1 -0
  97. package/dist/discord/components/component-router.js +123 -0
  98. package/dist/discord/components/component-router.js.map +1 -0
  99. package/dist/discord/components/interactive-message/index.d.ts +3 -0
  100. package/dist/discord/components/interactive-message/index.d.ts.map +1 -0
  101. package/dist/discord/components/interactive-message/index.js +11 -0
  102. package/dist/discord/components/interactive-message/index.js.map +1 -0
  103. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +195 -0
  104. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -0
  105. package/dist/discord/components/interactive-message/interactive-message-collector.js +151 -0
  106. package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -0
  107. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +30 -0
  108. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -0
  109. package/dist/discord/components/interactive-message/mount-interactive-message.js +44 -0
  110. package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -0
  111. package/dist/discord/components/paginator/index.d.ts +5 -0
  112. package/dist/discord/components/paginator/index.d.ts.map +1 -0
  113. package/dist/discord/components/paginator/index.js +15 -0
  114. package/dist/discord/components/paginator/index.js.map +1 -0
  115. package/dist/discord/components/paginator/mount-paginator.d.ts +25 -0
  116. package/dist/discord/components/paginator/mount-paginator.d.ts.map +1 -0
  117. package/dist/discord/components/paginator/mount-paginator.js +41 -0
  118. package/dist/discord/components/paginator/mount-paginator.js.map +1 -0
  119. package/dist/discord/components/paginator/page-state.d.ts +21 -0
  120. package/dist/discord/components/paginator/page-state.d.ts.map +1 -0
  121. package/dist/discord/components/paginator/page-state.js +44 -0
  122. package/dist/discord/components/paginator/page-state.js.map +1 -0
  123. package/dist/discord/components/paginator/paginator-buttons.d.ts +17 -0
  124. package/dist/discord/components/paginator/paginator-buttons.d.ts.map +1 -0
  125. package/dist/discord/components/paginator/paginator-buttons.js +45 -0
  126. package/dist/discord/components/paginator/paginator-buttons.js.map +1 -0
  127. package/dist/discord/components/paginator/paginator-view.d.ts +26 -0
  128. package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -0
  129. package/dist/discord/components/paginator/paginator-view.js +28 -0
  130. package/dist/discord/components/paginator/paginator-view.js.map +1 -0
  131. package/dist/discord/components/persistent-paginator.d.ts +47 -0
  132. package/dist/discord/components/persistent-paginator.d.ts.map +1 -0
  133. package/dist/discord/components/persistent-paginator.js +137 -0
  134. package/dist/discord/components/persistent-paginator.js.map +1 -0
  135. package/dist/discord/components/settings-editor/index.d.ts +7 -0
  136. package/dist/discord/components/settings-editor/index.d.ts.map +1 -0
  137. package/dist/discord/components/settings-editor/index.js +22 -0
  138. package/dist/discord/components/settings-editor/index.js.map +1 -0
  139. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +140 -0
  140. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -0
  141. package/dist/discord/components/settings-editor/mount-settings-editor.js +344 -0
  142. package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -0
  143. package/dist/discord/components/settings-editor/navigation.d.ts +49 -0
  144. package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -0
  145. package/dist/discord/components/settings-editor/navigation.js +39 -0
  146. package/dist/discord/components/settings-editor/navigation.js.map +1 -0
  147. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +56 -0
  148. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -0
  149. package/dist/discord/components/settings-editor/settings-editor-card.view.js +284 -0
  150. package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -0
  151. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +69 -0
  152. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -0
  153. package/dist/discord/components/settings-editor/settings-editor-field-modal.js +353 -0
  154. package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -0
  155. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +216 -0
  156. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -0
  157. package/dist/discord/components/settings-editor/settings-editor-fields.js +66 -0
  158. package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -0
  159. package/dist/discord/components/settings-editor/settings-editor.view.d.ts +303 -0
  160. package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -0
  161. package/dist/discord/components/settings-editor/settings-editor.view.js +316 -0
  162. package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -0
  163. package/dist/discord/events/create-event.d.ts +5 -0
  164. package/dist/discord/events/create-event.d.ts.map +1 -0
  165. package/dist/discord/events/create-event.js +8 -0
  166. package/dist/discord/events/create-event.js.map +1 -0
  167. package/dist/discord/events/event-router.d.ts +16 -0
  168. package/dist/discord/events/event-router.d.ts.map +1 -0
  169. package/dist/discord/events/event-router.js +51 -0
  170. package/dist/discord/events/event-router.js.map +1 -0
  171. package/dist/discord/events/types.d.ts +16 -0
  172. package/dist/discord/events/types.d.ts.map +1 -0
  173. package/dist/discord/events/types.js +3 -0
  174. package/dist/discord/events/types.js.map +1 -0
  175. package/dist/discord/i18n.d.ts +58 -0
  176. package/dist/discord/i18n.d.ts.map +1 -0
  177. package/dist/discord/i18n.js +173 -0
  178. package/dist/discord/i18n.js.map +1 -0
  179. package/dist/discord/interaction/button.d.ts +39 -0
  180. package/dist/discord/interaction/button.d.ts.map +1 -0
  181. package/dist/discord/interaction/button.js +36 -0
  182. package/dist/discord/interaction/button.js.map +1 -0
  183. package/dist/discord/interaction/interaction-router.d.ts +21 -0
  184. package/dist/discord/interaction/interaction-router.d.ts.map +1 -0
  185. package/dist/discord/interaction/interaction-router.js +24 -0
  186. package/dist/discord/interaction/interaction-router.js.map +1 -0
  187. package/dist/discord/interaction/modal.d.ts +171 -0
  188. package/dist/discord/interaction/modal.d.ts.map +1 -0
  189. package/dist/discord/interaction/modal.js +264 -0
  190. package/dist/discord/interaction/modal.js.map +1 -0
  191. package/dist/discord/interaction/select-menu.d.ts +104 -0
  192. package/dist/discord/interaction/select-menu.d.ts.map +1 -0
  193. package/dist/discord/interaction/select-menu.js +126 -0
  194. package/dist/discord/interaction/select-menu.js.map +1 -0
  195. package/dist/discord/permissions.d.ts +32 -0
  196. package/dist/discord/permissions.d.ts.map +1 -0
  197. package/dist/discord/permissions.js +40 -0
  198. package/dist/discord/permissions.js.map +1 -0
  199. package/dist/discord/placeholder-registry-errors.d.ts +10 -0
  200. package/dist/discord/placeholder-registry-errors.d.ts.map +1 -0
  201. package/dist/discord/placeholder-registry-errors.js +18 -0
  202. package/dist/discord/placeholder-registry-errors.js.map +1 -0
  203. package/dist/discord/placeholder-registry.d.ts +53 -0
  204. package/dist/discord/placeholder-registry.d.ts.map +1 -0
  205. package/dist/discord/placeholder-registry.js +52 -0
  206. package/dist/discord/placeholder-registry.js.map +1 -0
  207. package/dist/discord/presenter.d.ts +26 -0
  208. package/dist/discord/presenter.d.ts.map +1 -0
  209. package/dist/discord/presenter.js +3 -0
  210. package/dist/discord/presenter.js.map +1 -0
  211. package/dist/discord/ui/card.d.ts +114 -0
  212. package/dist/discord/ui/card.d.ts.map +1 -0
  213. package/dist/discord/ui/card.js +162 -0
  214. package/dist/discord/ui/card.js.map +1 -0
  215. package/dist/discord/ui/color-input-errors.d.ts +12 -0
  216. package/dist/discord/ui/color-input-errors.d.ts.map +1 -0
  217. package/dist/discord/ui/color-input-errors.js +21 -0
  218. package/dist/discord/ui/color-input-errors.js.map +1 -0
  219. package/dist/discord/ui/color-input.d.ts +37 -0
  220. package/dist/discord/ui/color-input.d.ts.map +1 -0
  221. package/dist/discord/ui/color-input.js +166 -0
  222. package/dist/discord/ui/color-input.js.map +1 -0
  223. package/dist/discord/ui/colors.d.ts +44 -0
  224. package/dist/discord/ui/colors.d.ts.map +1 -0
  225. package/dist/discord/ui/colors.js +45 -0
  226. package/dist/discord/ui/colors.js.map +1 -0
  227. package/dist/discord/ui/embed.d.ts +16 -0
  228. package/dist/discord/ui/embed.d.ts.map +1 -0
  229. package/dist/discord/ui/embed.js +53 -0
  230. package/dist/discord/ui/embed.js.map +1 -0
  231. package/dist/discord/ui/message-attachments.d.ts +40 -0
  232. package/dist/discord/ui/message-attachments.d.ts.map +1 -0
  233. package/dist/discord/ui/message-attachments.js +27 -0
  234. package/dist/discord/ui/message-attachments.js.map +1 -0
  235. package/dist/errors/business-error.d.ts +41 -0
  236. package/dist/errors/business-error.d.ts.map +1 -0
  237. package/dist/errors/business-error.js +39 -0
  238. package/dist/errors/business-error.js.map +1 -0
  239. package/dist/errors/describe-error.d.ts +29 -0
  240. package/dist/errors/describe-error.d.ts.map +1 -0
  241. package/dist/errors/describe-error.js +49 -0
  242. package/dist/errors/describe-error.js.map +1 -0
  243. package/dist/errors/incident-ref.d.ts +7 -0
  244. package/dist/errors/incident-ref.d.ts.map +1 -0
  245. package/dist/errors/incident-ref.js +13 -0
  246. package/dist/errors/incident-ref.js.map +1 -0
  247. package/dist/errors/index.d.ts +4 -0
  248. package/dist/errors/index.d.ts.map +1 -0
  249. package/dist/errors/index.js +15 -0
  250. package/dist/errors/index.js.map +1 -0
  251. package/dist/i18n/business-message.d.ts +14 -0
  252. package/dist/i18n/business-message.d.ts.map +1 -0
  253. package/dist/i18n/business-message.js +19 -0
  254. package/dist/i18n/business-message.js.map +1 -0
  255. package/dist/i18n/catalog.d.ts +27 -0
  256. package/dist/i18n/catalog.d.ts.map +1 -0
  257. package/dist/i18n/catalog.js +25 -0
  258. package/dist/i18n/catalog.js.map +1 -0
  259. package/dist/i18n/errors.d.ts +10 -0
  260. package/dist/i18n/errors.d.ts.map +1 -0
  261. package/dist/i18n/errors.js +18 -0
  262. package/dist/i18n/errors.js.map +1 -0
  263. package/dist/i18n/index.d.ts +6 -0
  264. package/dist/i18n/index.d.ts.map +1 -0
  265. package/dist/i18n/index.js +16 -0
  266. package/dist/i18n/index.js.map +1 -0
  267. package/dist/i18n/locale.d.ts +22 -0
  268. package/dist/i18n/locale.d.ts.map +1 -0
  269. package/dist/i18n/locale.js +40 -0
  270. package/dist/i18n/locale.js.map +1 -0
  271. package/dist/i18n/translator.d.ts +35 -0
  272. package/dist/i18n/translator.d.ts.map +1 -0
  273. package/dist/i18n/translator.js +40 -0
  274. package/dist/i18n/translator.js.map +1 -0
  275. package/dist/logger.d.ts +28 -0
  276. package/dist/logger.d.ts.map +1 -0
  277. package/dist/logger.js +3 -0
  278. package/dist/logger.js.map +1 -0
  279. package/dist/module/module.d.ts +42 -0
  280. package/dist/module/module.d.ts.map +1 -0
  281. package/dist/module/module.js +3 -0
  282. package/dist/module/module.js.map +1 -0
  283. package/dist/persistence/database.d.ts +24 -0
  284. package/dist/persistence/database.d.ts.map +1 -0
  285. package/dist/persistence/database.js +3 -0
  286. package/dist/persistence/database.js.map +1 -0
  287. package/dist/resilience/retry.d.ts +36 -0
  288. package/dist/resilience/retry.d.ts.map +1 -0
  289. package/dist/resilience/retry.js +78 -0
  290. package/dist/resilience/retry.js.map +1 -0
  291. package/dist/scheduler/errors.d.ts +25 -0
  292. package/dist/scheduler/errors.d.ts.map +1 -0
  293. package/dist/scheduler/errors.js +45 -0
  294. package/dist/scheduler/errors.js.map +1 -0
  295. package/dist/scheduler/scheduler.d.ts +68 -0
  296. package/dist/scheduler/scheduler.d.ts.map +1 -0
  297. package/dist/scheduler/scheduler.js +162 -0
  298. package/dist/scheduler/scheduler.js.map +1 -0
  299. package/package.json +89 -0
  300. package/src/authz/authorizer.ts +76 -0
  301. package/src/authz/require-level-guard.ts +42 -0
  302. package/src/clock.ts +18 -0
  303. package/src/concurrency/keyed-queue.ts +50 -0
  304. package/src/concurrency/run-with-concurrency.ts +44 -0
  305. package/src/config/env.ts +58 -0
  306. package/src/config/errors.ts +21 -0
  307. package/src/datetime.ts +221 -0
  308. package/src/discord/api-errors.ts +91 -0
  309. package/src/discord/channel-exporter.ts +48 -0
  310. package/src/discord/command/autocomplete-dispatcher.ts +54 -0
  311. package/src/discord/command/autocomplete.ts +76 -0
  312. package/src/discord/command/context.ts +85 -0
  313. package/src/discord/command/cooldown-guard.ts +81 -0
  314. package/src/discord/command/create-command.ts +431 -0
  315. package/src/discord/command/create-context-menu-command.ts +227 -0
  316. package/src/discord/command/errors.ts +22 -0
  317. package/src/discord/command/guard.ts +54 -0
  318. package/src/discord/command/localization.ts +11 -0
  319. package/src/discord/command/options.ts +847 -0
  320. package/src/discord/command/permission-guard.ts +74 -0
  321. package/src/discord/command/router.ts +142 -0
  322. package/src/discord/command/types.ts +52 -0
  323. package/src/discord/components/component-router.ts +213 -0
  324. package/src/discord/components/interactive-message/index.ts +18 -0
  325. package/src/discord/components/interactive-message/interactive-message-collector.ts +374 -0
  326. package/src/discord/components/interactive-message/mount-interactive-message.ts +71 -0
  327. package/src/discord/components/paginator/index.ts +17 -0
  328. package/src/discord/components/paginator/mount-paginator.ts +69 -0
  329. package/src/discord/components/paginator/page-state.ts +45 -0
  330. package/src/discord/components/paginator/paginator-buttons.ts +60 -0
  331. package/src/discord/components/paginator/paginator-view.ts +61 -0
  332. package/src/discord/components/persistent-paginator.ts +197 -0
  333. package/src/discord/components/settings-editor/index.ts +41 -0
  334. package/src/discord/components/settings-editor/mount-settings-editor.ts +646 -0
  335. package/src/discord/components/settings-editor/navigation.ts +67 -0
  336. package/src/discord/components/settings-editor/settings-editor-card.view.ts +344 -0
  337. package/src/discord/components/settings-editor/settings-editor-field-modal.ts +538 -0
  338. package/src/discord/components/settings-editor/settings-editor-fields.ts +290 -0
  339. package/src/discord/components/settings-editor/settings-editor.view.ts +654 -0
  340. package/src/discord/events/create-event.ts +7 -0
  341. package/src/discord/events/event-router.ts +53 -0
  342. package/src/discord/events/types.ts +17 -0
  343. package/src/discord/i18n.ts +173 -0
  344. package/src/discord/interaction/button.ts +75 -0
  345. package/src/discord/interaction/interaction-router.ts +28 -0
  346. package/src/discord/interaction/modal.ts +505 -0
  347. package/src/discord/interaction/select-menu.ts +246 -0
  348. package/src/discord/permissions.ts +57 -0
  349. package/src/discord/placeholder-registry-errors.ts +11 -0
  350. package/src/discord/placeholder-registry.ts +77 -0
  351. package/src/discord/presenter.ts +26 -0
  352. package/src/discord/ui/card.ts +303 -0
  353. package/src/discord/ui/color-input-errors.ts +17 -0
  354. package/src/discord/ui/color-input.ts +179 -0
  355. package/src/discord/ui/colors.ts +58 -0
  356. package/src/discord/ui/embed.ts +64 -0
  357. package/src/discord/ui/message-attachments.ts +57 -0
  358. package/src/errors/business-error.ts +49 -0
  359. package/src/errors/describe-error.ts +73 -0
  360. package/src/errors/incident-ref.ts +10 -0
  361. package/src/errors/index.ts +12 -0
  362. package/src/i18n/business-message.ts +23 -0
  363. package/src/i18n/catalog.ts +41 -0
  364. package/src/i18n/errors.ts +11 -0
  365. package/src/i18n/index.ts +11 -0
  366. package/src/i18n/locale.ts +41 -0
  367. package/src/i18n/translator.ts +78 -0
  368. package/src/logger.ts +28 -0
  369. package/src/module/module.ts +42 -0
  370. package/src/persistence/database.ts +23 -0
  371. package/src/resilience/retry.ts +102 -0
  372. package/src/scheduler/errors.ts +40 -0
  373. package/src/scheduler/scheduler.ts +248 -0
@@ -0,0 +1,431 @@
1
+ import {
2
+ type ChatInputCommandInteraction,
3
+ InteractionContextType,
4
+ type LocalizationMap,
5
+ MessageFlags,
6
+ SlashCommandBuilder,
7
+ type SlashCommandSubcommandBuilder,
8
+ } from "discord.js";
9
+ import { resolveRouteOptions, respondWithSuggestions } from "@/discord/command/autocomplete";
10
+ import { type Context, createContext, sendEmbed } from "@/discord/command/context";
11
+ import type { Guard } from "@/discord/command/guard";
12
+ import type { Options, Values } from "@/discord/command/options";
13
+ import type { CommandRuntime, SlashCommand } from "@/discord/command/types";
14
+ import { CORE_MESSAGES } from "@/discord/i18n";
15
+ import { type PermissionBit, resolvePermissions } from "@/discord/permissions";
16
+ import { BusinessError, createIncidentRef } from "@/errors";
17
+ import { type Locale, type LocalizedText, resolveLocale } from "@/i18n";
18
+
19
+ export interface SubcommandDef<O extends Options> {
20
+ description: string;
21
+ /** Translated names, keyed by Discord locale (e.g. `{ fr: "ajouter" }`). */
22
+ nameLocalizations?: LocalizationMap;
23
+ /** Translated descriptions, keyed by Discord locale. */
24
+ descriptionLocalizations?: LocalizationMap;
25
+ options?: O;
26
+ guard?: Guard;
27
+ /** Fallback message shown for unexpected (non-business) errors. */
28
+ fallbackError?: LocalizedText;
29
+ /** Defer the reply before running the handler (for work that may exceed 3s). */
30
+ defer?: boolean;
31
+ /** Whether replies (and the defer) are ephemeral. Defaults to true. */
32
+ ephemeral?: boolean;
33
+ handler: (ctx: Context<O>) => Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * Type-erased subcommand. `createSubCommand()` captures the per-subcommand option
38
+ * generic `O` inside `invoke`, so a single command can hold subcommands with
39
+ * differing option shapes in one record without leaking `any`.
40
+ */
41
+ export interface CompiledSubcommand {
42
+ description: string;
43
+ nameLocalizations?: LocalizationMap;
44
+ descriptionLocalizations?: LocalizationMap;
45
+ guard?: Guard;
46
+ fallbackError?: LocalizedText;
47
+ defer: boolean;
48
+ ephemeral: boolean;
49
+ options: Options;
50
+ invoke(ctx: Context<Options>): Promise<void>;
51
+ }
52
+
53
+ export function createSubCommand<O extends Options>(def: SubcommandDef<O>): CompiledSubcommand {
54
+ return {
55
+ description: def.description,
56
+ nameLocalizations: def.nameLocalizations,
57
+ descriptionLocalizations: def.descriptionLocalizations,
58
+ guard: def.guard,
59
+ fallbackError: def.fallbackError,
60
+ defer: def.defer ?? false,
61
+ ephemeral: def.ephemeral ?? true,
62
+ options: def.options ?? {},
63
+ invoke: (ctx) => def.handler(ctx as Context<O>),
64
+ };
65
+ }
66
+
67
+ interface GroupDef {
68
+ description: string;
69
+ nameLocalizations?: LocalizationMap;
70
+ descriptionLocalizations?: LocalizationMap;
71
+ subcommands: Record<string, CompiledSubcommand>;
72
+ }
73
+
74
+ /**
75
+ * A command is either *flat* (a single `handler`, like `/ping`) or *grouped*
76
+ * (`subcommands`/`groups`, like `/notes add`). Provide `handler` for the
77
+ * former; `handler` takes precedence over `subcommands`/`groups` if both are set.
78
+ */
79
+ export interface CommandDef<O extends Options = Options> {
80
+ name: string;
81
+ description: string;
82
+ /** Translated names, keyed by Discord locale (e.g. `{ fr: "notes" }`). */
83
+ nameLocalizations?: LocalizationMap;
84
+ /** Translated descriptions, keyed by Discord locale. */
85
+ descriptionLocalizations?: LocalizationMap;
86
+ guard?: Guard;
87
+ /**
88
+ * Permissions a member must hold for Discord to *show* the command. This is a
89
+ * display filter, never a security boundary: a server admin can override it in
90
+ * the Integrations UI, so always pair it with a guard (see `permission-guard.ts`).
91
+ * Only meaningful on the root command — Discord has no per-subcommand equivalent.
92
+ */
93
+ defaultMemberPermissions?: readonly PermissionBit[];
94
+ /** Restrict the command to guild contexts (no DM, no user-app install). */
95
+ guildOnly?: boolean;
96
+ /** Restrict deployment to specific guild(s); omit to deploy globally (all servers). */
97
+ guildIds?: readonly string[];
98
+ /** Flat command: typed options for the single handler. */
99
+ options?: O;
100
+ /** Flat command: defer the reply before running the handler. */
101
+ defer?: boolean;
102
+ /** Flat command: whether replies (and the defer) are ephemeral. Defaults to true. */
103
+ ephemeral?: boolean;
104
+ /** Flat command: fallback message for unexpected errors. */
105
+ fallbackError?: LocalizedText;
106
+ /** Flat command: the single handler invoked when the command runs. */
107
+ handler?: (ctx: Context<O>) => Promise<void>;
108
+ /** Grouped command: named subcommands. */
109
+ subcommands?: Record<string, CompiledSubcommand>;
110
+ /** Grouped command: named subcommand groups. */
111
+ groups?: Record<string, GroupDef>;
112
+ }
113
+
114
+ interface SubcommandContainer {
115
+ addSubcommand(
116
+ input: (sub: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder,
117
+ ): unknown;
118
+ }
119
+
120
+ /** Resolve the reply locale for an interaction handled outside `createContext`. */
121
+ function interactionLocale(
122
+ interaction: ChatInputCommandInteraction,
123
+ runtime: CommandRuntime,
124
+ ): Locale {
125
+ return resolveLocale(
126
+ [interaction.locale, interaction.guildLocale],
127
+ runtime.translator.defaultLocale,
128
+ );
129
+ }
130
+
131
+ /**
132
+ * The key a subcommand is stored and looked up under. Exported because dispatch
133
+ * and autocomplete must agree on it: if they ever built it differently, an
134
+ * autocomplete inside a group would silently resolve to no options instead of
135
+ * failing loudly.
136
+ */
137
+ export function routeKey(group: string | null, name: string): string {
138
+ return group ? `${group}/${name}` : name;
139
+ }
140
+
141
+ /**
142
+ * The root-level settings, split out of {@link CommandDef} so applying them does
143
+ * not drag the option generic `O` along (a `CommandDef<O>` is not assignable to
144
+ * a `CommandDef<Options>` — its handler is contravariant in the options).
145
+ */
146
+ interface RootSettings {
147
+ nameLocalizations?: LocalizationMap;
148
+ descriptionLocalizations?: LocalizationMap;
149
+ defaultMemberPermissions?: readonly PermissionBit[];
150
+ guildOnly?: boolean;
151
+ }
152
+
153
+ /**
154
+ * Apply the settings that only exist on the root command: localisations, the
155
+ * visibility filter and the context restriction.
156
+ *
157
+ * `defaultMemberPermissions` is resolved from flags into the single bitfield the
158
+ * API expects. An empty array is meaningful — it means "administrators only" to
159
+ * Discord — so it is passed through rather than skipped.
160
+ */
161
+ function applyRootSettings(builder: SlashCommandBuilder, def: RootSettings): void {
162
+ if (def.nameLocalizations) {
163
+ builder.setNameLocalizations(def.nameLocalizations);
164
+ }
165
+ if (def.descriptionLocalizations) {
166
+ builder.setDescriptionLocalizations(def.descriptionLocalizations);
167
+ }
168
+ if (def.defaultMemberPermissions) {
169
+ builder.setDefaultMemberPermissions(resolvePermissions(def.defaultMemberPermissions));
170
+ }
171
+ if (def.guildOnly) {
172
+ builder.setContexts(InteractionContextType.Guild);
173
+ }
174
+ }
175
+
176
+ function applySubcommand(
177
+ container: SubcommandContainer,
178
+ name: string,
179
+ def: CompiledSubcommand,
180
+ ): void {
181
+ container.addSubcommand((subBuilder) => {
182
+ subBuilder.setName(name).setDescription(def.description);
183
+ if (def.nameLocalizations) {
184
+ subBuilder.setNameLocalizations(def.nameLocalizations);
185
+ }
186
+ if (def.descriptionLocalizations) {
187
+ subBuilder.setDescriptionLocalizations(def.descriptionLocalizations);
188
+ }
189
+ for (const [optionName, option] of Object.entries(def.options)) {
190
+ option.apply(subBuilder, optionName);
191
+ }
192
+ return subBuilder;
193
+ });
194
+ }
195
+
196
+ function readOptions(
197
+ options: Options,
198
+ interaction: ChatInputCommandInteraction,
199
+ ): Record<string, unknown> {
200
+ const values: Record<string, unknown> = {};
201
+ for (const [name, option] of Object.entries(options)) {
202
+ values[name] = option.read(interaction, name);
203
+ }
204
+ return values;
205
+ }
206
+
207
+ function ephemeralReplyOptions(ephemeral: boolean): { flags?: MessageFlags.Ephemeral } {
208
+ if (ephemeral) {
209
+ return { flags: MessageFlags.Ephemeral };
210
+ }
211
+ return {};
212
+ }
213
+
214
+ /** Run a guard if present; reply with a denial and return `false` when it fails. */
215
+ async function passesGuard(
216
+ interaction: ChatInputCommandInteraction,
217
+ guard: Guard | undefined,
218
+ runtime: CommandRuntime,
219
+ ): Promise<boolean> {
220
+ if (!guard) {
221
+ return true;
222
+ }
223
+ const result = await guard.check(interaction);
224
+ if (!result.ok) {
225
+ const locale = interactionLocale(interaction, runtime);
226
+ await interaction.reply({
227
+ embeds: [
228
+ runtime.presenter.denial(runtime.translator.resolve(locale, result.message), locale),
229
+ ],
230
+ flags: MessageFlags.Ephemeral,
231
+ });
232
+ return false;
233
+ }
234
+ return true;
235
+ }
236
+
237
+ /** Read options, optionally defer, build the context and invoke the handler. */
238
+ async function executeRoute(
239
+ interaction: ChatInputCommandInteraction,
240
+ route: CompiledSubcommand,
241
+ label: string,
242
+ runtime: CommandRuntime,
243
+ ): Promise<void> {
244
+ const { presenter, logger, translator } = runtime;
245
+ try {
246
+ const values = readOptions(route.options, interaction);
247
+ if (route.defer) {
248
+ await interaction.deferReply(ephemeralReplyOptions(route.ephemeral));
249
+ }
250
+ const ctx = createContext(
251
+ interaction,
252
+ values as Values<Options>,
253
+ route.ephemeral,
254
+ presenter,
255
+ logger,
256
+ translator,
257
+ );
258
+ await route.invoke(ctx);
259
+ } catch (error) {
260
+ await renderError(interaction, route, label, error, runtime);
261
+ }
262
+ }
263
+
264
+ async function dispatchFlat(
265
+ interaction: ChatInputCommandInteraction,
266
+ root: CompiledSubcommand,
267
+ runtime: CommandRuntime,
268
+ ): Promise<void> {
269
+ if (!(await passesGuard(interaction, root.guard, runtime))) {
270
+ return;
271
+ }
272
+ await executeRoute(interaction, root, interaction.commandName, runtime);
273
+ }
274
+
275
+ async function dispatchGrouped(
276
+ interaction: ChatInputCommandInteraction,
277
+ routes: Map<string, CompiledSubcommand>,
278
+ commandGuard: Guard | undefined,
279
+ runtime: CommandRuntime,
280
+ ): Promise<void> {
281
+ const group = interaction.options.getSubcommandGroup(false);
282
+ const name = interaction.options.getSubcommand();
283
+ const label = routeKey(group, name);
284
+ const route = routes.get(label);
285
+
286
+ if (!route) {
287
+ const locale = interactionLocale(interaction, runtime);
288
+ await interaction.reply({
289
+ embeds: [
290
+ runtime.presenter.error(
291
+ runtime.translator.translate(locale, CORE_MESSAGES.unknownSubcommand),
292
+ locale,
293
+ ),
294
+ ],
295
+ flags: MessageFlags.Ephemeral,
296
+ });
297
+ return;
298
+ }
299
+
300
+ if (!(await passesGuard(interaction, route.guard ?? commandGuard, runtime))) {
301
+ return;
302
+ }
303
+ await executeRoute(interaction, route, label, runtime);
304
+ }
305
+
306
+ async function renderError(
307
+ interaction: ChatInputCommandInteraction,
308
+ route: CompiledSubcommand,
309
+ label: string,
310
+ error: unknown,
311
+ runtime: CommandRuntime,
312
+ ): Promise<void> {
313
+ const { presenter, logger, translator } = runtime;
314
+ const locale = interactionLocale(interaction, runtime);
315
+ if (error instanceof BusinessError) {
316
+ const text = error.translation
317
+ ? translator.translate(locale, error.translation.key, error.translation.params)
318
+ : error.message;
319
+ const embed =
320
+ error.severity === "warning"
321
+ ? presenter.warning(text, locale)
322
+ : presenter.error(text, locale);
323
+ await sendEmbed(interaction, embed, route.ephemeral, logger);
324
+ return;
325
+ }
326
+
327
+ const ref = createIncidentRef();
328
+ logger.error(
329
+ {
330
+ command: interaction.commandName,
331
+ subcommand: label,
332
+ userId: interaction.user.id,
333
+ ref,
334
+ err: error instanceof Error ? error.message : String(error),
335
+ stack: error instanceof Error ? error.stack : undefined,
336
+ },
337
+ "Command handler failed",
338
+ );
339
+ await sendEmbed(
340
+ interaction,
341
+ presenter.systemError(
342
+ translator.resolve(locale, route.fallbackError ?? { key: CORE_MESSAGES.genericError }),
343
+ ref,
344
+ locale,
345
+ ),
346
+ route.ephemeral,
347
+ logger,
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Build a {@link SlashCommand} from a declarative definition: it produces the
353
+ * discord.js builder *and* the routing/guard/error pipeline. The runtime
354
+ * (presenter, logger) is injected per-dispatch by the command router.
355
+ */
356
+ export function createCommand<O extends Options = Options>(def: CommandDef<O>): SlashCommand {
357
+ const builder = new SlashCommandBuilder().setName(def.name).setDescription(def.description);
358
+
359
+ // Configured before the flat/grouped branching below: both return paths share
360
+ // this builder, so root-level settings must not be duplicated per branch.
361
+ applyRootSettings(builder, def);
362
+
363
+ if (def.handler) {
364
+ const handler = def.handler;
365
+ const root: CompiledSubcommand = {
366
+ description: def.description,
367
+ guard: def.guard,
368
+ fallbackError: def.fallbackError,
369
+ defer: def.defer ?? false,
370
+ ephemeral: def.ephemeral ?? true,
371
+ options: def.options ?? {},
372
+ invoke: (ctx) => handler(ctx as Context<O>),
373
+ };
374
+ for (const [optionName, option] of Object.entries(root.options)) {
375
+ option.apply(builder, optionName);
376
+ }
377
+ return {
378
+ data: builder,
379
+ guildIds: def.guildIds,
380
+ dispatch: (interaction, runtime) => dispatchFlat(interaction, root, runtime),
381
+ autocomplete: hasAutocomplete(root.options)
382
+ ? (interaction) => respondWithSuggestions(interaction, root.options)
383
+ : undefined,
384
+ };
385
+ }
386
+
387
+ const routes = new Map<string, CompiledSubcommand>();
388
+
389
+ if (def.subcommands) {
390
+ for (const [name, subDef] of Object.entries(def.subcommands)) {
391
+ applySubcommand(builder, name, subDef);
392
+ routes.set(name, subDef);
393
+ }
394
+ }
395
+
396
+ if (def.groups) {
397
+ for (const [groupName, group] of Object.entries(def.groups)) {
398
+ builder.addSubcommandGroup((groupBuilder) => {
399
+ groupBuilder.setName(groupName).setDescription(group.description);
400
+ if (group.nameLocalizations) {
401
+ groupBuilder.setNameLocalizations(group.nameLocalizations);
402
+ }
403
+ if (group.descriptionLocalizations) {
404
+ groupBuilder.setDescriptionLocalizations(group.descriptionLocalizations);
405
+ }
406
+ for (const [name, subDef] of Object.entries(group.subcommands)) {
407
+ applySubcommand(groupBuilder, name, subDef);
408
+ }
409
+ return groupBuilder;
410
+ });
411
+ for (const [name, subDef] of Object.entries(group.subcommands)) {
412
+ routes.set(routeKey(groupName, name), subDef);
413
+ }
414
+ }
415
+ }
416
+
417
+ return {
418
+ data: builder,
419
+ guildIds: def.guildIds,
420
+ dispatch: (interaction, runtime) => dispatchGrouped(interaction, routes, def.guard, runtime),
421
+ autocomplete: [...routes.values()].some((route) => hasAutocomplete(route.options))
422
+ ? (interaction) =>
423
+ respondWithSuggestions(interaction, resolveRouteOptions(interaction, routes, null))
424
+ : undefined,
425
+ };
426
+ }
427
+
428
+ /** Whether any option of a route declares a resolver, so the seam stays absent otherwise. */
429
+ function hasAutocomplete(options: Options): boolean {
430
+ return Object.values(options).some((option) => option.autocomplete !== undefined);
431
+ }
@@ -0,0 +1,227 @@
1
+ import {
2
+ ApplicationCommandType,
3
+ ContextMenuCommandBuilder,
4
+ type ContextMenuCommandInteraction,
5
+ type EmbedBuilder,
6
+ InteractionContextType,
7
+ type LocalizationMap,
8
+ type Message,
9
+ MessageFlags,
10
+ type User,
11
+ } from "discord.js";
12
+ import { ContextMenuTargetError } from "@/discord/command/errors";
13
+ import type { Guard } from "@/discord/command/guard";
14
+ import type { CommandRuntime, ContextMenuCommand } from "@/discord/command/types";
15
+ import { CORE_MESSAGES } from "@/discord/i18n";
16
+ import { type PermissionBit, resolvePermissions } from "@/discord/permissions";
17
+ import { BusinessError, createIncidentRef } from "@/errors";
18
+ import { type Locale, type LocalizedText, resolveLocale, type TranslationParams } from "@/i18n";
19
+ import type { Logger } from "@/logger";
20
+
21
+ /**
22
+ * Context-menu commands: the entries a user reaches by right-clicking a member
23
+ * ("user" target) or a message ("message" target), under the Apps submenu.
24
+ *
25
+ * They are declared here rather than through `createCommand` because they are a
26
+ * different application command type: no name is typed, no options are parsed —
27
+ * the interaction carries the clicked target instead. Everything else (guards,
28
+ * permissions, error rendering) mirrors the slash-command pipeline so a module
29
+ * writes both the same way.
30
+ */
31
+
32
+ /** The target handed to the handler, discriminated by the command's kind. */
33
+ export interface ContextMenuTarget {
34
+ readonly user: User;
35
+ readonly message: Message;
36
+ }
37
+
38
+ export interface ContextMenuContext<K extends keyof ContextMenuTarget> {
39
+ readonly interaction: ContextMenuCommandInteraction;
40
+ /** The right-clicked member or message. */
41
+ readonly target: ContextMenuTarget[K];
42
+ /** Reply language: user's client locale → guild locale → configured default. */
43
+ readonly locale: Locale;
44
+ /** Translate a catalog key for this interaction's locale. */
45
+ t(key: string, params?: TranslationParams): string;
46
+ confirm(message: string): Promise<void>;
47
+ error(message: string): Promise<void>;
48
+ deny(message: string): Promise<void>;
49
+ }
50
+
51
+ export interface ContextMenuCommandDef<K extends keyof ContextMenuTarget> {
52
+ /** Displayed verbatim in the context menu, so spaces and capitals are allowed. */
53
+ name: string;
54
+ /** Which right-click surface the entry appears on. */
55
+ target: K;
56
+ nameLocalizations?: LocalizationMap;
57
+ guard?: Guard;
58
+ /** Display filter — pair it with a guard, it is not a security boundary. */
59
+ defaultMemberPermissions?: readonly PermissionBit[];
60
+ guildOnly?: boolean;
61
+ guildIds?: readonly string[];
62
+ /** Whether replies are ephemeral. Defaults to true. */
63
+ ephemeral?: boolean;
64
+ defer?: boolean;
65
+ fallbackError?: LocalizedText;
66
+ handler: (ctx: ContextMenuContext<K>) => Promise<void>;
67
+ }
68
+
69
+ const COMMAND_TYPES = {
70
+ user: ApplicationCommandType.User,
71
+ message: ApplicationCommandType.Message,
72
+ } as const;
73
+
74
+ /**
75
+ * Resolve the clicked target. discord.js exposes it through kind-specific
76
+ * getters, so the narrowing happens once here rather than in every handler.
77
+ *
78
+ * The kind cannot actually mismatch at runtime — Discord routes an interaction
79
+ * to the command it was registered as — but the resolver still asserts it, so a
80
+ * misroute surfaces as a named error instead of an undefined target reaching the
81
+ * handler.
82
+ */
83
+ function resolveTarget<K extends keyof ContextMenuTarget>(
84
+ interaction: ContextMenuCommandInteraction,
85
+ kind: K,
86
+ ): ContextMenuTarget[K] {
87
+ if (kind === "user" && interaction.isUserContextMenuCommand()) {
88
+ return interaction.targetUser as ContextMenuTarget[K];
89
+ }
90
+ if (kind === "message" && interaction.isMessageContextMenuCommand()) {
91
+ return interaction.targetMessage as ContextMenuTarget[K];
92
+ }
93
+ throw new ContextMenuTargetError(interaction.commandName, kind);
94
+ }
95
+
96
+ export function createContextMenuCommand<K extends keyof ContextMenuTarget>(
97
+ def: ContextMenuCommandDef<K>,
98
+ ): ContextMenuCommand {
99
+ const ephemeral = def.ephemeral ?? true;
100
+ const builder = new ContextMenuCommandBuilder()
101
+ .setName(def.name)
102
+ .setType(COMMAND_TYPES[def.target]);
103
+
104
+ if (def.nameLocalizations) {
105
+ builder.setNameLocalizations(def.nameLocalizations);
106
+ }
107
+ if (def.defaultMemberPermissions) {
108
+ builder.setDefaultMemberPermissions(resolvePermissions(def.defaultMemberPermissions));
109
+ }
110
+ if (def.guildOnly) {
111
+ builder.setContexts(InteractionContextType.Guild);
112
+ }
113
+
114
+ return {
115
+ data: builder,
116
+ guildIds: def.guildIds,
117
+ dispatch: (interaction, runtime) => dispatch(interaction, def, ephemeral, runtime),
118
+ };
119
+ }
120
+
121
+ async function dispatch<K extends keyof ContextMenuTarget>(
122
+ interaction: ContextMenuCommandInteraction,
123
+ def: ContextMenuCommandDef<K>,
124
+ ephemeral: boolean,
125
+ runtime: CommandRuntime,
126
+ ): Promise<void> {
127
+ const { presenter, logger, translator } = runtime;
128
+ const locale = resolveLocale(
129
+ [interaction.locale, interaction.guildLocale],
130
+ translator.defaultLocale,
131
+ );
132
+
133
+ if (def.guard) {
134
+ const result = await def.guard.check(interaction);
135
+ if (!result.ok) {
136
+ await interaction.reply({
137
+ embeds: [presenter.denial(translator.resolve(locale, result.message), locale)],
138
+ flags: MessageFlags.Ephemeral,
139
+ });
140
+ return;
141
+ }
142
+ }
143
+
144
+ try {
145
+ if (def.defer) {
146
+ await interaction.deferReply(ephemeral ? { flags: MessageFlags.Ephemeral } : {});
147
+ }
148
+ await def.handler({
149
+ interaction,
150
+ target: resolveTarget(interaction, def.target),
151
+ locale,
152
+ t: (key, params) => translator.translate(locale, key, params),
153
+ confirm: (message) =>
154
+ send(interaction, presenter.confirmation(message, locale), ephemeral, logger),
155
+ error: (message) => send(interaction, presenter.error(message, locale), ephemeral, logger),
156
+ deny: (message) => send(interaction, presenter.denial(message, locale), ephemeral, logger),
157
+ });
158
+ } catch (error) {
159
+ if (error instanceof BusinessError) {
160
+ const text = error.translation
161
+ ? translator.translate(locale, error.translation.key, error.translation.params)
162
+ : error.message;
163
+ const embed =
164
+ error.severity === "warning"
165
+ ? presenter.warning(text, locale)
166
+ : presenter.error(text, locale);
167
+ await send(interaction, embed, ephemeral, logger);
168
+ return;
169
+ }
170
+ const ref = createIncidentRef();
171
+ logger.error(
172
+ {
173
+ command: interaction.commandName,
174
+ userId: interaction.user.id,
175
+ ref,
176
+ err: error instanceof Error ? error.message : String(error),
177
+ stack: error instanceof Error ? error.stack : undefined,
178
+ },
179
+ "Context menu handler failed",
180
+ );
181
+ await send(
182
+ interaction,
183
+ presenter.systemError(
184
+ translator.resolve(locale, def.fallbackError ?? { key: CORE_MESSAGES.genericError }),
185
+ ref,
186
+ locale,
187
+ ),
188
+ ephemeral,
189
+ logger,
190
+ );
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Deliver an embed whatever the interaction's lifecycle state, and never throw:
196
+ * a failed delivery (e.g. an expired interaction) must not escape into the
197
+ * client's unhandled `error` event. Mirrors the command pipeline's `sendEmbed`,
198
+ * which is typed for chat-input interactions only.
199
+ */
200
+ async function send(
201
+ interaction: ContextMenuCommandInteraction,
202
+ embed: EmbedBuilder,
203
+ ephemeral: boolean,
204
+ logger: Logger,
205
+ ): Promise<void> {
206
+ const flags = ephemeral ? MessageFlags.Ephemeral : undefined;
207
+ try {
208
+ if (interaction.deferred) {
209
+ await interaction.editReply({ embeds: [embed] });
210
+ return;
211
+ }
212
+ if (interaction.replied) {
213
+ await interaction.followUp({ embeds: [embed], flags });
214
+ return;
215
+ }
216
+ await interaction.reply({ embeds: [embed], flags });
217
+ } catch (error) {
218
+ logger.error(
219
+ {
220
+ commandName: interaction.commandName,
221
+ interactionId: interaction.id,
222
+ err: error instanceof Error ? error.message : String(error),
223
+ },
224
+ "Failed to deliver interaction response",
225
+ );
226
+ }
227
+ }
@@ -0,0 +1,22 @@
1
+ import { ValidationError } from "@/errors";
2
+
3
+ /**
4
+ * Raised when a slash command option is declared or read in a way discord.js
5
+ * cannot honour (an unsupported `choices` call, a value that fails parsing).
6
+ * A `ValidationError`, so the command pipeline renders it as user feedback
7
+ * rather than an incident.
8
+ */
9
+ export class OptionValidationError extends ValidationError {}
10
+
11
+ /**
12
+ * Raised when a context-menu interaction does not carry the target its command
13
+ * was declared for. Discord routes an interaction to the command it was
14
+ * registered as, so this signals a misregistration rather than user input —
15
+ * hence a plain `Error`, rendered as an incident with a reference.
16
+ */
17
+ export class ContextMenuTargetError extends Error {
18
+ constructor(commandName: string, expected: string) {
19
+ super(`Context menu command "${commandName}" expected a ${expected} target.`);
20
+ this.name = "ContextMenuTargetError";
21
+ }
22
+ }