@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,1146 @@
1
+ /**
2
+ * Fluxer Example Bot
3
+ *
4
+ * Demonstrates prefix commands, embeds, DMs, voice join, audio, and video playback.
5
+ * DMs: !dm (DM yourself), !dmuser @user [message] (DM another user).
6
+ * Guild profile: !setnick [nickname] (change nickname), !setavatar [url] (change guild avatar).
7
+ * Voice: !play (joins your VC and plays WebM/Opus audio via youtube-dl-exec). No FFmpeg.
8
+ * Video: !playvideo [url] [480p|720p|1080p|1440p|4k] (streams MP4 in your VC; default 720p 30fps).
9
+ * Resolution forces FFmpeg path. Set FLUXER_VIDEO_FFMPEG=1 to use FFmpeg without resolution.
10
+ * !stop stops playback and leaves.
11
+ *
12
+ * Usage (from repo root after npm install && npm run build):
13
+ * FLUXER_BOT_TOKEN=your_token node examples/ping-bot.js
14
+ *
15
+ * Optional env: FLUXER_API_URL for custom API base; VOICE_DEBUG=1 for voice connection logs;
16
+ * SETAVATAR_DEBUG=1 for guild avatar request/response logs.
17
+ */
18
+
19
+ import youtubedl from 'youtube-dl-exec';
20
+ import {
21
+ Client,
22
+ Events,
23
+ EmbedBuilder,
24
+ Routes,
25
+ User,
26
+ VoiceChannel,
27
+ cdnBannerURL,
28
+ cdnMemberAvatarURL,
29
+ UserFlagsBits,
30
+ PermissionFlags,
31
+ } from '@erinjs/core';
32
+ import { getVoiceManager, LiveKitRtcConnection } from '@erinjs/voice';
33
+
34
+ /** Fixed non‑copyrighted track; we get a direct WebM/Opus URL so the voice package can play without FFmpeg. */
35
+ const PLAY_URL = 'https://www.youtube.com/watch?v=eVTXPUF4Oz4';
36
+ const YTDLP_FORMAT = 'bestaudio[ext=webm][acodec=opus]/bestaudio[ext=webm]/bestaudio';
37
+
38
+ /** Default MP4 video URL for !playvideo (short public domain clip). Must be direct MP4 (H.264). */
39
+ const DEFAULT_VIDEO_URL = 'https://www.w3schools.com/html/mov_bbb.mp4';
40
+
41
+ /** yt-dlp format for MP4 video (prefer 1080p, then 720p, 360p, then best). */
42
+ const YTDLP_VIDEO_FORMAT =
43
+ 'best[height<=1080][ext=mp4]/best[height<=1080]/22/18/best[ext=mp4]/best';
44
+
45
+ /** Regex for YouTube and similar sites that yt-dlp supports for video extraction. */
46
+ const YOUTUBE_LIKE = /youtube\.com|youtu\.be|yt\.be/i;
47
+
48
+ async function getStreamUrl(url) {
49
+ const result = await youtubedl(
50
+ url,
51
+ {
52
+ getUrl: true,
53
+ f: YTDLP_FORMAT,
54
+ formatSort: 'acodec:opus',
55
+ noWarnings: true,
56
+ noPlaylist: true,
57
+ },
58
+ { timeout: 15000 },
59
+ );
60
+ return String(result ?? '').trim();
61
+ }
62
+
63
+ /** Get a direct MP4 video URL from YouTube or similar. Returns null on failure. */
64
+ async function getVideoUrl(url) {
65
+ const result = await youtubedl(
66
+ url,
67
+ {
68
+ getUrl: true,
69
+ f: YTDLP_VIDEO_FORMAT,
70
+ noWarnings: true,
71
+ noPlaylist: true,
72
+ },
73
+ { timeout: 20000 },
74
+ );
75
+ return String(result ?? '').trim() || null;
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ // Configuration
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ const PREFIX = '!';
83
+
84
+ /** Per-guild play state for auto-reconnect when LiveKit server sends leave. */
85
+ const playState = new Map();
86
+
87
+ function setPlayState(guildId, channel, streamUrl) {
88
+ playState.set(guildId, { channel, streamUrl });
89
+ }
90
+
91
+ function clearPlayState(guildId) {
92
+ playState.delete(guildId);
93
+ }
94
+ const BRAND_COLOR = 0x4641d9;
95
+
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+ // Helpers
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+
100
+ /** Format milliseconds into human-readable uptime. */
101
+ function formatUptime(ms) {
102
+ const seconds = Math.floor(ms / 1000) % 60;
103
+ const minutes = Math.floor(ms / (1000 * 60)) % 60;
104
+ const hours = Math.floor(ms / (1000 * 60 * 60)) % 24;
105
+ const days = Math.floor(ms / (1000 * 60 * 60 * 24));
106
+
107
+ const parts = [];
108
+ if (days > 0) parts.push(`${days}d`);
109
+ if (hours > 0) parts.push(`${hours}h`);
110
+ if (minutes > 0) parts.push(`${minutes}m`);
111
+ parts.push(`${seconds}s`);
112
+
113
+ return parts.join(' ');
114
+ }
115
+
116
+ /** Measure REST API latency by timing a lightweight request. */
117
+ async function measureApiLatency(client) {
118
+ const start = Date.now();
119
+ try {
120
+ await client.rest.get(Routes.gatewayBot());
121
+ } catch {
122
+ // Ignore errors; we just want the round-trip time
123
+ }
124
+ return Date.now() - start;
125
+ }
126
+
127
+ /** Resolve a user ID from the first argument: mention (<@id> or <@!id>) or raw snowflake. Returns null if invalid. */
128
+ function resolveUserId(arg, authorId) {
129
+ if (!arg) return authorId;
130
+ const mentionMatch = arg.match(/^<@!?(\d+)>$/);
131
+ if (mentionMatch) return mentionMatch[1];
132
+ if (/^\d{17,19}$/.test(arg)) return arg;
133
+ return null;
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Command Handlers
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+
140
+ const commands = new Map();
141
+
142
+ commands.set('ping', {
143
+ description: 'Check bot and API latency',
144
+ async execute(message, client) {
145
+ const apiLatency = await measureApiLatency(client);
146
+
147
+ const embed = new EmbedBuilder()
148
+ .setTitle('Pong!')
149
+ .setColor(BRAND_COLOR)
150
+ .addFields({ name: 'API Latency', value: `\`${apiLatency}ms\``, inline: true })
151
+ .setFooter({ text: 'Fluxer Bot' })
152
+ .setTimestamp();
153
+
154
+ await message.reply({ embeds: [embed.toJSON()] });
155
+ },
156
+ });
157
+
158
+ commands.set('info', {
159
+ description: 'Display bot information',
160
+ async execute(message, client) {
161
+ const uptime = client.readyAt ? Date.now() - client.readyAt.getTime() : 0;
162
+ const apiLatency = await measureApiLatency(client);
163
+
164
+ const embed = new EmbedBuilder()
165
+ .setTitle('Bot Information')
166
+ .setColor(BRAND_COLOR)
167
+ .setThumbnail(client.user?.avatarURL?.() ?? null)
168
+ .addFields(
169
+ { name: 'Username', value: client.user?.username ?? 'Unknown', inline: true },
170
+ { name: 'Guilds', value: `${client.guilds.size}`, inline: true },
171
+ { name: 'Channels', value: `${client.channels.size}`, inline: true },
172
+ { name: 'Uptime', value: formatUptime(uptime), inline: true },
173
+ { name: 'API Latency', value: `${apiLatency}ms`, inline: true },
174
+ { name: 'Node.js', value: process.version, inline: true },
175
+ )
176
+ .setFooter({ text: 'Powered by @erinjs/core' })
177
+ .setTimestamp();
178
+
179
+ await message.reply({ embeds: [embed.toJSON()] });
180
+ },
181
+ });
182
+
183
+ commands.set('setnick', {
184
+ description: "Change the bot's nickname in this server (!setnick [nickname])",
185
+ async execute(message, client, args) {
186
+ const guildId = message.guildId;
187
+ if (!guildId) {
188
+ await message.reply('Use this command in a server.');
189
+ return;
190
+ }
191
+ const guild = client.guilds.get(guildId) ?? (await client.guilds.fetch(guildId));
192
+ if (!guild) {
193
+ await message.reply('Could not find this server.');
194
+ return;
195
+ }
196
+ const me = guild.members.me ?? (await guild.members.fetchMe());
197
+ const newNick = args.join(' ').trim() || null;
198
+ try {
199
+ await me.edit({ nick: newNick });
200
+ await message.reply(
201
+ newNick
202
+ ? `Nickname set to \`${newNick}\` in this server.`
203
+ : 'Nickname cleared (showing username again).',
204
+ );
205
+ } catch {
206
+ await message
207
+ .reply('Failed to change nickname. The bot may need Change Nickname permission.')
208
+ .catch(() => {});
209
+ }
210
+ },
211
+ });
212
+
213
+ const SETAVATAR_DEBUG =
214
+ process.env.SETAVATAR_DEBUG === '1' || process.env.SETAVATAR_DEBUG === 'true';
215
+
216
+ commands.set('setavatar', {
217
+ description: "Change the bot's guild avatar (!setavatar [image URL] or !setavatar clear)",
218
+ async execute(message, client, args) {
219
+ const guildId = message.guildId;
220
+ if (!guildId) {
221
+ await message.reply('Use this command in a server.');
222
+ return;
223
+ }
224
+ const guild = client.guilds.get(guildId) ?? (await client.guilds.fetch(guildId));
225
+ if (!guild) {
226
+ await message.reply('Could not find this server.');
227
+ return;
228
+ }
229
+ const me = guild.members.me ?? (await guild.members.fetchMe());
230
+ const arg = args[0]?.toLowerCase();
231
+ if (arg === 'clear' || arg === 'reset') {
232
+ try {
233
+ if (SETAVATAR_DEBUG) {
234
+ console.log(
235
+ '[setavatar] PATCH /guilds/%s/members/@me with avatar: null (clear)',
236
+ guildId,
237
+ );
238
+ }
239
+ await me.edit({ avatar: null });
240
+ if (SETAVATAR_DEBUG) {
241
+ const updated = await guild.fetchMember(me.id);
242
+ console.log('[setavatar] Response: avatar=%s', updated.avatar ?? 'null');
243
+ }
244
+ await message.reply('Guild avatar cleared. Showing global avatar again.');
245
+ } catch (err) {
246
+ if (SETAVATAR_DEBUG) console.error('[setavatar] Clear failed:', err);
247
+ await message.reply('Failed to clear guild avatar.').catch(() => {});
248
+ }
249
+ return;
250
+ }
251
+ const url = args[0]?.trim();
252
+ if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
253
+ await message.reply(
254
+ 'Provide an image URL: `!setavatar https://example.com/image.png` or `!setavatar clear` to reset.',
255
+ );
256
+ return;
257
+ }
258
+ try {
259
+ const controller = new AbortController();
260
+ const timeout = setTimeout(() => controller.abort(), 30000);
261
+ const res = await fetch(url, { signal: controller.signal });
262
+ clearTimeout(timeout);
263
+ if (!res.ok) {
264
+ await message.reply(`Could not fetch image: ${res.status}`);
265
+ return;
266
+ }
267
+ const contentType = res.headers.get('content-type') ?? 'image/png';
268
+ const mime = contentType.split(';')[0].trim();
269
+ if (!mime.startsWith('image/')) {
270
+ await message.reply('URL must point to an image (png, jpeg, gif, webp).');
271
+ return;
272
+ }
273
+ const buf = Buffer.from(await res.arrayBuffer());
274
+ const base64 = buf.toString('base64');
275
+ const dataUri = `data:${mime};base64,${base64}`;
276
+ if (SETAVATAR_DEBUG) {
277
+ console.log(
278
+ '[setavatar] PATCH /guilds/%s/members/@me with avatar (dataUri len=%d, mime=%s)',
279
+ guildId,
280
+ dataUri.length,
281
+ mime,
282
+ );
283
+ }
284
+ await me.edit({ avatar: dataUri });
285
+ const updated = await guild.fetchMember(me.id);
286
+ if (SETAVATAR_DEBUG) {
287
+ console.log('[setavatar] Response: avatar=%s', updated.avatar ?? 'null');
288
+ }
289
+ if (dataUri && !updated.avatar) {
290
+ await message.reply(
291
+ 'Request succeeded but the avatar was not applied. On Fluxer, guild avatars require a premium subscription—bots cannot set guild avatars.',
292
+ );
293
+ } else {
294
+ await message.reply('Guild avatar updated!');
295
+ }
296
+ } catch (err) {
297
+ if (SETAVATAR_DEBUG) console.error('[setavatar] Set failed:', err);
298
+ if (err?.name === 'AbortError') {
299
+ await message.reply('Timed out fetching image (30s).');
300
+ } else {
301
+ await message
302
+ .reply('Failed to set guild avatar. Check the URL and try again.')
303
+ .catch(() => {});
304
+ }
305
+ }
306
+ },
307
+ });
308
+
309
+ commands.set('bme', {
310
+ description: "Display guild.members.me (bot's member) info in this server",
311
+ async execute(message, client) {
312
+ const guildId = message.guildId;
313
+ if (!guildId) {
314
+ await message.reply('Use this command in a server.');
315
+ return;
316
+ }
317
+ const guild = client.guilds.get(guildId) ?? (await client.guilds.fetch(guildId));
318
+ if (!guild) {
319
+ await message.reply('Could not find this server.');
320
+ return;
321
+ }
322
+ const me = guild.members.me ?? (await guild.members.fetchMe());
323
+ const avatarUrl = me.displayAvatarURL({ size: 256 });
324
+ const accentColor = me.accentColor ?? me.user.avatarColor ?? BRAND_COLOR;
325
+ const roleNames = me.roles
326
+ .filter((id) => id !== guild.id)
327
+ .map((id) => guild.roles.get(id)?.name ?? id);
328
+ const permNames = [];
329
+ try {
330
+ for (const [name, bit] of Object.entries(PermissionFlags)) {
331
+ if (typeof bit === 'number' && me.permissions.has(bit)) permNames.push(name);
332
+ }
333
+ } catch {
334
+ /* ignore import error */
335
+ }
336
+
337
+ const embed = new EmbedBuilder()
338
+ .setTitle('guild.members.me')
339
+ .setDescription("Bot's GuildMember in this server")
340
+ .setColor(accentColor)
341
+ .setThumbnail(avatarUrl)
342
+ .addFields(
343
+ { name: 'ID', value: `\`${me.id}\``, inline: true },
344
+ { name: 'Username', value: me.user.username ?? '—', inline: true },
345
+ { name: 'Display name', value: me.displayName ?? '—', inline: true },
346
+ { name: 'Nickname', value: me.nick ?? '*(none)*', inline: true },
347
+ { name: 'Joined', value: me.joinedAt.toISOString(), inline: true },
348
+ {
349
+ name: 'Roles',
350
+ value: roleNames.length ? roleNames.slice(0, 15).join(', ') : '*(none)*',
351
+ inline: false,
352
+ },
353
+ { name: 'Mute', value: String(me.mute), inline: true },
354
+ { name: 'Deaf', value: String(me.deaf), inline: true },
355
+ {
356
+ name: 'Permissions (sample)',
357
+ value: permNames.length ? permNames.slice(0, 12).join(', ') : '*(none)*',
358
+ inline: false,
359
+ },
360
+ )
361
+ .setFooter({ text: `Guild: ${guild.name}` })
362
+ .setTimestamp();
363
+
364
+ await message.reply({ embeds: [embed.toJSON()] });
365
+ },
366
+ });
367
+
368
+ commands.set('attachurl', {
369
+ description: 'Test file attachment by URL (sends image fetched from URL)',
370
+ async execute(message) {
371
+ const url = 'https://www.w3schools.com/html/pic_trulli.jpg';
372
+ await message.reply({
373
+ content: 'File attached from URL:',
374
+ files: [{ name: 'trulli.jpg', url }],
375
+ });
376
+ },
377
+ });
378
+
379
+ /** Get human-readable badge names from user flags (checks common badges that fit in 32-bit). */
380
+ function getBadgeNames(flags) {
381
+ if (flags == null || typeof flags !== 'number') return [];
382
+ const badges = [];
383
+ const DISPLAY_FLAGS = [
384
+ ['Staff', UserFlagsBits.Staff],
385
+ ['Partner', UserFlagsBits.Partner],
386
+ ['Bug Hunter', UserFlagsBits.BugHunter],
387
+ ['Ctp Member', UserFlagsBits.CtpMember],
388
+ ['Friendly Bot', UserFlagsBits.FriendlyBot],
389
+ ['Friendly Bot (Manual)', UserFlagsBits.FriendlyBotManualApproval],
390
+ ];
391
+ for (const [name, bit] of DISPLAY_FLAGS) {
392
+ if ((flags & bit) === bit) badges.push(name);
393
+ }
394
+ return badges;
395
+ }
396
+
397
+ function addProfileFields(fields, profile, profileData, prefix = '') {
398
+ if (!profile || typeof profile !== 'object') return;
399
+ if (profile.pronouns != null && profile.pronouns !== '')
400
+ fields.push({
401
+ name: prefix + 'Pronouns',
402
+ value: String(profile.pronouns).slice(0, 40),
403
+ inline: true,
404
+ });
405
+ if (profile.bio != null && profile.bio !== '')
406
+ fields.push({
407
+ name: prefix + 'Bio',
408
+ value: String(profile.bio).slice(0, 1024) || '—',
409
+ });
410
+ if (profile.banner != null && profile.banner !== '')
411
+ fields.push({ name: prefix + 'Banner', value: 'Set', inline: true });
412
+ const accent = profile.accent_color ?? profile.banner_color;
413
+ if (accent != null)
414
+ fields.push({
415
+ name: prefix + 'Accent color',
416
+ value: `#${Number(accent).toString(16).padStart(6, '0')}`,
417
+ inline: true,
418
+ });
419
+ if (profile.theme != null)
420
+ fields.push({ name: prefix + 'Theme', value: String(profile.theme), inline: true });
421
+ const mutualCount = profileData?.mutual_guilds?.length ?? profileData?.mutual_guild_ids?.length;
422
+ if (mutualCount != null && mutualCount > 0)
423
+ fields.push({ name: prefix + 'Mutual servers', value: String(mutualCount), inline: true });
424
+ const connected = profileData?.connected_accounts;
425
+ if (connected?.length)
426
+ fields.push({
427
+ name: prefix + 'Connected accounts',
428
+ value:
429
+ connected
430
+ .map((a) => a.name ?? a.type ?? '?')
431
+ .slice(0, 5)
432
+ .join(', ') + (connected.length > 5 ? ` (+${connected.length - 5})` : ''),
433
+ inline: true,
434
+ });
435
+ }
436
+
437
+ commands.set('userinfo', {
438
+ description: "Show a user's profile (mention or user ID); no arg = yourself",
439
+ async execute(message, client, args) {
440
+ const userId = resolveUserId(args[0], message.author.id);
441
+ if (!userId) {
442
+ await message.reply(
443
+ 'Provide a user mention (`@user`) or a user ID. Example: `!userinfo @Someone` or `!userinfo 123456789012345678`',
444
+ );
445
+ return;
446
+ }
447
+ let data;
448
+ try {
449
+ data = await client.users.fetchWithProfile(userId, {
450
+ guildId: message.guildId ?? undefined,
451
+ });
452
+ } catch {
453
+ await message.reply(
454
+ 'Could not fetch that user. They may not exist or the ID may be invalid.',
455
+ );
456
+ return;
457
+ }
458
+ const {
459
+ user,
460
+ userData,
461
+ globalProfile: globalProfileData,
462
+ serverProfile: serverProfileData,
463
+ memberData,
464
+ } = data;
465
+ const globalProfileUserProfile = globalProfileData?.user_profile;
466
+ const serverProfileUserProfile = serverProfileData?.user_profile;
467
+ const bannerUrl =
468
+ serverProfileUserProfile?.banner != null && serverProfileUserProfile.banner !== ''
469
+ ? cdnBannerURL(userData.id, serverProfileUserProfile.banner, { size: 512 })
470
+ : globalProfileUserProfile?.banner != null && globalProfileUserProfile.banner !== ''
471
+ ? cdnBannerURL(userData.id, globalProfileUserProfile.banner, { size: 512 })
472
+ : userData.banner != null && userData.banner !== ''
473
+ ? cdnBannerURL(userData.id, userData.banner, { size: 512 })
474
+ : null;
475
+ const accentColor =
476
+ serverProfileUserProfile &&
477
+ (serverProfileUserProfile.accent_color ?? serverProfileUserProfile.banner_color) != null
478
+ ? Number(serverProfileUserProfile.accent_color ?? serverProfileUserProfile.banner_color)
479
+ : globalProfileUserProfile &&
480
+ (globalProfileUserProfile.accent_color ?? globalProfileUserProfile.banner_color) != null
481
+ ? Number(globalProfileUserProfile.accent_color ?? globalProfileUserProfile.banner_color)
482
+ : userData.avatar_color != null
483
+ ? Number(userData.avatar_color)
484
+ : memberData?.accent_color != null
485
+ ? Number(memberData.accent_color)
486
+ : BRAND_COLOR;
487
+ // Prefer guild-specific avatar when in a server (like Discord.js member.displayAvatarURL)
488
+ const avatarUrl =
489
+ message.guildId && memberData?.avatar
490
+ ? cdnMemberAvatarURL(message.guildId, userData.id, memberData.avatar, { size: 256 })
491
+ : user.displayAvatarURL({ size: 256 });
492
+ const displayName = userData.global_name ?? userData.username ?? 'User';
493
+ const embed = new EmbedBuilder()
494
+ .setTitle(`${displayName}'s profile`)
495
+ .setAuthor({ name: displayName, iconURL: avatarUrl })
496
+ .setColor(accentColor);
497
+ if (bannerUrl) embed.setImage(bannerUrl);
498
+
499
+ const fields = [
500
+ { name: 'Username', value: userData.username ?? '—', inline: true },
501
+ {
502
+ name: 'Display name',
503
+ value: userData.global_name ?? userData.username ?? '—',
504
+ inline: true,
505
+ },
506
+ { name: 'ID', value: `\`${userData.id}\``, inline: true },
507
+ { name: 'Bot', value: userData.bot ? 'Yes' : 'No', inline: true },
508
+ { name: 'System', value: userData.system ? 'Yes' : 'No', inline: true },
509
+ { name: 'Discriminator', value: userData.discriminator ?? '0', inline: true },
510
+ {
511
+ name: 'Avatar',
512
+ value: userData.avatar
513
+ ? userData.avatar.startsWith('a_')
514
+ ? 'Animated'
515
+ : 'Set'
516
+ : 'Default',
517
+ inline: true,
518
+ },
519
+ {
520
+ name: 'Avatar color',
521
+ value:
522
+ userData.avatar_color != null
523
+ ? `#${Number(userData.avatar_color).toString(16).padStart(6, '0')}`
524
+ : '—',
525
+ inline: true,
526
+ },
527
+ {
528
+ name: 'User banner',
529
+ value: userData.banner != null && userData.banner !== '' ? 'Set' : '—',
530
+ inline: true,
531
+ },
532
+ ];
533
+
534
+ const flags = userData.flags ?? userData.public_flags;
535
+ if (flags != null) {
536
+ const badgeNames = getBadgeNames(flags);
537
+ fields.push({
538
+ name: 'Badges',
539
+ value: badgeNames.length ? badgeNames.join(', ') : '—',
540
+ inline: false,
541
+ });
542
+ fields.push({
543
+ name: 'Flags (raw)',
544
+ value: `\`${flags}\``,
545
+ inline: true,
546
+ });
547
+ }
548
+
549
+ if (globalProfileData) {
550
+ fields.push({ name: '\u200B', value: '**Global Profile**', inline: false });
551
+ addProfileFields(fields, globalProfileUserProfile, globalProfileData);
552
+ }
553
+
554
+ if (message.guildId && (serverProfileData || memberData)) {
555
+ const guild = client.guilds.get(message.guildId);
556
+ const guildName = guild?.name ?? 'this server';
557
+ fields.push({
558
+ name: '\u200B',
559
+ value: `**Server Profile** (${guildName})`,
560
+ inline: false,
561
+ });
562
+ if (serverProfileUserProfile && typeof serverProfileUserProfile === 'object') {
563
+ addProfileFields(fields, serverProfileUserProfile, serverProfileData, 'Server ');
564
+ }
565
+ if (memberData) {
566
+ const nick = memberData.nick ?? null;
567
+ if (nick)
568
+ fields.push({
569
+ name: 'Nickname',
570
+ value: String(nick).slice(0, 32),
571
+ inline: true,
572
+ });
573
+ if (memberData.joined_at)
574
+ fields.push({
575
+ name: 'Joined',
576
+ value: `<t:${Math.floor(new Date(memberData.joined_at).getTime() / 1000)}:R>`,
577
+ inline: true,
578
+ });
579
+ if (memberData.premium_since)
580
+ fields.push({
581
+ name: 'Boosting since',
582
+ value: `<t:${Math.floor(new Date(memberData.premium_since).getTime() / 1000)}:R>`,
583
+ inline: true,
584
+ });
585
+ if (memberData.communication_disabled_until) {
586
+ const until = new Date(memberData.communication_disabled_until);
587
+ if (until > new Date())
588
+ fields.push({
589
+ name: 'Timeout until',
590
+ value: `<t:${Math.floor(until.getTime() / 1000)}:F>`,
591
+ inline: true,
592
+ });
593
+ }
594
+ if (memberData.roles?.length) {
595
+ const roleIds = memberData.roles.filter((id) => id !== message.guildId);
596
+ const roleNames = guild
597
+ ? roleIds.map((id) => guild.roles.get(id)?.name ?? id).slice(0, 20)
598
+ : roleIds.slice(0, 20);
599
+ const display =
600
+ roleNames.length > 0
601
+ ? roleNames.join(', ') +
602
+ (roleIds.length > 20 ? ` (+${roleIds.length - 20} more)` : '')
603
+ : '—';
604
+ fields.push({ name: 'Roles', value: display.slice(0, 1024) || '—' });
605
+ }
606
+ if (memberData.avatar != null && memberData.avatar !== '')
607
+ fields.push({ name: 'Server avatar', value: 'Set', inline: true });
608
+ if (memberData.banner != null && memberData.banner !== '')
609
+ fields.push({ name: 'Server banner', value: 'Set', inline: true });
610
+ if (memberData.mute !== undefined)
611
+ fields.push({ name: 'Muted', value: memberData.mute ? 'Yes' : 'No', inline: true });
612
+ if (memberData.deaf !== undefined)
613
+ fields.push({ name: 'Deafened', value: memberData.deaf ? 'Yes' : 'No', inline: true });
614
+ }
615
+ }
616
+
617
+ embed
618
+ .addFields(...fields)
619
+ .setFooter({ text: `Requested by ${message.author.username}` })
620
+ .setTimestamp();
621
+ try {
622
+ await message.reply({ embeds: [embed.toJSON()] });
623
+ } catch (err) {
624
+ const fallback = `**${userData.username ?? userData.global_name ?? 'User'}** (ID: \`${userData.id}\`) — Could not send full embed (${err?.statusCode === 502 ? 'server error' : 'error'}). Try again.`;
625
+ await message.reply(fallback).catch(() => {});
626
+ }
627
+ },
628
+ });
629
+
630
+ const VERIFICATION_LEVELS = ['None', 'Low', 'Medium', 'High', 'Very High'];
631
+ const MFA_LEVELS = ['None', 'Elevated'];
632
+ const EXPLICIT_CONTENT_FILTERS = ['Disabled', 'Members without roles', 'All members'];
633
+ const DEFAULT_NOTIFICATION_LEVELS = ['All messages', 'Only mentions'];
634
+
635
+ commands.set('serverinfo', {
636
+ description: "Show this server's details",
637
+ async execute(message, client, args) {
638
+ const guildId = args[0] ?? message.guildId;
639
+ if (!guildId) {
640
+ await message.reply(
641
+ 'Use this command in a server or provide a guild ID: `!serverinfo [guild_id]`',
642
+ );
643
+ return;
644
+ }
645
+ let data;
646
+ try {
647
+ data = await client.rest.get(Routes.guild(guildId));
648
+ } catch {
649
+ await message.reply('Could not fetch that server. Check the ID or permissions.');
650
+ return;
651
+ }
652
+ const iconUrl = data.icon
653
+ ? `https://fluxerusercontent.com/icons/${data.id}/${data.icon}.png?size=256`
654
+ : null;
655
+ const embed = new EmbedBuilder()
656
+ .setTitle(data.name ?? 'Server')
657
+ .setColor(BRAND_COLOR)
658
+ .setThumbnail(iconUrl)
659
+ .addFields(
660
+ { name: 'ID', value: `\`${data.id}\``, inline: true },
661
+ { name: 'Owner ID', value: `\`${data.owner_id ?? '—'}\``, inline: true },
662
+ {
663
+ name: 'Verification',
664
+ value: VERIFICATION_LEVELS[data.verification_level] ?? String(data.verification_level),
665
+ inline: true,
666
+ },
667
+ {
668
+ name: 'MFA level',
669
+ value: MFA_LEVELS[data.mfa_level] ?? String(data.mfa_level),
670
+ inline: true,
671
+ },
672
+ {
673
+ name: 'AFK timeout',
674
+ value: data.afk_timeout != null ? `${data.afk_timeout}s` : '—',
675
+ inline: true,
676
+ },
677
+ { name: 'NSFW level', value: String(data.nsfw_level ?? 0), inline: true },
678
+ {
679
+ name: 'Explicit content filter',
680
+ value:
681
+ EXPLICIT_CONTENT_FILTERS[data.explicit_content_filter] ??
682
+ String(data.explicit_content_filter ?? 0),
683
+ inline: true,
684
+ },
685
+ {
686
+ name: 'Default notifications',
687
+ value: DEFAULT_NOTIFICATION_LEVELS[data.default_message_notifications] ?? '—',
688
+ inline: true,
689
+ },
690
+ {
691
+ name: 'Vanity URL',
692
+ value: data.vanity_url_code ? `/${data.vanity_url_code}` : '—',
693
+ inline: true,
694
+ },
695
+ {
696
+ name: 'System channel ID',
697
+ value: data.system_channel_id ? `\`${data.system_channel_id}\`` : '—',
698
+ inline: true,
699
+ },
700
+ {
701
+ name: 'Rules channel ID',
702
+ value: data.rules_channel_id ? `\`${data.rules_channel_id}\`` : '—',
703
+ inline: true,
704
+ },
705
+ {
706
+ name: 'AFK channel ID',
707
+ value: data.afk_channel_id ? `\`${data.afk_channel_id}\`` : '—',
708
+ inline: true,
709
+ },
710
+ { name: 'Features', value: data.features?.length ? data.features.join(', ') : '—' },
711
+ )
712
+ .setFooter({ text: `Requested by ${message.author.username}` })
713
+ .setTimestamp();
714
+ if (data.banner)
715
+ embed.setImage(
716
+ `https://fluxerusercontent.com/banners/${data.id}/${data.banner}.png?size=512`,
717
+ );
718
+ try {
719
+ await message.reply({ embeds: [embed.toJSON()] });
720
+ } catch {
721
+ await message
722
+ .reply(`Server: **${data.name}** (ID: \`${data.id}\`). Could not send embed.`)
723
+ .catch(() => {});
724
+ }
725
+ },
726
+ });
727
+
728
+ function resolveRoleIdOrName(arg) {
729
+ if (!arg) return null;
730
+ const mentionMatch = arg.match(/^<@&(\d+)>$/);
731
+ if (mentionMatch) return { type: 'id', value: mentionMatch[1] };
732
+ if (/^\d{17,19}$/.test(arg)) return { type: 'id', value: arg };
733
+ return { type: 'name', value: arg };
734
+ }
735
+
736
+ commands.set('roleinfo', {
737
+ description: "Show a role's details (role ID, mention, or name)",
738
+ async execute(message, client, args) {
739
+ const guildId = message.guildId;
740
+ if (!guildId) {
741
+ await message.reply('Use this command in a server.');
742
+ return;
743
+ }
744
+ const resolved = resolveRoleIdOrName(args[0]);
745
+ if (!resolved) {
746
+ await message.reply(
747
+ 'Provide a role ID, role mention (`@Role`), or role name. Example: `!roleinfo Moderator`',
748
+ );
749
+ return;
750
+ }
751
+ let roles;
752
+ try {
753
+ roles = await client.rest.get(Routes.guildRoles(guildId));
754
+ } catch {
755
+ await message.reply('Could not fetch roles for this server.');
756
+ return;
757
+ }
758
+ const roleList = Array.isArray(roles) ? roles : Object.values(roles ?? {});
759
+ const role =
760
+ roleList.find((r) =>
761
+ resolved.type === 'id'
762
+ ? r.id === resolved.value
763
+ : r.name && r.name.toLowerCase() === resolved.value.toLowerCase(),
764
+ ) ?? null;
765
+ if (!role) {
766
+ await message.reply(
767
+ resolved.type === 'id' ? 'No role found with that ID.' : 'No role found with that name.',
768
+ );
769
+ return;
770
+ }
771
+ const color = role.color != null && role.color !== 0 ? role.color : BRAND_COLOR;
772
+ const permStr = role.permissions ? String(role.permissions).slice(0, 1024) : '—';
773
+ const embed = new EmbedBuilder()
774
+ .setTitle(role.name ?? 'Role')
775
+ .setColor(color)
776
+ .addFields(
777
+ { name: 'ID', value: `\`${role.id}\``, inline: true },
778
+ { name: 'Name', value: role.name ?? '—', inline: true },
779
+ { name: 'Position', value: String(role.position ?? 0), inline: true },
780
+ {
781
+ name: 'Color',
782
+ value:
783
+ role.color != null && role.color !== 0
784
+ ? `#${Number(role.color).toString(16).padStart(6, '0')}`
785
+ : 'Default',
786
+ inline: true,
787
+ },
788
+ { name: 'Hoist', value: role.hoist ? 'Yes' : 'No', inline: true },
789
+ { name: 'Mentionable', value: role.mentionable ? 'Yes' : 'No', inline: true },
790
+ { name: 'Unicode emoji', value: role.unicode_emoji ?? '—', inline: true },
791
+ {
792
+ name: 'Hoist position',
793
+ value: role.hoist_position != null ? String(role.hoist_position) : '—',
794
+ inline: true,
795
+ },
796
+ { name: 'Permissions', value: permStr },
797
+ )
798
+ .setFooter({ text: `Requested by ${message.author.username}` })
799
+ .setTimestamp();
800
+ try {
801
+ await message.reply({ embeds: [embed.toJSON()] });
802
+ } catch {
803
+ await message
804
+ .reply(`Role: **${role.name}** (ID: \`${role.id}\`). Could not send embed.`)
805
+ .catch(() => {});
806
+ }
807
+ },
808
+ });
809
+
810
+ commands.set('dm', {
811
+ description: 'DM yourself (demo of user.send)',
812
+ async execute(message) {
813
+ try {
814
+ await message.author.send('You requested a DM! This is a direct message from the bot.');
815
+ await message.reply('Check your DMs! 📬');
816
+ } catch {
817
+ await message.reply('Could not DM you. You may have DMs disabled.').catch(() => {});
818
+ }
819
+ },
820
+ });
821
+
822
+ commands.set('dmuser', {
823
+ description: 'DM a user: !dmuser @user [message]',
824
+ async execute(message, client, args) {
825
+ const userId = resolveUserId(args[0], null);
826
+ if (!userId) {
827
+ await message.reply('Provide a user mention or ID. Example: `!dmuser @Someone Hello!`');
828
+ return;
829
+ }
830
+ const text = args.slice(1).join(' ') || 'Hello from the bot!';
831
+ try {
832
+ const userData = await client.rest.get(Routes.user(userId));
833
+ const user = new User(client, userData);
834
+ await user.send(text);
835
+ await message.reply(`Sent DM to **${user.globalName ?? user.username}**.`);
836
+ } catch {
837
+ await message
838
+ .reply('Could not send DM. The user may not exist or may have DMs disabled.')
839
+ .catch(() => {});
840
+ }
841
+ },
842
+ });
843
+
844
+ commands.set('replytest', {
845
+ description: 'Test message.reply() - bot replies to your message (shows as reply thread)',
846
+ async execute(message) {
847
+ await message.reply('This message is a reply to yours! You should see it linked/threaded.');
848
+ },
849
+ });
850
+
851
+ commands.set('react', {
852
+ description: 'Reply with a message and add reactions',
853
+ async execute(message) {
854
+ const reply = await message.reply('React below! 👇');
855
+ await reply.react('👍');
856
+ await reply.react('❤️');
857
+ await reply.react('🎉');
858
+ },
859
+ });
860
+
861
+ commands.set('editembed', {
862
+ description: 'Demonstrate editing an existing message embed',
863
+ async execute(message) {
864
+ const embed = new EmbedBuilder()
865
+ .setTitle('Original Embed')
866
+ .setDescription('This embed will be edited in 2 seconds...')
867
+ .setColor(BRAND_COLOR)
868
+ .setFooter({ text: 'Editing embeds demo' })
869
+ .setTimestamp();
870
+
871
+ const reply = await message.reply({ embeds: [embed.toJSON()] });
872
+
873
+ // Wait 2 seconds, then edit the embed
874
+ await new Promise((r) => setTimeout(r, 2000));
875
+
876
+ const updatedEmbed = new EmbedBuilder()
877
+ .setTitle('Edited Embed')
878
+ .setDescription(
879
+ 'The Fluxer API supports editing message embeds via `message.edit({ embeds: [...] })`.',
880
+ )
881
+ .setColor(0x57f287)
882
+ .addFields(
883
+ { name: 'Original', value: 'First state', inline: true },
884
+ { name: 'Edited', value: 'Updated state', inline: true },
885
+ )
886
+ .setFooter({ text: 'Embed was successfully edited' })
887
+ .setTimestamp();
888
+
889
+ await reply.edit({ embeds: [updatedEmbed] });
890
+ },
891
+ });
892
+
893
+ commands.set('help', {
894
+ description: 'List available commands',
895
+ async execute(message) {
896
+ const fields = [...commands.entries()].map(([name, cmd]) => ({
897
+ name: `${PREFIX}${name}`,
898
+ value: cmd.description,
899
+ inline: true,
900
+ }));
901
+
902
+ const embed = new EmbedBuilder()
903
+ .setTitle('Commands')
904
+ .setDescription(`Use \`${PREFIX}<command>\` to run a command.`)
905
+ .setColor(BRAND_COLOR)
906
+ .addFields(...fields)
907
+ .setFooter({ text: `Prefix: ${PREFIX}` })
908
+ .setTimestamp();
909
+
910
+ await message.reply({ embeds: [embed.toJSON()] });
911
+ },
912
+ });
913
+
914
+ commands.set('play', {
915
+ description: 'Join your voice channel and play music (WebM/Opus, no FFmpeg)',
916
+ async execute(message, client) {
917
+ const guildId = message.guildId;
918
+ if (!guildId) {
919
+ await message.reply('This command only works in a server.');
920
+ return;
921
+ }
922
+ const voiceManager = getVoiceManager(client);
923
+ const voiceChannelId = voiceManager.getVoiceChannelId(guildId, message.author.id);
924
+ if (!voiceChannelId) {
925
+ await message.reply('Join a voice channel first.');
926
+ return;
927
+ }
928
+ const channel = client.channels.get(voiceChannelId);
929
+ if (!(channel instanceof VoiceChannel)) {
930
+ await message.reply('Could not find that voice channel.');
931
+ return;
932
+ }
933
+ try {
934
+ const streamUrl = await getStreamUrl(PLAY_URL);
935
+ if (!streamUrl) {
936
+ await message.reply('Could not get stream URL. Is youtube-dl-exec installed?');
937
+ return;
938
+ }
939
+ setPlayState(guildId, channel, streamUrl);
940
+ const connection = await voiceManager.join(channel);
941
+ connection.on?.('serverLeave', async () => {
942
+ const state = playState.get(guildId);
943
+ if (!state) return;
944
+ console.log('[voice] LiveKit server sent leave; auto-reconnecting...');
945
+ try {
946
+ const conn = await voiceManager.join(state.channel);
947
+ await conn.play(state.streamUrl);
948
+ } catch (e) {
949
+ console.error('[voice] Auto-reconnect failed:', e);
950
+ playState.delete(guildId);
951
+ }
952
+ });
953
+ await connection.play(streamUrl);
954
+ await message.reply('Playing in your voice channel.');
955
+ } catch (err) {
956
+ console.error('Play error:', err);
957
+ await message.reply('Failed to join or play.').catch(() => {});
958
+ }
959
+ },
960
+ });
961
+
962
+ commands.set('playvideo', {
963
+ description:
964
+ 'Stream video in your VC (default 720p; !playvideo [url] [480p|720p|1080p|1440p|4k])',
965
+ async execute(message, client, args) {
966
+ const guildId = message.guildId;
967
+ if (!guildId) {
968
+ await message.reply('This command only works in a server.');
969
+ return;
970
+ }
971
+ const voiceManager = getVoiceManager(client);
972
+ const voiceChannelId = voiceManager.getVoiceChannelId(guildId, message.author.id);
973
+ if (!voiceChannelId) {
974
+ await message.reply('Join a voice channel first.');
975
+ return;
976
+ }
977
+ const channel = client.channels.get(voiceChannelId);
978
+ if (!(channel instanceof VoiceChannel)) {
979
+ await message.reply('Could not find that voice channel.');
980
+ return;
981
+ }
982
+ const firstArg = args[0]?.trim();
983
+ const secondArg = args[1]?.trim();
984
+ const RESOLUTIONS = ['480p', '720p', '1080p', '1440p', '4k'];
985
+ const resolutionPreset = (s) => s && RESOLUTIONS.includes(s.toLowerCase());
986
+ let inputUrl;
987
+ let resolution;
988
+ if (resolutionPreset(firstArg)) {
989
+ inputUrl = DEFAULT_VIDEO_URL;
990
+ resolution = firstArg.toLowerCase();
991
+ } else if (firstArg && (firstArg.startsWith('http://') || firstArg.startsWith('https://'))) {
992
+ inputUrl = firstArg;
993
+ resolution = resolutionPreset(secondArg) ? secondArg.toLowerCase() : '720p';
994
+ } else {
995
+ inputUrl = DEFAULT_VIDEO_URL;
996
+ resolution = resolutionPreset(secondArg) ? secondArg.toLowerCase() : '720p';
997
+ }
998
+ try {
999
+ let videoUrl = inputUrl;
1000
+ if (YOUTUBE_LIKE.test(inputUrl)) {
1001
+ await message.reply('Fetching video URL from YouTube...').catch(() => {});
1002
+ const resolved = await getVideoUrl(inputUrl);
1003
+ if (!resolved) {
1004
+ await message
1005
+ .reply('Could not get video URL. Is youtube-dl-exec installed?')
1006
+ .catch(() => {});
1007
+ return;
1008
+ }
1009
+ videoUrl = resolved;
1010
+ }
1011
+ const connection = await voiceManager.join(channel);
1012
+ if (!(connection instanceof LiveKitRtcConnection)) {
1013
+ await message.reply(
1014
+ 'Video requires LiveKit voice (this server may use a different voice backend).',
1015
+ );
1016
+ return;
1017
+ }
1018
+ if (!connection.isConnected()) {
1019
+ await message.reply('Voice connection not ready. Try again in a moment.');
1020
+ return;
1021
+ }
1022
+ await connection.playVideo(videoUrl, {
1023
+ source: 'screenshare',
1024
+ resolution,
1025
+ });
1026
+ const fps = 30;
1027
+ await message.reply(
1028
+ `Streaming video in your voice channel at ${resolution} ${fps}fps. Use \`${PREFIX}stop\` to leave.`,
1029
+ );
1030
+ } catch (err) {
1031
+ console.error('Playvideo error:', err);
1032
+ await message.reply('Failed to join or stream video.').catch(() => {});
1033
+ }
1034
+ },
1035
+ });
1036
+
1037
+ commands.set('stop', {
1038
+ description: 'Stop audio/video playback and leave voice channel',
1039
+ async execute(message, client) {
1040
+ const guildId = message.guildId;
1041
+ if (!guildId) {
1042
+ await message.reply('This command only works in a server.');
1043
+ return;
1044
+ }
1045
+ const voiceManager = getVoiceManager(client);
1046
+ const connection = voiceManager.getConnection(guildId);
1047
+ if (connection) {
1048
+ clearPlayState(guildId);
1049
+ connection.stop();
1050
+ voiceManager.leave(guildId);
1051
+ await message.reply('Stopped and left the voice channel.');
1052
+ } else {
1053
+ await message.reply('Not in a voice channel in this server.');
1054
+ }
1055
+ },
1056
+ });
1057
+
1058
+ // ─────────────────────────────────────────────────────────────────────────────
1059
+ // Client Setup
1060
+ // ─────────────────────────────────────────────────────────────────────────────
1061
+
1062
+ const token = process.env.FLUXER_BOT_TOKEN;
1063
+ if (!token) {
1064
+ console.error('Error: Set FLUXER_BOT_TOKEN environment variable');
1065
+ process.exit(1);
1066
+ }
1067
+
1068
+ const client = new Client({
1069
+ intents: 0,
1070
+ rest: process.env.FLUXER_API_URL ? { api: process.env.FLUXER_API_URL } : undefined,
1071
+ presence: {
1072
+ status: 'online',
1073
+ custom_status: { text: 'Watching the server' },
1074
+ },
1075
+ });
1076
+
1077
+ // Create VoiceManager before login so it receives VoiceStatesSync from READY/GUILD_CREATE
1078
+ // and can see users who were already in a voice channel when the bot started.
1079
+ getVoiceManager(client);
1080
+
1081
+ client.on(Events.Ready, () => {
1082
+ console.log(`Logged in as ${client.user?.username}`);
1083
+ console.log(`Serving ${client.guilds.size} guild(s)`);
1084
+ });
1085
+
1086
+ client.on(Events.MessageCreate, async (message) => {
1087
+ // Ignore bots and messages without content
1088
+ if (message.author.bot || !message.content) return;
1089
+
1090
+ // Ignore messages that don't start with the prefix
1091
+ const content = message.content.trim();
1092
+ if (!content.startsWith(PREFIX)) return;
1093
+
1094
+ // Parse command and arguments
1095
+ const args = content.slice(PREFIX.length).split(/\s+/);
1096
+ const commandName = args.shift()?.toLowerCase();
1097
+ if (!commandName) return;
1098
+
1099
+ // Execute command if it exists
1100
+ const command = commands.get(commandName);
1101
+ if (command) {
1102
+ try {
1103
+ await command.execute(message, client, args);
1104
+ } catch (err) {
1105
+ console.error(`Error executing ${commandName}:`, err);
1106
+ await message.reply('An error occurred while running that command.').catch(() => {});
1107
+ }
1108
+ }
1109
+ });
1110
+
1111
+ client.on(Events.Error, (err) => console.error('Client error:', err));
1112
+ client.on(Events.Debug, (msg) => console.log('[debug]', msg));
1113
+
1114
+ // Optional: log voice gateway events when VOICE_DEBUG=1 (helps diagnose connection timeouts)
1115
+ if (process.env.VOICE_DEBUG === '1' || process.env.VOICE_DEBUG === 'true') {
1116
+ client.on(Events.VoiceStateUpdate, (d) =>
1117
+ console.log('[voice] VoiceStateUpdate', {
1118
+ guild_id: d.guild_id,
1119
+ user_id: d.user_id,
1120
+ channel_id: d.channel_id,
1121
+ }),
1122
+ );
1123
+ client.on(Events.VoiceServerUpdate, (d) =>
1124
+ console.log('[voice] VoiceServerUpdate', {
1125
+ guild_id: d.guild_id,
1126
+ endpoint: d.endpoint ? 'present' : 'null',
1127
+ }),
1128
+ );
1129
+ }
1130
+
1131
+ // ─────────────────────────────────────────────────────────────────────────────
1132
+ // Start
1133
+ // ─────────────────────────────────────────────────────────────────────────────
1134
+
1135
+ process.on('unhandledRejection', (err) => {
1136
+ console.error('Unhandled rejection:', err);
1137
+ process.exit(1);
1138
+ });
1139
+
1140
+ try {
1141
+ await client.login(token);
1142
+ console.log('Gateway connected');
1143
+ } catch (err) {
1144
+ console.error('Login failed:', err);
1145
+ process.exit(1);
1146
+ }