@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,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { RequestManager } from './RequestManager.js';
|
|
3
|
+
import { HTTPError, FluxerAPIError } from './errors/index.js';
|
|
4
|
+
|
|
5
|
+
describe('RequestManager', () => {
|
|
6
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
fetchMock = vi.fn();
|
|
10
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.unstubAllGlobals();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('constructor uses defaults', () => {
|
|
18
|
+
const rm = new RequestManager({});
|
|
19
|
+
expect(rm.baseUrl).toBe('https://api.fluxer.app/v1');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('constructor accepts overrides', () => {
|
|
23
|
+
const rm = new RequestManager({ api: 'https://test', version: '2' });
|
|
24
|
+
expect(rm.baseUrl).toBe('https://test/v2');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('request succeeds with JSON body', async () => {
|
|
28
|
+
const rm = new RequestManager({ retries: 0 });
|
|
29
|
+
fetchMock.mockResolvedValueOnce({
|
|
30
|
+
ok: true,
|
|
31
|
+
status: 200,
|
|
32
|
+
text: () => Promise.resolve('{"id":"123"}'),
|
|
33
|
+
headers: new Headers(),
|
|
34
|
+
});
|
|
35
|
+
const result = await rm.request('GET', '/channels/123');
|
|
36
|
+
expect(result).toEqual({ id: '123' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('request returns undefined for 204', async () => {
|
|
40
|
+
const rm = new RequestManager({ retries: 0 });
|
|
41
|
+
fetchMock.mockResolvedValueOnce({
|
|
42
|
+
ok: true,
|
|
43
|
+
status: 204,
|
|
44
|
+
text: () => Promise.resolve(''),
|
|
45
|
+
headers: new Headers(),
|
|
46
|
+
});
|
|
47
|
+
const result = await rm.request('DELETE', '/channels/123');
|
|
48
|
+
expect(result).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('request throws FluxerAPIError for non-ok with JSON body', async () => {
|
|
52
|
+
const rm = new RequestManager({ retries: 0 });
|
|
53
|
+
fetchMock.mockResolvedValueOnce({
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 404,
|
|
56
|
+
text: () => Promise.resolve('{"code":10003,"message":"Unknown Channel"}'),
|
|
57
|
+
headers: new Headers(),
|
|
58
|
+
});
|
|
59
|
+
await expect(rm.request('GET', '/channels/999')).rejects.toThrow(FluxerAPIError);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('request throws HTTPError for non-JSON error body', async () => {
|
|
63
|
+
const rm = new RequestManager({ retries: 0 });
|
|
64
|
+
fetchMock.mockResolvedValueOnce({
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 500,
|
|
67
|
+
text: () => Promise.resolve('Internal Server Error'),
|
|
68
|
+
headers: new Headers(),
|
|
69
|
+
});
|
|
70
|
+
await expect(rm.request('GET', '/channels/1')).rejects.toThrow(HTTPError);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('request uses full URL when route starts with http', async () => {
|
|
74
|
+
const rm = new RequestManager({ retries: 0 });
|
|
75
|
+
fetchMock.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
status: 200,
|
|
78
|
+
text: () => Promise.resolve('{}'),
|
|
79
|
+
headers: new Headers(),
|
|
80
|
+
});
|
|
81
|
+
await rm.request('GET', 'https://cdn.example.com/asset/123');
|
|
82
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
83
|
+
'https://cdn.example.com/asset/123',
|
|
84
|
+
expect.objectContaining({ method: 'GET' }),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { RateLimitManager } from './RateLimitManager.js';
|
|
2
|
+
import { FluxerAPIError, RateLimitError, HTTPError } from './errors/index.js';
|
|
3
|
+
import { APIErrorBody, RateLimitErrorBody } from '@erinjs/types';
|
|
4
|
+
import { buildFormData } from './utils/files.js';
|
|
5
|
+
|
|
6
|
+
export interface RequestOptions {
|
|
7
|
+
body?: unknown | FormData;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
files?: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
data: Blob | ArrayBuffer | Uint8Array | Buffer;
|
|
12
|
+
filename?: string;
|
|
13
|
+
}>;
|
|
14
|
+
auth?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RestOptions {
|
|
18
|
+
api: string;
|
|
19
|
+
version: string;
|
|
20
|
+
authPrefix: 'Bot' | 'Bearer';
|
|
21
|
+
timeout: number;
|
|
22
|
+
retries: number;
|
|
23
|
+
userAgent: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RequestManager {
|
|
27
|
+
private token: string | null = null;
|
|
28
|
+
private readonly options: RestOptions;
|
|
29
|
+
private readonly rateLimiter = new RateLimitManager();
|
|
30
|
+
|
|
31
|
+
constructor(options: Partial<RestOptions>) {
|
|
32
|
+
this.options = {
|
|
33
|
+
api: options.api ?? 'https://api.fluxer.app',
|
|
34
|
+
version: options.version ?? '1',
|
|
35
|
+
authPrefix: options.authPrefix ?? 'Bot',
|
|
36
|
+
timeout: options.timeout ?? 15000,
|
|
37
|
+
retries: options.retries ?? 3,
|
|
38
|
+
userAgent: options.userAgent ?? 'erin.js',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setToken(token: string | null): void {
|
|
43
|
+
this.token = token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get baseUrl(): string {
|
|
47
|
+
return `${this.options.api}/v${this.options.version}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Hash route for rate limit bucket (use path without ids for grouping). */
|
|
51
|
+
private getRouteHash(route: string): string {
|
|
52
|
+
return route.replace(/\d{17,19}/g, ':id');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async waitForRateLimit(routeHash: string): Promise<void> {
|
|
56
|
+
const wait = this.rateLimiter.getWaitTime(routeHash);
|
|
57
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private buildHeaders(
|
|
61
|
+
_route: string,
|
|
62
|
+
options: RequestOptions,
|
|
63
|
+
body: string | FormData | undefined,
|
|
64
|
+
): Record<string, string> {
|
|
65
|
+
const headers: Record<string, string> = {
|
|
66
|
+
'User-Agent': this.options.userAgent,
|
|
67
|
+
...options.headers,
|
|
68
|
+
};
|
|
69
|
+
if (options.auth !== false && this.token) {
|
|
70
|
+
headers['Authorization'] = `${this.options.authPrefix} ${this.token}`;
|
|
71
|
+
}
|
|
72
|
+
if (body !== undefined && !(body instanceof FormData)) {
|
|
73
|
+
headers['Content-Type'] = 'application/json';
|
|
74
|
+
}
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async request<T>(method: string, route: string, options: RequestOptions = {}): Promise<T> {
|
|
79
|
+
const routeHash = this.getRouteHash(route);
|
|
80
|
+
const url = route.startsWith('http') ? route : `${this.baseUrl}${route}`;
|
|
81
|
+
|
|
82
|
+
await this.waitForRateLimit(routeHash);
|
|
83
|
+
|
|
84
|
+
let body: string | FormData | undefined;
|
|
85
|
+
if (options.body !== undefined) {
|
|
86
|
+
if (options.body instanceof FormData) {
|
|
87
|
+
body = options.body;
|
|
88
|
+
} else if (
|
|
89
|
+
options.files?.length &&
|
|
90
|
+
typeof options.body === 'object' &&
|
|
91
|
+
options.body !== null
|
|
92
|
+
) {
|
|
93
|
+
body = buildFormData(options.body as Record<string, unknown>, options.files);
|
|
94
|
+
} else {
|
|
95
|
+
body = JSON.stringify(options.body);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const headers = this.buildHeaders(route, options, body);
|
|
100
|
+
|
|
101
|
+
let lastError: Error | null = null;
|
|
102
|
+
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
body,
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.rateLimiter.updateFromHeaders(routeHash, response.headers);
|
|
114
|
+
|
|
115
|
+
if (response.status === 429) {
|
|
116
|
+
const data = (await response.json().catch(() => ({}))) as RateLimitErrorBody;
|
|
117
|
+
const retryAfter =
|
|
118
|
+
(data.retry_after ?? parseInt(response.headers.get('Retry-After') ?? '0', 10)) * 1000;
|
|
119
|
+
this.rateLimiter.setBucket(routeHash, 1, 0, Date.now() + retryAfter);
|
|
120
|
+
if (data.global) this.rateLimiter.setGlobalReset(Date.now() + retryAfter);
|
|
121
|
+
throw new RateLimitError(
|
|
122
|
+
{
|
|
123
|
+
...data,
|
|
124
|
+
code: 'RATE_LIMITED',
|
|
125
|
+
message: data.message ?? 'Rate limited',
|
|
126
|
+
retry_after: data.retry_after ?? 0,
|
|
127
|
+
},
|
|
128
|
+
response.status,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const text = await response.text();
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
let parsed: APIErrorBody;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text) as APIErrorBody;
|
|
137
|
+
} catch {
|
|
138
|
+
throw new HTTPError(response.status, text);
|
|
139
|
+
}
|
|
140
|
+
throw new FluxerAPIError(parsed, response.status);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (response.status === 204 || text.length === 0) return undefined as T;
|
|
144
|
+
return JSON.parse(text) as T;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
lastError =
|
|
148
|
+
attempt > 0
|
|
149
|
+
? new Error(`Retry ${attempt} failed: ${wrapped.message}`, {
|
|
150
|
+
cause: wrapped,
|
|
151
|
+
})
|
|
152
|
+
: wrapped;
|
|
153
|
+
if (err instanceof RateLimitError && attempt < this.options.retries) {
|
|
154
|
+
const retryMs = err.retryAfter * 1000;
|
|
155
|
+
if (Number.isFinite(retryMs)) {
|
|
156
|
+
await new Promise((r) => setTimeout(r, retryMs));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (err instanceof FluxerAPIError || err instanceof HTTPError) throw err;
|
|
161
|
+
if (attempt < this.options.retries) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw lastError;
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw lastError ?? new Error('Request failed');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FluxerAPIError } from './FluxerAPIError.js';
|
|
3
|
+
|
|
4
|
+
describe('FluxerAPIError', () => {
|
|
5
|
+
it('creates error with message from body', () => {
|
|
6
|
+
const err = new FluxerAPIError(
|
|
7
|
+
{ message: 'Channel not found', code: 'CHANNEL_NOT_FOUND' },
|
|
8
|
+
404,
|
|
9
|
+
);
|
|
10
|
+
expect(err.message).toBe('Channel not found');
|
|
11
|
+
expect(err.name).toBe('FluxerAPIError');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('stores code and statusCode', () => {
|
|
15
|
+
const err = new FluxerAPIError({ message: 'Rate limited', code: 'RATE_LIMITED' }, 429);
|
|
16
|
+
expect(err.code).toBe('RATE_LIMITED');
|
|
17
|
+
expect(err.statusCode).toBe(429);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('stores optional errors field', () => {
|
|
21
|
+
const errors = { field: ['invalid value'] };
|
|
22
|
+
const err = new FluxerAPIError(
|
|
23
|
+
{ message: 'Validation failed', code: 'VALIDATION_ERROR', errors },
|
|
24
|
+
400,
|
|
25
|
+
);
|
|
26
|
+
expect(err.errors).toEqual(errors);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('isRetryable returns true for 429', () => {
|
|
30
|
+
const err = new FluxerAPIError({ message: 'Rate limited', code: 'RATE_LIMITED' }, 429);
|
|
31
|
+
expect(err.isRetryable).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('isRetryable returns true for 5xx', () => {
|
|
35
|
+
expect(new FluxerAPIError({ message: 'Server error', code: 'INTERNAL' }, 500).isRetryable).toBe(
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
expect(
|
|
39
|
+
new FluxerAPIError({ message: 'Bad gateway', code: 'BAD_GATEWAY' }, 502).isRetryable,
|
|
40
|
+
).toBe(true);
|
|
41
|
+
expect(
|
|
42
|
+
new FluxerAPIError({ message: 'Unavailable', code: 'UNAVAILABLE' }, 503).isRetryable,
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('isRetryable returns false for 4xx (except 429)', () => {
|
|
47
|
+
expect(new FluxerAPIError({ message: 'Not found', code: 'NOT_FOUND' }, 404).isRetryable).toBe(
|
|
48
|
+
false,
|
|
49
|
+
);
|
|
50
|
+
expect(new FluxerAPIError({ message: 'Forbidden', code: 'FORBIDDEN' }, 403).isRetryable).toBe(
|
|
51
|
+
false,
|
|
52
|
+
);
|
|
53
|
+
expect(
|
|
54
|
+
new FluxerAPIError({ message: 'Bad request', code: 'BAD_REQUEST' }, 400).isRetryable,
|
|
55
|
+
).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { APIErrorBody } from '@erinjs/types';
|
|
2
|
+
|
|
3
|
+
export class FluxerAPIError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly statusCode: number;
|
|
6
|
+
readonly errors?: APIErrorBody['errors'];
|
|
7
|
+
|
|
8
|
+
constructor(body: APIErrorBody, statusCode: number) {
|
|
9
|
+
super(body.message);
|
|
10
|
+
this.name = 'FluxerAPIError';
|
|
11
|
+
this.code = body.code;
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.errors = body.errors;
|
|
14
|
+
Object.setPrototypeOf(this, FluxerAPIError.prototype);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** True if the error is retryable (429 rate limit, 5xx server errors). */
|
|
18
|
+
get isRetryable(): boolean {
|
|
19
|
+
return this.statusCode === 429 || (this.statusCode >= 500 && this.statusCode < 600);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { HTTPError } from './HTTPError.js';
|
|
3
|
+
|
|
4
|
+
describe('HTTPError', () => {
|
|
5
|
+
it('creates error with status and body', () => {
|
|
6
|
+
const err = new HTTPError(404, '{"error":"not found"}');
|
|
7
|
+
expect(err.message).toContain('404');
|
|
8
|
+
expect(err.message).toContain('not found');
|
|
9
|
+
expect(err.name).toBe('HTTPError');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('stores statusCode and body', () => {
|
|
13
|
+
const err = new HTTPError(500, 'Internal Server Error');
|
|
14
|
+
expect(err.statusCode).toBe(500);
|
|
15
|
+
expect(err.body).toBe('Internal Server Error');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts null body', () => {
|
|
19
|
+
const err = new HTTPError(502, null);
|
|
20
|
+
expect(err.body).toBeNull();
|
|
21
|
+
expect(err.message).toContain('502');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('uses status hint when body is empty', () => {
|
|
25
|
+
const err = new HTTPError(503, '');
|
|
26
|
+
expect(err.message).toContain('Service Unavailable');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('isRetryable returns true for 429', () => {
|
|
30
|
+
const err = new HTTPError(429, 'Too Many Requests');
|
|
31
|
+
expect(err.isRetryable).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('isRetryable returns true for 5xx', () => {
|
|
35
|
+
expect(new HTTPError(500, '').isRetryable).toBe(true);
|
|
36
|
+
expect(new HTTPError(502, '').isRetryable).toBe(true);
|
|
37
|
+
expect(new HTTPError(503, '').isRetryable).toBe(true);
|
|
38
|
+
expect(new HTTPError(504, '').isRetryable).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('isRetryable returns false for 4xx (except 429)', () => {
|
|
42
|
+
expect(new HTTPError(400, '').isRetryable).toBe(false);
|
|
43
|
+
expect(new HTTPError(403, '').isRetryable).toBe(false);
|
|
44
|
+
expect(new HTTPError(404, '').isRetryable).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('isRetryable returns false for 599 (upper boundary)', () => {
|
|
48
|
+
expect(new HTTPError(599, '').isRetryable).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('uses body when present over status hint', () => {
|
|
52
|
+
const err = new HTTPError(502, 'Custom error body');
|
|
53
|
+
expect(err.message).toContain('Custom error body');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const STATUS_MESSAGES: Record<number, string> = {
|
|
2
|
+
502: 'Bad Gateway — Fluxer API may be temporarily unavailable.',
|
|
3
|
+
503: 'Service Unavailable — Fluxer API is down or overloaded. Try again later.',
|
|
4
|
+
504: 'Gateway Timeout — Fluxer API did not respond in time.',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class HTTPError extends Error {
|
|
8
|
+
readonly statusCode: number;
|
|
9
|
+
readonly body: string | null;
|
|
10
|
+
|
|
11
|
+
constructor(statusCode: number, body: string | null) {
|
|
12
|
+
const hint = STATUS_MESSAGES[statusCode];
|
|
13
|
+
const detail = body?.trim() || (hint ?? 'No body');
|
|
14
|
+
super(`HTTP ${statusCode}: ${detail}`);
|
|
15
|
+
this.name = 'HTTPError';
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.body = body;
|
|
18
|
+
Object.setPrototypeOf(this, HTTPError.prototype);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True if the error is retryable (429 rate limit, 5xx server errors). */
|
|
22
|
+
get isRetryable(): boolean {
|
|
23
|
+
return this.statusCode === 429 || (this.statusCode >= 500 && this.statusCode < 600);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RateLimitError } from './RateLimitError.js';
|
|
3
|
+
|
|
4
|
+
describe('RateLimitError', () => {
|
|
5
|
+
it('extends FluxerAPIError with retryAfter and global', () => {
|
|
6
|
+
const err = new RateLimitError(
|
|
7
|
+
{ message: 'Rate limited', code: 'RATE_LIMITED', retry_after: 5 },
|
|
8
|
+
429,
|
|
9
|
+
);
|
|
10
|
+
expect(err.message).toBe('Rate limited');
|
|
11
|
+
expect(err.name).toBe('RateLimitError');
|
|
12
|
+
expect(err.statusCode).toBe(429);
|
|
13
|
+
expect(err.retryAfter).toBe(5);
|
|
14
|
+
expect(err.global).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('sets global from body when true', () => {
|
|
18
|
+
const err = new RateLimitError(
|
|
19
|
+
{ message: 'Global rate limit', code: 'RATE_LIMITED', retry_after: 10, global: true },
|
|
20
|
+
429,
|
|
21
|
+
);
|
|
22
|
+
expect(err.global).toBe(true);
|
|
23
|
+
expect(err.retryAfter).toBe(10);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('defaults global to false when omitted', () => {
|
|
27
|
+
const err = new RateLimitError(
|
|
28
|
+
{ message: 'Limited', code: 'RATE_LIMITED', retry_after: 1 },
|
|
29
|
+
429,
|
|
30
|
+
);
|
|
31
|
+
expect(err.global).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('isRetryable returns true (inherited from FluxerAPIError)', () => {
|
|
35
|
+
const err = new RateLimitError(
|
|
36
|
+
{ message: 'Limited', code: 'RATE_LIMITED', retry_after: 1 },
|
|
37
|
+
429,
|
|
38
|
+
);
|
|
39
|
+
expect(err.isRetryable).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RateLimitErrorBody } from '@erinjs/types';
|
|
2
|
+
import { FluxerAPIError } from './FluxerAPIError.js';
|
|
3
|
+
|
|
4
|
+
export class RateLimitError extends FluxerAPIError {
|
|
5
|
+
readonly retryAfter: number;
|
|
6
|
+
readonly global: boolean;
|
|
7
|
+
|
|
8
|
+
constructor(body: RateLimitErrorBody, statusCode: number) {
|
|
9
|
+
super(body, statusCode);
|
|
10
|
+
this.retryAfter = body.retry_after;
|
|
11
|
+
this.global = body.global ?? false;
|
|
12
|
+
this.name = 'RateLimitError';
|
|
13
|
+
Object.setPrototypeOf(this, RateLimitError.prototype);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { REST, type RESTOptions } from './REST.js';
|
|
2
|
+
export { RequestManager, type RequestOptions, type RestOptions } from './RequestManager.js';
|
|
3
|
+
export { RateLimitManager, type RateLimitState } from './RateLimitManager.js';
|
|
4
|
+
export { FluxerAPIError, RateLimitError, HTTPError } from './errors/index.js';
|
|
5
|
+
export { buildFormData, type AttachmentPayload, type AttachmentData } from './utils/files.js';
|
|
6
|
+
export { Routes } from '@erinjs/types';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_API,
|
|
4
|
+
DEFAULT_VERSION,
|
|
5
|
+
DEFAULT_USER_AGENT,
|
|
6
|
+
REQUEST_TIMEOUT,
|
|
7
|
+
MAX_RETRIES,
|
|
8
|
+
} from './constants.js';
|
|
9
|
+
|
|
10
|
+
describe('rest constants', () => {
|
|
11
|
+
it('DEFAULT_API points to Fluxer API', () => {
|
|
12
|
+
expect(DEFAULT_API).toBe('https://api.fluxer.app');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('DEFAULT_VERSION is 1', () => {
|
|
16
|
+
expect(DEFAULT_VERSION).toBe('1');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('DEFAULT_USER_AGENT contains erin.js and repository URL', () => {
|
|
20
|
+
expect(DEFAULT_USER_AGENT).toContain('erin.js');
|
|
21
|
+
expect(DEFAULT_USER_AGENT).toContain('github.com/blstmo-abandoned-us-for-the-milk/core');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('REQUEST_TIMEOUT is 15 seconds', () => {
|
|
25
|
+
expect(REQUEST_TIMEOUT).toBe(15000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('MAX_RETRIES is 3', () => {
|
|
29
|
+
expect(MAX_RETRIES).toBe(3);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildFormData } from './files.js';
|
|
3
|
+
|
|
4
|
+
describe('buildFormData', () => {
|
|
5
|
+
it('creates FormData with payload_json only', () => {
|
|
6
|
+
const payload = { content: 'Hello' };
|
|
7
|
+
const form = buildFormData(payload);
|
|
8
|
+
expect(form.get('payload_json')).toBe(JSON.stringify(payload));
|
|
9
|
+
expect(form.get('files[0]')).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('adds files with auto-generated attachments metadata', () => {
|
|
13
|
+
const payload = { content: 'With file' };
|
|
14
|
+
const files = [{ name: 'test.txt', data: new Uint8Array([1, 2, 3]) }];
|
|
15
|
+
const form = buildFormData(payload, files);
|
|
16
|
+
expect(form.get('payload_json')).toBeTruthy();
|
|
17
|
+
const parsed = JSON.parse(form.get('payload_json') as string);
|
|
18
|
+
expect(parsed.attachments).toEqual([{ id: 0, filename: 'test.txt' }]);
|
|
19
|
+
expect(form.get('files[0]')).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('uses custom filename when provided', () => {
|
|
23
|
+
const payload = {};
|
|
24
|
+
const files = [{ name: 'a', data: new Uint8Array(), filename: 'custom.png' }];
|
|
25
|
+
const form = buildFormData(payload, files);
|
|
26
|
+
const parsed = JSON.parse(form.get('payload_json') as string);
|
|
27
|
+
expect(parsed.attachments).toEqual([{ id: 0, filename: 'custom.png' }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('preserves existing attachments when files provided', () => {
|
|
31
|
+
const payload = { attachments: [{ id: 0, filename: 'existing.png', description: 'desc' }] };
|
|
32
|
+
const files = [{ name: 'x', data: new Uint8Array() }];
|
|
33
|
+
const form = buildFormData(payload, files);
|
|
34
|
+
const parsed = JSON.parse(form.get('payload_json') as string);
|
|
35
|
+
expect(parsed.attachments[0].description).toBe('desc');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File handling for multipart requests. Matches fluxer_api parseMultipartMessageData exactly:
|
|
3
|
+
* - payload_json: JSON string with content, embeds, attachments metadata
|
|
4
|
+
* - files[N]: File parts where N is 0-based index (fluxer API expects files[0], files[1], etc.)
|
|
5
|
+
*
|
|
6
|
+
* @see fluxer_api MessageController.parseMultipartMessageData
|
|
7
|
+
* @see fluxer_api AttachmentDTOs.ClientAttachmentRequest (id, filename required)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type AttachmentData = Blob | ArrayBuffer | Uint8Array | Buffer;
|
|
11
|
+
|
|
12
|
+
export interface AttachmentPayload {
|
|
13
|
+
/** Used as filename when filename is not set (required) */
|
|
14
|
+
name: string;
|
|
15
|
+
data: AttachmentData;
|
|
16
|
+
/** Override filename for the part (defaults to name) */
|
|
17
|
+
filename?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert attachment data to a Blob. Handles Node.js Buffer (extends Uint8Array).
|
|
22
|
+
*/
|
|
23
|
+
function toBlob(data: AttachmentData): Blob {
|
|
24
|
+
if (data instanceof Blob) return data;
|
|
25
|
+
return new Blob([data as BlobPart]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a File instance for FormData append. Ensures server receives proper File
|
|
30
|
+
* (fluxer_api checks `file instanceof File`). Node.js 20+ has global File.
|
|
31
|
+
*/
|
|
32
|
+
function toFormDataFile(data: AttachmentData, filename: string): Blob | File {
|
|
33
|
+
const blob = toBlob(data);
|
|
34
|
+
if (typeof File !== 'undefined') {
|
|
35
|
+
return new File([blob], filename, { type: blob.type || 'application/octet-stream' });
|
|
36
|
+
}
|
|
37
|
+
return blob;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build FormData for message with attachments.
|
|
42
|
+
* Matches fluxer_api parseMultipartMessageData expectations:
|
|
43
|
+
* - payload_json: JSON string (required)
|
|
44
|
+
* - files[0], files[1], ...: File parts with indices matching payload_json.attachments[].id
|
|
45
|
+
*
|
|
46
|
+
* Attachment metadata in payload_json must have id and filename for each file.
|
|
47
|
+
*/
|
|
48
|
+
export function buildFormData(
|
|
49
|
+
payloadJson: Record<string, unknown>,
|
|
50
|
+
files?: AttachmentPayload[],
|
|
51
|
+
): FormData {
|
|
52
|
+
const form = new FormData();
|
|
53
|
+
|
|
54
|
+
// payload_json is required; must include attachments metadata when files present
|
|
55
|
+
const payload = { ...payloadJson };
|
|
56
|
+
if (files?.length && !payload.attachments) {
|
|
57
|
+
payload.attachments = files.map((f, i) => ({
|
|
58
|
+
id: i,
|
|
59
|
+
filename: f.filename ?? f.name,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
form.append('payload_json', JSON.stringify(payload));
|
|
63
|
+
|
|
64
|
+
if (files?.length) {
|
|
65
|
+
for (let i = 0; i < files.length; i++) {
|
|
66
|
+
const file = files[i];
|
|
67
|
+
if (!file) continue;
|
|
68
|
+
const filename = file.filename ?? file.name;
|
|
69
|
+
const part = toFormDataFile(file.data, filename);
|
|
70
|
+
form.append(`files[${i}]`, part, filename);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return form;
|
|
75
|
+
}
|