@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,538 @@
1
+ import {
2
+ type Attachment,
3
+ ChannelType,
4
+ type MessageComponentInteraction,
5
+ type ModalBuilder,
6
+ type ModalMessageModalSubmitInteraction,
7
+ } from "discord.js";
8
+ import {
9
+ isPickerField,
10
+ type SettingsEditorFieldValue,
11
+ type SettingsEditorGroupField,
12
+ type SettingsEditorPickerField,
13
+ type SettingsEditorValueField,
14
+ } from "@/discord/components/settings-editor/settings-editor-fields";
15
+ import { SETTINGS_EDITOR_MESSAGES } from "@/discord/i18n";
16
+ import {
17
+ type ChannelFieldDef,
18
+ createModal,
19
+ type ModalFields,
20
+ type RoleFieldDef,
21
+ type SelectFieldDef,
22
+ type TextFieldDef,
23
+ type UserFieldDef,
24
+ } from "@/discord/interaction/modal";
25
+ import type { Locale, Translator } from "@/i18n";
26
+
27
+ const EDITOR_FIELD_MODAL_ID = "settings-editor:field-value";
28
+ const EDITOR_GROUP_MODAL_ID = "settings-editor:group-value";
29
+
30
+ /** Generous enough to write a whole embed description without racing a timeout. */
31
+ const MODAL_TIMEOUT_MS = 5 * 60 * 1000;
32
+
33
+ /**
34
+ * `createModal(...).build()` with no state produces the same constant
35
+ * customId for every field modal (`EDITOR_FIELD_MODAL_ID`) and every group
36
+ * modal (`EDITOR_GROUP_MODAL_ID`) — one identifier shared by the whole
37
+ * editor. Combined with a `filter` that only checked the user, a stale
38
+ * `awaitModalSubmit` left behind by a modal the administrator opened and then
39
+ * dismissed (it keeps listening for the rest of {@link MODAL_TIMEOUT_MS})
40
+ * would resolve on the *next* modal that same user submitted, whatever field
41
+ * it belonged to — reported as opening "Add a game", abandoning it, then
42
+ * declaring a request field named "Date" ending up creating a game named
43
+ * "Date".
44
+ *
45
+ * The fix needs an identifier that is unique per *opening*, not just per
46
+ * field: the field/group key alone is not enough, because the same field can
47
+ * be reopened while an earlier, dismissed opening's wait is still alive. A
48
+ * process-local monotonic counter appended to the key is enough for that —
49
+ * it only has to distinguish opens within this process's own lifetime, never
50
+ * survive a restart, and carries no notion of time, so a `Clock` would be the
51
+ * wrong tool (and untestable without faking time for no reason).
52
+ *
53
+ * Making the customId dynamic like this is safe *here*: unlike a persistent
54
+ * component, this modal is never registered on the `ComponentRouter` — both
55
+ * `promptEditorFieldValue` and `promptEditorGroupValue` collect their
56
+ * submission directly with `awaitModalSubmit`, a one-shot collector that
57
+ * matches by whatever customId it is told to expect, not by the router's
58
+ * `customId`/`customId:*` rule. A dynamic id would break the router (it needs
59
+ * a stable prefix to keep matching after a restart); it does not break a
60
+ * collector.
61
+ */
62
+ let modalOpenSequence = 0;
63
+
64
+ /**
65
+ * A customId suffix unique to one modal opening: `key` for readability when
66
+ * debugging, plus a counter so reopening the same field still yields a
67
+ * different id from the opening before it.
68
+ */
69
+ function nextModalOpenState(key: string): string {
70
+ modalOpenSequence += 1;
71
+ return `${key}-${modalOpenSequence}`;
72
+ }
73
+
74
+ /** The customId a built modal actually carries, read back rather than reassembled. */
75
+ function customIdOf(modal: ModalBuilder): string {
76
+ return (modal.toJSON() as { custom_id: string }).custom_id;
77
+ }
78
+
79
+ /** One image per override — the embed has exactly one slot for each. */
80
+ const MAX_UPLOADS = 1;
81
+
82
+ export interface SettingsEditorSubmission {
83
+ /** The modal's own submit interaction — the only one still usable to answer. */
84
+ readonly interaction: ModalMessageModalSubmitInteraction;
85
+ /** `null` when the field came back empty, which is how an override is cleared. */
86
+ readonly value: string | null;
87
+ /**
88
+ * The uploaded image, on the fields that offer one. Always `null` elsewhere,
89
+ * and `null` on those when nothing was uploaded.
90
+ */
91
+ readonly upload: Attachment | null;
92
+ /**
93
+ * What a picker field reported. `null` when the selection was emptied — that
94
+ * is how such a setting is cleared. Always `null` on a value field.
95
+ */
96
+ readonly ids: readonly string[] | null;
97
+ }
98
+
99
+ /**
100
+ * Ask for one field's new value, prefilled with the current one, and wait for
101
+ * the answer. Returns `null` on timeout or on any failure to show/collect it
102
+ * (the administrator dismissed the modal, say) — the caller treats that as
103
+ * "nothing happened", never as an error, and the screen stays as it was.
104
+ *
105
+ * `showModal` finally acknowledges `interaction`: nothing can `.update()` on it
106
+ * again, which is why the submission carries its own interaction to answer
107
+ * through.
108
+ *
109
+ * `MessageComponentInteraction` rather than `AnySelectMenuInteraction`: a menu
110
+ * entry opens the very same modal whether it is reached by picking it from a
111
+ * select (the embeds layout) or by clicking the button its own `Section`
112
+ * carries (the card layout), and both interaction kinds carry `showModal` and
113
+ * `awaitModalSubmit` from that shared base.
114
+ */
115
+ export async function promptEditorFieldValue<F extends string>(
116
+ interaction: MessageComponentInteraction,
117
+ field: SettingsEditorValueField<F> | SettingsEditorPickerField<F>,
118
+ current: SettingsEditorFieldValue,
119
+ translator: Translator,
120
+ locale: Locale,
121
+ ): Promise<SettingsEditorSubmission | null> {
122
+ const modal = isPickerField(field)
123
+ ? createPickerModal(field, current, translator, locale)
124
+ : field.kind === "image"
125
+ ? createImageModal(field, current, translator, locale)
126
+ : createTextModal(field, current, translator, locale);
127
+
128
+ try {
129
+ const built = modal.build(nextModalOpenState(field.key));
130
+ const expectedCustomId = customIdOf(built);
131
+ await interaction.showModal(built);
132
+ const submitted = await interaction.awaitModalSubmit({
133
+ time: MODAL_TIMEOUT_MS,
134
+ filter: (submission) =>
135
+ submission.user.id === interaction.user.id && submission.customId === expectedCustomId,
136
+ });
137
+ // A modal opened from a message submits as a `ModalMessageModalSubmit`
138
+ // interaction — the only kind that can `.update()` the screen in place.
139
+ // Anything else did not come from it, so it is dropped rather than answered
140
+ // on a message it has no claim to.
141
+ if (!submitted.isFromMessage()) {
142
+ return null;
143
+ }
144
+ return { interaction: submitted, ...modal.read(submitted) };
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /** What one member field of a group came back with — the group's own per-member slice. */
151
+ export interface SettingsEditorGroupMemberSubmission {
152
+ readonly value: string | null;
153
+ readonly upload: Attachment | null;
154
+ readonly ids: readonly string[] | null;
155
+ }
156
+
157
+ /**
158
+ * What a group's modal came back with, one entry per member field it declared.
159
+ * A member absent from a submission cannot happen — every member the group
160
+ * declares is a component of the very modal that produced this — but the type
161
+ * stays a partial record rather than asserting it, since nothing here proves it
162
+ * to the compiler.
163
+ */
164
+ export type SettingsEditorGroupSubmission<F extends string> = Readonly<
165
+ Partial<Record<F, SettingsEditorGroupMemberSubmission>>
166
+ >;
167
+
168
+ /** What opening a group's modal resolves to: the submitted values, and the one interaction left to answer through. */
169
+ export interface SettingsEditorGroupPrompt<F extends string> {
170
+ readonly interaction: ModalMessageModalSubmitInteraction;
171
+ readonly values: SettingsEditorGroupSubmission<F>;
172
+ }
173
+
174
+ /**
175
+ * Ask for every setting a group declares at once, in a single modal — the
176
+ * group's counterpart of {@link promptEditorFieldValue}. Each member is laid
177
+ * out in the geometry its own `kind` imposes, exactly as it would if it were
178
+ * prompted on its own, and prefilled the same way: from `currentValue`, asked
179
+ * once per member.
180
+ *
181
+ * `legend` is extra text shown above the fields — a token list a message field
182
+ * reads against, say — already counted against the modal's own five-component
183
+ * budget by the caller that built it; `null` when the group has nothing to say
184
+ * there.
185
+ */
186
+ export async function promptEditorGroupValue<F extends string>(
187
+ interaction: MessageComponentInteraction,
188
+ group: SettingsEditorGroupField<F>,
189
+ currentValue: (memberKey: F) => SettingsEditorFieldValue,
190
+ legend: string | null,
191
+ translator: Translator,
192
+ locale: Locale,
193
+ ): Promise<SettingsEditorGroupPrompt<F> | null> {
194
+ const modal = createGroupModal(group, currentValue, legend, translator, locale);
195
+ try {
196
+ const built = modal.build(nextModalOpenState(group.key));
197
+ const expectedCustomId = customIdOf(built);
198
+ await interaction.showModal(built);
199
+ const submitted = await interaction.awaitModalSubmit({
200
+ time: MODAL_TIMEOUT_MS,
201
+ filter: (submission) =>
202
+ submission.user.id === interaction.user.id && submission.customId === expectedCustomId,
203
+ });
204
+ if (!submitted.isFromMessage()) {
205
+ return null;
206
+ }
207
+ return { interaction: submitted, values: modal.read(submitted) };
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ /** Reading a group's submission back — the group's own counterpart of {@link EditorFieldModal}. */
214
+ interface EditorGroupModal<F extends string> {
215
+ build(state?: string): ModalBuilder;
216
+ read(interaction: ModalMessageModalSubmitInteraction): SettingsEditorGroupSubmission<F>;
217
+ }
218
+
219
+ /**
220
+ * A group's modal: one component per member field, each in the shape its own
221
+ * `kind` names, built through the very builders a single-field modal uses —
222
+ * `createModal`'s own generic `read` is what lets this stay one function
223
+ * whatever mix of text and picker members a group declares.
224
+ *
225
+ * A member of kind `image` is refused here rather than supported: an image
226
+ * field needs two components of its own (the upload, and the address beside
227
+ * it), which a group already spending one slot per member — and possibly one
228
+ * more on `legend` — has no general room for, and no group this repository
229
+ * declares needs one.
230
+ */
231
+ function createGroupModal<F extends string>(
232
+ group: SettingsEditorGroupField<F>,
233
+ currentValue: (memberKey: F) => SettingsEditorFieldValue,
234
+ legend: string | null,
235
+ translator: Translator,
236
+ locale: Locale,
237
+ ): EditorGroupModal<F> {
238
+ const label = translator.translate(locale, group.labelKey);
239
+ const fields: ModalFields = {};
240
+ for (const member of group.fields) {
241
+ const current = currentValue(member.key);
242
+ if (isPickerField(member)) {
243
+ fields[member.key] = pickerFieldDef(
244
+ member,
245
+ pickerShared(member, translator, locale),
246
+ current.picked,
247
+ translator,
248
+ locale,
249
+ );
250
+ continue;
251
+ }
252
+ if (member.kind === "image") {
253
+ throw new Error(
254
+ `Settings editor group "${group.key}" declares member "${member.key}" of kind "image"; a group's modal has no room for the separate upload component an image field needs. Declare it as "text" instead.`,
255
+ );
256
+ }
257
+ fields[member.key] = valueFieldDefOf(member, current, translator, locale);
258
+ }
259
+
260
+ const modal = createModal({
261
+ id: EDITOR_GROUP_MODAL_ID,
262
+ title: modalTitle(label, translator, locale),
263
+ ...(legend === null ? {} : { legend }),
264
+ fields,
265
+ });
266
+ return {
267
+ build: (state) => modal.build(state),
268
+ read: (interaction) => {
269
+ const raw = modal.read(interaction) as unknown as Record<
270
+ string,
271
+ string | null | readonly string[]
272
+ >;
273
+ const values: Partial<Record<F, SettingsEditorGroupMemberSubmission>> = {};
274
+ for (const member of group.fields) {
275
+ values[member.key] = isPickerField(member)
276
+ ? { value: null, upload: null, ids: raw[member.key] as readonly string[] | null }
277
+ : { value: raw[member.key] as string | null, upload: null, ids: null };
278
+ }
279
+ return values;
280
+ },
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Reading a submission back, whichever of the two modals produced it. Both
286
+ * answer the same two questions, so the caller never branches on which was
287
+ * shown.
288
+ */
289
+ interface EditorFieldModal {
290
+ build(state?: string): ModalBuilder;
291
+ read(interaction: ModalMessageModalSubmitInteraction): {
292
+ value: string | null;
293
+ upload: Attachment | null;
294
+ ids: readonly string[] | null;
295
+ };
296
+ }
297
+
298
+ function modalTitle(label: string, translator: Translator, locale: Locale): string {
299
+ return translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalTitle, { field: label });
300
+ }
301
+
302
+ /** The greyed-out example inside the box, for a field that declared one. */
303
+ function placeholderOf<F extends string>(
304
+ field: SettingsEditorValueField<F>,
305
+ translator: Translator,
306
+ locale: Locale,
307
+ ): { placeholder?: string } {
308
+ if (field.placeholderKey === undefined) {
309
+ return {};
310
+ }
311
+ return {
312
+ placeholder: translator.translate(locale, field.placeholderKey, field.placeholderParams),
313
+ };
314
+ }
315
+
316
+ /**
317
+ * The one text-input component a `text`-kind field builds, whether it is
318
+ * prompted on its own (via {@link createTextModal}) or as a member of a group
319
+ * — the shape a group's modal needs from `createModal`'s own generic `read`
320
+ * is exactly this, unlabelled by which caller asked for it.
321
+ */
322
+ function valueFieldDefOf<F extends string>(
323
+ field: SettingsEditorValueField<F>,
324
+ current: SettingsEditorFieldValue,
325
+ translator: Translator,
326
+ locale: Locale,
327
+ ): TextFieldDef {
328
+ return {
329
+ kind: "text",
330
+ label: translator.translate(locale, field.labelKey),
331
+ description: translator.translate(
332
+ locale,
333
+ field.helperKey ?? SETTINGS_EDITOR_MESSAGES.modalHelper,
334
+ ),
335
+ style: field.style,
336
+ required: false,
337
+ maxLength: field.maxLength,
338
+ ...placeholderOf(field, translator, locale),
339
+ ...(current.text === null ? {} : { value: current.text }),
340
+ };
341
+ }
342
+
343
+ /** Every field but the images: one optional text input, emptied to clear. */
344
+ function createTextModal<F extends string>(
345
+ field: SettingsEditorValueField<F>,
346
+ current: SettingsEditorFieldValue,
347
+ translator: Translator,
348
+ locale: Locale,
349
+ ): EditorFieldModal {
350
+ const label = translator.translate(locale, field.labelKey);
351
+ const modal = createModal({
352
+ id: EDITOR_FIELD_MODAL_ID,
353
+ title: modalTitle(label, translator, locale),
354
+ fields: { value: valueFieldDefOf(field, current, translator, locale) },
355
+ });
356
+ return {
357
+ build: (state) => modal.build(state),
358
+ read: (interaction) => ({ value: modal.read(interaction).value, upload: null, ids: null }),
359
+ };
360
+ }
361
+
362
+ /**
363
+ * An image field: a file upload and the address input side by side, both
364
+ * optional, two of the five components a modal may carry.
365
+ *
366
+ * Offering both is what keeps an administrator from having to host the file
367
+ * somewhere first, without taking away the address that a guild already
368
+ * pointing at its own CDN relies on.
369
+ */
370
+ function createImageModal<F extends string>(
371
+ field: SettingsEditorValueField<F>,
372
+ current: SettingsEditorFieldValue,
373
+ translator: Translator,
374
+ locale: Locale,
375
+ ): EditorFieldModal {
376
+ const label = translator.translate(locale, field.labelKey);
377
+ const modal = createModal({
378
+ id: EDITOR_FIELD_MODAL_ID,
379
+ title: modalTitle(label, translator, locale),
380
+ fields: {
381
+ upload: {
382
+ kind: "file",
383
+ label: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalUploadLabel),
384
+ description: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalUploadHelper, {
385
+ max: field.uploadMaxMib ?? 0,
386
+ }),
387
+ required: false,
388
+ maxValues: MAX_UPLOADS,
389
+ },
390
+ value: {
391
+ kind: "text",
392
+ label: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalImageUrlLabel),
393
+ description: translator.translate(
394
+ locale,
395
+ current.uploaded
396
+ ? SETTINGS_EDITOR_MESSAGES.modalImageHelperUploaded
397
+ : SETTINGS_EDITOR_MESSAGES.modalImageHelper,
398
+ ),
399
+ style: field.style,
400
+ required: false,
401
+ maxLength: field.maxLength,
402
+ ...(current.text === null ? {} : { value: current.text }),
403
+ },
404
+ },
405
+ });
406
+ return {
407
+ build: (state) => modal.build(state),
408
+ read: (interaction) => {
409
+ const values = modal.read(interaction);
410
+ return { value: values.value, upload: values.upload?.[0] ?? null, ids: null };
411
+ },
412
+ };
413
+ }
414
+
415
+ /**
416
+ * A picker field's modal: one select of the shape its kind names — role, user,
417
+ * channel or a closed set of choices — prefilled with what it already holds.
418
+ *
419
+ * `category` narrows a channel select to {@link ChannelType.GuildCategory}: the
420
+ * very shorthand `createPickerRow` used to express, reproduced here rather than
421
+ * left behind now that the picker lives in a modal instead of the message.
422
+ */
423
+ function createPickerModal<F extends string>(
424
+ field: SettingsEditorPickerField<F>,
425
+ current: SettingsEditorFieldValue,
426
+ translator: Translator,
427
+ locale: Locale,
428
+ ): EditorFieldModal {
429
+ const label = translator.translate(locale, field.labelKey);
430
+ const modal = createModal({
431
+ id: EDITOR_FIELD_MODAL_ID,
432
+ title: modalTitle(label, translator, locale),
433
+ fields: {
434
+ value: pickerFieldDef(
435
+ field,
436
+ pickerShared(field, translator, locale),
437
+ current.picked,
438
+ translator,
439
+ locale,
440
+ ),
441
+ },
442
+ });
443
+ return {
444
+ build: (state) => modal.build(state),
445
+ read: (interaction) => {
446
+ const read = modal.read(interaction) as { value: readonly string[] | null };
447
+ return { value: null, upload: null, ids: read.value };
448
+ },
449
+ };
450
+ }
451
+
452
+ /**
453
+ * What every picker component shares, whatever its own kind narrows it to —
454
+ * used whether the field is prompted on its own or as a member of a group.
455
+ */
456
+ function pickerShared<F extends string>(
457
+ field: SettingsEditorPickerField<F>,
458
+ translator: Translator,
459
+ locale: Locale,
460
+ ): {
461
+ label: string;
462
+ required: boolean;
463
+ minValues?: number;
464
+ maxValues?: number;
465
+ placeholder?: string;
466
+ } {
467
+ const placeholder =
468
+ field.pickerPlaceholderKey === undefined
469
+ ? undefined
470
+ : translator.translate(locale, field.pickerPlaceholderKey);
471
+ return {
472
+ label: translator.translate(locale, field.labelKey),
473
+ // Zero is what lets the administrator clear the setting by emptying the
474
+ // select — `createModal` then hands `null` back on an empty submission
475
+ // instead of refusing to open with nothing picked.
476
+ required: (field.minValues ?? 1) !== 0,
477
+ minValues: field.minValues,
478
+ maxValues: field.maxValues,
479
+ ...(placeholder === undefined ? {} : { placeholder }),
480
+ };
481
+ }
482
+
483
+ /** The one component a picker modal carries, in the shape its field's kind names. */
484
+ function pickerFieldDef<F extends string>(
485
+ field: SettingsEditorPickerField<F>,
486
+ shared: {
487
+ label: string;
488
+ required: boolean;
489
+ minValues?: number;
490
+ maxValues?: number;
491
+ placeholder?: string;
492
+ },
493
+ held: readonly string[],
494
+ translator: Translator,
495
+ locale: Locale,
496
+ ): RoleFieldDef | UserFieldDef | ChannelFieldDef | SelectFieldDef {
497
+ // `modal.ts`'s builders open a default whenever the key is present at all,
498
+ // empty array included — so an empty selection is left out entirely rather
499
+ // than handed through, the same guard `createRoleSelect` (the select-menu
500
+ // DSL the in-message picker used) already applied.
501
+ switch (field.kind) {
502
+ case "role":
503
+ return { kind: "role", ...shared, ...(held.length === 0 ? {} : { defaultRoleIds: held }) };
504
+ case "user":
505
+ return { kind: "user", ...shared, ...(held.length === 0 ? {} : { defaultUserIds: held }) };
506
+ case "category":
507
+ return {
508
+ kind: "channel",
509
+ ...shared,
510
+ channelTypes: [ChannelType.GuildCategory],
511
+ ...(held.length === 0 ? {} : { defaultChannelIds: held }),
512
+ };
513
+ case "channel":
514
+ return {
515
+ kind: "channel",
516
+ ...shared,
517
+ channelTypes: field.channelTypes,
518
+ ...(held.length === 0 ? {} : { defaultChannelIds: held }),
519
+ };
520
+ case "choice":
521
+ return {
522
+ kind: "select",
523
+ ...shared,
524
+ options: field.choices.map((choice) => ({
525
+ label: translator.translate(locale, choice.labelKey),
526
+ value: choice.value,
527
+ ...(choice.descriptionKey === undefined
528
+ ? {}
529
+ : { description: translator.translate(locale, choice.descriptionKey) }),
530
+ // Marked rather than prefilled the way an entity select's default is:
531
+ // Discord has no "default value" concept for a string select, only a
532
+ // per-option flag, which is also how the reader still sees which of a
533
+ // closed set is in force.
534
+ default: held.includes(choice.value),
535
+ })),
536
+ };
537
+ }
538
+ }