@erinjs/core 1.0.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 (389) hide show
  1. package/core/.changeset/README.md +8 -0
  2. package/core/.changeset/community-bootstrap-release.md +17 -0
  3. package/core/.changeset/config.json +11 -0
  4. package/core/.changeset/no-changelog.js +16 -0
  5. package/core/.changeset/pre.json +17 -0
  6. package/core/.editorconfig +13 -0
  7. package/core/.gitattributes +2 -0
  8. package/core/.github/CODE_OF_CONDUCT.md +23 -0
  9. package/core/.github/FUNDING.yml +7 -0
  10. package/core/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  11. package/core/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  12. package/core/.github/PULL_REQUEST_TEMPLATE.md +16 -0
  13. package/core/.github/dependabot.yml +16 -0
  14. package/core/.github/workflows/autoapp.yml +16 -0
  15. package/core/.github/workflows/ci.yml +187 -0
  16. package/core/.github/workflows/codeql.yml +30 -0
  17. package/core/.github/workflows/deploy-docs.yml +54 -0
  18. package/core/.github/workflows/publish.yml +43 -0
  19. package/core/.lintstagedrc.json +4 -0
  20. package/core/.nvmrc +1 -0
  21. package/core/.prettierignore +8 -0
  22. package/core/.prettierrc +11 -0
  23. package/core/CONTRIBUTING.md +70 -0
  24. package/core/LICENSE +203 -0
  25. package/core/README.md +61 -0
  26. package/core/SECURITY.md +21 -0
  27. package/core/apps/docs/index.html +28 -0
  28. package/core/apps/docs/middleware.ts +21 -0
  29. package/core/apps/docs/package.json +33 -0
  30. package/core/apps/docs/public/@flux.png +0 -0
  31. package/core/apps/docs/public/docs/latest/guides.json +1420 -0
  32. package/core/apps/docs/public/docs/latest/main.json +14981 -0
  33. package/core/apps/docs/public/docs/latest/rag-index.json +1 -0
  34. package/core/apps/docs/public/docs/v1.0.5/guides.json +226 -0
  35. package/core/apps/docs/public/docs/v1.0.5/main.json +7920 -0
  36. package/core/apps/docs/public/docs/v1.0.6/guides.json +226 -0
  37. package/core/apps/docs/public/docs/v1.0.6/main.json +7920 -0
  38. package/core/apps/docs/public/docs/v1.0.7/guides.json +259 -0
  39. package/core/apps/docs/public/docs/v1.0.7/main.json +8652 -0
  40. package/core/apps/docs/public/docs/v1.0.8/guides.json +313 -0
  41. package/core/apps/docs/public/docs/v1.0.8/main.json +9618 -0
  42. package/core/apps/docs/public/docs/v1.0.9/guides.json +319 -0
  43. package/core/apps/docs/public/docs/v1.0.9/main.json +10694 -0
  44. package/core/apps/docs/public/docs/v1.1.0/guides.json +589 -0
  45. package/core/apps/docs/public/docs/v1.1.0/main.json +12576 -0
  46. package/core/apps/docs/public/docs/v1.1.2/guides.json +650 -0
  47. package/core/apps/docs/public/docs/v1.1.2/main.json +13239 -0
  48. package/core/apps/docs/public/docs/v1.1.3/guides.json +650 -0
  49. package/core/apps/docs/public/docs/v1.1.3/main.json +13239 -0
  50. package/core/apps/docs/public/docs/v1.1.4/guides.json +708 -0
  51. package/core/apps/docs/public/docs/v1.1.4/main.json +13231 -0
  52. package/core/apps/docs/public/docs/v1.1.5/guides.json +1035 -0
  53. package/core/apps/docs/public/docs/v1.1.5/main.json +13838 -0
  54. package/core/apps/docs/public/docs/v1.1.6/guides.json +1041 -0
  55. package/core/apps/docs/public/docs/v1.1.6/main.json +14313 -0
  56. package/core/apps/docs/public/docs/v1.1.8/guides.json +1047 -0
  57. package/core/apps/docs/public/docs/v1.1.8/main.json +14421 -0
  58. package/core/apps/docs/public/docs/v1.1.9/guides.json +1047 -0
  59. package/core/apps/docs/public/docs/v1.1.9/main.json +14421 -0
  60. package/core/apps/docs/public/docs/v1.2.0/guides.json +1212 -0
  61. package/core/apps/docs/public/docs/v1.2.0/main.json +14663 -0
  62. package/core/apps/docs/public/docs/v1.2.1/guides.json +1293 -0
  63. package/core/apps/docs/public/docs/v1.2.1/main.json +14828 -0
  64. package/core/apps/docs/public/docs/v1.2.2/guides.json +1293 -0
  65. package/core/apps/docs/public/docs/v1.2.2/main.json +15025 -0
  66. package/core/apps/docs/public/docs/v1.2.3/guides.json +1420 -0
  67. package/core/apps/docs/public/docs/v1.2.3/main.json +14954 -0
  68. package/core/apps/docs/public/docs/v1.2.4/guides.json +1420 -0
  69. package/core/apps/docs/public/docs/v1.2.4/main.json +14981 -0
  70. package/core/apps/docs/public/docs/versions.json +24 -0
  71. package/core/apps/docs/public/flux.png +0 -0
  72. package/core/apps/docs/public/locales/en.json +50 -0
  73. package/core/apps/docs/public/locales/guides-en.json +512 -0
  74. package/core/apps/docs/public/robots.txt +4 -0
  75. package/core/apps/docs/public/sitemap.xml +33 -0
  76. package/core/apps/docs/src/App.vue +538 -0
  77. package/core/apps/docs/src/components/ApiCategorySection.vue +42 -0
  78. package/core/apps/docs/src/components/ApiDiscordCompat.vue +65 -0
  79. package/core/apps/docs/src/components/ApiEndpointCard.vue +313 -0
  80. package/core/apps/docs/src/components/ApiSchemaBlock.vue +131 -0
  81. package/core/apps/docs/src/components/CodeBlock.vue +177 -0
  82. package/core/apps/docs/src/components/CommunityCallout.vue +90 -0
  83. package/core/apps/docs/src/components/ConstructorSection.vue +82 -0
  84. package/core/apps/docs/src/components/DocDescription.vue +40 -0
  85. package/core/apps/docs/src/components/FluxerLogo.vue +3 -0
  86. package/core/apps/docs/src/components/Footer.vue +106 -0
  87. package/core/apps/docs/src/components/GuideCodeBlock.vue +102 -0
  88. package/core/apps/docs/src/components/GuideDiscordCompat.vue +77 -0
  89. package/core/apps/docs/src/components/GuideDiscordCompatCallout.vue +83 -0
  90. package/core/apps/docs/src/components/GuideTable.vue +77 -0
  91. package/core/apps/docs/src/components/GuideTip.vue +38 -0
  92. package/core/apps/docs/src/components/MethodsSection.vue +195 -0
  93. package/core/apps/docs/src/components/ParamsTable.vue +70 -0
  94. package/core/apps/docs/src/components/PropertiesSection.vue +143 -0
  95. package/core/apps/docs/src/components/SearchBar.vue +76 -0
  96. package/core/apps/docs/src/components/SearchModal.vue +361 -0
  97. package/core/apps/docs/src/components/SidebarNav.vue +225 -0
  98. package/core/apps/docs/src/components/SponsorBanner.vue +153 -0
  99. package/core/apps/docs/src/components/TypeSignature.vue +187 -0
  100. package/core/apps/docs/src/components/VersionPicker.vue +191 -0
  101. package/core/apps/docs/src/composables/useSearchIndex.ts +144 -0
  102. package/core/apps/docs/src/composables/useVersionedPath.ts +20 -0
  103. package/core/apps/docs/src/data/apiEndpoints.ts +1073 -0
  104. package/core/apps/docs/src/data/changelog.ts +717 -0
  105. package/core/apps/docs/src/data/guides.ts +2362 -0
  106. package/core/apps/docs/src/env.d.ts +7 -0
  107. package/core/apps/docs/src/locales/guides-en.json +512 -0
  108. package/core/apps/docs/src/main.ts +27 -0
  109. package/core/apps/docs/src/pages/ApiReferenceLayout.vue +175 -0
  110. package/core/apps/docs/src/pages/ApiReferencePage.vue +128 -0
  111. package/core/apps/docs/src/pages/Changelog.vue +288 -0
  112. package/core/apps/docs/src/pages/ClassPage.vue +319 -0
  113. package/core/apps/docs/src/pages/ClassesList.vue +100 -0
  114. package/core/apps/docs/src/pages/DocsLayout.vue +127 -0
  115. package/core/apps/docs/src/pages/GuidePage.vue +279 -0
  116. package/core/apps/docs/src/pages/GuidesIndex.vue +166 -0
  117. package/core/apps/docs/src/pages/GuidesLayout.vue +245 -0
  118. package/core/apps/docs/src/pages/Home.vue +125 -0
  119. package/core/apps/docs/src/pages/NotFound.vue +57 -0
  120. package/core/apps/docs/src/pages/TypedefPage.vue +230 -0
  121. package/core/apps/docs/src/pages/TypedefsList.vue +168 -0
  122. package/core/apps/docs/src/pages/VersionLayout.vue +15 -0
  123. package/core/apps/docs/src/router.ts +73 -0
  124. package/core/apps/docs/src/stores/docs.ts +54 -0
  125. package/core/apps/docs/src/stores/guides.ts +53 -0
  126. package/core/apps/docs/src/stores/version.ts +67 -0
  127. package/core/apps/docs/src/styles/main.css +278 -0
  128. package/core/apps/docs/src/styles/prism.css +95 -0
  129. package/core/apps/docs/src/types/doc-schema.ts +112 -0
  130. package/core/apps/docs/tsconfig.json +17 -0
  131. package/core/apps/docs/tsconfig.node.json +10 -0
  132. package/core/apps/docs/vite.config.d.ts +2 -0
  133. package/core/apps/docs/vite.config.js +26 -0
  134. package/core/apps/docs/vite.config.ts +28 -0
  135. package/core/apps/docs-vitepress/.vitepress/config.ts +141 -0
  136. package/core/apps/docs-vitepress/api-data/latest/main.json +15035 -0
  137. package/core/apps/docs-vitepress/api-data/v1.2.4/main.json +15035 -0
  138. package/core/apps/docs-vitepress/api-data/versions.json +6 -0
  139. package/core/apps/docs-vitepress/index.md +15 -0
  140. package/core/apps/docs-vitepress/package-lock.json +2924 -0
  141. package/core/apps/docs-vitepress/package.json +20 -0
  142. package/core/apps/docs-vitepress/public/CNAME +1 -0
  143. package/core/apps/docs-vitepress/scripts/generate-api.ts +243 -0
  144. package/core/apps/docs-vitepress/scripts/migrate-guides.ts +129 -0
  145. package/core/apps/docs-vitepress/tsconfig.json +11 -0
  146. package/core/apps/docs-vitepress/v/latest/guides/attachments-by-url.md +57 -0
  147. package/core/apps/docs-vitepress/v/latest/guides/attachments.md +62 -0
  148. package/core/apps/docs-vitepress/v/latest/guides/basic-bot.md +49 -0
  149. package/core/apps/docs-vitepress/v/latest/guides/channels.md +180 -0
  150. package/core/apps/docs-vitepress/v/latest/guides/deprecated-apis.md +58 -0
  151. package/core/apps/docs-vitepress/v/latest/guides/discord-js-compatibility.md +42 -0
  152. package/core/apps/docs-vitepress/v/latest/guides/editing-embeds.md +65 -0
  153. package/core/apps/docs-vitepress/v/latest/guides/embed-media.md +87 -0
  154. package/core/apps/docs-vitepress/v/latest/guides/embeds.md +166 -0
  155. package/core/apps/docs-vitepress/v/latest/guides/emojis.md +77 -0
  156. package/core/apps/docs-vitepress/v/latest/guides/events.md +202 -0
  157. package/core/apps/docs-vitepress/v/latest/guides/gifs.md +47 -0
  158. package/core/apps/docs-vitepress/v/latest/guides/installation.md +10 -0
  159. package/core/apps/docs-vitepress/v/latest/guides/moderation.md +89 -0
  160. package/core/apps/docs-vitepress/v/latest/guides/permissions.md +130 -0
  161. package/core/apps/docs-vitepress/v/latest/guides/prefix-commands.md +41 -0
  162. package/core/apps/docs-vitepress/v/latest/guides/profile-urls.md +58 -0
  163. package/core/apps/docs-vitepress/v/latest/guides/reactions.md +69 -0
  164. package/core/apps/docs-vitepress/v/latest/guides/roles.md +130 -0
  165. package/core/apps/docs-vitepress/v/latest/guides/sending-without-reply.md +172 -0
  166. package/core/apps/docs-vitepress/v/latest/guides/voice.md +109 -0
  167. package/core/apps/docs-vitepress/v/latest/guides/wait-for-guilds.md +37 -0
  168. package/core/apps/docs-vitepress/v/latest/guides/webhook-attachments-embeds.md +73 -0
  169. package/core/apps/docs-vitepress/v/latest/guides/webhooks.md +131 -0
  170. package/core/eslint.config.js +80 -0
  171. package/core/examples/.env.example +22 -0
  172. package/core/examples/README.md +68 -0
  173. package/core/examples/first-steps-bot.js +118 -0
  174. package/core/examples/minimal-bot.js +17 -0
  175. package/core/examples/moderation-bot.js +209 -0
  176. package/core/examples/package.json +14 -0
  177. package/core/examples/ping-bot.js +1146 -0
  178. package/core/examples/reaction-bot.js +70 -0
  179. package/core/examples/reaction-roles-bot.js +140 -0
  180. package/core/examples/webhook-bot.js +239 -0
  181. package/core/flux.png +0 -0
  182. package/core/package.json +78 -0
  183. package/core/packages/builders/package.json +51 -0
  184. package/core/packages/builders/src/index.ts +13 -0
  185. package/core/packages/builders/src/messages/AttachmentBuilder.test.ts +79 -0
  186. package/core/packages/builders/src/messages/AttachmentBuilder.ts +69 -0
  187. package/core/packages/builders/src/messages/EmbedBuilder.test.ts +266 -0
  188. package/core/packages/builders/src/messages/EmbedBuilder.ts +239 -0
  189. package/core/packages/builders/src/messages/MessagePayload.test.ts +118 -0
  190. package/core/packages/builders/src/messages/MessagePayload.ts +122 -0
  191. package/core/packages/builders/tsconfig.json +9 -0
  192. package/core/packages/builders/tsup.config.ts +9 -0
  193. package/core/packages/builders/vitest.config.ts +9 -0
  194. package/core/packages/collection/package.json +47 -0
  195. package/core/packages/collection/src/Collection.test.ts +232 -0
  196. package/core/packages/collection/src/Collection.ts +196 -0
  197. package/core/packages/collection/src/index.ts +1 -0
  198. package/core/packages/collection/tsconfig.json +9 -0
  199. package/core/packages/collection/tsup.config.ts +9 -0
  200. package/core/packages/collection/vitest.config.ts +9 -0
  201. package/core/packages/docgen/package.json +26 -0
  202. package/core/packages/docgen/src/extract.ts +262 -0
  203. package/core/packages/docgen/src/formatType.ts +24 -0
  204. package/core/packages/docgen/src/index.ts +103 -0
  205. package/core/packages/docgen/src/schema.ts +100 -0
  206. package/core/packages/docgen/src/visitor.ts +147 -0
  207. package/core/packages/docgen/tsconfig.json +9 -0
  208. package/core/packages/docgen/tsup.config.ts +9 -0
  209. package/core/packages/fluxer-core/README.md +26 -0
  210. package/core/packages/fluxer-core/package.json +60 -0
  211. package/core/packages/fluxer-core/src/client/ChannelManager.ts +143 -0
  212. package/core/packages/fluxer-core/src/client/Client.gateway.test.ts +84 -0
  213. package/core/packages/fluxer-core/src/client/Client.resolveEmoji.test.ts +45 -0
  214. package/core/packages/fluxer-core/src/client/Client.ts +558 -0
  215. package/core/packages/fluxer-core/src/client/ClientUser.ts +40 -0
  216. package/core/packages/fluxer-core/src/client/EventHandlerRegistry.ts +469 -0
  217. package/core/packages/fluxer-core/src/client/GuildManager.ts +79 -0
  218. package/core/packages/fluxer-core/src/client/GuildMemberManager.ts +91 -0
  219. package/core/packages/fluxer-core/src/client/MessageManager.ts +58 -0
  220. package/core/packages/fluxer-core/src/client/UsersManager.ts +122 -0
  221. package/core/packages/fluxer-core/src/errors/ErrorCodes.test.ts +19 -0
  222. package/core/packages/fluxer-core/src/errors/ErrorCodes.ts +12 -0
  223. package/core/packages/fluxer-core/src/errors/FluxerError.test.ts +32 -0
  224. package/core/packages/fluxer-core/src/errors/FluxerError.ts +15 -0
  225. package/core/packages/fluxer-core/src/index.ts +85 -0
  226. package/core/packages/fluxer-core/src/structures/Base.ts +7 -0
  227. package/core/packages/fluxer-core/src/structures/Channel.ts +508 -0
  228. package/core/packages/fluxer-core/src/structures/Guild.test.ts +189 -0
  229. package/core/packages/fluxer-core/src/structures/Guild.ts +734 -0
  230. package/core/packages/fluxer-core/src/structures/GuildBan.ts +35 -0
  231. package/core/packages/fluxer-core/src/structures/GuildEmoji.ts +57 -0
  232. package/core/packages/fluxer-core/src/structures/GuildMember.test.ts +203 -0
  233. package/core/packages/fluxer-core/src/structures/GuildMember.ts +213 -0
  234. package/core/packages/fluxer-core/src/structures/GuildMemberRoleManager.ts +121 -0
  235. package/core/packages/fluxer-core/src/structures/GuildSticker.ts +56 -0
  236. package/core/packages/fluxer-core/src/structures/Invite.test.ts +103 -0
  237. package/core/packages/fluxer-core/src/structures/Invite.ts +121 -0
  238. package/core/packages/fluxer-core/src/structures/Message.test.ts +109 -0
  239. package/core/packages/fluxer-core/src/structures/Message.ts +397 -0
  240. package/core/packages/fluxer-core/src/structures/MessageReaction.ts +72 -0
  241. package/core/packages/fluxer-core/src/structures/PartialMessage.ts +12 -0
  242. package/core/packages/fluxer-core/src/structures/Role.test.ts +77 -0
  243. package/core/packages/fluxer-core/src/structures/Role.ts +112 -0
  244. package/core/packages/fluxer-core/src/structures/User.test.ts +110 -0
  245. package/core/packages/fluxer-core/src/structures/User.ts +109 -0
  246. package/core/packages/fluxer-core/src/structures/Webhook.test.ts +109 -0
  247. package/core/packages/fluxer-core/src/structures/Webhook.ts +258 -0
  248. package/core/packages/fluxer-core/src/util/Constants.test.ts +16 -0
  249. package/core/packages/fluxer-core/src/util/Constants.ts +7 -0
  250. package/core/packages/fluxer-core/src/util/Events.ts +46 -0
  251. package/core/packages/fluxer-core/src/util/MessageCollector.ts +87 -0
  252. package/core/packages/fluxer-core/src/util/Options.ts +33 -0
  253. package/core/packages/fluxer-core/src/util/ReactionCollector.ts +116 -0
  254. package/core/packages/fluxer-core/src/util/cdn.test.ts +108 -0
  255. package/core/packages/fluxer-core/src/util/cdn.ts +130 -0
  256. package/core/packages/fluxer-core/src/util/guildUtils.ts +33 -0
  257. package/core/packages/fluxer-core/src/util/messageUtils.test.ts +74 -0
  258. package/core/packages/fluxer-core/src/util/messageUtils.ts +119 -0
  259. package/core/packages/fluxer-core/src/util/permissions.test.ts +95 -0
  260. package/core/packages/fluxer-core/src/util/permissions.ts +43 -0
  261. package/core/packages/fluxer-core/tsconfig.json +9 -0
  262. package/core/packages/fluxer-core/tsup.config.ts +9 -0
  263. package/core/packages/fluxer-core/vitest.config.ts +9 -0
  264. package/core/packages/rest/package.json +52 -0
  265. package/core/packages/rest/src/REST.test.ts +64 -0
  266. package/core/packages/rest/src/REST.ts +90 -0
  267. package/core/packages/rest/src/RateLimitManager.test.ts +71 -0
  268. package/core/packages/rest/src/RateLimitManager.ts +60 -0
  269. package/core/packages/rest/src/RequestManager.test.ts +87 -0
  270. package/core/packages/rest/src/RequestManager.ts +172 -0
  271. package/core/packages/rest/src/errors/FluxerAPIError.test.ts +57 -0
  272. package/core/packages/rest/src/errors/FluxerAPIError.ts +21 -0
  273. package/core/packages/rest/src/errors/HTTPError.test.ts +55 -0
  274. package/core/packages/rest/src/errors/HTTPError.ts +25 -0
  275. package/core/packages/rest/src/errors/RateLimitError.test.ts +41 -0
  276. package/core/packages/rest/src/errors/RateLimitError.ts +15 -0
  277. package/core/packages/rest/src/errors/index.ts +3 -0
  278. package/core/packages/rest/src/index.ts +6 -0
  279. package/core/packages/rest/src/utils/constants.test.ts +31 -0
  280. package/core/packages/rest/src/utils/constants.ts +5 -0
  281. package/core/packages/rest/src/utils/files.test.ts +37 -0
  282. package/core/packages/rest/src/utils/files.ts +75 -0
  283. package/core/packages/rest/tsconfig.json +9 -0
  284. package/core/packages/rest/tsup.config.ts +9 -0
  285. package/core/packages/rest/vitest.config.ts +9 -0
  286. package/core/packages/types/package.json +46 -0
  287. package/core/packages/types/src/api/ban.ts +8 -0
  288. package/core/packages/types/src/api/channel.ts +65 -0
  289. package/core/packages/types/src/api/embed.ts +82 -0
  290. package/core/packages/types/src/api/emoji.ts +12 -0
  291. package/core/packages/types/src/api/errors.ts +68 -0
  292. package/core/packages/types/src/api/gateway.ts +14 -0
  293. package/core/packages/types/src/api/guild.ts +123 -0
  294. package/core/packages/types/src/api/index.ts +15 -0
  295. package/core/packages/types/src/api/instance.ts +32 -0
  296. package/core/packages/types/src/api/interaction.ts +26 -0
  297. package/core/packages/types/src/api/invite.ts +28 -0
  298. package/core/packages/types/src/api/message.ts +140 -0
  299. package/core/packages/types/src/api/role.ts +41 -0
  300. package/core/packages/types/src/api/sticker.ts +14 -0
  301. package/core/packages/types/src/api/user.ts +79 -0
  302. package/core/packages/types/src/api/webhook.ts +41 -0
  303. package/core/packages/types/src/common/index.ts +1 -0
  304. package/core/packages/types/src/common/snowflake.test.ts +9 -0
  305. package/core/packages/types/src/common/snowflake.ts +8 -0
  306. package/core/packages/types/src/gateway/events.ts +189 -0
  307. package/core/packages/types/src/gateway/index.ts +3 -0
  308. package/core/packages/types/src/gateway/opcodes.ts +17 -0
  309. package/core/packages/types/src/gateway/payloads.ts +481 -0
  310. package/core/packages/types/src/index.ts +4 -0
  311. package/core/packages/types/src/rest/index.ts +1 -0
  312. package/core/packages/types/src/rest/routes.test.ts +169 -0
  313. package/core/packages/types/src/rest/routes.ts +109 -0
  314. package/core/packages/types/tsconfig.json +9 -0
  315. package/core/packages/types/tsup.config.ts +9 -0
  316. package/core/packages/types/vitest.config.ts +9 -0
  317. package/core/packages/util/package.json +51 -0
  318. package/core/packages/util/src/BitField.test.ts +96 -0
  319. package/core/packages/util/src/BitField.ts +105 -0
  320. package/core/packages/util/src/MessageFlagsBitField.test.ts +42 -0
  321. package/core/packages/util/src/MessageFlagsBitField.ts +20 -0
  322. package/core/packages/util/src/PermissionsBitField.test.ts +79 -0
  323. package/core/packages/util/src/PermissionsBitField.ts +97 -0
  324. package/core/packages/util/src/SnowflakeUtil.test.ts +69 -0
  325. package/core/packages/util/src/SnowflakeUtil.ts +65 -0
  326. package/core/packages/util/src/UserFlagsBitField.test.ts +39 -0
  327. package/core/packages/util/src/UserFlagsBitField.ts +48 -0
  328. package/core/packages/util/src/deprecation.test.ts +44 -0
  329. package/core/packages/util/src/deprecation.ts +28 -0
  330. package/core/packages/util/src/emojiShortcodes.generated.ts +5 -0
  331. package/core/packages/util/src/emojiShortcodes.test.ts +41 -0
  332. package/core/packages/util/src/emojiShortcodes.ts +22 -0
  333. package/core/packages/util/src/formatters.test.ts +65 -0
  334. package/core/packages/util/src/formatters.ts +35 -0
  335. package/core/packages/util/src/index.ts +34 -0
  336. package/core/packages/util/src/resolvers.test.ts +198 -0
  337. package/core/packages/util/src/resolvers.ts +127 -0
  338. package/core/packages/util/src/tenorUtils.test.ts +75 -0
  339. package/core/packages/util/src/tenorUtils.ts +86 -0
  340. package/core/packages/util/tsconfig.json +9 -0
  341. package/core/packages/util/tsup.config.ts +9 -0
  342. package/core/packages/util/vitest.config.ts +9 -0
  343. package/core/packages/voice/README.md +42 -0
  344. package/core/packages/voice/package.json +67 -0
  345. package/core/packages/voice/src/LiveKitRtcConnection.receive.test.ts +24 -0
  346. package/core/packages/voice/src/LiveKitRtcConnection.ts +1767 -0
  347. package/core/packages/voice/src/VoiceConnection.ts +413 -0
  348. package/core/packages/voice/src/VoiceManager.receive.test.ts +61 -0
  349. package/core/packages/voice/src/VoiceManager.test.ts +44 -0
  350. package/core/packages/voice/src/VoiceManager.ts +503 -0
  351. package/core/packages/voice/src/exports.test.ts +38 -0
  352. package/core/packages/voice/src/index.ts +51 -0
  353. package/core/packages/voice/src/livekit.test.ts +48 -0
  354. package/core/packages/voice/src/livekit.ts +33 -0
  355. package/core/packages/voice/src/mp4box.d.ts +32 -0
  356. package/core/packages/voice/src/opusUtils.test.ts +29 -0
  357. package/core/packages/voice/src/opusUtils.ts +86 -0
  358. package/core/packages/voice/src/streamPreviewPlaceholder.test.ts +16 -0
  359. package/core/packages/voice/src/streamPreviewPlaceholder.ts +8 -0
  360. package/core/packages/voice/src/ws.d.ts +1 -0
  361. package/core/packages/voice/tsconfig.json +5 -0
  362. package/core/packages/voice/tsup.config.ts +10 -0
  363. package/core/packages/voice/vitest.config.ts +9 -0
  364. package/core/packages/ws/package.json +52 -0
  365. package/core/packages/ws/src/WebSocketManager.ts +130 -0
  366. package/core/packages/ws/src/WebSocketShard.ts +296 -0
  367. package/core/packages/ws/src/index.ts +12 -0
  368. package/core/packages/ws/src/utils/constants.test.ts +46 -0
  369. package/core/packages/ws/src/utils/constants.ts +22 -0
  370. package/core/packages/ws/src/utils/getWebSocket.ts +55 -0
  371. package/core/packages/ws/src/ws.d.ts +10 -0
  372. package/core/packages/ws/tsconfig.json +9 -0
  373. package/core/packages/ws/tsup.config.ts +9 -0
  374. package/core/pnpm-lock.yaml +7033 -0
  375. package/core/pnpm-workspace.yaml +4 -0
  376. package/core/scripts/generate-ai-rag.ts +240 -0
  377. package/core/scripts/generate-docs.ts +143 -0
  378. package/core/scripts/generate-emoji-shortcodes.ts +58 -0
  379. package/core/scripts/generate-types.ts +6 -0
  380. package/core/scripts/publish-ordered.js +63 -0
  381. package/core/scripts/test-cjs-require.mjs +43 -0
  382. package/core/scripts/test-esm-imports.mjs +42 -0
  383. package/core/scripts/test-package-exports.mjs +98 -0
  384. package/core/scripts/test-smoke.mjs +103 -0
  385. package/core/tsconfig.json +18 -0
  386. package/core/turbo.json +30 -0
  387. package/core/vitest.config.ts +17 -0
  388. package/core/wrangler.jsonc +9 -0
  389. package/package.json +26 -0
