@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.
- package/core/.changeset/README.md +8 -0
- package/core/.changeset/community-bootstrap-release.md +17 -0
- package/core/.changeset/config.json +11 -0
- package/core/.changeset/no-changelog.js +16 -0
- package/core/.changeset/pre.json +17 -0
- package/core/.editorconfig +13 -0
- package/core/.gitattributes +2 -0
- package/core/.github/CODE_OF_CONDUCT.md +23 -0
- package/core/.github/FUNDING.yml +7 -0
- package/core/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/core/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/core/.github/PULL_REQUEST_TEMPLATE.md +16 -0
- package/core/.github/dependabot.yml +16 -0
- package/core/.github/workflows/autoapp.yml +16 -0
- package/core/.github/workflows/ci.yml +187 -0
- package/core/.github/workflows/codeql.yml +30 -0
- package/core/.github/workflows/deploy-docs.yml +54 -0
- package/core/.github/workflows/publish.yml +43 -0
- package/core/.lintstagedrc.json +4 -0
- package/core/.nvmrc +1 -0
- package/core/.prettierignore +8 -0
- package/core/.prettierrc +11 -0
- package/core/CONTRIBUTING.md +70 -0
- package/core/LICENSE +203 -0
- package/core/README.md +61 -0
- package/core/SECURITY.md +21 -0
- package/core/apps/docs/index.html +28 -0
- package/core/apps/docs/middleware.ts +21 -0
- package/core/apps/docs/package.json +33 -0
- package/core/apps/docs/public/@flux.png +0 -0
- package/core/apps/docs/public/docs/latest/guides.json +1420 -0
- package/core/apps/docs/public/docs/latest/main.json +14981 -0
- package/core/apps/docs/public/docs/latest/rag-index.json +1 -0
- package/core/apps/docs/public/docs/v1.0.5/guides.json +226 -0
- package/core/apps/docs/public/docs/v1.0.5/main.json +7920 -0
- package/core/apps/docs/public/docs/v1.0.6/guides.json +226 -0
- package/core/apps/docs/public/docs/v1.0.6/main.json +7920 -0
- package/core/apps/docs/public/docs/v1.0.7/guides.json +259 -0
- package/core/apps/docs/public/docs/v1.0.7/main.json +8652 -0
- package/core/apps/docs/public/docs/v1.0.8/guides.json +313 -0
- package/core/apps/docs/public/docs/v1.0.8/main.json +9618 -0
- package/core/apps/docs/public/docs/v1.0.9/guides.json +319 -0
- package/core/apps/docs/public/docs/v1.0.9/main.json +10694 -0
- package/core/apps/docs/public/docs/v1.1.0/guides.json +589 -0
- package/core/apps/docs/public/docs/v1.1.0/main.json +12576 -0
- package/core/apps/docs/public/docs/v1.1.2/guides.json +650 -0
- package/core/apps/docs/public/docs/v1.1.2/main.json +13239 -0
- package/core/apps/docs/public/docs/v1.1.3/guides.json +650 -0
- package/core/apps/docs/public/docs/v1.1.3/main.json +13239 -0
- package/core/apps/docs/public/docs/v1.1.4/guides.json +708 -0
- package/core/apps/docs/public/docs/v1.1.4/main.json +13231 -0
- package/core/apps/docs/public/docs/v1.1.5/guides.json +1035 -0
- package/core/apps/docs/public/docs/v1.1.5/main.json +13838 -0
- package/core/apps/docs/public/docs/v1.1.6/guides.json +1041 -0
- package/core/apps/docs/public/docs/v1.1.6/main.json +14313 -0
- package/core/apps/docs/public/docs/v1.1.8/guides.json +1047 -0
- package/core/apps/docs/public/docs/v1.1.8/main.json +14421 -0
- package/core/apps/docs/public/docs/v1.1.9/guides.json +1047 -0
- package/core/apps/docs/public/docs/v1.1.9/main.json +14421 -0
- package/core/apps/docs/public/docs/v1.2.0/guides.json +1212 -0
- package/core/apps/docs/public/docs/v1.2.0/main.json +14663 -0
- package/core/apps/docs/public/docs/v1.2.1/guides.json +1293 -0
- package/core/apps/docs/public/docs/v1.2.1/main.json +14828 -0
- package/core/apps/docs/public/docs/v1.2.2/guides.json +1293 -0
- package/core/apps/docs/public/docs/v1.2.2/main.json +15025 -0
- package/core/apps/docs/public/docs/v1.2.3/guides.json +1420 -0
- package/core/apps/docs/public/docs/v1.2.3/main.json +14954 -0
- package/core/apps/docs/public/docs/v1.2.4/guides.json +1420 -0
- package/core/apps/docs/public/docs/v1.2.4/main.json +14981 -0
- package/core/apps/docs/public/docs/versions.json +24 -0
- package/core/apps/docs/public/flux.png +0 -0
- package/core/apps/docs/public/locales/en.json +50 -0
- package/core/apps/docs/public/locales/guides-en.json +512 -0
- package/core/apps/docs/public/robots.txt +4 -0
- package/core/apps/docs/public/sitemap.xml +33 -0
- package/core/apps/docs/src/App.vue +538 -0
- package/core/apps/docs/src/components/ApiCategorySection.vue +42 -0
- package/core/apps/docs/src/components/ApiDiscordCompat.vue +65 -0
- package/core/apps/docs/src/components/ApiEndpointCard.vue +313 -0
- package/core/apps/docs/src/components/ApiSchemaBlock.vue +131 -0
- package/core/apps/docs/src/components/CodeBlock.vue +177 -0
- package/core/apps/docs/src/components/CommunityCallout.vue +90 -0
- package/core/apps/docs/src/components/ConstructorSection.vue +82 -0
- package/core/apps/docs/src/components/DocDescription.vue +40 -0
- package/core/apps/docs/src/components/FluxerLogo.vue +3 -0
- package/core/apps/docs/src/components/Footer.vue +106 -0
- package/core/apps/docs/src/components/GuideCodeBlock.vue +102 -0
- package/core/apps/docs/src/components/GuideDiscordCompat.vue +77 -0
- package/core/apps/docs/src/components/GuideDiscordCompatCallout.vue +83 -0
- package/core/apps/docs/src/components/GuideTable.vue +77 -0
- package/core/apps/docs/src/components/GuideTip.vue +38 -0
- package/core/apps/docs/src/components/MethodsSection.vue +195 -0
- package/core/apps/docs/src/components/ParamsTable.vue +70 -0
- package/core/apps/docs/src/components/PropertiesSection.vue +143 -0
- package/core/apps/docs/src/components/SearchBar.vue +76 -0
- package/core/apps/docs/src/components/SearchModal.vue +361 -0
- package/core/apps/docs/src/components/SidebarNav.vue +225 -0
- package/core/apps/docs/src/components/SponsorBanner.vue +153 -0
- package/core/apps/docs/src/components/TypeSignature.vue +187 -0
- package/core/apps/docs/src/components/VersionPicker.vue +191 -0
- package/core/apps/docs/src/composables/useSearchIndex.ts +144 -0
- package/core/apps/docs/src/composables/useVersionedPath.ts +20 -0
- package/core/apps/docs/src/data/apiEndpoints.ts +1073 -0
- package/core/apps/docs/src/data/changelog.ts +717 -0
- package/core/apps/docs/src/data/guides.ts +2362 -0
- package/core/apps/docs/src/env.d.ts +7 -0
- package/core/apps/docs/src/locales/guides-en.json +512 -0
- package/core/apps/docs/src/main.ts +27 -0
- package/core/apps/docs/src/pages/ApiReferenceLayout.vue +175 -0
- package/core/apps/docs/src/pages/ApiReferencePage.vue +128 -0
- package/core/apps/docs/src/pages/Changelog.vue +288 -0
- package/core/apps/docs/src/pages/ClassPage.vue +319 -0
- package/core/apps/docs/src/pages/ClassesList.vue +100 -0
- package/core/apps/docs/src/pages/DocsLayout.vue +127 -0
- package/core/apps/docs/src/pages/GuidePage.vue +279 -0
- package/core/apps/docs/src/pages/GuidesIndex.vue +166 -0
- package/core/apps/docs/src/pages/GuidesLayout.vue +245 -0
- package/core/apps/docs/src/pages/Home.vue +125 -0
- package/core/apps/docs/src/pages/NotFound.vue +57 -0
- package/core/apps/docs/src/pages/TypedefPage.vue +230 -0
- package/core/apps/docs/src/pages/TypedefsList.vue +168 -0
- package/core/apps/docs/src/pages/VersionLayout.vue +15 -0
- package/core/apps/docs/src/router.ts +73 -0
- package/core/apps/docs/src/stores/docs.ts +54 -0
- package/core/apps/docs/src/stores/guides.ts +53 -0
- package/core/apps/docs/src/stores/version.ts +67 -0
- package/core/apps/docs/src/styles/main.css +278 -0
- package/core/apps/docs/src/styles/prism.css +95 -0
- package/core/apps/docs/src/types/doc-schema.ts +112 -0
- package/core/apps/docs/tsconfig.json +17 -0
- package/core/apps/docs/tsconfig.node.json +10 -0
- package/core/apps/docs/vite.config.d.ts +2 -0
- package/core/apps/docs/vite.config.js +26 -0
- package/core/apps/docs/vite.config.ts +28 -0
- package/core/apps/docs-vitepress/.vitepress/config.ts +141 -0
- package/core/apps/docs-vitepress/api-data/latest/main.json +15035 -0
- package/core/apps/docs-vitepress/api-data/v1.2.4/main.json +15035 -0
- package/core/apps/docs-vitepress/api-data/versions.json +6 -0
- package/core/apps/docs-vitepress/index.md +15 -0
- package/core/apps/docs-vitepress/package-lock.json +2924 -0
- package/core/apps/docs-vitepress/package.json +20 -0
- package/core/apps/docs-vitepress/public/CNAME +1 -0
- package/core/apps/docs-vitepress/scripts/generate-api.ts +243 -0
- package/core/apps/docs-vitepress/scripts/migrate-guides.ts +129 -0
- package/core/apps/docs-vitepress/tsconfig.json +11 -0
- package/core/apps/docs-vitepress/v/latest/guides/attachments-by-url.md +57 -0
- package/core/apps/docs-vitepress/v/latest/guides/attachments.md +62 -0
- package/core/apps/docs-vitepress/v/latest/guides/basic-bot.md +49 -0
- package/core/apps/docs-vitepress/v/latest/guides/channels.md +180 -0
- package/core/apps/docs-vitepress/v/latest/guides/deprecated-apis.md +58 -0
- package/core/apps/docs-vitepress/v/latest/guides/discord-js-compatibility.md +42 -0
- package/core/apps/docs-vitepress/v/latest/guides/editing-embeds.md +65 -0
- package/core/apps/docs-vitepress/v/latest/guides/embed-media.md +87 -0
- package/core/apps/docs-vitepress/v/latest/guides/embeds.md +166 -0
- package/core/apps/docs-vitepress/v/latest/guides/emojis.md +77 -0
- package/core/apps/docs-vitepress/v/latest/guides/events.md +202 -0
- package/core/apps/docs-vitepress/v/latest/guides/gifs.md +47 -0
- package/core/apps/docs-vitepress/v/latest/guides/installation.md +10 -0
- package/core/apps/docs-vitepress/v/latest/guides/moderation.md +89 -0
- package/core/apps/docs-vitepress/v/latest/guides/permissions.md +130 -0
- package/core/apps/docs-vitepress/v/latest/guides/prefix-commands.md +41 -0
- package/core/apps/docs-vitepress/v/latest/guides/profile-urls.md +58 -0
- package/core/apps/docs-vitepress/v/latest/guides/reactions.md +69 -0
- package/core/apps/docs-vitepress/v/latest/guides/roles.md +130 -0
- package/core/apps/docs-vitepress/v/latest/guides/sending-without-reply.md +172 -0
- package/core/apps/docs-vitepress/v/latest/guides/voice.md +109 -0
- package/core/apps/docs-vitepress/v/latest/guides/wait-for-guilds.md +37 -0
- package/core/apps/docs-vitepress/v/latest/guides/webhook-attachments-embeds.md +73 -0
- package/core/apps/docs-vitepress/v/latest/guides/webhooks.md +131 -0
- package/core/eslint.config.js +80 -0
- package/core/examples/.env.example +22 -0
- package/core/examples/README.md +68 -0
- package/core/examples/first-steps-bot.js +118 -0
- package/core/examples/minimal-bot.js +17 -0
- package/core/examples/moderation-bot.js +209 -0
- package/core/examples/package.json +14 -0
- package/core/examples/ping-bot.js +1146 -0
- package/core/examples/reaction-bot.js +70 -0
- package/core/examples/reaction-roles-bot.js +140 -0
- package/core/examples/webhook-bot.js +239 -0
- package/core/flux.png +0 -0
- package/core/package.json +78 -0
- package/core/packages/builders/package.json +51 -0
- package/core/packages/builders/src/index.ts +13 -0
- package/core/packages/builders/src/messages/AttachmentBuilder.test.ts +79 -0
- package/core/packages/builders/src/messages/AttachmentBuilder.ts +69 -0
- package/core/packages/builders/src/messages/EmbedBuilder.test.ts +266 -0
- package/core/packages/builders/src/messages/EmbedBuilder.ts +239 -0
- package/core/packages/builders/src/messages/MessagePayload.test.ts +118 -0
- package/core/packages/builders/src/messages/MessagePayload.ts +122 -0
- package/core/packages/builders/tsconfig.json +9 -0
- package/core/packages/builders/tsup.config.ts +9 -0
- package/core/packages/builders/vitest.config.ts +9 -0
- package/core/packages/collection/package.json +47 -0
- package/core/packages/collection/src/Collection.test.ts +232 -0
- package/core/packages/collection/src/Collection.ts +196 -0
- package/core/packages/collection/src/index.ts +1 -0
- package/core/packages/collection/tsconfig.json +9 -0
- package/core/packages/collection/tsup.config.ts +9 -0
- package/core/packages/collection/vitest.config.ts +9 -0
- package/core/packages/docgen/package.json +26 -0
- package/core/packages/docgen/src/extract.ts +262 -0
- package/core/packages/docgen/src/formatType.ts +24 -0
- package/core/packages/docgen/src/index.ts +103 -0
- package/core/packages/docgen/src/schema.ts +100 -0
- package/core/packages/docgen/src/visitor.ts +147 -0
- package/core/packages/docgen/tsconfig.json +9 -0
- package/core/packages/docgen/tsup.config.ts +9 -0
- package/core/packages/fluxer-core/README.md +26 -0
- package/core/packages/fluxer-core/package.json +60 -0
- package/core/packages/fluxer-core/src/client/ChannelManager.ts +143 -0
- package/core/packages/fluxer-core/src/client/Client.gateway.test.ts +84 -0
- package/core/packages/fluxer-core/src/client/Client.resolveEmoji.test.ts +45 -0
- package/core/packages/fluxer-core/src/client/Client.ts +558 -0
- package/core/packages/fluxer-core/src/client/ClientUser.ts +40 -0
- package/core/packages/fluxer-core/src/client/EventHandlerRegistry.ts +469 -0
- package/core/packages/fluxer-core/src/client/GuildManager.ts +79 -0
- package/core/packages/fluxer-core/src/client/GuildMemberManager.ts +91 -0
- package/core/packages/fluxer-core/src/client/MessageManager.ts +58 -0
- package/core/packages/fluxer-core/src/client/UsersManager.ts +122 -0
- package/core/packages/fluxer-core/src/errors/ErrorCodes.test.ts +19 -0
- package/core/packages/fluxer-core/src/errors/ErrorCodes.ts +12 -0
- package/core/packages/fluxer-core/src/errors/FluxerError.test.ts +32 -0
- package/core/packages/fluxer-core/src/errors/FluxerError.ts +15 -0
- package/core/packages/fluxer-core/src/index.ts +85 -0
- package/core/packages/fluxer-core/src/structures/Base.ts +7 -0
- package/core/packages/fluxer-core/src/structures/Channel.ts +508 -0
- package/core/packages/fluxer-core/src/structures/Guild.test.ts +189 -0
- package/core/packages/fluxer-core/src/structures/Guild.ts +734 -0
- package/core/packages/fluxer-core/src/structures/GuildBan.ts +35 -0
- package/core/packages/fluxer-core/src/structures/GuildEmoji.ts +57 -0
- package/core/packages/fluxer-core/src/structures/GuildMember.test.ts +203 -0
- package/core/packages/fluxer-core/src/structures/GuildMember.ts +213 -0
- package/core/packages/fluxer-core/src/structures/GuildMemberRoleManager.ts +121 -0
- package/core/packages/fluxer-core/src/structures/GuildSticker.ts +56 -0
- package/core/packages/fluxer-core/src/structures/Invite.test.ts +103 -0
- package/core/packages/fluxer-core/src/structures/Invite.ts +121 -0
- package/core/packages/fluxer-core/src/structures/Message.test.ts +109 -0
- package/core/packages/fluxer-core/src/structures/Message.ts +397 -0
- package/core/packages/fluxer-core/src/structures/MessageReaction.ts +72 -0
- package/core/packages/fluxer-core/src/structures/PartialMessage.ts +12 -0
- package/core/packages/fluxer-core/src/structures/Role.test.ts +77 -0
- package/core/packages/fluxer-core/src/structures/Role.ts +112 -0
- package/core/packages/fluxer-core/src/structures/User.test.ts +110 -0
- package/core/packages/fluxer-core/src/structures/User.ts +109 -0
- package/core/packages/fluxer-core/src/structures/Webhook.test.ts +109 -0
- package/core/packages/fluxer-core/src/structures/Webhook.ts +258 -0
- package/core/packages/fluxer-core/src/util/Constants.test.ts +16 -0
- package/core/packages/fluxer-core/src/util/Constants.ts +7 -0
- package/core/packages/fluxer-core/src/util/Events.ts +46 -0
- package/core/packages/fluxer-core/src/util/MessageCollector.ts +87 -0
- package/core/packages/fluxer-core/src/util/Options.ts +33 -0
- package/core/packages/fluxer-core/src/util/ReactionCollector.ts +116 -0
- package/core/packages/fluxer-core/src/util/cdn.test.ts +108 -0
- package/core/packages/fluxer-core/src/util/cdn.ts +130 -0
- package/core/packages/fluxer-core/src/util/guildUtils.ts +33 -0
- package/core/packages/fluxer-core/src/util/messageUtils.test.ts +74 -0
- package/core/packages/fluxer-core/src/util/messageUtils.ts +119 -0
- package/core/packages/fluxer-core/src/util/permissions.test.ts +95 -0
- package/core/packages/fluxer-core/src/util/permissions.ts +43 -0
- package/core/packages/fluxer-core/tsconfig.json +9 -0
- package/core/packages/fluxer-core/tsup.config.ts +9 -0
- package/core/packages/fluxer-core/vitest.config.ts +9 -0
- package/core/packages/rest/package.json +52 -0
- package/core/packages/rest/src/REST.test.ts +64 -0
- package/core/packages/rest/src/REST.ts +90 -0
- package/core/packages/rest/src/RateLimitManager.test.ts +71 -0
- package/core/packages/rest/src/RateLimitManager.ts +60 -0
- package/core/packages/rest/src/RequestManager.test.ts +87 -0
- package/core/packages/rest/src/RequestManager.ts +172 -0
- package/core/packages/rest/src/errors/FluxerAPIError.test.ts +57 -0
- package/core/packages/rest/src/errors/FluxerAPIError.ts +21 -0
- package/core/packages/rest/src/errors/HTTPError.test.ts +55 -0
- package/core/packages/rest/src/errors/HTTPError.ts +25 -0
- package/core/packages/rest/src/errors/RateLimitError.test.ts +41 -0
- package/core/packages/rest/src/errors/RateLimitError.ts +15 -0
- package/core/packages/rest/src/errors/index.ts +3 -0
- package/core/packages/rest/src/index.ts +6 -0
- package/core/packages/rest/src/utils/constants.test.ts +31 -0
- package/core/packages/rest/src/utils/constants.ts +5 -0
- package/core/packages/rest/src/utils/files.test.ts +37 -0
- package/core/packages/rest/src/utils/files.ts +75 -0
- package/core/packages/rest/tsconfig.json +9 -0
- package/core/packages/rest/tsup.config.ts +9 -0
- package/core/packages/rest/vitest.config.ts +9 -0
- package/core/packages/types/package.json +46 -0
- package/core/packages/types/src/api/ban.ts +8 -0
- package/core/packages/types/src/api/channel.ts +65 -0
- package/core/packages/types/src/api/embed.ts +82 -0
- package/core/packages/types/src/api/emoji.ts +12 -0
- package/core/packages/types/src/api/errors.ts +68 -0
- package/core/packages/types/src/api/gateway.ts +14 -0
- package/core/packages/types/src/api/guild.ts +123 -0
- package/core/packages/types/src/api/index.ts +15 -0
- package/core/packages/types/src/api/instance.ts +32 -0
- package/core/packages/types/src/api/interaction.ts +26 -0
- package/core/packages/types/src/api/invite.ts +28 -0
- package/core/packages/types/src/api/message.ts +140 -0
- package/core/packages/types/src/api/role.ts +41 -0
- package/core/packages/types/src/api/sticker.ts +14 -0
- package/core/packages/types/src/api/user.ts +79 -0
- package/core/packages/types/src/api/webhook.ts +41 -0
- package/core/packages/types/src/common/index.ts +1 -0
- package/core/packages/types/src/common/snowflake.test.ts +9 -0
- package/core/packages/types/src/common/snowflake.ts +8 -0
- package/core/packages/types/src/gateway/events.ts +189 -0
- package/core/packages/types/src/gateway/index.ts +3 -0
- package/core/packages/types/src/gateway/opcodes.ts +17 -0
- package/core/packages/types/src/gateway/payloads.ts +481 -0
- package/core/packages/types/src/index.ts +4 -0
- package/core/packages/types/src/rest/index.ts +1 -0
- package/core/packages/types/src/rest/routes.test.ts +169 -0
- package/core/packages/types/src/rest/routes.ts +109 -0
- package/core/packages/types/tsconfig.json +9 -0
- package/core/packages/types/tsup.config.ts +9 -0
- package/core/packages/types/vitest.config.ts +9 -0
- package/core/packages/util/package.json +51 -0
- package/core/packages/util/src/BitField.test.ts +96 -0
- package/core/packages/util/src/BitField.ts +105 -0
- package/core/packages/util/src/MessageFlagsBitField.test.ts +42 -0
- package/core/packages/util/src/MessageFlagsBitField.ts +20 -0
- package/core/packages/util/src/PermissionsBitField.test.ts +79 -0
- package/core/packages/util/src/PermissionsBitField.ts +97 -0
- package/core/packages/util/src/SnowflakeUtil.test.ts +69 -0
- package/core/packages/util/src/SnowflakeUtil.ts +65 -0
- package/core/packages/util/src/UserFlagsBitField.test.ts +39 -0
- package/core/packages/util/src/UserFlagsBitField.ts +48 -0
- package/core/packages/util/src/deprecation.test.ts +44 -0
- package/core/packages/util/src/deprecation.ts +28 -0
- package/core/packages/util/src/emojiShortcodes.generated.ts +5 -0
- package/core/packages/util/src/emojiShortcodes.test.ts +41 -0
- package/core/packages/util/src/emojiShortcodes.ts +22 -0
- package/core/packages/util/src/formatters.test.ts +65 -0
- package/core/packages/util/src/formatters.ts +35 -0
- package/core/packages/util/src/index.ts +34 -0
- package/core/packages/util/src/resolvers.test.ts +198 -0
- package/core/packages/util/src/resolvers.ts +127 -0
- package/core/packages/util/src/tenorUtils.test.ts +75 -0
- package/core/packages/util/src/tenorUtils.ts +86 -0
- package/core/packages/util/tsconfig.json +9 -0
- package/core/packages/util/tsup.config.ts +9 -0
- package/core/packages/util/vitest.config.ts +9 -0
- package/core/packages/voice/README.md +42 -0
- package/core/packages/voice/package.json +67 -0
- package/core/packages/voice/src/LiveKitRtcConnection.receive.test.ts +24 -0
- package/core/packages/voice/src/LiveKitRtcConnection.ts +1767 -0
- package/core/packages/voice/src/VoiceConnection.ts +413 -0
- package/core/packages/voice/src/VoiceManager.receive.test.ts +61 -0
- package/core/packages/voice/src/VoiceManager.test.ts +44 -0
- package/core/packages/voice/src/VoiceManager.ts +503 -0
- package/core/packages/voice/src/exports.test.ts +38 -0
- package/core/packages/voice/src/index.ts +51 -0
- package/core/packages/voice/src/livekit.test.ts +48 -0
- package/core/packages/voice/src/livekit.ts +33 -0
- package/core/packages/voice/src/mp4box.d.ts +32 -0
- package/core/packages/voice/src/opusUtils.test.ts +29 -0
- package/core/packages/voice/src/opusUtils.ts +86 -0
- package/core/packages/voice/src/streamPreviewPlaceholder.test.ts +16 -0
- package/core/packages/voice/src/streamPreviewPlaceholder.ts +8 -0
- package/core/packages/voice/src/ws.d.ts +1 -0
- package/core/packages/voice/tsconfig.json +5 -0
- package/core/packages/voice/tsup.config.ts +10 -0
- package/core/packages/voice/vitest.config.ts +9 -0
- package/core/packages/ws/package.json +52 -0
- package/core/packages/ws/src/WebSocketManager.ts +130 -0
- package/core/packages/ws/src/WebSocketShard.ts +296 -0
- package/core/packages/ws/src/index.ts +12 -0
- package/core/packages/ws/src/utils/constants.test.ts +46 -0
- package/core/packages/ws/src/utils/constants.ts +22 -0
- package/core/packages/ws/src/utils/getWebSocket.ts +55 -0
- package/core/packages/ws/src/ws.d.ts +10 -0
- package/core/packages/ws/tsconfig.json +9 -0
- package/core/packages/ws/tsup.config.ts +9 -0
- package/core/pnpm-lock.yaml +7033 -0
- package/core/pnpm-workspace.yaml +4 -0
- package/core/scripts/generate-ai-rag.ts +240 -0
- package/core/scripts/generate-docs.ts +143 -0
- package/core/scripts/generate-emoji-shortcodes.ts +58 -0
- package/core/scripts/generate-types.ts +6 -0
- package/core/scripts/publish-ordered.js +63 -0
- package/core/scripts/test-cjs-require.mjs +43 -0
- package/core/scripts/test-esm-imports.mjs +42 -0
- package/core/scripts/test-package-exports.mjs +98 -0
- package/core/scripts/test-smoke.mjs +103 -0
- package/core/tsconfig.json +18 -0
- package/core/turbo.json +30 -0
- package/core/vitest.config.ts +17 -0
- package/core/wrangler.jsonc +9 -0
- package/package.json +26 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Client } from '@erinjs/core';
|
|
3
|
+
import { VoiceChannel } from '@erinjs/core';
|
|
4
|
+
import { Events } from '@erinjs/core';
|
|
5
|
+
import { GatewayOpcodes, Routes } from '@erinjs/types';
|
|
6
|
+
import { thumbnail } from './streamPreviewPlaceholder.js';
|
|
7
|
+
import {
|
|
8
|
+
GatewayVoiceServerUpdateDispatchData,
|
|
9
|
+
GatewayVoiceStateUpdateDispatchData,
|
|
10
|
+
} from '@erinjs/types';
|
|
11
|
+
import { VoiceConnection } from './VoiceConnection.js';
|
|
12
|
+
import { LiveKitRtcConnection, type LiveKitReceiveSubscription } from './LiveKitRtcConnection.js';
|
|
13
|
+
import { isLiveKitEndpoint } from './livekit.js';
|
|
14
|
+
import { Collection } from '@erinjs/collection';
|
|
15
|
+
|
|
16
|
+
/** Maps guild_id -> user_id -> channel_id (null if not in voice). */
|
|
17
|
+
export type VoiceStateMap = Map<string, Map<string, string | null>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for creating a VoiceManager.
|
|
21
|
+
*
|
|
22
|
+
* @property shardId - Gateway shard ID to use for voice connections (default 0).
|
|
23
|
+
* Use when the client runs multiple shards and you need to target a specific one.
|
|
24
|
+
*/
|
|
25
|
+
export interface VoiceManagerOptions {
|
|
26
|
+
/** Gateway shard ID to use for voice (default 0). */
|
|
27
|
+
shardId?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Manages voice connections. Use `getVoiceManager(client)` to obtain an instance. */
|
|
31
|
+
export class VoiceManager extends EventEmitter {
|
|
32
|
+
readonly client: Client;
|
|
33
|
+
/** channel_id -> connection (Fluxer multi-channel: allows multiple connections per guild) */
|
|
34
|
+
private readonly connections = new Collection<string, VoiceConnection | LiveKitRtcConnection>();
|
|
35
|
+
/** channel_id -> connection_id (from VoiceServerUpdate; required for voice state updates) */
|
|
36
|
+
private readonly connectionIds = new Map<string, string>();
|
|
37
|
+
/** guild_id -> user_id -> channel_id */
|
|
38
|
+
readonly voiceStates: VoiceStateMap = new Map();
|
|
39
|
+
/** channel_id -> pending join */
|
|
40
|
+
private readonly pending = new Map<
|
|
41
|
+
string,
|
|
42
|
+
{
|
|
43
|
+
channel: VoiceChannel;
|
|
44
|
+
resolve: (c: VoiceConnection | LiveKitRtcConnection) => void;
|
|
45
|
+
reject: (e: Error) => void;
|
|
46
|
+
server?: GatewayVoiceServerUpdateDispatchData;
|
|
47
|
+
state?: GatewayVoiceStateUpdateDispatchData;
|
|
48
|
+
}
|
|
49
|
+
>();
|
|
50
|
+
private readonly shardId: number;
|
|
51
|
+
|
|
52
|
+
constructor(client: Client, options: VoiceManagerOptions = {}) {
|
|
53
|
+
super();
|
|
54
|
+
this.client = client;
|
|
55
|
+
this.shardId = options.shardId ?? 0;
|
|
56
|
+
this.client.on(Events.VoiceStateUpdate, (data: GatewayVoiceStateUpdateDispatchData) =>
|
|
57
|
+
this.handleVoiceStateUpdate(data),
|
|
58
|
+
);
|
|
59
|
+
this.client.on(Events.VoiceServerUpdate, (data: GatewayVoiceServerUpdateDispatchData) =>
|
|
60
|
+
this.handleVoiceServerUpdate(data),
|
|
61
|
+
);
|
|
62
|
+
this.client.on(
|
|
63
|
+
Events.VoiceStatesSync,
|
|
64
|
+
(data: {
|
|
65
|
+
guildId: string;
|
|
66
|
+
voiceStates: Array<{ user_id: string; channel_id: string | null }>;
|
|
67
|
+
}) => this.handleVoiceStatesSync(data),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private handleVoiceStatesSync(data: {
|
|
72
|
+
guildId: string;
|
|
73
|
+
voiceStates: Array<{ user_id: string; channel_id: string | null }>;
|
|
74
|
+
}): void {
|
|
75
|
+
let guildMap = this.voiceStates.get(data.guildId);
|
|
76
|
+
if (!guildMap) {
|
|
77
|
+
guildMap = new Map();
|
|
78
|
+
this.voiceStates.set(data.guildId, guildMap);
|
|
79
|
+
}
|
|
80
|
+
for (const vs of data.voiceStates) {
|
|
81
|
+
guildMap.set(vs.user_id, vs.channel_id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the voice channel ID the user is currently in, or null if not in voice.
|
|
87
|
+
* @param guildId - Guild ID to look up
|
|
88
|
+
* @param userId - User ID to look up
|
|
89
|
+
*/
|
|
90
|
+
getVoiceChannelId(guildId: string, userId: string): string | null {
|
|
91
|
+
const guildMap = this.voiceStates.get(guildId);
|
|
92
|
+
if (!guildMap) return null;
|
|
93
|
+
return guildMap.get(userId) ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List participant user IDs currently in a specific voice channel.
|
|
98
|
+
*/
|
|
99
|
+
listParticipantsInChannel(guildId: string, channelId: string): string[] {
|
|
100
|
+
const guildMap = this.voiceStates.get(guildId);
|
|
101
|
+
if (!guildMap) return [];
|
|
102
|
+
const participants: string[] = [];
|
|
103
|
+
for (const [userId, voiceChannelId] of guildMap.entries()) {
|
|
104
|
+
if (voiceChannelId === channelId) participants.push(userId);
|
|
105
|
+
}
|
|
106
|
+
return participants;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to inbound audio for all known participants currently in a voice channel.
|
|
111
|
+
* Only supported for LiveKit connections.
|
|
112
|
+
*/
|
|
113
|
+
subscribeChannelParticipants(
|
|
114
|
+
channelId: string,
|
|
115
|
+
opts?: { autoResubscribe?: boolean },
|
|
116
|
+
): LiveKitReceiveSubscription[] {
|
|
117
|
+
const conn = this.connections.get(channelId);
|
|
118
|
+
if (!(conn instanceof LiveKitRtcConnection)) return [];
|
|
119
|
+
const guildId = conn.channel.guildId;
|
|
120
|
+
const participants = this.listParticipantsInChannel(guildId, channelId).filter(
|
|
121
|
+
(participantId) => participantId !== this.client.user?.id,
|
|
122
|
+
);
|
|
123
|
+
return participants.map((participantId) => conn.subscribeParticipantAudio(participantId, opts));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private handleVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void {
|
|
127
|
+
const guildId = data.guild_id ?? '';
|
|
128
|
+
if (!guildId) return;
|
|
129
|
+
this.client.emit?.(
|
|
130
|
+
'debug',
|
|
131
|
+
`[VoiceManager] VoiceStateUpdate guild=${guildId} user=${data.user_id} channel=${data.channel_id ?? 'null'} (bot=${this.client.user?.id})`,
|
|
132
|
+
);
|
|
133
|
+
let guildMap = this.voiceStates.get(guildId);
|
|
134
|
+
if (!guildMap) {
|
|
135
|
+
guildMap = new Map();
|
|
136
|
+
this.voiceStates.set(guildId, guildMap);
|
|
137
|
+
}
|
|
138
|
+
guildMap.set(data.user_id, data.channel_id);
|
|
139
|
+
|
|
140
|
+
const channelKey = data.channel_id ?? guildId;
|
|
141
|
+
const pendingByChannel = this.pending.get(channelKey);
|
|
142
|
+
const pendingByGuild = this.pending.get(guildId);
|
|
143
|
+
const pending = pendingByChannel ?? pendingByGuild;
|
|
144
|
+
const isBot = String(data.user_id) === String(this.client.user?.id);
|
|
145
|
+
if (isBot && data.connection_id) {
|
|
146
|
+
this.storeConnectionId(channelKey, data.connection_id);
|
|
147
|
+
}
|
|
148
|
+
if (pending && isBot) {
|
|
149
|
+
this.client.emit?.(
|
|
150
|
+
'debug',
|
|
151
|
+
`[VoiceManager] VoiceStateUpdate for bot - completing pending channel ${channelKey}`,
|
|
152
|
+
);
|
|
153
|
+
pending.state = data;
|
|
154
|
+
this.tryCompletePending(pendingByChannel ? channelKey : guildId, pending);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void {
|
|
159
|
+
const guildId = data.guild_id;
|
|
160
|
+
|
|
161
|
+
let pending = this.pending.get(guildId);
|
|
162
|
+
if (!pending) {
|
|
163
|
+
for (const [, p] of this.pending) {
|
|
164
|
+
if (p.channel?.guildId === guildId) {
|
|
165
|
+
pending = p;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (pending) {
|
|
171
|
+
const channelKey = pending.channel?.id ?? guildId;
|
|
172
|
+
const hasToken = !!(data.token && data.token.length > 0);
|
|
173
|
+
this.client.emit?.(
|
|
174
|
+
'debug',
|
|
175
|
+
`[VoiceManager] VoiceServerUpdate guild=${guildId} channel=${channelKey} endpoint=${data.endpoint ?? 'null'} token=${hasToken ? 'yes' : 'NO'}`,
|
|
176
|
+
);
|
|
177
|
+
pending.server = data;
|
|
178
|
+
this.tryCompletePending(channelKey, pending);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const userId = this.client.user?.id;
|
|
183
|
+
if (!userId) {
|
|
184
|
+
this.client.emit?.(
|
|
185
|
+
'debug',
|
|
186
|
+
'[VoiceManager] Client user not available. Ensure the client is logged in.',
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let conn: VoiceConnection | LiveKitRtcConnection | undefined;
|
|
192
|
+
for (const [, c] of this.connections) {
|
|
193
|
+
if (c?.channel?.guildId === guildId) {
|
|
194
|
+
conn = c;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!conn) return;
|
|
199
|
+
|
|
200
|
+
if (!data.endpoint || !data.token) {
|
|
201
|
+
this.client.emit?.(
|
|
202
|
+
'debug',
|
|
203
|
+
`[VoiceManager] Voice server endpoint null for guild ${guildId}; disconnecting`,
|
|
204
|
+
);
|
|
205
|
+
conn.destroy();
|
|
206
|
+
this.connections.delete(conn.channel.id);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!isLiveKitEndpoint(data.endpoint, data.token)) return;
|
|
211
|
+
|
|
212
|
+
if (conn instanceof LiveKitRtcConnection && conn.isSameServer(data.endpoint, data.token)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const channel = conn.channel;
|
|
217
|
+
this.client.emit?.(
|
|
218
|
+
'debug',
|
|
219
|
+
`[VoiceManager] Voice server migration for guild ${guildId} channel ${channel.id}; reconnecting`,
|
|
220
|
+
);
|
|
221
|
+
conn.destroy();
|
|
222
|
+
this.connections.delete(channel.id);
|
|
223
|
+
this.connectionIds.delete(channel.id);
|
|
224
|
+
this.storeConnectionId(channel.id, data.connection_id);
|
|
225
|
+
|
|
226
|
+
const ConnClass = LiveKitRtcConnection;
|
|
227
|
+
const newConn = new ConnClass(this.client, channel, userId);
|
|
228
|
+
this.registerConnection(channel.id, newConn);
|
|
229
|
+
|
|
230
|
+
const state: GatewayVoiceStateUpdateDispatchData = {
|
|
231
|
+
guild_id: guildId,
|
|
232
|
+
channel_id: channel.id,
|
|
233
|
+
user_id: userId,
|
|
234
|
+
session_id: '',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
newConn.connect(data, state).catch((e) => {
|
|
238
|
+
this.connections.delete(channel.id);
|
|
239
|
+
newConn.emit('error', e instanceof Error ? e : new Error(String(e)));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private storeConnectionId(channelId: string, connectionId: string | null | undefined): void {
|
|
244
|
+
const id = connectionId != null ? String(connectionId) : null;
|
|
245
|
+
if (id) this.connectionIds.set(channelId, id);
|
|
246
|
+
else this.connectionIds.delete(channelId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private registerConnection(
|
|
250
|
+
channelId: string,
|
|
251
|
+
conn: VoiceConnection | LiveKitRtcConnection,
|
|
252
|
+
): void {
|
|
253
|
+
const cid = conn.channel?.id ?? channelId;
|
|
254
|
+
this.connections.set(cid, conn);
|
|
255
|
+
conn.once('disconnect', () => {
|
|
256
|
+
this.connections.delete(cid);
|
|
257
|
+
this.connectionIds.delete(cid);
|
|
258
|
+
});
|
|
259
|
+
conn.on('requestVoiceStateSync', (p: { self_stream?: boolean; self_video?: boolean }) => {
|
|
260
|
+
this.updateVoiceState(cid, p);
|
|
261
|
+
if (p.self_stream) {
|
|
262
|
+
this.uploadStreamPreview(cid, conn).catch((e) =>
|
|
263
|
+
this.client.emit?.('debug', `[VoiceManager] Stream preview upload failed: ${String(e)}`),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Upload a placeholder stream preview so the preview URL returns 200 instead of 404. */
|
|
270
|
+
private async uploadStreamPreview(
|
|
271
|
+
channelId: string,
|
|
272
|
+
conn: VoiceConnection | LiveKitRtcConnection,
|
|
273
|
+
): Promise<void> {
|
|
274
|
+
const cid = conn.channel?.id ?? channelId;
|
|
275
|
+
const connectionId = this.connectionIds.get(cid);
|
|
276
|
+
if (!connectionId) return;
|
|
277
|
+
|
|
278
|
+
const streamKey = `${conn.channel.guildId}:${conn.channel.id}:${connectionId}`;
|
|
279
|
+
const route = Routes.streamPreview(streamKey);
|
|
280
|
+
const body = { channel_id: conn.channel.id, thumbnail, content_type: 'image/png' };
|
|
281
|
+
|
|
282
|
+
await this.client.rest.post(route, { body, auth: true });
|
|
283
|
+
this.client.emit?.('debug', `[VoiceManager] Uploaded stream preview for ${streamKey}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private tryCompletePending(
|
|
287
|
+
channelId: string,
|
|
288
|
+
pending: {
|
|
289
|
+
channel: VoiceChannel;
|
|
290
|
+
resolve: (c: VoiceConnection | LiveKitRtcConnection) => void;
|
|
291
|
+
reject: (e: Error) => void;
|
|
292
|
+
server?: GatewayVoiceServerUpdateDispatchData;
|
|
293
|
+
state?: GatewayVoiceStateUpdateDispatchData;
|
|
294
|
+
},
|
|
295
|
+
): void {
|
|
296
|
+
if (!pending?.server) return;
|
|
297
|
+
|
|
298
|
+
const useLiveKit = isLiveKitEndpoint(pending.server.endpoint, pending.server.token);
|
|
299
|
+
const hasState = !!pending.state;
|
|
300
|
+
|
|
301
|
+
if (!useLiveKit && !hasState) return;
|
|
302
|
+
if (useLiveKit && !hasState) {
|
|
303
|
+
this.client.emit?.(
|
|
304
|
+
'debug',
|
|
305
|
+
`[VoiceManager] Proceeding with VoiceServerUpdate only (LiveKit does not require VoiceStateUpdate)`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const userId = this.client.user?.id;
|
|
310
|
+
if (!userId) {
|
|
311
|
+
this.client.emit?.(
|
|
312
|
+
'debug',
|
|
313
|
+
'[VoiceManager] Client user not available. Ensure the client is logged in.',
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const guildId = pending.channel?.guildId ?? '';
|
|
319
|
+
const state: GatewayVoiceStateUpdateDispatchData = pending.state ?? {
|
|
320
|
+
guild_id: guildId,
|
|
321
|
+
channel_id: pending.channel.id,
|
|
322
|
+
user_id: userId,
|
|
323
|
+
session_id: '',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
this.storeConnectionId(
|
|
327
|
+
channelId,
|
|
328
|
+
pending.server.connection_id ?? (state as { connection_id?: string }).connection_id,
|
|
329
|
+
);
|
|
330
|
+
this.pending.delete(channelId);
|
|
331
|
+
const ConnClass = useLiveKit ? LiveKitRtcConnection : VoiceConnection;
|
|
332
|
+
const conn = new ConnClass(this.client, pending.channel, userId);
|
|
333
|
+
this.registerConnection(channelId, conn);
|
|
334
|
+
conn.connect(pending.server, state).then(
|
|
335
|
+
() => pending.resolve(conn),
|
|
336
|
+
(e) => pending.reject(e),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Join a voice channel. Resolves when the connection is ready.
|
|
342
|
+
* Supports multiple connections per guild (Fluxer multi-channel).
|
|
343
|
+
* @param channel - The voice channel to join
|
|
344
|
+
* @returns The voice connection (LiveKitRtcConnection when Fluxer uses LiveKit)
|
|
345
|
+
*/
|
|
346
|
+
async join(channel: VoiceChannel): Promise<VoiceConnection | LiveKitRtcConnection> {
|
|
347
|
+
const channelId = channel.id;
|
|
348
|
+
const existing = this.connections.get(channelId);
|
|
349
|
+
if (existing) {
|
|
350
|
+
const isReusable = existing instanceof LiveKitRtcConnection ? existing.isConnected() : true;
|
|
351
|
+
if (isReusable) return existing;
|
|
352
|
+
existing.destroy();
|
|
353
|
+
this.connections.delete(channelId);
|
|
354
|
+
}
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
this.client.emit?.(
|
|
357
|
+
'debug',
|
|
358
|
+
`[VoiceManager] Requesting voice join guild=${channel.guildId} channel=${channelId}`,
|
|
359
|
+
);
|
|
360
|
+
const timeout = setTimeout(() => {
|
|
361
|
+
if (this.pending.has(channelId)) {
|
|
362
|
+
this.pending.delete(channelId);
|
|
363
|
+
reject(
|
|
364
|
+
new Error(
|
|
365
|
+
'Voice connection timeout. Ensure the server has voice enabled and the bot has Connect permissions. ' +
|
|
366
|
+
'The gateway must send VoiceServerUpdate and VoiceStateUpdate in response.',
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}, 20_000);
|
|
371
|
+
this.pending.set(channelId, {
|
|
372
|
+
channel,
|
|
373
|
+
resolve: (c) => {
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
resolve(c);
|
|
376
|
+
},
|
|
377
|
+
reject: (e) => {
|
|
378
|
+
clearTimeout(timeout);
|
|
379
|
+
reject(e);
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
this.client.sendToGateway(this.shardId, {
|
|
383
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
384
|
+
d: {
|
|
385
|
+
guild_id: channel.guildId,
|
|
386
|
+
channel_id: channel.id,
|
|
387
|
+
self_mute: false,
|
|
388
|
+
self_deaf: false,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Leave all voice channels in a guild.
|
|
396
|
+
* With multi-channel support, disconnects from every channel in the guild.
|
|
397
|
+
* @param guildId - Guild ID to leave
|
|
398
|
+
*/
|
|
399
|
+
leave(guildId: string): void {
|
|
400
|
+
const toLeave: { channelId: string; conn: VoiceConnection | LiveKitRtcConnection }[] = [];
|
|
401
|
+
for (const [cid, c] of this.connections) {
|
|
402
|
+
if (c?.channel?.guildId === guildId) toLeave.push({ channelId: cid, conn: c });
|
|
403
|
+
}
|
|
404
|
+
for (const { channelId, conn } of toLeave) {
|
|
405
|
+
conn.destroy();
|
|
406
|
+
this.connections.delete(channelId);
|
|
407
|
+
this.connectionIds.delete(channelId);
|
|
408
|
+
}
|
|
409
|
+
if (toLeave.length > 0) {
|
|
410
|
+
this.client.sendToGateway(this.shardId, {
|
|
411
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
412
|
+
d: {
|
|
413
|
+
guild_id: guildId,
|
|
414
|
+
channel_id: null,
|
|
415
|
+
self_mute: false,
|
|
416
|
+
self_deaf: false,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Leave a specific voice channel by channel ID.
|
|
424
|
+
* @param channelId - Channel ID to leave
|
|
425
|
+
*/
|
|
426
|
+
leaveChannel(channelId: string): void {
|
|
427
|
+
const conn = this.connections.get(channelId);
|
|
428
|
+
if (conn) {
|
|
429
|
+
const guildId = conn.channel?.guildId;
|
|
430
|
+
conn.destroy();
|
|
431
|
+
this.connections.delete(channelId);
|
|
432
|
+
this.connectionIds.delete(channelId);
|
|
433
|
+
if (guildId) {
|
|
434
|
+
this.client.sendToGateway(this.shardId, {
|
|
435
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
436
|
+
d: {
|
|
437
|
+
guild_id: guildId,
|
|
438
|
+
channel_id: null,
|
|
439
|
+
self_mute: false,
|
|
440
|
+
self_deaf: false,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get the active voice connection for a channel or guild.
|
|
449
|
+
* @param channelOrGuildId - Channel ID (primary) or guild ID (returns first connection in that guild)
|
|
450
|
+
*/
|
|
451
|
+
getConnection(channelOrGuildId: string): VoiceConnection | LiveKitRtcConnection | undefined {
|
|
452
|
+
const byChannel = this.connections.get(channelOrGuildId);
|
|
453
|
+
if (byChannel) return byChannel;
|
|
454
|
+
for (const [, c] of this.connections) {
|
|
455
|
+
if (c?.channel?.guildId === channelOrGuildId) return c;
|
|
456
|
+
}
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Update voice state (e.g. self_stream, self_video) while in a channel.
|
|
462
|
+
* Sends a VoiceStateUpdate to the gateway so the server and clients see the change.
|
|
463
|
+
* Requires connection_id (from VoiceServerUpdate); without it, the gateway would treat
|
|
464
|
+
* the update as a new join and trigger a new VoiceServerUpdate, causing connection loops.
|
|
465
|
+
* @param channelId - Channel ID (connection key)
|
|
466
|
+
* @param partial - Partial voice state to update (self_stream, self_video, self_mute, self_deaf)
|
|
467
|
+
*/
|
|
468
|
+
updateVoiceState(
|
|
469
|
+
channelId: string,
|
|
470
|
+
partial: {
|
|
471
|
+
self_stream?: boolean;
|
|
472
|
+
self_video?: boolean;
|
|
473
|
+
self_mute?: boolean;
|
|
474
|
+
self_deaf?: boolean;
|
|
475
|
+
},
|
|
476
|
+
): void {
|
|
477
|
+
const conn = this.connections.get(channelId);
|
|
478
|
+
if (!conn) return;
|
|
479
|
+
|
|
480
|
+
const connectionId = this.connectionIds.get(channelId);
|
|
481
|
+
const guildId = conn.channel?.guildId;
|
|
482
|
+
if (!connectionId) {
|
|
483
|
+
this.client.emit?.(
|
|
484
|
+
'debug',
|
|
485
|
+
`[VoiceManager] Skipping voice state sync: no connection_id for channel ${channelId}`,
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.client.sendToGateway(this.shardId, {
|
|
491
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
492
|
+
d: {
|
|
493
|
+
guild_id: guildId ?? '',
|
|
494
|
+
channel_id: conn.channel.id,
|
|
495
|
+
connection_id: connectionId,
|
|
496
|
+
self_mute: partial.self_mute ?? false,
|
|
497
|
+
self_deaf: partial.self_deaf ?? false,
|
|
498
|
+
self_video: partial.self_video ?? false,
|
|
499
|
+
self_stream: partial.self_stream ?? false,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { VoiceManager, VoiceConnection, LiveKitRtcConnection, getVoiceManager } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('@erinjs/voice exports', () => {
|
|
5
|
+
it('exports VoiceManager class', () => {
|
|
6
|
+
expect(VoiceManager).toBeDefined();
|
|
7
|
+
expect(typeof VoiceManager).toBe('function');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('exports VoiceConnection class', () => {
|
|
11
|
+
expect(VoiceConnection).toBeDefined();
|
|
12
|
+
expect(typeof VoiceConnection).toBe('function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('exports LiveKitRtcConnection class', () => {
|
|
16
|
+
expect(LiveKitRtcConnection).toBeDefined();
|
|
17
|
+
expect(typeof LiveKitRtcConnection).toBe('function');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('exports getVoiceManager function', () => {
|
|
21
|
+
expect(getVoiceManager).toBeDefined();
|
|
22
|
+
expect(typeof getVoiceManager).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('getVoiceManager returns VoiceManager for mock client', () => {
|
|
26
|
+
const mockClient = { on: () => {} };
|
|
27
|
+
const manager = getVoiceManager(mockClient);
|
|
28
|
+
expect(manager).toBeInstanceOf(VoiceManager);
|
|
29
|
+
expect(typeof manager.join).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('getVoiceManager returns same instance for same client', () => {
|
|
33
|
+
const mockClient = { on: () => {} };
|
|
34
|
+
const m1 = getVoiceManager(mockClient);
|
|
35
|
+
const m2 = getVoiceManager(mockClient);
|
|
36
|
+
expect(m1).toBe(m2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export { VoiceManager, type VoiceManagerOptions, type VoiceStateMap } from './VoiceManager.js';
|
|
2
|
+
export { VoiceConnection, type VoiceConnectionEvents } from './VoiceConnection.js';
|
|
3
|
+
export {
|
|
4
|
+
LiveKitRtcConnection,
|
|
5
|
+
type LiveKitRtcConnectionEvents,
|
|
6
|
+
type LiveKitAudioFrame,
|
|
7
|
+
type LiveKitReceiveSubscription,
|
|
8
|
+
type VideoPlayOptions,
|
|
9
|
+
} from './LiveKitRtcConnection.js';
|
|
10
|
+
import { Client } from '@erinjs/core';
|
|
11
|
+
import { VoiceChannel } from '@erinjs/core';
|
|
12
|
+
import { VoiceManager } from './VoiceManager.js';
|
|
13
|
+
import { VoiceConnection } from './VoiceConnection';
|
|
14
|
+
import { LiveKitRtcConnection } from './LiveKitRtcConnection';
|
|
15
|
+
|
|
16
|
+
/** Union of connection types (Discord-style or LiveKit). */
|
|
17
|
+
export type VoiceConnectionLike = VoiceConnection | LiveKitRtcConnection;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a voice manager and join a voice channel in one call.
|
|
21
|
+
*
|
|
22
|
+
* @param client - The Fluxer client instance
|
|
23
|
+
* @param channel - The voice channel to join
|
|
24
|
+
* @param options - Optional options; `shardId` for the gateway shard to use (default 0)
|
|
25
|
+
* @returns The voice connection (LiveKitRtcConnection when using LiveKit)
|
|
26
|
+
*/
|
|
27
|
+
export async function joinVoiceChannel(
|
|
28
|
+
client: Client,
|
|
29
|
+
channel: VoiceChannel,
|
|
30
|
+
options?: { shardId?: number },
|
|
31
|
+
): Promise<VoiceConnectionLike> {
|
|
32
|
+
const manager = getVoiceManager(client, options);
|
|
33
|
+
return manager.join(channel);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const voiceManagers = new WeakMap<Client, VoiceManager>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get or create the VoiceManager for this client.
|
|
40
|
+
*
|
|
41
|
+
* @param client - The Fluxer client instance
|
|
42
|
+
* @param options - Optional options; `shardId` for the gateway shard to use (default 0)
|
|
43
|
+
*/
|
|
44
|
+
export function getVoiceManager(client: Client, options?: { shardId?: number }): VoiceManager {
|
|
45
|
+
let manager = voiceManagers.get(client);
|
|
46
|
+
if (!manager) {
|
|
47
|
+
manager = new VoiceManager(client, options);
|
|
48
|
+
voiceManagers.set(client, manager);
|
|
49
|
+
}
|
|
50
|
+
return manager;
|
|
51
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isLiveKitEndpoint, buildLiveKitUrlForRtcSdk } from './livekit.js';
|
|
3
|
+
|
|
4
|
+
describe('isLiveKitEndpoint', () => {
|
|
5
|
+
it('returns false for null, undefined, empty', () => {
|
|
6
|
+
expect(isLiveKitEndpoint(null)).toBe(false);
|
|
7
|
+
expect(isLiveKitEndpoint(undefined)).toBe(false);
|
|
8
|
+
expect(isLiveKitEndpoint('')).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns true when access_token in URL', () => {
|
|
12
|
+
expect(isLiveKitEndpoint('https://ferret.fluxer.media?access_token=abc')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns true when /rtc and query params', () => {
|
|
16
|
+
expect(isLiveKitEndpoint('wss://host/rtc?token=xyz')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns true when token provided and endpoint has no query', () => {
|
|
20
|
+
expect(isLiveKitEndpoint('ferret.iad.fluxer.media', 'token123')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns false for plain host without token', () => {
|
|
24
|
+
expect(isLiveKitEndpoint('ferret.iad.fluxer.media')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('buildLiveKitUrlForRtcSdk', () => {
|
|
29
|
+
it('strips scheme and path, keeps host', () => {
|
|
30
|
+
const url = buildLiveKitUrlForRtcSdk('wss://ferret.fluxer.media/rtc');
|
|
31
|
+
expect(url).toMatch(/wss:\/\/ferret\.fluxer\.media/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('uses wss when endpoint has wss', () => {
|
|
35
|
+
const url = buildLiveKitUrlForRtcSdk('wss://host.com');
|
|
36
|
+
expect(url).toBe('wss://host.com');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('uses ws when endpoint has ws', () => {
|
|
40
|
+
const url = buildLiveKitUrlForRtcSdk('ws://host.com');
|
|
41
|
+
expect(url).toBe('ws://host.com');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('defaults to wss for host without scheme', () => {
|
|
45
|
+
const url = buildLiveKitUrlForRtcSdk('host.example.com');
|
|
46
|
+
expect(url).toContain('wss://');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for LiveKit voice endpoints (e.g. Fluxer with access_token in URL).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when we should use LiveKit: full URL with access_token, or host + token (Fluxer sends host only to bots).
|
|
7
|
+
*/
|
|
8
|
+
export function isLiveKitEndpoint(
|
|
9
|
+
endpoint: string | null | undefined,
|
|
10
|
+
token?: string | null,
|
|
11
|
+
): boolean {
|
|
12
|
+
if (!endpoint || typeof endpoint !== 'string') return false;
|
|
13
|
+
const s = endpoint.trim();
|
|
14
|
+
if (s.includes('access_token=') || (s.includes('/rtc') && s.includes('?'))) return true;
|
|
15
|
+
// Gateway may send only host (e.g. ferret.iad.fluxer.media) + token
|
|
16
|
+
if (token && !s.includes('?')) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build base WebSocket URL for @livekit/rtc-node SDK. The SDK adds /rtc internally.
|
|
22
|
+
* Use this for room.connect(url, token) - pass token separately.
|
|
23
|
+
* Removes trailing slashes and /rtc path to avoid 404 (SDK adds /rtc itself).
|
|
24
|
+
*/
|
|
25
|
+
export function buildLiveKitUrlForRtcSdk(endpoint: string): string {
|
|
26
|
+
const base =
|
|
27
|
+
endpoint
|
|
28
|
+
.replace(/^(wss|ws|https?):\/\//i, '')
|
|
29
|
+
.replace(/^\/+/, '')
|
|
30
|
+
.split('/')[0] ?? endpoint;
|
|
31
|
+
const scheme = /^wss?:\/\//i.test(endpoint) ? (endpoint.startsWith('wss') ? 'wss' : 'ws') : 'wss';
|
|
32
|
+
return `${scheme}://${base.replace(/\/+$/, '')}`;
|
|
33
|
+
}
|