@@ -0,0 +1,2362 @@
1
+ /**
2
+ * Guide content for the docs site.
3
+ * Each guide is a separate page.
4
+ */
5
+
6
+ export interface GuideTable {
7
+ headers: string[];
8
+ rows: string[][];
9
+ /** Column indices to render as inline code (e.g. [1] for payload column) */
10
+ codeColumns?: number[];
11
+ }
12
+
13
+ /** Alternate code snippet shown in a tab alongside the main code block. */
14
+ export interface GuideAlternateSnippet {
15
+ label: string;
16
+ code: string;
17
+ language?: 'javascript' | 'bash' | 'text';
18
+ }
19
+
20
+ export interface GuideSection {
21
+ title?: string;
22
+ description?: string;
23
+ code?: string;
24
+ language?: 'javascript' | 'bash' | 'text';
25
+ table?: GuideTable;
26
+ /** Short tip shown in a callout (e.g. "You can use client.events for chainable handlers.") */
27
+ tip?: string;
28
+ /** Alternative code snippet; shown as a second tab (e.g. "client.events" vs "client.on"). */
29
+ alternateCode?: GuideAlternateSnippet;
30
+ /** If set, shows green "Discord.js compatible" badge. String = custom link (e.g. to discord.js docs). */
31
+ discordJsCompat?: boolean | string;
32
+ }
33
+
34
+ export interface Guide {
35
+ id: string;
36
+ slug: string;
37
+ title: string;
38
+ description: string;
39
+ category:
40
+ | 'getting-started'
41
+ | 'sending-messages'
42
+ | 'media'
43
+ | 'channels'
44
+ | 'emojis'
45
+ | 'webhooks'
46
+ | 'voice'
47
+ | 'events'
48
+ | 'other';
49
+ sections: GuideSection[];
50
+ }
51
+
52
+ export const guides: Guide[] = [
53
+ {
54
+ id: 'installation',
55
+ slug: 'installation',
56
+ title: 'Installation',
57
+ description: 'Install the package and configure your bot token.',
58
+ category: 'getting-started',
59
+ sections: [
60
+ {
61
+ code: `npm install @erinjs/core
62
+
63
+ # Run your bot (Node 18+)
64
+ FLUXER_BOT_TOKEN=your_token node your-bot.js`,
65
+ language: 'bash',
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ id: 'basic-bot',
71
+ slug: 'basic-bot',
72
+ title: 'Basic Bot',
73
+ description:
74
+ 'A minimal bot that responds to !ping with Pong. See examples/first-steps-bot.js for !hello, !avatar, !embed, !perms.',
75
+ category: 'getting-started',
76
+ sections: [
77
+ {
78
+ code: `import { Client, Events } from '@erinjs/core';
79
+
80
+ const client = new Client({ intents: 0 });
81
+
82
+ client.on(Events.Ready, () => console.log('Ready!'));
83
+ client.on(Events.MessageCreate, async (message) => {
84
+ if (message.content === '!ping') {
85
+ await message.reply('Pong!');
86
+ }
87
+ });
88
+
89
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
90
+ language: 'javascript',
91
+ tip: 'You can also use client.events for chainable, typed handlers with better autocomplete.',
92
+ alternateCode: {
93
+ label: 'client.events',
94
+ code: `import { Client, Events } from '@erinjs/core';
95
+
96
+ const client = new Client({ intents: 0 });
97
+
98
+ client
99
+ .events.Ready(() => console.log('Ready!'))
100
+ .events.MessageCreate(async (message) => {
101
+ if (message.content === '!ping') await message.reply('Pong!');
102
+ });
103
+
104
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
105
+ },
106
+ },
107
+ {
108
+ title: 'Common mistakes',
109
+ description:
110
+ 'Always await message.reply() to avoid unhandled promise rejections. Use intents: 0 (Fluxer does not support intents yet). Set FLUXER_SUPPRESS_DEPRECATION=1 to silence deprecation warnings.',
111
+ code: `// ❌ BAD — unhandled rejection if reply fails
112
+ message.reply('Pong!');
113
+
114
+ // ✅ GOOD
115
+ await message.reply('Pong!');`,
116
+ language: 'javascript',
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ id: 'discord-js-compatibility',
122
+ slug: 'discord-js-compatibility',
123
+ title: 'Discord.js Compatibility',
124
+ description:
125
+ 'APIs designed to ease migration from Discord.js. Look for the green "Discord.js compatible" badge in guides.',
126
+ category: 'getting-started',
127
+ sections: [
128
+ {
129
+ title: 'Overview',
130
+ description:
131
+ 'erin.js provides Discord.js-style APIs where it makes sense. Sections marked with the green "Discord.js compatible" badge offer familiar patterns — click the badge to see the full API reference.',
132
+ },
133
+ {
134
+ title: 'member.roles (GuildMemberRoleManager)',
135
+ discordJsCompat: '/docs/classes/GuildMemberRoleManager',
136
+ description:
137
+ 'member.roles is a manager with add(), remove(), set(), and cache. Use member.roles.add(roleId), member.roles.remove(roleId), member.roles.set(roleIds), and member.roles.cache.has(roleId) instead of the old member.addRole() / member.roles.includes() pattern.',
138
+ code: `// Discord.js style
139
+ await member.roles.add(roleId);
140
+ await member.roles.remove(roleId);
141
+ await member.roles.set(['id1', 'id2']);
142
+ if (member.roles.cache.has(roleId)) { ... }`,
143
+ language: 'javascript',
144
+ },
145
+ {
146
+ title: 'guild.members.me',
147
+ discordJsCompat: '/docs/classes/GuildMemberManager',
148
+ description:
149
+ "guild.members.me returns the bot's GuildMember in that guild. Use guild.members.fetchMe() to load it when not cached. Same as Discord.js.",
150
+ code: `const me = guild.members.me ?? await guild.members.fetchMe();
151
+ if (me?.permissions.has(PermissionFlags.BanMembers)) {
152
+ await message.reply('I can ban members here.');
153
+ }`,
154
+ language: 'javascript',
155
+ },
156
+ {
157
+ title: 'Other parity',
158
+ description:
159
+ 'client.channels.cache and client.guilds.cache are compatibility aliases. Collection extends Map with find(), filter(), etc. See the API reference for full details.',
160
+ },
161
+ ],
162
+ },
163
+ {
164
+ id: 'sending-without-reply',
165
+ slug: 'sending-without-reply',
166
+ title: 'Sending Without Reply',
167
+ description:
168
+ 'Send messages to the same channel or to specific channels. Covers message.send(), message.sendTo(), client.channels.send(), and client.channels.resolve().',
169
+ category: 'sending-messages',
170
+ sections: [
171
+ {
172
+ title: 'message.send() vs message.reply()',
173
+ description:
174
+ 'message.reply() sends a message that references another message (shows as a "reply" in Discord). message.send() sends to the same channel with no reference—a regular standalone message.',
175
+ },
176
+ {
177
+ title: 'Sending to the same channel',
178
+ description:
179
+ 'Use message.send() when you want to post in the channel without replying. Same signature as reply(): pass a string or object with content and/or embeds.',
180
+ code: `import { Client, Events } from '@erinjs/core';
181
+
182
+ const client = new Client({ intents: 0 });
183
+
184
+ client.on(Events.MessageCreate, async (message) => {
185
+ if (message.content === '!hello') {
186
+ await message.send('Hello! This is a regular message, not a reply.');
187
+ }
188
+ });
189
+
190
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
191
+ language: 'javascript',
192
+ },
193
+ {
194
+ title: 'Sending to a specific channel (e.g. logging)',
195
+ description:
196
+ 'Use message.sendTo(channelId, payload) to send to another channel—handy for logging, announcements, or forwarding. You only need the target channel ID.',
197
+ code: `import { Client, Events, EmbedBuilder } from '@erinjs/core';
198
+
199
+ const client = new Client({ intents: 0 });
200
+ const LOG_CHANNEL_ID = process.env.LOG_CHANNEL_ID; // Your log channel's snowflake
201
+
202
+ client.on(Events.MessageCreate, async (message) => {
203
+ if (message.content === '!report' && message.guildId && LOG_CHANNEL_ID) {
204
+ const embed = new EmbedBuilder()
205
+ .setTitle('User report')
206
+ .setDescription(message.content)
207
+ .addFields(
208
+ { name: 'Author', value: message.author.username, inline: true },
209
+ { name: 'Channel', value: \`<#\${message.channelId}>\`, inline: true }
210
+ )
211
+ .setTimestamp();
212
+
213
+ await message.sendTo(LOG_CHANNEL_ID, { embeds: [embed] });
214
+ await message.send('Report logged.');
215
+ }
216
+ });
217
+
218
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
219
+ language: 'javascript',
220
+ },
221
+ {
222
+ title: 'client.channels.send() — send by channel ID',
223
+ description:
224
+ 'Use client.channels.send(channelId, payload) when you have a channel ID. Works even if the channel is not cached. No need to fetch first when you only need to send.',
225
+ code: `import { Client, Events } from '@erinjs/core';
226
+
227
+ const client = new Client({ intents: 0 });
228
+ const ANNOUNCE_CHANNEL_ID = process.env.ANNOUNCE_CHANNEL_ID;
229
+
230
+ client.on(Events.Ready, async () => {
231
+ if (ANNOUNCE_CHANNEL_ID) {
232
+ await client.channels.send(ANNOUNCE_CHANNEL_ID, 'Bot is online!');
233
+ }
234
+ });
235
+
236
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
237
+ language: 'javascript',
238
+ },
239
+ {
240
+ title: 'client.channels.resolve() — get channel by ID',
241
+ description:
242
+ 'Resolve a channel by ID from cache or API. Use channel.canSendMessage() or channel.isTextBased() before sending. For sending when you only have an ID, prefer client.channels.send() which skips the fetch.',
243
+ code: `import { Client } from '@erinjs/core';
244
+
245
+ const client = new Client({ intents: 0 });
246
+ await client.login(process.env.FLUXER_BOT_TOKEN);
247
+
248
+ // Fetch channel (from API if not cached)
249
+ const channel = await client.channels.resolve(channelId);
250
+ if (channel?.canSendMessage()) {
251
+ await channel.send('Hello!');
252
+ }
253
+ // Or for webhooks: if (channel?.createWebhook) { ... }`,
254
+ language: 'javascript',
255
+ },
256
+ {
257
+ title: 'fetch message by id',
258
+ description:
259
+ 'Use channel.messages.fetch(messageId) when you have the channel. For IDs-only, fetch the channel first.',
260
+ code: `// When you have the channel
261
+ const message = await channel.messages.fetch(messageId);
262
+ if (message) {
263
+ await message.edit({ content: 'Updated!' });
264
+ await message.react('👍');
265
+ }
266
+
267
+ // When you only have IDs (e.g. from sqlite)
268
+ const ch = await client.channels.resolve(channelId);
269
+ const msg = await ch?.messages?.fetch(messageId);
270
+ if (msg) await msg.delete();
271
+
272
+ // When channel is cached
273
+ const m = client.channels.get(channelId);
274
+ if (m?.canSendMessage()) {
275
+ const mes = await m.messages.fetch(messageId);
276
+ if (mes) await mes.edit({ content: 'Edited!' });
277
+ }
278
+
279
+ // Refresh a stale message instance
280
+ const updated = await message.fetch();
281
+ if (updated) console.log(updated.content);`,
282
+ language: 'javascript',
283
+ },
284
+ {
285
+ title: 'message.channel and message.channel.send()',
286
+ description:
287
+ 'message.channel returns the channel (from cache); null if not cached. Messages only exist in text-based channels, so when non-null it always has send(). Use message.channel.send() for the same as message.send() but via the channel object.',
288
+ code: `client.on(Events.MessageCreate, async (message) => {
289
+ const channel = message.channel; // TextChannel | DMChannel | GuildChannel | null
290
+ const guild = message.guild; // Guild | null (null for DMs)
291
+ if (channel) {
292
+ await channel.send('Same channel, different API'); // or message.send()
293
+ }
294
+ });`,
295
+ language: 'javascript',
296
+ },
297
+ {
298
+ title: 'channel.canSendMessage() — permission check',
299
+ description:
300
+ 'Before sending, use canSendMessage() to check if the bot has ViewChannel and SendMessages. For DMs always true; for guild channels uses guild.members.me permissions.',
301
+ code: `const channel = await client.channels.resolve(channelId);
302
+ if (channel?.canSendMessage()) {
303
+ await channel.send('Hello!');
304
+ }`,
305
+ language: 'javascript',
306
+ },
307
+ {
308
+ title: 'Typing indicator',
309
+ description:
310
+ 'Use channel.sendTyping() before a slow operation so users see "Bot is typing...". Lasts ~10 seconds.',
311
+ code: `const channel = message.channel ?? (await message.resolveChannel());
312
+ if (channel?.canSendMessage?.()) {
313
+ await channel.sendTyping();
314
+ await slowOperation(); // e.g. fetch external API
315
+ await message.reply('Done!');
316
+ }`,
317
+ language: 'javascript',
318
+ },
319
+ {
320
+ title: 'Quick reference',
321
+ code: `// Same channel, no reply
322
+ await message.send('Pong!');
323
+
324
+ // Reply to the message
325
+ await message.reply('Pong!');
326
+
327
+ // Send to a specific channel
328
+ await message.sendTo(logChannelId, 'User joined!');
329
+ await client.channels.send(channelId, 'New update available!');`,
330
+ language: 'javascript',
331
+ },
332
+ ],
333
+ },
334
+ {
335
+ id: 'embeds',
336
+ slug: 'embeds',
337
+ title: 'Embeds',
338
+ description:
339
+ 'Complete reference for EmbedBuilder: title, description, author, footer, fields, color, media, and more.',
340
+ category: 'sending-messages',
341
+ sections: [
342
+ {
343
+ title: 'Overview',
344
+ description:
345
+ 'Use EmbedBuilder to create rich embeds. EmbedBuilder instances are auto-converted—no need to call .toJSON() when passing to reply(), send(), or edit(). An embed must have at least one of: title, description, fields, or image/thumbnail. A description-only embed (no title) is valid.',
346
+ },
347
+ {
348
+ title: 'Basic embed',
349
+ description: 'Minimal embed with title, description, color, fields, footer, and timestamp.',
350
+ code: `import { Client, Events, EmbedBuilder } from '@erinjs/core';
351
+
352
+ const client = new Client({ intents: 0 });
353
+
354
+ client.on(Events.MessageCreate, async (message) => {
355
+ if (message.content === '!embed') {
356
+ const embed = new EmbedBuilder()
357
+ .setTitle('Hello!')
358
+ .setDescription('This is a Fluxer embed.')
359
+ .setColor(0x5865f2)
360
+ .addFields(
361
+ { name: 'Field 1', value: 'Value 1', inline: true },
362
+ { name: 'Field 2', value: 'Value 2', inline: true }
363
+ )
364
+ .setFooter({ text: 'Powered by erin.js' })
365
+ .setTimestamp();
366
+
367
+ await message.reply({ embeds: [embed] });
368
+ }
369
+ });
370
+
371
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
372
+ language: 'javascript',
373
+ },
374
+ {
375
+ title: 'Title, Description, and URL',
376
+ description:
377
+ 'setTitle() and setDescription() accept strings (max 256 and 4096 chars). setURL() makes the title a clickable link.',
378
+ code: `const embed = new EmbedBuilder()
379
+ .setTitle('Clickable Title')
380
+ .setDescription('Main body text here.')
381
+ .setURL('https://example.com');`,
382
+ language: 'javascript',
383
+ },
384
+ {
385
+ title: 'Color',
386
+ description:
387
+ 'setColor() accepts: number (0x5865f2), hex string ("#5865f2"), or [r, g, b] array.',
388
+ code: `embed.setColor(0x5865f2);
389
+ embed.setColor('#57f287');
390
+ embed.setColor([88, 101, 242]);`,
391
+ language: 'javascript',
392
+ },
393
+ {
394
+ title: 'Author',
395
+ description: 'setAuthor() adds a header line with name. Optional: iconURL, url.',
396
+ code: `embed.setAuthor({
397
+ name: 'erin.js',
398
+ iconURL: 'https://example.com/icon.png',
399
+ url: 'https://example.com',
400
+ });`,
401
+ language: 'javascript',
402
+ },
403
+ {
404
+ title: 'Footer',
405
+ description: 'setFooter() adds text at the bottom. Optional: iconURL.',
406
+ code: `embed.setFooter({
407
+ text: 'Powered by erin.js',
408
+ iconURL: 'https://example.com/footer-icon.png',
409
+ });`,
410
+ language: 'javascript',
411
+ },
412
+ {
413
+ title: 'Timestamp',
414
+ description:
415
+ 'setTimestamp() shows a date. Omit or pass null for current time. Pass Date or number (ms) for a specific time.',
416
+ code: `embed.setTimestamp(); // current time
417
+ embed.setTimestamp(new Date('2026-01-01'));
418
+ embed.setTimestamp(Date.now() - 3600000); // 1 hour ago`,
419
+ language: 'javascript',
420
+ },
421
+ {
422
+ title: 'Fields',
423
+ description:
424
+ 'addFields() adds name/value pairs. Max 25 fields. Use inline: true for side-by-side layout. spliceFields() to insert/remove.',
425
+ code: `embed.addFields(
426
+ { name: 'Field 1', value: 'Value 1', inline: true },
427
+ { name: 'Field 2', value: 'Value 2', inline: true },
428
+ { name: 'Long field', value: 'Not inline, full width' }
429
+ );
430
+
431
+ // Insert/replace fields
432
+ embed.spliceFields(1, 1, { name: 'Replaced', value: 'New value' });`,
433
+ language: 'javascript',
434
+ },
435
+ {
436
+ title: 'Image and Thumbnail',
437
+ description:
438
+ 'setImage() adds a large image. setThumbnail() adds a small image (e.g. top-right). Pass a URL string or EmbedMediaOptions (url, width, height, content_type, etc).',
439
+ code: `embed.setImage('https://example.com/image.png');
440
+ embed.setThumbnail('https://example.com/thumb.png');
441
+
442
+ // With metadata
443
+ embed.setImage({
444
+ url: 'https://example.com/image.png',
445
+ width: 400,
446
+ height: 200,
447
+ content_type: 'image/png',
448
+ });`,
449
+ language: 'javascript',
450
+ },
451
+ {
452
+ title: 'Video and Audio',
453
+ description:
454
+ 'setVideo() and setAudio() add video/audio to embeds (Fluxer supports these). Pass URL or EmbedMediaOptions. Include a title when using video. See Embed Media guide for full examples.',
455
+ code: `embed.setVideo('https://example.com/video.mp4');
456
+ embed.setAudio({
457
+ url: 'https://example.com/audio.mp3',
458
+ duration: 120,
459
+ content_type: 'audio/mpeg',
460
+ });`,
461
+ language: 'javascript',
462
+ },
463
+ {
464
+ title: 'Multiple embeds',
465
+ description: 'Messages can include up to 10 embeds. Pass an array to embeds.',
466
+ code: `await message.reply({
467
+ embeds: [
468
+ new EmbedBuilder().setTitle('First').setColor(0x5865f2),
469
+ new EmbedBuilder().setTitle('Second').setColor(0x57f287),
470
+ ],
471
+ });`,
472
+ language: 'javascript',
473
+ },
474
+ {
475
+ title: 'Load from existing embed',
476
+ description:
477
+ 'EmbedBuilder.from() creates a builder from an API embed (e.g. from a received message). Edit and toJSON() to send.',
478
+ code: `const existing = message.embeds[0];
479
+ if (existing) {
480
+ const edited = EmbedBuilder.from(existing)
481
+ .setTitle('Updated title')
482
+ .setColor(0x57f287);
483
+ await message.edit({ embeds: [edited] });
484
+ }`,
485
+ language: 'javascript',
486
+ },
487
+ {
488
+ title: 'Limits',
489
+ description:
490
+ 'Title ≤256, description ≤4096, field name ≤256, field value ≤1024, footer ≤2048, author name ≤256. Max 25 fields. Combined title+description+fields+footer ≤6000 chars.',
491
+ },
492
+ ],
493
+ },
494
+ {
495
+ id: 'editing-embeds',
496
+ slug: 'editing-embeds',
497
+ title: 'Editing Embeds',
498
+ description: 'Edit existing message embeds with message.edit().',
499
+ category: 'sending-messages',
500
+ sections: [
501
+ {
502
+ title: 'Overview',
503
+ description:
504
+ 'The Fluxer API supports editing existing messages via PATCH. You can update the message content, embeds, or both. Only the message author (or admins with proper permissions) can edit messages.',
505
+ },
506
+ {
507
+ title: 'Edit Content',
508
+ description: 'Update the text content of a message you sent.',
509
+ code: `const reply = await message.reply('Initial message');
510
+ await reply.edit({ content: 'Updated message!' });`,
511
+ language: 'javascript',
512
+ },
513
+ {
514
+ title: 'Edit Embeds',
515
+ description:
516
+ 'Replace or update embeds on an existing message. Pass an array of EmbedBuilder instances or APIEmbed objects.',
517
+ code: `import { Client, Events, EmbedBuilder } from '@erinjs/core';
518
+
519
+ const client = new Client({ intents: 0 });
520
+
521
+ client.on(Events.MessageCreate, async (message) => {
522
+ if (message.content === '!editembed') {
523
+ const embed = new EmbedBuilder()
524
+ .setTitle('Loading...')
525
+ .setColor(0x5865f2)
526
+ .setTimestamp();
527
+
528
+ const reply = await message.reply({ embeds: [embed] });
529
+
530
+ // Simulate loading, then update the embed
531
+ await new Promise((r) => setTimeout(r, 2000));
532
+
533
+ const updatedEmbed = new EmbedBuilder()
534
+ .setTitle('Done!')
535
+ .setDescription('This embed was edited after 2 seconds.')
536
+ .setColor(0x57f287)
537
+ .setTimestamp();
538
+
539
+ await reply.edit({ embeds: [updatedEmbed] });
540
+ }
541
+ });
542
+
543
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
544
+ language: 'javascript',
545
+ },
546
+ {
547
+ title: 'Edit Content and Embeds Together',
548
+ description: 'You can update both content and embeds in a single edit call.',
549
+ code: `await message.edit({
550
+ content: 'Updated text',
551
+ embeds: [new EmbedBuilder().setTitle('Updated embed').setColor(0x5865f2)],
552
+ });`,
553
+ language: 'javascript',
554
+ },
555
+ {
556
+ title: 'API Reference',
557
+ description:
558
+ 'The edit endpoint is PATCH /channels/{channel_id}/messages/{message_id}. See openapi.json for the full request body schema. The SDK Message.edit() accepts { content?: string; embeds?: (APIEmbed | EmbedBuilder)[] }.',
559
+ },
560
+ ],
561
+ },
562
+ {
563
+ id: 'embed-media',
564
+ slug: 'embed-media',
565
+ title: 'Embed Media',
566
+ description:
567
+ 'Add images, thumbnails, video, and audio to embeds with EmbedBuilder and EmbedMediaOptions.',
568
+ category: 'media',
569
+ sections: [
570
+ {
571
+ title: 'Images and Thumbnails',
572
+ description:
573
+ 'Use setImage() and setThumbnail() with a URL string, or pass full EmbedMediaOptions for width, height, content_type, and other metadata.',
574
+ code: `import { Client, Events, EmbedBuilder } from '@erinjs/core';
575
+
576
+ const client = new Client({ intents: 0 });
577
+
578
+ client.on(Events.MessageCreate, async (message) => {
579
+ if (message.content === '!embedimg') {
580
+ const embed = new EmbedBuilder()
581
+ .setTitle('Image Embed')
582
+ .setDescription('Simple image from URL.')
583
+ .setImage('https://placehold.co/400x200/5865f2/white?text=Image')
584
+ .setThumbnail('https://placehold.co/100x100/57f287/white?text=Thumb')
585
+ .setColor(0x5865f2);
586
+
587
+ await message.reply({ embeds: [embed] });
588
+ }
589
+ });`,
590
+ language: 'javascript',
591
+ },
592
+ {
593
+ title: 'Image with Full Media Options',
594
+ description:
595
+ 'Pass an object to setImage or setThumbnail with url, width, height, content_type, description, placeholder, duration, and flags. Use EmbedMediaFlags.IS_ANIMATED for animated GIFs.',
596
+ code: `const embed = new EmbedBuilder()
597
+ .setTitle('Image with metadata')
598
+ .setDescription('EmbedMediaOptions: width, height')
599
+ .setImage({
600
+ url: 'https://placehold.co/400x200/5865f2/white?text=Image',
601
+ width: 400,
602
+ height: 200,
603
+ content_type: 'image/png',
604
+ })
605
+ .setColor(0x5865f2);`,
606
+ language: 'javascript',
607
+ },
608
+ {
609
+ title: 'GIFs in embeds',
610
+ description:
611
+ 'Embeds require GIF format for animated images (not MP4). Add EmbedMediaFlags.IS_ANIMATED to the flags field. For Tenor URLs, use resolveTenorToImageUrl() to get the GIF URL and flag — see the GIFs (Tenor) guide.',
612
+ },
613
+ {
614
+ title: 'Video in Embeds',
615
+ description:
616
+ 'Use setVideo() to add video to a rich embed. Fluxer supports the .video field. Include a title when using video. Pass a URL or EmbedMediaOptions (e.g. duration for progress bars).',
617
+ code: `const embed = new EmbedBuilder()
618
+ .setTitle('Video embed')
619
+ .setDescription('Rich embed with video field.')
620
+ .setVideo('https://example.com/sample.mp4')
621
+ .setURL('https://example.com/sample.mp4')
622
+ .setColor(0x5865f2);
623
+
624
+ // With full options (duration, dimensions for progress bar):
625
+ const embedWithDuration = new EmbedBuilder()
626
+ .setTitle('Video with metadata')
627
+ .setVideo({
628
+ url: 'https://example.com/video.mp4',
629
+ duration: 120,
630
+ width: 1280,
631
+ height: 720,
632
+ })
633
+ .setColor(0x5865f2);`,
634
+ language: 'javascript',
635
+ },
636
+ {
637
+ title: 'Audio in Embeds',
638
+ description:
639
+ 'Use setAudio() to add audio to an embed. Pass a URL or EmbedMediaOptions (e.g. duration, content_type).',
640
+ code: `const embed = new EmbedBuilder()
641
+ .setTitle('Audio embed')
642
+ .setDescription('Rich embed with audio field.')
643
+ .setAudio({
644
+ url: 'https://example.com/sample.mp3',
645
+ duration: 180,
646
+ content_type: 'audio/mpeg',
647
+ })
648
+ .setColor(0x5865f2);`,
649
+ language: 'javascript',
650
+ },
651
+ ],
652
+ },
653
+ {
654
+ id: 'gifs',
655
+ slug: 'gifs',
656
+ title: 'GIFs (Tenor)',
657
+ description:
658
+ 'Send Tenor GIFs as content (gifv) or in embeds using resolveTenorToImageUrl() for GIF URLs.',
659
+ category: 'media',
660
+ sections: [
661
+ {
662
+ title: 'How Tenor GIFs Work',
663
+ description:
664
+ 'Tenor embeds are created by the Fluxer unfurler when you send a Tenor URL as message content. Do not use custom embeds for Tenor GIFs—the API turns the URL into a type: "gifv" embed.',
665
+ },
666
+ {
667
+ title: 'Send a Tenor GIF',
668
+ description:
669
+ 'Send the Tenor URL as content. No embeds needed. The unfurler detects the URL and creates the gifv embed.',
670
+ code: `import { Client, Events } from '@erinjs/core';
671
+
672
+ const client = new Client({ intents: 0 });
673
+
674
+ client.on(Events.MessageCreate, async (message) => {
675
+ if (message.content === '!gif') {
676
+ const tenorUrl = 'https://tenor.com/view/stressed-gif-7048057395502071840';
677
+ await message.reply({ content: tenorUrl });
678
+ }
679
+ });`,
680
+ language: 'javascript',
681
+ },
682
+ {
683
+ title: 'Tenor URL in an embed',
684
+ description:
685
+ 'Tenor page URLs do not work as setImage() URLs. Use resolveTenorToImageUrl() to fetch the Tenor page or oEmbed, derive the GIF URL (embeds require GIF, not MP4), and return { url, flags: IS_ANIMATED }. For full gifv embeds, send the Tenor URL as content.',
686
+ code: `import { EmbedBuilder, resolveTenorToImageUrl } from '@erinjs/core';
687
+
688
+ const tenorUrl = 'https://tenor.com/view/stressed-gif-7048057395502071840';
689
+ const media = await resolveTenorToImageUrl(tenorUrl);
690
+ if (media) {
691
+ const embed = new EmbedBuilder()
692
+ .setTitle('Tenor in embed')
693
+ .setDescription('GIF URL + IS_ANIMATED flag')
694
+ .setImage(media)
695
+ .setColor(0x5865f2);
696
+ await message.reply({ embeds: [embed] });
697
+ }`,
698
+ language: 'javascript',
699
+ },
700
+ {
701
+ title: 'Important',
702
+ description:
703
+ 'Custom embeds cannot create gifv embeds. For full animated gifv, send the Tenor URL as content. resolveTenorToImageUrl() returns GIF URL + IS_ANIMATED (derived from media.tenor.com path).',
704
+ },
705
+ ],
706
+ },
707
+ {
708
+ id: 'attachments',
709
+ slug: 'attachments',
710
+ title: 'File Attachments',
711
+ description:
712
+ 'Upload files with messages and set attachment metadata (title, description, flags for spoiler, animated, explicit).',
713
+ category: 'media',
714
+ sections: [
715
+ {
716
+ title: 'Basic File Upload',
717
+ description:
718
+ 'Pass files in your send options. Each file needs a name and data (Buffer, Blob, Uint8Array). Use with message.reply(), message.send(), or channel.send().',
719
+ code: `import { Client, Events } from '@erinjs/core';
720
+ import { readFileSync } from 'fs';
721
+
722
+ const client = new Client({ intents: 0 });
723
+
724
+ client.on(Events.MessageCreate, async (message) => {
725
+ if (message.content === '!file') {
726
+ const data = Buffer.from('Hello from Fluxer!', 'utf-8');
727
+ await message.reply({
728
+ content: 'Here is a file:',
729
+ files: [{ name: 'hello.txt', data }],
730
+ });
731
+ }
732
+ });`,
733
+ language: 'javascript',
734
+ },
735
+ {
736
+ title: 'Attachment Metadata',
737
+ description:
738
+ 'When using files, you can pass attachments to set metadata per file: filename, title, description, and flags. The id in each attachment matches the file index (0, 1, 2...).',
739
+ code: `import { MessageAttachmentFlags } from '@erinjs/core';
740
+
741
+ await message.reply({
742
+ content: 'Spoiler image:',
743
+ files: [{ name: 'secret.png', data: imageBuffer }],
744
+ attachments: [
745
+ {
746
+ id: 0,
747
+ filename: 'secret.png',
748
+ title: 'Hidden image',
749
+ flags: MessageAttachmentFlags.IS_SPOILER,
750
+ },
751
+ ],
752
+ });`,
753
+ language: 'javascript',
754
+ },
755
+ {
756
+ title: 'Attachment Flags',
757
+ description:
758
+ 'MessageAttachmentFlags: IS_SPOILER (8) blurs until clicked, CONTAINS_EXPLICIT_MEDIA (16) for explicit content, IS_ANIMATED (32) for GIFs and animated WebP. Combine with bitwise OR.',
759
+ code: `import { MessageAttachmentFlags } from '@erinjs/core';
760
+
761
+ // Spoiler (blurred until clicked)
762
+ flags: MessageAttachmentFlags.IS_SPOILER
763
+
764
+ // Animated image (GIF, animated WebP)
765
+ flags: MessageAttachmentFlags.IS_ANIMATED
766
+
767
+ // Combine flags
768
+ flags: MessageAttachmentFlags.IS_SPOILER | MessageAttachmentFlags.IS_ANIMATED`,
769
+ language: 'javascript',
770
+ },
771
+ ],
772
+ },
773
+ {
774
+ id: 'attachments-by-url',
775
+ slug: 'attachments-by-url',
776
+ title: 'File Attachments by URL',
777
+ description:
778
+ 'Attach files by passing a URL instead of buffer data. The SDK fetches the URL and uploads it as a normal attachment.',
779
+ category: 'media',
780
+ sections: [
781
+ {
782
+ title: 'Using a URL',
783
+ description:
784
+ 'Pass { name, url } in the files array. The SDK fetches the URL (30s timeout), validates it with URL.canParse(), and uploads the result. Works with channel.send(), message.reply(), message.send(), webhook.send(), and client.channels.send().',
785
+ code: `import { Client, Events } from '@erinjs/core';
786
+
787
+ const client = new Client({ intents: 0 });
788
+
789
+ client.on(Events.MessageCreate, async (message) => {
790
+ if (message.content === '!attachurl') {
791
+ await message.reply({
792
+ content: 'Image from URL:',
793
+ files: [
794
+ {
795
+ name: 'image.png',
796
+ url: 'https://example.com/image.png',
797
+ },
798
+ ],
799
+ });
800
+ }
801
+ });
802
+
803
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
804
+ language: 'javascript',
805
+ },
806
+ {
807
+ title: 'Mixing buffers and URLs',
808
+ description:
809
+ 'You can combine file data and URLs in the same message. Order is preserved; attachments metadata id matches the file index.',
810
+ code: `await message.reply({
811
+ content: 'Two files:',
812
+ files: [
813
+ { name: 'local.txt', data: Buffer.from('Hello') },
814
+ { name: 'remote.png', url: 'https://example.com/logo.png' },
815
+ ],
816
+ });`,
817
+ language: 'javascript',
818
+ },
819
+ {
820
+ title: 'Optional filename override',
821
+ description:
822
+ 'Use filename to control the displayed attachment name independently from the local name used during upload.',
823
+ code: `files: [
824
+ {
825
+ name: 'fetched-image.png',
826
+ url: 'https://example.com/image.jpg',
827
+ filename: 'custom-display.png',
828
+ },
829
+ ]`,
830
+ language: 'javascript',
831
+ },
832
+ ],
833
+ },
834
+ {
835
+ id: 'profile-urls',
836
+ slug: 'profile-urls',
837
+ title: 'Profile URLs',
838
+ description:
839
+ 'Get avatar, banner, and other CDN URLs easily with User/Webhook/GuildMember methods or standalone CDN helpers for raw API data.',
840
+ category: 'media',
841
+ sections: [
842
+ {
843
+ title: 'User avatar and banner',
844
+ description:
845
+ 'When you have a User object (e.g. message.author), use avatarURL(), displayAvatarURL(), and bannerURL(). These handle animated avatars (a_ prefix) and default fallbacks.',
846
+ code: `import { Client, Events, EmbedBuilder } from '@erinjs/core';
847
+
848
+ const client = new Client({ intents: 0 });
849
+
850
+ client.on(Events.MessageCreate, async (message) => {
851
+ if (message.content === '!avatar') {
852
+ const user = message.author;
853
+ // avatarURL() returns null if no custom avatar; displayAvatarURL() uses default
854
+ const avatarUrl = user.displayAvatarURL({ size: 256 });
855
+ const bannerUrl = user.bannerURL({ size: 512 });
856
+
857
+ const embed = new EmbedBuilder()
858
+ .setTitle(\`\${user.username}'s profile\`)
859
+ .setThumbnail(avatarUrl)
860
+ .setColor(user.avatarColor ?? 0x5865f2);
861
+ if (bannerUrl) embed.setImage(bannerUrl);
862
+ await message.reply({ embeds: [embed] });
863
+ }
864
+ });`,
865
+ language: 'javascript',
866
+ },
867
+ {
868
+ title: 'Raw API data: CDN helpers',
869
+ description:
870
+ 'When you have raw API data (e.g. from client.rest.get(Routes.user(id))), use the standalone CDN helpers. They work with id + hash and support size and extension options.',
871
+ code: `import { cdnAvatarURL, cdnBannerURL } from '@erinjs/core';
872
+
873
+ // From REST response
874
+ const userData = await client.rest.get(Routes.user(userId));
875
+ const avatarUrl = cdnAvatarURL(userData.id, userData.avatar, { size: 256 });
876
+ const bannerUrl = cdnBannerURL(userData.id, profile?.banner ?? null, { size: 512 });
877
+
878
+ // Or use User: client.getOrCreateUser(userData) then user.displayAvatarURL()
879
+ const user = client.getOrCreateUser(userData);
880
+ const avatarUrl2 = user.displayAvatarURL({ size: 256 });`,
881
+ language: 'javascript',
882
+ },
883
+ {
884
+ title: 'Guild member and webhook avatars',
885
+ description:
886
+ 'GuildMember has displayAvatarURL() (guild avatar or fallback to user) and bannerURL(). Webhook has avatarURL().',
887
+ code: `// Member avatar (guild-specific or user fallback)
888
+ const memberAvatar = member.displayAvatarURL({ size: 128 });
889
+
890
+ // Webhook avatar
891
+ const webhookAvatar = webhook.avatarURL({ size: 64 });`,
892
+ language: 'javascript',
893
+ },
894
+ ],
895
+ },
896
+ {
897
+ id: 'reactions',
898
+ slug: 'reactions',
899
+ title: 'Reactions',
900
+ description:
901
+ 'Add, remove, and listen for message reactions with Message.react(), removeReaction(), and reaction events.',
902
+ category: 'sending-messages',
903
+ sections: [
904
+ {
905
+ title: 'Add a Reaction',
906
+ description:
907
+ 'Use message.react() to add an emoji reaction as the bot. Pass a unicode emoji string or custom emoji { name, id }.',
908
+ code: `const reply = await message.reply('React to this!');
909
+ await reply.react('👍');
910
+ await reply.react({ name: 'customemoji', id: '123456789012345678' });`,
911
+ language: 'javascript',
912
+ },
913
+ {
914
+ title: 'Remove Reactions',
915
+ description:
916
+ "Remove the bot's reaction with removeReaction(emoji). Remove a specific user's reaction with removeReaction(emoji, userId). Clear all reactions with removeAllReactions() or removeReactionEmoji(emoji).",
917
+ code: `// Remove the bot's reaction
918
+ await message.removeReaction('👍');
919
+
920
+ // Remove a specific user's reaction (requires moderator permissions)
921
+ await message.removeReaction('👍', userId);
922
+
923
+ // Remove all reactions of one emoji from the message
924
+ await message.removeReactionEmoji('👍');
925
+
926
+ // Remove all reactions from the message
927
+ await message.removeAllReactions();`,
928
+ language: 'javascript',
929
+ },
930
+ {
931
+ title: 'Listen for Reactions',
932
+ description:
933
+ 'MessageReactionAdd and MessageReactionRemove emit (reaction, user, messageId, channelId, emoji, userId). Use client.on(Events.X, handler) or client.events.MessageReactionAdd(handler).',
934
+ code: `client.on(Events.MessageReactionAdd, async (reaction, user, messageId, channelId, emoji, userId) => {
935
+ if (emoji.name === '👍') {
936
+ console.log(\`User \${userId} voted yes on message \${messageId}\`);
937
+ const message = await reaction.fetchMessage();
938
+ if (message) await message.react('✅');
939
+ }
940
+ });
941
+
942
+ client.on(Events.MessageReactionRemove, (reaction, user, messageId, channelId, emoji, userId) => {
943
+ console.log(\`User \${userId} removed \${emoji.name} from message \${messageId}\`);
944
+ });`,
945
+ language: 'javascript',
946
+ },
947
+ {
948
+ title: 'Reaction Roles Example',
949
+ description:
950
+ 'See examples/reaction-roles-bot.js for a full bot that assigns roles when users react to a message. Uses (reaction, user), Guild.fetchMember(), member.roles.add() (Discord.js parity), and guild.createRole() if you need to create roles programmatically. See the Roles guide for role CRUD.',
951
+ discordJsCompat: '/docs/classes/GuildMemberRoleManager',
952
+ code: `// Simplified reaction-roles logic
953
+ client.on(Events.MessageReactionAdd, async (reaction, user) => {
954
+ if (!reaction.guildId || reaction.messageId !== rolesMessageId) return;
955
+ const roleId = ROLE_EMOJI_MAP[reaction.emoji.name];
956
+ if (!roleId) return;
957
+ const guild = client.guilds.get(reaction.guildId);
958
+ const member = await guild?.fetchMember(user.id);
959
+ if (member && !member.roles.cache.has(roleId)) await member.roles.add(roleId);
960
+ });`,
961
+ language: 'javascript',
962
+ },
963
+ ],
964
+ },
965
+ {
966
+ id: 'webhooks',
967
+ slug: 'webhooks',
968
+ title: 'Webhooks',
969
+ description:
970
+ 'A complete guide to Discord webhooks—sending messages without a gateway, creating, editing, and managing webhooks.',
971
+ category: 'webhooks',
972
+ sections: [
973
+ {
974
+ title: 'What are Webhooks?',
975
+ description:
976
+ 'Webhooks let you send messages to a channel using a URL (ID + token). You can use them in scripts, CI pipelines, or anywhere you need to post without a full bot connection. No gateway, no events—just REST.',
977
+ },
978
+ {
979
+ title: 'Webhooks Without a Bot',
980
+ description:
981
+ 'A Client with intents: 0 is enough. No need to connect to the gateway or handle events. Ideal for scripts or one-off sends.',
982
+ code: `import { Client, Webhook } from '@erinjs/core';
983
+
984
+ const client = new Client({ intents: 0 });
985
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
986
+ await webhook.send('Message from a script!');`,
987
+ language: 'javascript',
988
+ },
989
+ {
990
+ title: 'Creating a Webhook',
991
+ description:
992
+ 'Create a webhook on a text channel. Requires Manage Webhooks permission. The token is returned only when creating—store it securely. It will never be returned when listing or fetching.',
993
+ code: `import { Client } from '@erinjs/core';
994
+
995
+ const client = new Client({ intents: 0 });
996
+ await client.login(process.env.FLUXER_BOT_TOKEN);
997
+
998
+ const channel = client.channels.get(channelId);
999
+ if (!channel?.createWebhook) throw new Error('Channel does not support webhooks');
1000
+
1001
+ const webhook = await channel.createWebhook({ name: 'My Webhook' });
1002
+ console.log(webhook.id, webhook.token); // Store token—it won't be returned when listing`,
1003
+ language: 'javascript',
1004
+ },
1005
+ {
1006
+ title: 'Sending Messages',
1007
+ description:
1008
+ 'Send text, embeds, or both. You can override the username and avatar for each message.',
1009
+ code: `import { Client, Webhook, EmbedBuilder } from '@erinjs/core';
1010
+
1011
+ const client = new Client({ intents: 0 });
1012
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
1013
+
1014
+ await webhook.send({
1015
+ content: 'Hello from webhook!',
1016
+ embeds: [
1017
+ new EmbedBuilder()
1018
+ .setTitle('Webhook Message')
1019
+ .setColor(0x5865f2)
1020
+ .setTimestamp(),
1021
+ ],
1022
+ username: 'Custom Name',
1023
+ avatar_url: 'https://example.com/avatar.png',
1024
+ });`,
1025
+ language: 'javascript',
1026
+ },
1027
+ {
1028
+ title: 'Simple text only',
1029
+ code: `await webhook.send('Plain text message');`,
1030
+ language: 'javascript',
1031
+ },
1032
+ {
1033
+ title: 'Embeds without a title',
1034
+ description:
1035
+ 'Embeds can use only a description—no title required. At least one of title, description, fields, or image is needed.',
1036
+ code: `await webhook.send({
1037
+ embeds: [
1038
+ new EmbedBuilder()
1039
+ .setDescription('Description-only embed works.')
1040
+ .setColor(0x5865f2),
1041
+ ],
1042
+ });`,
1043
+ language: 'javascript',
1044
+ },
1045
+ {
1046
+ title: 'Fetching & Listing Webhooks',
1047
+ description:
1048
+ 'Fetch by ID or list channel/guild webhooks. Requires a logged-in bot. Fetched webhooks have no token and cannot send—but you can edit or delete them with bot auth.',
1049
+ code: `import { Client, Webhook } from '@erinjs/core';
1050
+
1051
+ const client = new Client({ intents: 0 });
1052
+ await client.login(process.env.FLUXER_BOT_TOKEN);
1053
+
1054
+ // Fetch single webhook (no token)
1055
+ const webhook = await Webhook.fetch(client, webhookId);
1056
+
1057
+ // List channel webhooks
1058
+ const channel = client.channels.get(channelId);
1059
+ const channelWebhooks = await channel?.fetchWebhooks() ?? [];
1060
+
1061
+ // List guild webhooks
1062
+ const guild = client.guilds.get(guildId);
1063
+ const guildWebhooks = await guild?.fetchWebhooks() ?? [];`,
1064
+ language: 'javascript',
1065
+ },
1066
+ {
1067
+ title: 'Editing a Webhook',
1068
+ description:
1069
+ 'Use webhook.edit() to change name, avatar, or (with bot auth) channel. With a token (e.g. from createWebhook or fromToken), you can update name and avatar. Without a token (fetched webhook), bot auth lets you also change the target channel.',
1070
+ code: `import { Client, Webhook } from '@erinjs/core';
1071
+
1072
+ const client = new Client({ intents: 0 });
1073
+ await client.login(process.env.FLUXER_BOT_TOKEN);
1074
+
1075
+ // With token (name and avatar only)
1076
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
1077
+ await webhook.edit({ name: 'New Name', avatar: null });
1078
+ // avatar: null clears the webhook avatar
1079
+
1080
+ // With bot auth (fetched webhook — can also move to another channel)
1081
+ const fetched = await Webhook.fetch(client, webhookId);
1082
+ await fetched.edit({
1083
+ name: 'Updated Name',
1084
+ channel_id: newChannelId, // move webhook to different channel
1085
+ });`,
1086
+ language: 'javascript',
1087
+ },
1088
+ {
1089
+ title: 'Deleting a Webhook',
1090
+ code: `const webhook = await Webhook.fetch(client, webhookId);
1091
+ await webhook.delete();`,
1092
+ language: 'javascript',
1093
+ },
1094
+ ],
1095
+ },
1096
+ {
1097
+ id: 'webhook-attachments-embeds',
1098
+ slug: 'webhook-attachments-embeds',
1099
+ title: 'Webhook Attachments & Embeds',
1100
+ description:
1101
+ 'Send embeds with or without a title, and attach files to webhook messages—same API as channel messages.',
1102
+ category: 'webhooks',
1103
+ sections: [
1104
+ {
1105
+ title: 'Overview',
1106
+ description:
1107
+ 'Webhooks support rich embeds and file attachments. Embeds can have just a description (no title required), and you can attach files the same way as with channel.send or message.reply.',
1108
+ },
1109
+ {
1110
+ title: 'Embeds Without a Title',
1111
+ description:
1112
+ 'You do not need a title for embeds to work. At least one of title, description, fields, or image/thumbnail is required. A description-only embed is valid.',
1113
+ code: `import { Client, Webhook, EmbedBuilder } from '@erinjs/core';
1114
+
1115
+ const client = new Client({ intents: 0 });
1116
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
1117
+
1118
+ // Description only—no title
1119
+ await webhook.send({
1120
+ embeds: [
1121
+ new EmbedBuilder()
1122
+ .setDescription('This embed has no title. Description-only works fine.')
1123
+ .setColor(0x5865f2)
1124
+ .setTimestamp(),
1125
+ ],
1126
+ });`,
1127
+ language: 'javascript',
1128
+ },
1129
+ {
1130
+ title: 'Direct Attachments',
1131
+ description:
1132
+ 'Attach files to webhook messages using the files array. Each file needs name and data (Blob, ArrayBuffer, or Uint8Array). Optional filename overrides the display name.',
1133
+ code: `import { Client, Webhook } from '@erinjs/core';
1134
+ import { readFileSync } from 'fs';
1135
+
1136
+ const client = new Client({ intents: 0 });
1137
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
1138
+
1139
+ const buffer = readFileSync('report.pdf');
1140
+ await webhook.send({
1141
+ content: 'Report attached',
1142
+ files: [
1143
+ { name: 'report.pdf', data: buffer },
1144
+ { name: 'log.txt', data: new TextEncoder().encode('Log content'), filename: 'log-2025.txt' },
1145
+ ],
1146
+ });`,
1147
+ language: 'javascript',
1148
+ },
1149
+ {
1150
+ title: 'Full Example: Embed + Files',
1151
+ description:
1152
+ 'Combine content, description-only embed, and file attachments in a single webhook send.',
1153
+ code: `import { Client, Webhook, EmbedBuilder } from '@erinjs/core';
1154
+ import { readFileSync } from 'fs';
1155
+
1156
+ const client = new Client({ intents: 0 });
1157
+ const webhook = Webhook.fromToken(client, webhookId, webhookToken);
1158
+
1159
+ await webhook.send({
1160
+ content: 'Build completed',
1161
+ embeds: [
1162
+ new EmbedBuilder()
1163
+ .setDescription('Deploy succeeded. See attachment for logs.')
1164
+ .setColor(0x57f287)
1165
+ .setTimestamp(),
1166
+ ],
1167
+ files: [{ name: 'deploy.log', data: readFileSync('deploy.log') }],
1168
+ username: 'CI Bot',
1169
+ });`,
1170
+ language: 'javascript',
1171
+ },
1172
+ ],
1173
+ },
1174
+ {
1175
+ id: 'voice',
1176
+ slug: 'voice',
1177
+ title: 'Voice',
1178
+ description:
1179
+ 'Join voice channels and play audio with @erinjs/voice. Supports WebM/Opus streams—no FFmpeg required.',
1180
+ category: 'voice',
1181
+ sections: [
1182
+ {
1183
+ title: 'Installation',
1184
+ description: 'Add the voice package alongside the core library.',
1185
+ code: `pnpm add @erinjs/voice @erinjs/core`,
1186
+ language: 'bash',
1187
+ },
1188
+ {
1189
+ title: 'Setup',
1190
+ description:
1191
+ 'Create a VoiceManager before login so it receives VoiceStatesSync from READY/GUILD_CREATE. This lets the manager see users already in voice when the bot starts.',
1192
+ code: `import { Client, Events, VoiceChannel } from '@erinjs/core';
1193
+ import { getVoiceManager } from '@erinjs/voice';
1194
+
1195
+ const client = new Client({ intents: 0 });
1196
+ getVoiceManager(client); // Must be before login
1197
+
1198
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1199
+ language: 'javascript',
1200
+ },
1201
+ {
1202
+ title: 'Join a Voice Channel',
1203
+ description:
1204
+ "Get the user's voice channel with getVoiceChannelId, then join. The connection resolves when ready.",
1205
+ code: `const voiceManager = getVoiceManager(client);
1206
+ const voiceChannelId = voiceManager.getVoiceChannelId(guildId, userId);
1207
+ if (!voiceChannelId) return; // User not in voice
1208
+
1209
+ const channel = client.channels.get(voiceChannelId);
1210
+ if (!(channel instanceof VoiceChannel)) return;
1211
+
1212
+ const connection = await voiceManager.join(channel);`,
1213
+ language: 'javascript',
1214
+ },
1215
+ {
1216
+ title: 'Play Audio',
1217
+ description:
1218
+ 'Play a WebM/Opus URL or stream. The voice package does not use FFmpeg—input must be WebM with Opus. Use yt-dlp or similar to get direct stream URLs from YouTube.',
1219
+ code: `// URL (fetched and demuxed automatically)
1220
+ await connection.play('https://example.com/audio.webm');
1221
+
1222
+ // Or a Node.js ReadableStream of Opus
1223
+ await connection.play(opusStream);`,
1224
+ language: 'javascript',
1225
+ },
1226
+ {
1227
+ title: 'Getting Stream URLs from YouTube',
1228
+ description: 'Use youtube-dl-exec or yt-dlp to extract a WebM/Opus URL.',
1229
+ code: `import youtubedl from 'youtube-dl-exec';
1230
+
1231
+ const result = await youtubedl(videoUrl, {
1232
+ getUrl: true,
1233
+ f: 'bestaudio[ext=webm][acodec=opus]/bestaudio[ext=webm]/bestaudio',
1234
+ }, { timeout: 15000 });
1235
+
1236
+ const streamUrl = String(result ?? '').trim();
1237
+ await connection.play(streamUrl);`,
1238
+ language: 'javascript',
1239
+ },
1240
+ {
1241
+ title: 'Volume Control',
1242
+ description:
1243
+ 'LiveKitRtcConnection supports setVolume(0-200) and getVolume(). 100 = normal, 50 = half, 200 = double. Affects current and future playback.',
1244
+ code: `import { LiveKitRtcConnection } from '@erinjs/voice';
1245
+
1246
+ if (connection instanceof LiveKitRtcConnection) {
1247
+ connection.setVolume(80); // 80% volume
1248
+ console.log('Current volume:', connection.getVolume());
1249
+ }`,
1250
+ language: 'javascript',
1251
+ },
1252
+ {
1253
+ title: 'Stop and Leave',
1254
+ description:
1255
+ 'Stop playback and disconnect. getConnection accepts channel ID or guild ID. leave(guildId) leaves all channels; leaveChannel(channelId) leaves a specific channel.',
1256
+ code: `// By channel ID (primary) or guild ID
1257
+ const connection = voiceManager.getConnection(channelId) ?? voiceManager.getConnection(guildId);
1258
+ connection?.stop();
1259
+ if (connection) voiceManager.leaveChannel(connection.channel.id);
1260
+ // Or leave all channels in the guild:
1261
+ voiceManager.leave(guildId);`,
1262
+ language: 'javascript',
1263
+ },
1264
+ {
1265
+ title: 'LiveKit and serverLeave',
1266
+ description:
1267
+ 'If using LiveKit, the server may emit serverLeave. Listen and reconnect if needed.',
1268
+ code: `connection.on?.('serverLeave', async () => {
1269
+ try {
1270
+ const conn = await voiceManager.join(channel);
1271
+ await conn.play(streamUrl);
1272
+ } catch (e) {
1273
+ console.error('Auto-reconnect failed:', e);
1274
+ }
1275
+ });`,
1276
+ language: 'javascript',
1277
+ },
1278
+ ],
1279
+ },
1280
+ {
1281
+ id: 'wait-for-guilds',
1282
+ slug: 'wait-for-guilds',
1283
+ title: 'Wait for All Guilds',
1284
+ description:
1285
+ 'Delay the Ready event until all guilds have been received. Use when your bot needs the full guild cache before handling Ready.',
1286
+ category: 'events',
1287
+ sections: [
1288
+ {
1289
+ title: 'Overview',
1290
+ description:
1291
+ 'By default, Ready fires as soon as the gateway sends the READY payload. Some guilds may be sent as unavailable stubs and arrive later via GUILD_CREATE. Enable waitForGuilds if your Ready handler needs every guild to be in client.guilds before proceeding.',
1292
+ },
1293
+ {
1294
+ title: 'Usage',
1295
+ description:
1296
+ 'Pass waitForGuilds: true in ClientOptions. Ready will emit only after all guilds from READY (including those marked unavailable) have been received via GUILD_CREATE.',
1297
+ code: `import { Client, Events } from '@erinjs/core';
1298
+
1299
+ const client = new Client({
1300
+ waitForGuilds: true,
1301
+ });
1302
+
1303
+ client.on(Events.Ready, () => {
1304
+ // client.guilds now contains every guild — no stubs, all fully loaded
1305
+ console.log(\`Bot is in \${client.guilds.size} guilds\`);
1306
+ for (const [id, guild] of client.guilds) {
1307
+ console.log(\`- \${guild.name} (\${guild.channels.size} channels)\`);
1308
+ }
1309
+ });
1310
+
1311
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1312
+ language: 'javascript',
1313
+ },
1314
+ {
1315
+ title: 'When to use it',
1316
+ description:
1317
+ 'Use waitForGuilds when your bot iterates over all guilds in Ready (e.g. syncing state, broadcasting announcements, or building in-memory caches). Without it, client.guilds may be incomplete at Ready time.',
1318
+ tip: 'If you only need a few guilds by ID, prefer client.guilds.resolve(guildId) instead — no need to wait for all guilds.',
1319
+ },
1320
+ ],
1321
+ },
1322
+ {
1323
+ id: 'events',
1324
+ slug: 'events',
1325
+ title: 'Events',
1326
+ description:
1327
+ 'Listen to gateway events with client.on. Handle messages, guild updates, voice state changes, and more.',
1328
+ category: 'events',
1329
+ sections: [
1330
+ {
1331
+ title: 'Basic Usage',
1332
+ description:
1333
+ 'Use client.on(Events.X, handler) to subscribe to events. Handlers receive event-specific payloads.',
1334
+ code: `import { Client, Events } from '@erinjs/core';
1335
+
1336
+ const client = new Client({ intents: 0 });
1337
+
1338
+ client.on(Events.Ready, () => {
1339
+ console.log('Bot is ready!');
1340
+ });
1341
+
1342
+ client.on(Events.MessageCreate, async (message) => {
1343
+ console.log(message.content);
1344
+ });
1345
+
1346
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1347
+ language: 'javascript',
1348
+ tip: 'client.events.X(handler) offers the same API with chaining and better autocomplete.',
1349
+ alternateCode: {
1350
+ label: 'client.events',
1351
+ code: `import { Client, Events } from '@erinjs/core';
1352
+
1353
+ const client = new Client({ intents: 0 });
1354
+
1355
+ client
1356
+ .events.Ready(() => console.log('Bot is ready!'))
1357
+ .events.MessageCreate(async (message) => console.log(message.content));
1358
+
1359
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1360
+ },
1361
+ },
1362
+ {
1363
+ title: 'Common Events',
1364
+ description: 'Essential events for most bots.',
1365
+ code: `// Bot finished loading
1366
+ client.on(Events.Ready, () => {});
1367
+
1368
+ // New message (DM or guild)
1369
+ client.on(Events.MessageCreate, async (message) => {});
1370
+
1371
+ // Reaction events
1372
+ client.on(Events.MessageReactionAdd, (reaction, user, messageId, channelId, emoji, userId) => {});
1373
+ client.on(Events.MessageReactionRemove, (reaction, user, messageId, channelId, emoji, userId) => {});
1374
+
1375
+ // Guild joined/left/updated
1376
+ client.on(Events.GuildCreate, (guild) => {});
1377
+ client.on(Events.GuildDelete, (guild) => {});
1378
+
1379
+ // Channel created/updated/deleted
1380
+ client.on(Events.ChannelCreate, (channel) => {});
1381
+ client.on(Events.ChannelDelete, (channel) => {});
1382
+
1383
+ // Member joined/left/updated
1384
+ client.on(Events.GuildMemberAdd, (member) => {});
1385
+ client.on(Events.GuildMemberRemove, (member) => {});
1386
+
1387
+ // Voice state changed (for @erinjs/voice)
1388
+ client.on(Events.VoiceStateUpdate, (data) => {});
1389
+ client.on(Events.VoiceServerUpdate, (data) => {});`,
1390
+ language: 'javascript',
1391
+ },
1392
+ {
1393
+ title: 'Reaction Events',
1394
+ description:
1395
+ 'Listen for when users add or remove reactions. Handlers receive (reaction, user, messageId, channelId, emoji, userId). Use MessageReactionRemoveAll and MessageReactionRemoveEmoji for moderator actions.',
1396
+ code: `import { Client, Events } from '@erinjs/core';
1397
+
1398
+ const client = new Client({ intents: 0 });
1399
+
1400
+ client.on(Events.MessageReactionAdd, (reaction, user, messageId, channelId, emoji, userId) => {
1401
+ const emojiStr = emoji.id ? \`<:\${emoji.name}:\${emoji.id}>\` : emoji.name;
1402
+ console.log(\`User \${userId} reacted with \${emojiStr} on message \${messageId}\`);
1403
+
1404
+ // Filter for specific message (e.g. poll) or emoji
1405
+ if (emoji.name === '👍') {
1406
+ console.log('Someone voted yes!');
1407
+ }
1408
+ });
1409
+
1410
+ client.on(Events.MessageReactionRemove, (reaction, user, messageId, channelId, emoji, userId) => {
1411
+ console.log(\`User \${userId} removed \${emoji.name} from message \${messageId}\`);
1412
+ });
1413
+
1414
+ client.on(Events.MessageReactionRemoveAll, (data) => {
1415
+ console.log(\`All reactions cleared from message \${data.message_id}\`);
1416
+ });
1417
+
1418
+ client.on(Events.MessageReactionRemoveEmoji, (data) => {
1419
+ console.log(\`All \${data.emoji.name} reactions removed from message \${data.message_id}\`);
1420
+ });
1421
+
1422
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1423
+ language: 'javascript',
1424
+ },
1425
+ {
1426
+ title: 'Error Handling',
1427
+ code: `client.on(Events.Error, (err) => {
1428
+ console.error('Client error:', err);
1429
+ });`,
1430
+ language: 'javascript',
1431
+ },
1432
+ {
1433
+ title: 'Gateway Dispatch Events Reference',
1434
+ description:
1435
+ 'All events the Fluxer gateway can send. Use GatewayDispatchEvents from @erinjs/types for type-safe checks.',
1436
+ table: {
1437
+ headers: ['Category', 'Events'],
1438
+ codeColumns: [1],
1439
+ rows: [
1440
+ ['Connection & Session', 'Ready, Resumed, SessionsReplace'],
1441
+ [
1442
+ 'User',
1443
+ 'UserUpdate, UserSettingsUpdate, UserGuildSettingsUpdate, UserPinnedDmsUpdate, UserNoteUpdate, RecentMentionDelete',
1444
+ ],
1445
+ ['Saved Messages & Auth', 'SavedMessageCreate, SavedMessageDelete, AuthSessionChange'],
1446
+ ['Presence', 'PresenceUpdate'],
1447
+ [
1448
+ 'Guild',
1449
+ 'GuildCreate, GuildUpdate, GuildDelete, GuildMemberAdd, GuildMemberUpdate, GuildMemberRemove, GuildMembersChunk, GuildMemberListUpdate, GuildSync',
1450
+ ],
1451
+ ['Roles', 'GuildRoleCreate, GuildRoleUpdate, GuildRoleUpdateBulk, GuildRoleDelete'],
1452
+ ['Guild Assets', 'GuildEmojisUpdate, GuildStickersUpdate'],
1453
+ ['Moderation', 'GuildBanAdd, GuildBanRemove'],
1454
+ [
1455
+ 'Channels',
1456
+ 'ChannelCreate, ChannelUpdate, ChannelUpdateBulk, ChannelDelete, ChannelRecipientAdd, ChannelRecipientRemove, ChannelPinsUpdate, ChannelPinsAck',
1457
+ ],
1458
+ ['Passive', 'PassiveUpdates'],
1459
+ ['Invites', 'InviteCreate, InviteDelete'],
1460
+ [
1461
+ 'Messages',
1462
+ 'MessageCreate, MessageUpdate, MessageDelete, MessageDeleteBulk, MessageReactionAdd, MessageReactionRemove, MessageReactionRemoveAll, MessageReactionRemoveEmoji, MessageAck',
1463
+ ],
1464
+ ['Typing', 'TypingStart'],
1465
+ ['Webhooks', 'WebhooksUpdate'],
1466
+ ['Relationships', 'RelationshipAdd, RelationshipUpdate, RelationshipRemove'],
1467
+ ['Voice', 'VoiceStateUpdate, VoiceServerUpdate'],
1468
+ ['Calls', 'CallCreate, CallUpdate, CallDelete'],
1469
+ ['Favorites', 'FavoriteMemeCreate, FavoriteMemeUpdate, FavoriteMemeDelete'],
1470
+ [
1471
+ 'SDK / Compatibility',
1472
+ 'InteractionCreate, GuildIntegrationsUpdate, GuildScheduledEventCreate, GuildScheduledEventUpdate, GuildScheduledEventDelete',
1473
+ ],
1474
+ ],
1475
+ },
1476
+ },
1477
+ {
1478
+ title: 'Event Payload Reference',
1479
+ description:
1480
+ 'Payload structure for each event. Handler receives (data) or (message), (reaction, user, ...) etc. Types: Gateway*DispatchData from @erinjs/types.',
1481
+ table: {
1482
+ headers: ['Event', 'Payload'],
1483
+ codeColumns: [0, 1],
1484
+ rows: [
1485
+ ['READY', '{ v, user, guilds, session_id, shard?, application: { id, flags } }'],
1486
+ ['RESUMED', '(no payload)'],
1487
+ ['SESSIONS_REPLACE', 'Array of session objects'],
1488
+ ['USER_UPDATE', 'APIUser — id, username, discriminator, global_name, avatar, etc.'],
1489
+ [
1490
+ 'GUILD_CREATE',
1491
+ 'APIGuild — id, name, icon, owner_id, channels[], members[], roles[], ...',
1492
+ ],
1493
+ ['GUILD_UPDATE', 'APIGuild — full guild object'],
1494
+ ['GUILD_DELETE', '{ id, unavailable? }'],
1495
+ [
1496
+ 'GUILD_MEMBER_ADD',
1497
+ 'APIGuildMember & { guild_id } — user, roles, nick, joined_at, ...',
1498
+ ],
1499
+ ['GUILD_MEMBER_UPDATE', '{ guild_id, roles, user, nick?, avatar?, joined_at?, ... }'],
1500
+ ['GUILD_MEMBER_REMOVE', '{ guild_id, user }'],
1501
+ [
1502
+ 'GUILD_MEMBERS_CHUNK',
1503
+ '{ guild_id, members[], chunk_index, chunk_count, presences?, nonce? }',
1504
+ ],
1505
+ [
1506
+ 'GUILD_MEMBER_LIST_UPDATE',
1507
+ '{ guild_id, id, member_count, online_count, groups[], ops[] }',
1508
+ ],
1509
+ ['GUILD_ROLE_CREATE', '{ guild_id, role: APIRole }'],
1510
+ ['GUILD_ROLE_UPDATE', '{ guild_id, role: APIRole }'],
1511
+ ['GUILD_ROLE_UPDATE_BULK', '{ guild_id, roles: APIRole[] }'],
1512
+ ['GUILD_ROLE_DELETE', '{ guild_id, role_id }'],
1513
+ ['GUILD_EMOJIS_UPDATE', '{ guild_id, emojis: APIEmoji[] }'],
1514
+ ['GUILD_STICKERS_UPDATE', '{ guild_id, stickers: APISticker[] }'],
1515
+ ['GUILD_BAN_ADD', '{ guild_id, user, reason? }'],
1516
+ ['GUILD_BAN_REMOVE', '{ guild_id, user }'],
1517
+ ['CHANNEL_CREATE', 'APIChannel — id, name, type, guild_id?, parent_id, ...'],
1518
+ ['CHANNEL_UPDATE', 'APIChannel'],
1519
+ ['CHANNEL_UPDATE_BULK', '{ channels: APIChannel[] }'],
1520
+ ['CHANNEL_DELETE', 'APIChannel'],
1521
+ ['CHANNEL_RECIPIENT_ADD', '{ channel_id, user }'],
1522
+ ['CHANNEL_RECIPIENT_REMOVE', '{ channel_id, user }'],
1523
+ ['CHANNEL_PINS_UPDATE', '{ channel_id, guild_id?, last_pin_timestamp? }'],
1524
+ ['CHANNEL_PINS_ACK', '{ channel_id, last_pin_timestamp? }'],
1525
+ ['INVITE_CREATE', 'APIInvite — code, guild, channel, inviter?, expires_at?, ...'],
1526
+ ['INVITE_DELETE', '{ code, channel_id, guild_id? }'],
1527
+ [
1528
+ 'MESSAGE_CREATE',
1529
+ 'APIMessage — id, channel_id, author, content, embeds, attachments, member?, ...',
1530
+ ],
1531
+ ['MESSAGE_UPDATE', 'APIMessage — partial (edited fields)'],
1532
+ ['MESSAGE_DELETE', '{ id, channel_id, guild_id?, content?, author_id? }'],
1533
+ ['MESSAGE_DELETE_BULK', '{ ids[], channel_id, guild_id? }'],
1534
+ [
1535
+ 'MESSAGE_REACTION_ADD',
1536
+ '{ message_id, channel_id, user_id, guild_id?, emoji: { id, name, animated? } }',
1537
+ ],
1538
+ ['MESSAGE_REACTION_REMOVE', '{ message_id, channel_id, user_id, guild_id?, emoji }'],
1539
+ ['MESSAGE_REACTION_REMOVE_ALL', '{ message_id, channel_id, guild_id? }'],
1540
+ ['MESSAGE_REACTION_REMOVE_EMOJI', '{ message_id, channel_id, guild_id?, emoji }'],
1541
+ ['MESSAGE_ACK', '{ message_id, channel_id } — read receipt'],
1542
+ ['TYPING_START', '{ channel_id, user_id, timestamp, guild_id?, member? }'],
1543
+ [
1544
+ 'VOICE_STATE_UPDATE',
1545
+ '{ guild_id?, channel_id, user_id, member?, session_id, deaf?, mute?, ... }',
1546
+ ],
1547
+ ['VOICE_SERVER_UPDATE', '{ token, guild_id, endpoint, connection_id? }'],
1548
+ ['WEBHOOKS_UPDATE', '{ guild_id, channel_id }'],
1549
+ [
1550
+ 'PRESENCE_UPDATE',
1551
+ '{ user: { id }, guild_id?, status?, activities?, custom_status? }',
1552
+ ],
1553
+ ['GUILD_INTEGRATIONS_UPDATE', '{ guild_id }'],
1554
+ ['GUILD_SCHEDULED_EVENT_CREATE', '{ guild_id, id }'],
1555
+ ['GUILD_SCHEDULED_EVENT_UPDATE', '{ guild_id, id }'],
1556
+ ['GUILD_SCHEDULED_EVENT_DELETE', '{ guild_id, id }'],
1557
+ ['USER_NOTE_UPDATE', '{ id, note? }'],
1558
+ ['SAVED_MESSAGE_CREATE', 'APIMessage'],
1559
+ ['SAVED_MESSAGE_DELETE', '{ id }'],
1560
+ ['RELATIONSHIP_ADD / UPDATE', '{ id, type }'],
1561
+ ['RELATIONSHIP_REMOVE', '{ id }'],
1562
+ ['CALL_CREATE / UPDATE / DELETE', '{ id, channel_id, ... }'],
1563
+ ['INTERACTION_CREATE', 'APIApplicationCommandInteraction'],
1564
+ ],
1565
+ },
1566
+ },
1567
+ ],
1568
+ },
1569
+ {
1570
+ id: 'deprecated-apis',
1571
+ slug: 'deprecated-apis',
1572
+ title: 'Deprecated APIs',
1573
+ description:
1574
+ 'APIs that are deprecated and will be removed in a future release. Migrate to the recommended alternatives.',
1575
+ category: 'other',
1576
+ sections: [
1577
+ {
1578
+ title: 'Overview',
1579
+ description:
1580
+ 'The following methods emit a one-time deprecation warning at runtime. Set FLUXER_SUPPRESS_DEPRECATION=1 to silence warnings. Migrate to the recommended replacements.',
1581
+ },
1582
+ {
1583
+ title: 'Client.sendToChannel',
1584
+ description:
1585
+ 'Use client.channels.send(channelId, payload) instead. Accepts the same MessageSendOptions (content, embeds, files).',
1586
+ table: {
1587
+ headers: ['Deprecated', 'Replacement'],
1588
+ rows: [
1589
+ [
1590
+ 'client.sendToChannel(channelId, content)',
1591
+ 'client.channels.send(channelId, payload)',
1592
+ ],
1593
+ ],
1594
+ codeColumns: [0, 1],
1595
+ },
1596
+ code: `// ❌ Deprecated
1597
+ await client.sendToChannel(channelId, 'Hello!');
1598
+ await client.sendToChannel(channelId, { embeds: [embed] });
1599
+
1600
+ // ✅ Use instead
1601
+ await client.channels.send(channelId, 'Hello!');
1602
+ await client.channels.send(channelId, { embeds: [embed] });`,
1603
+ language: 'javascript',
1604
+ },
1605
+ {
1606
+ title: 'Client.fetchMessage',
1607
+ description:
1608
+ 'Use channel.messages.fetch(messageId) instead. Resolve the channel first if you only have IDs.',
1609
+ table: {
1610
+ headers: ['Deprecated', 'Replacement'],
1611
+ rows: [
1612
+ [
1613
+ 'client.fetchMessage(channelId, messageId)',
1614
+ '(await client.channels.resolve(channelId))?.messages?.fetch(messageId)',
1615
+ ],
1616
+ ],
1617
+ codeColumns: [0, 1],
1618
+ },
1619
+ code: `// ❌ Deprecated
1620
+ const message = await client.fetchMessage(channelId, messageId);
1621
+
1622
+ // ✅ Use instead
1623
+ const channel = await client.channels.resolve(channelId);
1624
+ const message = channel?.messages ? await channel.messages.fetch(messageId) : null;`,
1625
+ language: 'javascript',
1626
+ },
1627
+ {
1628
+ title: 'ChannelManager.fetchMessage',
1629
+ description: 'Use channel.messages.fetch(messageId) instead.',
1630
+ table: {
1631
+ headers: ['Deprecated', 'Replacement'],
1632
+ rows: [
1633
+ [
1634
+ 'client.channels.fetchMessage(channelId, messageId)',
1635
+ 'channel.messages.fetch(messageId)',
1636
+ ],
1637
+ ],
1638
+ codeColumns: [0, 1],
1639
+ },
1640
+ },
1641
+ {
1642
+ title: 'Channel.fetchMessage',
1643
+ description:
1644
+ 'Use channel.messages.fetch(messageId) instead. Available on TextChannel and DMChannel.',
1645
+ table: {
1646
+ headers: ['Deprecated', 'Replacement'],
1647
+ rows: [['channel.fetchMessage(messageId)', 'channel.messages.fetch(messageId)']],
1648
+ codeColumns: [0, 1],
1649
+ },
1650
+ },
1651
+ ],
1652
+ },
1653
+ {
1654
+ id: 'permissions',
1655
+ slug: 'permissions',
1656
+ title: 'Permissions',
1657
+ description:
1658
+ 'Check member permissions (guild-level and channel-specific), bot permissions via guild.members.me, owner override, and PermissionFlags.',
1659
+ category: 'other',
1660
+ sections: [
1661
+ {
1662
+ title: 'Overview',
1663
+ description:
1664
+ 'Use member.permissions for guild-level checks (roles only) and member.permissionsIn(channel) for channel-specific permissions (includes overwrites). The server owner always has all permissions.',
1665
+ },
1666
+ {
1667
+ title: 'Guild-level permissions',
1668
+ description:
1669
+ 'member.permissions returns an object with has(permission). Use it for server-wide actions like ban, kick, manage roles.',
1670
+ code: `import { Client, Events, PermissionFlags } from '@erinjs/core';
1671
+
1672
+ const client = new Client({ intents: 0 });
1673
+
1674
+ async function getMemberPerms(message) {
1675
+ const guild = message.guild ?? await message.client.guilds.resolve(message.guildId);
1676
+ if (!guild) return null;
1677
+ const member = guild.members.get(message.author.id) ?? await guild.fetchMember(message.author.id);
1678
+ return member?.permissions ?? null;
1679
+ }
1680
+
1681
+ client.on(Events.MessageCreate, async (message) => {
1682
+ const perms = await getMemberPerms(message);
1683
+ if (!perms) return;
1684
+
1685
+ if (perms.has(PermissionFlags.BanMembers)) {
1686
+ await message.reply('You can ban members.');
1687
+ }
1688
+ if (perms.has(PermissionFlags.Administrator)) {
1689
+ await message.reply('You have Administrator.');
1690
+ }
1691
+ });
1692
+
1693
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1694
+ language: 'javascript',
1695
+ },
1696
+ {
1697
+ title: "Bot's own permissions (guild.members.me)",
1698
+ discordJsCompat: '/docs/classes/GuildMemberManager',
1699
+ description:
1700
+ "Use guild.members.me to get the bot's GuildMember. Returns null if not cached. Use guild.members.fetchMe() to load it. Discord.js parity.",
1701
+ code: `// Check if the bot can ban members in this guild
1702
+ const guild = message.guild ?? await message.client.guilds.resolve(message.guildId);
1703
+ const me = guild?.members.me ?? (guild ? await guild.members.fetchMe() : null);
1704
+ if (me?.permissions.has(PermissionFlags.BanMembers)) {
1705
+ await message.reply('I have Ban Members permission.');
1706
+ }`,
1707
+ language: 'javascript',
1708
+ },
1709
+ {
1710
+ title: "Editing the bot's guild profile (nickname)",
1711
+ description:
1712
+ "Use guild.members.me.edit({ nick }) to change the bot's nickname in that guild. Pass nick: null to clear and show the username. Requires Change Nickname permission (or bot has Manage Nicknames). See examples/ping-bot.js for a !setnick command.",
1713
+ code: `const guild = message.guild ?? await client.guilds.resolve(message.guildId);
1714
+ const me = guild?.members.me ?? (guild ? await guild.members.fetchMe() : null);
1715
+ if (me) {
1716
+ await me.edit({ nick: 'My Custom Nick' });
1717
+ await message.reply('Nickname updated!');
1718
+ }
1719
+ // Clear nickname (show username)
1720
+ await me.edit({ nick: null });`,
1721
+ language: 'javascript',
1722
+ },
1723
+ {
1724
+ title: 'Owner override',
1725
+ description:
1726
+ 'The guild owner automatically receives all permissions regardless of roles. No need to give the owner a role with Administrator.',
1727
+ code: `// When the message author is the server owner:
1728
+ const perms = member.permissions;
1729
+ perms.has(PermissionFlags.BanMembers); // true
1730
+ perms.has(PermissionFlags.ManageRoles); // true
1731
+ perms.has(PermissionFlags.Administrator); // true
1732
+ // ... all permission flags return true for the owner`,
1733
+ language: 'javascript',
1734
+ },
1735
+ {
1736
+ title: 'Channel-specific permissions',
1737
+ description:
1738
+ 'member.permissionsIn(channel) applies channel overwrites. Use it when checking if a user can send messages, read history, or connect to voice in a specific channel.',
1739
+ code: `const channel = message.channel;
1740
+ if (channel?.canSendMessage?.()) {
1741
+ const perms = member.permissionsIn(channel);
1742
+ if (perms.has(PermissionFlags.SendMessages)) {
1743
+ await channel.send('You can send here!');
1744
+ }
1745
+ }`,
1746
+ language: 'javascript',
1747
+ },
1748
+ {
1749
+ title: 'Managing roles',
1750
+ description:
1751
+ 'Create, fetch, edit, and delete roles with guild.createRole(), guild.fetchRoles(), guild.fetchRole(roleId), role.edit(), and role.delete(). Use resolvePermissionsToBitfield() for permission bitfields. See the Roles guide for full examples.',
1752
+ code: `// Create a role with specific permissions
1753
+ const role = await guild.createRole({
1754
+ name: 'Mod',
1755
+ permissions: ['KickMembers', 'BanMembers', 'ManageMessages'],
1756
+ });
1757
+
1758
+ // Add/remove roles from members
1759
+ await guild.addRoleToMember(userId, roleId);
1760
+ await guild.removeRoleFromMember(userId, roleId);`,
1761
+ language: 'javascript',
1762
+ },
1763
+ {
1764
+ title: 'PermissionFlags reference',
1765
+ description:
1766
+ 'Common flags: BanMembers, KickMembers, Administrator, ManageRoles, ManageChannels, ManageGuild, ViewAuditLog, ManageMessages, SendMessages, EmbedLinks, AttachFiles, ReadMessageHistory, MentionEveryone, Connect, Speak, MuteMembers, ModerateMembers, CreateExpressions, PinMessages, BypassSlowmode.',
1767
+ code: `import { PermissionFlags } from '@erinjs/core';
1768
+
1769
+ // Check multiple
1770
+ const canModerate = perms.has(PermissionFlags.BanMembers) || perms.has(PermissionFlags.Administrator);
1771
+
1772
+ // List all permissions the user has
1773
+ const names = Object.keys(PermissionFlags).filter((name) =>
1774
+ perms.has(PermissionFlags[name])
1775
+ );
1776
+ await message.reply(\`Your permissions: \${names.join(', ')}\`);`,
1777
+ language: 'javascript',
1778
+ },
1779
+ ],
1780
+ },
1781
+ {
1782
+ id: 'moderation',
1783
+ slug: 'moderation',
1784
+ title: 'Moderation',
1785
+ description:
1786
+ 'Implement ban, kick, and unban commands. Check permissions first (see Permissions guide).',
1787
+ category: 'other',
1788
+ sections: [
1789
+ {
1790
+ title: 'Overview',
1791
+ description:
1792
+ 'Use guild.ban(), guild.kick(), and guild.unban() for moderation. Always check member permissions before allowing moderation commands—see the Permissions guide.',
1793
+ },
1794
+ {
1795
+ title: 'Ban a member',
1796
+ description:
1797
+ 'guild.ban(userId, options) bans a user. Pass reason for the audit log. Requires BanMembers permission.',
1798
+ code: `const userId = parseUserMention(target);
1799
+ if (userId) {
1800
+ await guild.ban(userId, { reason: rest.join(' ') || undefined });
1801
+ await message.reply(\`Banned <@\${userId}>.\`);
1802
+ }`,
1803
+ language: 'javascript',
1804
+ },
1805
+ {
1806
+ title: 'Kick a member',
1807
+ description:
1808
+ 'guild.kick(userId, options) kicks a user from the guild. Pass reason for the audit log. Requires KickMembers permission.',
1809
+ code: `const userId = parseUserMention(target);
1810
+ if (userId) {
1811
+ await guild.kick(userId, { reason: rest.join(' ') || undefined });
1812
+ await message.reply(\`Kicked <@\${userId}>.\`);
1813
+ }`,
1814
+ language: 'javascript',
1815
+ },
1816
+ {
1817
+ title: 'Unban a user',
1818
+ description: 'guild.unban(userId, reason?) removes a ban. Requires BanMembers permission.',
1819
+ code: `const userId = parseUserMention(target);
1820
+ if (userId) {
1821
+ await guild.unban(userId, rest.join(' ') || undefined);
1822
+ await message.reply(\`Unbanned <@\${userId}>.\`);
1823
+ }`,
1824
+ language: 'javascript',
1825
+ },
1826
+ {
1827
+ title: 'Full moderation example',
1828
+ description:
1829
+ 'See examples/moderation-bot.js for a complete bot with !ban, !kick, !unban, and !perms commands.',
1830
+ code: `import { Client, Events, PermissionFlags, parseUserMention } from '@erinjs/core';
1831
+
1832
+ const PREFIX = '!';
1833
+ const client = new Client({ intents: 0 });
1834
+
1835
+ async function getModeratorPerms(message) {
1836
+ const guild = message.guild ?? await message.client.guilds.resolve(message.guildId);
1837
+ if (!guild) return null;
1838
+ const member = guild.members.get(message.author.id);
1839
+ const resolved = member ?? await guild.fetchMember(message.author.id);
1840
+ return resolved?.permissions ?? null;
1841
+ }
1842
+
1843
+ client.on(Events.MessageCreate, async (message) => {
1844
+ if (message.author.bot || !message.content?.startsWith(PREFIX)) return;
1845
+ const [cmd, target, ...rest] = message.content.slice(PREFIX.length).trim().split(/\\s+/);
1846
+ const perms = await getModeratorPerms(message);
1847
+ if (!perms) return;
1848
+
1849
+ const guild = message.guild ?? await message.client.guilds.resolve(message.guildId);
1850
+ if (!guild) return;
1851
+
1852
+ if (cmd === 'ban' && (perms.has(PermissionFlags.BanMembers) || perms.has(PermissionFlags.Administrator))) {
1853
+ const userId = parseUserMention(target);
1854
+ if (userId) {
1855
+ await guild.ban(userId, { reason: rest.join(' ') || undefined });
1856
+ await message.reply(\`Banned <@\${userId}>.\`);
1857
+ }
1858
+ }
1859
+ if (cmd === 'kick' && (perms.has(PermissionFlags.KickMembers) || perms.has(PermissionFlags.Administrator))) {
1860
+ const userId = parseUserMention(target);
1861
+ if (userId) {
1862
+ await guild.kick(userId, { reason: rest.join(' ') || undefined });
1863
+ await message.reply(\`Kicked <@\${userId}>.\`);
1864
+ }
1865
+ }
1866
+ });
1867
+
1868
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1869
+ language: 'javascript',
1870
+ },
1871
+ ],
1872
+ },
1873
+ {
1874
+ id: 'roles',
1875
+ slug: 'roles',
1876
+ title: 'Roles',
1877
+ description:
1878
+ 'Create, fetch, edit, and delete guild roles. Use PermissionFlags and resolvePermissionsToBitfield for permission bitfields.',
1879
+ category: 'channels',
1880
+ sections: [
1881
+ {
1882
+ title: 'Overview',
1883
+ description:
1884
+ "Guild roles can be created, fetched, edited, and deleted. Use guild.createRole(), guild.fetchRoles(), guild.fetchRole(roleId), role.edit(), and role.delete(). Requires Manage Roles permission. For permission bitfields, use resolvePermissionsToBitfield() or role.has() to check a role's permissions.",
1885
+ },
1886
+ {
1887
+ title: 'Create a role',
1888
+ description:
1889
+ 'Use guild.createRole() to create a new role. Pass name, permissions, color, hoist, mentionable, unicode_emoji, position, or hoist_position. Permissions accept PermissionResolvable (string, number, array) for convenience.',
1890
+ code: `import { Client, Events, PermissionFlags, resolvePermissionsToBitfield } from '@erinjs/core';
1891
+
1892
+ const client = new Client({ intents: 0 });
1893
+
1894
+ client.on(Events.MessageCreate, async (message) => {
1895
+ if (message.content === '!createrole' && message.guildId) {
1896
+ const guild = client.guilds.get(message.guildId) ?? await client.guilds.resolve(message.guildId);
1897
+ if (!guild) return;
1898
+
1899
+ const role = await guild.createRole({
1900
+ name: 'Moderator',
1901
+ permissions: ['BanMembers', 'KickMembers', 'ManageMessages'],
1902
+ color: 0x5865f2,
1903
+ hoist: true,
1904
+ mentionable: false,
1905
+ });
1906
+ await message.reply(\`Created role \${role.name} (\${role.id})\`);
1907
+ }
1908
+ });
1909
+
1910
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
1911
+ language: 'javascript',
1912
+ },
1913
+ {
1914
+ title: 'Fetch roles',
1915
+ description:
1916
+ 'Use guild.fetchRoles() to fetch all roles from the API and cache them. Use guild.fetchRole(roleId) to fetch a single role by ID. Throws FluxerError with ROLE_NOT_FOUND on 404.',
1917
+ code: `// Fetch all roles (updates guild.roles cache)
1918
+ const roles = await guild.fetchRoles();
1919
+
1920
+ // Fetch a single role by ID
1921
+ const role = await guild.fetchRole(roleId);
1922
+ console.log(role.name, role.color);`,
1923
+ language: 'javascript',
1924
+ },
1925
+ {
1926
+ title: 'Edit a role',
1927
+ description:
1928
+ 'Use role.edit() to update a role. Pass any of name, permissions, color, hoist, mentionable, unicode_emoji, position, hoist_position. Permissions accept PermissionResolvable.',
1929
+ code: `const role = guild.roles.get(roleId) ?? await guild.fetchRole(roleId);
1930
+ await role.edit({
1931
+ name: 'Senior Mod',
1932
+ permissions: ['BanMembers', 'KickMembers', 'ManageMessages', 'ManageRoles'],
1933
+ color: 0x57f287,
1934
+ });`,
1935
+ language: 'javascript',
1936
+ },
1937
+ {
1938
+ title: 'Delete a role',
1939
+ description:
1940
+ 'Use role.delete() to remove a role. The role is removed from guild.roles cache.',
1941
+ code: `const role = guild.roles.get(roleId) ?? await guild.fetchRole(roleId);
1942
+ await role.delete();
1943
+ await message.reply('Role deleted.');`,
1944
+ language: 'javascript',
1945
+ },
1946
+ {
1947
+ title: 'Check role permissions',
1948
+ description:
1949
+ 'Use role.has(permission) to check if a role has a specific permission. Administrator implies all permissions.',
1950
+ code: `import { PermissionFlags } from '@erinjs/core';
1951
+
1952
+ if (role.has(PermissionFlags.BanMembers)) {
1953
+ await message.reply('This role can ban members.');
1954
+ }
1955
+ if (role.has('ManageChannels')) {
1956
+ await message.reply('This role can manage channels.');
1957
+ }`,
1958
+ language: 'javascript',
1959
+ },
1960
+ {
1961
+ title: 'Add/remove roles from members (member.roles)',
1962
+ discordJsCompat: '/docs/classes/GuildMemberRoleManager',
1963
+ description:
1964
+ 'Use member.roles.add(), member.roles.remove(), and member.roles.set() for Discord.js-style role management. member.roles.cache is a Collection of Role objects. Also available: guild.addRoleToMember() and guild.removeRoleFromMember() when you only have user ID.',
1965
+ code: `// Discord.js parity: member.roles.add(), remove(), set()
1966
+ const member = await guild.fetchMember(userId);
1967
+
1968
+ await member.roles.add(roleId); // Add a role
1969
+ await member.roles.remove(roleId); // Remove a role
1970
+ await member.roles.set(['id1', 'id2']); // Replace all roles
1971
+
1972
+ // Check if member has a role
1973
+ if (member.roles.cache.has(roleId)) {
1974
+ await message.reply('Member already has this role.');
1975
+ }
1976
+
1977
+ // Guild-level: when you only have user ID (no member fetch needed)
1978
+ await guild.addRoleToMember(userId, roleId);
1979
+ await guild.removeRoleFromMember(userId, roleId);`,
1980
+ language: 'javascript',
1981
+ },
1982
+ {
1983
+ title: 'Permission bitfields for create/edit',
1984
+ description:
1985
+ 'When creating or editing roles, pass permissions as a string (API format), number, PermissionString, or array. Use resolvePermissionsToBitfield() to combine multiple permissions. Handles high bits (PinMessages, ModerateMembers, etc.) correctly with BigInt.',
1986
+ code: `import { resolvePermissionsToBitfield, PermissionFlags } from '@erinjs/core';
1987
+
1988
+ // Single permission by name
1989
+ resolvePermissionsToBitfield('SendMessages'); // "2048"
1990
+
1991
+ // Array of permissions (OR'd together)
1992
+ resolvePermissionsToBitfield(['SendMessages', 'ViewChannel', 'ReadMessageHistory']);
1993
+ // Returns combined bitfield as string
1994
+
1995
+ // From PermissionFlags enum
1996
+ resolvePermissionsToBitfield(PermissionFlags.BanMembers); // "4"`,
1997
+ language: 'javascript',
1998
+ },
1999
+ ],
2000
+ },
2001
+ {
2002
+ id: 'prefix-commands',
2003
+ slug: 'prefix-commands',
2004
+ title: 'Prefix Commands',
2005
+ description: 'Handle !commands by listening to MessageCreate and parsing the content.',
2006
+ category: 'events',
2007
+ sections: [
2008
+ {
2009
+ title: 'Basic Structure',
2010
+ description: 'Check for a prefix, split args, and dispatch to command handlers.',
2011
+ code: `import { Client, Events } from '@erinjs/core';
2012
+
2013
+ const PREFIX = '!';
2014
+ const client = new Client({ intents: 0 });
2015
+
2016
+ client.on(Events.MessageCreate, async (message) => {
2017
+ if (message.author.bot || !message.content) return;
2018
+ if (!message.content.startsWith(PREFIX)) return;
2019
+
2020
+ const args = message.content.slice(PREFIX.length).trim().split(/\\s+/);
2021
+ const command = args.shift()?.toLowerCase();
2022
+
2023
+ if (command === 'ping') {
2024
+ await message.reply('Pong!');
2025
+ }
2026
+ if (command === 'hello') {
2027
+ const name = args[0] ?? 'there';
2028
+ await message.reply(\`Hello, \${name}!\`);
2029
+ }
2030
+ });
2031
+
2032
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
2033
+ language: 'javascript',
2034
+ },
2035
+ {
2036
+ title: 'Guild-Only Commands',
2037
+ code: `if (!message.guildId) {
2038
+ await message.reply('This command only works in a server.');
2039
+ return;
2040
+ }`,
2041
+ language: 'javascript',
2042
+ },
2043
+ ],
2044
+ },
2045
+ {
2046
+ id: 'channels',
2047
+ slug: 'channels',
2048
+ title: 'Channels',
2049
+ description:
2050
+ 'Create and manage channels, roles, and invites. Covers guild.createChannel(), channel.edit(), channel.createInvite(), guild.createRole(), and more.',
2051
+ category: 'channels',
2052
+ sections: [
2053
+ {
2054
+ title: 'Channels — Create',
2055
+ description:
2056
+ 'Use guild.createChannel() to create text (0), voice (2), category (4), or link (5) channels. Requires Manage Channels permission. Pass parent_id to put a channel under a category.',
2057
+ code: `import { Client, Events } from '@erinjs/core';
2058
+
2059
+ const client = new Client({ intents: 0 });
2060
+
2061
+ client.on(Events.MessageCreate, async (message) => {
2062
+ if (!message.guildId || message.content !== '!createchannel') return;
2063
+ const guild = client.guilds.get(message.guildId) ?? await client.guilds.resolve(message.guildId);
2064
+ if (!guild) return;
2065
+
2066
+ // Text channel (0), voice (2), category (4), link (5)
2067
+ const textChannel = await guild.createChannel({
2068
+ type: 0,
2069
+ name: 'general',
2070
+ });
2071
+
2072
+ // Category, then voice channel under it
2073
+ const category = await guild.createChannel({
2074
+ type: 4,
2075
+ name: 'Voice Chats',
2076
+ });
2077
+ const voiceChannel = await guild.createChannel({
2078
+ type: 2,
2079
+ name: 'Lounge',
2080
+ parent_id: category.id,
2081
+ bitrate: 64000,
2082
+ });
2083
+
2084
+ await message.reply(\`Created \${textChannel.name} and \${voiceChannel.name} in \${category.name}\`);
2085
+ });
2086
+
2087
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
2088
+ language: 'javascript',
2089
+ },
2090
+ {
2091
+ title: 'Channels — Fetch and Edit',
2092
+ description:
2093
+ 'Use guild.fetchChannels() to load all guild channels. Use channel.edit() to rename, set topic, move to a category, set slowmode, or update permission overwrites.',
2094
+ code: `const guild = client.guilds.get(guildId) ?? await client.guilds.resolve(guildId);
2095
+ if (!guild) return;
2096
+
2097
+ const channels = await guild.fetchChannels();
2098
+
2099
+ // Edit a text channel
2100
+ const channel = guild.channels.get(channelId);
2101
+ if (channel) {
2102
+ await channel.edit({
2103
+ name: 'renamed-channel',
2104
+ topic: 'New topic here',
2105
+ parent_id: categoryId, // Move under category
2106
+ rate_limit_per_user: 5, // 5 second slowmode
2107
+ nsfw: false,
2108
+ });
2109
+ }`,
2110
+ language: 'javascript',
2111
+ },
2112
+ {
2113
+ title: 'Channels — Delete and Reorder',
2114
+ description:
2115
+ 'Use channel.delete() to remove a channel. Use guild.setChannelPositions() to reorder channels or move them between categories.',
2116
+ code: `// Delete channel (silent: true skips system message)
2117
+ await channel.delete();
2118
+ await channel.delete({ silent: true });
2119
+
2120
+ // Reorder channels
2121
+ await guild.setChannelPositions([
2122
+ { id: channelId1, position: 0 },
2123
+ { id: channelId2, position: 1, parent_id: categoryId },
2124
+ ]);`,
2125
+ language: 'javascript',
2126
+ },
2127
+ {
2128
+ title: 'Channel Permission Overwrites',
2129
+ description:
2130
+ 'Use channel.editPermission() to add or update overwrites (type 0=role, 1=member). Use channel.deletePermission() to remove. Use resolvePermissionsToBitfield() for allow/deny bitfields.',
2131
+ code: `import { resolvePermissionsToBitfield, PermissionFlags } from '@erinjs/core';
2132
+
2133
+ // Deny SendMessages for a role (type 0=role, 1=member)
2134
+ await channel.editPermission(roleId, {
2135
+ type: 0,
2136
+ deny: resolvePermissionsToBitfield(['SendMessages']),
2137
+ });
2138
+
2139
+ // Allow ViewChannel for a specific member
2140
+ await channel.editPermission(userId, {
2141
+ type: 1,
2142
+ allow: resolvePermissionsToBitfield([PermissionFlags.ViewChannel]),
2143
+ });
2144
+
2145
+ // Remove overwrite
2146
+ await channel.deletePermission(roleId);`,
2147
+ language: 'javascript',
2148
+ },
2149
+ {
2150
+ title: 'Roles — Quick Reference',
2151
+ description:
2152
+ 'Create roles with guild.createRole(), fetch with guild.fetchRoles() or guild.fetchRole(roleId). Add/remove with guild.addRoleToMember() / guild.removeRoleFromMember(). Reorder with guild.setRolePositions().',
2153
+ code: `// See the Roles guide for full examples and permission bitfields.
2154
+ const role = await guild.createRole({ name: 'Mod', permissions: ['KickMembers', 'BanMembers'] });
2155
+ await guild.addRoleToMember(userId, role.id);
2156
+ await guild.removeRoleFromMember(userId, role.id);
2157
+ await guild.setRolePositions([{ id: role.id, position: 5 }]);`,
2158
+ language: 'javascript',
2159
+ tip: 'See the Roles guide for full examples, permission bitfields, and role.edit() / role.delete().',
2160
+ },
2161
+ {
2162
+ title: 'Invites',
2163
+ description:
2164
+ 'Use channel.createInvite() to create an invite. Use channel.fetchInvites() to list channel invites. Use invite.delete() to revoke. invite.url gives the full invite URL.',
2165
+ code: `import { Client, Events } from '@erinjs/core';
2166
+
2167
+ const client = new Client({ intents: 0 });
2168
+
2169
+ client.on(Events.MessageCreate, async (message) => {
2170
+ if (!message.content.startsWith('!invite') || !message.guildId) return;
2171
+ const channel = message.channel;
2172
+ if (!channel?.createInvite) return;
2173
+
2174
+ if (message.content === '!invite') {
2175
+ const invite = await channel.createInvite({
2176
+ max_age: 86400, // 24 hours
2177
+ max_uses: 10,
2178
+ temporary: false,
2179
+ });
2180
+ await message.reply(\`Invite: \${invite.url}\`);
2181
+ }
2182
+
2183
+ if (message.content === '!invitelist') {
2184
+ const invites = await channel.fetchInvites();
2185
+ const list = invites.map((i) => \`\${i.code} (\${i.max_uses ?? '∞'} uses)\`).join('\\n');
2186
+ await message.reply(list || 'No invites.');
2187
+ }
2188
+
2189
+ if (message.content.startsWith('!inviterevoke ')) {
2190
+ const code = message.content.slice(13).trim();
2191
+ const invites = await channel.fetchInvites();
2192
+ const inv = invites.find((i) => i.code === code);
2193
+ if (inv) {
2194
+ await inv.delete();
2195
+ await message.reply('Invite revoked.');
2196
+ }
2197
+ }
2198
+ });
2199
+
2200
+ await client.login(process.env.FLUXER_BOT_TOKEN);`,
2201
+ language: 'javascript',
2202
+ },
2203
+ {
2204
+ title: 'Quick Reference',
2205
+ table: {
2206
+ headers: ['API', 'Method', 'Purpose'],
2207
+ codeColumns: [0, 1],
2208
+ rows: [
2209
+ ['Channels', 'guild.createChannel()', 'Create text, voice, category, or link channel'],
2210
+ ['Channels', 'guild.fetchChannels()', 'Fetch all guild channels'],
2211
+ ['Channels', 'channel.edit()', 'Rename, set topic, slowmode, parent, overwrites'],
2212
+ ['Channels', 'channel.delete()', 'Delete a channel'],
2213
+ ['Channels', 'guild.setChannelPositions()', 'Reorder or reparent channels'],
2214
+ ['Channels', 'channel.editPermission()', 'Add or update permission overwrite'],
2215
+ ['Channels', 'channel.deletePermission()', 'Remove permission overwrite'],
2216
+ ['Roles', 'guild.createRole()', 'Create a role'],
2217
+ ['Roles', 'guild.addRoleToMember()', 'Add role to member'],
2218
+ ['Roles', 'guild.removeRoleFromMember()', 'Remove role from member'],
2219
+ ['Invites', 'channel.createInvite()', 'Create invite with max_uses, max_age'],
2220
+ ['Invites', 'channel.fetchInvites()', 'List channel invites'],
2221
+ ['Invites', 'invite.delete()', 'Revoke invite'],
2222
+ ],
2223
+ },
2224
+ },
2225
+ ],
2226
+ },
2227
+ {
2228
+ id: 'emojis',
2229
+ slug: 'emojis',
2230
+ title: 'Emojis & Stickers',
2231
+ description:
2232
+ 'Fetch, create, edit, and delete guild emojis and stickers. Use guild.fetchEmojis(), guild.createEmojisBulk(), and guild.createStickersBulk().',
2233
+ category: 'emojis',
2234
+ sections: [
2235
+ {
2236
+ title: 'Fetch Emojis',
2237
+ description:
2238
+ 'Use guild.fetchEmojis() to get all emojis in a guild. Cached in guild.emojis. Use guild.fetchEmoji(emojiId) for a single emoji. Use emoji.delete() to remove an emoji (e.g. autocreated ones).',
2239
+ code: `import { Client, Events } from '@erinjs/core';
2240
+
2241
+ const client = new Client({ intents: 0 });
2242
+
2243
+ client.on(Events.MessageCreate, async (message) => {
2244
+ if (!message.guildId || message.content !== '!emojis') return;
2245
+ const guild = client.guilds.get(message.guildId) ?? await client.guilds.resolve(message.guildId);
2246
+ if (!guild) return;
2247
+
2248
+ const emojis = await guild.fetchEmojis();
2249
+ const list = emojis.map((e) => \`:\${e.name}: (\${e.id})\`).join(', ');
2250
+ await message.reply(emojis.length ? list : 'No emojis.');
2251
+
2252
+ // Or get from cache after fetching: guild.emojis.get(emojiId)
2253
+ });
2254
+
2255
+ // Fetch single emoji by ID
2256
+ const emoji = await guild.fetchEmoji(emojiId);
2257
+ await emoji.delete();`,
2258
+ language: 'javascript',
2259
+ },
2260
+ {
2261
+ title: 'Create Emojis & Stickers',
2262
+ description:
2263
+ 'Use guild.createEmojisBulk() and guild.createStickersBulk() with base64 image data. Use emoji.edit() / emoji.delete() and sticker.edit() / sticker.delete() for individual updates.',
2264
+ code: `import { Client, Events } from '@erinjs/core';
2265
+
2266
+ const client = new Client({ intents: 0 });
2267
+
2268
+ // Create emoji from URL (fetch and convert to base64)
2269
+ async function createEmojiFromUrl(guild, name, imageUrl) {
2270
+ const res = await fetch(imageUrl);
2271
+ const buf = await res.arrayBuffer();
2272
+ const base64 = Buffer.from(buf).toString('base64');
2273
+ const [emoji] = await guild.createEmojisBulk([{ name, image: base64 }]);
2274
+ return emoji;
2275
+ }
2276
+
2277
+ client.on(Events.MessageCreate, async (message) => {
2278
+ if (!message.guildId || !message.content.startsWith('!addemoji ')) return;
2279
+ const guild = client.guilds.get(message.guildId) ?? await client.guilds.resolve(message.guildId);
2280
+ if (!guild) return;
2281
+
2282
+ const [_, name, url] = message.content.split(/\\s+/);
2283
+ if (!name || !url) return;
2284
+ const emoji = await createEmojiFromUrl(guild, name, url);
2285
+ await message.reply(\`Created emoji :\${emoji.name}:\`);
2286
+ });
2287
+
2288
+ // Bulk create stickers
2289
+ const stickers = await guild.createStickersBulk([
2290
+ { name: 'cool', image: base64Image, description: 'A cool sticker' },
2291
+ ]);
2292
+
2293
+ // Edit and delete
2294
+ await emoji.edit({ name: 'newname' });
2295
+ await emoji.delete();`,
2296
+ language: 'javascript',
2297
+ },
2298
+ {
2299
+ title: 'Quick Reference',
2300
+ table: {
2301
+ headers: ['API', 'Method', 'Purpose'],
2302
+ codeColumns: [0, 1],
2303
+ rows: [
2304
+ ['Emojis', 'guild.fetchEmojis()', 'Fetch all guild emojis (cached in guild.emojis)'],
2305
+ ['Emojis', 'guild.fetchEmoji(emojiId)', 'Fetch single emoji by ID'],
2306
+ ['Emojis', 'guild.createEmojisBulk()', 'Bulk create emojis (base64 image)'],
2307
+ ['Stickers', 'guild.createStickersBulk()', 'Bulk create stickers'],
2308
+ ],
2309
+ },
2310
+ },
2311
+ ],
2312
+ },
2313
+ ];
2314
+
2315
+ const CATEGORY_LABELS: Record<string, string> = {
2316
+ 'getting-started': 'Getting Started',
2317
+ 'sending-messages': 'Sending Messages',
2318
+ media: 'Media',
2319
+ channels: 'Channels',
2320
+ emojis: 'Emojis',
2321
+ webhooks: 'Webhooks',
2322
+ voice: 'Voice',
2323
+ events: 'Events',
2324
+ other: 'Other',
2325
+ };
2326
+
2327
+ /** Category order for guides index (Getting Started first, etc). */
2328
+ export const CATEGORY_ORDER: string[] = [
2329
+ 'getting-started',
2330
+ 'sending-messages',
2331
+ 'media',
2332
+ 'channels',
2333
+ 'emojis',
2334
+ 'webhooks',
2335
+ 'voice',
2336
+ 'events',
2337
+ 'other',
2338
+ ];
2339
+
2340
+ /** Slugs of guides to show as quick links on the guides index. */
2341
+ export const QUICK_LINK_SLUGS: string[] = [
2342
+ 'installation',
2343
+ 'basic-bot',
2344
+ 'sending-without-reply',
2345
+ 'embeds',
2346
+ 'attachments',
2347
+ 'attachments-by-url',
2348
+ 'permissions',
2349
+ 'moderation',
2350
+ 'channels',
2351
+ 'emojis',
2352
+ 'roles',
2353
+ 'prefix-commands',
2354
+ ];
2355
+
2356
+ export function getCategoryLabel(cat?: string): string {
2357
+ return (cat && CATEGORY_LABELS[cat]) ?? 'Guides';
2358
+ }
2359
+
2360
+ export function getGuideBySlug(slug: string): Guide | undefined {
2361
+ return guides.find((g) => g.slug === slug);
2362
+ }