From 87cdad0b03d08284b07afa38200ea62e62a09385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= Date: Mon, 29 May 2023 22:34:47 -0400 Subject: [PATCH] Next: A complete Biscuit rewrite (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FINALLY WE REACHED AN AGREEMENT * chore: d-types adapt * websocket sucks, rest * changes * new look for core * 💀 * fix cdn routes, more structures Co-authored-by: Free 公園 * CDN routes * chore: change to rome Co-authored-by: Free 公園 * Oh shit, here we go again Co-authored-by: Free 公園 * fixes * mixin, handler events, ws Co-authored-by: Yuzu Co-authored-by: Free 公園 * change type * Error system (#133) * Co-authored-by: Free 公園 * chore: biscuit rebase * token leak goes brrrr * fix: events * chore: road to raw data * fix: managers typing * chore: fix gateway typing * feat: helpers * style: linter * Types for routes (#134) * typing for routes * managers Co-authored-by: Marcos Susaña * Types for routes (#134) * I wanna cry * Next (#136) * Merge #137 * chore: lineWidth to 140 * chore: README update --------- Co-authored-by: Yuzu Co-authored-by: Free 公園 Co-authored-by: ThisIsAName <46913407+NejireSupremacy@users.noreply.github.com> Co-authored-by: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com> Co-authored-by: MARCROCK22 --- .editorconfig | 6 +- .eslintignore | 5 - .eslintrc.yml | 224 -- .prettierrc.yml | 2 - package.json | 19 +- packages/api-types/CHANGELOG.md | 62 - packages/api-types/README.md | 38 - packages/api-types/package.json | 71 - packages/api-types/src/common.ts | 1295 --------- packages/api-types/src/index.ts | 5 - packages/api-types/src/utils/cdn.ts | 29 - packages/api-types/src/utils/constants.ts | 25 - packages/api-types/src/utils/routes.ts | 519 ---- packages/api-types/src/v10/index.ts | 2497 ----------------- packages/cache/CHANGELOG.md | 95 - packages/cache/README.md | 39 - packages/cache/package.json | 75 - packages/cache/src/cache.ts | 252 -- packages/cache/src/index.ts | 7 - packages/cache/src/resources/base-resource.ts | 117 - .../cache/src/resources/channel-resource.ts | 139 - .../src/resources/guild-emoji-resource.ts | 128 - .../src/resources/guild-member-resource.ts | 128 - .../cache/src/resources/guild-resource.ts | 339 --- .../src/resources/guild-role-resource.ts | 133 - .../src/resources/guild-sticker-resource.ts | 124 - .../src/resources/guild-voice-resource.ts | 127 - packages/cache/src/resources/index.ts | 15 - .../cache/src/resources/presence-resource.ts | 135 - packages/cache/src/resources/user-resource.ts | 122 - .../src/scheme/adapters/cache-adapter.ts | 63 - .../scheme/adapters/memory-cache-adapter.ts | 149 - .../scheme/adapters/redis-cache-adapter.ts | 195 -- .../scheme/transporters/base-transporter.ts | 7 - .../scheme/transporters/redis-transporter.ts | 3 - .../scheme/transporters/tcp-transporter.ts | 3 - packages/cache/src/types.ts | 55 - packages/cache/src/utils/options.ts | 48 - packages/cache/tsconfig.json | 7 - packages/cache/tsup.config.ts | 12 - packages/common/README.MD | 20 + packages/common/package.json | 75 + packages/common/src/Collection.ts | 163 ++ packages/common/src/Constants.ts | 20 + packages/common/src/Logger.ts | 128 + packages/common/src/Types.ts | 39 + packages/common/src/Util.ts | 596 ++++ packages/common/src/applyToClass.ts | 29 + packages/common/src/index.ts | 7 + packages/{api-types => common}/tsconfig.json | 0 packages/{api-types => common}/tsup.config.ts | 2 +- packages/core/README.md | 29 +- packages/core/package.json | 151 +- .../src/adapters/default-event-adapter.ts | 25 - packages/core/src/adapters/event-adapter.ts | 25 - packages/core/src/adapters/events.ts | 837 ------ packages/core/src/biscuit.ts | 496 ---- packages/core/src/events/handler.ts | 15 + packages/core/src/index.ts | 27 +- .../core/src/managers/ApplicationManager.ts | 121 + packages/core/src/managers/ChannelManager.ts | 214 ++ packages/core/src/managers/GuildManager.ts | 348 +++ .../core/src/managers/InteractionManager.ts | 57 + packages/core/src/managers/MainManager.ts | 19 + packages/core/src/managers/MemberManager.ts | 19 + packages/core/src/managers/UserManager.ts | 64 + packages/core/src/managers/WebhookManager.ts | 124 + packages/core/src/session.ts | 110 + packages/core/src/snowflakes.ts | 13 - packages/core/src/structures.ts | 34 - packages/core/src/structures/application.ts | 112 - packages/core/src/structures/attachment.ts | 36 - packages/core/src/structures/automod.ts | 215 -- packages/core/src/structures/base.ts | 12 - packages/core/src/structures/channels.ts | 1155 -------- packages/core/src/structures/components.ts | 280 -- packages/core/src/structures/embed.ts | 155 - packages/core/src/structures/emojis.ts | 85 - packages/core/src/structures/guilds.ts | 1360 --------- packages/core/src/structures/integration.ts | 87 - packages/core/src/structures/interactions.ts | 669 ----- packages/core/src/structures/invite.ts | 206 -- packages/core/src/structures/members.ts | 239 -- .../core/src/structures/message-reaction.ts | 97 - packages/core/src/structures/message.ts | 644 ----- packages/core/src/structures/presence.ts | 93 - packages/core/src/structures/role.ts | 95 - .../core/src/structures/scheduled-events.ts | 53 - .../structures/special/interaction-options.ts | 267 -- .../src/structures/special/permissions.ts | 147 - .../core/src/structures/stage-instance.ts | 63 - packages/core/src/structures/sticker.ts | 73 - packages/core/src/structures/user.ts | 123 - packages/core/src/structures/webhook.ts | 211 -- packages/core/src/structures/welcome.ts | 55 - packages/core/src/structures/widget.ts | 41 - packages/core/src/utils/calculate-shard.ts | 5 - packages/core/src/utils/url-to-base-64.ts | 68 - packages/core/src/utils/util.ts | 108 - packages/core/tsconfig.json | 2 +- packages/core/tsup.config.ts | 2 +- packages/helpers/CHANGELOG.md | 101 - packages/helpers/README.md | 4 +- packages/helpers/package.json | 7 +- packages/helpers/src/MessageEmbed.ts | 70 + packages/helpers/src/Utils.ts | 55 + .../builders/components/InputTextBuilder.ts | 52 - .../components/MessageActionRowBuilder.ts | 34 - .../components/MessageButtonBuilder.ts | 47 - .../components/MessageSelectMenuBuilder.ts | 103 - .../src/builders/embeds/embed-builder.ts | 106 - .../src/builders/slash/ApplicationCommand.ts | 122 - .../slash/ApplicationCommandOption.ts | 360 --- packages/helpers/src/collectors.ts | 70 - .../commands/contextMenu/ContextCommand.ts | 49 + packages/helpers/src/commands/index.ts | 1 + .../src/commands/slash/SlashCommand.ts | 57 + .../src/commands/slash/SlashCommandOption.ts | 346 +++ packages/helpers/src/components/ActionRow.ts | 30 + .../helpers/src/components/BaseComponent.ts | 11 + .../helpers/src/components/MessageButton.ts | 55 + packages/helpers/src/components/SelectMenu.ts | 117 + packages/helpers/src/components/TextInput.ts | 45 + packages/helpers/src/components/index.ts | 4 + packages/helpers/src/index.ts | 20 +- packages/helpers/tsup.config.ts | 2 +- packages/rest/README.md | 12 +- packages/rest/package.json | 144 +- packages/rest/src/CDN.ts | 68 + packages/rest/src/REST.ts | 82 + packages/rest/src/Router.ts | 68 + packages/rest/src/Routes/applications.ts | 89 + packages/rest/src/Routes/channels.ts | 218 ++ packages/rest/src/Routes/gateway.ts | 12 + packages/rest/src/Routes/guilds.ts | 381 +++ packages/rest/src/Routes/index.ts | 23 + packages/rest/src/Routes/interactions.ts | 15 + packages/rest/src/Routes/invites.ts | 10 + packages/rest/src/Routes/stage-instances.ts | 21 + packages/rest/src/Routes/stickers.ts | 12 + packages/rest/src/Routes/users.ts | 53 + packages/rest/src/Routes/voice.ts | 11 + packages/rest/src/Routes/webhooks.ts | 61 + .../rest/src/adapters/default-rest-adapter.ts | 807 ------ packages/rest/src/adapters/rest-adapter.ts | 125 - packages/rest/src/index.ts | 8 +- packages/rest/tsup.config.ts | 2 +- packages/ws/README.md | 42 +- packages/ws/package.json | 154 +- packages/ws/src/SharedTypes.ts | 238 ++ packages/ws/src/defaults.ts | 19 + packages/ws/src/index.ts | 10 +- packages/ws/src/manager/GatewayManager.ts | 182 ++ .../ws/src/manager/GatewayManagerTypes.ts | 88 + packages/ws/src/services/shard-manager.ts | 123 - packages/ws/src/services/shard.ts | 446 --- packages/ws/src/shard/Shard.ts | 582 ++++ packages/ws/src/shard/ShardTypes.ts | 80 + packages/ws/src/types.ts | 102 - packages/ws/src/utils/Bucket.ts | 122 + packages/ws/src/utils/bucket.ts | 168 -- packages/ws/src/utils/options.ts | 42 - packages/ws/tsconfig.json | 2 +- packages/ws/tsconfig.test.json | 7 + packages/ws/tsup.config.ts | 2 +- rome.json | 34 + tsconfig.build.json | 2 +- tsconfig.json | 3 +- turbo.json | 33 +- 169 files changed, 5810 insertions(+), 18640 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.yml delete mode 100644 .prettierrc.yml delete mode 100644 packages/api-types/CHANGELOG.md delete mode 100644 packages/api-types/README.md delete mode 100644 packages/api-types/package.json delete mode 100644 packages/api-types/src/common.ts delete mode 100644 packages/api-types/src/index.ts delete mode 100644 packages/api-types/src/utils/cdn.ts delete mode 100644 packages/api-types/src/utils/constants.ts delete mode 100644 packages/api-types/src/utils/routes.ts delete mode 100644 packages/api-types/src/v10/index.ts delete mode 100644 packages/cache/CHANGELOG.md delete mode 100644 packages/cache/README.md delete mode 100644 packages/cache/package.json delete mode 100644 packages/cache/src/cache.ts delete mode 100644 packages/cache/src/index.ts delete mode 100644 packages/cache/src/resources/base-resource.ts delete mode 100644 packages/cache/src/resources/channel-resource.ts delete mode 100644 packages/cache/src/resources/guild-emoji-resource.ts delete mode 100644 packages/cache/src/resources/guild-member-resource.ts delete mode 100644 packages/cache/src/resources/guild-resource.ts delete mode 100644 packages/cache/src/resources/guild-role-resource.ts delete mode 100644 packages/cache/src/resources/guild-sticker-resource.ts delete mode 100644 packages/cache/src/resources/guild-voice-resource.ts delete mode 100644 packages/cache/src/resources/index.ts delete mode 100644 packages/cache/src/resources/presence-resource.ts delete mode 100644 packages/cache/src/resources/user-resource.ts delete mode 100644 packages/cache/src/scheme/adapters/cache-adapter.ts delete mode 100644 packages/cache/src/scheme/adapters/memory-cache-adapter.ts delete mode 100644 packages/cache/src/scheme/adapters/redis-cache-adapter.ts delete mode 100644 packages/cache/src/scheme/transporters/base-transporter.ts delete mode 100644 packages/cache/src/scheme/transporters/redis-transporter.ts delete mode 100644 packages/cache/src/scheme/transporters/tcp-transporter.ts delete mode 100644 packages/cache/src/types.ts delete mode 100644 packages/cache/src/utils/options.ts delete mode 100644 packages/cache/tsconfig.json delete mode 100644 packages/cache/tsup.config.ts create mode 100644 packages/common/README.MD create mode 100644 packages/common/package.json create mode 100644 packages/common/src/Collection.ts create mode 100644 packages/common/src/Constants.ts create mode 100644 packages/common/src/Logger.ts create mode 100644 packages/common/src/Types.ts create mode 100644 packages/common/src/Util.ts create mode 100644 packages/common/src/applyToClass.ts create mode 100644 packages/common/src/index.ts rename packages/{api-types => common}/tsconfig.json (100%) rename packages/{api-types => common}/tsup.config.ts (92%) delete mode 100644 packages/core/src/adapters/default-event-adapter.ts delete mode 100644 packages/core/src/adapters/event-adapter.ts delete mode 100644 packages/core/src/adapters/events.ts delete mode 100644 packages/core/src/biscuit.ts create mode 100644 packages/core/src/events/handler.ts create mode 100644 packages/core/src/managers/ApplicationManager.ts create mode 100644 packages/core/src/managers/ChannelManager.ts create mode 100644 packages/core/src/managers/GuildManager.ts create mode 100644 packages/core/src/managers/InteractionManager.ts create mode 100644 packages/core/src/managers/MainManager.ts create mode 100644 packages/core/src/managers/MemberManager.ts create mode 100644 packages/core/src/managers/UserManager.ts create mode 100644 packages/core/src/managers/WebhookManager.ts create mode 100644 packages/core/src/session.ts delete mode 100644 packages/core/src/snowflakes.ts delete mode 100644 packages/core/src/structures.ts delete mode 100644 packages/core/src/structures/application.ts delete mode 100644 packages/core/src/structures/attachment.ts delete mode 100644 packages/core/src/structures/automod.ts delete mode 100644 packages/core/src/structures/base.ts delete mode 100644 packages/core/src/structures/channels.ts delete mode 100644 packages/core/src/structures/components.ts delete mode 100644 packages/core/src/structures/embed.ts delete mode 100644 packages/core/src/structures/emojis.ts delete mode 100644 packages/core/src/structures/guilds.ts delete mode 100644 packages/core/src/structures/integration.ts delete mode 100644 packages/core/src/structures/interactions.ts delete mode 100644 packages/core/src/structures/invite.ts delete mode 100644 packages/core/src/structures/members.ts delete mode 100644 packages/core/src/structures/message-reaction.ts delete mode 100644 packages/core/src/structures/message.ts delete mode 100644 packages/core/src/structures/presence.ts delete mode 100644 packages/core/src/structures/role.ts delete mode 100644 packages/core/src/structures/scheduled-events.ts delete mode 100644 packages/core/src/structures/special/interaction-options.ts delete mode 100644 packages/core/src/structures/special/permissions.ts delete mode 100644 packages/core/src/structures/stage-instance.ts delete mode 100644 packages/core/src/structures/sticker.ts delete mode 100644 packages/core/src/structures/user.ts delete mode 100644 packages/core/src/structures/webhook.ts delete mode 100644 packages/core/src/structures/welcome.ts delete mode 100644 packages/core/src/structures/widget.ts delete mode 100644 packages/core/src/utils/calculate-shard.ts delete mode 100644 packages/core/src/utils/url-to-base-64.ts delete mode 100644 packages/core/src/utils/util.ts delete mode 100644 packages/helpers/CHANGELOG.md create mode 100644 packages/helpers/src/MessageEmbed.ts create mode 100644 packages/helpers/src/Utils.ts delete mode 100644 packages/helpers/src/builders/components/InputTextBuilder.ts delete mode 100644 packages/helpers/src/builders/components/MessageActionRowBuilder.ts delete mode 100644 packages/helpers/src/builders/components/MessageButtonBuilder.ts delete mode 100644 packages/helpers/src/builders/components/MessageSelectMenuBuilder.ts delete mode 100644 packages/helpers/src/builders/embeds/embed-builder.ts delete mode 100644 packages/helpers/src/builders/slash/ApplicationCommand.ts delete mode 100644 packages/helpers/src/builders/slash/ApplicationCommandOption.ts delete mode 100644 packages/helpers/src/collectors.ts create mode 100644 packages/helpers/src/commands/contextMenu/ContextCommand.ts create mode 100644 packages/helpers/src/commands/index.ts create mode 100644 packages/helpers/src/commands/slash/SlashCommand.ts create mode 100644 packages/helpers/src/commands/slash/SlashCommandOption.ts create mode 100644 packages/helpers/src/components/ActionRow.ts create mode 100644 packages/helpers/src/components/BaseComponent.ts create mode 100644 packages/helpers/src/components/MessageButton.ts create mode 100644 packages/helpers/src/components/SelectMenu.ts create mode 100644 packages/helpers/src/components/TextInput.ts create mode 100644 packages/helpers/src/components/index.ts create mode 100644 packages/rest/src/CDN.ts create mode 100644 packages/rest/src/REST.ts create mode 100644 packages/rest/src/Router.ts create mode 100644 packages/rest/src/Routes/applications.ts create mode 100644 packages/rest/src/Routes/channels.ts create mode 100644 packages/rest/src/Routes/gateway.ts create mode 100644 packages/rest/src/Routes/guilds.ts create mode 100644 packages/rest/src/Routes/index.ts create mode 100644 packages/rest/src/Routes/interactions.ts create mode 100644 packages/rest/src/Routes/invites.ts create mode 100644 packages/rest/src/Routes/stage-instances.ts create mode 100644 packages/rest/src/Routes/stickers.ts create mode 100644 packages/rest/src/Routes/users.ts create mode 100644 packages/rest/src/Routes/voice.ts create mode 100644 packages/rest/src/Routes/webhooks.ts delete mode 100644 packages/rest/src/adapters/default-rest-adapter.ts delete mode 100644 packages/rest/src/adapters/rest-adapter.ts create mode 100644 packages/ws/src/SharedTypes.ts create mode 100644 packages/ws/src/defaults.ts create mode 100644 packages/ws/src/manager/GatewayManager.ts create mode 100644 packages/ws/src/manager/GatewayManagerTypes.ts delete mode 100644 packages/ws/src/services/shard-manager.ts delete mode 100644 packages/ws/src/services/shard.ts create mode 100644 packages/ws/src/shard/Shard.ts create mode 100644 packages/ws/src/shard/ShardTypes.ts delete mode 100644 packages/ws/src/types.ts create mode 100644 packages/ws/src/utils/Bucket.ts delete mode 100644 packages/ws/src/utils/bucket.ts delete mode 100644 packages/ws/src/utils/options.ts create mode 100644 packages/ws/tsconfig.test.json create mode 100644 rome.json diff --git a/.editorconfig b/.editorconfig index a457722..916e2e5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,9 +5,9 @@ charset = utf-8 end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true -indent_style = tab -indent_size = 4 +indent_style = space +indent_size = 2 quote_type = single [Makefile] -indent_style = tab \ No newline at end of file +indent_style = space diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f81bd34..0000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -build -dist -examples/** -tsup.config.ts diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 9e5c366..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,224 +0,0 @@ -root: true -env: - browser: true - es2020: true - node: true - -extends: - - 'prettier' - - 'eslint:recommended' - - 'plugin:@typescript-eslint/eslint-recommended' - - 'plugin:@typescript-eslint/recommended' - -ignorePatterns: - - 'node_modules' - - 'dist' - - 'coverage' - - '**/*.js' - - '**/*.d.ts' - - '__tests__' - - '__test__' - -parser: '@typescript-eslint/parser' - -parserOptions: - project: './packages/**/tsconfig.json' - sourceType: 'module' - -plugins: - - '@typescript-eslint' - -# silly eslint bug -overrides: - - files: ['*.ts'] - rules: - no-undef: 'off' - -rules: - '@typescript-eslint/consistent-type-imports': 'error' - '@typescript-eslint/no-duplicate-imports': 'error' - '@typescript-eslint/prefer-optional-chain': 'error' - '@typescript-eslint/no-explicit-any': 'off' - '@typescript-eslint/explicit-function-return-type': 'off' - '@typescript-eslint/no-non-null-assertion': 'off' - '@typescript-eslint/ban-ts-comment': 'off' - '@typescript-eslint/no-unused-vars': 'off' - '@typescript-eslint/naming-convention': - - 'error' - - selector: 'default' - format: null - - selector: 'variable' - format: - - 'camelCase' - - 'PascalCase' - - 'UPPER_CASE' - - selector: 'typeLike' - format: ['PascalCase'] - - '@typescript-eslint/explicit-module-boundary-types': 'off' - '@typescript-eslint/no-empty-interface': 'off' - '@typescript-eslint/adjacent-overload-signatures': 'error' - '@typescript-eslint/consistent-type-assertions': 'error' - '@typescript-eslint/no-array-constructor': 'error' - '@typescript-eslint/no-empty-function': 'error' - '@typescript-eslint/no-inferrable-types': 'error' - '@typescript-eslint/no-misused-new': 'error' - '@typescript-eslint/no-namespace': 'error' - '@typescript-eslint/no-this-alias': 'error' - '@typescript-eslint/no-use-before-define': 'off' - '@typescript-eslint/no-var-requires': 'error' - '@typescript-eslint/triple-slash-reference': 'error' - '@typescript-eslint/type-annotation-spacing': 'error' - '@typescript-eslint/array-type': 'error' - '@typescript-eslint/no-unnecessary-qualifier': 'error' - '@typescript-eslint/no-unnecessary-type-arguments': 'off' # disabled as it started to be buggy - '@typescript-eslint/quotes': - - 'error' - - 'single' - - avoidEscape: true - allowTemplateLiterals: true - - '@typescript-eslint/semi': - - 'error' - - 'always' - - '@typescript-eslint/no-useless-constructor': 'error' - '@typescript-eslint/no-redeclare': - - 'error' - - '@typescript-eslint/member-delimiter-style': - - 'error' - - multiline: - delimiter: 'semi' - requireLast: true - singleline: - delimiter: 'semi' - requireLast: false - - '@typescript-eslint/space-before-function-paren': - - 'error' - - anonymous: 'always' - named: 'never' - asyncArrow: 'always' - - 'arrow-parens': - - 'error' - - 'as-needed' - - 'no-var': 'error' - 'prefer-const': 'error' - 'prefer-rest-params': 'error' - 'prefer-spread': 'error' - 'constructor-super': 'error' - 'for-direction': 'error' - 'getter-return': 'error' - 'no-async-promise-executor': 'error' - 'no-case-declarations': 'error' - 'no-class-assign': 'error' - 'no-compare-neg-zero': 'error' - 'no-cond-assign': 'error' - 'no-const-assign': 'error' - 'no-constant-condition': 'error' - 'no-control-regex': 'error' - 'no-debugger': 'error' - 'no-delete-var': 'error' - 'no-dupe-args': 'error' - 'no-dupe-keys': 'error' - 'no-duplicate-case': 'error' - 'no-empty': 'error' - 'no-empty-character-class': 'error' - 'no-empty-pattern': 'error' - 'no-ex-assign': 'error' - 'no-extra-boolean-cast': 'error' - 'no-extra-semi': 'error' - 'no-fallthrough': 'error' - 'no-func-assign': 'error' - 'no-global-assign': 'error' - 'no-inner-declarations': 'error' - 'no-invalid-regexp': 'error' - 'no-irregular-whitespace': 'error' - 'no-misleading-character-class': 'error' - 'no-mixed-spaces-and-tabs': 'error' - 'no-new-symbol': 'error' - 'no-obj-calls': 'error' - 'no-octal': 'error' - 'no-prototype-builtins': 'error' - 'no-redeclare': 'off' - 'no-regex-spaces': 'error' - 'no-self-assign': 'error' - 'no-shadow-restricted-names': 'error' - 'no-sparse-arrays': 'error' - 'no-this-before-super': 'error' - 'no-undef': 'error' - 'no-unexpected-multiline': 'error' - 'no-unreachable': 'error' - 'no-unsafe-finally': 'error' - 'no-unsafe-negation': 'error' - 'no-unused-labels': 'error' - 'no-useless-catch': 'error' - 'no-useless-escape': 'error' - 'no-with': 'error' - 'require-yield': 'error' - 'use-isnan': 'error' - 'valid-typeof': 'error' - # 'comma-dangle': ['error', 'never'] - 'dot-notation': 'error' - 'eol-last': 'error' - eqeqeq: - - 'error' - - 'always' - - 'null': 'ignore' - - 'no-console': 'warn' - 'no-duplicate-imports': 'off' - 'no-multiple-empty-lines': 'error' - 'no-throw-literal': 'error' - 'no-trailing-spaces': 'error' - 'no-undef-init': 'error' - 'object-shorthand': 'error' - 'quote-props': - - 'error' - - 'consistent-as-needed' - - 'spaced-comment': 'error' - yoda: 'error' - curly: 'error' - 'object-curly-spacing': - - 'error' - - 'always' - - 'lines-between-class-members': - - 'error' - - 'always' - - exceptAfterSingleLine: true - - 'no-else-return': 'error' - - # always - 'padded-blocks': - - 'error' - - classes: 'never' - - 'block-spacing': - - 'error' - - 'always' - - 'space-before-blocks': - - 'error' - - 'always' - - 'brace-style': - - 'error' - - '1tbs' - - allowSingleLine: true - - 'keyword-spacing': - - 'error' - - before: true - after: true - - 'space-in-parens': - - 'error' - - 'never' - -# settings: {} diff --git a/.prettierrc.yml b/.prettierrc.yml deleted file mode 100644 index 087aee0..0000000 --- a/.prettierrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -arrowParens: 'avoid' -singleQuote: true diff --git a/package.json b/package.json index e4d661a..aa1138a 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,21 @@ "scripts": { "build": "turbo run build", "clean": "turbo run clean", - "lint": "turbo run lint", + "check": "rome check ./packages/", + "check:apply": "rome check ./packages/ --apply", + "lint": "rome format ./packages/ --write --quote-style single --trailing-comma none", "dev": "turbo run dev --parallel" }, "engines": { "npm": ">=7.0.0", - "node": ">=14.0.0" + "node": ">=16.0.0" }, "devDependencies": { + "@changesets/cli": "^2.24.1", "@types/node": "^18.6.3", - "@typescript-eslint/eslint-plugin": "^5.32.0", - "@typescript-eslint/parser": "^5.32.0", - "eslint": "^8.21.0", - "eslint-config-prettier": "^8.5.0", + "rome": "^12.0.0", "turbo": "^1.4.2", - "typescript": "^4.7.4", - "@changesets/cli": "^2.24.1" + "typescript": "^5.0.4" }, "packageManager": "npm@8.14.0", "bugs": { @@ -57,6 +56,10 @@ { "name": "Drylozu", "url": "https://github.com/Drylozu" + }, + { + "name": "FreeAoi", + "url": "https://github.com/FreeAoi" } ], "homepage": "https://biscuitjs.com", diff --git a/packages/api-types/CHANGELOG.md b/packages/api-types/CHANGELOG.md deleted file mode 100644 index efc059d..0000000 --- a/packages/api-types/CHANGELOG.md +++ /dev/null @@ -1,62 +0,0 @@ -# @biscuitland/api-types - -## 2.3.0 - -### Minor Changes - -- fix TODO - -## 2.2.3 - -### Patch Changes - -- bug fixes - -## 2.2.2 - -### Patch Changes - -- rename guildLocales to guildLocale in interactions - -## 2.2.1 - -### Patch Changes - -- select menu options now can be empty since the latest Discord API update - -## 2.2.0 - -### Minor Changes - -- Functionality to delete ephemeral messages added, select menus were updated - -## 2.1.2 - -### Patch Changes - -- minor changes - -## 2.1.1 - -### Patch Changes - -- dumb hotfix that LH asked for (blame Yuzu) - -## 2.1.0 - -### Minor Changes - -- Changes to cache and forum channels ✨ -- Forum channels and updates to @biscuitland/cache ✨ - -## 2.0.6 - -### Patch Changes - -- Minor fixes - -## 2.0.5 - -### Major Changes - -- publish diff --git a/packages/api-types/README.md b/packages/api-types/README.md deleted file mode 100644 index 3a32a09..0000000 --- a/packages/api-types/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @biscuitland/api-types - -## Most importantly, api-types is: - -1:1 type definitions package for the [Discord](https://discord.com/developers/docs/intro) API. - -[](https://github.com/oasisjs/biscuit) -[](https://discord.gg/XNw2RZFzaP) - -biscuit - -## Install (for [node18](https://nodejs.org/en/download/)) - -```sh-session -npm install @biscuitland/api-types -yarn add @biscuitland/api-types -``` - -## Example - -```ts -import type { DiscordUser } from '@biscuitland/api-types'; -``` - -## Example for [Deno](https://deno.land/) - -```ts -import type { DiscordUser } from "https://unpkg.com/@biscuitland/api-types@2.0.5/dist/index.d.ts"; -``` - -We deliver this package through [unpkg](https://unpkg.com/) and it does contain constants and routes too - -## Links - -- [Website](https://biscuitjs.com/) -- [Documentation](https://docs.biscuitjs.com/) -- [Discord](https://discord.gg/XNw2RZFzaP) -- [core](https://www.npmjs.com/package/@biscuitland/core) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) diff --git a/packages/api-types/package.json b/packages/api-types/package.json deleted file mode 100644 index 42bf770..0000000 --- a/packages/api-types/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "@biscuitland/api-types", - "version": "2.3.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": [ - "dist/**" - ], - "scripts": { - "build": "tsup", - "clean": "rm -rf dist && rm -rf .turbo", - "dev": "tsup --watch" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "require": "./dist/index.js" - } - }, - "devDependencies": { - "tsup": "^6.1.3" - }, - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - }, - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "publishConfig": { - "access": "public" - } -} diff --git a/packages/api-types/src/common.ts b/packages/api-types/src/common.ts deleted file mode 100644 index 3a1d836..0000000 --- a/packages/api-types/src/common.ts +++ /dev/null @@ -1,1295 +0,0 @@ -/** https://discord.com/developers/docs/resources/user#user-object-premium-types */ -export enum PremiumTypes { - None, - NitroClassic, - Nitro, -} - -/** https://discord.com/developers/docs/resources/user#user-object-user-flags */ -export enum UserFlags { - DiscordEmployee = 1 << 0, - PartneredServerOwner = 1 << 1, - HypeSquadEventsMember = 1 << 2, - BugHunterLevel1 = 1 << 3, - HouseBravery = 1 << 6, - HouseBrilliance = 1 << 7, - HouseBalance = 1 << 8, - EarlySupporter = 1 << 9, - TeamUser = 1 << 10, - BugHunterLevel2 = 1 << 14, - VerifiedBot = 1 << 16, - EarlyVerifiedBotDeveloper = 1 << 17, - DiscordCertifiedModerator = 1 << 18, - BotHttpInteractions = 1 << 19, -} - -/** https://discord.com/developers/docs/resources/channel#channels-resource */ -export enum ChannelFlags { - None, - Pinned = 1 << 1, -} - -/** https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors */ -export enum IntegrationExpireBehaviors { - RemoveRole, - Kick, -} - -/** https://discord.com/developers/docs/resources/user#connection-object-visibility-types */ -export enum VisibilityTypes { - /** Invisible to everyone except the user themselves */ - None, - /** Visible to everyone */ - Everyone, -} - -/** https://discord.com/developers/docs/topics/teams#data-models-membership-state-enum */ -export enum TeamMembershipStates { - Invited = 1, - Accepted, -} - -/** https://discord.com/developers/docs/topics/oauth2#application-application-flags */ -export enum ApplicationFlags { - /** Intent required for bots in **100 or more servers** to receive [`presence_update` events](#DOCS_TOPICS_GATEWAY/presence-update) */ - GatewayPresence = 1 << 12, - /** Intent required for bots in under 100 servers to receive [`presence_update` events](#DOCS_TOPICS_GATEWAY/presence-update), found in Bot Settings */ - GatewayPresenceLimited = 1 << 13, - /** Intent required for bots in **100 or more servers** to receive member-related events like `guild_member_add`. See list of member-related events [under `GUILD_MEMBERS`](#DOCS_TOPICS_GATEWAY/list-of-intents) */ - GatewayGuildMembers = 1 << 14, - /** Intent required for bots in under 100 servers to receive member-related events like `guild_member_add`, found in Bot Settings. See list of member-related events [under `GUILD_MEMBERS`](#DOCS_TOPICS_GATEWAY/list-of-intents) */ - GatewayGuildMembersLimited = 1 << 15, - /** Indicates unusual growth of an app that prevents verification */ - VerificationPendingGuildLimit = 1 << 16, - /** Indicates if an app is embedded within the Discord client (currently unavailable publicly) */ - Embedded = 1 << 17, - /** Intent required for bots in **100 or more servers** to receive [message content](https://support-dev.discord.com/hc/en-us/articles/4404772028055) */ - GatewayMessageCount = 1 << 18, - /** Intent required for bots in under 100 servers to receive [message content](https://support-dev.discord.com/hc/en-us/articles/4404772028055), found in Bot Settings */ - GatewayMessageContentLimited = 1 << 19, -} - -/** https://discord.com/developers/docs/interactions/message-components#component-types */ -export enum MessageComponentTypes { - /** A container for other components */ - ActionRow = 1, - /** A button object */ - Button = 2, - /** A select menu for picking from choices */ - SelectMenu = 3, - /** A text input object */ - InputText = 4, - /** A select menu for picking from users */ - UserSelect = 5, - /** A select menu for picking from roles */ - RoleSelect = 6, - /** A select menu for picking from users and roles */ - MentionableSelect = 7, - /** A select menu for picking from channels */ - ChannelSelect = 8 -} - -export enum TextStyles { - /** Intended for short single-line text */ - Short = 1, - /** Intended for much longer inputs */ - Paragraph = 2, -} - -/** https://discord.com/developers/docs/interactions/message-components#buttons-button-styles */ -export enum ButtonStyles { - /** A blurple button */ - Primary = 1, - /** A grey button */ - Secondary, - /** A green button */ - Success, - /** A red button */ - Danger, - /** A button that navigates to a URL */ - Link, -} - -/** https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types */ -export enum AllowedMentionsTypes { - /** Controls role mentions */ - RoleMentions = 'roles', - /** Controls user mentions */ - UserMentions = 'users', - /** Controls @everyone and @here mentions */ - EveryoneMentions = 'everyone', -} - -/** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types */ -export enum WebhookTypes { - /** Incoming Webhooks can post messages to channels with a generated token */ - Incoming = 1, - /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ - ChannelFollower, - /** Application webhooks are webhooks used with Interactions */ - Application, -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-types */ -export type EmbedTypes = - | 'rich' - | 'image' - | 'video' - | 'gifv' - | 'article' - | 'link'; - -/** https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level */ -export enum DefaultMessageNotificationLevels { - /** Members will receive notifications for all messages by default */ - AllMessages, - /** Members will receive notifications only for messages that @mention them by default */ - OnlyMentions, -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level */ -export enum ExplicitContentFilterLevels { - /** Media content will not be scanned */ - Disabled, - /** Media content sent by members without roles will be scanned */ - MembersWithoutRoles, - /** Media content sent by all members will be scanned */ - AllMembers, -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-verification-level */ -export enum VerificationLevels { - /** Unrestricted */ - None, - /** Must have verified email on account */ - Low, - /** Must be registered on Discord for longer than 5 minutes */ - Medium, - /** Must be a member of the server for longer than 10 minutes */ - High, - /** Must have a verified phone number */ - VeryHigh, -} - -/** https://discord.com/developers/docs/topics/permissions#role-object-role-structure */ -export interface BaseRole { - /** Role name */ - name: string; - /** Integer representation of hexadecimal color code */ - color: number; - /** Position of this role */ - position: number; - /** role unicode emoji */ - unicodeEmoji?: string; -} - -/** https://discord.com/developers/docs/resources/channel#forum-tag-object */ -export interface DiscordForumTag { - /** the id of the tag */ - id: Snowflake; - /** the name of the tag (0-20 characters) */ - name: string; - /** whether this tag can only be added to or removed from threads by a member with the MANAGE_THREADS permission */ - moderated: boolean; - /** the id of a guild's custom emoji * */ - emoji_id: Snowflake | null; - /** he unicode character of the emoji */ - emoji_name: string | null; -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-guild-features */ -export enum GuildFeatures { - /** Guild has access to set an invite splash background */ - InviteSplash = 'INVITE_SPLASH', - /** Guild has access to set 384 kbps bitrate in voice (previously VIP voice servers) */ - VipRegions = 'VIP_REGIONS', - /** Guild has access to set a vanity URL */ - VanityUrl = 'VANITY_URL', - /** Guild is verified */ - Verified = 'VERIFIED', - /** Guild is partnered */ - Partnered = 'PARTNERED', - /** Guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates */ - Community = 'COMMUNITY', - /** Guild has access to use commerce features (i.e. create store channels) */ - Commerce = 'COMMERCE', - /** Guild has access to create news channels */ - News = 'NEWS', - /** Guild is able to be discovered in the directory */ - Discoverable = 'DISCOVERABLE', - /** guild cannot be discoverable */ - DiscoverableDisabled = 'DISCOVERABLE_DISABLED', - /** Guild is able to be featured in the directory */ - Feature = 'FEATURABLE', - /** Guild has access to set an animated guild icon */ - AnimatedIcon = 'ANIMATED_ICON', - /** Guild has access to set a guild banner image */ - Banner = 'BANNER', - /** Guild has enabled the welcome screen */ - WelcomeScreenEnabled = 'WELCOME_SCREEN_ENABLED', - /** Guild has enabled [Membership Screening](https://discord.com/developers/docs/resources/guild#membership-screening-object) */ - MemberVerificationGateEnabled = 'MEMBER_VERIFICATION_GATE_ENABLED', - /** Guild can be previewed before joining via Membership Screening or the directory */ - PreviewEnabled = 'PREVIEW_ENABLED', - /** Guild has enabled ticketed events */ - TicketedEventsEnabled = 'TICKETED_EVENTS_ENABLED', - /** Guild has enabled monetization */ - MonetizationEnabled = 'MONETIZATION_ENABLED', - /** Guild has increased custom sticker slots */ - MoreStickers = 'MORE_STICKERS', - /** Guild has access to create private threads */ - PrivateThreads = 'PRIVATE_THREADS', - /** Guild is able to set role icons */ - RoleIcons = 'ROLE_ICONS', - /** Guild has set up auto moderation rules */ - AutoModeration = 'AUTO_MODERATION', -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-mfa-level */ -export enum MfaLevels { - /** Guild has no MFA/2FA requirement for moderation actions */ - None, - /** Guild has a 2FA requirement for moderation actions */ - Elevated, -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags */ -export enum SystemChannelFlags { - /** Suppress member join notifications */ - SuppressJoinNotifications = 1 << 0, - /** Suppress server boost notifications */ - SuppressPremiumSubscriptions = 1 << 1, - /** Suppress server setup tips */ - SuppressGuildReminderNotifications = 1 << 2, - /** Hide member join sticker reply buttons */ - SuppressJoinNotificationReplies = 1 << 3, -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-premium-tier */ -export enum PremiumTiers { - /** Guild has not unlocked any Server Boost perks */ - None, - /** Guild has unlocked Server Boost level 1 perks */ - Tier1, - /** Guild has unlocked Server Boost level 2 perks */ - Tier2, - /** Guild has unlocked Server Boost level 3 perks */ - Tier3, -} - -/** https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level */ -export enum GuildNsfwLevel { - Default, - Explicit, - Safe, - AgeRestricted, -} - -/** https://discord.com/developers/docs/resources/channel#channel-object-channel-types */ -export enum ChannelTypes { - /** A text channel within a server */ - GuildText, - /** A direct message between users */ - DM, - /** A voice channel within a server */ - GuildVoice, - /** A direct message between multiple users */ - GroupDm, - /** An organizational category that contains up to 50 channels */ - GuildCategory, - /** A channel that users can follow and crosspost into their own server */ - GuildNews, - /** A temporary sub-channel within a GUILD_NEWS channel */ - GuildNewsThread = 10, - /** A temporary sub-channel within a GUILD_TEXT channel */ - GuildPublicThread, - /** A temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission */ - GuildPrivateThread, - /** A voice channel for hosting events with an audience */ - GuildStageVoice, - /** A channel in a hub containing the listed servers */ - GuildDirectory, - /** A channel which can only contains threads */ - GuildForum, -} - -export enum OverwriteTypes { - Role, - Member, -} - -export enum VideoQualityModes { - /** Discord chooses the quality for optimal performance */ - Auto = 1, - /** 720p */ - Full, -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-types */ -export enum ActivityTypes { - Game, - Streaming, - Listening, - Watching, - Custom = 4, - Competing, -} - -/** https://discord.com/developers/docs/resources/channel#message-object-message-types */ -export enum MessageTypes { - Default, - RecipientAdd, - RecipientRemove, - Call, - ChannelNameChange, - ChannelIconChange, - ChannelPinnedMessage, - GuildMemberJoin, - UserPremiumGuildSubscription, - UserPremiumGuildSubscriptionTier1, - UserPremiumGuildSubscriptionTier2, - UserPremiumGuildSubscriptionTier3, - ChannelFollowAdd, - GuildDiscoveryDisqualified = 14, - GuildDiscoveryRequalified, - GuildDiscoveryGracePeriodInitialWarning, - GuildDiscoveryGracePeriodFinalWarning, - ThreadCreated, - Reply, - ChatInputCommand, - ThreadStarterMessage, - GuildInviteReminder, - ContextMenuCommand, - AutoModerationAction, -} - -/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-types */ -export enum MessageActivityTypes { - Join = 1, - Spectate, - Listen, - JoinRequest, -} - -/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types */ -export enum StickerTypes { - /** an official sticker in a pack, part of Nitro or in a removed purchasable pack */ - Standard = 1, - /** a sticker uploaded to a Boosted guild for the guild's members */ - Guild, -} - -/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types */ -export enum StickerFormatTypes { - Png = 1, - APng, - Lottie, -} - -/** https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype */ -export enum InteractionTypes { - Ping = 1, - ApplicationCommand = 2, - MessageComponent = 3, - ApplicationCommandAutocomplete = 4, - ModalSubmit = 5, -} - -/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype */ -export enum ApplicationCommandOptionTypes { - SubCommand = 1, - SubCommandGroup, - String, - Integer, - Boolean, - User, - Channel, - Role, - Mentionable, - Number, - Attachment, -} - -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events */ -export enum AuditLogEvents { - /** Server settings were updated */ - GuildUpdate = 1, - /** Channel was created */ - ChannelCreate = 10, - /** Channel settings were updated */ - ChannelUpdate, - /** Channel was deleted */ - ChannelDelete, - /** Permission overwrite was added to a channel */ - ChannelOverwriteCreate, - /** Permission overwrite was updated for a channel */ - ChannelOverwriteUpdate, - /** Permission overwrite was deleted from a channel */ - ChannelOverwriteDelete, - /** Member was removed from server */ - MemberKick = 20, - /** Members were pruned from server */ - MemberPrune, - /** Member was banned from server */ - MemberBanAdd, - /** Server ban was lifted for a member */ - MemberBanRemove, - /** Member was updated in server */ - MemberUpdate, - /** Member was added or removed from a role */ - MemberRoleUpdate, - /** Member was moved to a different voice channel */ - MemberMove, - /** Member was disconnected from a voice channel */ - MemberDisconnect, - /** Bot user was added to server */ - BotAdd, - /** Role was created */ - RoleCreate = 30, - /** Role was edited */ - RoleUpdate, - /** Role was deleted */ - RoleDelete, - /** Server invite was created */ - InviteCreate = 40, - /** Server invite was updated */ - InviteUpdate, - /** Server invite was deleted */ - InviteDelete, - /** Webhook was created */ - WebhookCreate = 50, - /** Webhook properties or channel were updated */ - WebhookUpdate, - /** Webhook was deleted */ - WebhookDelete, - /** Emoji was created */ - EmojiCreate = 60, - /** Emoji name was updated */ - EmojiUpdate, - /** Emoji was deleted */ - EmojiDelete, - /** Single message was deleted */ - MessageDelete = 72, - /** Multiple messages were deleted */ - MessageBulkDelete, - /** Messaged was pinned to a channel */ - MessagePin, - /** Message was unpinned from a channel */ - MessageUnpin, - /** App was added to server */ - IntegrationCreate = 80, - /** App was updated (as an example, its scopes were updated) */ - IntegrationUpdate, - /** App was removed from server */ - IntegrationDelete, - /** Stage instance was created (stage channel becomes live) */ - StageInstanceCreate, - /** Stage instace details were updated */ - StageInstanceUpdate, - /** Stage instance was deleted (stage channel no longer live) */ - StageInstanceDelete, - /** Sticker was created */ - StickerCreate = 90, - /** Sticker details were updated */ - StickerUpdate, - /** Sticker was deleted */ - StickerDelete, - /** Event was created */ - GuildScheduledEventCreate = 100, - /** Event was updated */ - GuildScheduledEventUpdate, - /** Event was cancelled */ - GuildScheduledEventDelete, - /** Thread was created in a channel */ - ThreadCreate = 110, - /** Thread was updated */ - ThreadUpdate, - /** Thread was deleted */ - ThreadDelete, - /** Permissions were updated for a command */ - ApplicationCommandPermissionUpdate = 121, - /** Auto moderation rule was created */ - AutoModerationRuleCreate = 140, - /** Auto moderation rule was updated */ - AutoModerationRuleUpdate, - /** Auto moderation rule was deleted */ - AutoModerationRuleDelete, - /** Message was blocked by AutoMod according to a rule. */ - AutoModerationBlockMessage, -} - -export enum ScheduledEventPrivacyLevel { - /** the scheduled event is public and available in discovery. DISCORD DEVS DISABLED THIS! WILL ERROR IF USED! */ - // Public = 1, - /** the scheduled event is only accessible to guild members */ - GuildOnly = 2, -} - -export enum ScheduledEventEntityType { - StageInstance = 1, - Voice, - External, -} - -export enum ScheduledEventStatus { - Scheduled = 1, - Active, - Completed, - Canceled, -} - -/** https://discord.com/developers/docs/resources/invite#invite-object-target-user-types */ -export enum TargetTypes { - Stream = 1, - EmbeddedApplication, -} - -export enum ApplicationCommandTypes { - /** A text-based command that shows up when a user types `/` */ - ChatInput = 1, - /** A UI-based command that shows up when you right click or tap on a user */ - User, - /** A UI-based command that shows up when you right click or tap on a message */ - Message, -} - -export enum ApplicationCommandPermissionTypes { - Role = 1, - User, - Channel, -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-flags */ -export enum ActivityFlags { - Instance = 1 << 0, - Join = 1 << 1, - Spectate = 1 << 2, - JoinRequest = 1 << 3, - Sync = 1 << 4, - Play = 1 << 5, - PartyPrivacyFriends = 1 << 6, - PartyPrivacyVoiceChannel = 1 << 7, - Embedded = 1 << 8, -} - -/** https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags */ -export enum BitwisePermissionFlags { - /** Allows creation of instant invites */ - CREATE_INSTANT_INVITE = 0x0000000000000001, - /** Allows kicking members */ - KICK_MEMBERS = 0x0000000000000002, - /** Allows banning members */ - BAN_MEMBERS = 0x0000000000000004, - /** Allows all permissions and bypasses channel permission overwrites */ - ADMINISTRATOR = 0x0000000000000008, - /** Allows management and editing of channels */ - MANAGE_CHANNELS = 0x0000000000000010, - /** Allows management and editing of the guild */ - MANAGE_GUILD = 0x0000000000000020, - /** Allows for the addition of reactions to messages */ - ADD_REACTIONS = 0x0000000000000040, - /** Allows for viewing of audit logs */ - VIEW_AUDIT_LOG = 0x0000000000000080, - /** Allows for using priority speaker in a voice channel */ - PRIORITY_SPEAKER = 0x0000000000000100, - /** Allows the user to go live */ - STREAM = 0x0000000000000200, - /** Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels */ - VIEW_CHANNEL = 0x0000000000000400, - /** Allows for sending messages in a channel. (does not allow sending messages in threads) */ - SEND_MESSAGES = 0x0000000000000800, - /** Allows for sending of /tts messages */ - SEND_TTS_MESSAGES = 0x0000000000001000, - /** Allows for deletion of other users messages */ - MANAGE_MESSAGES = 0x0000000000002000, - /** Links sent by users with this permission will be auto-embedded */ - EMBED_LINKS = 0x0000000000004000, - /** Allows for uploading images and files */ - ATTACH_FILES = 0x0000000000008000, - /** Allows for reading of message history */ - READ_MESSAGE_HISTORY = 0x0000000000010000, - /** Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel */ - MENTION_EVERYONE = 0x0000000000020000, - /** Allows the usage of custom emojis from other servers */ - USE_EXTERNAL_EMOJIS = 0x0000000000040000, - /** Allows for viewing guild insights */ - VIEW_GUILD_INSIGHTS = 0x0000000000080000, - /** Allows for joining of a voice channel */ - CONNECT = 0x0000000000100000, - /** Allows for speaking in a voice channel */ - SPEAK = 0x0000000000200000, - /** Allows for muting members in a voice channel */ - MUTE_MEMBERS = 0x0000000000400000, - /** Allows for deafening of members in a voice channel */ - DEAFEN_MEMBERS = 0x0000000000800000, - /** Allows for moving of members between voice channels */ - MOVE_MEMBERS = 0x0000000001000000, - /** Allows for using voice-activity-detection in a voice channel */ - USE_VAD = 0x0000000002000000, - /** Allows for modification of own nickname */ - CHANGE_NICKNAME = 0x0000000004000000, - /** Allows for modification of other users nicknames */ - MANAGE_NICKNAMES = 0x0000000008000000, - /** Allows management and editing of roles */ - MANAGE_ROLES = 0x0000000010000000, - /** Allows management and editing of webhooks */ - MANAGE_WEBHOOKS = 0x0000000020000000, - /** @deprecated use MANAGE_EMOJIS_AND_STICKERS instead */ - MANAGE_EMOJIS = 0x0000000040000000, - /** Allows management and editing of emojis and stickers */ - MANAGE_EMOJIS_AND_STICKERS = 0x0000000040000000, - /** Allows members to use application commands in text channels */ - USE_APPLICATION_COMMANDS = 0x0000000080000000, - /** @deprecated use USE_APPLICATION_COMMANDS instead */ - USE_SLASH_COMMANDS = 0x0000000080000000, - /** Allows for requesting to speak in stage channels. */ - REQUEST_TO_SPEAK = 0x0000000100000000, - /** Allows for creating, editing, and deleting scheduled events */ - MANAGE_EVENTS = 0x0000000200000000, - /** Allows for deleting and archiving threads, and viewing all private threads */ - MANAGE_THREADS = 0x0000000400000000, - /** Allows for creating public and announcement threads */ - CREATE_PUBLIC_THREADS = 0x0000000800000000, - /** Allows for creating private threads */ - CREATE_PRIVATE_THREADS = 0x0000001000000000, - /** Allows the usage of custom stickers from other servers */ - USE_EXTERNAL_STICKERS = 0x0000002000000000, - /** Allows for sending messages in threads */ - SEND_MESSAGES_IN_THREADS = 0x0000004000000000, - /** Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. */ - USE_EMBEDDED_ACTIVITIES = 0x0000008000000000, - /** Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels */ - MODERATE_MEMBERS = 0x0000010000000000, -} - -export type PermissionStrings = keyof typeof BitwisePermissionFlags; - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice */ -export enum VoiceOpcodes { - /** Begin a voice websocket connection. */ - Identify, - /** Select the voice protocol. */ - SelectProtocol, - /** Complete the websocket handshake. */ - Ready, - /** Keep the websocket connection alive. */ - Heartbeat, - /** Describe the session. */ - SessionDescription, - /** Indicate which users are speaking. */ - Speaking, - /** Sent to acknowledge a received client heartbeat. */ - HeartbeatACK, - /** Resume a connection. */ - Resume, - /** Time to wait between sending heartbeats in milliseconds. */ - Hello, - /** Acknowledge a successful session resume. */ - Resumed, - /** A client has disconnected from the voice channel */ - ClientDisconnect = 13, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice */ -export enum VoiceCloseEventCodes { - /** You sent an invalid [opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-opcodes). */ - UnknownOpcode = 4001, - /** You sent a invalid payload in your [identifying](https://discord.com/developers/docs/topics/gateway#identify) to the Gateway. */ - FailedToDecodePayload, - /** You sent a payload before [identifying](https://discord.com/developers/docs/topics/gateway#identify) with the Gateway. */ - NotAuthenticated, - /** The token you sent in your [identify](https://discord.com/developers/docs/topics/gateway#identify) payload is incorrect. */ - AuthenticationFailed, - /** You sent more than one [identify](https://discord.com/developers/docs/topics/gateway#identify) payload. Stahp. */ - AlreadyAuthenticated, - /** Your session is no longer valid. */ - SessionNoLongerValid, - /** Your session has timed out. */ - SessionTimedOut = 4009, - /** We can't find the server you're trying to connect to. */ - ServerNotFound = 4011, - /** We didn't recognize the [protocol](https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-example-select-protocol-payload) you sent. */ - UnknownProtocol, - /** Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect. */ - Disconnect = 4014, - /** The server crashed. Our bad! Try [resuming](https://discord.com/developers/docs/topics/voice-connections#resuming-voice-connection). */ - VoiceServerCrashed, - /** We didn't recognize your [encryption](https://discord.com/developers/docs/topics/voice-connections#encrypting-and-sending-voice). */ - UnknownEncryptionMode, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#rpc */ -export enum RpcErrorCodes { - /** An unknown error occurred. */ - UnknownError = 1000, - /** You sent an invalid payload. */ - InvalidPayload = 4000, - /** Invalid command name specified. */ - InvalidCommand = 4002, - /** Invalid guild ID specified. */ - InvalidGuild, - /** Invalid event name specified. */ - InvalidEvent, - /** Invalid channel ID specified. */ - InvalidChannel, - /** You lack permissions to access the given resource. */ - InvalidPermissions, - /** An invalid OAuth2 application ID was used to authorize or authenticate with. */ - InvalidClientId, - /** An invalid OAuth2 application origin was used to authorize or authenticate with. */ - InvalidOrigin, - /** An invalid OAuth2 token was used to authorize or authenticate with. */ - InvalidToken, - /** The specified user ID was invalid. */ - InvalidUser, - /** A standard OAuth2 error occurred; check the data object for the OAuth2 error details. */ - OAuth2Error = 5000, - /** An asynchronous `SELECT_TEXT_CHANNEL`/`SELECT_VOICE_CHANNEL` command timed out. */ - SelectChannelTimedOut, - /** An asynchronous `GET_GUILD` command timed out. */ - GetGuildTimedOut, - /** You tried to join a user to a voice channel but the user was already in one. */ - SelectVoiceForceRequired, - /** You tried to capture more than one shortcut key at once. */ - CaptureShortcutAlreadyListening, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#rpc */ -export enum RpcCloseEventCodes { - /** You connected to the RPC server with an invalid client ID. */ - InvalidClientId = 4000, - /** You connected to the RPC server with an invalid origin. */ - InvalidOrigin, - /** You are being rate limited. */ - RateLimited, - /** The OAuth2 token associated with a connection was revoked, get a new one! */ - TokenRevoked, - /** The RPC Server version specified in the connection string was not valid. */ - InvalidVersion, - /** The encoding specified in the connection string was not valid. */ - InvalidEncoding, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#http */ -export enum HTTPResponseCodes { - /** The request completed successfully. */ - Ok = 200, - /** The entity was created successfully. */ - Created, - /** The request completed successfully but returned no content. */ - NoContent = 204, - /** The entity was not modified (no action was taken). */ - NotModified = 304, - /** The request was improperly formatted, or the server couldn't understand it. */ - BadRequest = 400, - /** The `Authorization` header was missing or invalid. */ - Unauthorized, - /** The `Authorization` token you passed did not have permission to the resource. */ - Forbidden = 403, - /** The resource at the location specified doesn't exist. */ - NotFound, - /** The HTTP method used is not valid for the location specified. */ - MethodNotAllowed, - /** You are being rate limited, see [Rate Limits](https://discord.com/developers/docs/topics/rate-limits). */ - TooManyRequests = 429, - /** There was not a gateway available to process your request. Wait a bit and retry. */ - GatewayUnavailable = 502, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#opcodes-and-status-codes */ -export enum GatewayCloseEventCodes { - /** A normal closure of the gateway. - * You may attempt to reconnect. - */ - NormalClosure = 1000, - /** We're not sure what went wrong. Try reconnecting? */ - UnknownError = 4000, - /** You sent an invalid [Gateway opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes) or an invalid payload for an opcode. Don't do that! */ - UnknownOpcode, - /** You sent an invalid [payload](https://discord.com/developers/docs/topics/gateway#sending-payloads) to us. Don't do that! */ - DecodeError, - /** You sent us a payload prior to [identifying](https://discord.com/developers/docs/topics/gateway#identify). */ - NotAuthenticated, - /** The account token sent with your [identify payload](https://discord.com/developers/docs/topics/gateway#identify) is incorrect. */ - AuthenticationFailed, - /** You sent more than one identify payload. Don't do that! */ - AlreadyAuthenticated, - /** The sequence sent when [resuming](https://discord.com/developers/docs/topics/gateway#resume) the session was invalid. Reconnect and start a new session. */ - InvalidSeq = 4007, - /** Woah nelly! You're sending payloads to us too quickly. Slow it down! You will be disconnected on receiving this. */ - RateLimited, - /** Your session timed out. Reconnect and start a new one. */ - SessionTimedOut, - /** You sent us an invalid [shard when identifying](https://discord.com/developers/docs/topics/gateway#sharding). */ - InvalidShard, - /** The session would have handled too many guilds - you are required to [shard](https://discord.com/developers/docs/topics/gateway#sharding) your connection in order to connect. */ - ShardingRequired, - /** You sent an invalid version for the gateway. */ - InvalidApiVersion, - /** You sent an invalid intent for a [Gateway Intent](https://discord.com/developers/docs/topics/gateway#gateway-intents). You may have incorrectly calculated the bitwise value. */ - InvalidIntents, - /** You sent a disallowed intent for a [Gateway Intent](https://discord.com/developers/docs/topics/gateway#gateway-intents). You may have tried to specify an intent that you [have not enabled or are not approved for](https://discord.com/developers/docs/topics/gateway#privileged-intents). */ - DisallowedIntents, -} - -/** https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types */ -export enum InviteTargetTypes { - Stream = 1, - EmbeddedApplication, -} - -/** https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes */ -export enum GatewayOpcodes { - /** An event was dispatched. */ - Dispatch, - /** Fired periodically by the client to keep the connection alive. */ - Heartbeat, - /** Starts a new session during the initial handshake. */ - Identify, - /** Update the client's presence. */ - PresenceUpdate, - /** Used to join/leave or move between voice channels. */ - - VoiceStateUpdate, - /** Resume a previous session that was disconnected. */ - Resume = 6, - /** You should attempt to reconnect and resume immediately. */ - Reconnect, - /** Request information about offline guild members in a large guild. */ - RequestGuildMembers, - /** The session has been invalidated. You should reconnect and identify/resume accordingly. */ - InvalidSession, - /** Sent immediately after connecting, contains the `heartbeat_interval` to use. */ - Hello, - /** Sent in response to receiving a heartbeat to acknowledge that it has been received. */ - HeartbeatACK, -} - -export type GatewayDispatchEventNames = - | 'READY' - | 'CHANNEL_CREATE' - | 'CHANNEL_DELETE' - | 'CHANNEL_PINS_UPDATE' - | 'CHANNEL_UPDATE' - | 'GUILD_BAN_ADD' - | 'GUILD_BAN_REMOVE' - | 'GUILD_CREATE' - | 'GUILD_DELETE' - | 'GUILD_EMOJIS_UPDATE' - | 'GUILD_INTEGRATIONS_UPDATE' - | 'GUILD_MEMBER_ADD' - | 'GUILD_MEMBER_REMOVE' - | 'GUILD_MEMBER_UPDATE' - | 'GUILD_MEMBERS_CHUNK' - | 'GUILD_ROLE_CREATE' - | 'GUILD_ROLE_DELETE' - | 'GUILD_ROLE_UPDATE' - | 'GUILD_UPDATE' - | 'GUILD_SCHEDULED_EVENT_CREATE' - | 'GUILD_SCHEDULED_EVENT_DELETE' - | 'GUILD_SCHEDULED_EVENT_UPDATE' - | 'GUILD_SCHEDULED_EVENT_USER_ADD' - | 'GUILD_SCHEDULED_EVENT_USER_REMOVE' - | 'INTERACTION_CREATE' - | 'INVITE_CREATE' - | 'INVITE_DELETE' - | 'MESSAGE_CREATE' - | 'MESSAGE_DELETE_BULK' - | 'MESSAGE_DELETE' - | 'MESSAGE_REACTION_ADD' - | 'MESSAGE_REACTION_REMOVE_ALL' - | 'MESSAGE_REACTION_REMOVE_EMOJI' - | 'MESSAGE_REACTION_REMOVE' - | 'MESSAGE_UPDATE' - | 'PRESENCE_UPDATE' - | 'TYPING_START' - | 'USER_UPDATE' - | 'VOICE_SERVER_UPDATE' - | 'VOICE_STATE_UPDATE' - | 'WEBHOOKS_UPDATE' - | 'INTEGRATION_CREATE' - | 'INTEGRATION_UPDATE' - | 'INTEGRATION_DELETE' - | 'STAGE_INSTANCE_CREATE' - | 'STAGE_INSTANCE_UPDATE' - | 'STAGE_INSTANCE_DELETE' - | 'THREAD_CREATE' - | 'THREAD_UPDATE' - | 'THREAD_DELETE' - | 'THREAD_LIST_SYNC' - | 'THREAD_MEMBERS_UPDATE'; - -export type GatewayEventNames = - | GatewayDispatchEventNames - | 'READY' - | 'RESUMED' - // THIS IS A CUSTOM DD EVENT NOT A DISCORD EVENT - | 'GUILD_LOADED_DD'; - -/** https://discord.com/developers/docs/topics/gateway#list-of-intents */ -export enum GatewayIntents { - /** - * - GUILD_CREATE - * - GUILD_DELETE - * - GUILD_ROLE_CREATE - * - GUILD_ROLE_UPDATE - * - GUILD_ROLE_DELETE - * - CHANNEL_CREATE - * - CHANNEL_UPDATE - * - CHANNEL_DELETE - * - CHANNEL_PINS_UPDATE - * - THREAD_CREATE - * - THREAD_UPDATE - * - THREAD_DELETE - * - THREAD_LIST_SYNC - * - THREAD_MEMBER_UPDATE - * - THREAD_MEMBERS_UPDATE - * - STAGE_INSTANCE_CREATE - * - STAGE_INSTANCE_UPDATE - * - STAGE_INSTANCE_DELETE - */ - Guilds = 1 << 0, - /** - * - GUILD_MEMBER_ADD - * - GUILD_MEMBER_UPDATE - * - GUILD_MEMBER_REMOVE - */ - GuildMembers = 1 << 1, - /** - * - GUILD_BAN_ADD - * - GUILD_BAN_REMOVE - */ - GuildBans = 1 << 2, - /** - * - GUILD_EMOJIS_UPDATE - */ - GuildEmojis = 1 << 3, - /** - * - GUILD_INTEGRATIONS_UPDATE - * - INTEGRATION_CREATE - * - INTEGRATION_UPDATE - * - INTEGRATION_DELETE - */ - GuildIntegrations = 1 << 4, - /** Enables the following events: - * - WEBHOOKS_UPDATE - */ - GuildWebhooks = 1 << 5, - /** - * - INVITE_CREATE - * - INVITE_DELETE - */ - GuildInvites = 1 << 6, - /** - * - VOICE_STATE_UPDATE - */ - GuildVoiceStates = 1 << 7, - /** - * - PRESENCE_UPDATE - */ - GuildPresences = 1 << 8, - /** - * - MESSAGE_CREATE - * - MESSAGE_UPDATE - * - MESSAGE_DELETE - */ - GuildMessages = 1 << 9, - /** - * - MESSAGE_REACTION_ADD - * - MESSAGE_REACTION_REMOVE - * - MESSAGE_REACTION_REMOVE_ALL - * - MESSAGE_REACTION_REMOVE_EMOJI - */ - GuildMessageReactions = 1 << 10, - /** - * - TYPING_START - */ - GuildMessageTyping = 1 << 11, - /** - * - CHANNEL_CREATE - * - MESSAGE_CREATE - * - MESSAGE_UPDATE - * - MESSAGE_DELETE - * - CHANNEL_PINS_UPDATE - */ - DirectMessages = 1 << 12, - /** - * - MESSAGE_REACTION_ADD - * - MESSAGE_REACTION_REMOVE - * - MESSAGE_REACTION_REMOVE_ALL - * - MESSAGE_REACTION_REMOVE_EMOJI - */ - DirectMessageReactions = 1 << 13, - /** - * - TYPING_START - */ - DirectMessageTyping = 1 << 14, - - /** - * This intent will add `content` values to all message objects. - */ - MessageContent = 1 << 15, - /** - * - GUILD_SCHEDULED_EVENT_CREATE - * - GUILD_SCHEDULED_EVENT_UPDATE - * - GUILD_SCHEDULED_EVENT_DELETE - * - GUILD_SCHEDULED_EVENT_USER_ADD this is experimental and unstable. - * - GUILD_SCHEDULED_EVENT_USER_REMOVE this is experimental and unstable. - */ - GuildScheduledEvents = 1 << 16, - - /** - * - AUTO_MODERATION_RULE_CREATE - * - AUTO_MODERATION_RULE_UPDATE - * - AUTO_MODERATION_RULE_DELETE - */ - AutoModerationConfiguration = 1 << 20, - /** - * - AUTO_MODERATION_ACTION_EXECUTION - */ - AutoModerationExecution = 1 << 21, -} - -// ALIASES JUST FOR BETTER UX IN THIS CASE - -/** https://discord.com/developers/docs/topics/gateway#list-of-intents */ -export const Intents = GatewayIntents; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export type Intents = GatewayIntents; - -/** https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype */ -export enum InteractionResponseTypes { - /** ACK a `Ping` */ - Pong = 1, - /** Respond to an interaction with a message */ - ChannelMessageWithSource = 4, - /** ACK an interaction and edit a response later, the user sees a loading state */ - DeferredChannelMessageWithSource = 5, - /** For components, ACK an interaction and edit the original message later; the user does not see a loading state */ - DeferredUpdateMessage = 6, - /** For components, edit the message the component was attached to */ - UpdateMessage = 7, - /** For Application Command Options, send an autocomplete result */ - ApplicationCommandAutocompleteResult = 8, - /** For Command or Component interactions, send a Modal response */ - Modal = 9, -} - -export enum Errors { - // Bot Role errors - BOTS_HIGHEST_ROLE_TOO_LOW = 'BOTS_HIGHEST_ROLE_TOO_LOW', - // Channel Errors - CHANNEL_NOT_FOUND = 'CHANNEL_NOT_FOUND', - CHANNEL_NOT_IN_GUILD = 'CHANNEL_NOT_IN_GUILD', - CHANNEL_NOT_TEXT_BASED = 'CHANNEL_NOT_TEXT_BASED', - CHANNEL_NOT_STAGE_VOICE = 'CHANNEL_NOT_STAGE_VOICE', - MESSAGE_MAX_LENGTH = 'MESSAGE_MAX_LENGTH', - RULES_CHANNEL_CANNOT_BE_DELETED = 'RULES_CHANNEL_CANNOT_BE_DELETED', - UPDATES_CHANNEL_CANNOT_BE_DELETED = 'UPDATES_CHANNEL_CANNOT_BE_DELETED', - INVALID_TOPIC_LENGTH = 'INVALID_TOPIC_LENGTH', - // Guild Errors - GUILD_NOT_DISCOVERABLE = 'GUILD_NOT_DISCOVERABLE', - GUILD_WIDGET_NOT_ENABLED = 'GUILD_WIDGET_NOT_ENABLED', - GUILD_NOT_FOUND = 'GUILD_NOT_FOUND', - MEMBER_NOT_FOUND = 'MEMBER_NOT_FOUND', - MEMBER_NOT_IN_VOICE_CHANNEL = 'MEMBER_NOT_IN_VOICE_CHANNEL', - MEMBER_SEARCH_LIMIT_TOO_HIGH = 'MEMBER_SEARCH_LIMIT_TOO_HIGH', - MEMBER_SEARCH_LIMIT_TOO_LOW = 'MEMBER_SEARCH_LIMIT_TOO_LOW', - PRUNE_MAX_DAYS = 'PRUNE_MAX_DAYS', - ROLE_NOT_FOUND = 'ROLE_NOT_FOUND', - // Thread errors - INVALID_THREAD_PARENT_CHANNEL_TYPE = 'INVALID_THREAD_PARENT_CHANNEL_TYPE', - GUILD_NEWS_CHANNEL_ONLY_SUPPORT_PUBLIC_THREADS = 'GUILD_NEWS_CHANNEL_ONLY_SUPPORT_PUBLIC_THREADS', - NOT_A_THREAD_CHANNEL = 'NOT_A_THREAD_CHANNEL', - MISSING_MANAGE_THREADS_AND_NOT_MEMBER = 'MISSING_MANAGE_THREADS_AND_NOT_MEMBER', - CANNOT_GET_MEMBERS_OF_AN_UNJOINED_PRIVATE_THREAD = 'CANNOT_GET_MEMBERS_OF_AN_UNJOINED_PRIVATE_THREAD', - HAVE_TO_BE_THE_CREATOR_OF_THE_THREAD_OR_HAVE_MANAGE_THREADS_TO_REMOVE_MEMBERS = 'HAVE_TO_BE_THE_CREATOR_OF_THE_THREAD_OR_HAVE_MANAGE_THREADS_TO_REMOVE_MEMBERS', - // Message Get Errors - INVALID_GET_MESSAGES_LIMIT = 'INVALID_GET_MESSAGES_LIMIT', - // Message Delete Errors - DELETE_MESSAGES_MIN = 'DELETE_MESSAGES_MIN', - PRUNE_MIN_DAYS = 'PRUNE_MIN_DAYS', - // Interaction Errors - INVALID_SLASH_DESCRIPTION = 'INVALID_SLASH_DESCRIPTION', - INVALID_SLASH_NAME = 'INVALID_SLASH_NAME', - INVALID_SLASH_OPTIONS = 'INVALID_SLASH_OPTIONS', - INVALID_SLASH_OPTIONS_CHOICES = 'INVALID_SLASH_OPTIONS_CHOICES', - TOO_MANY_SLASH_OPTIONS = 'TOO_MANY_SLASH_OPTIONS', - INVALID_SLASH_OPTION_CHOICE_NAME = 'INVALID_SLASH_OPTION_CHOICE_NAME', - INVALID_SLASH_OPTIONS_CHOICE_VALUE_TYPE = 'INVALID_SLASH_OPTIONS_CHOICE_VALUE_TYPE', - TOO_MANY_SLASH_OPTION_CHOICES = 'TOO_MANY_SLASH_OPTION_CHOICES', - ONLY_STRING_OR_INTEGER_OPTIONS_CAN_HAVE_CHOICES = 'ONLY_STRING_OR_INTEGER_OPTIONS_CAN_HAVE_CHOICES', - INVALID_SLASH_OPTION_NAME = 'INVALID_SLASH_OPTION_NAME', - INVALID_SLASH_OPTION_DESCRIPTION = 'INVALID_SLASH_OPTION_DESCRIPTION', - INVALID_CONTEXT_MENU_COMMAND_NAME = 'INVALID_CONTEXT_MENU_COMMAND_NAME', - INVALID_CONTEXT_MENU_COMMAND_DESCRIPTION = 'INVALID_CONTEXT_MENU_COMMAND_DESCRIPTION', - // Webhook Errors - INVALID_WEBHOOK_NAME = 'INVALID_WEBHOOK_NAME', - INVALID_WEBHOOK_OPTIONS = 'INVALID_WEBHOOK_OPTIONS', - // Permission Errors - MISSING_ADD_REACTIONS = 'MISSING_ADD_REACTIONS', - MISSING_ADMINISTRATOR = 'MISSING_ADMINISTRATOR', - MISSING_ATTACH_FILES = 'MISSING_ATTACH_FILES', - MISSING_BAN_MEMBERS = 'MISSING_BAN_MEMBERS', - MISSING_CHANGE_NICKNAME = 'MISSING_CHANGE_NICKNAME', - MISSING_CONNECT = 'MISSING_CONNECT', - MISSING_CREATE_INSTANT_INVITE = 'MISSING_CREATE_INSTANT_INVITE', - MISSING_DEAFEN_MEMBERS = 'MISSING_DEAFEN_MEMBERS', - MISSING_EMBED_LINKS = 'MISSING_EMBED_LINKS', - MISSING_INTENT_GUILD_MEMBERS = 'MISSING_INTENT_GUILD_MEMBERS', - MISSING_KICK_MEMBERS = 'MISSING_KICK_MEMBERS', - MISSING_MANAGE_CHANNELS = 'MISSING_MANAGE_CHANNELS', - MISSING_MANAGE_EMOJIS = 'MISSING_MANAGE_EMOJIS', - MISSING_MANAGE_GUILD = 'MISSING_MANAGE_GUILD', - MISSING_MANAGE_MESSAGES = 'MISSING_MANAGE_MESSAGES', - MISSING_MANAGE_NICKNAMES = 'MISSING_MANAGE_NICKNAMES', - MISSING_MANAGE_ROLES = 'MISSING_MANAGE_ROLES', - MISSING_MANAGE_WEBHOOKS = 'MISSING_MANAGE_WEBHOOKS', - MISSING_MENTION_EVERYONE = 'MISSING_MENTION_EVERYONE', - MISSING_MOVE_MEMBERS = 'MISSING_MOVE_MEMBERS', - MISSING_MUTE_MEMBERS = 'MISSING_MUTE_MEMBERS', - MISSING_PRIORITY_SPEAKER = 'MISSING_PRIORITY_SPEAKER', - MISSING_READ_MESSAGE_HISTORY = 'MISSING_READ_MESSAGE_HISTORY', - MISSING_SEND_MESSAGES = 'MISSING_SEND_MESSAGES', - MISSING_SEND_TTS_MESSAGES = 'MISSING_SEND_TTS_MESSAGES', - MISSING_SPEAK = 'MISSING_SPEAK', - MISSING_STREAM = 'MISSING_STREAM', - MISSING_USE_VAD = 'MISSING_USE_VAD', - MISSING_USE_EXTERNAL_EMOJIS = 'MISSING_USE_EXTERNAL_EMOJIS', - MISSING_VIEW_AUDIT_LOG = 'MISSING_VIEW_AUDIT_LOG', - MISSING_VIEW_CHANNEL = 'MISSING_VIEW_CHANNEL', - MISSING_VIEW_GUILD_INSIGHTS = 'MISSING_VIEW_GUILD_INSIGHTS', - // User Errors - NICKNAMES_MAX_LENGTH = 'NICKNAMES_MAX_LENGTH', - USERNAME_INVALID_CHARACTER = 'USERNAME_INVALID_CHARACTER', - USERNAME_INVALID_USERNAME = 'USERNAME_INVALID_USERNAME', - USERNAME_MAX_LENGTH = 'USERNAME_MAX_LENGTH', - USERNAME_MIN_LENGTH = 'USERNAME_MIN_LENGTH', - NONCE_TOO_LONG = 'NONCE_TOO_LONG', - INVITE_MAX_AGE_INVALID = 'INVITE_MAX_AGE_INVALID', - INVITE_MAX_USES_INVALID = 'INVITE_MAX_USES_INVALID', - // API Errors - RATE_LIMIT_RETRY_MAXED = 'RATE_LIMIT_RETRY_MAXED', - REQUEST_CLIENT_ERROR = 'REQUEST_CLIENT_ERROR', - REQUEST_SERVER_ERROR = 'REQUEST_SERVER_ERROR', - REQUEST_UNKNOWN_ERROR = 'REQUEST_UNKNOWN_ERROR', - // Component Errors - TOO_MANY_COMPONENTS = 'TOO_MANY_COMPONENTS', - TOO_MANY_ACTION_ROWS = 'TOO_MANY_ACTION_ROWS', - LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID = 'LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID', - COMPONENT_LABEL_TOO_BIG = 'COMPONENT_LABEL_TOO_BIG', - COMPONENT_CUSTOM_ID_TOO_BIG = 'COMPONENT_CUSTOM_ID_TOO_BIG', - BUTTON_REQUIRES_CUSTOM_ID = 'BUTTON_REQUIRES_CUSTOM_ID', - COMPONENT_SELECT_MUST_BE_ALONE = 'COMPONENT_SELECT_MUST_BE_ALONE', - COMPONENT_PLACEHOLDER_TOO_BIG = 'COMPONENT_PLACEHOLDER_TOO_BIG', - COMPONENT_SELECT_MIN_VALUE_TOO_LOW = 'COMPONENT_SELECT_MIN_VALUE_TOO_LOW', - COMPONENT_SELECT_MIN_VALUE_TOO_MANY = 'COMPONENT_SELECT_MIN_VALUE_TOO_MANY', - COMPONENT_SELECT_MAX_VALUE_TOO_LOW = 'COMPONENT_SELECT_MAX_VALUE_TOO_LOW', - COMPONENT_SELECT_MAX_VALUE_TOO_MANY = 'COMPONENT_SELECT_MAX_VALUE_TOO_MANY', - COMPONENT_SELECT_OPTIONS_TOO_LOW = 'COMPONENT_SELECT_OPTIONS_TOO_LOW', - COMPONENT_SELECT_OPTIONS_TOO_MANY = 'COMPONENT_SELECT_OPTIONS_TOO_MANY', - SELECT_OPTION_LABEL_TOO_BIG = 'SELECT_OPTION_LABEL_TOO_BIG', - SELECT_OPTION_VALUE_TOO_BIG = 'SELECT_OPTION_VALUE_TOO_BIG', - SELECT_OPTION_TOO_MANY_DEFAULTS = 'SELECT_OPTION_TOO_MANY_DEFAULTS', - COMPONENT_SELECT_MIN_HIGHER_THAN_MAX = 'COMPONENT_SELECT_MIN_HIGHER_THAN_MAX', - CANNOT_ADD_USER_TO_ARCHIVED_THREADS = 'CANNOT_ADD_USER_TO_ARCHIVED_THREADS', - CANNOT_LEAVE_ARCHIVED_THREAD = 'CANNOT_LEAVE_ARCHIVED_THREAD', - CANNOT_REMOVE_FROM_ARCHIVED_THREAD = 'CANNOT_REMOVE_FROM_ARCHIVED_THREAD', - YOU_CAN_NOT_DM_THE_BOT_ITSELF = 'YOU_CAN_NOT_DM_THE_BOT_ITSELF', -} - -export enum Locales { - Danish = 'da', - German = 'de', - EnglishUk = 'en-GB', - EnglishUs = 'en-US', - Spanish = 'es-ES', - French = 'fr', - Croatian = 'hr', - Italian = 'it', - Lithuanian = 'lt', - Hungarian = 'hu', - Dutch = 'nl', - Norwegian = 'no', - Polish = 'pl', - PortugueseBrazilian = 'pt-BR', - RomanianRomania = 'ro', - Finnish = 'fi', - Swedish = 'sv-SE', - Vietnamese = 'vi', - Turkish = 'tr', - Czech = 'cs', - Greek = 'el', - Bulgarian = 'bg', - Russian = 'ru', - Ukrainian = 'uk', - Hindi = 'hi', - Thai = 'th', - ChineseChina = 'zh-CN', - Japanese = 'ja', - ChineseTaiwan = 'zh-TW', - Korean = 'ko', -} - -export type Localization = Partial>; - -export interface FileContent { - /** The file blob */ - blob: Blob; - /** The name of the file */ - name: string; -} - -export interface GatewayBot { - /** The WSS URL that can be used for connecting to the gateway */ - url: string; - /** The recommended number of shards to use when connecting */ - shards: number; - /** Information on the current session start limit */ - sessionStartLimit: { - /** The total number of session starts the current user is allowed */ - total: number; - /** The remaining number of session starts the current user is allowed */ - remaining: number; - /** The number of milliseconds after which the limit resets */ - resetAfter: number; - /** The number of identify requests allowed per 5 seconds */ - maxConcurrency: number; - }; -} - -// UTILS - -export type AtLeastOne }> = Partial & - U[keyof U]; -export type MakeRequired = T & { [P in K]-?: T[P] }; - -// THANK YOU YUI FOR SHARING THIS! -export type CamelCase = - S extends `${infer P1}_${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase; -export type Camelize = { - // eslint-disable-next-line @typescript-eslint/array-type - [K in keyof T as CamelCase]: T[K] extends Array - ? // eslint-disable-next-line @typescript-eslint/ban-types - U extends {} - ? // eslint-disable-next-line @typescript-eslint/array-type - Array> - : T[K] - : // eslint-disable-next-line @typescript-eslint/ban-types - T[K] extends {} - ? Camelize - : never; -}; - -export type PickPartial = { - [P in keyof T]?: T[P] | undefined; -} & { [P in K]: T[P] }; - -export type OmitFirstFnArg = F extends (x: any, ...args: infer P) => infer R - ? (...args: P) => R - : never; - -export type Snowflake = string; diff --git a/packages/api-types/src/index.ts b/packages/api-types/src/index.ts deleted file mode 100644 index de7a384..0000000 --- a/packages/api-types/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * as Constants from './utils/constants'; -export * from './utils/routes'; - -export * from './v10/index'; -export * from './common'; diff --git a/packages/api-types/src/utils/cdn.ts b/packages/api-types/src/utils/cdn.ts deleted file mode 100644 index cdd4aad..0000000 --- a/packages/api-types/src/utils/cdn.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Snowflake } from '../common'; -import { baseEndpoints as Endpoints } from './constants'; - -export function USER_AVATAR(userId: Snowflake, icon: string): string { - return `${Endpoints.CDN_URL}/avatars/${userId}/${icon}`; -} - -export function EMOJI_URL(id: Snowflake, animated = false): string { - return `https://cdn.discordapp.com/emojis/${id}.${animated ? 'gif' : 'png'}`; -} - -export function USER_DEFAULT_AVATAR( - /** user discriminator */ - altIcon: number, -): string { - return `${Endpoints.CDN_URL}/embed/avatars/${altIcon}.png`; -} - -export function GUILD_BANNER(guildId: Snowflake, icon: string): string { - return `${Endpoints.CDN_URL}/banners/${guildId}/${icon}`; -} - -export function GUILD_SPLASH(guildId: Snowflake, icon: string): string { - return `${Endpoints.CDN_URL}/splashes/${guildId}/${icon}`; -} - -export function GUILD_ICON(guildId: Snowflake, icon: string): string { - return `${Endpoints.CDN_URL}/icons/${guildId}/${icon}`; -} diff --git a/packages/api-types/src/utils/constants.ts b/packages/api-types/src/utils/constants.ts deleted file mode 100644 index 07ecee1..0000000 --- a/packages/api-types/src/utils/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** https://discord.com/developers/docs/reference#api-reference-base-url */ -export const BASE_URL = 'https://discord.com/api'; - -/** https://discord.com/developers/docs/reference#api-versioning-api-versions */ -export const API_VERSION = 10; - -/** https://github.com/oasisjs/biscuit/releases */ -export const BISCUIT_VERSION = '2.0.5'; - -/** https://discord.com/developers/docs/reference#user-agent */ -export const USER_AGENT = `DiscordBot (https://github.com/oasisjs/biscuit, v${BISCUIT_VERSION})`; - -/** https://discord.com/developers/docs/reference#image-formatting-image-base-url */ -export const IMAGE_BASE_URL = 'https://cdn.discordapp.com'; - -// This can be modified by big brain bots and use a proxy -export const baseEndpoints = { - BASE_URL: `${BASE_URL}/v${API_VERSION}`, - CDN_URL: IMAGE_BASE_URL -}; - -export const SLASH_COMMANDS_NAME_REGEX = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u; -export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/; -export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g; -export const DISCORD_SNOWFLAKE_REGEX = /^(?\d{17,19})$/; diff --git a/packages/api-types/src/utils/routes.ts b/packages/api-types/src/utils/routes.ts deleted file mode 100644 index 0609558..0000000 --- a/packages/api-types/src/utils/routes.ts +++ /dev/null @@ -1,519 +0,0 @@ -import type { AuditLogEvents, Snowflake } from '../common'; -export * from './cdn'; - -export function USER(userId?: Snowflake): string { - if (!userId) { return '/users/@me'; } - return `/users/${userId}`; -} - -export function GATEWAY_BOT(): string { - return '/gateway/bot'; -} - -export interface GetMessagesOptions { - limit?: number; -} - -export interface GetMessagesOptions { - around?: Snowflake; - limit?: number; -} - -export interface GetMessagesOptions { - before?: Snowflake; - limit?: number; -} - -export interface GetMessagesOptions { - after?: Snowflake; - limit?: number; -} - -export function CHANNEL(channelId: Snowflake): string { - return `/channels/${channelId}`; -} - -export function CHANNEL_INVITES(channelId: Snowflake): string { - return `/channels/${channelId}/invites`; -} - -export function CHANNEL_TYPING(channelId: Snowflake): string { - return `/channels/${channelId}/typing`; -} - -export function CHANNEL_CREATE_THREAD(channelId: Snowflake): string { - return `/channels/${channelId}/threads`; -} - -export function MESSAGE_CREATE_THREAD(channelId: Snowflake, messageId: Snowflake): string { - return `/channels/${channelId}/messages/${messageId}/threads`; -} - -/** used to send messages */ -export function CHANNEL_MESSAGES(channelId: Snowflake, options?: GetMessagesOptions): string { - let url = `/channels/${channelId}/messages?`; - - if (options) { - if (options.after) { url += `after=${options.after}`; } - if (options.before) { url += `&before=${options.before}`; } - if (options.around) { url += `&around=${options.around}`; } - if (options.limit) { url += `&limit=${options.limit}`; } - } - - return url; -} - -/** used to edit messages */ -export function CHANNEL_MESSAGE(channelId: Snowflake, messageId: Snowflake): string { - return `/channels/${channelId}/messages/${messageId}`; -} - -/** used to kick members */ -export function GUILD_MEMBER(guildId: Snowflake, userId: Snowflake): string { - return `/guilds/${guildId}/members/${userId}`; -} - -export interface ListGuildMembers { - limit?: number; - after?: string; -} - -export function GUILD_MEMBERS(guildId: Snowflake, options?: ListGuildMembers) { - let url = `/guilds/${guildId}/members?`; - - if (options?.limit) { url += `limit=${options.limit}`; } - if (options?.after) { url += `&after=${options.after}`; } - - return url; -} - -/** used to ban members */ -export function GUILD_BAN(guildId: Snowflake, userId: Snowflake): string { - return `/guilds/${guildId}/bans/${userId}`; -} - -export interface GetBans { - limit?: number; - before?: Snowflake; - after?: Snowflake; -} - -/** used to unban members */ -export function GUILD_BANS(guildId: Snowflake, options?: GetBans): string { - let url = `/guilds/${guildId}/bans?`; - - if (options) { - if (options.limit) { url += `limit=${options.limit}`; } - if (options.after) { url += `&after=${options.after}`; } - if (options.before) { url += `&before=${options.before}`; } - } - - return url; -} - -export function GUILD_ROLE(guildId: Snowflake, roleId: Snowflake): string { - return `/guilds/${guildId}/roles/${roleId}`; -} - -export function GUILD_ROLES(guildId: Snowflake): string { - return `/guilds/${guildId}/roles`; -} - -export function USER_GUILDS(guildId?: Snowflake): string { - if (guildId) { return `/users/@me/guilds/${guildId}`; } - return `/users/@me/guilds`; -} - -export function USER_DM() { - return `/users/@me/channels`; -} - -export function GUILD_EMOJIS(guildId: Snowflake, emojiId?: Snowflake): string { - if (emojiId) { return `/guilds/${guildId}/emojis/${emojiId}`; } - return `/guilds/${guildId}/emojis`; -} - -export interface GetAuditLogs { - userId?: Snowflake; - actionType?: AuditLogEvents; - before?: Snowflake; - limit?: number; -} - -export function GUILD_AUDIT_LOGS(guildId: Snowflake, options?: GetAuditLogs) { - let url = `/guilds/${guildId}/audit-logs?`; - if (options) { - const obj = { - user_id: options.userId, - action_type: options.actionType, - before: options.before, - limit: options.limit - }; - for (const [key, value] of Object.entries(obj)) { - if (!value) { continue; } - url += `&${key}=${value}`; - } - } - return url; -} - -export function GUILD_EMOJI(guildId: Snowflake, emojiId: Snowflake): string { - return `/guilds/${guildId}/emojis/${emojiId}`; -} - -export interface GetInvite { - withCounts?: boolean; - withExpiration?: boolean; - scheduledEventId?: Snowflake; -} - -export function GUILDS(guildId?: Snowflake): string { - if (guildId) { return `/guilds/${guildId}`; } - return `/guilds`; -} - -export function AUTO_MODERATION_RULES(guildId: Snowflake, ruleId?: Snowflake): string { - if (ruleId) { - return `/guilds/${guildId}/auto-moderation/rules/${ruleId}`; - } - return `/guilds/${guildId}/auto-moderation/rules`; -} - -export function INVITE(inviteCode: string, options?: GetInvite): string { - let url = `/invites/${inviteCode}?`; - - if (options) { - if (options.withCounts) { url += `with_counts=${options.withCounts}`; } - if (options.withExpiration) { url += `&with_expiration=${options.withExpiration}`; } - if (options.scheduledEventId) { url += `&guild_scheduled_event_id=${options.scheduledEventId}`; } - } - - return url; -} - -export function GUILD_INVITES(guildId: Snowflake): string { - return `/guilds/${guildId}/invites`; -} - -export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string): string { - return `/interactions/${interactionId}/${token}/callback`; -} - -export function WEBHOOK_MESSAGE_ORIGINAL(webhookId: Snowflake, token: string, options?: { threadId?: bigint }): string { - let url = `/webhooks/${webhookId}/${token}/messages/@original?`; - - if (options) { - if (options.threadId) { url += `thread_id=${options.threadId}`; } - } - - return url; -} - -export function WEBHOOK_MESSAGE( - webhookId: Snowflake, - token: string, - messageId: Snowflake, - options?: { threadId?: Snowflake }, -): string { - let url = `/webhooks/${webhookId}/${token}/messages/${messageId}?`; - - if (options) { - if (options.threadId) { url += `thread_id=${options.threadId}`; } - } - - return url; -} - -export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string): string { - if (!token) { return `/webhooks/${webhookId}`; } - return `/webhooks/${webhookId}/${token}`; -} - -export interface WebhookOptions { - wait?: boolean; - threadId?: Snowflake; -} - -export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions): string { - let url = `/webhooks/${webhookId}/${token}`; - - if (options?.wait) { url += `?wait=${options.wait}`; } - if (options?.threadId) { url += `?thread_id=${options.threadId}`; } - if (options?.wait && options.threadId) { url += `?wait=${options.wait}&thread_id=${options.threadId}`; } - - return url; -} - -export function USER_NICK(guildId: Snowflake): string { - return `/guilds/${guildId}/members/@me`; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#get-guild-prune-count - */ -export interface GetGuildPruneCountQuery { - days?: number; - includeRoles?: Snowflake | Snowflake[]; -} - -export function GUILD_PRUNE(guildId: Snowflake, options?: GetGuildPruneCountQuery): string { - let url = `/guilds/${guildId}/prune?`; - - if (options?.days) { url += `days=${options.days}`; } - if (options?.includeRoles) { url += `&include_roles=${options.includeRoles}`; } - - return url; -} - -export function CHANNEL_PIN(channelId: Snowflake, messageId: Snowflake): string { - return `/channels/${channelId}/pins/${messageId}`; -} - -export function CHANNEL_PINS(channelId: Snowflake): string { - return `/channels/${channelId}/pins`; -} - -export function CHANNEL_MESSAGE_REACTION_ME(channelId: Snowflake, messageId: Snowflake, emoji: string): string { - return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`; -} - -export function CHANNEL_MESSAGE_REACTION_USER( - channelId: Snowflake, - messageId: Snowflake, - emoji: string, - userId: Snowflake, -) { - return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/${userId}`; -} - -export function CHANNEL_MESSAGE_REACTIONS(channelId: Snowflake, messageId: Snowflake) { - return `/channels/${channelId}/messages/${messageId}/reactions`; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params - */ -export interface GetReactions { - after?: string; - limit?: number; -} - -export function CHANNEL_MESSAGE_REACTION( - channelId: Snowflake, - messageId: Snowflake, - emoji: string, - options?: GetReactions, -): string { - let url = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}?`; - - if (options?.after) { url += `after=${options.after}`; } - if (options?.limit) { url += `&limit=${options.limit}`; } - - return url; -} - -export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowflake): string { - return `/channels/${channelId}/messages/${messageId}/crosspost`; -} - -export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleId: Snowflake): string { - return `/guilds/${guildId}/members/${memberId}/roles/${roleId}`; -} - -export function CHANNEL_WEBHOOKS(channelId: Snowflake): string { - return `/channels/${channelId}/webhooks`; -} - -export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake): string { - return `/channels/${channelId}/messages/${messageId}/threads`; -} - -export function THREAD_START_PRIVATE(channelId: Snowflake): string { - return `/channels/${channelId}/threads`; -} - -export function THREAD_ACTIVE(guildId: Snowflake): string { - return `/guilds/${guildId}/threads/active`; -} - -export interface ListArchivedThreads { - before?: number; - limit?: number; -} - -export function THREAD_ME(channelId: Snowflake): string { - return `/channels/${channelId}/thread-members/@me`; -} - -export function THREAD_MEMBERS(channelId: Snowflake): string { - return `/channels/${channelId}/thread-members`; -} - -export function THREAD_USER(channelId: Snowflake, userId: Snowflake): string { - return `/channels/${channelId}/thread-members/${userId}`; -} - -export function THREAD_ARCHIVED(channelId: Snowflake): string { - return `/channels/${channelId}/threads/archived`; -} - -export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads): string { - let url = `/channels/${channelId}/threads/archived/public?`; - - if (options) { - if (options.before) { url += `before=${new Date(options.before).toISOString()}`; } - if (options.limit) { url += `&limit=${options.limit}`; } - } - - return url; -} - -export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads): string { - let url = `/channels/${channelId}/threads/archived/private?`; - - if (options) { - if (options.before) { url += `before=${new Date(options.before).toISOString()}`; } - if (options.limit) { url += `&limit=${options.limit}`; } - } - - return url; -} - -export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads): string { - let url = `/channels/${channelId}/users/@me/threads/archived/private?`; - - if (options) { - if (options.before) { url += `before=${new Date(options.before).toISOString()}`; } - if (options.limit) { url += `&limit=${options.limit}`; } - } - - return url; -} - -export function FORUM_START(channelId: Snowflake): string { - return `/channels/${channelId}/threads?has_message=true`; -} - -export function STAGE_INSTANCES(): string { - return `/stage-instances`; -} - -export function STAGE_INSTANCE(channelId: Snowflake): string { - return `/stage-instances/${channelId}`; -} - -export function APPLICATION_COMMANDS(appId: Snowflake, commandId?: Snowflake): string { - if (commandId) { return `/applications/${appId}/commands/${commandId}`; } - return `/applications/${appId}/commands`; -} - -export function GUILD_APPLICATION_COMMANDS(appId: Snowflake, guildId: Snowflake, commandId?: Snowflake): string { - if (commandId) { return `/applications/${appId}/guilds/${guildId}/commands/${commandId}`; } - return `/applications/${appId}/guilds/${guildId}/commands`; -} - -export function GUILD_APPLICATION_COMMANDS_PERMISSIONS( - appId: Snowflake, - guildId: Snowflake, - commandId?: Snowflake, -): string { - if (commandId) { return `/applications/${appId}/guilds/${guildId}/commands/${commandId}/permissions`; } - return `/applications/${appId}/guilds/${guildId}/commands/permissions`; -} - -export function APPLICATION_COMMANDS_LOCALIZATIONS( - appId: Snowflake, - commandId: Snowflake, - withLocalizations?: boolean, -): string { - let url = `/applications/${appId}/commands/${commandId}?`; - - if (withLocalizations !== undefined) { - url += `withLocalizations=${withLocalizations}`; - } - - return url; -} - -export function GUILD_APPLICATION_COMMANDS_LOCALIZATIONS( - appId: Snowflake, - guildId: Snowflake, - commandId: Snowflake, - withLocalizations?: boolean, -): string { - let url = `/applications/${appId}/guilds/${guildId}/commands/${commandId}?`; - - if (withLocalizations !== undefined) { - url += `with_localizations=${withLocalizations}`; - } - - return url; -} - -export function STICKER(id: Snowflake): string { - return `stickers/${id}`; -} - -export function STICKER_PACKS(): string { - return `stickers-packs`; -} - -export function GUILD_STICKERS(guildId: Snowflake, stickerId?: Snowflake): string { - if (stickerId) { return `/guilds/${guildId}/stickers/${stickerId}`; } - return `/guilds/${guildId}/stickers`; -} - -/** - * Return the widget for the guild. - * @link https://discord.com/developers/docs/resources/guild#get-guild-widget-settings - */ -export interface GetWidget { - get: 'json' | 'image' | 'settings'; -} - -/** - * /guilds/{guildId}/widget - * @link https://discord.com/developers/docs/resources/guild#get-guild-widget-settings - */ -export function GUILD_WIDGET(guildId: Snowflake, options: GetWidget = { get: 'settings' }): string { - let url = `/guilds/${guildId}/widget`; - if (options.get === 'json') { - url += '.json'; - } else if (options.get === 'image') { - url += '.png'; - } - - return url; -} - -/** @link https://discord.com/developers/docs/resources/guild#get-guild-voice-regions */ -export function GUILD_VOICE_REGIONS(guildId: Snowflake): string { - return `/guilds/${guildId}/regions`; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#get-guild-vanity-url - * @param guildId The guild - * @returns Get vanity URL - */ -export function GUILD_VANITY(guildId: Snowflake): string { - return `/guilds/${guildId}/vanity-url`; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#get-guild-preview - * @param guildId The guild - * @returns Get guild preview url - */ -export function GUILD_PREVIEW(guildId: Snowflake): string { - return `/guilds/${guildId}/preview`; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#get-guild-channels - * @param guildId The guild id. - * @returns Get guild channels url. - */ -export function GUILD_CHANNELS(guildId: Snowflake): string { - return `/guilds/${guildId}/channels`; -} diff --git a/packages/api-types/src/v10/index.ts b/packages/api-types/src/v10/index.ts deleted file mode 100644 index 06addd7..0000000 --- a/packages/api-types/src/v10/index.ts +++ /dev/null @@ -1,2497 +0,0 @@ -import type { - ActivityTypes, - AllowedMentionsTypes, - ApplicationCommandOptionTypes, - ApplicationCommandPermissionTypes, - ApplicationCommandTypes, - ApplicationFlags, - AuditLogEvents, - ButtonStyles, - ChannelFlags, - ChannelTypes, - DefaultMessageNotificationLevels, - EmbedTypes, - ExplicitContentFilterLevels, - DiscordForumTag, - GatewayEventNames, - GuildFeatures, - GuildNsfwLevel, - IntegrationExpireBehaviors, - InteractionTypes, - Localization, - MessageActivityTypes, - MessageComponentTypes, - MessageTypes, - MfaLevels, - OverwriteTypes, - PickPartial, - PremiumTiers, - PremiumTypes, - ScheduledEventEntityType, - ScheduledEventPrivacyLevel, - ScheduledEventStatus, - StickerFormatTypes, - StickerTypes, - SystemChannelFlags, - TargetTypes, - TeamMembershipStates, - TextStyles, - UserFlags, - VerificationLevels, - VideoQualityModes, - VisibilityTypes, - WebhookTypes, -} from '../common'; - -/** https://discord.com/developers/docs/resources/user#user-object */ -export interface DiscordUser { - /** The user's username, not unique across the platform */ - username: string; - /** The user's chosen language option */ - locale?: string; - /** The flags on a user's account */ - flags?: UserFlags; - /** The type of Nitro subscription on a user's account */ - premium_type?: PremiumTypes; - /** The public flags on a user's account */ - public_flags?: UserFlags; - /** the user's banner color encoded as an integer representation of hexadecimal color code */ - accent_color?: number; - - /** The user's id */ - id: string; - /** The user's 4-digit discord-tag */ - discriminator: string; - /** The user's avatar hash */ - avatar: string | null; - /** Whether the user belongs to an OAuth2 application */ - bot?: boolean; - /** Whether the user is an Official Discord System user (part of the urgent message system) */ - system?: boolean; - /** Whether the user has two factor enabled on their account */ - mfa_enabled?: boolean; - /** Whether the email on this account has been verified */ - verified?: boolean; - /** The user's email */ - email?: string | null; - /** the user's banner, or null if unset */ - banner?: string; -} - -/** https://discord.com/developers/docs/resources/user#connection-object */ -export interface DiscordConnection { - /** id of the connection account */ - id: string; - /** The username of the connection account */ - name: string; - /** The service of the connection (twitch, youtube) */ - type: string; - /** Whether the connection is revoked */ - revoked?: boolean; - /** Whether the connection is verified */ - verified: boolean; - /** Whether friend sync is enabled for this connection */ - friendSync: boolean; - /** Whether activities related to this connection will be shown in presence updates */ - showActivity: boolean; - /** Visibility of this connection */ - visibility: VisibilityTypes; - - /** An array of partial server integrations */ - integrations?: DiscordIntegration[]; -} - -/** https://discord.com/developers/docs/resources/guild#integration-object-integration-structure */ -export interface DiscordIntegration { - /** Integration Id */ - id: string; - /** Integration name */ - name: string; - /** Integration type (twitch, youtube or discord) */ - type: 'twitch' | 'youtube' | 'discord'; - /** Is this integration enabled */ - enabled?: boolean; - /** Is this integration syncing */ - syncing?: boolean; - /** Role Id that this integration uses for "subscribers" */ - role_id?: string; - /** Whether emoticons should be synced for this integration (twitch only currently) */ - enable_emoticons?: boolean; - /** The behavior of expiring subscribers */ - expire_behavior?: IntegrationExpireBehaviors; - /** The grace period (in days) before expiring subscribers */ - expire_grace_period?: number; - /** When this integration was last synced */ - synced_at?: string; - /** How many subscribers this integration has */ - subscriber_count?: number; - /** Has this integration been revoked */ - revoked?: boolean; - - /** User for this integration */ - user?: DiscordUser; - /** Integration account information */ - account: DiscordIntegrationAccount; - /** The bot/OAuth2 application for discord integrations */ - application?: DiscordIntegrationApplication; -} - -/** https://discord.com/developers/docs/resources/guild#integration-account-object-integration-account-structure */ -export interface DiscordIntegrationAccount { - /** Id of the account */ - id: string; - /** Name of the account */ - name: string; -} - -/** https://discord.com/developers/docs/resources/guild#integration-application-object-integration-application-structure */ -export interface DiscordIntegrationApplication { - /** The id of the app */ - id: string; - /** The name of the app */ - name: string; - /** the icon hash of the app */ - icon: string | null; - /** The description of the app */ - description: string; - - /** The bot associated with this application */ - bot?: DiscordUser; -} - -/** https://github.com/discord/discord-api-docs/blob/master/docs/topics/Gateway.md#integration-create-event-additional-fields */ -export interface DiscordIntegrationCreateUpdate extends DiscordIntegration { - /** Id of the guild */ - guild_id: string; -} - -/** https://github.com/discord/discord-api-docs/blob/master/docs/topics/Gateway.md#integration-delete-event-fields */ -export interface DiscordIntegrationDelete { - /** Integration id */ - id: string; - /** Id of the guild */ - guild_id: string; - /** Id of the bot/OAuth2 application for this discord integration */ - application_id?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-integrations-update */ -export interface DiscordGuildIntegrationsUpdate { - /** id of the guild whose integrations were updated */ - guild_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#typing-start */ -export interface DiscordTypingStart { - /** Unix time (in seconds) of when the user started typing */ - timestamp: number; - - /** id of the channel */ - channel_id: string; - /** id of the guild */ - guild_id?: string; - /** id of the user */ - user_id: string; - /** The member who started typing if this happened in a guild */ - member?: DiscordMember; -} - -/** https://discord.com/developers/docs/resources/guild#guild-member-object */ -export interface DiscordMember { - /** Whether the user is deafened in voice channels */ - deaf?: boolean; - /** Whether the user is muted in voice channels */ - mute?: boolean; - /** Whether the user has not yet passed the guild's Membership Screening requirements */ - pending?: boolean; - - /** The user this guild member represents */ - user?: DiscordUser; - /** This users guild nickname */ - nick?: string | null; - /** The members custom avatar for this server. */ - avatar?: string; - /** Array of role object ids */ - roles: string[]; - /** When the user joined the guild */ - joined_at: string; - /** When the user started boosting the guild */ - premium_since?: string | null; - /** The permissions this member has in the guild. Only present on interaction events. */ - permissions?: string; - /** when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */ - communication_disabled_until?: string | null; -} - -/** https://discord.com/developers/docs/topics/oauth2#application-object */ -export interface DiscordApplication { - /** The name of the app */ - name: string; - /** The description of the app */ - description: string; - /** An array of rpc origin urls, if rpc is enabled */ - rpc_origins?: string[]; - /** The url of the app's terms of service */ - terms_of_service_url?: string; - /** The url of the app's privacy policy */ - privacy_policy_url?: string; - /** The hex encoded key for verification in interactions and the GameSDK's GetTicket */ - verify_key: string; - /** If this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists */ - primary_sku_id?: string; - /** If this application is a game sold on Discord, this field will be the URL slug that links to the store page */ - slug?: string; - /** The application's public flags */ - flags?: ApplicationFlags; - - /** The id of the app */ - id: string; - /** The icon hash of the app */ - icon: string | null; - /** When false only app owner can join the app's bot to guilds */ - bot_public: boolean; - /** When true the app's bot will only join upon completion of the full oauth2 code grant flow */ - bot_require_code_grant: boolean; - /** Partial user object containing info on the owner of the application */ - owner?: Partial; - /** If the application belongs to a team, this will be a list of the members of that team */ - team: DiscordTeam | null; - /** If this application is a game sold on Discord, this field will be the guild to which it has been linked */ - guild_id?: string; - /** If this application is a game sold on Discord, this field will be the hash of the image on store embeds */ - cover_image?: string; - /** up to 5 tags describing the content and functionality of the application */ - tags?: string[]; - /** settings for the application's default in-app authorization link, if enabled */ - install_params?: DiscordInstallParams; - /** the application's default custom authorization link, if enabled */ - custom_install_url?: string; -} - -/** https://discord.com/developers/docs/topics/teams#data-models-team-object */ -export interface DiscordTeam { - /** A hash of the image of the team's icon */ - icon: string | null; - /** The unique id of the team */ - id: string; - /** The members of the team */ - members: DiscordTeamMember[]; - /** The user id of the current team owner */ - owner_user_id: string; - /** The name of the team */ - name: string; -} - -/** https://discord.com/developers/docs/topics/teams#data-models-team-members-object */ -export interface DiscordTeamMember { - /** The user's membership state on the team */ - membership_state: TeamMembershipStates; - /** Will always be `["*"]` */ - permissions: '*'[]; - - /** The id of the parent team of which they are a member */ - team_id: string; - /** The avatar, discriminator, id, and username of the user */ - user: Partial & - Pick; -} - -/** https://discord.com/developers/docs/topics/gateway#webhooks-update-webhook-update-event-fields */ -export interface DiscordWebhookUpdate { - /** id of the guild */ - guild_id: string; - /** id of the channel */ - channel_id: string; -} - -/** https://discord.com/developers/docs/resources/channel#allowed-mentions-object */ -export interface DiscordAllowedMentions { - /** An array of allowed mention types to parse from the content. */ - parse?: AllowedMentionsTypes[]; - /** For replies, whether to mention the author of the message being replied to (default false) */ - replied_user?: boolean; - - /** Array of role_ids to mention (Max size of 100) */ - roles?: string[]; - /** Array of user_ids to mention (Max size of 100) */ - users?: string[]; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object */ -export interface DiscordEmbed { - /** Title of embed */ - title?: string; - /** Type of embed (always "rich" for webhook embeds) */ - type?: EmbedTypes; - /** Description of embed */ - description?: string; - /** Url of embed */ - url?: string; - /** Color code of the embed */ - color?: number; - - /** Timestamp of embed content */ - timestamp?: string; - /** Footer information */ - footer?: DiscordEmbedFooter; - /** Image information */ - image?: DiscordEmbedImage; - /** Thumbnail information */ - thumbnail?: DiscordEmbedThumbnail; - /** Video information */ - video?: DiscordEmbedVideo; - /** Provider information */ - provider?: DiscordEmbedProvider; - /** Author information */ - author?: DiscordEmbedAuthor; - /** Fields information */ - fields?: DiscordEmbedField[]; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure */ -export interface DiscordEmbedAuthor { - /** Name of author */ - name: string; - /** Url of author */ - url?: string; - /** Url of author icon (only supports http(s) and attachments) */ - icon_url?: string; - /** A proxied url of author icon */ - proxy_icon_url?: string; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure */ -export interface DiscordEmbedField { - /** Name of the field */ - name: string; - /** Value of the field */ - value: string; - /** Whether or not this field should display inline */ - inline?: boolean; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure */ -export interface DiscordEmbedFooter { - /** Footer text */ - text: string; - /** Url of footer icon (only supports http(s) and attachments) */ - icon_url?: string; - /** A proxied url of footer icon */ - proxy_icon_url?: string; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure */ -export interface DiscordEmbedImage { - /** Source url of image (only supports http(s) and attachments) */ - url: string; - /** A proxied url of the image */ - proxy_url?: string; - /** Height of image */ - height?: number; - /** Width of image */ - width?: number; -} - -export interface DiscordEmbedProvider { - /** Name of provider */ - name?: string; - /** Url of provider */ - url?: string; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure */ -export interface DiscordEmbedThumbnail { - /** Source url of thumbnail (only supports http(s) and attachments) */ - url: string; - /** A proxied url of the thumbnail */ - proxy_url?: string; - /** Height of thumbnail */ - height?: number; - /** Width of thumbnail */ - width?: number; -} - -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure */ -export interface DiscordEmbedVideo { - /** Source url of video */ - url?: string; - /** A proxied url of the video */ - proxy_url?: string; - /** Height of video */ - height?: number; - /** Width of video */ - width?: number; -} - -/** https://discord.com/developers/docs/resources/channel#attachment-object */ -export interface DiscordAttachment { - /** Name of file attached */ - filename: string; - /** The attachment's [media type](https://en.wikipedia.org/wiki/Media_type) */ - content_type?: string; - /** Size of file in bytes */ - size: number; - /** Source url of file */ - url: string; - /** A proxied url of file */ - proxy_url: string; - - /** Attachment id */ - id: string; - /** Height of file (if image) */ - height?: number | null; - /** Width of file (if image) */ - width?: number | null; - /** whether this attachment is ephemeral. Ephemeral attachments will automatically be removed after a set period of time. Ephemeral attachments on messages are guaranteed to be available as long as the message itself exists. */ - ephemeral?: boolean; -} - -/** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-structure */ -export type DiscordWebhook = DiscordIncomingWebhook | DiscordApplicationWebhook; - -export interface DiscordIncomingWebhook { - /** The type of the webhook */ - type: WebhookTypes; - /** The secure token of the webhook (returned for Incoming Webhooks) */ - token?: string; - /** The url used for executing the webhook (returned by the webhooks OAuth2 flow) */ - url?: string; - - /** The id of the webhook */ - id: string; - /** The guild id this webhook is for */ - guild_id?: string; - /** The channel id this webhook is for */ - channel_id: string; - /** The user this webhook was created by (not returned when getting a webhook with its token) */ - user?: DiscordUser; - /** The default name of the webhook */ - name: string | null; - /** The default user avatar hash of the webhook */ - avatar: string | null; - /** The bot/OAuth2 application that created this webhook */ - application_id: string | null; - /** The guild of the channel that this webhook is following (returned for Channel Follower Webhooks) */ - source_guild?: Partial; - /** The channel that this webhook is following (returned for Channel Follower Webhooks) */ - source_channel?: Partial; -} - -export interface DiscordApplicationWebhook { - /** The type of the webhook */ - type: WebhookTypes.Application; - /** The secure token of the webhook (returned for Incoming Webhooks) */ - token?: string; - /** The url used for executing the webhook (returned by the webhooks OAuth2 flow) */ - url?: string; - - /** The id of the webhook */ - id: string; - /** The guild id this webhook is for */ - guild_id?: string | null; - /** The channel id this webhook is for */ - channel_id?: string | null; - /** The user this webhook was created by (not returned when getting a webhook with its token) */ - user?: DiscordUser; - /** The default name of the webhook */ - name: string | null; - /** The default user avatar hash of the webhook */ - avatar: string | null; - /** The bot/OAuth2 application that created this webhook */ - application_id: string | null; - /** The guild of the channel that this webhook is following (returned for Channel Follower Webhooks) */ - source_guild?: Partial; - /** The channel that this webhook is following (returned for Channel Follower Webhooks) */ - source_channel?: Partial; -} - -/** https://discord.com/developers/docs/resources/guild#guild-object */ -export interface DiscordGuild { - /** Guild name (2-100 characters, excluding trailing and leading whitespace) */ - name: string; - /** True if the user is the owner of the guild */ - owner?: boolean; - /** Afk timeout in seconds */ - afk_timeout: number; - /** True if the server widget is enabled */ - widget_enabled?: boolean; - /** Verification level required for the guild */ - verification_level: VerificationLevels; - /** Default message notifications level */ - default_message_notifications: DefaultMessageNotificationLevels; - /** Explicit content filter level */ - explicit_content_filter: ExplicitContentFilterLevels; - /** Enabled guild features */ - features: GuildFeatures[]; - /** Required MFA level for the guild */ - mfa_level: MfaLevels; - /** System channel flags */ - system_channel_flags: SystemChannelFlags; - /** True if this is considered a large guild */ - large?: boolean; - /** True if this guild is unavailable due to an outage */ - unavailable?: boolean; - /** Total number of members in this guild */ - member_count?: number; - /** The maximum number of presences for the guild (the default value, currently 25000, is in effect when null is returned) */ - max_presences?: number | null; - /** The maximum number of members for the guild */ - max_members?: number; - /** The vanity url code for the guild */ - vanity_url_code: string | null; - /** The description of a guild */ - description: string | null; - /** Premium tier (Server Boost level) */ - premium_tier: PremiumTiers; - /** The number of boosts this guild currently has */ - premium_subscription_count?: number; - /** The maximum amount of users in a video channel */ - max_video_channel_users?: number; - /** Approximate number of members in this guild, returned from the GET /guilds/ endpoint when with_counts is true */ - approximate_member_count?: number; - /** Approximate number of non-offline members in this guild, returned from the GET /guilds/ endpoint when with_counts is true */ - approximate_presence_count?: number; - /** Guild NSFW level */ - nsfw_level: GuildNsfwLevel; - /** Whether the guild has the boost progress bar enabled */ - premium_progress_bar_enabled: boolean; - - /** Guild id */ - id: string; - /** Icon hash */ - icon: string | null; - /** Icon hash, returned when in the template object */ - icon_hash?: string | null; - /** Splash hash */ - splash: string | null; - /** Discovery splash hash; only present for guilds with the "DISCOVERABLE" feature */ - discovery_splash: string | null; - /** Id of the owner */ - owner_id: string; - /** Total permissions for the user in the guild (excludes overwrites) */ - permissions?: string; - /** Id of afk channel */ - afk_channel_id: string | null; - /** The channel id that the widget will generate an invite to, or null if set to no invite */ - widget_channel_id?: string | null; - /** Roles in the guild */ - roles: DiscordRole[]; - /** Custom guild emojis */ - emojis: DiscordEmoji[]; - /** Application id of the guild creator if it is bot-created */ - application_id: string | null; - /** The id of the channel where guild notices such as welcome messages and boost events are posted */ - system_channel_id: string | null; - /** The id of the channel where community guilds can display rules and/or guidelines */ - rules_channel_id: string | null; - /** When this guild was joined at */ - joined_at?: string; - /** States of members currently in voice channels; lacks the guild_id key */ - voice_states?: Omit[]; - /** Users in the guild */ - members?: DiscordMember[]; - /** Channels in the guild */ - channels?: DiscordChannel[]; - // TODO: check if need to omit - /** All active threads in the guild that the current user has permission to view */ - threads?: DiscordChannel[]; - /** Presences of the members in the guild, will only include non-offline members if the size is greater than large threshold */ - presences?: Partial[]; - /** Banner hash */ - banner: string | null; - // TODO: Can be optimized to a number but is it worth it? - /** The preferred locale of a Community guild; used in server discovery and notices from Discord; defaults to "en-US" */ - preferred_locale: string; - /** The id of the channel where admins and moderators of Community guilds receive notices from Discord */ - public_updates_channel_id: string | null; - /** The welcome screen of a Community guild, shown to new members, returned in an Invite's guild object */ - welcome_screen?: DiscordWelcomeScreen; - /** Stage instances in the guild */ - stage_instances?: DiscordStageInstance[]; -} - -/** https://discord.com/developers/docs/topics/permissions#role-object-role-structure */ -export interface DiscordRole { - /** Role id */ - id: string; - /** If this role is showed separately in the user listing */ - hoist: boolean; - /** Permission bit set */ - permissions: string; - /** Whether this role is managed by an integration */ - managed: boolean; - /** Whether this role is mentionable */ - mentionable: boolean; - /** The tags this role has */ - tags?: DiscordRoleTags; - /** the role emoji hash */ - icon?: string; - /** Role name */ - name: string; - /** Integer representation of hexadecimal color code */ - color: number; - /** Position of this role */ - position: number; - /** role unicode emoji */ - unicode_emoji?: string; -} - -/** https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure */ -export interface DiscordRoleTags { - /** The id of the bot this role belongs to */ - bot_id?: string; - /** The id of the integration this role belongs to */ - integration_id?: string; - /** Whether this is the guild's premium subscriber role */ - premium_subscriber?: null; -} - -/** https://discord.com/developers/docs/resources/emoji#emoji-object-emoji-structure */ -export interface DiscordEmoji { - /** Emoji name (can only be null in reaction emoji objects) */ - name?: string; - - /** Emoji id */ - id?: string; - /** Roles allowed to use this emoji */ - roles?: string[]; - /** User that created this emoji */ - user?: DiscordUser; - /** Whether this emoji must be wrapped in colons */ - require_colons?: boolean; - /** Whether this emoji is managed */ - managed?: boolean; - /** Whether this emoji is animated */ - animated?: boolean; - /** Whether this emoji can be used, may be false due to loss of Server Boosts */ - available?: boolean; -} - -/** https://discord.com/developers/docs/resources/voice#voice-state-object-voice-state-structure */ -export interface DiscordVoiceState { - /** The session id for this voice state */ - session_id: string; - - /** The guild id this voice state is for */ - guild_id?: string; - /** The channel id this user is connected to */ - channel_id: string | null; - /** The user id this voice state is for */ - user_id: string; - /** The guild member this voice state is for */ - member?: DiscordMemberWithUser; - /** Whether this user is deafened by the server */ - deaf: boolean; - /** Whether this user is muted by the server */ - mute: boolean; - /** Whether this user is locally deafened */ - self_deaf: boolean; - /** Whether this user is locally muted */ - self_mute: boolean; - /** Whether this user is streaming using "Go Live" */ - self_stream?: boolean; - /** Whether this user's camera is enabled */ - self_video: boolean; - /** Whether this user is muted by the current user */ - suppress: boolean; - /** The time at which the user requested to speak */ - request_to_speak_timestamp: string | null; -} - -/** https://discord.com/developers/docs/resources/channel#channel-object */ -export interface DiscordChannel { - /** The type of channel */ - type: ChannelTypes; - /** The flags of the channel */ - flags?: ChannelFlags; - /** Sorting position of the channel */ - position?: number; - /** The name of the channel (1-100 characters) */ - name?: string; - /** The channel topic (0-1024 characters) */ - topic?: string | null; - /** The bitrate (in bits) of the voice channel */ - bitrate?: number; - /** The user limit of the voice channel */ - user_limit?: number; - /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `manage_messages` or `manage_channel`, are unaffected */ - rate_limit_per_user?: number; - /** Voice region id for the voice channel, automatic when set to null */ - rtc_region?: string | null; - /** The camera video quality mode of the voice channel, 1 when not present */ - video_quality_mode?: VideoQualityModes; - /** An approximate count of messages in a thread, stops counting at 50 */ - message_count?: number; - /** An approximate count of users in a thread, stops counting at 50 */ - member_count?: number; - /** Default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - default_auto_archive_duration?: number; - - /** The id of the channel */ - id: string; - /** The id of the guild */ - guild_id?: string; - /** Explicit permission overwrites for members and roles */ - permission_overwrites?: DiscordOverwrite[]; - /** Whether the channel is nsfw */ - nsfw?: boolean; - /** The id of the last message sent in this channel (may not point to an existing or valid message) */ - last_message_id?: string | null; - /** Id of the creator of the thread */ - owner_id?: string; - /** Application id of the group DM creator if it is bot-created */ - application_id?: string; - /** For guild channels: Id of the parent category for a channel (each parent category can contain up to 50 channels), for threads: id of the text channel this thread was created */ - parent_id?: string | null; - /** When the last pinned message was pinned. This may be null in events such as GUILD_CREATE when a message is not pinned. */ - last_pin_timestamp?: string | null; - /** Thread-specific fields not needed by other channels */ - thread_metadata?: DiscordThreadMetadata; - /** Thread member object for the current user, if they have joined the thread, only included on certain API endpoints */ - member?: DiscordThreadMember; - /** computed permissions for the invoking user in the channel, including overwrites, only included when part of the resolved data received on a application command interaction */ - permissions?: string; - /** When a thread is created this will be true on that channel payload for the thread. */ - newly_created?: boolean; - /** The recipients of the DM*/ - recipients?: DiscordUser[]; - - /** number of messages ever sent in a thread */ - total_message_sent?: number; - /** the set of tags that can be used in a GUILD_FORUM channel */ - available_tags?: DiscordForumTag[]; - /** the IDs of the set of tags that have been applied to a thread in a GUILD_FORUM channel */ - applied_tags?: string[]; - /** the emoji to show in the add reaction button on a thread in a GUILD_FORUM channel */ - default_reaction_emoji?: { emoji_id: string; emoji_name: string | null }; - /** the initial rate_limit_per_user to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update. */ - default_thread_rate_limit_per_user?: number; -} - -/** https://discord.com/developers/docs/topics/gateway#presence-update */ -export interface DiscordPresenceUpdate { - /** Either "idle", "dnd", "online", or "offline" */ - status: 'idle' | 'dnd' | 'online' | 'offline'; - /** The user presence is being updated for */ - user: DiscordUser; - /** id of the guild */ - guild_id: string; - /** User's current activities */ - activities: DiscordActivity[]; - /** User's platform-dependent status */ - client_status: DiscordClientStatus; -} - -export interface DiscordStatusUpdate { - /** User's current activities */ - activities: DiscordActivity[]; - /** Either "idle", "dnd", "online", or "offline" */ - status: 'idle' | 'dnd' | 'online' | 'offline'; -} - -/** https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-structure */ -export interface DiscordWelcomeScreen { - /** The server description shown in the welcome screen */ - description: string | null; - /** The channels shown in the welcome screen, up to 5 */ - welcome_channels: DiscordWelcomeScreenChannel[]; -} - -/** https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure */ -export interface DiscordWelcomeScreenChannel { - /** The description shown for the channel */ - description: string; - - /** The channel's id */ - channel_id: string; - /** The emoji id, if the emoji is custom */ - emoji_id: string | null; - /** The emoji name if custom, the unicode character if standard, or `null` if no emoji is set */ - emoji_name: string | null; -} - -/** https://discord.com/developers/docs/resources/stage-instance#auto-closing-stage-instance-structure */ -export interface DiscordStageInstance { - /** The topic of the Stage instance (1-120 characters) */ - topic: string; - /** The id of this Stage instance */ - id: string; - /** The guild id of the associated Stage channel */ - guild_id: string; - /** The id of the associated Stage channel */ - channel_id: string; - /** The id of the scheduled event for this Stage instance */ - guild_scheduled_event_id?: string; -} - -export interface DiscordThreadMetadata { - /** Whether the thread is archived */ - archived: boolean; - /** Duration in minutes to automatically archive the thread after recent activity */ - auto_archive_duration: 60 | 1440 | 4320 | 10080; - /** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */ - locked: boolean; - /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ - invitable?: boolean; - /** Timestamp when the thread's archive status was last changed, used for calculating recent activity */ - archive_timestamp: string; - /** Timestamp when the thread was created; only populated for threads created after 2022-01-09 */ - create_timestamp?: string | null; -} - -export interface DiscordThreadMemberBase { - /** Any user-thread settings, currently only used for notifications */ - flags: number; -} - -export interface DiscordThreadMember { - /** Any user-thread settings, currently only used for notifications */ - flags: number; - /** The id of the thread */ - id: string; - /** The id of the user */ - user_id: string; - /** The time the current user last joined the thread */ - join_timestamp: string; -} - -export interface DiscordThreadMemberGuildCreate { - /** Any user-thread settings, currently only used for notifications */ - flags: number; - /** The time the current user last joined the thread */ - join_timestamp: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object */ -export interface DiscordActivity { - /** The activity's name */ - name: string; - /** Activity type */ - type: ActivityTypes; - /** Stream url, is validated when type is 1 */ - url?: string | null; - /** Unix timestamp of when the activity was added to the user's session */ - created_at: number; - /** What the player is currently doing */ - details?: string | null; - /** The user's current party status */ - state?: string | null; - /** Whether or not the activity is an instanced game session */ - instance?: boolean; - /** Activity flags `OR`d together, describes what the payload includes */ - flags?: number; - /** Unix timestamps for start and/or end of the game */ - timestamps?: DiscordActivityTimestamps; - /** Application id for the game */ - application_id?: string; - /** The emoji used for a custom status */ - emoji?: DiscordActivityEmoji | null; - /** Information for the current party of the player */ - party?: DiscordActivityParty; - /** Images for the presence and their hover texts */ - assets?: DiscordActivityAssets; - /** Secrets for Rich Presence joining and spectating */ - secrets?: DiscordActivitySecrets; - /** The custom buttons shown in the Rich Presence (max 2) */ - buttons?: DiscordActivityButton[]; -} - -/** https://discord.com/developers/docs/topics/gateway#client-status-object */ -export interface DiscordClientStatus { - /** The user's status set for an active desktop (Windows, Linux, Mac) application session */ - desktop?: string; - /** The user's status set for an active mobile (iOS, Android) application session */ - mobile?: string; - /** The user's status set for an active web (browser, bot account) application session */ - web?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-timestamps */ -export interface DiscordActivityTimestamps { - /** Unix time (in milliseconds) of when the activity started */ - start?: number; - /** Unix time (in milliseconds) of when the activity ends */ - end?: number; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-emoji */ -export interface DiscordActivityEmoji { - /** The name of the emoji */ - name: string; - /** Whether this emoji is animated */ - animated?: boolean; - /** The id of the emoji */ - id?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-party */ -export interface DiscordActivityParty { - /** Used to show the party's current and maximum size */ - size?: [currentSize: number, maxSize: number]; - /** The id of the party */ - id?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-assets */ -export interface DiscordActivityAssets { - /** Text displayed when hovering over the large image of the activity */ - large_text?: string; - /** Text displayed when hovering over the small image of the activity */ - small_text?: string; - /** The id for a large asset of the activity, usually a snowflake */ - large_image?: string; - /** The id for a small asset of the activity, usually a snowflake */ - small_image?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-secrets */ -export interface DiscordActivitySecrets { - /** The secret for joining a party */ - join?: string; - /** The secret for spectating a game */ - spectate?: string; - /** The secret for a specific instanced match */ - match?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#activity-object-activity-buttons */ -export interface DiscordActivityButton { - /** The text shown on the button (1-32 characters) */ - label: string; - /** The url opened when clicking the button (1-512 characters) */ - url: string; -} - -export interface DiscordOverwrite { - /** Either 0 (role) or 1 (member) */ - type: OverwriteTypes; - /** Role or user id */ - id: string; - /** Permission bit set */ - allow?: string; - /** Permission bit set */ - deny?: string; -} - -export interface DiscordMemberWithUser extends DiscordMember { - /** The user object for this member */ - user: DiscordUser; -} - -/** https://discord.com/developers/docs/resources/channel#message-object */ -export interface DiscordMessage { - /** id of the message */ - id: string; - /** id of the channel the message was sent in */ - channel_id: string; - /** - * id of the guild the message was sent in - * Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages. - */ - guild_id?: string; - /** - * The author of this message (not guaranteed to be a valid user) - * Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object. - */ - author: DiscordUser; - /** - * Member properties for this message's author - * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. - */ - member?: DiscordMember; - /** Contents of the message */ - content?: string; - /** When this message was sent */ - timestamp: string; - /** When this message was edited (or null if never) */ - edited_timestamp: string | null; - /** Whether this was a TTS message */ - tts: boolean; - /** Whether this message mentions everyone */ - mention_everyone: boolean; - /** - * Users specifically mentioned in the message - * Note: The user objects in the mentions array will only have the partial member field present in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. - */ - mentions?: (DiscordUser & { member?: Partial })[]; - /** Roles specifically mentioned in this message */ - mention_roles?: string[]; - /** - * Channels specifically mentioned in this message - * Note: Not all channel mentions in a message will appear in `mention_channels`. Only textual channels that are visible to everyone in a lurkable guild will ever be included. Only crossposted messages (via Channel Following) currently include `mention_channels` at all. If no mentions in the message meet these requirements, this field will not be sent. - */ - mention_channels?: DiscordChannelMention[]; - /** Any attached files */ - attachments: DiscordAttachment[]; - /** Any embedded content */ - embeds: DiscordEmbed[]; - /** Reactions to the message */ - reactions?: DiscordReaction[]; - /** Used for validating a message was sent */ - nonce?: number | string; - /** Whether this message is pinned */ - pinned: boolean; - /** If the message is generated by a webhook, this is the webhook's id */ - webhook_id?: string; - /** Type of message */ - type: MessageTypes; - /** Sent with Rich Presence-related chat embeds */ - activity?: DiscordMessageActivity; - /** Sent with Rich Presence-related chat embeds */ - application?: Partial; - /** if the message is an Interaction or application-owned webhook, this is the id of the application */ - application_id?: string; - /** Data showing the source of a crossposted channel follow add, pin or reply message */ - message_reference?: Omit; - /** Message flags combined as a bitfield */ - flags?: number; - /** - * The stickers sent with the message (bots currently can only receive messages with stickers, not send) - * @deprecated - */ - stickers?: DiscordSticker[]; - /** - * The message associated with the `message_reference` - * Note: This field is only returned for messages with a `type` of `19` (REPLY). If the message is a reply but the `referenced_message` field is not present, the backend did not attempt to fetch the message that was being replied to, so its state is unknown. If the field exists but is null, the referenced message was deleted. - */ - referenced_message?: DiscordMessage; - /** Sent if the message is a response to an Interaction */ - interaction?: DiscordMessageInteraction; - /** The thread that was started from this message, includes thread member object */ - thread?: Omit & { member: DiscordThreadMember }; - /** The components related to this message */ - components?: DiscordMessageComponents; - /** Sent if the message contains stickers */ - sticker_items?: DiscordStickerItem[]; - /** A generally increasing integer (there may be gaps or duplicates) */ - position?: number; -} - -/** https://discord.com/developers/docs/resources/channel#channel-mention-object */ -export interface DiscordChannelMention { - /** id of the channel */ - id: string; - /** id of the guild containing the channel */ - guild_id: string; - /** The type of channel */ - type: number; - /** The name of the channel */ - name: string; -} - -/** https://discord.com/developers/docs/resources/channel#reaction-object */ -export interface DiscordReaction { - /** Times this emoji has been used to react */ - count: number; - /** Whether the current user reacted using this emoji */ - me: boolean; - /** Emoji information */ - emoji: Partial; -} - -/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure */ -export interface DiscordMessageActivity { - /** Type of message activity */ - type: MessageActivityTypes; - /** `party_id` from a Rich Presence event */ - party_id?: string; -} - -/** https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure */ -export interface DiscordMessageReference { - /** id of the originating message */ - message_id?: string; - /** - * id of the originating message's channel - * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. - */ - channel_id?: string; - /** id of the originating message's guild */ - guild_id?: string; - /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ - fail_if_not_exists: boolean; -} - -/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure */ -export interface DiscordSticker { - /** [Id of the sticker](https://discord.com/developers/docs/reference#image-formatting) */ - id: string; - /** Id of the pack the sticker is from */ - pack_id?: string; - /** Name of the sticker */ - name: string; - /** Description of the sticker */ - description: string; - /** a unicode emoji representing the sticker's expression */ - tags: string; - /** [type of sticker](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types) */ - type: StickerTypes; - /** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */ - format_type: StickerFormatTypes; - /** Whether or not the sticker is available */ - available?: boolean; - /** Id of the guild that owns this sticker */ - guild_id?: string; - /** The user that uploaded the sticker */ - user?: DiscordUser; - /** A sticker's sort order within a pack */ - sort_value?: number; -} - -/** https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure */ -export interface DiscordMessageInteraction { - /** Id of the interaction */ - id: string; - /** The type of interaction */ - type: InteractionTypes; - /** The name of the ApplicationCommand */ - name: string; - /** The user who invoked the interaction */ - user: DiscordUser; - /** The member who invoked the interaction in the guild */ - member?: Partial; -} - -export type DiscordMessageComponents = DiscordActionRow[]; - -/** https://discord.com/developers/docs/interactions/message-components#actionrow */ -export interface DiscordActionRow { - /** Action rows are a group of buttons. */ - type: 1; - /** The components in this row */ - components: - | [ - | DiscordSelectMenuComponent - | DiscordButtonComponent - | DiscordInputTextComponent - ] - | [DiscordButtonComponent, DiscordButtonComponent] - | [ - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent - ] - | [ - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent - ] - | [ - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent - ]; -} - -export interface DiscordSelectMenuComponent { - type: MessageComponentTypes.SelectMenu | - MessageComponentTypes.RoleSelect | - MessageComponentTypes.UserSelect | - MessageComponentTypes.MentionableSelect | - MessageComponentTypes.ChannelSelect; - /** A custom identifier for this component. Maximum 100 characters. */ - custom_id: string; - /** A custom placeholder text if nothing is selected. Maximum 150 characters. */ - placeholder?: string; - /** The minimum number of items that must be selected. Default 1. Between 1-25. */ - min_values?: number; - /** The maximum number of items that can be selected. Default 1. Between 1-25. */ - max_values?: number; - /** The choices! Maximum of 25 items. */ - options: DiscordSelectOption[]; - /** Whether or not this select menu is disabled */ - disabled?: boolean; -} - -export interface DiscordSelectOption { - /** The user-facing name of the option. Maximum 25 characters. */ - label: string; - /** The dev-defined value of the option. Maximum 100 characters. */ - value: string; - /** An additional description of the option. Maximum 50 characters. */ - description?: string; - /** The id, name, and animated properties of an emoji. */ - emoji?: { - /** Emoji id */ - id?: string; - /** Emoji name */ - name?: string; - /** Whether this emoji is animated */ - animated?: boolean; - }; - /** Will render this option as already-selected by default. */ - default?: boolean; -} - -/** https://discord.com/developers/docs/interactions/message-components#buttons-button-object */ -export interface DiscordButtonComponent { - /** All button components have type 2 */ - type: MessageComponentTypes.Button; - /** for what the button says (max 80 characters) */ - label: string; - /** a dev-defined unique string sent on click (max 100 characters). type 5 Link buttons can not have a custom_id */ - custom_id?: string; - /** For different styles/colors of the buttons */ - style: ButtonStyles; - /** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */ - emoji?: { - /** Emoji id */ - id?: string; - /** Emoji name */ - name?: string; - /** Whether this emoji is animated */ - animated?: boolean; - }; - /** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */ - url?: string; - /** Whether or not this button is disabled */ - disabled?: boolean; -} - -/** https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure */ -export interface DiscordInputTextComponent { - /** InputText Component is of type 3 */ - type: MessageComponentTypes.InputText; - /** The style of the InputText */ - style: TextStyles; - /** The customId of the InputText */ - custom_id: string; - /** The label of the InputText */ - label: string; - /** The placeholder of the InputText */ - placeholder?: string; - /** The minimum length of the text the user has to provide */ - min_length?: number; - /** The maximum length of the text the user has to provide */ - max_length?: number; - /** Whether or not this input is required. */ - required?: boolean; - /** Pre-filled value for input text. */ - value?: string; -} - -/** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */ -export interface DiscordStickerItem { - /** Id of the sticker */ - id: string; - /** Name of the sticker */ - name: string; - /** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */ - format_type: StickerFormatTypes; -} - -/** https://discord.com/developers/docs/resources/sticker#sticker-pack-object-sticker-pack-structure */ -export interface DiscordStickerPack { - /** id of the sticker pack */ - id: string; - /** the stickers in the pack */ - stickers: DiscordSticker[]; - /** name of the sticker pack */ - name: string; - /** id of the pack's SKU */ - sku_id: string; - /** id of a sticker in the pack which is shown as the pack's icon */ - cover_sticker_id?: string; - /** description of the sticker pack */ - description: string; - /** id of the sticker pack's [banner image](https://discord.com/developers/docs/reference#image-formatting) */ - banner_asset_id?: string; -} - -export interface DiscordInteraction { - /** Id of the interaction */ - id: string; - /** Id of the application this interaction is for */ - application_id: string; - /** The type of interaction */ - type: InteractionTypes; - /** The guild it was sent from */ - guild_id?: string; - /** The channel it was sent from */ - channel_id?: string; - /** Guild member data for the invoking user, including permissions */ - member?: DiscordInteractionMember; - /** User object for the invoking user, if invoked in a DM */ - user?: DiscordUser; - /** A continuation token for responding to the interaction */ - token: string; - /** Read-only property, always `1` */ - version: 1; - /** For the message the button was attached to */ - message?: DiscordMessage; - /** the command data payload */ - data?: DiscordInteractionData; - /** The guild's preferred locale, if invoked in a guild */ - guild_locale?: string; - /** Bitwise set of permissions the app or bot has within the channel the interaction was sent from */ - app_permissions?: string; - /** Selected language of the invoking user */ - locale?: string; -} - -/** https://discord.com/developers/docs/resources/guild#guild-member-object */ -export interface DiscordInteractionMember extends DiscordMemberWithUser { - /** Total permissions of the member in the channel, including overwrites, returned when in the interaction object */ - permissions: string; -} - -export interface DiscordInteractionData { - /** The type of component */ - component_type?: MessageComponentTypes; - /** The custom id provided for this component. */ - custom_id?: string; - /** The components if its a Modal Submit interaction. */ - components?: DiscordMessageComponents; - /** The values chosen by the user. */ - values?: string[]; - /** The Id of the invoked command */ - id: string; - /** The name of the invoked command */ - name: string; - /** the type of the invoked command */ - type: ApplicationCommandTypes; - /** Converted users + roles + channels + attachments */ - resolved?: { - /** The Ids and Message objects */ - messages?: Record; - /** The Ids and User objects */ - users?: Record; - /** The Ids and partial Member objects */ - members?: Record< - string, - Omit - >; - /** The Ids and Role objects */ - roles?: Record; - /** The Ids and partial Channel objects */ - channels?: Record< - string, - Pick - >; - /** The ids and attachment objects */ - attachments: Record; - }; - /** The params + values from the user */ - options?: DiscordInteractionDataOption[]; - /** The target id if this is a context menu command. */ - target_id?: string; - /** the id of the guild the command is registered to */ - guild_id?: string; -} - -export type DiscordInteractionDataOption = { - /** Name of the parameter */ - name: string; - /** Value of application command option type */ - type: ApplicationCommandOptionTypes; - /** Value of the option resulting from user input */ - value?: - | string - | boolean - | number - | DiscordMember - | DiscordChannel - | DiscordRole; - /** Present if this option is a group or subcommand */ - options?: DiscordInteractionDataOption[]; - /** `true` if this option is the currently focused option for autocomplete */ - focused?: boolean; -}; - -export interface DiscordInteractionDataResolved { - /** The Ids and Message objects */ - messages?: Record; - /** The Ids and User objects */ - users?: Record; - /** The Ids and partial Member objects */ - members?: Record< - string, - Omit - >; - /** The Ids and Role objects */ - roles?: Record; - /** The Ids and partial Channel objects */ - channels?: Record< - string, - Pick - >; - /** The Ids and attachments objects */ - attachments?: Record; -} - -export interface DiscordListActiveThreads { - /** The active threads */ - threads: DiscordChannel[]; - /** A thread member object for each returned thread the current user has joined */ - members: DiscordThreadMember[]; -} - -export interface DiscordListArchivedThreads extends DiscordListActiveThreads { - /** Whether there are potentially additional threads that could be returned on a subsequent call */ - has_more: boolean; -} - -export interface DiscordThreadListSync { - /** The id of the guild */ - guild_id: string; - /** The parent channel ids whose threads are being synced. If omitted, then threads were synced for the entire guild. This array may contain channelIds that have no active threads as well, so you know to clear that data */ - channel_ids?: string[]; - /** All active threads in the given channels that the current user can access */ - threads: DiscordChannel[]; - /** All thread member objects from the synced threads for the current user, indicating which threads the current user has been added to */ - members: DiscordThreadMember[]; -} - -/** https://discord.com/developers/docs/resources/audit-log#audit-log-object */ -export interface DiscordAuditLog { - /** List of webhooks found in the audit log */ - webhooks: DiscordWebhook[]; - /** List of users found in the audit log */ - users: DiscordUser[]; - /** List of audit log entries, sorted from most to least recent */ - audit_log_entries: DiscordAuditLogEntry[]; - /** List of partial integration objects */ - integrations: ({ - type: 'youtube' | 'twitch' | 'discord'; - id: string; - name: string; - account: DiscordIntegrationAccount; - } & Partial)[]; - /** - * List of threads found in the audit log. - * Threads referenced in `THREAD_CREATE` and `THREAD_UPDATE` events are included in the threads map since archived threads might not be kept in memory by clients. - */ - threads: DiscordChannel[]; - /** List of guild scheduled events found in the audit log */ - guild_scheduled_events?: DiscordScheduledEvent[]; - /** List of auto moderation rules referenced in the audit log */ - auto_moderation_rules?: DiscordAutoModerationRule[]; -} - -export interface DiscordAutoModerationRule { - /** The id of this rule */ - id: string; - /** The guild id */ - guild_id: string; - /** The name of the rule */ - name: string; - /** The id of the user who created this rule. */ - creator_id: string; - /** Indicates in what event context a rule should be checked. */ - event_type: AutoModerationEventTypes; - /** The type of trigger for this rule */ - trigger_type: AutoModerationTriggerTypes; - /** The metadata used to determine whether a rule should be triggered. */ - trigger_metadata: DiscordAutoModerationRuleTriggerMetadata; - /** Actions which will execute whenever a rule is triggered. */ - actions: DiscordAutoModerationAction[]; - /** Whether the rule is enabled. */ - enabled: boolean; - /** The role ids that are whitelisted. Max 20. */ - exempt_roles: string[]; - /** The channel ids that are whitelisted. Max 50. */ - exempt_channels: string[]; -} - -export enum AutoModerationEventTypes { - /** When a user sends a message */ - MessageSend = 1, -} - -export enum AutoModerationTriggerTypes { - Keyword = 1, - HarmfulLink, - Spam, - KeywordPreset, -} - -export interface DiscordAutoModerationRuleTriggerMetadata { - // TODO: discord is considering renaming this before release - /** The keywords needed to match. Only present when TriggerType.Keyword */ - keyword_filter?: string[]; - /** The pre-defined lists of words to match from. Only present when TriggerType.KeywordPreset */ - presets?: DiscordAutoModerationRuleTriggerMetadataPresets[]; - /** substrings which will be exempt from triggering the preset trigger type */ - allow_list?: string[]; -} - -export enum DiscordAutoModerationRuleTriggerMetadataPresets { - /** Words that may be considered forms of swearing or cursing */ - Profanity = 1, - /** Words that refer to sexually explicit behavior or activity */ - SexualContent, - /** Personal insults or words that may be considered hate speech */ - Slurs, -} - -export interface DiscordAutoModerationAction { - /** The type of action to take when a rule is triggered */ - type: AutoModerationActionType; - /** additional metadata needed during execution for this specific action type */ - metadata: DiscordAutoModerationActionMetadata; -} - -export enum AutoModerationActionType { - /** Blocks the content of a message according to the rule */ - BlockMessage = 1, - /** Logs user content to a specified channel */ - SendAlertMessage, - /** Times out user for specified duration */ - Timeout, -} - -export interface DiscordAutoModerationActionMetadata { - /** The id of channel to which user content should be logged. Only in ActionType.SendAlertMessage */ - channel_id?: string; - /** Timeout duration in seconds maximum of 2419200 seconds (4 weeks). Only supported for TriggerType.Keyword && Only in ActionType.Timeout */ - duration_seconds?: number; -} - -export interface DiscordAutoModerationActionExecution { - /** The id of the guild */ - guild_id: string; - /** The id of the rule that was executed */ - rule_id: string; - /** The id of the user which generated the content which triggered the rule */ - user_id: string; - /** The content from the user */ - content: string; - /** Action which was executed */ - action: DiscordAutoModerationAction; - /** The trigger type of the rule that was executed. */ - rule_trigger_type: AutoModerationTriggerTypes; - /** The id of the channel in which user content was posted */ - channel_id?: string | null; - /** The id of the message. Will not exist if message was blocked by automod or content was not part of any message */ - message_id?: string | null; - /** The id of any system auto moderation messages posted as a result of this action */ - alert_system_message_id?: string | null; - /** The word or phrase that triggerred the rule. */ - matched_keyword: string | null; - /** The substring in content that triggered rule */ - matched_content: string | null; -} - -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure */ -export interface DiscordAuditLogEntry { - /** ID of the affected entity (webhook, user, role, etc.) */ - target_id: string | null; - /** Changes made to the `target_id` */ - changes?: DiscordAuditLogChange[]; - /** User or app that made the changes */ - user_id: string | null; - /** ID of the entry */ - id: string; - /** Type of action that occurred */ - action_type: AuditLogEvents; - /** Additional info for certain event types */ - options?: DiscordOptionalAuditEntryInfo; - /** Reason for the change (1-512 characters) */ - reason?: string; -} - -/** https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-structure */ -export type DiscordAuditLogChange = - | { - new_value: string; - old_value: string; - key: - | 'name' - | 'description' - | 'discovery_splash_hash' - | 'banner_hash' - | 'preferred_locale' - | 'rules_channel_id' - | 'public_updates_channel_id' - | 'icon_hash' - | 'image_hash' - | 'splash_hash' - | 'owner_id' - | 'region' - | 'afk_channel_id' - | 'vanity_url_code' - | 'widget_channel_id' - | 'system_channel_id' - | 'topic' - | 'application_id' - | 'permissions' - | 'allow' - | 'deny' - | 'code' - | 'channel_id' - | 'inviter_id' - | 'nick' - | 'avatar_hash' - | 'id' - | 'location' - | 'command_id'; - } - | { - new_value: number; - old_value: number; - key: - | 'afk_timeout' - | 'mfa_level' - | 'verification_level' - | 'explicit_content_filter' - | 'default_message_notifications' - | 'prune_delete_days' - | 'position' - | 'bitrate' - | 'rate_limit_per_user' - | 'color' - | 'max_uses' - | 'uses' - | 'max_age' - | 'expire_behavior' - | 'expire_grace_period' - | 'user_limit' - | 'privacy_level' - | 'auto_archive_duration' - | 'default_auto_archive_duration' - | 'entity_type' - | 'status' - | 'communication_disabled_until'; - } - | { - new_value: Partial[]; - old_value?: Partial[]; - key: '$add' | '$remove'; - } - | { - new_value: boolean; - old_value: boolean; - key: - | 'widget_enabled' - | 'nsfw' - | 'hoist' - | 'mentionable' - | 'temporary' - | 'deaf' - | 'mute' - | 'enable_emoticons' - | 'archived' - | 'locked' - | 'invitable'; - } - | { - new_value: DiscordOverwrite[]; - old_value: DiscordOverwrite[]; - key: 'permission_overwrites'; - } - | { - new_value: string | number; - old_value: string | number; - key: 'type'; - }; - -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info */ -export interface DiscordOptionalAuditEntryInfo { - /** - * Number of days after which inactive members were kicked. - * - * Event types: `MEMBER_PRUNE` - */ - delete_member_days: string; - /** - * Number of members removed by the prune. - * - * Event types: `MEMBER_PRUNE` - */ - members_removed: string; - /** - * Channel in which the entities were targeted. - * - * Event types: `MEMBER_MOVE`, `MESSAGE_PIN`, `MESSAGE_UNPIN`, `MESSAGE_DELETE`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` - */ - channel_id: string; - /** - * ID of the message that was targeted. - * - * Event types: `MESSAGE_PIN`, `MESSAGE_UNPIN`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` - */ - message_id: string; - /** - * Number of entities that were targeted. - * - * Event types: `MESSAGE_DELETE`, `MESSAGE_BULK_DELETE`, `MEMBER_DISCONNECT`, `MEMBER_MOVE` - */ - count: string; - /** - * ID of the overwritten entity. - * - * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` - */ - id: string; - /** - * Type of overwritten entity - "0", for "role", or "1" for "member". - * - * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` - */ - type: string; - /** - * Name of the role if type is "0" (not present if type is "1"). - * - * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` - */ - role_name: string; - /** - * ID of the app whose permissions were targeted. - * - * Event types: `APPLICATION_COMMAND_PERMISSION_UPDATE` - */ - application_id: string; -} - -export interface DiscordScheduledEvent { - /** the id of the scheduled event */ - id: string; - /** the guild id which the scheduled event belongs to */ - guild_id: string; - /** the channel id in which the scheduled event will be hosted if specified */ - channel_id: string | null; - /** the id of the user that created the scheduled event */ - creator_id?: string | null; - /** the name of the scheduled event */ - name: string; - /** the description of the scheduled event */ - description?: string; - /** the time the scheduled event will start */ - scheduled_start_time: string; - /** the time the scheduled event will end if it does end. */ - scheduled_end_time: string | null; - /** the privacy level of the scheduled event */ - privacy_level: ScheduledEventPrivacyLevel; - /** the status of the scheduled event */ - status: ScheduledEventStatus; - /** the type of hosting entity associated with a scheduled event */ - entity_type: ScheduledEventEntityType; - /** any additional id of the hosting entity associated with event */ - entity_id: string | null; - /** the entity metadata for the scheduled event */ - entity_metadata: DiscordScheduledEventEntityMetadata | null; - /** the user that created the scheduled event */ - creator?: DiscordUser; - /** the number of users subscribed to the scheduled event */ - user_count?: number; - /** the cover image hash of the scheduled event */ - image?: string | null; -} - -export interface DiscordScheduledEventEntityMetadata { - /** location of the event */ - location?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#get-gateway-bot */ -export interface DiscordGetGatewayBot { - /** The WSS URL that can be used for connecting to the gateway */ - url: string; - /** The recommended number of shards to use when connecting */ - shards: number; - /** Information on the current session start limit */ - session_start_limit: DiscordSessionStartLimit; -} - -/** https://discord.com/developers/docs/topics/gateway#session-start-limit-object */ -export interface DiscordSessionStartLimit { - /** The total number of session starts the current user is allowed */ - total: number; - /** The remaining number of session starts the current user is allowed */ - remaining: number; - /** The number of milliseconds after which the limit resets */ - reset_after: number; - /** The number of identify requests allowed per 5 seconds */ - max_concurrency: number; -} - -/** https://discord.com/developers/docs/resources/invite#invite-metadata-object */ -export interface DiscordInviteMetadata extends DiscordInvite { - /** Number of times this invite has been used */ - uses: number; - /** Max number of times this invite can be used */ - max_uses: number; - /** Duration (in seconds) after which the invite expires */ - max_age: number; - /** Whether this invite only grants temporary membership */ - temporary: boolean; - /** When this invite was created */ - created_at: string; -} - -/** https://discord.com/developers/docs/resources/invite#invite-object */ -export interface DiscordInvite { - /** The invite code (unique Id) */ - code: string; - /** The guild this invite is for */ - guild?: Partial; - /** The channel this invite is for */ - channel: Partial | null; - /** The user who created the invite */ - inviter?: DiscordUser; - /** The type of target for this voice channel invite */ - target_type?: TargetTypes; - /** The target user for this invite */ - target_user?: DiscordUser; - /** The embedded application to open for this voice channel embedded application invite */ - target_application?: Partial; - /** Approximate count of online members (only present when target_user is set) */ - approximate_presence_count?: number; - /** Approximate count of total members */ - approximate_member_count?: number; - /** The expiration date of this invite, returned from the `GET /invites/` endpoint when `with_expiration` is `true` */ - expires_at?: string | null; - /** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */ - stage_instance?: DiscordInviteStageInstance; - /** guild scheduled event data */ - guild_scheduled_event?: DiscordScheduledEvent; -} - -export interface DiscordInviteStageInstance { - /** The members speaking in the Stage */ - members: Partial[]; - /** The number of users in the Stage */ - participant_count: number; - /** The number of users speaking in the Stage */ - speaker_count: number; - /** The topic of the Stage instance (1-120 characters) */ - topic: string; -} - -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure */ -export interface DiscordApplicationCommand { - /** Unique ID of command */ - id: string; - /** Type of command, defaults to `ApplicationCommandTypes.ChatInput` */ - type?: ApplicationCommandTypes; - /** ID of the parent application */ - application_id: string; - /** Guild id of the command, if not global */ - guild_id?: string; - /** - * Name of command, 1-32 characters. - * `ApplicationCommandTypes.ChatInput` command names must match the following regex `^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$` with the unicode flag set. - * If there is a lowercase variant of any letters used, you must use those. - * Characters with no lowercase variants and/or uncased letters are still allowed. - * ApplicationCommandTypes.User` and `ApplicationCommandTypes.Message` commands may be mixed case and can include spaces. - */ - name: string; - /** Localization object for `name` field. Values follow the same restrictions as `name` */ - name_localizations?: Localization | null; - /** Description for `ApplicationCommandTypes.ChatInput` commands, 1-100 characters. Empty string for `ApplicationCommandTypes.User` and `ApplicationCommandTypes.Message` commands */ - description: string; - /** Localization object for `description` field. Values follow the same restrictions as `description` */ - description_localizations?: Localization | null; - /** Parameters for the command, max of 25 */ - options?: DiscordApplicationCommandOption[]; - /** Set of permissions represented as a bit set */ - default_member_permissions: string | null; - /** Indicates whether the command is available in DMs with the app, only for globally-scoped commands. By default, commands are visible. */ - dm_permission?: boolean; - /** Auto incrementing version identifier updated during substantial record changes */ - version: string; -} - -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure */ -export interface DiscordApplicationCommandOption { - /** Type of option */ - type: ApplicationCommandOptionTypes; - /** - * Name of command, 1-32 characters. - * `ApplicationCommandTypes.ChatInput` command names must match the following regex `^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$` with the unicode flag set. - * If there is a lowercase variant of any letters used, you must use those. - * Characters with no lowercase variants and/or uncased letters are still allowed. - * ApplicationCommandTypes.User` and `ApplicationCommandTypes.Message` commands may be mixed case and can include spaces. - */ - name: string; - /** Localization object for the `name` field. Values follow the same restrictions as `name` */ - name_localizations?: Localization | null; - /** 1-100 character description */ - description: string; - /** Localization object for the `description` field. Values follow the same restrictions as `description` */ - description_localizations?: Localization | null; - /** If the parameter is required or optional--default `false` */ - required?: boolean; - /** Choices for the option types `ApplicationCommandOptionTypes.String`, `ApplicationCommandOptionTypes.Integer`, and `ApplicationCommandOptionTypes.Number`, from which the user can choose, max 25 */ - choices?: DiscordApplicationCommandOptionChoice[]; - /** If the option is a subcommand or subcommand group type, these nested options will be the parameters */ - options?: DiscordApplicationCommandOption[]; - /** - * If autocomplete interactions are enabled for this option. - * - * Only available for `ApplicationCommandOptionTypes.String`, `ApplicationCommandOptionTypes.Integer` and `ApplicationCommandOptionTypes.Number` option types - */ - autocomplete?: boolean; - /** If the option is a channel type, the channels shown will be restricted to these types */ - channel_types?: ChannelTypes[]; - /** If the option type is `ApplicationCommandOptionTypes.Integer` or `ApplicationCommandOptionTypes.Number`, the minimum permitted value */ - min_value?: number; - /** If the option type is `ApplicationCommandOptionTypes.Integer` or `ApplicationCommandOptionTypes.Number`, the maximum permitted value */ - max_value?: number; -} - -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure */ -export interface DiscordApplicationCommandOptionChoice { - /** 1-100 character choice name */ - name: string; - /** Localization object for the `name` field. Values follow the same restrictions as `name` */ - name_localizations?: Localization | null; - /** Value for the choice, up to 100 characters if string */ - value: string | number; -} - -/** https://discord.com/developers/docs/interactions/slash-commands#guildapplicationcommandpermissions */ -export interface DiscordGuildApplicationCommandPermissions { - /** ID of the command or the application ID. When the `id` field is the application ID instead of a command ID, the permissions apply to all commands that do not contain explicit overwrites. */ - id: string; - /** ID of the application the command belongs to */ - application_id: string; - /** ID of the guild */ - guild_id: string; - /** Permissions for the command in the guild, max of 100 */ - permissions: DiscordApplicationCommandPermissions[]; -} - -/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandpermissions */ -export interface DiscordApplicationCommandPermissions { - /** ID of the role, user, or channel. It can also be a permission constant */ - id: string; - /** ApplicationCommandPermissionTypes.Role, ApplicationCommandPermissionTypes.User, or ApplicationCommandPermissionTypes.Channel */ - type: ApplicationCommandPermissionTypes; - /** `true` to allow, `false`, to disallow */ - permission: boolean; -} - -/** https://discord.com/developers/docs/resources/guild#get-guild-widget-example-get-guild-widget */ -export interface DiscordGuildWidget { - id: string; - name: string; - instant_invite: string; - channels: { - id: string; - name: string; - position: number; - }[]; - members: { - id: string; - username: string; - discriminator: string; - avatar?: string | null; - status: string; - avatar_url: string; - }[]; - presence_count: number; -} - -/** https://discord.com/developers/docs/resources/guild#guild-preview-object */ -export interface DiscordGuildPreview { - /** Guild id */ - id: string; - /** Guild name (2-100 characters) */ - name: string; - /** Icon hash */ - icon: string | null; - /** Splash hash */ - splash: string | null; - /** Discovery splash hash */ - discovery_splash: string | null; - /** Custom guild emojis */ - emojis: DiscordEmoji[]; - /** Enabled guild features */ - features: GuildFeatures[]; - /** Approximate number of members in this guild */ - approximate_member_count: number; - /** Approximate number of online members in this guild */ - approximate_presence_count: number; - /** The description for the guild, if the guild is discoverable */ - description: string | null; - /** Custom guild stickers */ - stickers: DiscordSticker[]; -} - -export interface DiscordDiscoveryCategory { - /** Numeric id of the category */ - id: number; - /** The name of this category, in multiple languages */ - name: DiscordDiscoveryName; - /** Whether this category can be set as a guild's primary category */ - is_primary: boolean; -} - -export interface DiscordDiscoveryName { - /** The name in English */ - default: string; - /** The name in other languages */ - localizations?: Record; -} - -export interface DiscordDiscoveryMetadata { - /** The guild Id */ - guild_id: string; - /** The id of the primary discovery category set for this guild */ - primary_category_id: number; - /** Up to 10 discovery search keywords set for this guild */ - keywords: string[] | null; - /** Whether guild info is shown when custom emojis from this guild are clicked */ - emoji_discoverability_enabled: boolean; - /** When the server's partner application was accepted or denied, for applications via Server Settings */ - partner_actioned_timestamp: string | null; - /** When the server applied for partnership, if it has a pending application */ - partner_application_timestamp: string | null; - /** Ids of up to 5 discovery subcategories set for this guild */ - category_ids: number[]; -} - -/** https://discord.com/developers/docs/resources/channel#followed-channel-object */ -export interface DiscordFollowedChannel { - /** Source message id */ - channel_id: string; - /** Created target webhook id */ - webhook_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#payloads-gateway-payload-structure */ -export interface DiscordGatewayPayload { - /** opcode for the payload */ - op: number; - /** Event data */ - d: unknown | null; - /** Sequence number, used for resuming sessions and heartbeats */ - s: number | null; - /** The event name for this payload */ - t: GatewayEventNames | null; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-members-chunk */ -export interface DiscordGuildMembersChunk { - /** The id of the guild */ - guild_id: string; - /** Set of guild members */ - members: DiscordMemberWithUser[]; - /** The chunk index in the expected chunks for this response (0 <= chunk_index < chunk_count) */ - chunk_index: number; - /** The total number of expected chunks for this response */ - chunk_count: number; - /** If passing an invalid id to `REQUEST_GUILD_MEMBERS`, it will be returned here */ - not_found?: string[]; - /** If passing true to `REQUEST_GUILD_MEMBERS`, presences of the returned members will be here */ - presences?: DiscordPresenceUpdate[]; - /** The nonce used in the Guild Members Request */ - nonce?: string; -} - -export interface DiscordComponent { - /** component type */ - type: MessageComponentTypes; - /** a developer-defined identifier for the component, max 100 characters */ - custom_id?: string; - /** whether the component is disabled, default false */ - disabled?: boolean; - /** For different styles/colors of the buttons */ - style?: ButtonStyles | TextStyles; - /** text that appears on the button (max 80 characters) */ - label?: string; - /** the dev-define value of the option, max 100 characters for select or 4000 for input. */ - value?: string; - /** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */ - emoji?: { - /** Emoji id */ - id?: string; - /** Emoji name */ - name?: string; - /** Whether this emoji is animated */ - animated?: boolean; - }; - /** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */ - url?: string; - /** The choices! Maximum of 25 items. */ - options?: DiscordSelectOption[]; - /** A custom placeholder text if nothing is selected. Maximum 150 characters. */ - placeholder?: string; - /** The minimum number of items that must be selected. Default 1. Between 1-25. */ - min_values?: number; - /** The maximum number of items that can be selected. Default 1. Between 1-25. */ - max_values?: number; - /** a list of child components */ - components?: DiscordComponent[]; -} - -/** https://discord.com/developers/docs/topics/gateway#channel-pins-update */ -export interface DiscordChannelPinsUpdate { - /** The id of the guild */ - guild_id?: string; - /** The id of the channel */ - channel_id: string; - /** The time at which the most recent pinned message was pinned */ - last_pin_timestamp?: string | null; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-role-delete */ -export interface DiscordGuildRoleDelete { - /** id of the guild */ - guild_id: string; - /** id of the role */ - role_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-ban-add */ -export interface DiscordGuildBanAddRemove { - /** id of the guild */ - guild_id: string; - /** The banned user */ - user: DiscordUser; -} - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove */ -export interface DiscordMessageReactionRemove - extends Omit { } - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-add */ -export interface DiscordMessageReactionAdd { - /** The id of the user */ - user_id: string; - /** The id of the channel */ - channel_id: string; - /** The id of the message */ - message_id: string; - /** The id of the guild */ - guild_id?: string; - /** The member who reacted if this happened in a guild */ - member?: DiscordMemberWithUser; - /** The emoji used to react */ - emoji: Partial; -} - -/** https://discord.com/developers/docs/topics/gateway#voice-server-update */ -export interface DiscordVoiceServerUpdate { - /** Voice connection token */ - token: string; - /** The guild this voice server update is for */ - guild_id: string; - /** The voice server host */ - endpoint: string | null; -} - -/** https://discord.com/developers/docs/topics/gateway#invite-create */ -export interface DiscordInviteCreate { - /** The channel the invite is for */ - channel_id: string; - /** The unique invite code */ - code: string; - /** The time at which the invite was created */ - created_at: string; - /** The guild of the invite */ - guild_id?: string; - /** The user that created the invite */ - inviter?: DiscordUser; - /** How long the invite is valid for (in seconds) */ - max_age: number; - /** The maximum number of times the invite can be used */ - max_uses: number; - /** The type of target for this voice channel invite */ - target_type: TargetTypes; - /** The target user for this invite */ - target_user?: DiscordUser; - /** The embedded application to open for this voice channel embedded application invite */ - target_application?: Partial; - /** Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */ - temporary: boolean; - /** How many times the invite has been used (always will be 0) */ - uses: number; -} - -/** https://discord.com/developers/docs/topics/gateway#hello */ -export interface DiscordHello { - /** The interval (in milliseconds) the client should heartbeat with */ - heartbeat_interval: number; -} - -/** https://discord.com/developers/docs/topics/gateway#ready */ -export interface DiscordReady { - /** Gateway version */ - v: number; - /** Information about the user including email */ - user: DiscordUser; - /** The guilds the user is in */ - guilds: DiscordUnavailableGuild[]; - /** Used for resuming connections */ - session_id: string; - /** The shard information associated with this session, if sent when identifying */ - shard?: [number, number]; - /** Contains id and flags */ - application: Partial & - Pick; -} - -/** https://discord.com/developers/docs/resources/guild#unavailable-guild-object */ -export interface DiscordUnavailableGuild - extends Pick { } - -/** https://discord.com/developers/docs/topics/gateway#message-delete-bulk */ -export interface DiscordMessageDeleteBulk { - /** The ids of the messages */ - ids: string[]; - /** The id of the channel */ - channel_id: string; - /** The id of the guild */ - guild_id?: string; -} - -/** https://discord.com/developers/docs/resources/template#template-object-template-structure */ -export interface DiscordTemplate { - /** The template code (unique Id) */ - code: string; - /** Template name */ - name: string; - /** The description for the template */ - description: string | null; - /** Number of times this template has been used */ - usage_count: number; - /** The Id of the user who created the template */ - creator_id: string; - /** The user who created the template */ - creator: DiscordUser; - /** When this template was created */ - created_at: string; - /** When this template was last synced to the source guild */ - updated_at: string; - /** The Id of the guild this template is based on */ - source_guild_id: string; - /** The guild snapshot this template contains */ - serialized_source_guild: Omit< - PickPartial< - DiscordGuild, - | 'name' - | 'description' - | 'verification_level' - | 'default_message_notifications' - | 'explicit_content_filter' - | 'preferred_locale' - | 'afk_timeout' - | 'channels' - | 'afk_channel_id' - | 'system_channel_id' - | 'system_channel_flags' - >, - 'roles' - > & { - roles: (Omit< - PickPartial< - DiscordRole, - | 'name' - | 'color' - | 'hoist' - | 'mentionable' - | 'permissions' - | 'icon' - | 'unicode_emoji' - >, - 'id' - > & { id: number })[]; - }; - /** Whether the template has un-synced changes */ - is_dirty: boolean | null; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-member-add */ -export interface DiscordGuildMemberAdd extends DiscordMemberWithUser { - /** id of the guild */ - guild_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#message-delete */ -export interface DiscordMessageDelete { - /** The id of the message */ - id: string; - /** The id of the channel */ - channel_id: string; - /** The id of the guild */ - guild_id?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#thread-members-update-thread-members-update-event-fields */ -export interface DiscordThreadMembersUpdate { - /** The id of the thread */ - id: string; - /** The id of the guild */ - guild_id: string; - /** The users who were added to the thread */ - added_members?: DiscordThreadMember[]; - /** The id of the users who were removed from the thread */ - removed_member_ids?: string[]; - /** the approximate number of members in the thread, capped at 50 */ - member_count: number; -} - -/** https://discord.com/developers/docs/topics/gateway#thread-member-update */ -export interface DiscordThreadMemberUpdate { - /** The id of the thread */ - id: string; - /** The id of the user */ - user_id: string; - /** The id of the guild */ - guild_id: string; - /** The timestamp when the bot joined this thread. */ - joined_at: string; - /** The flags this user has for this thread. Not useful for bots. */ - flags: number; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-role-create */ -export interface DiscordGuildRoleCreate { - /** The id of the guild */ - guild_id: string; - /** The role created */ - role: DiscordRole; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-emojis-update */ -export interface DiscordGuildEmojisUpdate { - /** id of the guild */ - guild_id: string; - /** Array of emojis */ - emojis: DiscordEmoji[]; -} - -export interface DiscordAddGuildDiscoverySubcategory { - /** The guild Id of the subcategory was added to */ - guild_id: string; - /** The Id of the subcategory added */ - category_id: number; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-ban-add */ -export interface DiscordGuildBanAddRemove { - /** id of the guild */ - guild_id: string; - /** The banned user */ - user: DiscordUser; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-member-update */ -export interface DiscordGuildMemberUpdate { - /** The id of the guild */ - guild_id: string; - /** User role ids */ - roles: string[]; - /** The user */ - user: DiscordUser; - /** Nickname of the user in the guild */ - nick?: string | null; - /** the member's [guild avatar hash](https://discord.com/developers/docs/reference#image-formatting) */ - avatar: string; - /** When the user joined the guild */ - joined_at: string; - /** When the user starting boosting the guild */ - premium_since?: string | null; - /** whether the user is deafened in voice channels */ - deaf?: boolean; - /** whether the user is muted in voice channels */ - mute?: boolean; - /** Whether the user has not yet passed the guild's Membership Screening requirements */ - pending?: boolean; - /** when the user's [timeout](https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ) will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out. Will throw a 403 error if the user has the ADMINISTRATOR permission or is the owner of the guild */ - communication_disabled_until?: string; -} - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all */ -export interface DiscordMessageReactionRemoveAll - extends Pick< - DiscordMessageReactionAdd, - 'channel_id' | 'message_id' | 'guild_id' - > { } - -// TODO: add docs link -export interface DiscordValidateDiscoverySearchTerm { - /** Whether the provided term is valid */ - valid: boolean; -} - -/** https://discord.com/developers/docs/topics/gateway#guild-role-update */ -export interface DiscordGuildRoleUpdate { - /** The id of the guild */ - guild_id: string; - /** The role updated */ - role: DiscordRole; -} - -export interface DiscordScheduledEventUserAdd { - /** id of the guild scheduled event */ - guild_scheduled_event_id: string; - /** id of the user */ - user_id: string; - /** id of the guild */ - guild_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji */ -export type DiscordMessageReactionRemoveEmoji = Pick< - DiscordMessageReactionAdd, - 'channel_id' | 'guild_id' | 'message_id' | 'emoji' ->; - -/** https://discord.com/developers/docs/topics/gateway#guild-member-remove */ -export interface DiscordGuildMemberRemove { - /** The id of the guild */ - guild_id: string; - /** The user who was removed */ - user: DiscordUser; -} - -/** https://discord.com/developers/docs/resources/guild#ban-object */ -export interface DiscordBan { - /** The reason for the ban */ - reason: string | null; - /** The banned user */ - user: DiscordUser; -} - -export interface DiscordScheduledEventUserRemove { - /** id of the guild scheduled event */ - guild_scheduled_event_id: string; - /** id of the user */ - user_id: string; - /** id of the guild */ - guild_id: string; -} - -/** https://discord.com/developers/docs/topics/gateway#invite-delete */ -export interface DiscordInviteDelete { - /** The channel of the invite */ - channel_id: string; - /** The guild of the invite */ - guild_id?: string; - /** The unique invite code */ - code: string; -} - -/** https://discord.com/developers/docs/resources/voice#voice-region-object-voice-region-structure */ -export interface DiscordVoiceRegion { - /** Unique Id for the region */ - id: string; - /** Name of the region */ - name: string; - /** true for a single server that is closest to the current user's client */ - optimal: boolean; - /** Whether this is a deprecated voice region (avoid switching to these) */ - deprecated: boolean; - /** Whether this is a custom voice region (used for events/etc) */ - custom: boolean; -} - -export interface DiscordGuildWidgetSettings { - /** whether the widget is enabled */ - enabled: boolean; - /** the widget channel id */ - channel_id: string | null; -} - -export interface DiscordInstallParams { - /** he scopes to add the application to the server with */ - scopes: string[]; - /** the permissions to request for the bot role */ - permissions: string; -} diff --git a/packages/cache/CHANGELOG.md b/packages/cache/CHANGELOG.md deleted file mode 100644 index 954660f..0000000 --- a/packages/cache/CHANGELOG.md +++ /dev/null @@ -1,95 +0,0 @@ -# @biscuitland/cache - -## 2.3.0 - -### Minor Changes - -- fix TODO - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.3.0 - -## 2.2.3 - -### Patch Changes - -- bug fixes -- Updated dependencies - - @biscuitland/api-types@2.2.3 - -## 2.2.2 - -### Patch Changes - -- rename guildLocales to guildLocale in interactions -- Updated dependencies - - @biscuitland/api-types@2.2.2 - -## 2.2.1 - -### Patch Changes - -- select menu options now can be empty since the latest Discord API update -- Updated dependencies - - @biscuitland/api-types@2.2.1 - -## 2.2.0 - -### Minor Changes - -- Functionality to delete ephemeral messages added, select menus were updated - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.2.0 - -## 2.1.2 - -### Patch Changes - -- minor changes -- Updated dependencies - - @biscuitland/api-types@2.1.2 - -## 2.1.1 - -### Patch Changes - -- dumb hotfix that LH asked for (blame Yuzu) -- Updated dependencies - - @biscuitland/api-types@2.1.1 - -## 2.1.0 - -### Minor Changes - -- Changes to cache and forum channels ✨ -- Forum channels and updates to @biscuitland/cache ✨ - -### Patch Changes - -- Updated dependencies -- Updated dependencies - - @biscuitland/api-types@2.1.0 - -## 2.0.6 - -### Patch Changes - -- Minor fixes -- Updated dependencies - - @biscuitland/api-types@2.0.6 - -## 2.0.5 - -### Major Changes - -- publish - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.0.5 diff --git a/packages/cache/README.md b/packages/cache/README.md deleted file mode 100644 index 94c7157..0000000 --- a/packages/cache/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# @biscuitland/cache - -## Most importantly, biscuit's cache is: - -A resource control cache layer, based on carriers and resource-intensive policies - -[](https://github.com/oasisjs/biscuit) -[](https://discord.gg/XNw2RZFzaP) - -biscuit - -## Install (for [node18](https://nodejs.org/en/download/)) - -```sh-session -npm install @biscuitland/cache -``` - -## Example (Basic) - -```ts -import { Cache, MemoryCacheAdapter } from '@biscuitland/cache'; - -const bootstrap = async () => { - const cache = new Cache({ - adapter: new MemoryCacheAdapter(), - }); - - // You can listen to the raw biscuit event - - cache.start(); -}; - -bootstrap(); -``` - -## Links - -- [Documentation](https://docs.biscuitjs.com/) -- [Website](https://biscuitjs.com/) diff --git a/packages/cache/package.json b/packages/cache/package.json deleted file mode 100644 index 5cfb7aa..0000000 --- a/packages/cache/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@biscuitland/cache", - "version": "2.3.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": [ - "dist/**" - ], - "scripts": { - "build": "tsup", - "clean": "rm -rf dist && rm -rf .turbo", - "dev": "tsup --watch" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "require": "./dist/index.js" - } - }, - "dependencies": { - "@biscuitland/api-types": "^2.3.0", - "ioredis": "^5.2.2" - }, - "devDependencies": { - "tsup": "^6.1.3" - }, - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - }, - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "publishConfig": { - "access": "public" - } -} diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts deleted file mode 100644 index f073969..0000000 --- a/packages/cache/src/cache.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* eslint-disable no-case-declarations */ -import type { CacheOptions, CO } from './types'; - -import type { CacheAdapter } from './scheme/adapters/cache-adapter'; -import { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; - -import { - ChannelResource, - GuildEmojiResource, - GuildMemberResource, - GuildResource, - GuildRoleResource, - GuildStickerResource, - GuildVoiceResource, - PresenceResource, - UserResource, -} from './resources'; - -import { Options } from './utils/options'; - -export class Cache { - static readonly DEFAULTS = { - adapter: new MemoryCacheAdapter(), - }; - - readonly options: CO; - #adapter: CacheAdapter; - - // move to resources assigned - - readonly channels: ChannelResource; - - readonly emojis: GuildEmojiResource; - readonly members: GuildMemberResource; - readonly guilds: GuildResource; - readonly roles: GuildRoleResource; - readonly stickers: GuildStickerResource; - readonly voices: GuildVoiceResource; - - readonly presences: PresenceResource; - readonly users: UserResource; - - constructor(options: CacheOptions) { - this.options = Options({}, Cache.DEFAULTS, options); - this.#adapter = this.options.adapter; - - this.channels = new ChannelResource(this.#adapter); - - this.emojis = new GuildEmojiResource(this.#adapter); - this.members = new GuildMemberResource(this.#adapter); - - this.guilds = new GuildResource(this.#adapter); - this.roles = new GuildRoleResource(this.#adapter); - - this.stickers = new GuildStickerResource(this.#adapter); - this.voices = new GuildVoiceResource(this.#adapter); - - this.presences = new PresenceResource(this.#adapter); - this.users = new UserResource(this.#adapter); - } - - /** - * @inheritDoc - */ - - async start(event: any) { - let resources: any[] = []; - - let contents: any[] = []; - - switch (event.t) { - case 'READY': - resources = []; - - await this.users.set(event.d.user.id, event.d.user); - - for (const guild of event.d.guilds) { - resources.push(this.guilds.set(guild.id, guild)); - } - - await Promise.all(resources); - - break; - - case 'USER_UPDATE': - await this.users.set(event.d.id, event.d); - break; - case 'PRESENCE_UPDATE': - await this.presences.set(event.d.user?.id, event.d); - - break; - - case 'GUILD_CREATE': - case 'GUILD_UPDATE': - await this.guilds.set(event.d.id, event.d); - break; - - case 'GUILD_DELETE': - if (event.d.unavailable) { - await this.guilds.set(event.d.id, event.d); - } else { - await this.guilds.remove(event.d.id); - } - break; - - case 'CHANNEL_CREATE': - case 'CHANNEL_UPDATE': - // modify [Add elimination system] - await this.channels.set(event.d.id, event.d); - break; - - case 'CHANNEL_DELETE': - // modify [Add elimination system] - await this.channels.remove(event.d.id); - break; - - case 'MESSAGE_CREATE': - if (event.d.webhook_id) { - return; - } - - if (event.d.author) { - await this.users.set(event.d.author.id, event.d.author); - } - - break; - - case 'GUILD_ROLE_CREATE': - case 'GUILD_ROLE_UPDATE': - await this.roles.set( - event.d.role.id, - event.d.guild_id, - event.d.role - ); - break; - - case 'GUILD_ROLE_DELETE': - await this.roles.remove(event.d.role.id, event.d.guild_id); - break; - - case 'GUILD_EMOJIS_UPDATE': - contents = []; - contents = await this.emojis.items(event.d.guild_id); - - for (const emoji of event.d.emojis) { - const emote = contents.find(o => o?.id === emoji.id); - - if (!emote || emote !== emoji) { - await this.emojis.set( - emoji.id, - event.d.guild_id, - emoji - ); - } - } - - for (const emoji of contents) { - const emote = event.d.emojis.find( - (o: any) => o.id === emoji?.id - ); - - if (!emote) { - await this.emojis.remove(emote.id, event.d.guild_id); - } - } - - break; - - case 'GUILD_STICKERS_UPDATE': - contents = []; - contents = await this.stickers.items(event.d.guild_id); - - for (const sticker of event.d.stickers) { - const stick = contents.find(o => o?.id === sticker.id); - - if (!stick || stick !== sticker) { - await this.stickers.set( - sticker.id, - event.d.guild_id, - sticker - ); - } - } - - for (const sticker of contents) { - const stick = event.d.stickers.find( - (o: any) => o.id === sticker?.id - ); - - if (!stick) { - await this.stickers.remove(stick.id, event.d.guild_id); - } - } - - break; - - case 'GUILD_MEMBER_ADD': - case 'GUILD_MEMBER_UPDATE': - await this.members.set( - event.d.user.id, - event.d.guild_id, - event.d - ); - break; - - case 'GUILD_MEMBER_REMOVE': - await this.members.remove(event.d.user.id, event.d.guild_id); - break; - - case 'GUILD_MEMBERS_CHUNK': - resources = []; - - for (const member of event.d.members) { - resources.push( - this.members.set( - member.user.id, - event.d.guild_id, - member - ) - ); - } - - await Promise.all(resources); - - break; - - case 'VOICE_STATE_UPDATE': - if (!event.d.guild_id) { - return; - } - - if (event.d.guild_id && event.d.member && event.d.user_id) { - await this.members.set(event.d.user_id, event.d.guild_id, { - guild_id: event.d.guild_id, - ...event.d.member, - }); - } - - if (event.d.channel_id != null) { - await this.members.set( - event.d.user_id, - event.d.guild_id, - event.d - ); - } else { - await this.voices.remove(event.d.user_id, event.d.guild_id); - } - - break; - } - } -} diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts deleted file mode 100644 index 1b88152..0000000 --- a/packages/cache/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; -export { RedisCacheAdapter } from './scheme/adapters/redis-cache-adapter'; - -export { CacheAdapter } from './scheme/adapters/cache-adapter'; - -export { RedisOptions, MemoryOptions, CacheOptions } from './types'; -export { Cache } from './cache'; diff --git a/packages/cache/src/resources/base-resource.ts b/packages/cache/src/resources/base-resource.ts deleted file mode 100644 index 1457344..0000000 --- a/packages/cache/src/resources/base-resource.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; - -/** - * Base class for all resources - * All Methods from BaseResource are also available on every class extends - */ - -class Base { - /** - * Resource name - */ - - #namespace = 'base'; - - /** - * Adapter for storage processes and operations - */ - - #adapter: CacheAdapter; - - /** - * Guild linked and assigned to the current entity (resource) - */ - - parent?: string; - - /** - * Constructor - */ - - constructor(namespace: string, adapter: CacheAdapter) { - this.#namespace = namespace; - this.#adapter = adapter; - } - - /** - * Entity linked - */ - - setEntity(entity: T): void { - Object.assign(this, entity); - } - - /** - * Parent linked - */ - - setParent(parent: string): void { - // rename - this.parent = parent; - } - - /** - * Count how many resources there are in the relationships - */ - - async count(to: string): Promise { - return await this.#adapter.count(this.hashId(to)); - } - - /** - * Check if the resource is in the relationships - */ - - async contains( - id: string, - guild: string = this.parent as string - ): Promise { - return await this.#adapter.contains(this.hashId(guild), id); - } - - /** - * Gets the resource relationships - */ - - async getToRelationship( - id: string = this.parent as string - ): Promise { - return await this.#adapter.getToRelationship(this.hashId(id)); - } - - /** - * Adds the resource to relationships - */ - - async addToRelationship( - id: string, - guild: string = this.parent as string - ): Promise { - await this.#adapter.addToRelationship(this.hashId(guild), id); - } - - /** - * Removes the relationship resource - */ - - async removeToRelationship( - id: string, - guild: string = this.parent as string - ): Promise { - await this.#adapter.removeToRelationship(this.hashId(guild), id); - } - - /** - * Construct an id consisting of namespace.id - */ - - protected hashId(id: string): string { - return `${this.#namespace}.${id}`; - } -} - -export const BaseResource = Base as new ( - data: string, - adapter: CacheAdapter -) => Base & T; diff --git a/packages/cache/src/resources/channel-resource.ts b/packages/cache/src/resources/channel-resource.ts deleted file mode 100644 index 93abe8b..0000000 --- a/packages/cache/src/resources/channel-resource.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordChannel } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; -import { UserResource } from './user-resource'; - -/** - * Resource represented by an channel of discord - */ - -export class ChannelResource extends BaseResource { - #namespace = 'channel'; - - #adapter: CacheAdapter; - - #users: UserResource; - - constructor(adapter: CacheAdapter, entity?: DiscordChannel | null) { - super('channel', adapter); - - this.#adapter = adapter; - this.#users = new UserResource(adapter); - - if (entity) { - this.setEntity(entity); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashId(id)); - - if (kv) { - return new ChannelResource(this.#adapter, kv); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set(id: string, data: any): Promise { - if (data.recipients) { - const recipients = []; - - for (const recipient of data.recipients) { - recipients.push(this.#users.set(recipient.id, recipient)); - } - - await Promise.all(recipients); - } - - delete data.recipients; - delete data.permission_overwrites; - - await this.addToRelationship(id); - await this.#adapter.set(this.hashId(id), data); - } - - /** - * @inheritDoc - */ - - async items(): Promise { - const data = await this.#adapter.items(this.#namespace); - - if (data) { - return data.map(dt => { - const resource = new ChannelResource(this.#adapter, dt); - resource.setParent(resource.id); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async count(): Promise { - return await this.#adapter.count(this.#namespace); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - await this.#adapter.remove(this.hashId(id)); - } - - /** - * @inheritDoc - */ - - async contains(id: string): Promise { - return await this.#adapter.contains(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async getToRelationship(): Promise { - return await this.#adapter.getToRelationship(this.#namespace); - } - - /** - * @inheritDoc - */ - - async addToRelationship(id: string): Promise { - await this.#adapter.addToRelationship(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(id: string): Promise { - await this.#adapter.removeToRelationship(this.#namespace, id); - } -} diff --git a/packages/cache/src/resources/guild-emoji-resource.ts b/packages/cache/src/resources/guild-emoji-resource.ts deleted file mode 100644 index 85e6eba..0000000 --- a/packages/cache/src/resources/guild-emoji-resource.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordEmoji } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; -import { UserResource } from './user-resource'; - -/** - * Resource represented by an emoji of discord - */ - -export class GuildEmojiResource extends BaseResource { - #namespace = 'emoji' as const; - - #adapter: CacheAdapter; - - #users: UserResource; - - constructor( - adapter: CacheAdapter, - entity?: DiscordEmoji | null, - parent?: string - ) { - super('emoji', adapter); - - this.#adapter = adapter; - this.#users = new UserResource(adapter); - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get( - id: string, - guild: string | undefined = this.parent - ): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return new GuildEmojiResource(this.#adapter, kv, guild); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string | undefined = this.parent, - data: any - ): Promise { - if (data.user) { - await this.#users.set(data.user.id, data.user); - } - - delete data.user; - delete data.roles; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id, guild); - await this.#adapter.set(this.hashGuildId(id, guild), data); - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - if (!to && this.parent) { - to = this.parent; - } - - const data = await this.#adapter.items(this.hashId(to)); - - if (data) { - return data.map(dt => { - const resource = new GuildEmojiResource(this.#adapter, dt); - resource.setParent(to); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async remove( - id: string, - guild: string | undefined = this.parent - ): Promise { - await this.removeToRelationship(id, guild); - await this.#adapter.remove(this.hashGuildId(id, guild)); - } - - /** - * @inheritDoc - */ - - protected hashGuildId(id: string, guild?: string): string { - if (!guild) { - return this.hashId(id); - } - - return `${this.#namespace}.${guild}.${id}`; - } -} diff --git a/packages/cache/src/resources/guild-member-resource.ts b/packages/cache/src/resources/guild-member-resource.ts deleted file mode 100644 index 603d8ca..0000000 --- a/packages/cache/src/resources/guild-member-resource.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordMember } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; -import { UserResource } from './user-resource'; - -/** - * Resource represented by an member of discord - */ - -export class GuildMemberResource extends BaseResource { - #namespace = 'member' as const; - - #adapter: CacheAdapter; - - #users: UserResource; - - constructor( - adapter: CacheAdapter, - entity?: DiscordMember | null, - parent?: string - ) { - super('member', adapter); - - this.#adapter = adapter; - this.#users = new UserResource(adapter); - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get( - id: string, - guild: string | undefined = this.parent - ): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return new GuildMemberResource(this.#adapter, kv, guild); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string | undefined = this.parent, - data: any - ): Promise { - if (data.user) { - await this.#users.set(data.user.id, data.user); - } - - delete data.user; - delete data.roles; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id, guild); - await this.#adapter.set(this.hashGuildId(id, guild), data); - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - if (!to && this.parent) { - to = this.parent; - } - - const data = await this.#adapter.items(this.hashId(to)); - - if (data) { - return data.map(dt => { - const resource = new GuildMemberResource(this.#adapter, dt); - resource.setParent(to); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async remove( - id: string, - guild: string | undefined = this.parent - ): Promise { - await this.removeToRelationship(id, guild); - await this.#adapter.remove(this.hashGuildId(id, guild)); - } - - /** - * @inheritDoc - */ - - protected hashGuildId(id: string, guild?: string): string { - if (!guild) { - return this.hashId(id); - } - - return `${this.#namespace}.${guild}.${id}`; - } -} diff --git a/packages/cache/src/resources/guild-resource.ts b/packages/cache/src/resources/guild-resource.ts deleted file mode 100644 index 3633ec0..0000000 --- a/packages/cache/src/resources/guild-resource.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordGuild } from '@biscuitland/api-types'; - -import { ChannelResource } from './channel-resource'; -import { GuildEmojiResource } from './guild-emoji-resource'; -import { GuildMemberResource } from './guild-member-resource'; -import { GuildRoleResource } from './guild-role-resource'; -import { GuildStickerResource } from './guild-sticker-resource'; -import { GuildVoiceResource } from './guild-voice-resource'; - -import { PresenceResource } from './presence-resource'; -import { BaseResource } from './base-resource'; - -/** - * Resource represented by an guild of discord - */ - -export class GuildResource extends BaseResource { - #namespace = 'guild' as const; - - #adapter: CacheAdapter; - - #channels: ChannelResource; - #emojis: GuildEmojiResource; - #members: GuildMemberResource; - #roles: GuildRoleResource; - #stickers: GuildStickerResource; - #voices: GuildVoiceResource; - - #presences: PresenceResource; - - constructor( - adapter: CacheAdapter, - entity?: DiscordGuild | null, - parent?: string, - channels?: ChannelResource, - emojis?: GuildEmojiResource, - members?: GuildMemberResource, - roles?: GuildRoleResource, - stickers?: GuildStickerResource, - voices?: GuildVoiceResource, - presences?: PresenceResource - ) { - super('guild', adapter); - - this.#adapter = adapter; - - this.#channels = channels ?? new ChannelResource(adapter); - - this.#emojis = emojis ?? new GuildEmojiResource(adapter); - this.#members = members ?? new GuildMemberResource(adapter); - - this.#roles = roles ?? new GuildRoleResource(adapter); - - this.#stickers = stickers ?? new GuildStickerResource(adapter); - - this.#voices = voices ?? new GuildVoiceResource(adapter); - this.#presences = presences ?? new PresenceResource(adapter); - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashId(id)); - - if (kv) { - return new GuildResource( - this.#adapter, - kv, - id, - new ChannelResource(this.#adapter), - new GuildEmojiResource(this.#adapter, null, id), - new GuildMemberResource(this.#adapter, null, id), - new GuildRoleResource(this.#adapter, null, id), - new GuildStickerResource(this.#adapter, null, id), - new GuildVoiceResource(this.#adapter, null, id), - new PresenceResource(this.#adapter) - ); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set(id: string, data: any): Promise { - if (data.channels) { - const channels: unknown[] = []; - - for (const channel of data.channels) { - channel.guild_id = id; - - await this.#channels.set(channel.id, channel); - } - - await Promise.all(channels); - } - - if (data.emojis) { - const emojis: unknown[] = []; - - for (const emoji of data.emojis) { - emoji.guild_id = id; - - await this.#emojis.set(emoji.id, id, emoji); - } - - await Promise.all(emojis); - } - - if (data.members) { - const members: unknown[] = []; - - for (const member of data.members) { - member.guild_id = id; - - await this.#members.set(member.user.id, id, member); - } - - await Promise.all(members); - } - - if (data.roles) { - const roles: unknown[] = []; - - for (const role of data.roles) { - role.guild_id = id; - - await this.#roles.set(role.id, id, role); - } - - await Promise.all(roles); - } - - if (data.stickers) { - const stickers: unknown[] = []; - - for (const sticker of data.stickers) { - sticker.guild_id = id; - - await this.#stickers.set(sticker.id, id, sticker); - } - - await Promise.all(stickers); - } - - if (data.voice_states) { - const voices: unknown[] = []; - - for (const voice of data.voice_states) { - voice.guild_id = id; - - voices.push(this.#voices.set(voice.user_id, id, voice)); - } - - await Promise.all(voices); - } - - if (data.presences) { - const presences: unknown[] = []; - - for (const presence of data.presences) { - await this.#presences.set(presence.user.id, presence); - } - - await Promise.all(presences); - } - - delete data.channels; - delete data.emojis; - delete data.members; - delete data.roles; - delete data.stickers; - - delete data.voice_states; - delete data.guild_hashes; - - delete data.presences; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id); - await this.#adapter.set(this.hashId(id), data); - } - - /** - * @inheritDoc - */ - - async items(): Promise { - const data = await this.#adapter.items(this.#namespace); - - if (data) { - return data.map(dt => { - const resource = new GuildResource(this.#adapter, dt); - resource.setParent(resource.id); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async count(): Promise { - return await this.#adapter.count(this.#namespace); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - const members = await this.#members.getToRelationship(id); - - for (const member of members) { - await this.#members.remove(member, id); - } - - const roles = await this.#roles.getToRelationship(id); - - for (const role of roles) { - await this.#roles.remove(role, id); - } - - const emojis = await this.#emojis.getToRelationship(id); - - for (const emoji of emojis) { - await this.#emojis.remove(emoji, id); - } - - const stickers = await this.#stickers.getToRelationship(id); - - for (const sticker of stickers) { - await this.#stickers.remove(sticker, id); - } - - await this.removeToRelationship(id); - await this.#adapter.remove(this.hashId(id)); - } - - /** - * @inheritDoc - */ - - async contains(id: string): Promise { - return await this.#adapter.contains(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async getToRelationship(): Promise { - return await this.#adapter.getToRelationship(this.#namespace); - } - - /** - * @inheritDoc - */ - - async addToRelationship(id: string): Promise { - await this.#adapter.addToRelationship(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(id: string): Promise { - await this.#adapter.removeToRelationship(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async getEmojis(): Promise { - return await this.#emojis.items(this.parent as string); - } - - /** - * @inheritDoc - */ - - async getMembers(): Promise { - return await this.#members.items(this.parent as string); - } - - /** - * @inheritDoc - */ - - async getRoles(): Promise { - return await this.#roles.items(this.parent as string); - } - - /** - * @inheritDoc - */ - - async getStickers(): Promise { - return await this.#stickers.items(this.parent as string); - } - - /** - * @inheritDoc - */ - - async getVoiceStates(): Promise { - return await this.#voices.items(this.parent as string); - } -} diff --git a/packages/cache/src/resources/guild-role-resource.ts b/packages/cache/src/resources/guild-role-resource.ts deleted file mode 100644 index e9170f5..0000000 --- a/packages/cache/src/resources/guild-role-resource.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordRole } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; - -/** - * Resource represented by an role of discord - */ - -export class GuildRoleResource extends BaseResource { - #namespace = 'role' as const; - - #adapter: CacheAdapter; - - constructor( - adapter: CacheAdapter, - entity?: DiscordRole | null, - parent?: string - ) { - super('role', adapter); - - this.#adapter = adapter; - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get( - id: string, - guild: string | undefined = this.parent - ): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return new GuildRoleResource(this.#adapter, kv, guild); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string | undefined = this.parent, - data: any - ): Promise { - if (!data.id) { - data.id = id; - } - - if (!data.guild_id) { - data.guild_id = guild; - } - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id, guild); - await this.#adapter.set(this.hashGuildId(id, guild), data); - } - - /** - * @inheritDoc - */ - - async count(): Promise { - return await this.#adapter.count(this.#namespace); - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - if (!to && this.parent) { - to = this.parent; - } - - const data = await this.#adapter.items(this.hashId(to)); - - if (data) { - return data.map(dt => { - const resource = new GuildRoleResource(this.#adapter, dt); - resource.setParent(to); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async remove( - id: string, - guild: string | undefined = this.parent - ): Promise { - await this.removeToRelationship(id, guild); - await this.#adapter.remove(this.hashGuildId(id, guild)); - } - - /** - * @inheritDoc - */ - - protected hashGuildId(id: string, guild?: string): string { - if (!guild) { - return this.hashId(id); - } - - return `${this.#namespace}.${guild}.${id}`; - } -} diff --git a/packages/cache/src/resources/guild-sticker-resource.ts b/packages/cache/src/resources/guild-sticker-resource.ts deleted file mode 100644 index de8d610..0000000 --- a/packages/cache/src/resources/guild-sticker-resource.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordSticker } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; -import { UserResource } from './user-resource'; - -/** - * Resource represented by an sticker of discord - */ - -export class GuildStickerResource extends BaseResource { - #namespace = 'sticker' as const; - - #adapter: CacheAdapter; - - #users: UserResource; - - constructor( - adapter: CacheAdapter, - entity?: DiscordSticker | null, - parent?: string - ) { - super('sticker', adapter); - - this.#adapter = adapter; - this.#users = new UserResource(adapter); - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get(id: string, guild: string): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return new GuildStickerResource(this.#adapter, kv, guild); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string | undefined = this.parent, - data: any - ): Promise { - if (data.user) { - await this.#users.set(data.user.id, data.user); - } - - delete data.user; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id, guild); - await this.#adapter.set(this.hashGuildId(id, guild), data); - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - if (!to && this.parent) { - to = this.parent; - } - - const data = await this.#adapter.items(this.hashId(to)); - - if (data) { - return data.map(dt => { - const resource = new GuildStickerResource(this.#adapter, dt); - resource.setParent(to); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async remove( - id: string, - guild: string | undefined = this.parent - ): Promise { - await this.removeToRelationship(id, guild); - await this.#adapter.remove(this.hashGuildId(id, guild)); - } - - /** - * @inheritDoc - */ - - protected hashGuildId(id: string, guild?: string): string { - if (!guild) { - return this.hashId(id); - } - - return `${this.#namespace}.${guild}.${id}`; - } -} diff --git a/packages/cache/src/resources/guild-voice-resource.ts b/packages/cache/src/resources/guild-voice-resource.ts deleted file mode 100644 index 0c370eb..0000000 --- a/packages/cache/src/resources/guild-voice-resource.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordVoiceState } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; - -/** - * Resource represented by an voice state of discord - */ - -export class GuildVoiceResource extends BaseResource { - #namespace = 'voice' as const; - - #adapter: CacheAdapter; - - constructor( - adapter: CacheAdapter, - entity?: DiscordVoiceState | null, - parent?: string - ) { - super('voice', adapter); - - this.#adapter = adapter; - - if (entity) { - this.setEntity(entity); - } - - if (parent) { - this.setParent(parent); - } - } - - /** - * @inheritDoc - */ - - async get( - id: string, - guild: string | undefined = this.parent - ): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return new GuildVoiceResource(this.#adapter, kv, guild); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string | undefined = this.parent, - data: any - ): Promise { - if (!data.guild_id) { - data.guild_id = guild; - } - - delete data.member; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id, guild); - await this.#adapter.set(this.hashGuildId(id, guild), data); - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - if (!to && this.parent) { - to = this.parent; - } - - const data = await this.#adapter.items(this.hashId(to)); - - if (data) { - return data.map(dt => { - const resource = new GuildVoiceResource(this.#adapter, dt); - resource.setParent(to); - - return resource; - }); - } - - return []; - } - - /** - * @inheritDoc - */ - - async remove( - id: string, - guild: string | undefined = this.parent - ): Promise { - await this.removeToRelationship(id, guild); - await this.#adapter.remove(this.hashGuildId(id, guild)); - } - - /** - * @inheritDoc - */ - - protected hashGuildId(id: string, guild?: string): string { - if (!guild) { - return this.hashId(id); - } - - return `${this.#namespace}.${guild}.${id}`; - } -} diff --git a/packages/cache/src/resources/index.ts b/packages/cache/src/resources/index.ts deleted file mode 100644 index 3207fa7..0000000 --- a/packages/cache/src/resources/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { BaseResource } from './base-resource'; -export { ChannelResource } from './channel-resource'; - -export { GuildEmojiResource } from './guild-emoji-resource'; -export { GuildMemberResource } from './guild-member-resource'; - -export { GuildResource } from './guild-resource'; - -export { GuildRoleResource } from './guild-role-resource'; - -export { GuildStickerResource } from './guild-sticker-resource'; -export { GuildVoiceResource } from './guild-voice-resource'; - -export { PresenceResource } from './presence-resource'; -export { UserResource } from './user-resource'; diff --git a/packages/cache/src/resources/presence-resource.ts b/packages/cache/src/resources/presence-resource.ts deleted file mode 100644 index 50ce1c1..0000000 --- a/packages/cache/src/resources/presence-resource.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordPresenceUpdate } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; -import { UserResource } from './user-resource'; - -/** - * Resource represented by an presence of discord - */ - -export class PresenceResource extends BaseResource { - #namespace = 'presence' as const; - - #adapter: CacheAdapter; - - #users: UserResource; - - constructor(adapter: CacheAdapter, entity?: DiscordPresenceUpdate | null) { - super('presence', adapter); - - this.#adapter = adapter; - this.#users = new UserResource(this.#adapter); - - if (entity) { - this.setEntity(entity); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashId(id)); - - if (kv) { - return new PresenceResource(this.#adapter, kv); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set(id: string, data: any): Promise { - if (data.user) { - await this.#users.set(data.user.id, data.user); - } - - delete data.user; - delete data.roles; - - delete data.guild_id; - - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id); - await this.#adapter.set(this.hashId(id), data); - } - - /** - * @inheritDoc - */ - - async items(): Promise { - const data = await this.#adapter.items(this.#namespace); - - if (data) { - return data.map(dt => new PresenceResource(this.#adapter, dt)); - } - - return []; - } - - /** - * @inheritDoc - */ - - async count(): Promise { - return await this.#adapter.count(this.#namespace); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - await this.removeToRelationship(id); - await this.#adapter.remove(this.hashId(id)); - } - - /** - * @inheritDoc - */ - - async contains(id: string): Promise { - return await this.#adapter.contains(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async getToRelationship(): Promise { - return await this.#adapter.getToRelationship(this.#namespace); - } - - /** - * @inheritDoc - */ - - async addToRelationship(id: string): Promise { - await this.#adapter.addToRelationship(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(id: string): Promise { - await this.#adapter.removeToRelationship(this.#namespace, id); - } -} diff --git a/packages/cache/src/resources/user-resource.ts b/packages/cache/src/resources/user-resource.ts deleted file mode 100644 index 8d100a4..0000000 --- a/packages/cache/src/resources/user-resource.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -import type { DiscordUser } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; - -/** - * Resource represented by an user of discord - */ - -export class UserResource extends BaseResource { - #namespace = 'user' as const; - - #adapter: CacheAdapter; - - constructor(adapter: CacheAdapter, entity?: DiscordUser | null) { - super('user', adapter); - - this.#adapter = adapter; - - if (entity) { - this.setEntity(entity); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - if (this.parent) { - return this; - } - - const kv = await this.#adapter.get(this.hashId(id)); - - if (kv) { - return new UserResource(this.#adapter, kv); - } - - return null; - } - - /** - * @inheritDoc - */ - - async set(id: string, data: any): Promise { - if (this.parent) { - this.setEntity(data); - } - - await this.addToRelationship(id); - await this.#adapter.set(this.hashId(id), data); - } - - /** - * @inheritDoc - */ - - async items(): Promise { - const data = await this.#adapter.items(this.#namespace); - - if (data) { - return data.map(dt => new UserResource(this.#adapter, dt)); - } - - return []; - } - - /** - * @inheritDoc - */ - - async count(): Promise { - return await this.#adapter.count(this.#namespace); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - await this.removeToRelationship(id); - await this.#adapter.remove(this.hashId(id)); - } - - /** - * @inheritDoc - */ - - async contains(id: string): Promise { - return await this.#adapter.contains(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async getToRelationship(): Promise { - return await this.#adapter.getToRelationship(this.#namespace); - } - - /** - * @inheritDoc - */ - - async addToRelationship(id: string): Promise { - await this.#adapter.addToRelationship(this.#namespace, id); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(id: string): Promise { - await this.#adapter.removeToRelationship(this.#namespace, id); - } -} diff --git a/packages/cache/src/scheme/adapters/cache-adapter.ts b/packages/cache/src/scheme/adapters/cache-adapter.ts deleted file mode 100644 index c5323a2..0000000 --- a/packages/cache/src/scheme/adapters/cache-adapter.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Base class for all adapters - * All Methods from CacheAdapter are also available on every class extends - */ - -export interface CacheAdapter { - /** - * Gets the resource to adapter - */ - - get(id: string): any | Promise; - get(id: string, guild: string): string | Promise; - - /** - * Sets the resource to adapter - */ - - set(id: string, data: any): void | Promise; - set(id: string, guild: string, data: any): void | Promise; - - /** - * Get the items of a relationship - */ - - items(to?: string): any[] | Promise; - - /** - * Count how many resources there are in the relationships - */ - - count(to: string): number | Promise; - - /** - * Removes the adapter resource - */ - - remove(id: string): void | Promise; - remove(id: string, guild: string): void | Promise; - - /** - * Check if the resource is in the relationships - */ - - contains(to: string, id: string): boolean | Promise; - - /** - * Gets the resource relationships - */ - - getToRelationship(to: string): string[] | Promise; - - /** - * Adds the resource to relationships - */ - - addToRelationship(to: string, id: string): void | Promise; - - /** - * Removes the relationship resource - */ - - removeToRelationship(to: string, id: string): void | Promise; -} diff --git a/packages/cache/src/scheme/adapters/memory-cache-adapter.ts b/packages/cache/src/scheme/adapters/memory-cache-adapter.ts deleted file mode 100644 index fabb5ce..0000000 --- a/packages/cache/src/scheme/adapters/memory-cache-adapter.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from './cache-adapter'; -import type { MemoryOptions, MO } from '../../types'; - -import { Options } from '../../utils/options'; - -export class MemoryCacheAdapter implements CacheAdapter { - static readonly DEFAULTS = { - expire: 3600000, - }; - - readonly relationships = new Map(); - readonly storage = new Map(); - - readonly options: MO; - - constructor(options?: MemoryOptions) { - this.options = Options({}, MemoryCacheAdapter.DEFAULTS, options); - } - - /** - * @inheritDoc - */ - - get(id: string): T | null { - const data = this.storage.get(id); - - if (data) { - if (data.expire && data.expire < Date.now()) { - this.storage.delete(id); - } else { - return JSON.parse(data.data); - } - } - - return null; - } - - /** - * @inheritDoc - */ - - set(id: string, data: any): void { - const expire = this.options.expire; - - if (expire) { - this.storage.set(id, { - data: JSON.stringify(data), - expire: Date.now() + expire, - }); - } else { - this.storage.set(id, { data: JSON.stringify(data) }); - } - } - - /** - * @inheritDoc - */ - - items(to: string): any[] { - const array: unknown[] = []; - let data = this.getToRelationship(to); - - data = data.map(id => `${to}.${id}`); - - for (const key of data) { - const content = this.get(key); - - if (content) { - array.push(content); - } - } - - return array; - } - - /** - * @inheritDoc - */ - - count(to: string): number { - return this.getToRelationship(to).length; - } - - /** - * @inheritDoc - */ - - remove(id: string): void { - this.storage.delete(id); - } - - /** - * @inheritDoc - */ - - contains(to: string, id: string): boolean { - return this.getToRelationship(to).includes(id); - } - - /** - * @inheritDoc - */ - - getToRelationship(to: string): string[] { - return this.relationships.get(to) || []; - } - - /** - * @inheritDoc - */ - - addToRelationship(to: string, id: string): void { - const data = this.getToRelationship(to); - - if (data.includes(id)) { - return; - } - - data.push(id); - - const has = !!this.relationships.get(to); - - if (!has) { - this.relationships.set(to, data); - } - } - - /** - * @inheritDoc - */ - - removeToRelationship(to: string, id: string): void { - const data = this.getToRelationship(to); - - if (data) { - const idx = data.indexOf(id); - - if (idx === -1) { - return; - } - - data.splice(idx, 1); - } - } -} diff --git a/packages/cache/src/scheme/adapters/redis-cache-adapter.ts b/packages/cache/src/scheme/adapters/redis-cache-adapter.ts deleted file mode 100644 index 8a87193..0000000 --- a/packages/cache/src/scheme/adapters/redis-cache-adapter.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * refactor - */ - -import type { CacheAdapter } from './cache-adapter'; - -import type { RedisOptions } from 'ioredis'; -import type Redis from 'ioredis'; -import IORedis from 'ioredis'; - -interface BaseOptions { - namespace: string; - expire?: number; -} - -interface BuildOptions extends BaseOptions, RedisOptions {} - -interface ClientOptions extends BaseOptions { - client: Redis; -} - -type Options = BuildOptions | ClientOptions; - -export class RedisCacheAdapter implements CacheAdapter { - static readonly DEFAULTS = { - namespace: 'biscuitland', - }; - - readonly options: Options; - - readonly client: Redis; - - constructor(options?: Options) { - this.options = Object.assign(RedisCacheAdapter.DEFAULTS, options); - - if ((this.options as ClientOptions).client) { - this.client = (this.options as ClientOptions).client; - } else { - const { ...redisOpt } = this.options as BuildOptions; - this.client = new IORedis(redisOpt); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - const data = await this.client.get(this.build(id)); - - if (!data) { - return null; - } - - return JSON.parse(data); - } - - /** - * @inheritDoc - */ - - async set(id: string, data: unknown): Promise { - const expire = this.options.expire; - - if (expire) { - await this.client.set( - this.build(id), - JSON.stringify(data), - 'EX', - expire - ); - } else { - await this.client.set(this.build(id), JSON.stringify(data)); - } - } - - /** - * @inheritDoc - */ - - async items(to: string): Promise { - const array: unknown[] = []; - - let data = await this.getToRelationship(to); - data = data.map(id => this.build(`${to}.${id}`)); - - if (data && data.length > 0) { - const items = await this.client.mget(data); - - for (const item of items) { - if (item) { - array.push(JSON.parse(item)); - } - } - } - - return array; - } - - /** - * @inheritDoc - */ - - async count(to: string): Promise { - return new Promise((resolve, reject) => { - this.client.scard(this.build(to), (err, result) => { - if (err) { - return reject(err); - } - - return resolve(result || 0); - }); - }); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - await this.client.del(this.build(id)); - } - - /** - * @inheritDoc - */ - - async contains(to: string, id: string): Promise { - return new Promise((resolve, reject) => { - this.client.sismember(this.build(to), id, (err, result) => { - if (err) { - return reject(err); - } - - return resolve(result === 1); - }); - }); - } - - /** - * @inheritDoc - */ - - async getToRelationship(to: string): Promise { - return new Promise((resolve, reject) => { - this.client.smembers(this.build(to), (err, result) => { - if (err) { - reject(err); - } - - resolve(result || []); - }); - }); - } - - /** - * @inheritDoc - */ - - async addToRelationship(to: string, id: string): Promise { - return new Promise((resolve, reject) => { - this.client.sadd(this.build(to), id, err => { - if (err) { - reject(err); - } - - resolve(); - }); - }); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(to: string, id: string): Promise { - return new Promise((resolve, reject) => { - this.client.srem(this.build(to), id, err => { - if (err) { - reject(err); - } - - resolve(); - }); - }); - } - - /** - * @inheritDoc - */ - - protected build(id: string): string { - return `${this.options.namespace}:${id}`; - } -} diff --git a/packages/cache/src/scheme/transporters/base-transporter.ts b/packages/cache/src/scheme/transporters/base-transporter.ts deleted file mode 100644 index 674185f..0000000 --- a/packages/cache/src/scheme/transporters/base-transporter.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * future update - */ - -export interface BaseTransporter { - // -} diff --git a/packages/cache/src/scheme/transporters/redis-transporter.ts b/packages/cache/src/scheme/transporters/redis-transporter.ts deleted file mode 100644 index 1b41bcf..0000000 --- a/packages/cache/src/scheme/transporters/redis-transporter.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { BaseTransporter } from './base-transporter'; - -export class RedisTransporter implements BaseTransporter {} diff --git a/packages/cache/src/scheme/transporters/tcp-transporter.ts b/packages/cache/src/scheme/transporters/tcp-transporter.ts deleted file mode 100644 index f8851c3..0000000 --- a/packages/cache/src/scheme/transporters/tcp-transporter.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { BaseTransporter } from './base-transporter'; - -export class TcpTransporter implements BaseTransporter {} diff --git a/packages/cache/src/types.ts b/packages/cache/src/types.ts deleted file mode 100644 index 179491b..0000000 --- a/packages/cache/src/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Cache } from './cache'; -import type { CacheAdapter } from './scheme/adapters/cache-adapter'; - -import type { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; - -// - -export type CacheOptions = Pick< - CO, - Exclude -> & - Partial; - -export interface CO { - /** - * Adapter to be used for storing resources - * @default MemoryCacheAdapter - */ - - adapter: CacheAdapter; -} - -// - -export type MemoryOptions = Pick< - MO, - Exclude -> & - Partial; - -export interface MO { - /** - * Time the resource will be stored - * @default 3600000 - */ - - expire: number; -} - -// - -export type RedisOptions = Pick< - RO, - Exclude -> & - Partial; - -export interface RO { - /** - * Time the resource will be stored - * @default 300 - */ - - expire: number; -} diff --git a/packages/cache/src/utils/options.ts b/packages/cache/src/utils/options.ts deleted file mode 100644 index 17ca5da..0000000 --- a/packages/cache/src/utils/options.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Needs to be moved to a common location - * refactor - */ - -const isPlainObject = (value: any) => { - return ( - (value !== null && - typeof value === 'object' && - typeof value.constructor === 'function' && - // eslint-disable-next-line no-prototype-builtins - (value.constructor.prototype.hasOwnProperty('isPrototypeOf') || - Object.getPrototypeOf(value.constructor.prototype) === null)) || - (value && Object.getPrototypeOf(value) === null) - ); -}; - -const isObject = (o: any) => { - return !!o && typeof o === 'object' && !Array.isArray(o); -}; - -export const Options = (defaults: any, ...options: any[]): any => { - if (!options.length) { - return defaults; - } - - const source = options.shift(); - - if (isObject(defaults) && isPlainObject(source)) { - Object.entries(source).forEach(([key, value]) => { - if (typeof value === 'undefined') { - return; - } - - if (isPlainObject(value)) { - if (!(key in defaults)) { - Object.assign(defaults, { [key]: {} }); - } - - Options(defaults[key], value); - } else { - Object.assign(defaults, { [key]: value }); - } - }); - } - - return Options(defaults, ...options); -}; diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json deleted file mode 100644 index 9b4f197..0000000 --- a/packages/cache/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["src/**/*"] -} diff --git a/packages/cache/tsup.config.ts b/packages/cache/tsup.config.ts deleted file mode 100644 index 2e8c9eb..0000000 --- a/packages/cache/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'tsup'; - -const isProduction = process.env.NODE_ENV === 'production'; - -export default defineConfig({ - clean: true, - dts: true, - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - minify: isProduction, - sourcemap: false, -}); diff --git a/packages/common/README.MD b/packages/common/README.MD new file mode 100644 index 0000000..b7813fb --- /dev/null +++ b/packages/common/README.MD @@ -0,0 +1,20 @@ +# @biscuitland/common +## Most importantly, biscuit's common is: +Custom types, functions and utility classes, a feature-rich package for sharing in the biscuit libraries + +[](https://github.com/oasisjs/biscuit) +[](https://discord.gg/XNw2RZFzaP) + +biscuit + +## Install (for [node18](https://nodejs.org/en/download/)) + +```sh-session +npm install @biscuitland/common +yarn add @biscuitland/common +``` +## Links +* [Website](https://biscuitjs.com/) +* [Documentation](https://docs.biscuitjs.com/) +* [Discord](https://discord.gg/XNw2RZFzaP) +* [core](https://www.npmjs.com/package/@biscuitland/core) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 0000000..51aa644 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,75 @@ +{ + "name": "@biscuitland/common", + "version": "0.0.1", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "type": "module", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist && rm -rf .turbo", + "dev": "tsup --watch" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": "./dist/index.cjs" + } + }, + "devDependencies": { + "tsup": "^6.1.3" + }, + "license": "Apache-2.0", + "author": "Yuzuru ", + "contributors": [ + { + "name": "Yuzuru", + "url": "https://github.com/yuzudev" + }, + { + "name": "miia", + "url": "https://github.com/dragurimu" + }, + { + "name": "n128", + "url": "https://github.com/nicolito128" + }, + { + "name": "socram03", + "url": "https://github.com/socram03", + "author": true + }, + { + "name": "Drylozu", + "url": "https://github.com/Drylozu" + } + ], + "homepage": "https://biscuitjs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/oasisjs/biscuit.git" + }, + "bugs": { + "url": "https://github.com/oasisjs/biscuit" + }, + "keywords": [ + "api", + "discord", + "bots", + "typescript", + "botdev" + ], + "publishConfig": { + "access": "public" + }, + "dependencies": { + "discord-api-types": "^0.37.39" + } +} diff --git a/packages/common/src/Collection.ts b/packages/common/src/Collection.ts new file mode 100644 index 0000000..bb15a2a --- /dev/null +++ b/packages/common/src/Collection.ts @@ -0,0 +1,163 @@ +// https://github.com/discordeno/discordeno/blob/main/packages/utils/src/Collection.ts +export class Collection extends Map { + /** + * The maximum amount of items allowed in this collection. To disable cache, set it 0, set to undefined to make it infinite. + * @default undefined + */ + maxSize: number | undefined; + /** Handler to remove items from the collection every so often. */ + sweeper: (CollectionSweeper & { intervalId?: NodeJS.Timer }) | undefined; + + constructor(entries?: (ReadonlyArray | null) | Map, options?: CollectionOptions) { + super(entries ?? []); + + this.maxSize = options?.maxSize; + + if (!options?.sweeper) return; + + this.startSweeper(options.sweeper); + } + + startSweeper(options: CollectionSweeper): NodeJS.Timer { + if (this.sweeper?.intervalId) clearInterval(this.sweeper.intervalId); + + this.sweeper = options; + this.sweeper.intervalId = setInterval(() => { + this.forEach((value, key) => { + if (!this.sweeper?.filter(value, key)) return; + + this.delete(key); + return key; + }); + }, options.interval); + + return this.sweeper.intervalId; + } + + stopSweeper(): void { + clearInterval(this.sweeper?.intervalId); + } + + changeSweeperInterval(newInterval: number): void { + if (this.sweeper == null) return; + + this.startSweeper({ filter: this.sweeper.filter, interval: newInterval }); + } + + changeSweeperFilter(newFilter: (value: V, key: K) => boolean): void { + if (this.sweeper == null) return; + + this.startSweeper({ filter: newFilter, interval: this.sweeper.interval }); + } + + /** Add an item to the collection. Makes sure not to go above the maxSize. */ + set(key: K, value: V): this { + // When this collection is maxSized make sure we can add first + if ((this.maxSize !== undefined || this.maxSize === 0) && this.size >= this.maxSize) { + return this; + } + + return super.set(key, value); + } + + /** Add an item to the collection, no matter what the maxSize is. */ + forceSet(key: K, value: V): this { + return super.set(key, value); + } + + /** Convert the collection to an array. */ + array(): V[] { + return [...this.values()]; + } + + /** Retrieve the value of the first element in this collection. */ + first(): V | undefined { + return this.values().next().value; + } + + /** Retrieve the value of the last element in this collection. */ + last(): V | undefined { + return [...this.values()][this.size - 1]; + } + + /** Retrieve the value of a random element in this collection. */ + random(): V | undefined { + const array = [...this.values()]; + return array[Math.floor(Math.random() * array.length)]; + } + + /** Find a specific element in this collection. */ + find(callback: (value: V, key: K) => boolean): NonNullable | undefined { + for (const key of this.keys()) { + const value = this.get(key)!; + if (callback(value, key)) return value; + } + return undefined; + } + + /** Find all elements in this collection that match the given pattern. */ + filter(callback: (value: V, key: K) => boolean): Collection { + const relevant = new Collection(); + this.forEach((value, key) => { + if (callback(value, key)) relevant.set(key, value); + }); + + return relevant; + } + + /** Converts the collection into an array by running a callback on all items in the collection. */ + map(callback: (value: V, key: K) => T): T[] { + const results = []; + for (const key of this.keys()) { + const value = this.get(key)!; + results.push(callback(value, key)); + } + return results; + } + + /** Check if one of the items in the collection matches the pattern. */ + some(callback: (value: V, key: K) => boolean): boolean { + for (const key of this.keys()) { + const value = this.get(key)!; + if (callback(value, key)) return true; + } + + return false; + } + + /** Check if all of the items in the collection matches the pattern. */ + every(callback: (value: V, key: K) => boolean): boolean { + for (const key of this.keys()) { + const value = this.get(key)!; + if (!callback(value, key)) return false; + } + + return true; + } + + /** Runs a callback on all items in the collection, merging them into a single value. */ + reduce(callback: (accumulator: T, value: V, key: K) => T, initialValue?: T): T { + let accumulator: T = initialValue!; + + for (const key of this.keys()) { + const value = this.get(key)!; + accumulator = callback(accumulator, value, key); + } + + return accumulator; + } +} + +export interface CollectionOptions { + /** Handler to clean out the items in the collection every so often. */ + sweeper?: CollectionSweeper; + /** The maximum number of items allowed in the collection. */ + maxSize?: number; +} + +export interface CollectionSweeper { + /** The filter to determine whether an element should be deleted or not */ + filter: (value: V, key: K, ...args: any[]) => boolean; + /** The interval in which the sweeper should run */ + interval: number; +} diff --git a/packages/common/src/Constants.ts b/packages/common/src/Constants.ts new file mode 100644 index 0000000..78152c2 --- /dev/null +++ b/packages/common/src/Constants.ts @@ -0,0 +1,20 @@ +export const DiscordEpoch = 14200704e5; + +export const API_VERSION = '10'; + +export const BASE_URL = `/api/v${API_VERSION}`; +export const BASE_HOST = 'https://discord.com'; + +export const CDN_URL = 'https://cdn.discordapp.com'; + +export const GATEWAY_BASE_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'; + +export const OK_STATUS_CODES = [200, 201, 204, 304]; + +export enum HTTPMethods { + Delete = 'DELETE', + Get = 'GET', + Patch = 'PATCH', + Post = 'POST', + Put = 'PUT' +} diff --git a/packages/common/src/Logger.ts b/packages/common/src/Logger.ts new file mode 100644 index 0000000..7b141bc --- /dev/null +++ b/packages/common/src/Logger.ts @@ -0,0 +1,128 @@ +import { Options, bgBrightWhite, black, bold, cyan, gray, italic, red, yellow } from './Util'; + +export enum LogLevels { + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, + Fatal = 4 +} + +export enum LogDepth { + Minimal = 0, + Full = 1 +} + +export type LoggerOptions = { + logLevel?: LogLevels; + name?: string; + active?: boolean; +}; + +export class Logger { + readonly options: Required; + + constructor(options: LoggerOptions) { + this.options = Options(Logger.DEFAULT_OPTIONS, options); + } + + set level(level: LogLevels) { + this.options.logLevel = level; + } + + get level(): LogLevels { + return this.options.logLevel; + } + + set active(active: boolean) { + this.options.active = active; + } + + get active(): boolean { + return this.options.active; + } + + set name(name: string) { + this.options.name = name; + } + + get name(): string { + return this.options.name; + } + + rawLog(level: LogLevels, ...args: unknown[]) { + if (!this.active) return; + if (level < this.level) return; + + const color = Logger.colorFunctions.get(level) ?? Logger.noColor; + + const date = new Date(); + const log = [ + bgBrightWhite(black(`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`)), + color(Logger.prefixes.get(level) ?? 'DEBUG'), + this.name ? `${this.name} >` : '>', + ...args + ]; + + switch (level) { + case LogLevels.Debug: + return console.debug(...log); + case LogLevels.Info: + return console.info(...log); + case LogLevels.Warn: + return console.warn(...log); + case LogLevels.Error: + return console.error(...log); + case LogLevels.Fatal: + return console.error(...log); + default: + return console.log(...log); + } + } + + debug(...args: any[]) { + this.rawLog(LogLevels.Debug, ...args); + } + + info(...args: any[]) { + this.rawLog(LogLevels.Info, ...args); + } + + warn(...args: any[]) { + this.rawLog(LogLevels.Warn, ...args); + } + + error(...args: any[]) { + this.rawLog(LogLevels.Error, ...args); + } + + fatal(...args: any[]) { + this.rawLog(LogLevels.Fatal, ...args); + } + + static DEFAULT_OPTIONS: Required = { + logLevel: LogLevels.Info, + name: 'BISCUIT', + active: true + }; + + static noColor(msg: string) { + return msg; + } + + static colorFunctions = new Map string>([ + [LogLevels.Debug, gray], + [LogLevels.Info, cyan], + [LogLevels.Warn, yellow], + [LogLevels.Error, (str: string) => red(str)], + [LogLevels.Fatal, (str: string) => red(bold(italic(str)))] + ]); + + static prefixes = new Map([ + [LogLevels.Debug, 'DEBUG'], + [LogLevels.Info, 'INFO'], + [LogLevels.Warn, 'WARN'], + [LogLevels.Error, 'ERROR'], + [LogLevels.Fatal, 'FATAL'] + ]); +} diff --git a/packages/common/src/Types.ts b/packages/common/src/Types.ts new file mode 100644 index 0000000..ba03139 --- /dev/null +++ b/packages/common/src/Types.ts @@ -0,0 +1,39 @@ +export type Tail = A extends [unknown, ...infer rest] ? rest : A extends [unknown] ? [] : A extends (infer first)[] ? first[] : never; + +export type ValueOf = T[keyof T]; + +export type ArrayFirsElement = A extends [...infer arr] ? arr[0] : never; + +export type RestToKeys = T extends [infer V, ...infer Keys] ? { [K in Extract]: V } : never; + +export type Identify = T extends infer U ? { [K in keyof U]: U[K] } : never; + +export type TypeArray = T | T[]; + +export type When = T extends true ? A : B; + +export type PickPartial = { + [P in keyof T]?: T[P] | undefined; +} & { + [P in K]: T[P]; +}; + +export type MakeRequired = T & { [P in K]-?: T[P] }; + +export type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` + ? `${Lowercase}${Uppercase}${CamelCase}` + : Lowercase; + +export type SnakeCase = S extends `${infer A}${infer Rest}` + ? A extends Uppercase + ? `_${Lowercase}${SnakeCase}` + : `${A}${SnakeCase}` + : Lowercase; + +export type ObjectToLower = Identify<{ + [K in keyof T as CamelCase>]: T[K] extends object ? Identify> : T[K]; +}>; + +export type ObjectToSnake = Identify<{ + [K in keyof T as SnakeCase>]: T[K] extends object ? Identify> : T[K]; +}>; diff --git a/packages/common/src/Util.ts b/packages/common/src/Util.ts new file mode 100644 index 0000000..86f69aa --- /dev/null +++ b/packages/common/src/Util.ts @@ -0,0 +1,596 @@ +import { setTimeout } from 'node:timers/promises'; +import { ObjectToSnake, ObjectToLower } from './Types'; + +const isPlainObject = (value: any) => { + return ( + (value !== null && + typeof value === 'object' && + typeof value.constructor === 'function' && + // eslint-disable-next-line no-prototype-builtins + (value.constructor.prototype.hasOwnProperty('isPrototypeOf') || Object.getPrototypeOf(value.constructor.prototype) === null)) || + (value && Object.getPrototypeOf(value) === null) + ); +}; + +const isObject = (o: any) => { + return !!o && typeof o === 'object' && !Array.isArray(o); +}; + +export const Options = (defaults: any, ...options: any[]): T => { + if (!options.length) { + return defaults; + } + + const source = options.shift(); + + if (isObject(defaults) && isPlainObject(source)) { + Object.entries(source).forEach(([key, value]) => { + if (typeof value === 'undefined') { + return; + } + + if (isPlainObject(value)) { + if (!(key in defaults)) { + Object.assign(defaults, { [key]: {} }); + } + + Options(defaults[key], value); + } else { + Object.assign(defaults, { [key]: value }); + } + }); + } + + return Options(defaults, ...options); +}; +/** + * Convert a camelCase object to snake_case. + * @param target The object to convert. + * @returns The converted object. + */ +export function toSnakeCase(target: Obj): ObjectToSnake { + const result = {}; + for (const [key, value] of Object.entries(target)) { + switch (typeof value) { + case 'string': + case 'bigint': + case 'boolean': + case 'function': + case 'symbol': + case 'undefined': + result[ReplaceRegex.camel(key)] = value; + break; + case 'object': + if (Array.isArray(value)) { + result[ReplaceRegex.camel(key)] = Promise.all(value.map((prop) => toSnakeCase(prop))); + break; + } + if (!Number.isNaN(value)) { + result[ReplaceRegex.camel(key)] = null; + break; + } + result[ReplaceRegex.camel(key)] = toSnakeCase({ ...value }); + break; + } + } + return result as ObjectToSnake; +} + +/** + * Convert a snake_case object to camelCase. + * @param target The object to convert. + * @returns The converted object. + */ +export function toCamelCase(target: Obj): ObjectToLower { + const result = {}; + for (const [key, value] of Object.entries(target)) { + switch (typeof value) { + case 'string': + case 'bigint': + case 'boolean': + case 'function': + case 'symbol': + case 'undefined': + result[ReplaceRegex.snake(key)] = value; + break; + case 'object': + if (Array.isArray(value)) { + result[ReplaceRegex.snake(key)] = Promise.all(value.map((prop) => toCamelCase(prop))); + break; + } + if (!Number.isNaN(value)) { + result[ReplaceRegex.snake(key)] = null; + break; + } + result[ReplaceRegex.snake(key)] = toCamelCase({ ...value }); + break; + } + } + return result as ObjectToLower; +} + +export const ReplaceRegex = { + snake: (s: string) => { + return s.replace(/(_\S)/gi, (a) => a[1].toUpperCase()); + }, + camel: (s: string) => { + return s.replace(/[A-Z]/g, (a) => `_${a.toLowerCase()}`); + } +}; + +// https://github.com/discordeno/discordeno/blob/main/packages/utils/src/colors.ts + +export interface Code { + open: string; + close: string; + regexp: RegExp; +} + +/** RGB 8-bits per channel. Each in range `0->255` or `0x00->0xff` */ +export interface Rgb { + r: number; + g: number; + b: number; +} + +let enabled = true; + +/** + * Set changing text color to enabled or disabled + * @param value + */ +export function setColorEnabled(value: boolean) { + enabled = value; +} + +/** Get whether text color change is enabled or disabled. */ +export function getColorEnabled(): boolean { + return enabled; +} + +/** + * Builds color code + * @param open + * @param close + */ +function code(open: number[], close: number): Code { + return { + open: `\x1b[${open.join(';')}m`, + close: `\x1b[${close}m`, + regexp: new RegExp(`\\x1b\\[${close}m`, 'g') + }; +} + +/** + * Applies color and background based on color code and its associated text + * @param str text to apply color settings to + * @param code color code to apply + */ +function run(str: string, code: Code): string { + return enabled ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` : str; +} + +/** + * Reset the text modified + * @param str text to reset + */ +export function reset(str: string): string { + return run(str, code([0], 0)); +} + +/** + * Make the text bold. + * @param str text to make bold + */ +export function bold(str: string): string { + return run(str, code([1], 22)); +} + +/** + * The text emits only a small amount of light. + * @param str text to dim + */ +export function dim(str: string): string { + return run(str, code([2], 22)); +} + +/** + * Make the text italic. + * @param str text to make italic + */ +export function italic(str: string): string { + return run(str, code([3], 23)); +} + +/** + * Make the text underline. + * @param str text to underline + */ +export function underline(str: string): string { + return run(str, code([4], 24)); +} + +/** + * Invert background color and text color. + * @param str text to invert its color + */ +export function inverse(str: string): string { + return run(str, code([7], 27)); +} + +/** + * Make the text hidden. + * @param str text to hide + */ +export function hidden(str: string): string { + return run(str, code([8], 28)); +} + +/** + * Put horizontal line through the center of the text. + * @param str text to strike through + */ +export function strikethrough(str: string): string { + return run(str, code([9], 29)); +} + +/** + * Set text color to black. + * @param str text to make black + */ +export function black(str: string): string { + return run(str, code([30], 39)); +} + +/** + * Set text color to red. + * @param str text to make red + */ +export function red(str: string): string { + return run(str, code([31], 39)); +} + +/** + * Set text color to green. + * @param str text to make green + */ +export function green(str: string): string { + return run(str, code([32], 39)); +} + +/** + * Set text color to yellow. + * @param str text to make yellow + */ +export function yellow(str: string): string { + return run(str, code([33], 39)); +} + +/** + * Set text color to blue. + * @param str text to make blue + */ +export function blue(str: string): string { + return run(str, code([34], 39)); +} + +/** + * Set text color to magenta. + * @param str text to make magenta + */ +export function magenta(str: string): string { + return run(str, code([35], 39)); +} + +/** + * Set text color to cyan. + * @param str text to make cyan + */ +export function cyan(str: string): string { + return run(str, code([36], 39)); +} + +/** + * Set text color to white. + * @param str text to make white + */ +export function white(str: string): string { + return run(str, code([37], 39)); +} + +/** + * Set text color to gray. + * @param str text to make gray + */ +export function gray(str: string): string { + return brightBlack(str); +} + +/** + * Set text color to bright black. + * @param str text to make bright-black + */ +export function brightBlack(str: string): string { + return run(str, code([90], 39)); +} + +/** + * Set text color to bright red. + * @param str text to make bright-red + */ +export function brightRed(str: string): string { + return run(str, code([91], 39)); +} + +/** + * Set text color to bright green. + * @param str text to make bright-green + */ +export function brightGreen(str: string): string { + return run(str, code([92], 39)); +} + +/** + * Set text color to bright yellow. + * @param str text to make bright-yellow + */ +export function brightYellow(str: string): string { + return run(str, code([93], 39)); +} + +/** + * Set text color to bright blue. + * @param str text to make bright-blue + */ +export function brightBlue(str: string): string { + return run(str, code([94], 39)); +} + +/** + * Set text color to bright magenta. + * @param str text to make bright-magenta + */ +export function brightMagenta(str: string): string { + return run(str, code([95], 39)); +} + +/** + * Set text color to bright cyan. + * @param str text to make bright-cyan + */ +export function brightCyan(str: string): string { + return run(str, code([96], 39)); +} + +/** + * Set text color to bright white. + * @param str text to make bright-white + */ +export function brightWhite(str: string): string { + return run(str, code([97], 39)); +} + +/** + * Set background color to black. + * @param str text to make its background black + */ +export function bgBlack(str: string): string { + return run(str, code([40], 49)); +} + +/** + * Set background color to red. + * @param str text to make its background red + */ +export function bgRed(str: string): string { + return run(str, code([41], 49)); +} + +/** + * Set background color to green. + * @param str text to make its background green + */ +export function bgGreen(str: string): string { + return run(str, code([42], 49)); +} + +/** + * Set background color to yellow. + * @param str text to make its background yellow + */ +export function bgYellow(str: string): string { + return run(str, code([43], 49)); +} + +/** + * Set background color to blue. + * @param str text to make its background blue + */ +export function bgBlue(str: string): string { + return run(str, code([44], 49)); +} + +/** + * Set background color to magenta. + * @param str text to make its background magenta + */ +export function bgMagenta(str: string): string { + return run(str, code([45], 49)); +} + +/** + * Set background color to cyan. + * @param str text to make its background cyan + */ +export function bgCyan(str: string): string { + return run(str, code([46], 49)); +} + +/** + * Set background color to white. + * @param str text to make its background white + */ +export function bgWhite(str: string): string { + return run(str, code([47], 49)); +} + +/** + * Set background color to bright black. + * @param str text to make its background bright-black + */ +export function bgBrightBlack(str: string): string { + return run(str, code([100], 49)); +} + +/** + * Set background color to bright red. + * @param str text to make its background bright-red + */ +export function bgBrightRed(str: string): string { + return run(str, code([101], 49)); +} + +/** + * Set background color to bright green. + * @param str text to make its background bright-green + */ +export function bgBrightGreen(str: string): string { + return run(str, code([102], 49)); +} + +/** + * Set background color to bright yellow. + * @param str text to make its background bright-yellow + */ +export function bgBrightYellow(str: string): string { + return run(str, code([103], 49)); +} + +/** + * Set background color to bright blue. + * @param str text to make its background bright-blue + */ +export function bgBrightBlue(str: string): string { + return run(str, code([104], 49)); +} + +/** + * Set background color to bright magenta. + * @param str text to make its background bright-magenta + */ +export function bgBrightMagenta(str: string): string { + return run(str, code([105], 49)); +} + +/** + * Set background color to bright cyan. + * @param str text to make its background bright-cyan + */ +export function bgBrightCyan(str: string): string { + return run(str, code([106], 49)); +} + +/** + * Set background color to bright white. + * @param str text to make its background bright-white + */ +export function bgBrightWhite(str: string): string { + return run(str, code([107], 49)); +} + +/* Special Color Sequences */ + +/** + * Clam and truncate color codes + * @param n + * @param max number to truncate to + * @param min number to truncate from + */ +function clampAndTruncate(n: number, max = 255, min = 0): number { + return Math.trunc(Math.max(Math.min(n, max), min)); +} + +/** + * Set text color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @param str text color to apply paletted 8bit colors to + * @param color code + */ +export function rgb8(str: string, color: number): string { + return run(str, code([38, 5, clampAndTruncate(color)], 39)); +} + +/** + * Set background color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @param str text color to apply paletted 8bit background colors to + * @param color code + */ +export function bgRgb8(str: string, color: number): string { + return run(str, code([48, 5, clampAndTruncate(color)], 49)); +} + +/** + * Set text color using 24bit rgb. + * `color` can be a number in range `0x000000` to `0xffffff` or + * an `Rgb`. + * + * To produce the color magenta: + * + * ```ts + * import { rgb24 } from "./colors.ts"; + * rgb24("foo", 0xff00ff); + * rgb24("foo", {r: 255, g: 0, b: 255}); + * ``` + * @param str text color to apply 24bit rgb to + * @param color code + */ +export function rgb24(str: string, color: number | Rgb): string { + if (typeof color === 'number') { + return run(str, code([38, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], 39)); + } + return run(str, code([38, 2, clampAndTruncate(color.r), clampAndTruncate(color.g), clampAndTruncate(color.b)], 39)); +} + +/** + * Set background color using 24bit rgb. + * `color` can be a number in range `0x000000` to `0xffffff` or + * an `Rgb`. + * + * To produce the color magenta: + * + * ```ts + * import { bgRgb24 } from "./colors.ts"; + * bgRgb24("foo", 0xff00ff); + * bgRgb24("foo", {r: 255, g: 0, b: 255}); + * ``` + * @param str text color to apply 24bit rgb to + * @param color code + */ +export function bgRgb24(str: string, color: number | Rgb): string { + if (typeof color === 'number') { + return run(str, code([48, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], 49)); + } + return run(str, code([48, 2, clampAndTruncate(color.r), clampAndTruncate(color.g), clampAndTruncate(color.b)], 49)); +} + +// https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js +const ANSI_PATTERN = new RegExp( + [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))' + ].join('|'), + 'g' +); + +/** + * Remove ANSI escape codes from the string. + * @param string to remove ANSI escape codes from + */ +export function stripColor(string: string): string { + return string.replace(ANSI_PATTERN, ''); +} + +export function delay(time: number, result?: T) { + return setTimeout(time, result); +} diff --git a/packages/common/src/applyToClass.ts b/packages/common/src/applyToClass.ts new file mode 100644 index 0000000..e1a3a82 --- /dev/null +++ b/packages/common/src/applyToClass.ts @@ -0,0 +1,29 @@ +export function applyToClass< + T extends new ( + ..._args: ConstructorParameters + ) => InstanceType, + U extends new ( + ..._args: ConstructorParameters + ) => InstanceType + // @ts-expect-error +>(structToApply: T, struct: U, ignore?: (keyof T['prototype'])[]) { + const props = Object.getOwnPropertyNames(structToApply.prototype); + for (const prop of props) { + if (ignore?.includes(prop as keyof T) || prop === 'constructor') continue; + Object.defineProperty(struct.prototype, prop, Object.getOwnPropertyDescriptor(structToApply.prototype, prop)!); + } + return struct as unknown as Struct; +} + +// rome-ignore lint/nursery/noBannedTypes: fix applyToClass typing +export type Struct = Final extends new ( + ..._args: never[] +) => infer F + ? ToMix extends new ( + ..._args: never[] + ) => infer TM + ? new ( + ..._args: ConstructorParameters + ) => F & TM + : never + : never; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts new file mode 100644 index 0000000..e200ec3 --- /dev/null +++ b/packages/common/src/index.ts @@ -0,0 +1,7 @@ +export * from './Constants'; +export * from './Util'; +export * from './Types'; +export * from 'discord-api-types/v10'; +export * from './applyToClass'; +export * from './Collection'; +export * from './Logger'; diff --git a/packages/api-types/tsconfig.json b/packages/common/tsconfig.json similarity index 100% rename from packages/api-types/tsconfig.json rename to packages/common/tsconfig.json diff --git a/packages/api-types/tsup.config.ts b/packages/common/tsup.config.ts similarity index 92% rename from packages/api-types/tsup.config.ts rename to packages/common/tsup.config.ts index 2e8c9eb..dfbea63 100644 --- a/packages/api-types/tsup.config.ts +++ b/packages/common/tsup.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], minify: isProduction, - sourcemap: false, + sourcemap: false }); diff --git a/packages/core/README.md b/packages/core/README.md index 5a73c61..e7c4f0b 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,5 +1,5 @@ # @biscuitland/core -Classes, functions and main structures to create an application with biscuit. Core contains the essentials to launch you to develop your own customized and scalable bot. +Core contains the essentials to launch you to develop your own customized and scalable bot. [](https://github.com/oasisjs/biscuit) [](https://discord.gg/XNw2RZFzaP) @@ -17,29 +17,12 @@ yarn add @biscuitland/core `project/index.js`: ```js import { Session } from '@biscuitland/core'; -import { GatewayIntents } from '@biscuitland/api-types'; +import { GatewayIntentBits } from "discord-api-types/v10"; -const session = new Session({ token: 'your token', intents: GatewayIntents.Guilds }); +const session = new Session({ token: 'your token', intents: GatewayIntentBits.Guilds }); -const commands = [ - { - name: 'ping', - description: 'Replies with pong!' - } -]; - -session.events.on('ready', ({ user }) => { - console.log('Logged in as:', user.tag); - session.upsertApplicationCommands(commands, 'GUILD_ID'); -}); - -session.events.on('interactionCreate', (interaction) => { - if (interaction.isCommand()) { - // your commands go here - if (interaction.commandName === 'ping') { - interaction.respondWith({ content: 'pong!' }); - } - } +session.events.on('READY', (payload) => { + console.log('Logged in as:', payload.user.username); }); session.start(); @@ -60,4 +43,4 @@ B:\project> node --experimental-fetch index.js * [Website](https://biscuitjs.com/) * [Documentation](https://docs.biscuitjs.com/) * [Discord](https://discord.gg/XNw2RZFzaP) -* [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) +* [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) diff --git a/packages/core/package.json b/packages/core/package.json index 12edacb..6f77ec1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,77 +1,78 @@ { - "name": "@biscuitland/core", - "version": "2.3.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": [ - "dist/**" - ], - "scripts": { - "build": "tsup", - "clean": "rm -rf dist && rm -rf .turbo", - "dev": "tsup --watch" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "require": "./dist/index.js" - } - }, - "dependencies": { - "@biscuitland/api-types": "^2.3.0", - "@biscuitland/rest": "^2.3.0", - "@biscuitland/ws": "^2.3.0" - }, - "devDependencies": { - "@types/node": "^18.7.14", - "tsup": "^6.1.3" - }, - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - }, - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "publishConfig": { - "access": "public" - } + "name": "@biscuitland/core", + "version": "3.0.0", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist && rm -rf .turbo", + "dev": "tsup --watch" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": "./dist/index.js" + } + }, + "dependencies": { + "@biscuitland/common": "^0.0.1", + "@biscuitland/rest": "^3.0.0", + "@biscuitland/ws": "^3.0.0", + "eventemitter2": "^6.4.9" + }, + "devDependencies": { + "@types/node": "^18.7.14", + "tsup": "^6.1.3" + }, + "license": "Apache-2.0", + "author": "Yuzuru ", + "contributors": [ + { + "name": "Yuzuru", + "url": "https://github.com/yuzudev" + }, + { + "name": "miia", + "url": "https://github.com/dragurimu" + }, + { + "name": "n128", + "url": "https://github.com/nicolito128" + }, + { + "name": "socram03", + "url": "https://github.com/socram03", + "author": true + }, + { + "name": "Drylozu", + "url": "https://github.com/Drylozu" + } + ], + "homepage": "https://biscuitjs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/oasisjs/biscuit.git" + }, + "bugs": { + "url": "https://github.com/oasisjs/biscuit" + }, + "keywords": [ + "api", + "discord", + "bots", + "typescript", + "botdev" + ], + "publishConfig": { + "access": "public" + } } diff --git a/packages/core/src/adapters/default-event-adapter.ts b/packages/core/src/adapters/default-event-adapter.ts deleted file mode 100644 index 437a556..0000000 --- a/packages/core/src/adapters/default-event-adapter.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { EventAdapter } from './event-adapter'; -import type { Events } from './events'; -import EventEmitter from 'node:events'; - -export class DefaultEventAdapter extends EventEmitter implements EventAdapter { - override on(event: K, func: Events[K]): this; - override on(event: K, func: (...args: unknown[]) => unknown): this { - return super.on(event, func); - } - - override off(event: K, func: Events[K]): this; - override off(event: K, func: (...args: unknown[]) => unknown): this { - return super.off(event, func); - } - - override once(event: K, func: Events[K]): this; - override once(event: K, func: (...args: unknown[]) => unknown): this { - return super.once(event, func); - } - - override emit(event: K, ...params: Parameters): boolean; - override emit(event: K, ...params: unknown[]): boolean { - return super.emit(event, ...params); - } -} diff --git a/packages/core/src/adapters/event-adapter.ts b/packages/core/src/adapters/event-adapter.ts deleted file mode 100644 index 1c5c664..0000000 --- a/packages/core/src/adapters/event-adapter.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Events } from './events'; - -export interface EventAdapter extends Omit { - options?: any; - - emit( - event: K, - ...params: Parameters - ): boolean; - - on( - event: K, - func: Events[K] - ): unknown; - - off( - event: K, - func: Events[K] - ): unknown; - - once( - event: K, - func: Events[K] - ): unknown; -} diff --git a/packages/core/src/adapters/events.ts b/packages/core/src/adapters/events.ts deleted file mode 100644 index 0e9d279..0000000 --- a/packages/core/src/adapters/events.ts +++ /dev/null @@ -1,837 +0,0 @@ -import type { - DiscordAutoModerationActionExecution, - DiscordAutoModerationRule, - DiscordChannel, - DiscordChannelPinsUpdate, - DiscordEmoji, - DiscordGuild, - DiscordGuildBanAddRemove, - DiscordGuildEmojisUpdate, - DiscordGuildMemberAdd, - DiscordGuildMemberRemove, - DiscordGuildMemberUpdate, - DiscordGuildMembersChunk, - DiscordGuildRoleCreate, - DiscordGuildRoleDelete, - DiscordGuildRoleUpdate, - DiscordIntegration, - DiscordIntegrationDelete, - DiscordInteraction, - DiscordInviteCreate, - DiscordInviteDelete, - DiscordMemberWithUser, - DiscordMessage, - DiscordMessageDelete, - DiscordMessageReactionAdd, - DiscordMessageReactionRemove, - DiscordMessageReactionRemoveAll, - DiscordMessageReactionRemoveEmoji, - DiscordPresenceUpdate, - DiscordReady, - DiscordRole, - DiscordScheduledEvent, - DiscordScheduledEventUserAdd, - DiscordScheduledEventUserRemove, - DiscordThreadListSync, - DiscordThreadMembersUpdate, - DiscordThreadMemberUpdate, - DiscordTypingStart, - DiscordUser, - DiscordWebhookUpdate, - DiscordVoiceState, - DiscordVoiceServerUpdate, -} from '@biscuitland/api-types'; - -import type { Session } from '../biscuit'; -import type { Interaction } from '../structures/interactions'; -import type { Snowflake } from '../snowflakes'; - -import { - AutoModerationRule, - AutoModerationExecution, -} from '../structures/automod'; - -import type { Channel } from '../structures/channels'; -import { - ChannelFactory, - GuildChannel, - ThreadChannel, -} from '../structures/channels'; - -import type { DiscordStageInstanceB } from '../structures/stage-instance'; -import { StageInstance } from '../structures/stage-instance'; -import { ScheduledEvent } from '../structures/scheduled-events'; -import { Presence } from '../structures/presence'; - -import { Member, ThreadMember } from '../structures/members'; - -import { Message } from '../structures/message'; -import { User } from '../structures/user'; -import { Integration } from '../structures/integration'; - -import { Guild } from '../structures/guilds'; -import { InteractionFactory } from '../structures/interactions'; -import type { InviteCreate } from '../structures/invite'; -import { NewInviteCreate } from '../structures/invite'; - -import type { - MessageReactionAdd, - MessageReactionRemove, - MessageReactionRemoveAll, - MessageReactionRemoveEmoji, -} from '../structures/message-reaction'; - -import { NewMessageReactionAdd } from '../structures/message-reaction'; -import { Util, PartialMessage } from '../utils/util'; - -export type RawHandler = (...args: [Session, number, T]) => void; -export type Handler = ( - ...args: T -) => unknown; - -export const READY: RawHandler = (session, shardId, payload) => { - session.applicationId = payload.application.id; - session.botId = payload.user.id; - session.events.emit( - 'ready', - { ...payload, user: new User(session, payload.user) }, - shardId - ); -}; - -export const MESSAGE_CREATE: RawHandler = ( - session, - _shardId, - message -) => { - session.events.emit('messageCreate', new Message(session, message)); -}; - -export const MESSAGE_UPDATE: RawHandler = ( - session, - _shardId, - new_message -) => { - // message is partial - if (Util.isPartialMessage(new_message)) { - const message = { - session, - id: new_message.id, - guildId: new_message.guild_id, - channelId: new_message.channel_id, - fields: new_message, - }; //satisfies Partial; - - // all methods of Message can run on partial messages - // we aknowledge people that their callback could be partial but giving them all functions of Message - Object.setPrototypeOf(message, Message.prototype); - - session.events.emit('messageUpdate', message); - return; - } - - if (Util.isFullMessage(new_message)) { - session.events.emit('messageUpdate', { - ...new Message(session, new_message), - fields: {} as PartialMessage - }); - } -}; - -export const MESSAGE_DELETE: RawHandler = ( - session, - _shardId, - { id, channel_id, guild_id } -) => { - session.events.emit('messageDelete', { - id, - channelId: channel_id, - guildId: guild_id, - }); -}; - -export const GUILD_CREATE: RawHandler = ( - session, - _shardId, - guild -) => { - session.events.emit('guildCreate', new Guild(session, guild)); -}; - -export const GUILD_DELETE: RawHandler = ( - session, - _shardId, - guild -) => { - session.events.emit('guildDelete', { id: guild.id, unavailable: true }); -}; - -export const GUILD_MEMBER_ADD: RawHandler = ( - session, - _shardId, - member -) => { - session.events.emit( - 'guildMemberAdd', - new Member(session, member, member.guild_id) - ); -}; - -export const GUILD_MEMBER_UPDATE: RawHandler = ( - session, - _shardId, - member -) => { - session.events.emit( - 'guildMemberUpdate', - new Member(session, member, member.guild_id) - ); -}; - -export const GUILD_MEMBER_REMOVE: RawHandler = ( - session, - _shardId, - member -) => { - session.events.emit( - 'guildMemberRemove', - new User(session, member.user), - member.guild_id - ); -}; - -export const GUILD_BAN_ADD: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildBanAdd', { - guildId: data.guild_id, - user: data.user, - }); -}; - -export const GUILD_BAN_REMOVE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildBanRemove', { - guildId: data.guild_id, - user: data.user, - }); -}; - -export const GUILD_EMOJIS_UPDATE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildEmojisUpdate', { - guildId: data.guild_id, - emojis: data.emojis, - }); -}; - -export const GUILD_ROLE_CREATE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildRoleCreate', { - guildId: data.guild_id, - role: data.role, - }); -}; - -export const GUILD_ROLE_UPDATE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildRoleUpdate', { - guildId: data.guild_id, - role: data.role, - }); -}; - -export const GUILD_ROLE_DELETE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('guildRoleDelete', { - guildId: data.guild_id, - roleId: data.role_id, - }); -}; - -export const TYPING_START: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('typingStart', { - channelId: payload.channel_id, - guildId: payload.guild_id ? payload.guild_id : undefined, - userId: payload.user_id, - timestamp: payload.timestamp, - member: payload.guild_id - ? new Member( - session, - payload.member as DiscordMemberWithUser, - payload.guild_id - ) - : undefined, - }); -}; - -export const INTERACTION_CREATE: RawHandler = ( - session, - _shardId, - interaction -) => { - session.events.emit( - 'interactionCreate', - InteractionFactory.from(session, interaction) - ); -}; - -export const CHANNEL_CREATE: RawHandler = ( - session, - _shardId, - channel -) => { - session.events.emit('channelCreate', ChannelFactory.from(session, channel)); -}; - -export const CHANNEL_UPDATE: RawHandler = ( - session, - _shardId, - channel -) => { - session.events.emit('channelUpdate', ChannelFactory.from(session, channel)); -}; - -export const CHANNEL_DELETE: RawHandler = ( - session, - _shardId, - channel -) => { - if (!channel.guild_id) { - return; - } - - session.events.emit( - 'channelDelete', - new GuildChannel(session, channel, channel.guild_id) - ); -}; - -export const THREAD_CREATE: RawHandler = ( - session, - _shardId, - channel -) => { - if (!channel.guild_id) { - return; - } - - session.events.emit( - 'threadCreate', - new ThreadChannel(session, channel, channel.guild_id) - ); -}; - -export const THREAD_UPDATE: RawHandler = ( - session, - _shardId, - channel -) => { - if (!channel.guild_id) { - return; - } - - session.events.emit( - 'threadUpdate', - new ThreadChannel(session, channel, channel.guild_id) - ); -}; - -export const THREAD_DELETE: RawHandler = ( - session, - _shardId, - channel -) => { - if (!channel.guild_id) { - return; - } - - session.events.emit( - 'threadDelete', - new ThreadChannel(session, channel, channel.guild_id) - ); -}; - -export const THREAD_MEMBER_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('threadMemberUpdate', { - guildId: payload.guild_id, - id: payload.id, - userId: payload.user_id, - joinedAt: payload.joined_at, - flags: payload.flags, - }); -}; - -export const THREAD_MEMBERS_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('threadMembersUpdate', { - memberCount: payload.member_count, - addedMembers: payload.added_members - ? payload.added_members.map(tm => new ThreadMember(session, tm)) - : undefined, - removedMemberIds: payload.removed_member_ids - ? payload.removed_member_ids - : undefined, - guildId: payload.guild_id, - id: payload.id, - }); -}; - -export const THREAD_LIST_SYNC: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('threadListSync', { - guildId: payload.guild_id, - channelIds: payload.channel_ids ?? [], - threads: payload.threads.map( - channel => new ThreadChannel(session, channel, payload.guild_id) - ), - members: payload.members.map( - member => new ThreadMember(session, member) - ), - }); -}; - -export const CHANNEL_PINS_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('channelPinsUpdate', { - guildId: payload.guild_id, - channelId: payload.channel_id, - lastPinTimestamp: payload.last_pin_timestamp - ? Date.parse(payload.last_pin_timestamp) - : undefined, - }); -}; - -export const USER_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('userUpdate', new User(session, payload)); -}; - -export const PRESENCE_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('presenceUpdate', new Presence(session, payload)); -}; - -export const WEBHOOKS_UPDATE: RawHandler = ( - session, - _shardId, - webhook -) => { - session.events.emit('webhooksUpdate', { - guildId: webhook.guild_id, - channelId: webhook.channel_id, - }); -}; - -export const INTEGRATION_CREATE: RawHandler< - DiscordIntegration & { guildId?: Snowflake } -> = (session, _shardId, payload) => { - session.events.emit('integrationCreate', new Integration(session, payload)); -}; - -export const INTEGRATION_UPDATE: RawHandler< - DiscordIntegration & { guildId?: Snowflake } -> = (session, _shardId, payload) => { - session.events.emit('integrationCreate', new Integration(session, payload)); -}; - -export const INTEGRATION_DELETE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit('integrationDelete', { - id: payload.id, - guildId: payload.guild_id, - applicationId: payload.application_id, - }); -}; - -export const AUTO_MODERATION_RULE_CREATE: RawHandler< - DiscordAutoModerationRule -> = (session, _shardId, payload) => { - session.events.emit( - 'autoModerationRuleCreate', - new AutoModerationRule(session, payload) - ); -}; - -export const AUTO_MODERATION_RULE_UPDATE: RawHandler< - DiscordAutoModerationRule -> = (session, _shardId, payload) => { - session.events.emit( - 'autoModerationRuleUpdate', - new AutoModerationRule(session, payload) - ); -}; - -export const AUTO_MODERATION_RULE_DELETE: RawHandler< - DiscordAutoModerationRule -> = (session, _shardId, payload) => { - session.events.emit( - 'autoModerationRuleDelete', - new AutoModerationRule(session, payload) - ); -}; - -export const AUTO_MODERATION_ACTION_EXECUTE: RawHandler< - DiscordAutoModerationActionExecution -> = (session, _shardId, payload) => { - session.events.emit( - 'autoModerationActionExecution', - new AutoModerationExecution(session, payload) - ); -}; - -export const MESSAGE_REACTION_ADD: RawHandler = ( - session, - _shardId, - reaction -) => { - session.events.emit( - 'messageReactionAdd', - NewMessageReactionAdd(session, reaction) - ); -}; - -export const MESSAGE_REACTION_REMOVE: RawHandler< - DiscordMessageReactionRemove -> = (session, _shardId, reaction) => { - session.events.emit( - 'messageReactionRemove', - NewMessageReactionAdd(session, reaction) - ); -}; - -export const MESSAGE_REACTION_REMOVE_ALL: RawHandler< - DiscordMessageReactionRemoveAll -> = (session, _shardId, reaction) => { - session.events.emit( - 'messageReactionRemoveAll', - NewMessageReactionAdd(session, reaction as DiscordMessageReactionAdd) - ); -}; - -export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler< - DiscordMessageReactionRemoveEmoji -> = (session, _shardId, reaction) => { - session.events.emit( - 'messageReactionRemoveEmoji', - NewMessageReactionAdd(session, reaction as DiscordMessageReactionAdd) - ); -}; - -export const INVITE_CREATE: RawHandler = ( - session, - _shardId, - invite -) => { - session.events.emit('inviteCreate', NewInviteCreate(session, invite)); -}; - -export const INVITE_DELETE: RawHandler = ( - session, - _shardId, - data -) => { - session.events.emit('inviteDelete', { - channelId: data.channel_id, - guildId: data.guild_id, - code: data.code, - }); -}; - -export const STAGE_INSTANCE_CREATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'stageInstanceCreate', - new StageInstance(session, payload) - ); -}; - -export const STAGE_INSTANCE_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'stageInstanceUpdate', - new StageInstance(session, payload) - ); -}; - -export const STAGE_INSTANCE_DELETE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'stageInstanceDelete', - new StageInstance(session, payload) - ); -}; - -export const GUILD_SCHEDULED_EVENT_CREATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'guildScheduledEventCreate', - new ScheduledEvent(session, payload) - ); -}; - -export const GUILD_SCHEDULED_EVENT_UPDATE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'guildScheduledEventUpdate', - new ScheduledEvent(session, payload) - ); -}; - -export const GUILD_SCHEDULED_EVENT_DELETE: RawHandler = ( - session, - _shardId, - payload -) => { - session.events.emit( - 'guildScheduledEventDelete', - new ScheduledEvent(session, payload) - ); -}; - -export const GUILD_SCHEDULED_EVENT_USER_ADD: RawHandler< - DiscordScheduledEventUserAdd -> = (session, _shardId, payload) => { - session.events.emit('guildScheduledEventUserAdd', { - scheduledEventId: payload.guild_scheduled_event_id, - userId: payload.user_id, - guildId: payload.guild_id, - }); -}; - -export const GUILD_SCHEDULED_EVENT_USER_REMOVE: RawHandler< - DiscordScheduledEventUserRemove -> = (session, _shardId, payload) => { - session.events.emit('guildScheduledEventUserRemove', { - scheduledEventId: payload.guild_scheduled_event_id, - userId: payload.user_id, - guildId: payload.guild_id, - }); -}; - -export const VOICE_STATE_UPDATE: RawHandler = (session, _shardId, payload) => { - if (!payload.guild_id) { return; } - session.events.emit('voiceStateUpdate', payload); -}; - -export const VOICE_SERVER_UPDATE: RawHandler = (session, _shardId, payload) => { - session.events.emit('voiceServerUpdate', { - token: payload.token, - guildId: payload.guild_id, - endpoint: payload.endpoint ? payload.endpoint : undefined - }); -}; - -export const GUILD_MEMBERS_CHUNK: RawHandler = (session, _shardId, payload) => { - session.events.emit('guildMembersChunk', { - guildId: payload.guild_id, - members: new Map(payload.members.map(m => [m.user.id, new Member(session, m, payload.guild_id)])), - chunkIndex: payload.chunk_index, - chunkCount: payload.chunk_count, - notFound: payload.not_found, - presences: payload.presences?.map(p => new Presence(session, p)) ?? [], - }); -}; - -export const raw: RawHandler = (session, shardId, data) => { - session.events.emit('raw', data as { t: string; d: unknown }, shardId); -}; - -export interface Ready extends Omit { - user: User; -} - -export interface GuildMembersChunk { - guildId: Snowflake; - members: Map; - chunkIndex: number; - chunkCount: number; - notFound?: string[]; - presences: Presence[]; - nonce?: string; -} - -/** - * Alias of `keyof Events`. List of all events that can be emitted by the session. - * Ex: ready, messageCreate, guildCreate, etc. - * @see {@link Events} -*/ -export type AllEvents = keyof Events; - -export interface Events { - ready: Handler<[Ready, number]>; - messageCreate: Handler<[Message]>; - messageUpdate: Handler<[Partial & { fields: PartialMessage }]>; - messageDelete: Handler< - [{ id: Snowflake; channelId: Snowflake; guildId?: Snowflake }] - >; - messageReactionAdd: Handler<[MessageReactionAdd]>; - messageReactionRemove: Handler<[MessageReactionRemove]>; - messageReactionRemoveAll: Handler<[MessageReactionRemoveAll]>; - messageReactionRemoveEmoji: Handler<[MessageReactionRemoveEmoji]>; - guildCreate: Handler<[Guild]>; - guildDelete: Handler<[{ id: Snowflake; unavailable: boolean }]>; - guildMemberAdd: Handler<[Member]>; - guildMemberUpdate: Handler<[Member]>; - guildMemberRemove: Handler<[User, Snowflake]>; - guildMembersChunk: Handler<[GuildMembersChunk]>; - guildBanAdd: Handler<[{ guildId: Snowflake; user: DiscordUser }]>; - guildBanRemove: Handler<[{ guildId: Snowflake; user: DiscordUser }]>; - guildEmojisUpdate: Handler< - [{ guildId: Snowflake; emojis: DiscordEmoji[] }] - >; - guildRoleCreate: Handler<[{ guildId: Snowflake; role: DiscordRole }]>; - guildRoleUpdate: Handler<[{ guildId: Snowflake; role: DiscordRole }]>; - guildRoleDelete: Handler<[{ guildId: Snowflake; roleId: Snowflake }]>; - typingStart: Handler< - [ - { - channelId: Snowflake; - guildId?: Snowflake; - userId: Snowflake; - timestamp: number; - member?: Member; - } - ] - >; - channelCreate: Handler<[Channel]>; - channelUpdate: Handler<[Channel]>; - channelDelete: Handler<[GuildChannel]>; - channelPinsUpdate: Handler< - [ - { - guildId?: Snowflake; - channelId: Snowflake; - lastPinTimestamp?: number; - } - ] - >; - threadCreate: Handler<[ThreadChannel]>; - threadUpdate: Handler<[ThreadChannel]>; - threadDelete: Handler<[ThreadChannel]>; - threadListSync: Handler< - [ - { - guildId: Snowflake; - channelIds: Snowflake[]; - threads: ThreadChannel[]; - members: ThreadMember[]; - } - ] - >; - threadMemberUpdate: Handler< - [ - { - id: Snowflake; - userId: Snowflake; - guildId: Snowflake; - joinedAt: string; - flags: number; - } - ] - >; - threadMembersUpdate: Handler< - [ - { - id: Snowflake; - memberCount: number; - addedMembers?: ThreadMember[]; - guildId: Snowflake; - removedMemberIds?: Snowflake[]; - } - ] - >; - interactionCreate: Handler<[Interaction]>; - integrationCreate: Handler<[Integration]>; - integrationUpdate: Handler<[Integration]>; - integrationDelete: Handler< - [{ id: Snowflake; guildId?: Snowflake; applicationId?: Snowflake }] - >; - inviteCreate: Handler<[InviteCreate]>; - inviteDelete: Handler< - [{ channelId: string; guildId?: string; code: string }] - >; - autoModerationRuleCreate: Handler<[AutoModerationRule]>; - autoModerationRuleUpdate: Handler<[AutoModerationRule]>; - autoModerationRuleDelete: Handler<[AutoModerationRule]>; - autoModerationActionExecution: Handler<[AutoModerationExecution]>; - stageInstanceCreate: Handler<[StageInstance]>; - stageInstanceUpdate: Handler<[StageInstance]>; - stageInstanceDelete: Handler<[StageInstance]>; - guildScheduledEventCreate: Handler<[ScheduledEvent]>; - guildScheduledEventUpdate: Handler<[ScheduledEvent]>; - guildScheduledEventDelete: Handler<[ScheduledEvent]>; - guildScheduledEventUserAdd: Handler< - [{ scheduledEventId: Snowflake; userId: Snowflake; guildId: Snowflake }] - >; - guildScheduledEventUserRemove: Handler< - [{ scheduledEventId: Snowflake; userId: Snowflake; guildId: Snowflake }] - >; - raw: Handler<[{ t: string; d: unknown }, number]>; - webhooksUpdate: Handler<[{ guildId: Snowflake; channelId: Snowflake }]>; - userUpdate: Handler<[User]>; - presenceUpdate: Handler<[Presence]>; - debug: Handler<[string]>; - voiceStateUpdate: Handler<[DiscordVoiceState]>; - voiceServerUpdate: Handler< - [{ token: string; guildId: Snowflake; endpoint?: string }] - >; -} diff --git a/packages/core/src/biscuit.ts b/packages/core/src/biscuit.ts deleted file mode 100644 index 4872048..0000000 --- a/packages/core/src/biscuit.ts +++ /dev/null @@ -1,496 +0,0 @@ -import type { - AtLeastOne, - ApplicationCommandPermissionTypes, - DiscordApplicationCommand, - // DiscordGatewayPayload, - DiscordGuildApplicationCommandPermissions, - DiscordUser, - DiscordApplicationCommandOption, - GatewayIntents, - Localization, - Snowflake, - DiscordGetGatewayBot, - DiscordGatewayPayload -} from '@biscuitland/api-types'; - -import { ApplicationCommandTypes, GatewayOpcodes , - - APPLICATION_COMMANDS, - GUILD_APPLICATION_COMMANDS, - GUILD_APPLICATION_COMMANDS_PERMISSIONS, - GUILD_APPLICATION_COMMANDS_LOCALIZATIONS, - USER -} from '@biscuitland/api-types'; - -// routes - -import type { PermissionResolvable } from './structures/special/permissions'; -import type { Activities, StatusTypes } from './structures/presence'; - -// structs - -import { User } from './structures/user'; - -// DiscordGetGatewayBot; - -import type { RestAdapter } from '@biscuitland/rest'; -import { DefaultRestAdapter } from '@biscuitland/rest'; - -import type { Shard } from '@biscuitland/ws'; -import { ShardManager, Options as mergeOptions } from '@biscuitland/ws'; - -import type { EventAdapter } from './adapters/event-adapter'; -import { DefaultEventAdapter } from './adapters/default-event-adapter'; - -import { Util } from './utils/util'; - -// PRESENCE - -/** - * @link https://discord.com/developers/docs/topics/gateway#update-status - */ -export interface StatusUpdate { - activities: Activities[]; - status: keyof typeof StatusTypes; -} - -// END PRESENCE - -// INTERACTIONS - -export type CreateApplicationCommands = CreateApplicationCommand | CreateContextApplicationCommand; -export type UpsertDataApplicationCommands = - | AtLeastOne - | AtLeastOne; -export type LastCreateApplicationCommands = - | AtLeastOne - | AtLeastOne; - -/** - * @link https://discord.com/developers/docs/interactions/application-commands#endpoints-json-params - */ -export interface CreateApplicationCommand { - name: string; - name_localizations?: Localization; - description: string; - description_localizations?: Localization; - type?: ApplicationCommandTypes; - options?: DiscordApplicationCommandOption[]; - default_member_permissions?: PermissionResolvable; - dm_permission?: boolean; -} - -/** - * @link https://discord.com/developers/docs/interactions/application-commands#endpoints-json-params - */ -export interface CreateContextApplicationCommand extends Omit { - type: ApplicationCommandTypes.Message | ApplicationCommandTypes.User; -} - -/** - * @link https://discord.com/developers/docs/interactions/application-commands#endpoints-query-string-params - */ -export interface GetApplicationCommand { - guildId?: Snowflake; - withLocalizations?: boolean; -} - -export interface UpsertApplicationCommands extends CreateApplicationCommand { - id?: Snowflake; -} - -/** - * @link https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions - */ -export interface ApplicationCommandPermissions { - id: Snowflake; - type: ApplicationCommandPermissionTypes; - permission: boolean; -} - -/** - * @link https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions - */ -export interface ApplicationCommandPermissions { - id: Snowflake; - type: ApplicationCommandPermissionTypes; - permission: boolean; -} - -// END INTERACTIONS - -export type DiscordRawEventHandler = ( - shard: Shard, - data: MessageEvent -) => unknown; - -export type PickOptions = Pick< - BiscuitOptions, - Exclude -> & - Partial; - -export interface BiscuitOptions { - intents?: GatewayIntents; - token: string; - precense?: { - status: keyof typeof StatusTypes; - afk: boolean; - since: number | null; - activities: Activities[]; - }; - events?: { - adapter?: { new (...args: any[]): EventAdapter }; - options: any; - }; - - rest: { - adapter?: { new (...args: any[]): RestAdapter }; - options: any; - }; - - ws: { - adapter?: { new (...args: any[]): ShardManager }; - options: any; - }; -} - -import * as Actions from './adapters/events'; - -export class Session { - private _applicationId?: Snowflake; - private _botId?: Snowflake; - token: string; - - set botId(snowflake: Snowflake) { - this._botId = snowflake; - } - - get botId(): Snowflake { - return this._botId ?? Util.getBotIdFromToken(this.token); - } - - set applicationId(snowflake: Snowflake) { - this._applicationId = snowflake; - } - - get applicationId(): Snowflake { - return this._applicationId ?? this.botId; - } - - static readonly DEFAULTS = { - intents: 0, - rest: { - adapter: DefaultRestAdapter, - options: null, - }, - ws: { - adapter: ShardManager, - options: null, - } - }; - - options: BiscuitOptions; - - readonly events: EventAdapter; - - readonly rest: RestAdapter; - readonly ws: ShardManager; - - private adapters = new Map(); - - constructor(options: PickOptions) { - this.options = mergeOptions(Session.DEFAULTS, options); - - // makeRest - - if (!this.options.rest.options) { - this.options.rest.options = { - intents: this.options.intents, - token: this.options.token, - }; - } - - this.rest = this.getRest(); - - // makeWs - - const defHandler = (shard: Shard, payload: DiscordGatewayPayload) => { - Actions.raw(this, shard.options.id, payload); - - if (!payload.t || !payload.d) { - return; - } - - Actions[payload.t as keyof typeof Actions]?.( - this, - shard.options.id, - payload.d as any - ); - }; - - if (!this.options.ws.options) { - this.options.ws.options = { - handleDiscordPayload: defHandler, - - gateway: { - url: '', - shards: '', - - session_start_limit: { - total: 1000, - remaining: 1000, - reset_after: 3600000, - - max_concurrency: 1 - } - }, - config: { - token: this.options.token, - intents: this.options.intents, - }, - makePresence: this.options.precense - }; - } - - // makeEvents - - this.events = this.options.events?.adapter ? new this.options.events.adapter() : new DefaultEventAdapter(); - - this.ws = this.getWs(); - this.token = options.token; - } - - /** - * @inheritDoc - */ - - private getAdapter }>( - adapter: T, - ...args: ConstructorParameters - ): InstanceType { - if (!this.adapters.has(adapter.name)) { - const Class = adapter as { new (...args: any[]): T }; - this.adapters.set(adapter.name, new Class(...args)); - } - - return this.adapters.get(adapter.name); - } - - /** - * @inheritDoc - */ - - private getRest(): RestAdapter { - return this.getAdapter( - this.options.rest.adapter!, - this.options.rest.options - ); - } - - /** - * @inheritDoc - */ - - private getWs(): ShardManager { - return this.getAdapter( - this.options.ws.adapter!, - this.options.ws.options - ); - } - - /** - * @inheritDoc - */ - - async start(): Promise { - const gateway = await this.rest.get('/gateway/bot'); - this.ws.options.gateway = gateway; - - this.ws.spawns(); - } - - // USEFUL METHODS - - async editProfile(nick?: string, avatar?: string): Promise { - const user = await this.rest.patch(USER(), { - username: nick ?? null, - avatar: avatar ?? null, - }); - - return new User(this, user); - } - - // END USEFUL METHODS - - // PRESENCE - - - /** - * Edit bot's status - * tip: execute this on the ready event if possible - * @example - * for (const { id } of session.gateway.manager.shards) { - * session.editStatus(id, data); - * } - */ - editStatus(shardId: number, status: StatusUpdate, prio = true): void { - const shard = this.ws.shards.get(shardId); - - if (!shard) { - throw new Error(`Unknown shard ${shardId}`); - } - - shard.send({ - op: GatewayOpcodes.PresenceUpdate, - d: { - status: status.status, - since: null, - afk: false, - activities: this.makePresenceActivites(status.activities), - }, - }, prio); - } - - private makePresenceActivites(act: Activities[]): Record[] { - return act.map(activity => { - return { - name: activity.name, - type: activity.type, - url: activity.url, - created_at: activity.createdAt, - timestamps: activity.timestamps, - application_id: this.applicationId, - details: activity.details, - state: activity.state, - emoji: activity.emoji && { - name: activity.emoji.name, - id: activity.emoji.id, - animated: activity.emoji.animated, - }, - party: activity.party, - assets: activity.assets && - { - large_image: activity.assets.largeImage, - large_text: activity.assets.largeText, - small_image: activity.assets.smallImage, - small_text: activity.assets.smallText, - }, - secrets: activity.secrets, - instance: activity.instance, - flags: activity.flags, - buttons: activity.buttons, - }; - }); - } - - // END PRESENCE - - // INTERACTIONS - - updateApplicationCommandPermissions( - guildId: Snowflake, - id: Snowflake, - bearerToken: string, - options: ApplicationCommandPermissions[], - ): Promise { - return this.rest.post( - GUILD_APPLICATION_COMMANDS_PERMISSIONS(this.applicationId, guildId, id), - { - permissions: options, - }, - { - headers: { Authorization: `Bearer ${bearerToken}` }, - }, - ); - } - - fetchApplicationCommand(id: Snowflake, options?: GetApplicationCommand): Promise { - return this.rest.get( - options?.guildId - ? GUILD_APPLICATION_COMMANDS_LOCALIZATIONS( - this.applicationId, - options.guildId, - id, - options?.withLocalizations, - ) - : APPLICATION_COMMANDS(this.applicationId, id), - ); - } - - fetchApplicationCommandPermissions(guildId: Snowflake): Promise { - return this.rest.get( - GUILD_APPLICATION_COMMANDS_PERMISSIONS(this.applicationId, guildId), - ); - } - - fetchApplicationCommandPermission( - guildId: Snowflake, - id: Snowflake, - ): Promise { - return this.rest.get( - GUILD_APPLICATION_COMMANDS_PERMISSIONS(this.applicationId, guildId, id), - ); - } - - upsertApplicationCommand( - id: Snowflake, - options: UpsertDataApplicationCommands, - guildId?: Snowflake, - ): Promise { - return this.rest.patch( - guildId - ? GUILD_APPLICATION_COMMANDS(this.applicationId, guildId) - : APPLICATION_COMMANDS(this.applicationId, id), - this.isContextApplicationCommand(options) - ? { - name: options.name, - type: options.type, - } - : { - name: options.name, - description: options.description, - type: options.type, - options: options.options, - }, - ); - } - - upsertApplicationCommands( - options: UpsertDataApplicationCommands[], - guildId?: Snowflake, - ): Promise { - return this.rest.put( - guildId - ? GUILD_APPLICATION_COMMANDS(this.applicationId, guildId) - : APPLICATION_COMMANDS(this.applicationId), - options.map(o => - this.isContextApplicationCommand(o) - ? { - name: o.name, - type: o.type, - } - : { - name: o.name, - description: o.description, - type: o.type, - options: o.options, - } - ), - ); - } - - fetchCommands(guildId?: Snowflake): Promise { - return this.rest.get( - guildId - ? GUILD_APPLICATION_COMMANDS(this.applicationId, guildId) - : APPLICATION_COMMANDS(this.applicationId), - ); - } - - isContextApplicationCommand(cmd: LastCreateApplicationCommands): cmd is AtLeastOne { - return cmd.type === ApplicationCommandTypes.Message || cmd.type === ApplicationCommandTypes.User; - } - - // END INTERACTIONS -} diff --git a/packages/core/src/events/handler.ts b/packages/core/src/events/handler.ts new file mode 100644 index 0000000..28f1082 --- /dev/null +++ b/packages/core/src/events/handler.ts @@ -0,0 +1,15 @@ +import type { Session } from '../index'; +import type { GatewayEvents } from '@biscuitland/ws'; + +export function actionHandler([session, payload, shardId]: Parameters) { + // @ts-expect-error At this point, typescript sucks + session.emit(payload.t, payload.d, shardId); +} + +export type ActionHandler = ( + ...args: [Session, { t: G; d: GatewayEvents[G] }, number] +) => unknown; + +export type Handler = { + [K in keyof GatewayEvents]: (...args: [GatewayEvents[K], number]) => unknown; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dc05416..21584ea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,21 +1,6 @@ -// SESSION -export * as Actions from './adapters/events'; - -export { Session as Biscuit } from './biscuit'; -export * from './biscuit'; - -// STRUCTURES -export * from './structures'; - -// EVENTS -export * from './adapters/events'; -export * from './adapters/event-adapter'; -export * from './adapters/default-event-adapter'; - -// ETC -export * from './snowflakes'; - -// UTIL -export * from './utils/calculate-shard'; -export * from './utils/url-to-base-64'; -export * from './utils/util'; +export * from './utils/types'; +export * from './utils/utils'; +export * from './session'; +export { Session as Biscuit } from './session'; +export * from './managers/MainManager'; +export * from './events/handler'; diff --git a/packages/core/src/managers/ApplicationManager.ts b/packages/core/src/managers/ApplicationManager.ts new file mode 100644 index 0000000..d3a9a6f --- /dev/null +++ b/packages/core/src/managers/ApplicationManager.ts @@ -0,0 +1,121 @@ +import { + APIApplicationCommand, + Identify, + MakeRequired, + RESTGetAPIApplicationCommandsQuery, + RESTGetAPIApplicationGuildCommandsQuery, + RESTPatchAPIApplicationCommandJSONBody, + RESTPatchAPIApplicationGuildCommandJSONBody, + RESTPostAPIApplicationCommandsJSONBody, + RESTPostAPIApplicationGuildCommandsJSONBody, + RESTPutAPIApplicationCommandPermissionsJSONBody, + RESTPutAPIApplicationCommandsJSONBody, + RESTPutAPIApplicationGuildCommandsJSONBody, + RESTPutAPIApplicationRoleConnectionMetadataJSONBody +} from '@biscuitland/common'; +import { Session } from '..'; + +export class ApplicationManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + getBotGateway() { + return this.session.api.gateway.bot.get(); + } + + getGateway() { + return this.session.api.gateway.get(); + } + + getNitroStickerPacks() { + return this.session.api['sticker-packs'].get(); + } + + getRoleConnectionMetadata(applicationId: string) { + return this.session.api.applications(applicationId)['role-connections'].metadata.get(); + } + + editRoleConnectionMetadata(applicationId: string, body: RESTPutAPIApplicationRoleConnectionMetadataJSONBody) { + return this.session.api.applications(applicationId)['role-connections'].metadata.put({ body }); + } + + getCommands( + applicationId: string, + query: RESTGetAPIApplicationCommandsQuery + ): Promise; + getCommands(applicationId: string, query?: RESTGetAPIApplicationCommandsQuery) { + return this.session.api.applications(applicationId).commands.get({ query }); + } + + createCommand(applicationId: string, body: RESTPostAPIApplicationCommandsJSONBody) { + return this.session.api.applications(applicationId).commands.post({ body }); + } + + getCommand(applicationId: string, commandId: string) { + return this.session.api.applications(applicationId).commands(commandId).get(); + } + + editCommand(applicationId: string, commandId: string, body: RESTPatchAPIApplicationCommandJSONBody) { + return this.session.api.applications(applicationId).commands(commandId).patch({ body }); + } + + deleteCommand(applicationId: string, commandId: string) { + return this.session.api.applications(applicationId).commands(commandId).delete(); + } + + bulkCommands(applicationId: string, body: RESTPutAPIApplicationCommandsJSONBody) { + return this.session.api.applications(applicationId).commands.put({ body }); + } + + getCommandPermissions(applicationId: string, guildId: string, commandId: string) { + return this.session.api.applications(applicationId).guilds(guildId).commands(commandId).permissions.get(); + } + + editCommandPermissions(applicationId: string, guildId: string, commandId: string, body: RESTPutAPIApplicationCommandPermissionsJSONBody) { + return this.session.api.applications(applicationId).guilds(guildId).commands(commandId).permissions.put({ body }); + } + + getGuildCommands( + applicationId: string, + guildId: string, + query: RESTGetAPIApplicationGuildCommandsQuery + ): Promise; + getGuildCommands(applicationId: string, guildId: string, query?: RESTGetAPIApplicationGuildCommandsQuery) { + return this.session.api.applications(applicationId).guilds(guildId).commands.get({ query }); + } + + createGuildCommand(applicationId: string, guildId: string, body: RESTPostAPIApplicationGuildCommandsJSONBody) { + return this.session.api.applications(applicationId).guilds(guildId).commands.post({ body }); + } + + getGuildCommand(applicationId: string, guildId: string, commandId: string) { + return this.session.api.applications(applicationId).guilds(guildId).commands(commandId).get(); + } + + editGuildCommand(applicationId: string, guildId: string, commandId: string, body: RESTPatchAPIApplicationGuildCommandJSONBody) { + return this.session.api.applications(applicationId).guilds(guildId).commands(commandId).patch({ body }); + } + + deleteGuildCommand(applicationId: string, guildId: string, commandId: string) { + return this.session.api.applications(applicationId).guilds(guildId).commands(commandId).delete(); + } + + bulkGuildCommands(applicationId: string, guildId: string, body: RESTPutAPIApplicationGuildCommandsJSONBody) { + return this.session.api.applications(applicationId).guilds(guildId).commands.put({ body }); + } + + getGuildCommandPermissions(applicationId: string, guildId: string) { + return this.session.api.applications(applicationId).guilds(guildId).commands.permissions.get(); + } +} + +export type RESTGetAPIApplicationCommandsWithLocalizationsResult = Identify< + MakeRequired +>[]; + +export type RESTGetAPIApplicationGuildCommandsWithLocalizationsResult = RESTGetAPIApplicationCommandsWithLocalizationsResult; diff --git a/packages/core/src/managers/ChannelManager.ts b/packages/core/src/managers/ChannelManager.ts new file mode 100644 index 0000000..85229ce --- /dev/null +++ b/packages/core/src/managers/ChannelManager.ts @@ -0,0 +1,214 @@ +import type { + APIChannel, + RESTPostAPIChannelMessageJSONBody, + RESTPatchAPIChannelJSONBody, + RESTGetAPIChannelThreadsArchivedQuery, + RESTGetAPIChannelMessageReactionUsersQuery, + RESTPatchAPIChannelMessageJSONBody, + RESTPostAPIChannelMessagesBulkDeleteJSONBody, + RESTPutAPIChannelPermissionJSONBody, + RESTPostAPIChannelInviteJSONBody, + RESTPostAPIChannelFollowersJSONBody, + RESTPutAPIChannelRecipientJSONBody, + RESTPostAPIChannelMessagesThreadsJSONBody, + RESTPostAPIChannelThreadsJSONBody, + RESTPostAPIChannelThreadsResult, + RESTPostAPIGuildForumThreadsJSONBody, + RESTGetAPIChannelThreadMembersQuery, + RESTGetAPIChannelThreadMemberQuery, + RESTPostAPIChannelWebhookJSONBody, + RESTPatchAPIStageInstanceJSONBody +} from '@biscuitland/common'; +import type { RawFile } from '@biscuitland/rest'; + +import type { Session } from '../session'; + +export class ChannelManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + get(id: string) { + return this.session.api.channels(id).get() as Promise; + } + + getWebhooks(channelId: string) { + return this.session.api.channels(channelId).webhooks.get(); + } + + createWebhook(channelId: string, body: RESTPostAPIChannelWebhookJSONBody) { + return this.session.api.channels(channelId).webhooks.post({ body }); + } + + edit(id: string, data: RESTPatchAPIChannelJSONBody) { + return this.session.api.channels(id).patch({ body: data }); + } + + delete(id: string) { + return this.session.api.channels(id).delete(); + } + + getMessages(id: string, limit = 50) { + return this.session.api.channels(id).messages.get({ + query: { limit } + }); + } + + getMessage(id: string, messageId: string) { + return this.session.api.channels(id).messages(messageId).get(); + } + + createMessage(id: string, data: RESTPostAPIChannelMessageJSONBody) { + return this.session.api.channels(id).messages.post({ body: data }); + } + + sendTyping(id: string) { + return this.session.api.channels(id).typing.post(); + } + + getArchivedThreads(channelId: string, options: RESTGetAPIChannelThreadsArchivedOptions) { + const { type, ...query } = options; + if (type === 'private') { + return this.session.api.channels(channelId).threads.archived.private.get({ query }); + } + + return this.session.api.channels(channelId).threads.archived.public.get({ query }); + } + + crosspostMessage(channelId: string, messageId: string) { + return this.session.api.channels(channelId).messages(messageId).crosspost.post({}); + } + + createReaction(channelId: string, messageId: string, emoji: string) { + return this.session.api.channels(channelId).messages(messageId).reactions(emoji)('@me').put({}); + } + + deleteReaction(channelId: string, messageId: string, emoji: string, user = '@me') { + return this.session.api.channels(channelId).messages(messageId).reactions(emoji)(user).delete(); + } + + getReactions(channelId: string, messageId: string, emoji: string, query?: RESTGetAPIChannelMessageReactionUsersQuery) { + return this.session.api.channels(channelId).messages(messageId).reactions(emoji).get({ query }); + } + + deleteAllReactions(channelId: string, messageId: string, emoji?: string) { + if (emoji?.length) return this.session.api.channels(channelId).messages(messageId).reactions(emoji).delete(); + return this.session.api.channels(channelId).messages(messageId).reactions.delete(); + } + + editMessage(channelId: string, messageId: string, body: RESTPatchAPIChannelMessageJSONBody, files?: RawFile[]) { + return this.session.api.channels(channelId).messages(messageId).patch({ + body, + files + }); + } + + deleteMessage(channelId: string, messageId: string, reason?: string) { + return this.session.api.channels(channelId).messages(messageId).delete({ reason }); + } + + bulkMessages(channelId: string, body: RESTPostAPIChannelMessagesBulkDeleteJSONBody, reason?: string) { + return this.session.api.channels(channelId).messages['bulk-delete'].post({ body, reason }); + } + + editPermissions(channelId: string, overwriteId: string, body: RESTPutAPIChannelPermissionJSONBody, reason?: string) { + return this.session.api.channels(channelId).permissions(overwriteId).put({ body, reason }); + } + + deletePermission(channelId: string, overwriteId: string, reason?: string) { + return this.session.api.channels(channelId).permissions(overwriteId).delete({ reason }); + } + + getInvites(channelId: string) { + return this.session.api.channels(channelId).invites.get(); + } + + createInvite(channelId: string, body: RESTPostAPIChannelInviteJSONBody) { + return this.session.api.channels(channelId).invites.post({ body }); + } + + followAnnoucement(channelId: string, body: RESTPostAPIChannelFollowersJSONBody) { + return this.session.api.channels(channelId).followers.post({ body }); + } + + getPinnedMessages(channelId: string) { + return this.session.api.channels(channelId).pins.get(); + } + + pinMessage(channelId: string, messageId: string, reason?: string) { + return this.session.api.channels(channelId).pins(messageId).put({ reason }); + } + + unpinMessage(channelId: string, messageId: string, reason?: string) { + return this.session.api.channels(channelId).pins(messageId).delete({ reason }); + } + + groupDMAddRecipient(channelId: string, userId: string, body: RESTPutAPIChannelRecipientJSONBody) { + return this.session.api.channels(channelId).recipients(userId).put({ body }); + } + + groupDMRemoveRecipient(channelId: string, userId: string) { + return this.session.api.channels(channelId).recipients(userId).delete(); + } + + startThreadFromMessage(channelId: string, messageId: string, body: RESTPostAPIChannelMessagesThreadsJSONBody, reason?: string) { + return this.session.api.channels(channelId).messages(messageId).threads.post({ body, reason }); + } + + startThread(channelId: string, body: RESTPostAPIChannelThreadsJSONBody, reason?: string): Promise; + startThread(channelId: string, body: RESTPostAPIGuildForumThreadsJSONBody, reason?: string) { + return this.session.api.channels(channelId).threads.post({ body, reason }); + } + + getListJoinedPrivateArchivedThreads(channelId: string, query?: RESTGetAPIChannelThreadsArchivedQuery) { + return this.session.api.channels(channelId).users('@me').threads.archived.private.get({ query }); + } + + getThreadMembers(channelId: string, query?: RESTGetAPIChannelThreadMembersQuery) { + return this.session.api.channels(channelId)['thread-members'].get({ query }); + } + + getThreadMember(channelId: string, userId: string, query?: RESTGetAPIChannelThreadMemberQuery) { + return this.session.api.channels(channelId)['thread-members'](userId).get({ query }); + } + + addThreadMember(channelId: string, userId: string) { + return this.session.api.channels(channelId)['thread-members'](userId).put({}); + } + + removeThreadMember(channelId: string, userId: string) { + return this.session.api.channels(channelId)['thread-members'](userId).delete(); + } + + leaveThread(channelId: string) { + return this.session.api.channels(channelId)['thread-members']('@me').delete(); + } + + joinThread(channelId: string) { + return this.session.api.channels(channelId)['thread-members']('@me').put({}); + } + + getVoiceRegions() { + return this.session.api.voice.region.get(); + } + + getStageInstance(channelId: string) { + return this.session.api['stage-instances'](channelId).get(); + } + + editStageInstance(channelId: string, body: RESTPatchAPIStageInstanceJSONBody, reason?: string) { + return this.session.api['stage-instances'](channelId).patch({ body, reason }); + } + + deleteStageInstance(channelId: string, reason?: string) { + return this.session.api['stage-instances'](channelId).delete({ reason }); + } +} + +export type RESTGetAPIChannelThreadsArchivedOptions = { + type: 'private' | 'public'; +} & RESTGetAPIChannelThreadsArchivedQuery; diff --git a/packages/core/src/managers/GuildManager.ts b/packages/core/src/managers/GuildManager.ts new file mode 100644 index 0000000..0ec418c --- /dev/null +++ b/packages/core/src/managers/GuildManager.ts @@ -0,0 +1,348 @@ +import type { Session } from '../session'; +import type { + GuildMFALevel, + APIGuildChannel, + GuildChannelType, + RESTPostAPIGuildPruneJSONBody, + RESTPostAPIGuildsJSONBody, + RESTPatchAPIGuildJSONBody, + RESTPostAPIGuildChannelJSONBody, + RESTPatchAPIGuildChannelPositionsJSONBody, + RESTGetAPIGuildBansQuery, + RESTPutAPIGuildBanJSONBody, + RESTPostAPIGuildRoleJSONBody, + RESTPatchAPIGuildRolePositionsJSONBody, + RESTPatchAPIGuildRoleJSONBody, + RESTPatchAPIGuildWidgetSettingsJSONBody, + RESTPatchAPIGuildWelcomeScreenJSONBody, + RESTGetAPIGuildPruneCountQuery, + RESTGetAPIAuditLogQuery, + RESTPostAPIAutoModerationRuleJSONBody, + RESTPatchAPIAutoModerationRuleJSONBody, + RESTPostAPITemplateCreateGuildJSONBody, + RESTGetAPIGuildMembersQuery, + RESTGetAPIGuildMembersSearchQuery, + RESTPatchAPICurrentGuildMemberJSONBody, + RESTPutAPIGuildMemberJSONBody, + RESTPatchAPIGuildMemberJSONBody, + RESTGetAPIGuildWidgetImageQuery, + RESTPatchAPIGuildEmojiJSONBody, + RESTPostAPIGuildEmojiJSONBody, + RESTPatchAPIGuildVoiceStateUserJSONBody, + RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody, + RESTPatchAPIGuildStickerJSONBody, + RESTPostAPIGuildStickerFormDataBody, + RESTGetAPIGuildScheduledEventsQuery, + RESTPatchAPIGuildScheduledEventJSONBody, + RESTPostAPIGuildScheduledEventJSONBody, + RESTGetAPIGuildScheduledEventQuery, + RESTGetAPIGuildScheduledEventUsersQuery, + RESTPatchAPIGuildTemplateJSONBody, + RESTPostAPIGuildTemplatesJSONBody +} from '@biscuitland/common'; + +export class GuildManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + get(guildId: string) { + return this.session.api.guilds(guildId).get(); + } + + create(options: RESTPostAPIGuildsJSONBody) { + return this.session.api.guilds.post({ body: options }); + } + + delete(guildId: string) { + return this.session.api.guilds(guildId).delete(); + } + + edit(guildId: string, options: RESTPatchAPIGuildJSONBody) { + return this.session.api.guilds(guildId).patch({ body: options }); + } + + getChannels(guildId: string) { + return this.session.api.guilds(guildId).channels.get(); + } + + createChannel>(guildId: string, body: RESTPostAPIGuildChannelJSONBody) { + return this.session.api.guilds(guildId).channels.post({ body }) as Promise; + } + + editChannelPositions(guildId: string, body: RESTPatchAPIGuildChannelPositionsJSONBody): Promise { + return this.session.api.guilds(guildId).channels.patch({ body }); + } + + getThreads(guildId: string) { + return this.session.api.guilds(guildId).threads.active.get(); + } + + getBans(guildId: string, query: RESTGetAPIGuildBansQuery = {}) { + return this.session.api.guilds(guildId).bans.get({ + query + }); + } + + getBan(guildId: string, userId: string) { + return this.session.api.guilds(guildId).bans(userId).get(); + } + + createBan(guildId: string, userId: string, body: RESTPutAPIGuildBanJSONBody) { + return this.session.api.guilds(guildId).bans(userId).put({ body }); + } + + removeBan(guildId: string, userId: string) { + return this.session.api.guilds(guildId).bans(userId).delete(); + } + + getRoles(guildId: string) { + return this.session.api.guilds(guildId).roles.get(); + } + + createRole(guildId: string, options: RESTPostAPIGuildRoleJSONBody) { + return this.session.api.guilds(guildId).roles.post({ body: options }); + } + + editRolePositions(guildId: string, options: RESTPatchAPIGuildRolePositionsJSONBody) { + return this.session.api.guilds(guildId).roles.patch({ body: options }); + } + + editRole(guildId: string, roleId: string, options: RESTPatchAPIGuildRoleJSONBody) { + return this.session.api.guilds(guildId).roles(roleId).patch({ body: options }); + } + + deleteRole(guildId: string, roleId: string) { + return this.session.api.guilds(guildId).roles(roleId).delete(); + } + + editGuildMFALevel(guildId: string, level: GuildMFALevel) { + return this.session.api.guilds(guildId).mfa.post({ body: { level } }); + } + + getPruneCount(guildId: string, query: RESTGetAPIGuildPruneCountQuery) { + return this.session.api.guilds(guildId).prune.get({ + query + }); + } + + beginGuildPrune(guildId: string, options: RESTPostAPIGuildPruneJSONBody) { + return this.session.api.guilds(guildId).prune.post({ body: options }); + } + + getVoiceRegions(guildId: string) { + return this.session.api.guilds(guildId).regions.get(); + } + + getInvites(guildId: string) { + return this.session.api.guilds(guildId).invites.get(); + } + + getIntegrations(guildId: string) { + return this.session.api.guilds(guildId).integrations.get(); + } + + deleteIntegration(guildId: string, integrationId: string) { + return this.session.api.guilds(guildId).integrations(integrationId).delete(); + } + + getWidget(guildId: string) { + return this.session.api.guilds(guildId).widget.get(); + } + + editWidget(guildId: string, options: RESTPatchAPIGuildWidgetSettingsJSONBody) { + return this.session.api.guilds(guildId).widget.patch({ body: options }); + } + + getVanityUrl(guildId: string) { + return this.session.api.guilds(guildId)['vanity-url'].get(); + } + + getWelcomeScreen(guildId: string) { + return this.session.api.guilds(guildId)['welcome-screen'].get(); + } + + editWelcomeScreen(guildId: string, options: RESTPatchAPIGuildWelcomeScreenJSONBody) { + return this.session.api.guilds(guildId)['welcome-screen'].patch({ body: options }); + } + + getAuditLog(guildId: string, query?: RESTGetAPIAuditLogQuery) { + return this.session.api.guilds(guildId)['audit-logs'].get({ query }); + } + + getAutoModerationRules(guildId: string) { + return this.session.api.guilds(guildId)['auto-moderation'].rules.get(); + } + + getAutoModerationRule(guildId: string, ruleId: string) { + return this.session.api.guilds(guildId)['auto-moderation'].rules(ruleId).get(); + } + + createAutoModerationRule(guildId: string, body: RESTPostAPIAutoModerationRuleJSONBody, reason?: string) { + return this.session.api.guilds(guildId)['auto-moderation'].rules.post({ body, reason }); + } + + editAutoModerationRule(guildId: string, body: RESTPatchAPIAutoModerationRuleJSONBody, reason?: string) { + return this.session.api.guilds(guildId)['auto-moderation'].rules.patch({ body, reason }); + } + + deleteAutoModerationRule(guildId: string, ruleId: string, reason?: string) { + return this.session.api.guilds(guildId)['auto-moderation'].rules(ruleId).delete({ reason }); + } + + getTemplate(code: string) { + return this.session.api.guilds.templates(code).get(); + } + + createTemplate(code: string, body: RESTPostAPITemplateCreateGuildJSONBody) { + return this.session.api.guilds.templates(code).post({ body }); + } + + getWebhooks(guildId: string) { + return this.session.api.guilds(guildId).webhooks.get(); + } + + getPreview(guildId: string) { + return this.session.api.guilds(guildId).preview.get(); + } + + getMembers(guildId: string, query?: RESTGetAPIGuildMembersQuery) { + return this.session.api.guilds(guildId).members.get({ query }); + } + + searchMembers(guildId: string, query?: RESTGetAPIGuildMembersSearchQuery) { + return this.session.api.guilds(guildId).members.search.get({ query }); + } + + editCurrentMember(guildId: string, body: RESTPatchAPICurrentGuildMemberJSONBody) { + return this.session.api.guilds(guildId).members['@me'].patch({ body }); + } + + getMember(guildId: string, memberId: string) { + return this.session.api.guilds(guildId).members(memberId).get(); + } + + addMember(guildId: string, memberId: string, body: RESTPutAPIGuildMemberJSONBody) { + return this.session.api.guilds(guildId).members(memberId).put({ body }); + } + + editMember(guildId: string, memberId: string, body: RESTPatchAPIGuildMemberJSONBody) { + return this.session.api.guilds(guildId).members(memberId).patch({ body }); + } + + removeMember(guildId: string, memberId: string) { + return this.session.api.guilds(guildId).members(memberId).delete(); + } + + addRoleMember(guildId: string, memberId: string, roleId: string) { + return this.session.api.guilds(guildId).members(memberId).roles(roleId).put({}); + } + + removeRoleMember(guildId: string, memberId: string, roleId: string) { + return this.session.api.guilds(guildId).members(memberId).roles(roleId).delete({}); + } + + getWidgetJson(guildId: string) { + return this.session.api.guilds(guildId)['widget.json'].get(); + } + + getWidgetPng(guildId: string, query?: RESTGetAPIGuildWidgetImageQuery) { + return this.session.api.guilds(guildId)['widget.png'].get({ query }); + } + + getEmojis(guildId: string) { + return this.session.api.guilds(guildId).emojis.get(); + } + + createEmoji(guildId: string, body: RESTPostAPIGuildEmojiJSONBody) { + return this.session.api.guilds(guildId).emojis.post({ body }); + } + + getEmoji(guildId: string, emojiId: string) { + return this.session.api.guilds(guildId).emojis(emojiId).get(); + } + + editEmoji(guildId: string, emojiId: string, body: RESTPatchAPIGuildEmojiJSONBody) { + return this.session.api.guilds(guildId).emojis(emojiId).patch({ body }); + } + + deleteEmoji(guildId: string, emojiId: string) { + return this.session.api.guilds(guildId).emojis(emojiId).delete(); + } + + editCurrentVoiceState(guildId: string, body: RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody) { + return this.session.api.guilds(guildId)['voice-states']['@me'].patch({ body }); + } + + editMemberVoiceState(guildId: string, memberId: string, body: RESTPatchAPIGuildVoiceStateUserJSONBody) { + return this.session.api.guilds(guildId)['voice-states'](memberId).patch({ body }); + } + + getStickers(guildId: string) { + return this.session.api.guilds(guildId).stickers.get(); + } + + createSticker(guildId: string, body: RESTPostAPIGuildStickerFormDataBody) { + return this.session.api.guilds(guildId).stickers.post({ body }); + } + + getSticker(guildId: string, stickerId: string) { + return this.session.api.guilds(guildId).stickers(stickerId).get(); + } + + editSticker(guildId: string, stickerId: string, body: RESTPatchAPIGuildStickerJSONBody) { + return this.session.api.guilds(guildId).stickers(stickerId).patch({ body }); + } + + deleteSticker(guildId: string, stickerId: string) { + return this.session.api.guilds(guildId).stickers(stickerId).delete(); + } + + getScheduledEvents(guildId: string, query?: RESTGetAPIGuildScheduledEventsQuery) { + return this.session.api.guilds(guildId)['scheduled-events'].get({ query }); + } + + createScheduledEvent(guildId: string, body: RESTPostAPIGuildScheduledEventJSONBody) { + return this.session.api.guilds(guildId)['scheduled-events'].post({ body }); + } + + getScheduledEvent(guildId: string, scheduledEvent: string, query?: RESTGetAPIGuildScheduledEventQuery) { + return this.session.api.guilds(guildId)['scheduled-events'](scheduledEvent).get({ query }); + } + + editScheduledEvent(guildId: string, scheduledEvent: string, body: RESTPatchAPIGuildScheduledEventJSONBody) { + return this.session.api.guilds(guildId)['scheduled-events'](scheduledEvent).patch({ body }); + } + + deleteScheduledEvent(guildId: string, scheduledEvent: string) { + return this.session.api.guilds(guildId)['scheduled-events'](scheduledEvent).delete(); + } + + getUsersScheduledEvent(guildId: string, scheduledEvent: string, query: RESTGetAPIGuildScheduledEventUsersQuery) { + return this.session.api.guilds(guildId)['scheduled-events'](scheduledEvent).users.get({ query }); + } + + getGuildTemplates(guildId: string) { + return this.session.api.guilds(guildId).templates.get(); + } + + createGuildTemplate(guildId: string, body: RESTPostAPIGuildTemplatesJSONBody) { + return this.session.api.guilds(guildId).templates.post({ body }); + } + + syncGuildTemplate(guildId: string, code: string) { + return this.session.api.guilds(guildId).templates(code).put({}); + } + + modifyGuildTemaplte(guildId: string, code: string, body: RESTPatchAPIGuildTemplateJSONBody) { + return this.session.api.guilds(guildId).templates(code).patch({ body }); + } + + deleteCodeTemplate(guildId: string, code: string) { + return this.session.api.guilds(guildId).templates(code).delete(); + } +} diff --git a/packages/core/src/managers/InteractionManager.ts b/packages/core/src/managers/InteractionManager.ts new file mode 100644 index 0000000..03759ec --- /dev/null +++ b/packages/core/src/managers/InteractionManager.ts @@ -0,0 +1,57 @@ +import type { + RESTPatchAPIWebhookWithTokenMessageJSONBody, + RESTPostAPIInteractionCallbackJSONBody, + RESTPostAPIInteractionFollowupJSONBody +} from '@biscuitland/common'; +import type { Session } from '..'; +import type { RawFile } from '@biscuitland/rest'; + +export class InteractionManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + reply( + interactionId: string, + token: string, + body: T, + files?: RawFile[] + ) { + return this.session.api.interactions(interactionId)(token).callback.post({ + body, + files + }); + } + + getResponse(applicationId: string, token: string, messageId = '@original') { + return this.session.api.webhooks(applicationId)(token).messages(messageId).get(); + } + + editResponse( + applicationId: string, + token: string, + messageId: string, + body: RESTPatchAPIWebhookWithTokenMessageJSONBody, + files?: RawFile[] + ) { + return this.session.api.webhooks(applicationId)(token).messages(messageId).patch({ + body, + files + }); + } + + deleteResponse(applicationId: string, token: string, messageId = '@original') { + return this.session.api.webhooks(applicationId)(token).messages(messageId).delete(); + } + + followUp(applicationId: string, token: string, body: RESTPostAPIInteractionFollowupJSONBody, files?: RawFile[]) { + return this.session.api.webhooks(applicationId)(token).post({ + body, + files + }); + } +} diff --git a/packages/core/src/managers/MainManager.ts b/packages/core/src/managers/MainManager.ts new file mode 100644 index 0000000..1a346e9 --- /dev/null +++ b/packages/core/src/managers/MainManager.ts @@ -0,0 +1,19 @@ +import type { Session } from '../session'; +import { ChannelManager } from './ChannelManager'; +import { GuildManager } from './GuildManager'; +import { MemberManager } from './MemberManager'; +import { UserManager } from './UserManager'; + +export class MainManager { + constructor(private readonly session: Session) { + this.users = new UserManager(this.session); + this.guilds = new GuildManager(this.session); + this.members = new MemberManager(this.session); + this.channels = new ChannelManager(this.session); + } + + users: UserManager; + guilds: GuildManager; + members: MemberManager; + channels: ChannelManager; +} diff --git a/packages/core/src/managers/MemberManager.ts b/packages/core/src/managers/MemberManager.ts new file mode 100644 index 0000000..879aef5 --- /dev/null +++ b/packages/core/src/managers/MemberManager.ts @@ -0,0 +1,19 @@ +import type { APIGuildMember, MakeRequired } from '@biscuitland/common'; +import type { Session, ImageOptions } from '../index'; +import { formatImageURL } from '../index'; + +export class MemberManager { + constructor(private readonly session: Session) {} + + dynamicAvatarURL({ avatar, guild_id, user }: DynamicMember, { size, format }: ImageOptions): string { + if (avatar?.length) { + return formatImageURL(this.session.cdn.guilds(guild_id).users(user.id).avatars(avatar).get(), size, format); + } + + return this.session.managers.users.avatarURL(user, { size, format }); + } +} + +export type DynamicMember = MakeRequired & { + guild_id: string; +}; diff --git a/packages/core/src/managers/UserManager.ts b/packages/core/src/managers/UserManager.ts new file mode 100644 index 0000000..939d0dc --- /dev/null +++ b/packages/core/src/managers/UserManager.ts @@ -0,0 +1,64 @@ +import type { + APIUser, + RESTGetAPICurrentUserGuildsQuery, + RESTPatchAPICurrentUserJSONBody, + RESTPutAPICurrentUserApplicationRoleConnectionJSONBody +} from '@biscuitland/common'; +import type { Session, ImageOptions } from '../index'; +import { formatImageURL } from '../index'; + +export class UserManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + get(userId = '@me') { + return this.session.api.users(userId).get(); + } + + avatarURL(user: APIUser, { size, format }: ImageOptions) { + if (user.avatar?.length) { + return formatImageURL(this.session.cdn.avatars(user.id).get(user.avatar), size, format); + } + + return formatImageURL(this.session.cdn.embed.avatars.get(Number(user.discriminator) % 5)); + } + + createDM(userId: string) { + return this.session.api.users('@me').channels.post({ body: { recipient_id: userId } }); + } + + editCurrent(body: RESTPatchAPICurrentUserJSONBody) { + return this.session.api.users('@me').patch({ + body + }); + } + + getGuilds(query?: RESTGetAPICurrentUserGuildsQuery) { + return this.session.api.users('@me').guilds.get({ query }); + } + + getGuildMember(guildId: string) { + return this.session.api.users('@me').guilds(guildId).member.get(); + } + + leaveGuild(guildId: string) { + return this.session.api.users('@me').guilds(guildId).delete(); + } + + getConnections() { + return this.session.api.users('@me').connections.get(); + } + + getRoleConnections(applicationId: string) { + return this.session.api.users('@me').applications(applicationId)['role-connection'].get(); + } + + updateRoleConnection(applicationId: string, body: RESTPutAPICurrentUserApplicationRoleConnectionJSONBody) { + return this.session.api.users('@me').applications(applicationId)['role-connection'].put({ body }); + } +} diff --git a/packages/core/src/managers/WebhookManager.ts b/packages/core/src/managers/WebhookManager.ts new file mode 100644 index 0000000..fa8272a --- /dev/null +++ b/packages/core/src/managers/WebhookManager.ts @@ -0,0 +1,124 @@ +import { + Identify, + RESTPatchAPIWebhookJSONBody, + RESTPatchAPIWebhookResult, + RESTPatchAPIWebhookWithTokenJSONBody, + RESTPatchAPIWebhookWithTokenMessageJSONBody, + RESTPatchAPIWebhookWithTokenResult, + RESTPostAPIChannelWebhookJSONBody, + RESTPostAPIWebhookWithTokenGitHubQuery, + RESTPostAPIWebhookWithTokenGitHubWaitResult, + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery, + RESTPostAPIWebhookWithTokenSlackQuery, + RESTPostAPIWebhookWithTokenSlackWaitResult, + RESTPostAPIWebhookWithTokenWaitResult +} from '@biscuitland/common'; +import type { Session } from '..'; + +export class WebhookManager { + readonly session!: Session; + constructor(session: Session) { + Object.defineProperty(this, 'session', { + value: session, + writable: false + }); + } + + create(channelId: string, body: RESTPostAPIChannelWebhookJSONBody) { + return this.session.api.channels(channelId).webhooks.post({ body }); + } + + getChannelWebhooks(channelId: string) { + return this.session.api.channels(channelId).webhooks.get(); + } + + getGuildWebhooks(guildId: string) { + return this.session.api.guilds(guildId).webhooks.get(); + } + + get(webhookdId: string, token?: string) { + if (!token?.length) return this.session.api.webhooks(webhookdId).get(); + return this.session.api.webhooks(webhookdId)(token).get(); + } + + edit(webhookId: string, body: RESTPatchAPIWebhookJSONBody): Promise; + edit(webhookId: string, body: RESTPatchAPIWebhookWithTokenJSONBody, token: string): Promise; + edit(webhookId: string, body: RESTPatchAPIWebhookJSONBody, token?: string) { + if (!token?.length) { + return this.session.api.webhooks(webhookId).patch({ body }); + } + return this.session.api.webhooks(webhookId)(token).patch({ body }); + } + + delete(webhookdId: string, token?: string) { + if (!token?.length) return this.session.api.webhooks(webhookdId).delete(); + return this.session.api.webhooks(webhookdId)(token).delete(); + } + + execute( + webhookId: string, + token: string, + body: RESTPostAPIWebhookWithTokenJSONBody, + query: RESTPostAPIWebhookWithTokenWaitQuery + ): Promise; + execute(webhookId: string, token: string, body: RESTPostAPIWebhookWithTokenJSONBody, query?: RESTPostAPIWebhookWithTokenQuery) { + return this.session.api.webhooks(webhookId)(token).post({ + body, + query + }); + } + + executeGithub( + webhookId: string, + token: string, + body: RESTPostAPIWebhookWithTokenJSONBody, + query: Identify + ): Promise; + executeGithub( + webhookId: string, + token: string, + body: RESTPostAPIWebhookWithTokenJSONBody, + query?: RESTPostAPIWebhookWithTokenGitHubQuery + ) { + return this.session.api.webhooks(webhookId)(token).github.post({ + body, + query + }); + } + + executeSlack( + webhookId: string, + token: string, + body: RESTPostAPIWebhookWithTokenJSONBody, + query: Identify + ): Promise; + executeSlack(webhookId: string, token: string, body: RESTPostAPIWebhookWithTokenJSONBody, query?: RESTPostAPIWebhookWithTokenSlackQuery) { + return this.session.api.webhooks(webhookId)(token).slack.post({ + body, + query + }); + } + + getMessage(webhookId: string, token: string, messageId: string, query?: { thread_id?: string }) { + return this.session.api.webhooks(webhookId)(token).messages(messageId).get({ + query + }); + } + + editMessage( + webhookId: string, + token: string, + messageId: string, + body: RESTPatchAPIWebhookWithTokenMessageJSONBody, + query?: { thread_id?: string } + ) { + return this.session.api.webhooks(webhookId)(token).messages(messageId).patch({ query, body }); + } + + deleteMessage(webhookId: string, token: string, messageId: string, query?: { thread_id?: string }) { + return this.session.api.webhooks(webhookId)(token).messages(messageId).delete({ query }); + } +} + +export type RESTPostAPIWebhookWithTokenWaitQuery = Identify; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts new file mode 100644 index 0000000..21f818c --- /dev/null +++ b/packages/core/src/session.ts @@ -0,0 +1,110 @@ +import type { BiscuitRESTOptions, CDNRoutes, Routes } from '@biscuitland/rest'; +import { CDN, BiscuitREST, Router } from '@biscuitland/rest'; +import { When } from '@biscuitland/common'; +import { EventEmitter2 } from 'eventemitter2'; +import { MainManager, getBotIdFromToken } from '.'; +import { GatewayManager, CreateGatewayManagerOptions, GatewayEvents } from '@biscuitland/ws'; +import { GatewayIntentBits } from '@biscuitland/common'; +import { actionHandler, Handler } from './events/handler'; + +export class Session extends EventEmitter2 { + constructor(public options: BiscuitOptions) { + super(); + this.rest = this.createRest(this.options.rest); + this.api = new Router(this.rest).createProxy(); + this.cdn = CDN.createProxy(); + this.managers = new MainManager(this); + } + rest: BiscuitREST; + api: Routes; + cdn: CDNRoutes; + managers: MainManager; + gateway!: When; + private _applicationId?: string; + private _botId?: string; + + override on(event: `${K}`, func: Handler[K]): this; + override on(event: `${K}`, func: (...args: unknown[]) => unknown): this { + const ev = super.on(event, func); + + // @ts-expect-error Eventemitter can sometimes return a listener + return ev.emitter ? ev.emitter : ev; + } + override off(event: `${K}`, func: Handler[K]): this; + override off(event: `${K}`, func: (...args: unknown[]) => unknown): this { + return super.off(event, func); + } + + override once(event: `${K}`, func: Handler[K]): this; + override once(event: `${K}`, func: (...args: unknown[]) => unknown): this { + const ev = super.on(event, func); + + // @ts-expect-error Eventemitter can sometimes return a listener + return ev.emitter ? ev.emitter : ev; + } + + override emit(event: `${K}`, ...params: Parameters): boolean; + override emit(event: `${K}`, ...params: unknown[]): boolean { + return super.emit(event, ...params); + } + + set botId(id: string) { + this._botId = id; + } + + set applicationId(id: string) { + this._applicationId = id; + } + + get botId() { + return this._botId ?? getBotIdFromToken(this.options.token); + } + + get applicationId() { + return this._applicationId ?? this.botId; + } + + private createRest(rest: any) { + if (!rest) { + return new BiscuitREST({ + ...this.options.defaultRestOptions, + token: this.options.token + }); + } + + if (rest instanceof BiscuitREST || rest.cRest) { + return rest; + } + + throw new Error('[CORE] REST not found'); + } + + async start() { + // alias fixed `this` on handlePayload + const ctx = this as Session; + + ctx.gateway = new GatewayManager({ + token: this.options.token, + intents: this.options.intents ?? 0, + connection: this.options.defaultGatewayOptions?.connection ?? (await this.rest.get('/gateway/bot')), + async handlePayload(shard, data) { + const { t, d } = data; + if (!(t && d)) return; + actionHandler([ctx, { t, d }, shard]); + }, + ...this.options.defaultGatewayOptions + }); + + await ctx.gateway.spawnShards(); + } +} + +export type HandlePayload = Pick['handlePayload']; + +export interface BiscuitOptions { + token: string; + intents: number | GatewayIntentBits; + rest?: BiscuitREST; + defaultRestOptions?: Partial; + defaultGatewayOptions?: Omit; +} diff --git a/packages/core/src/snowflakes.ts b/packages/core/src/snowflakes.ts deleted file mode 100644 index 1b2a918..0000000 --- a/packages/core/src/snowflakes.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** snowflake type */ -export type Snowflake = string; - -/** Discord epoch */ -export const DiscordEpoch = 14200704e5; - -/** utilities for Snowflakes */ -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const Snowflake = { - snowflakeToTimestamp(id: Snowflake): number { - return (Number(id) >> 22) + DiscordEpoch; - }, -}; diff --git a/packages/core/src/structures.ts b/packages/core/src/structures.ts deleted file mode 100644 index 9512260..0000000 --- a/packages/core/src/structures.ts +++ /dev/null @@ -1,34 +0,0 @@ -// STRUCTURES -export * from './structures/application'; -export * from './structures/attachment'; -export * from './structures/automod'; -export * from './structures/base'; -export * from './structures/embed'; -export * from './structures/emojis'; -export * from './structures/scheduled-events'; -export * from './structures/integration'; -export * from './structures/invite'; -export * from './structures/members'; -export * from './structures/message'; -export * from './structures/message-reaction'; -export * from './structures/special/interaction-options'; -export * from './structures/special/permissions'; -export * from './structures/presence'; -export * from './structures/role'; -export * from './structures/stage-instance'; -export * from './structures/sticker'; -export * from './structures/user'; -export * from './structures/webhook'; -export * from './structures/welcome'; - -// INTERACTIONS -export * from './structures/interactions'; - -// CHANNELS -export * from './structures/channels'; - -// COMPONENTS -export * from './structures/components'; - -// GUILDS -export * from './structures/guilds'; diff --git a/packages/core/src/structures/application.ts b/packages/core/src/structures/application.ts deleted file mode 100644 index 57c284d..0000000 --- a/packages/core/src/structures/application.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { - DiscordApplication, - DiscordInstallParams, - DiscordTeam, - DiscordUser, - TeamMembershipStates, -} from '@biscuitland/api-types'; -import { User } from './user'; - -/** - * @internal - */ -export type SummaryDeprecated = ''; - -/** - * Discord team that holds members - */ -export interface Team { - /** a hash of the image of the team's icon */ - icon?: string; - /** the unique id of the team */ - id: string; - /** the members of the team */ - members: TeamMember[]; - /** user id of the current team owner */ - ownerUserId: string; - /** team name */ - name: string; -} - -export interface TeamMember { - /** the user's membership state on the team */ - membershipState: TeamMembershipStates; - permissions: '*'[]; - teamId: string; - user: Partial & - Pick; -} - -// NewTeam create a new Team object for discord applications -export function NewTeam(session: Session, data: DiscordTeam): Team { - return { - icon: data.icon ? data.icon : undefined, - id: data.id, - members: data.members.map(member => { - return { - membershipState: member.membership_state, - permissions: member.permissions, - teamId: member.team_id, - user: new User(session, member.user), - }; - }), - ownerUserId: data.owner_user_id, - name: data.name, - }; -} - -/** - * @link https://discord.com/developers/docs/resources/application#application-object - */ -export class Application implements Model { - constructor(session: Session, data: DiscordApplication) { - this.id = data.id; - this.session = session; - - this.name = data.name; - this.icon = data.icon ?? undefined; - this.description = data.description; - this.rpcOrigins = data.rpc_origins; - this.botPublic = data.bot_public; - this.botRequireCodeGrant = data.bot_require_code_grant; - this.termsOfServiceURL = data.terms_of_service_url; - this.privacyPolicyURL = data.privacy_policy_url; - this.owner = data.owner - ? new User(session, data.owner as DiscordUser) - : undefined; - this.summary = ''; - this.verifyKey = data.verify_key; - this.team = data.team ? NewTeam(session, data.team) : undefined; - this.guildId = data.guild_id; - this.coverImage = data.cover_image; - this.tags = data.tags; - this.installParams = data.install_params; - this.customInstallURL = data.custom_install_url; - } - - readonly session: Session; - id: Snowflake; - name: string; - icon?: string; - description: string; - rpcOrigins?: string[]; - botPublic: boolean; - botRequireCodeGrant: boolean; - termsOfServiceURL?: string; - privacyPolicyURL?: string; - owner?: Partial; - summary: SummaryDeprecated; - verifyKey: string; - team?: Team; - guildId?: Snowflake; - primarySkuId?: Snowflake; - slug?: string; - coverImage?: string; - flags?: number; - tags?: string[]; - installParams?: DiscordInstallParams; - customInstallURL?: string; -} diff --git a/packages/core/src/structures/attachment.ts b/packages/core/src/structures/attachment.ts deleted file mode 100644 index 9d4ab1e..0000000 --- a/packages/core/src/structures/attachment.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { DiscordAttachment } from '@biscuitland/api-types'; - -/** - * Represents an attachment - * @link https://discord.com/developers/docs/resources/channel#attachment-object - */ -export class Attachment implements Model { - constructor(session: Session, data: DiscordAttachment) { - this.session = session; - this.id = data.id; - - this.contentType = data.content_type ? data.content_type : undefined; - this.attachment = data.url; - this.proxyUrl = data.proxy_url; - this.name = data.filename; - this.size = data.size; - this.height = data.height ? data.height : undefined; - this.width = data.width ? data.width : undefined; - this.ephemeral = !!data.ephemeral; - } - - readonly session: Session; - readonly id: Snowflake; - - contentType?: string; - attachment: string; - proxyUrl: string; - name: string; - size: number; - height?: number; - width?: number; - ephemeral: boolean; -} diff --git a/packages/core/src/structures/automod.ts b/packages/core/src/structures/automod.ts deleted file mode 100644 index 63dd0c1..0000000 --- a/packages/core/src/structures/automod.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { - AutoModerationActionType, - AutoModerationEventTypes, - AutoModerationTriggerTypes, - DiscordAutoModerationRule, - DiscordAutoModerationRuleTriggerMetadataPresets, - DiscordAutoModerationActionExecution } from '@biscuitland/api-types'; -import { - AUTO_MODERATION_RULES -} from '@biscuitland/api-types'; - -export interface AutoModerationRuleTriggerMetadata { - keywordFilter?: string[]; - presets?: DiscordAutoModerationRuleTriggerMetadataPresets[]; - allowList?: string[]; -} - -export interface ActionMetadata { - channelId: Snowflake; - durationSeconds: number; -} - -export interface AutoModerationAction { - type: AutoModerationActionType; - metadata: ActionMetadata; -} - -/** @link https://discord.com/developers/docs/resources/auto-moderation#create-auto-moderation-rule-json-params */ -export interface CreateAutoModerationRule { - name: string; - eventType: 1; - triggerType: AutoModerationTriggerTypes; - triggerMetadata?: AutoModerationRuleTriggerMetadata; - actions: AutoModerationAction[]; - enabled?: boolean; - exemptRoles?: Snowflake[]; - exemptChannels?: Snowflake[]; -} - -export class AutoModerationRule implements Model { - constructor(session: Session, data: DiscordAutoModerationRule) { - this.session = session; - this.id = data.id; - this.guildId = data.guild_id; - this.name = data.name; - this.creatorId = data.creator_id; - this.eventType = data.event_type; - this.triggerType = data.trigger_type; - this.triggerMetadata = { - keywordFilter: data.trigger_metadata.keyword_filter, - presets: data.trigger_metadata.presets, - allowList: data.trigger_metadata.allow_list - }; - this.actions = data.actions.map(action => - Object.create({ - type: action.type, - metadata: { - channelId: action.metadata.channel_id, - durationSeconds: action.metadata.duration_seconds - } - }) - ); - this.enabled = !!data.enabled; - this.exemptRoles = data.exempt_roles; - this.exemptChannels = data.exempt_channels; - } - - session: Session; - id: Snowflake; - guildId: Snowflake; - name: string; - creatorId: Snowflake; - eventType: AutoModerationEventTypes; - triggerType: AutoModerationTriggerTypes; - triggerMetadata: AutoModerationRuleTriggerMetadata; - actions: AutoModerationAction[]; - enabled: boolean; - exemptRoles: Snowflake[]; - exemptChannels: Snowflake[]; - - async getRules( - ruleId?: Snowflake - ): Promise { - const request = await this.session.rest.get< - DiscordAutoModerationRule | DiscordAutoModerationRule[] - >(AUTO_MODERATION_RULES(this.guildId, ruleId)); - if (Array.isArray(request)) { - return request.map( - amr => new AutoModerationRule(this.session, amr) - ); -} - return new AutoModerationRule(this.session, request); - } - - async createRule(options: CreateAutoModerationRule) { - const request = await this.session.rest.post( - AUTO_MODERATION_RULES(this.guildId), - { - name: options.name, - event_type: options.eventType, - trigger_type: options.triggerType, - trigger_metadata: options.triggerMetadata, - actions: options.actions - ? options.actions.map(x => - Object.assign( - {}, - { - type: x.type, - metadata: { - channel_id: x.metadata.channelId, - duration_seconds: - x.metadata.durationSeconds - } - } - ) - ) - : undefined, - enabled: !!options.enabled, - exempt_roles: options.exemptRoles, - exempt_channels: options.exemptChannels - } - ); - return new AutoModerationRule(this.session, request); - } - - async editRule( - ruleId = this.id, - options: Partial - ) { - const request = await this.session.rest.patch< - DiscordAutoModerationRule - >(AUTO_MODERATION_RULES(this.guildId, ruleId), { - name: options.name, - event_type: options.eventType, - trigger_type: options.triggerType, - trigger_metadata: options.triggerMetadata, - actions: options.actions - ? options.actions.map(x => - Object.assign( - {}, - { - type: x.type, - metadata: { - channel_id: x.metadata.channelId, - duration_seconds: x.metadata.durationSeconds - } - } - ) - ) - : undefined, - enabled: !!options.enabled, - exempt_roles: options.exemptRoles, - exempt_channels: options.exemptChannels - }); - return new AutoModerationRule(this.session, request); - } - - async deleteRule(ruleId = this.id): Promise { - await this.session.rest.delete( - AUTO_MODERATION_RULES(this.guildId, ruleId) - ); - return; - } -} - -export class AutoModerationExecution { - constructor(session: Session, data: DiscordAutoModerationActionExecution) { - this.session = session; - this.guildId = data.guild_id; - this.action = { - type: data.action.type, - metadata: { - channelId: data.action.metadata.channel_id as string, - durationSeconds: data.action.metadata.duration_seconds as number - } - }; - this.ruleId = data.rule_id; - this.ruleTriggerType = data.rule_trigger_type; - this.userId = data.user_id; - this.content = data.content; - if (data.channel_id) { - this.channelId = data.channel_id; - } - if (data.message_id) { - this.messageId = data.message_id; - } - if (data.alert_system_message_id) { - this.alertSystemMessageId = data.alert_system_message_id; - } - - if (data.matched_keyword) { - this.matchedKeyword = data.matched_keyword; - } - - if (data.matched_content) { - this.matched_content = data.matched_content; - } - } - - session: Session; - guildId: Snowflake; - action: AutoModerationAction; - ruleId: Snowflake; - ruleTriggerType: AutoModerationTriggerTypes; - userId: Snowflake; - channelId?: Snowflake; - messageId?: Snowflake; - alertSystemMessageId?: Snowflake; - content?: string; - matchedKeyword?: string; - matched_content?: string; -} diff --git a/packages/core/src/structures/base.ts b/packages/core/src/structures/base.ts deleted file mode 100644 index c81fbe0..0000000 --- a/packages/core/src/structures/base.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; - -/** - * Represents a Discord data model - */ -export interface Model { - /** id of the model */ - id: Snowflake; - /** reference to the client that instantiated the model */ - session: Session; -} diff --git a/packages/core/src/structures/channels.ts b/packages/core/src/structures/channels.ts deleted file mode 100644 index 06a2d04..0000000 --- a/packages/core/src/structures/channels.ts +++ /dev/null @@ -1,1155 +0,0 @@ -/** Types */ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { PermissionsOverwrites } from '../utils/util'; - -/** Functions and others */ -// import { calculateShardId } from '../utils/calculate-shard'; -import { urlToBase64 } from '../utils/url-to-base-64'; - -/** Classes and routes */ -import { - USER_DM, - CHANNEL, - CHANNEL_PINS, - CHANNEL_INVITES, - CHANNEL_TYPING, - CHANNEL_MESSAGES, - CHANNEL_WEBHOOKS, - THREAD_USER, - THREAD_ME, - THREAD_MEMBERS, - THREAD_START_PRIVATE, - THREAD_ARCHIVED_PUBLIC, - THREAD_ARCHIVED_PRIVATE_JOINED, - THREAD_START_PUBLIC, - ChannelTypes -} from '@biscuitland/api-types'; -import type { - DiscordChannel, - DiscordInvite, - DiscordInviteMetadata, - DiscordListArchivedThreads, - DiscordMessage, - DiscordOverwrite, - DiscordThreadMember, - DiscordWebhook, - TargetTypes, - VideoQualityModes, - GetReactions, - GetMessagesOptions, - ListArchivedThreads, - GatewayOpcodes as _GatewayOpcodes -} from '@biscuitland/api-types'; - -import type { CreateMessage, EditMessage, EmojiResolvable } from './message'; -import { Message } from './message'; -import { Invite } from './invite'; -import { Webhook } from './webhook'; -import { User } from './user'; -import { ThreadMember } from './members'; -import { Permissions } from './special/permissions'; - -/** - * Abstract class that represents the base for creating a new channel. - */ -export abstract class BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel) { - this.id = data.id; - this.session = session; - this.name = data.name; - this.type = data.type; - } - - /** id's refers to the identification of the channel */ - readonly id: Snowflake; - - /** The session that instantiated the channel */ - readonly session: Session; - - /** Channel name defined by the entity */ - name?: string; - - /** Refers to the possible channel type implemented (Guild, DM, Voice, News, etc...) */ - type: ChannelTypes; - - /** If the channel is a TextChannel */ - isText(): this is TextChannel.T { - return textBasedChannels.includes(this.type); - } - - /** If the channel is a VoiceChannel */ - isVoice(): this is VoiceChannel { - return this.type === ChannelTypes.GuildVoice; - } - - /** If the channel is a DMChannel */ - isDM(): this is DMChannel { - return this.type === ChannelTypes.DM; - } - - /** If the channel is a NewChannel */ - isNews(): this is NewsChannel { - return this.type === ChannelTypes.GuildNews; - } - - /** If the channel is a ThreadChannel */ - isThread(): this is ThreadChannel { - return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread; - } - - /** If the channel is a StageChannel */ - isStage(): this is StageChannel { - return this.type === ChannelTypes.GuildStageVoice; - } - - /** If the channel is a ForumChannel */ - isForum(): this is ForumChannel { - return this.type === ChannelTypes.GuildForum; - } - - async fetch(channelId?: Snowflake): Promise { - const channel = await this.session.rest.get(CHANNEL(channelId ?? this.id)); - - return ChannelFactory.from(this.session, channel); - } - - /** - * Deletes a channel. - * @param channelId The channel id to delete. - * @link https://discord.com/developers/docs/topics/gateway#channel-delete - */ - async delete(channelId?: Snowflake): Promise { - const deleted = await this.session.rest.delete(CHANNEL(channelId ?? this.id)); - - return ChannelFactory.from(this.session, deleted); - } - - toString(): string { - return `<#${this.id}>`; - } -} - -/** - * Represents a category channel. - */ -export class CategoryChannel extends BaseChannel { - constructor(session: Session, data: DiscordChannel) { - super(session, data); - this.id = data.id; - this.session = session; - this.name = data.name ? data.name : ''; - this.nsfw = data.nsfw ? data.nsfw : false; - this.guildId = data.guild_id ? data.guild_id : undefined; - this.type = ChannelTypes.GuildCategory; - this.position = data.position ? data.position : undefined; - this.parentId = data.parent_id ? data.parent_id : undefined; - - this.permissionOverwrites = data.permission_overwrites - ? ChannelFactory.permissionOverwrites(data.permission_overwrites) - : []; - } - - readonly id: Snowflake; - readonly session: Session; - parentId?: string; - name: string; - permissionOverwrites: PermissionsOverwrites[]; - nsfw: boolean; - guildId?: Snowflake; - position?: number; -} - -/** TextChannel */ -/** - * @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params - * Represents the options object to create an invitation - */ -export interface DiscordInviteOptions { - /** duration of invite in seconds before expiry, or 0 for never. between 0 and 604800 (7 days) */ - maxAge?: number; - /** max number of uses or 0 for unlimited. between 0 and 100 */ - maxUses?: number; - /** if the invitation is unique. If it's true, don't try to reuse a similar invite (useful for creating many unique one time use invites) */ - unique?: boolean; - /** whether this invite only grants temporary membership */ - temporary: boolean; - reason?: string; - /** the type of target for this voice channel invite */ - targetType?: TargetTypes; - /** the id of the user whose stream to display for this invite, required if targetType is 1, the user must be streaming in the channel */ - targetUserId?: Snowflake; - /** the id of the embedded application to open for this invite, required if targetType is 2, the application must have the EMBEDDED flag */ - targetApplicationId?: Snowflake; -} - -/** Webhook create object */ -export interface CreateWebhook { - /** name of the webhook (1-80 characters) */ - name: string; - /** image for the default webhook avatar */ - avatar?: string; - reason?: string; -} - -/** Available text-channel-types list */ -export const textBasedChannels: ChannelTypes[] = [ - ChannelTypes.DM, - ChannelTypes.GroupDm, - ChannelTypes.GuildPrivateThread, - ChannelTypes.GuildPublicThread, - ChannelTypes.GuildNews, - ChannelTypes.GuildVoice, - ChannelTypes.GuildText, -]; - -/** Available text-channel-types */ -export type TextBasedChannels = - | ChannelTypes.DM - | ChannelTypes.GroupDm - | ChannelTypes.GuildPrivateThread - | ChannelTypes.GuildPublicThread - | ChannelTypes.GuildNews - | ChannelTypes.GuildNewsThread - | ChannelTypes.GuildVoice - | ChannelTypes.GuildText; - -/** - * Represents a text channel. - * tbis trait can be implemented by using implements TextChannel.T - */ -export namespace TextChannel { - export interface T { - /** The session that instantiated the channel */ - readonly session: Session; - - /** id's refers to the identification of the channel */ - readonly id: Snowflake; - - /** Current channel name */ - name?: string; - - /** The type of the channel */ - type: TextBasedChannels; - - /** The id of the last message sent in this channel (or thread for GUILD_FORUM channels) (may not point to an existing or valid message or thread) */ - lastMessageId?: Snowflake; - - /** When the last pinned message was pinned. This may be undefined in events such as GUILD_CREATE when a message is not pinned. */ - lastPinTimestamp?: string; - - // methods to be implemented - - fetchPins(): Promise; - createInvite(options?: DiscordInviteOptions): Promise; - fetchMessages(options?: GetMessagesOptions): Promise; - sendTyping(): Promise; - pinMessage(messageId: Snowflake): Promise; - unpinMessage(messageId: Snowflake): Promise; - addReaction( - messageId: Snowflake, - reaction: EmojiResolvable, - ): Promise; - removeReaction( - messageId: Snowflake, - reaction: EmojiResolvable, - options: { userId: Snowflake }, - ): Promise; - removeReactionEmoji( - messageId: Snowflake, - reaction: EmojiResolvable, - ): Promise; - nukeReactions(messageId: Snowflake): Promise; - fetchReactions( - messageId: Snowflake, - reaction: EmojiResolvable, - options: GetReactions, - ): Promise; - sendMessage(options: CreateMessage): Promise; - editMessage(messageId: Snowflake, options: EditMessage): Promise; - createWebhook(options: CreateWebhook): Promise; - } - - /** - * fetchPins makes an asynchronous request and gets the current channel pins. - * @returns A promise that resolves with an array of Message objects. - */ - export async function fetchPins(this: T): Promise { - const messages = await this.session.rest.get( - CHANNEL_PINS(this.id), - ); - - return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : []; - } - - /** - * createInvite makes an asynchronous request to create a new invitation. - * @param options - The options to create the invitation - * @returns The created invite - */ - export async function createInvite(this: T, options?: DiscordInviteOptions): Promise { - const invite = await this.session.rest.post( - CHANNEL_INVITES(this.id), - options - ? { - max_age: options.maxAge, - max_uses: options.maxUses, - temporary: options.temporary, - unique: options.unique, - target_type: options.targetType, - target_user_id: options.targetUserId, - target_application_id: options.targetApplicationId, - } - : {}, - ); - - return new Invite(this.session, invite); - } - - /** - * fetchMessages makes an asynchronous request and gets the channel messages - * @param options - The options to get the messages - * @returns The messages - */ - export async function fetchMessages(this: T, options?: GetMessagesOptions): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - if (options?.limit! > 100) { throw Error('Values must be between 0-100'); } - const messages = await this.session.rest.get( - CHANNEL_MESSAGES(this.id, options), - ); - - return messages[0] ? messages.map(x => new Message(this.session, x)) : []; - } - - /** sendTyping sends a typing POST request */ - export async function sendTyping(this: T): Promise { - await this.session.rest.post(CHANNEL_TYPING(this.id), {}); - } - - /** - * pinMessage pins a channel message. - * Same as Message.pin(). - * @param messageId - The id of the message to pin - * @returns The promise that resolves when the request is complete - */ - export async function pinMessage(this: T, messageId: Snowflake): Promise { - await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - /** - * unpinMessage unpin a channel message. - * Same as Message.unpin() - * @param messageId - The id of the message to unpin - * @returns The promise of the request - */ - export async function unpinMessage(this: T, messageId: Snowflake): Promise { - await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); - } - - /** - * addReaction adds a reaction to the message. - * Same as Message.addReaction(). - * @param messageId - The message to add the reaction to - * @param reaction - The reaction to add - * @returns The promise of the request - */ - export async function addReaction( - this: T, - messageId: Snowflake, - reaction: EmojiResolvable, - ): Promise { - await Message.prototype.addReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - /** - * removeReaction removes a reaction from the message. - * Same as Message.removeReaction(). - * @param messageId - The id of the message to remove the reaction from - * @param reaction - The reaction to remove - * @param options - The user to remove the reaction from - */ - export async function removeReaction( - this: T, - messageId: Snowflake, - reaction: EmojiResolvable, - options?: { userId: Snowflake }, - ): Promise { - await Message.prototype.removeReaction.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - } - - /** - * removeReactionEmoji removes an emoji reaction from the messageId provided. - * Same as Message.removeReactionEmoji(). - * @param messageId - The message id to remove the reaction from. - */ - export async function removeReactionEmoji( - this: T, - messageId: Snowflake, - reaction: EmojiResolvable, - ): Promise { - await Message.prototype.removeReactionEmoji.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - ); - } - - /** nukeReactions nukes every reaction on the message. - * Same as Message.nukeReactions(). - * @param messageId The message id to nuke reactions from. - * @returns A promise that resolves when the reactions are nuked. - */ - export async function nukeReactions(this: T, messageId: Snowflake): Promise { - await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId }); - } - - /** - * fetchReactions gets the users who reacted with this emoji on the message. - * Same as Message.fetchReactions(). - * @param messageId - The message id to get the reactions from. - * @param reaction - The emoji to get the reactions from. - * @param options - The options to get the reactions with. - * @returns The users who reacted with this emoji on the message. - */ - export async function fetchReactions( - this: T, - messageId: Snowflake, - reaction: EmojiResolvable, - options?: GetReactions, - ): Promise { - const users = await Message.prototype.fetchReactions.call( - { channelId: this.id, id: messageId, session: this.session }, - reaction, - options, - ); - - return users; - } - - /** - * sendMessage sends a message to the channel. - * Same as Message.reply(). - * @param options - Options for a new message. - * @returns The sent message. - */ - export async function sendMessage(this: T, options: CreateMessage): Promise { - const message = await Message.prototype.reply.call({ - channelId: this.id, - session: this.session, - }, options); - - return message; - } - - /** - * editMessage edits a message. - * Same as Message.edit(). - * @param messageId - Message ID. - * @param options - Options for edit a message. - * @returns The edited message. - */ - export async function editMessage( - this: T, - messageId: Snowflake, - options: EditMessage, - ): Promise { - const message = await Message.prototype.edit.call({ - channelId: this.id, - id: messageId, - session: this.session, - }, options); - - return message; - } - - /** - * createWebhook creates a webhook. - * @param options - Options for a new webhook. - * @returns The created webhook. - */ - export async function createWebhook(this: T, options: CreateWebhook): Promise { - const webhook = await this.session.rest.post( - CHANNEL_WEBHOOKS(this.id), - { - name: options.name, - avatar: options.avatar ? urlToBase64(options.avatar) : undefined, - reason: options.reason, - }, - ); - - return new Webhook(this.session, webhook); - } -} - -/** GuildChannel */ -/** - * Represent the options object to create a thread channel - * @link https://discord.com/developers/docs/resources/channel#start-thread-without-message - */ -export interface ThreadCreateOptions { - name: string; - autoArchiveDuration?: 60 | 1440 | 4320 | 10080; - type: 10 | 11 | 12; - invitable?: boolean; - rateLimitPerUser?: number; - reason?: string; -} - -/** - * Representations of the objects to edit a guild channel - * @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel - */ -export interface EditGuildChannelOptions { - name?: string; - position?: number; - permissionOverwrites?: PermissionsOverwrites[]; -} - -export interface EditNewsChannelOptions extends EditGuildChannelOptions { - type?: ChannelTypes.GuildNews | ChannelTypes.GuildText; - topic?: string | null; - nfsw?: boolean | null; - parentId?: Snowflake | null; - defaultAutoArchiveDuration?: number | null; -} - -export interface EditForumChannelOptions extends EditGuildChannelOptions { - availableTags?: ForumTag[]; - defaultReactionEmoji?: DefaultReactionEmoji; - defaultThreadRateLimitPerUser?: number; -} - -export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { - rateLimitPerUser?: number | null; -} - -export interface EditThreadChannelOptions extends EditGuildTextChannelOptions { - appliedTags: string[]; -} - -export interface EditStageChannelOptions extends EditGuildChannelOptions { - bitrate?: number | null; - rtcRegion?: Snowflake | null; -} - -export interface EditVoiceChannelOptions extends EditStageChannelOptions { - nsfw?: boolean | null; - userLimit?: number | null; - parentId?: Snowflake | null; - videoQualityMode?: VideoQualityModes | null; -} - -/** - * Represents the option object to create a thread channel from a message - * @link https://discord.com/developers/docs/resources/channel#start-thread-from-message - */ -export interface ThreadCreateOptions { - name: string; - autoArchiveDuration?: 60 | 1440 | 4320 | 10080; - rateLimitPerUser?: number; - messageId: Snowflake; -} -/** - * @link https://discord.com/developers/docs/resources/channel#list-public-archived-threads-response-body - */ -export interface ReturnThreadsArchive { - threads: Record; - members: Record; - hasMore: boolean; -} - -/** - * Represents a GuildChannel. - * @extends BaseChannel - * @see {@link BaseChannel} - * @link https://discord.com/developers/docs/resources/channel#channel-object - */ -export class GuildChannel extends BaseChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data); - this.type = data.type as number; - this.guildId = guildId; - this.position = data.position; - data.topic ? this.topic = data.topic : null; - data.parent_id ? this.parentId = data.parent_id : undefined; - this.permissionOverwrites = data.permission_overwrites - ? ChannelFactory.permissionOverwrites(data.permission_overwrites) - : []; - this.nsfw = data.nsfw ?? false; - } - - /** Channel type. */ - override type: Exclude; - - /** Guild id. */ - guildId: Snowflake; - - /** Channel topic */ - topic?: string; - - /** Sorting position of the channel */ - position?: number; - - /** Id of the parent category for a channel (each parent category can contain up to 50 channels). */ - parentId?: Snowflake; - - /** Explicit permission overwrites for members and roles */ - permissionOverwrites: PermissionsOverwrites[]; - - /** If the channel is NSFW (Not-Safe-For-Work content) */ - nsfw: boolean; - - /** - * Gets the channel invites for the channel. - */ - async fetchInvites(): Promise { - const invites = await this.session.rest.get(CHANNEL_INVITES(this.id)); - - return invites.map(invite => new Invite(this.session, invite)); - } - - /** - * Edits the channel. - * @param options - Options for edit the channel. - */ - async edit(options: EditNewsChannelOptions): Promise; - async edit(options: EditStageChannelOptions): Promise; - async edit(options: EditVoiceChannelOptions): Promise; - async edit(options: EditForumChannelOptions): Promise; - async edit(options: EditThreadChannelOptions): Promise; - async edit( - options: - | EditGuildTextChannelOptions - | EditNewsChannelOptions - | EditVoiceChannelOptions - | EditForumChannelOptions - | EditThreadChannelOptions - ): Promise { - const channel = await this.session.rest.patch( - CHANNEL(this.id), - { - name: options.name, - type: 'type' in options ? options.type : undefined, - position: options.position, - topic: 'topic' in options ? options.topic : undefined, - nsfw: 'nfsw' in options ? options.nfsw : undefined, - rate_limit_per_user: 'rateLimitPerUser' in options ? options.rateLimitPerUser : undefined, - bitrate: 'bitrate' in options ? options.bitrate : undefined, - user_limit: 'userLimit' in options ? options.userLimit : undefined, - permissions_overwrites: options.permissionOverwrites, - parent_id: 'parentId' in options ? options.parentId : undefined, - rtc_region: 'rtcRegion' in options ? options.rtcRegion : undefined, - video_quality_mode: 'videoQualityMode' in options ? options.videoQualityMode : undefined, - applied_tags: 'appliedTags' in options ? options.appliedTags : undefined, - default_auto_archive_duration: 'defaultAutoArchiveDuration' in options - ? options.defaultAutoArchiveDuration - : undefined, - default_reaction_emoji: 'defaultReactionEmoji' in options - ? options.defaultReactionEmoji - : undefined, - default_thread_rate_limit_per_user: 'defaultThreadRateLimitPerUser' in options - ? options.defaultThreadRateLimitPerUser - : undefined, - available_tags: 'availableTags' in options - ? options.availableTags?.map(at => { - return { - id: at.id, - name: at.name, - moderated: at.moderated, - emoji_id: at.emojiId, - emoji_name: at.emojiName - }; - }) - : undefined - }, - ); - return ChannelFactory.fromGuildChannel(this.session, channel); - } - - /** - * Gets the channel archived threads. - * @param options - Options for select the archved threads. - */ - async getArchivedThreads( - options: ListArchivedThreads & { type: 'public' | 'private' | 'privateJoinedThreads' }, - ): Promise { - let func: (channelId: Snowflake, options: ListArchivedThreads) => string; - - switch (options.type) { - case 'public': - func = THREAD_ARCHIVED_PUBLIC; - break; - case 'private': - func = THREAD_START_PRIVATE; - break; - case 'privateJoinedThreads': - func = THREAD_ARCHIVED_PRIVATE_JOINED; - break; - } - - const { threads, members, has_more: hasMore } = await this.session.rest.get( - func(this.id, options), - ); - - return { - threads: Object.fromEntries( - threads.map(thread => [thread.id, new ThreadChannel(this.session, thread, this.id)]), - ) as Record, - members: Object.fromEntries( - members.map(threadMember => [threadMember.id, new ThreadMember(this.session, threadMember)]), - ) as Record, - hasMore, - }; - } - - /** - * Creates a new thread in the channel. - * @param options - Options for create a thread channel. - */ - async createThread(options: ThreadCreateOptions): Promise { - const thread = await this.session.rest.post( - 'messageId' in options - ? THREAD_START_PUBLIC(this.id, options.messageId) - : THREAD_START_PRIVATE(this.id), - { - name: options.name, - auto_archive_duration: options.autoArchiveDuration, - }, - ); - - return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId); - } - - /** - * Sets the channel's permissions overwrites. Same as GuildChannel.edit({ permissionOverwrites: ... }). - * @param overwrites - The overwrites to add to the channel. - */ - async setPermissions(overwrites: PermissionsOverwrites[]): Promise { - return this.edit({ permissionOverwrites: overwrites } as EditGuildChannelOptions); - } - - /** gets the url of the channel that points to the channel */ - get url(): string { - return `https://discord.com/channels/${this.guildId}/${this.id}`; - } -} - -/** BaseVoiceChannel */ -/** - * @link https://discord.com/developers/docs/topics/gateway#update-voice-state - */ -export interface UpdateVoiceState { - guildId: string; - channelId?: string; - selfMute: boolean; - selfDeaf: boolean; -} - -export abstract class BaseVoiceChannel extends GuildChannel implements Model { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.bitRate = data.bitrate; - this.userLimit = data.user_limit ?? 0; - this.videoQuality = data.video_quality_mode; - this.nsfw = !!data.nsfw; - this.type = data.type as number; - - if (data.rtc_region) { - this.rtcRegion = data.rtc_region; - } - } - - override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice; - bitRate?: number; - userLimit: number; - rtcRegion?: Snowflake; - - videoQuality?: VideoQualityModes; - nsfw: boolean; - - // TODO: CONNECT TO VOICE CHAT -} - -/** DMChannel */ -export class DMChannel extends BaseChannel implements Model, TextChannel.T { - constructor(session: Session, data: DiscordChannel) { - super(session, data); - - this.name = data.name; - this.type = data.type as number; - - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - - if (data.last_pin_timestamp) { - this.lastPinTimestamp = data.last_pin_timestamp; - } - - if (data.owner_id && data.recipients) { - this.user = new User(session, data.recipients.find(user => user.id === data.owner_id)!); - } else { - this.user = new User(session, data.recipients!.find(user => user.id === this.session.botId)!); - } - - if (data.recipients && data.recipients.length > 1) { - this.group = data.recipients.map(r => new User(this.session, r)); - } - - this.type = data.type as ChannelTypes.GroupDm | ChannelTypes.GroupDm; - - if (data.last_message_id) { - this.lastMessageId = data.last_message_id; - } - - if (data.rate_limit_per_user) { - this.rateLimitPerUser = data.rate_limit_per_user; - } - } - - override type: ChannelTypes.DM | ChannelTypes.GroupDm; - /** Onwer of the dm channel. */ - user: User; - - /** If the channel is a DM Group it's has multiple users. */ - group?: User[]; - - /** Last message id. */ - lastMessageId?: Snowflake; - - /** When the last pinned message was pinned. This may be undefined in events such as GUILD_CREATE when a message is not pinned. */ - lastPinTimestamp?: string; - - - /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ - rateLimitPerUser?: number; - - async close(): Promise { - const channel = await this.session.rest.delete(CHANNEL(this.id), {}); - - return new DMChannel(this.session, channel); - } - - async open(userId: Snowflake): Promise { - const channel = await this.session.rest.post(USER_DM(), { recipient_id: userId }); - - return new DMChannel(this.session, channel); - } -} - -Object.assign(DMChannel.prototype, TextChannel); - -export interface DMChannel extends BaseChannel, TextChannel.T {} - -/** VoiceChannel */ -export class VoiceChannel extends BaseVoiceChannel implements Model, TextChannel.T { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.rateLimitPerUser = data.rate_limit_per_user; - } - - override type: ChannelTypes.GuildVoice; - /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ - rateLimitPerUser?: number; -} - -Object.assign(VoiceChannel.prototype, TextChannel); - -export interface VoiceChannel extends BaseVoiceChannel, TextChannel.T { } - -/** NewsChannel */ -export class NewsChannel extends GuildChannel implements Model, TextChannel.T { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as ChannelTypes.GuildNews; - this.defaultAutoArchiveDuration = data.default_auto_archive_duration; - this.rateLimitPerUser = data.rate_limit_per_user; - } - - override type: ChannelTypes.GuildNews; - defaultAutoArchiveDuration?: number; - - /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ - rateLimitPerUser?: number; - - crosspostMessage(messageId: Snowflake): Promise { - return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session }); - } - - get publishMessage() { - return this.crosspostMessage; - } -} - -Object.assign(NewsChannel.prototype, TextChannel); - -export interface NewsChannel extends GuildChannel, TextChannel.T { } - -/** StageChannel */ -export class StageChannel extends BaseVoiceChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.topic = data.topic ? data.topic : undefined; - } - - override type: ChannelTypes.GuildStageVoice; - topic?: string; -} - -/** ThreadChannel */ -export class ThreadChannel extends GuildChannel implements Model, TextChannel.T { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as number; - this.archived = !!data.thread_metadata?.archived; - this.archiveTimestamp = data.thread_metadata?.archive_timestamp; - this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration; - this.locked = !!data.thread_metadata?.locked; - this.messageCount = data.message_count; - this.memberCount = data.member_count; - this.ownerId = data.owner_id; - this.rateLimitPerUser = data.rate_limit_per_user; - - if (data.member) { - this.member = new ThreadMember(session, data.member); - } - - if (data.total_message_sent) { this.totalMessageSent = data.total_message_sent; } - - if (data.applied_tags) { this.appliedTags = data.applied_tags; } - } - - override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; - archived?: boolean; - archiveTimestamp?: string; - autoArchiveDuration?: number; - locked?: boolean; - messageCount?: number; - memberCount?: number; - member?: ThreadMember; - ownerId?: Snowflake; - totalMessageSent?: number; - appliedTags?: string[]; - - /** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ - rateLimitPerUser?: number; - - async joinThread(): Promise { - await this.session.rest.put(THREAD_ME(this.id), {}); - } - - async addToThread(guildMemberId: Snowflake): Promise { - await this.session.rest.put(THREAD_USER(this.id, guildMemberId), {}); - } - - async leaveToThread(guildMemberId: Snowflake): Promise { - await this.session.rest.delete(THREAD_USER(this.id, guildMemberId), {}); - } - - removeMember(memberId: Snowflake = this.session.botId): Promise { - return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId); - } - - fetchMember(memberId: Snowflake = this.session.botId): Promise { - return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId); - } - - async fetchMembers(): Promise { - const members = await this.session.rest.get( - THREAD_MEMBERS(this.id), - ); - - return members.map(threadMember => new ThreadMember(this.session, threadMember)); - } - - async setAppliedTags(tags: string[]) { - const thread = await this.edit({ appliedTags: tags }); - return thread; - } -} - -Object.assign(ThreadChannel.prototype, TextChannel); - -export interface ThreadChannel extends GuildChannel, TextChannel.T { } - -/** ForumChannel */ -export class ForumChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - - if (data.available_tags) { - this.availableTags = data.available_tags.map(at => { - return { - id: at.id, - name: at.name, - moderated: at.moderated, - emojiId: at.emoji_id, - emojiName: at.emoji_name - }; - }); - } - if (data.default_reaction_emoji) { - this.defaultReactionEmoji = { - emojiId: data.default_reaction_emoji.emoji_id, - emojiName: data.default_reaction_emoji.emoji_name - }; - } - - this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user; - } - - /** the IDs of the set of tags that have been applied to a thread in a GUILD_FORUM channel */ - availableTags?: ForumTag[]; - - /** the emoji to show in the add reaction button on a thread in a GUILD_FORUM channel */ - defaultReactionEmoji?: DefaultReactionEmoji; - - /** the initial rate_limit_per_user to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update */ - defaultThreadRateLimitPerUser?: number; - - async setAvailableTags(tags: ForumTag[]) { - const forum = await this.edit({ availableTags: tags }); - return forum; - } - - async setDefaultReactionEmoji(emoji: DefaultReactionEmoji) { - const forum = await this.edit({ defaultReactionEmoji: emoji }); - return forum; - } - - async setDefaultThreadRateLimitPerUser(limit: number) { - const forum = await this.edit({ defaultThreadRateLimitPerUser: limit }); - return forum; - } -} - -export interface ForumTag { - id: Snowflake; - name: string; - moderated: boolean; - emojiId: Snowflake | null; - emojiName: string | null; -} - -export interface DefaultReactionEmoji { - emojiId: Snowflake; - emojiName: string | null; -} - -export class GuildTextChannel extends GuildChannel { - constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { - super(session, data, guildId); - this.type = data.type as ChannelTypes.GuildText; - } - - override type: ChannelTypes.GuildText; -} - -Object.assign(GuildTextChannel.prototype, TextChannel); - -export interface GuildTextChannel extends GuildChannel, TextChannel.T { } - -/** ChannelFactory */ -export type Channel = - | GuildTextChannel - | TextChannel.T - | VoiceChannel - | DMChannel - | NewsChannel - | ThreadChannel - | StageChannel - | CategoryChannel - | ForumChannel; - -export type ChannelInGuild = - | GuildTextChannel - | VoiceChannel - | StageChannel - | NewsChannel - | ThreadChannel - | ForumChannel; - -export type ChannelWithMessages = - | GuildTextChannel - | VoiceChannel - | DMChannel - | NewsChannel - | ThreadChannel; - -export type ChannelWithMessagesInGuild = Exclude; - -export type PartialChannel = { - id: string; - name: string; - position: number; -}; - -export class ChannelFactory { - static fromGuildChannel(session: Session, channel: DiscordChannel): ChannelInGuild { - switch (channel.type) { - case ChannelTypes.GuildPublicThread: - case ChannelTypes.GuildPrivateThread: - return new ThreadChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildForum: - return new ForumChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildText: - return new GuildTextChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildNews: - return new NewsChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildVoice: - return new VoiceChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildStageVoice: - return new StageChannel(session, channel, channel.guild_id!); - } - - return null as any; - } - - static from(session: Session, channel: DiscordChannel): Channel { - switch (channel.type) { - case ChannelTypes.GuildPublicThread: - case ChannelTypes.GuildPrivateThread: - return new ThreadChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildForum: - return new ForumChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildText: - return new GuildTextChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildNews: - return new NewsChannel(session, channel, channel.guild_id!); - case ChannelTypes.DM: - return new DMChannel(session, channel); - case ChannelTypes.GuildVoice: - return new VoiceChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildStageVoice: - return new StageChannel(session, channel, channel.guild_id!); - case ChannelTypes.GuildCategory: - return new CategoryChannel(session, channel); - default: - if (textBasedChannels.includes(channel.type)) { - // BLACK MAGIC DO NOT EDIT - return Object.assign(Object.create(TextChannel), { session, channel }) as TextChannel.T; - } - return null as any; - } - } - - static permissionOverwrites(overwrites: DiscordOverwrite[]): PermissionsOverwrites[] { - return overwrites.map(v => { - return { - id: v.id, - type: v.type, - allow: new Permissions(Number.parseInt(v.allow!)), - deny: new Permissions(Number.parseInt(v.deny!)), - }; - }); - } -} diff --git a/packages/core/src/structures/components.ts b/packages/core/src/structures/components.ts deleted file mode 100644 index 1828420..0000000 --- a/packages/core/src/structures/components.ts +++ /dev/null @@ -1,280 +0,0 @@ -import type { Session } from '../biscuit'; -import type { - DiscordComponent, - DiscordInputTextComponent, - TextStyles, -} from '@biscuitland/api-types'; -import { Emoji } from './emojis'; -import { ButtonStyles, MessageComponentTypes } from '@biscuitland/api-types'; - -export class BaseComponent { - constructor(type: MessageComponentTypes) { - this.type = type; - } - - type: MessageComponentTypes; - - isActionRow(): this is ActionRowComponent { - return this.type === MessageComponentTypes.ActionRow; - } - - isButton(): this is ButtonComponent { - return this.type === MessageComponentTypes.Button; - } - - isSelectMenu(): this is SelectMenuComponent { - return this.type === MessageComponentTypes.SelectMenu; - } - - isTextInput(): this is TextInputComponent { - return this.type === MessageComponentTypes.InputText; - } -} - -/** Action Row Component */ -export interface ActionRowComponent { - type: MessageComponentTypes.ActionRow; - components: Exclude[]; -} - -/** All Components */ -export type Component = - | ActionRowComponent - | ButtonComponent - | LinkButtonComponent - | SelectMenuComponent - | TextInputComponent; - -/** Button Component */ -export type ClassicButton = Exclude; - -export type ComponentsWithoutRow = Exclude; - -export interface ButtonComponent { - type: MessageComponentTypes.Button; - style: ClassicButton; - label?: string; - emoji?: Emoji; - customId?: string; - disabled?: boolean; -} - -/** Link Button Component */ -export interface LinkButtonComponent { - type: MessageComponentTypes.Button; - style: ButtonStyles.Link; - label?: string; - url: string; - disabled?: boolean; -} - -/** Select Menu Component */ -export interface SelectMenuComponent { - type: MessageComponentTypes.SelectMenu; - customId: string; - options?: SelectMenuOption[]; - placeholder?: string; - minValue?: number; - maxValue?: number; - disabled?: boolean; -} - -/** Text Input Component */ -export interface TextInputComponent { - type: MessageComponentTypes.InputText; - customId: string; - style: TextStyles; - label: string; - minLength?: number; - maxLength?: number; - required?: boolean; - value?: string; - placeholder?: string; -} - -export interface SelectMenuOption { - label: string; - value: string; - description?: string; - emoji?: Emoji; - default?: boolean; -} - -export class Button extends BaseComponent implements ButtonComponent { - constructor(session: Session, data: DiscordComponent) { - super(data.type); - - this.session = session; - this.type = data.type as MessageComponentTypes.Button; - this.customId = data.custom_id; - this.label = data.label; - this.style = data.style as ClassicButton; - this.disabled = data.disabled; - - if (data.emoji) { - this.emoji = new Emoji(session, data.emoji); - } - } - - readonly session: Session; - override type: MessageComponentTypes.Button; - customId?: string; - label?: string; - style: ClassicButton; - disabled?: boolean; - emoji?: Emoji; -} - -export class LinkButton extends BaseComponent implements LinkButtonComponent { - constructor(session: Session, data: DiscordComponent) { - super(data.type); - - this.session = session; - this.type = data.type as MessageComponentTypes.Button; - this.url = data.url!; - this.label = data.label; - this.style = data.style as number; - this.disabled = data.disabled; - - if (data.emoji) { - this.emoji = new Emoji(session, data.emoji); - } - } - - readonly session: Session; - override type: MessageComponentTypes.Button; - url: string; - label?: string; - style: ButtonStyles.Link; - disabled?: boolean; - emoji?: Emoji; -} - -export class SelectMenu extends BaseComponent implements SelectMenuComponent { - constructor(session: Session, data: DiscordComponent) { - super(data.type); - - this.session = session; - this.type = data.type as MessageComponentTypes.SelectMenu; - this.customId = data.custom_id!; - - if ('options' in data) { - this.options = data.options?.map(option => { - return { - label: option.label, - description: option.description, - emoji: option.emoji ? new Emoji(session, option.emoji) : undefined, - value: option.value, - } as SelectMenuOption; - }); - } - - this.placeholder = data.placeholder; - this.minValues = data.min_values; - this.maxValues = data.max_values; - this.disabled = data.disabled; - } - - readonly session: Session; - override type: MessageComponentTypes.SelectMenu; - customId: string; - options?: SelectMenuOption[]; - placeholder?: string; - minValues?: number; - maxValues?: number; - disabled?: boolean; -} - -export class TextInput extends BaseComponent implements TextInputComponent { - constructor(session: Session, data: DiscordInputTextComponent) { - super(data.type); - - this.session = session; - this.type = data.type as MessageComponentTypes.InputText; - this.customId = data.custom_id!; - this.label = data.label!; - this.style = data.style as TextStyles; - - this.placeholder = data.placeholder; - this.value = data.value; - - this.minLength = data.min_length; - this.maxLength = data.max_length; - } - - readonly session: Session; - override type: MessageComponentTypes.InputText; - style: TextStyles; - customId: string; - label: string; - placeholder?: string; - value?: string; - minLength?: number; - maxLength?: number; -} - -export class ActionRow extends BaseComponent implements ActionRowComponent { - constructor(session: Session, data: DiscordComponent) { - super(data.type); - - this.session = session; - this.type = data.type as MessageComponentTypes.ActionRow; - this.components = data.components!.map(component => { - switch (component.type) { - case MessageComponentTypes.Button: - if (component.style === ButtonStyles.Link) { - return new LinkButton(session, component); - } - return new Button(session, component); - case MessageComponentTypes.SelectMenu: - case MessageComponentTypes.RoleSelect: - case MessageComponentTypes.UserSelect: - case MessageComponentTypes.MentionableSelect: - case MessageComponentTypes.ChannelSelect: - return new SelectMenu(session, component); - case MessageComponentTypes.InputText: - return new TextInput( - session, - component as DiscordInputTextComponent - ); - case MessageComponentTypes.ActionRow: - throw new Error( - 'Cannot have an action row inside an action row' - ); - } - }); - } - - readonly session: Session; - override type: MessageComponentTypes.ActionRow; - components: ComponentsWithoutRow[]; -} - -export class ComponentFactory { - /** - * Component factory - * @internal - */ - static from(session: Session, component: DiscordComponent): Component { - switch (component.type) { - case MessageComponentTypes.ActionRow: - return new ActionRow(session, component); - case MessageComponentTypes.Button: - if (component.style === ButtonStyles.Link) { - return new LinkButton(session, component); - } - return new Button(session, component); - case MessageComponentTypes.SelectMenu: - case MessageComponentTypes.RoleSelect: - case MessageComponentTypes.UserSelect: - case MessageComponentTypes.MentionableSelect: - case MessageComponentTypes.ChannelSelect: - return new SelectMenu(session, component); - case MessageComponentTypes.InputText: - return new TextInput( - session, - component as DiscordInputTextComponent - ); - } - } -} diff --git a/packages/core/src/structures/embed.ts b/packages/core/src/structures/embed.ts deleted file mode 100644 index bb78d4e..0000000 --- a/packages/core/src/structures/embed.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { DiscordEmbed, EmbedTypes } from '@biscuitland/api-types'; - -export interface Embed { - title?: string; - timestamp?: string; - type?: EmbedTypes; - url?: string; - color?: number; - description?: string; - author?: { - name: string; - iconURL?: string; - proxyIconURL?: string; - url?: string; - }; - footer?: { - text: string; - iconURL?: string; - proxyIconURL?: string; - }; - fields?: { - name: string; - value: string; - inline?: boolean; - }[]; - thumbnail?: { - url: string; - proxyURL?: string; - width?: number; - height?: number; - }; - video?: { - url?: string; - proxyURL?: string; - width?: number; - height?: number; - }; - image?: { - url: string; - proxyURL?: string; - width?: number; - height?: number; - }; - provider?: { - url?: string; - name?: string; - }; -} - -export function NewEmbed(data: Embed): DiscordEmbed { - return { - title: data.title, - timestamp: data.timestamp, - type: data.type, - url: data.url, - color: data.color, - description: data.description, - author: data.author && { - name: data.author.name, - url: data.author.url, - icon_url: data.author.iconURL, - proxy_icon_url: data.author.proxyIconURL, - }, - footer: data.footer && { - text: data.footer.text, - icon_url: data.footer.iconURL, - proxy_icon_url: data.footer.proxyIconURL, - }, - fields: data.fields?.map(f => { - return { - name: f.name, - value: f.value, - inline: f.inline, - }; - }), - thumbnail: data.thumbnail && { - url: data.thumbnail.url, - proxy_url: data.thumbnail.proxyURL, - width: data.thumbnail.width, - height: data.thumbnail.height, - }, - video: { - url: data.video?.url, - proxy_url: data.video?.proxyURL, - width: data.video?.width, - height: data.video?.height, - }, - image: data.image && { - url: data.image.url, - proxy_url: data.image.proxyURL, - width: data.image.width, - height: data.image.height, - }, - provider: { - url: data.provider?.url, - name: data.provider?.name, - }, - }; -} - -export const embed = NewEmbed; - -export function NewEmbedR(data: DiscordEmbed): Embed { - return { - title: data.title, - timestamp: data.timestamp, - type: data.type, - url: data.url, - color: data.color, - description: data.description, - author: data.author && { - name: data.author.name, - url: data.author.url, - iconURL: data.author.icon_url, - proxyIconURL: data.author.proxy_icon_url, - }, - footer: data.footer && { - text: data.footer.text, - iconURL: data.footer.icon_url, - proxyIconURL: data.footer.proxy_icon_url, - }, - fields: data.fields?.map(f => { - return { - name: f.name, - value: f.value, - inline: f.inline, - }; - }), - thumbnail: data.thumbnail && { - url: data.thumbnail.url, - proxyURL: data.thumbnail.proxy_url, - width: data.thumbnail.width, - height: data.thumbnail.height, - }, - video: { - url: data.video?.url, - proxyURL: data.video?.proxy_url, - width: data.video?.width, - height: data.video?.height, - }, - image: data.image && { - url: data.image.url, - proxyURL: data.image.proxy_url, - width: data.image.width, - height: data.image.height, - }, - provider: { - url: data.provider?.url, - name: data.provider?.name, - }, - }; -} - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const embed_ = NewEmbedR; diff --git a/packages/core/src/structures/emojis.ts b/packages/core/src/structures/emojis.ts deleted file mode 100644 index de6b0d1..0000000 --- a/packages/core/src/structures/emojis.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { Session } from '../biscuit'; -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { ModifyGuildEmoji } from './guilds'; -import { Guild } from './guilds'; -import { User } from './user'; -import type { DiscordEmoji } from '@biscuitland/api-types'; -import { EMOJI_URL, GUILD_EMOJIS } from '@biscuitland/api-types'; - -export class Emoji implements Partial { - constructor(session: Session, data: DiscordEmoji) { - this.id = data.id; - this.name = data.name; - this.animated = !!data.animated; - this.available = !!data.available; - this.requireColons = !!data.require_colons; - this.session = session; - } - - readonly id?: Snowflake; - readonly session: Session; - - name?: string; - animated: boolean; - available: boolean; - requireColons: boolean; -} - -export class GuildEmoji extends Emoji implements Model { - constructor(session: Session, data: DiscordEmoji, guildId: Snowflake) { - super(session, data); - this.guildId = guildId; - this.roles = data.roles; - this.user = data.user ? new User(this.session, data.user) : undefined; - this.managed = !!data.managed; - this.id = super.id!; - } - - guildId: Snowflake; - roles?: Snowflake[]; - user?: User; - managed?: boolean; - - // id cannot be null in a GuildEmoji - override id: Snowflake; - - async edit(options: ModifyGuildEmoji): Promise { - const emoji = await Guild.prototype.editEmoji.call( - { id: this.guildId, session: this.session }, - this.id, - options - ); - - return emoji; - } - - async delete(reason?: string): Promise { - await Guild.prototype.deleteEmoji.call( - { id: this.guildId, session: this.session }, - this.id, - reason - ); - - return this; - } - - async fetchAuthor(): Promise { - const emoji = await this.session.rest.get(GUILD_EMOJIS(this.guildId, this.id)); - - if (emoji.user) { return new User(this.session, emoji.user); } - return null; - } - - setName(name: string): Promise { - return this.edit({ name }); - } - - get url(): string { - return EMOJI_URL(this.id, this.animated); - } - - toString(): string { - return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; - } -} diff --git a/packages/core/src/structures/guilds.ts b/packages/core/src/structures/guilds.ts deleted file mode 100644 index 8e7c5c9..0000000 --- a/packages/core/src/structures/guilds.ts +++ /dev/null @@ -1,1360 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { - PremiumTiers, - ChannelTypes, - DefaultMessageNotificationLevels, - DiscordBan, - DiscordEmoji, - DiscordGuild, - DiscordGuildPreview, - DiscordGuildWidget, - DiscordGuildWidgetSettings, - DiscordInvite, - DiscordInviteMetadata, - DiscordListActiveThreads, - DiscordMemberWithUser, - DiscordOverwrite, - DiscordRole, - DiscordVoiceRegion, - DiscordChannel, - ExplicitContentFilterLevels, - GuildNsfwLevel, - MakeRequired, - SystemChannelFlags, - VerificationLevels, - VideoQualityModes, - GetBans, - GetInvite, - ListGuildMembers, - GetAuditLogs, - DiscordAuditLog, - AuditLogEvents, - DiscordAuditLogChange } from '@biscuitland/api-types'; -import { - GUILD_AUDIT_LOGS, - INVITE, - GUILD_BANNER, - GuildFeatures, - GUILD_ICON, - GUILD_SPLASH, - USER_NICK, - GUILD_EMOJI, - GUILD_EMOJIS, - GUILDS, - GUILD_BANS, - GUILD_VOICE_REGIONS, - GUILD_BAN, - GUILD_PRUNE, - GUILD_INVITES, - GUILD_MEMBER, - GUILD_MEMBERS, - GUILD_MEMBER_ROLE, - GUILD_ROLE, - GUILD_ROLES, - THREAD_ACTIVE, - GUILD_PREVIEW, - GUILD_VANITY, - GUILD_WIDGET, - USER_GUILDS, - CHANNEL, - GUILD_CHANNELS } from '@biscuitland/api-types'; -import type { ImageFormat, ImageSize } from '../utils/util'; -import { Snowflake } from '../snowflakes'; -import { Util } from '../utils/util'; -import type { ReturnThreadsArchive, ChannelInGuild } from './channels'; -import { ChannelFactory, GuildChannel, ThreadChannel } from './channels'; -import { Member, ThreadMember } from './members'; -import { Role } from './role'; -import { GuildEmoji } from './emojis'; -import { urlToBase64 } from '../utils/url-to-base-64'; -import { Invite } from './invite'; -import { User } from './user'; -import { Widget } from './widget'; -import { Sticker } from './sticker'; -import { WelcomeScreen } from './welcome'; -import { AutoModerationRule } from './automod'; -import { Webhook } from './webhook'; -import { ScheduledEvent } from './scheduled-events'; -import Integration from './integration'; - -/** BaseGuild */ -/** - * Class for {@link Guild} and {@link AnonymousGuild} - */ -export abstract class BaseGuild implements Model { - constructor(session: Session, data: DiscordGuild) { - this.session = session; - this.id = data.id; - - this.name = data.name; - this.iconHash = data.icon ? data.icon : undefined; - - this.features = data.features; - } - - /** The session that instantiated the guild. */ - readonly session: Session; - - /** Guild id. */ - readonly id: Snowflake; - - /** Guild name. */ - name: string; - - /** - * Icon hash. Discord uses ids and hashes to render images in the client. - * @link https://discord.com/developers/docs/reference#image-formatting - */ - iconHash?: string; - - /** - * Enabled guild features (animated banner, news, auto moderation, etc). - * @see {@link GuildFeatures} - * @link https://discord.com/developers/docs/resources/guild#guild-object-guild-features - */ - features: GuildFeatures[]; - - /** createdTimestamp gets the current guild timestamp. */ - get createdTimestamp(): number { - return Snowflake.snowflakeToTimestamp(this.id); - } - - /** createdAt gets the creation Date object of the guild. */ - get createdAt(): Date { - return new Date(this.createdTimestamp); - } - - /** - * If the guild features includes partnered. - * @link https://discord.com/developers/docs/resources/guild#guild-object-guild-features - */ - get partnered(): boolean { - return this.features.includes(GuildFeatures.Partnered); - } - - /** gets the url of the guild that points to the guild */ - get url(): string { - return `https://discord.com/channels/${this.id}`; - } - - /** - * If the guild is verified. - * @link https://discord.com/developers/docs/resources/guild#guild-object-guild-features - */ - get verified(): boolean { - return this.features.includes(GuildFeatures.Verified); - } - - /** - * iconURL gets the current guild icon. - * @link https://discord.com/developers/docs/reference#image-formatting - */ - iconURL( - options: { size?: ImageSize; format?: ImageFormat } = { size: 128 } - ): string | void { - if (this.iconHash) { - return Util.formatImageURL( - GUILD_ICON(this.id, this.iconHash), - options.size, - options.format - ); - } - } - - /** toString gets the guild name */ - toString(): string { - return this.name; - } -} - -/** AnonymousGuild */ -/** - * Class for anonymous guilds. - * @see {@link BaseGuild} - * @link https://discord.com/developers/docs/resources/guild#guild-resource - */ -export class AnonymousGuild extends BaseGuild implements Model { - constructor(session: Session, data: Partial); // TODO: Improve this type (name and id are required) - constructor(session: Session, data: DiscordGuild) { - super(session, data); - - this.splashHash = data.splash - ? data.splash - : undefined; - - this.bannerHash = data.banner - ? data.banner - : undefined; - - this.verificationLevel = data.verification_level; - this.vanityUrlCode = data.vanity_url_code - ? data.vanity_url_code - : undefined; - - this.nsfwLevel = data.nsfw_level; - this.description = data.description ? data.description : undefined; - this.premiumSubscriptionCount = data.premium_subscription_count; - } - - /** - * The guild's splash hash. - * @link https://discord.com/developers/docs/reference#image-formatting - */ - splashHash?: string; - - /** - * The guild's banner hash. - * @link https://discord.com/developers/docs/reference#image-formatting - */ - bannerHash?: string; - - /** - * The guild's verification level. - * @see {@link VerificationLevels} - * @link https://discord.com/developers/docs/resources/guild#guild-object-verification-level - */ - verificationLevel: VerificationLevels; - - /** The guild's vanity url code. */ - vanityUrlCode?: string; - /** - * The guild's nsfw level. - * @see {@link GuildNsfwLevel} - * @link https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level - */ - nsfwLevel: GuildNsfwLevel; - - /** The guild's description. */ - description?: string; - - /** The number of boosts this guild currently has. */ - premiumSubscriptionCount?: number; - - /** - * splashURL gets the current guild splash as a string. - * @link https://discord.com/developers/docs/reference#image-formatting - * @param options - Image options for the splash url. - * @returns Splash url or void. - */ - splashURL( - options: { size?: ImageSize; format?: ImageFormat } = { size: 128 } - ): string | void { - if (this.splashHash) { - return Util.formatImageURL( - GUILD_SPLASH(this.id, this.splashHash), - options.size, - options.format - ); - } - } - - /** - * bannerURL gets the current guild banner as a string. - * @link https://discord.com/developers/docs/reference#image-formatting - * @param options - Image options for the banner url. - * @returns Banner url or void - */ - bannerURL( - options: { size?: ImageSize; format?: ImageFormat } = { size: 128 } - ): string | void { - if (this.bannerHash) { - return Util.formatImageURL( - GUILD_BANNER(this.id, this.bannerHash), - options.size, - options.format - ); - } - } -} - -/** InviteGuild */ -export class InviteGuild extends AnonymousGuild implements Model { - constructor(session: Session, data: Partial) { - super(session, data); - - if (data.welcome_screen) { - this.welcomeScreen = new WelcomeScreen( - session, - data.welcome_screen - ); - } - } - - welcomeScreen?: WelcomeScreen; -} - -/** - * Represent Discord Guild Preview Object - * @link https://discord.com/developers/docs/resources/guild#guild-preview-object - */ -export class GuildPreview implements Model { - constructor(session: Session, data: DiscordGuildPreview) { - this.session = session; - this.id = data.id; - this.name = data.name; - this.description = data.description ?? undefined; - - this.iconHash = data.icon - ? Util.iconHashToBigInt(data.icon) - : undefined; - - this.splashHash = data.splash - ? Util.iconHashToBigInt(data.splash) - : undefined; - - this.discoverySplashHash = data.discovery_splash - ? Util.iconHashToBigInt(data.discovery_splash) - : undefined; - - this.emojis = data.emojis.map( - x => new GuildEmoji(this.session, x, this.id) - ); - - this.features = data.features; - this.approximateMemberCount = data.approximate_member_count; - this.approximatePresenceCount = data.approximate_presence_count; - this.stickers = data.stickers.map(x => new Sticker(this.session, x)); - } - - session: Session; - /** guild id */ - id: Snowflake; - /** guild name (2-100 characters) */ - name: string; - iconHash?: bigint; - splashHash?: bigint; - discoverySplashHash?: bigint; - /** custom guild emojis */ - emojis: GuildEmoji[]; - /** enabled guild features */ - features: GuildFeatures[]; - /** approximate number of members in this guild */ - approximateMemberCount: number; - /** approximate number of online members in this guild */ - approximatePresenceCount: number; - /** the description for the guild */ - description?: string; - /** custom guild stickers */ - stickers: Sticker[]; -} - -/** Guild */ - -/** Maximun custom guild emojis per level */ -export type MaxEmojis = 50 | 100 | 150 | 250; - -/** Maximun custom guild stickers per level */ -export type MaxStickers = 5 | 15 | 30 | 60; - -export type EditBotNickname = { nick: string | null; reason?: string }; - -export interface CreateRole { - name?: string; - color?: number; - iconHash?: string | bigint; - unicodeEmoji?: string; - hoist?: boolean; - mentionable?: boolean; -} - -export interface ModifyGuildRole { - name?: string; - color?: number; - hoist?: boolean; - mentionable?: boolean; - unicodeEmoji?: string; -} - -export interface CreateGuildEmoji { - name: string; - image: string; - roles?: Snowflake[]; - reason?: string; -} - -export interface ModifyGuildEmoji { - name?: string; - roles?: Snowflake[]; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#create-guild-ban - */ -export interface CreateGuildBan { - deleteMessageDays?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - reason?: string; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#ban-object - */ -export interface GuildBan { - reason?: string; - user: User; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#guild-widget-settings-object-guild-widget-settings-structure - */ -export interface GuildWidgetSettings { - enabled: boolean; - channelId?: Snowflake; -} - -export interface PartialVanityURL { - code: string; - uses: number; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#modify-guild-member - */ -export interface ModifyGuildMember { - nick?: string; - roles?: Snowflake[]; - mute?: boolean; - deaf?: boolean; - channelId?: Snowflake; - communicationDisabledUntil?: number | null; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#begin-guild-prune - */ -export interface BeginGuildPrune { - days?: number; - computePruneCount?: boolean; - includeRoles?: Snowflake[]; -} - -export interface ModifyRolePositions { - id: Snowflake; - position?: number | null; -} - -export interface GuildCreateOptionsRole { - id: Snowflake; - name?: string; - color?: number; - hoist?: boolean; - position?: number; - permissions?: bigint; - mentionable?: boolean; - icon?: string; - unicodeEmoji?: string | null; -} - -export interface GuildCreateOptionsChannel { - id?: Snowflake; - parentId?: Snowflake; - type?: - | ChannelTypes.GuildText - | ChannelTypes.GuildVoice - | ChannelTypes.GuildCategory; - name: string; - topic?: string | null; - nsfw?: boolean; - bitrate?: number; - userLimit?: number; - rtcRegion?: string | null; - videoQualityMode?: VideoQualityModes; - permissionOverwrites?: MakeRequired, 'id'>[]; - rateLimitPerUser?: number; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#create-guild - */ -export interface GuildCreateOptions { - name: string; - afkChannelId?: Snowflake; - afkTimeout?: number; - channels?: GuildCreateOptionsChannel[]; - defaultMessageNotifications?: DefaultMessageNotificationLevels; - explicitContentFilter?: ExplicitContentFilterLevels; - icon?: string; - roles?: GuildCreateOptionsRole[]; - systemChannelFlags?: SystemChannelFlags; - systemChannelId?: Snowflake; - verificationLevel?: VerificationLevels; -} - -/** - * @link https://discord.com/developers/docs/resources/guild#modify-guild-json-params - */ -export interface GuildEditOptions extends Partial { - ownerId?: Snowflake; - splash?: string; - banner?: string; - discoverySplash?: string; - features?: GuildFeatures[]; - rulesChannelId?: Snowflake; - publicUpdatesChannelId?: Snowflake; - preferredLocale?: string | null; - description?: string; - premiumProgressBarEnabled?: boolean; -} - -export interface AuditLogResult { - auditLogEntries: { - targetId: string | null; - changes: { - key: DiscordAuditLogChange['key']; - oldValue: DiscordOverwrite[] | Role[] | string | number | boolean | null; - newValue: DiscordOverwrite[] | Role[] | string | number | boolean | null; - }[] | undefined; - userId: string | null; - id: string; - actionType: AuditLogEvents; - options: { - deleteMemberDays: string; - membersRemoved: string; - channelId: string; - messageId: string; - count: string; - id: string; - type: string; - roleName: string; - applicationId: string; - } | null; - reason: string | undefined; - }[]; - autoModerationRules: AutoModerationRule[] | undefined; - guildScheduledEvents: ScheduledEvent[] | undefined; - integrations: Integration[]; - threads: ThreadChannel[]; - users: User[]; - webhooks: Webhook[]; -} - -/** - * Represents a guild. - * @see {@link BaseGuild}. - * @link https://discord.com/developers/docs/resources/guild#guild-object - */ -export class Guild extends BaseGuild implements Model { - constructor(session: Session, data: DiscordGuild) { - super(session, data); - - this.splashHash = data.splash - ? Util.iconHashToBigInt(data.splash) - : undefined; - this.discoverySplashHash = data.discovery_splash - ? Util.iconHashToBigInt(data.discovery_splash) - : undefined; - this.ownerId = data.owner_id; - this.widgetEnabled = !!data.widget_enabled; - this.widgetChannelId = data.widget_channel_id - ? data.widget_channel_id - : undefined; - this.verificationLevel = data.verification_level; - this.defaultMessageNotificationLevel = - data.default_message_notifications; - this.explicitContentFilterLevel = data.explicit_content_filter; - this.premiumTier = data.premium_tier; - - this.members = new Map( - data.members?.map(member => [ - member.user!.id, - new Member(session, { ...member, user: member.user! }, data.id), - ]) - ); - - this.roles = new Map( - data.roles.map(role => [role.id, new Role(session, role, data.id)]) - ); - - this.emojis = new Map( - data.emojis.map(guildEmoji => [ - guildEmoji.id!, - new GuildEmoji(session, guildEmoji, data.id), - ]) - ); - - this.channels = new Map( - data.channels?.map(guildChannel => [ - guildChannel.id, - new GuildChannel(session, guildChannel, data.id), - ]) - ); - } - - /** - * The guild's splash hash. - * @link https://discord.com/developers/docs/reference#image-formatting - */ - splashHash?: bigint; - - /** - * Only present for guilds with the "DISCOVERABLE" feature - * @link https://discord.com/developers/docs/reference#image-formatting - */ - discoverySplashHash?: bigint; - - /** ID of the guild owner. */ - ownerId: Snowflake; - - /** True if the server widget is enabled */ - widgetEnabled: boolean; - - /** The channel id that the widget will generate an invite to, or undefined if set to no invite. */ - widgetChannelId?: Snowflake; - /** - * Verification level required for the guild. - * @see {@link VerificationLevels} - * @link https://discord.com/developers/docs/resources/guild#guild-object-verification-level - */ - verificationLevel: VerificationLevels; - - /** - * The default message notification level. - * @see {@link DefaultMessageNotificationLevels} - * @link https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level - */ - defaultMessageNotificationLevel: DefaultMessageNotificationLevels; - - /** - * The explicit content filter level. - * @see {@link ExplicitContentFilterLevels} - * @link https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level - */ - explicitContentFilterLevel: ExplicitContentFilterLevels; - - /** - * Premium tier (Server Boost level). - * @see {@link PremiumTiers} - * @link https://discord.com/developers/docs/resources/guild#guild-object-premium-tier - */ - premiumTier: PremiumTiers; - - /** - * A map with the guild's members. - * @see {@link Member} - * @link https://discord.com/developers/docs/resources/guild#guild-member-object - */ - members: Map; - - /** - * A map with the guild's roles. - * @see {@link Role} - * @link https://discord.com/developers/docs/topics/permissions#role-object - */ - - roles: Map; - - /** - * A map with the guild's emojis. - * @see {@link GuildEmoji} - * @link https://discord.com/developers/docs/resources/emoji#emoji-object-emoji-structure - */ - emojis: Map; - - /** - * A map with the guild's channels. - * @see {@link GuildChannel} - * @link https://discord.com/developers/docs/resources/channel#channel-object - */ - channels: Map; - - /** - * Returns the maximum number of emoji slots - */ - get maxEmojis(): MaxEmojis { - switch (this.premiumTier) { - case 1: - return 100; - case 2: - return 150; - case 3: - return 250; - default: - return 50; - } - } - - /** - * Returns the maximum number of custom sticker slots - */ - get maxStickers(): MaxStickers { - switch (this.premiumTier) { - case 1: - return 15; - case 2: - return 30; - case 3: - return 60; - default: - return 5; - } - } - - /** - * edits the bot's nickname in the guild. - * 'null' would reset the nickname. - */ - async editBotNickname( - options: EditBotNickname - ): Promise { - const result = await this.session.rest.patch< - { nick?: string } | undefined - >(USER_NICK(this.id), options); - - return result?.nick; - } - - /** - * creates an emoji in the guild. - * @see {@link CreateGuildEmoji} - * @see {@link GuildEmoji} - * @param options The options to create a emoji. - * @returns A promise that resolves to the guild's new emoji. - */ - async createEmoji(options: CreateGuildEmoji): Promise { - if (options.image && !options.image.startsWith('data:image/')) { - options.image = await urlToBase64(options.image); - } - - const emoji = await this.session.rest.post( - GUILD_EMOJIS(this.id), - options - ); - - return new GuildEmoji(this.session, emoji, this.id); - } - - /** - * deletes an emoji from the guild. - * @param id - The id of the emoji to delete. - * @param reason - The reason for deleting the emoji. - */ - async deleteEmoji(id: Snowflake, reason?: string): Promise { - await this.session.rest.delete(GUILD_EMOJI(this.id, id), { - reason, - }); - } - - /** - * edits an emoji in the guild. - * @see {@link ModifyGuildEmoji} - * @see {@link GuildEmoji} - * @param id - The id of the emoji to edit. - * @param options - Options to modify the emoji. - * @returns A promise that resolves to the edited emoji. - */ - async editEmoji( - id: Snowflake, - options: ModifyGuildEmoji - ): Promise { - const emoji = await this.session.rest.patch( - GUILD_EMOJI(this.id, id), - options - ); - - return new GuildEmoji(this.session, emoji, this.id); - } - - /** - * creates a role in the guild. - * @see {@link CreateRole} - * @see {@link Role} - * @param options - Options to create a new role. - */ - async createRole(options: CreateRole): Promise { - let icon: string | undefined; - - if (options.iconHash) { - if (typeof options.iconHash === 'string') { - icon = options.iconHash; - } else { - icon = Util.iconBigintToHash(options.iconHash); - } - } - - const role = await this.session.rest.put( - GUILD_ROLES(this.id), - { - name: options.name, - color: options.color, - icon, - unicode_emoji: options.unicodeEmoji, - hoist: options.hoist, - mentionable: options.mentionable, - } - ); - - return new Role(this.session, role, this.id); - } - - /** - * deletes a role from the guild. - * @param roleId - The id of the role to delete. - */ - async deleteRole(roleId: Snowflake): Promise { - await this.session.rest.delete( - GUILD_ROLE(this.id, roleId), - {} - ); - } - - /** - * edits a role in the guild. - * @see {@link ModifyGuildRole} - * @see {@link Role} - * @param roleId - The id of the role to edit. - * @param options - Options to modify the role. - */ - async editRole(roleId: Snowflake, options: ModifyGuildRole): Promise { - const role = await this.session.rest.patch( - GUILD_ROLE(this.id, roleId), - { - name: options.name, - color: options.color, - hoist: options.hoist, - mentionable: options.mentionable, - } - ); - - return new Role(this.session, role, this.id); - } - - /** - * adds a role to a user in the guild. - * @param memberId - The id of the member to add a role to. - * @param roleId - The id of the role to add. - * @param reason - The reason for adding the role to the member. - */ - async addRole( - memberId: Snowflake, - roleId: Snowflake, - reason?: string - ): Promise { - await this.session.rest.put( - GUILD_MEMBER_ROLE(this.id, memberId, roleId), - { reason } - ); - } - - /** - * removes a role from a user in the guild. - * @param memberId - The id of the member to remove a role from. - * @param roleId - The id of the role to remove. - * @param reason - The reason for removing the role from the member. - */ - async removeRole( - memberId: Snowflake, - roleId: Snowflake, - reason?: string - ): Promise { - await this.session.rest.delete( - GUILD_MEMBER_ROLE(this.id, memberId, roleId), - { reason } - ); - } - - /** - * the roles moved. - * @see {@link ModifyRolePositions} - * @see {@link Role} - * @param options - Options to modify the roles. - */ - async moveRoles(options: ModifyRolePositions[]): Promise { - const roles = await this.session.rest.patch( - GUILD_ROLES(this.id), - options - ); - - return roles.map(role => new Role(this.session, role, this.id)); - } - - /** - * deletes an invite from the guild. - * @param inviteCode - The invite code to get the invite for. - */ - async deleteInvite(inviteCode: string): Promise { - await this.session.rest.delete(INVITE(inviteCode), {}); - } - - /** - * gets an invite from the guild. - * @see {@link Routes.GetInvite} - * @see {@link Invite} - * @param inviteCode - The invite code to get the invite for. - * @param options - Options to get the invite. - * @returns Promise resolving to the invite. - */ - async fetchInvite(inviteCode: string, options: GetInvite): Promise { - const inviteMetadata = - await this.session.rest.get( - INVITE(inviteCode, options) - ); - - return new Invite(this.session, inviteMetadata); - } - - /** - * gets all invites from the guild. - * @see {@link Invite} - * @returns A promise that resolves to the guild's invites. - */ - async fetchInvites(): Promise { - const invites = await this.session.rest.get( - GUILD_INVITES(this.id) - ); - - return invites.map(invite => new Invite(this.session, invite)); - } - - /** - * bans a member from the guild. - * @see {@link CreateGuildBan} - * @param memberId - The id of the member to ban. - * @param options - Options to ban the member. - */ - async banMember( - memberId: Snowflake, - options: CreateGuildBan - ): Promise { - await this.session.rest.put( - GUILD_BAN(this.id, memberId), - options - ? { - delete_message_days: options.deleteMessageDays, - reason: options.reason, - } - : {} - ); - } - - /** - * kicks a member from the guild. - * @param memberId - The id of the member to kick. - * @param reason - The reason for kicking the member. - */ - async kickMember(memberId: Snowflake, reason?: string): Promise { - await this.session.rest.delete( - GUILD_MEMBER(this.id, memberId), - { reason } - ); - } - - /** - * unbans a member from the guild. - * @param memberId - The id of the member to get. - */ - async unbanMember(memberId: Snowflake): Promise { - await this.session.rest.delete( - GUILD_BAN(this.id, memberId), - {} - ); - } - - /** - * edits a member in the guild. - * @see {@link ModifyGuildMember} - * @see {@link Member} - * @param memberId - The id of the member to get. - * @param options - Options to edit the member. - * @returns Promise resolving to the edited member. - */ - async editMember( - memberId: Snowflake, - options: ModifyGuildMember - ): Promise { - const member = await this.session.rest.patch( - GUILD_MEMBER(this.id, memberId), - { - nick: options.nick, - roles: options.roles, - mute: options.mute, - deaf: options.deaf, - channel_id: options.channelId, - communication_disabled_until: options.communicationDisabledUntil - ? new Date(options.communicationDisabledUntil).toISOString() - : undefined, - } - ); - - return new Member(this.session, member, this.id); - } - - /** - * prunes members from the guild. - * @see {@link BeginGuildPrune} - * @param options - Options to prune the members. - * @returns A promise that resolves to the number of members pruned. - */ - async pruneMembers(options: BeginGuildPrune): Promise { - const result = await this.session.rest.post<{ pruned: number }>( - GUILD_PRUNE(this.id), - { - days: options.days, - compute_prune_count: options.computePruneCount, - include_roles: options.includeRoles, - } - ); - - return result.pruned; - } - - /** - * gets the number of members that would be pruned. - * @returns A promise that resolves to the number of members that would be pruned. - */ - async getPruneCount(): Promise { - const result = await this.session.rest.get<{ pruned: number }>( - GUILD_PRUNE(this.id) - ); - - return result.pruned; - } - - /** - * gets the active threads in the guild. - * @see {@link ReturnThreadsArchive} - * @returns Promise resolving a ReturnThreadsArchive without hasMore property. - */ - async getActiveThreads(): Promise> { - const { threads, members } = - await this.session.rest.get( - THREAD_ACTIVE(this.id) - ); - - return { - threads: Object.fromEntries( - threads.map(thread => [ - thread.id, - new ThreadChannel(this.session, thread, this.id), - ]) - ), - members: Object.fromEntries( - members.map(threadMember => [ - threadMember.id, - new ThreadMember(this.session, threadMember), - ]) - ), - }; - } - - /** * - * Deletes the guild. - */ - async delete_(): Promise { - await this.session.rest.delete(GUILDS(this.id), {}); - } - - /** - * Leaves the guild. - */ - async leave(): Promise { - await this.session.rest.delete(USER_GUILDS(this.id), {}); - } - - /** - * Creates a guild and returns its data, the bot joins the guild - * This was modified from discord.js to make it compatible - * precondition: Bot should be in less than 10 servers - * @see {@link Session} - * @see {@link GuildCreateOptions} - * @see {@link Guild} - * @param session - The session the guild should be created in. - * @param options - Options to create the guild. - * @returns A promise that resolves to the created guild. - */ - static async create( - session: Session, - options: GuildCreateOptions - ): Promise { - const guild = await session.rest.post(GUILDS(), { - name: options.name, - afk_channel_id: options.afkChannelId, - afk_timeout: options.afkTimeout, - default_message_notifications: options.defaultMessageNotifications, - explicit_content_filter: options.explicitContentFilter, - system_channel_flags: options.systemChannelFlags, - verification_level: options.verificationLevel, - icon: options.icon, - channels: options.channels?.map(channel => ({ - name: channel.name, - nsfw: channel.nsfw, - id: channel.id, - bitrate: channel.bitrate, - parent_id: channel.parentId, - permission_overwrites: channel.permissionOverwrites, - rtc_region: channel.rtcRegion, - user_limit: channel.userLimit, - video_quality_mode: channel.videoQualityMode, - rate_limit_per_user: channel.rateLimitPerUser, - })), - roles: options.roles?.map(role => ({ - name: role.name, - id: role.id, - color: role.color, - mentionable: role.mentionable, - hoist: role.hoist, - position: role.position, - unicode_emoji: role.unicodeEmoji, - icon: options.icon, - })), - }); - - return new Guild(session, guild); - } - - /** - * sets a new splash for the guild. Same as Guild.edit({..., splash: 'splashURL'}) - * @see {@link Guild} - */ - setSplash(splash: string): Promise { - return this.edit({ splash }); - } - - /** - * sets a new banner for the guild. Same as Guild.edit({..., banner: 'bannerURL'}) - * @see {@link Guild} - */ - setBanner(banner: string): Promise { - return this.edit({ banner }); - } - - /** - * Sets a new guild discovery splash image. Same as Guild.edit({..., discoverySplashURL: 'discoverySplashURL'}) - * @see {@link Guild} - */ - setDiscoverySplash(discoverySplash: string): Promise { - return this.edit({ discoverySplash }); - } - - /** - * Sets a new guild icon image. Same as Guild.edit({..., icon: 'url'}) - * @param icon - The new icon for the guild. - * @see {@link Guild} - */ - setIcon(icon: string): Promise { - return this.edit({ icon }); - } - - /** - * Edits a guild and returns its data. - * @see {@link Guild} - * @see {@link GuildEditOptions} - * @param options - Options to edit the guild. - * @returns A promise that resolves to the edited guild. - */ - async edit(options: GuildEditOptions): Promise { - const guild = await this.session.rest.patch(GUILDS(this.id), { - name: options.name, - afk_channel_id: options.afkChannelId, - afk_timeout: options.afkTimeout, - default_message_notifications: options.defaultMessageNotifications, - explicit_content_filter: options.explicitContentFilter, - system_channel_flags: options.systemChannelFlags, - verification_level: options.verificationLevel, - icon: options.icon, - // extra props - splash: options.splash, - banner: options.banner, - discovery_splash: options.discoverySplash, - owner_id: options.ownerId, - rules_channel_id: options.rulesChannelId, - public_updates_channel_id: options.publicUpdatesChannelId, - preferred_locale: options.preferredLocale, - features: options.features, - description: options.description, - premiumProgressBarEnabled: options.premiumProgressBarEnabled, - }); - - return new Guild(this.session, guild); - } - - /** - * gets the auto moderation rules for the guild. - * @see {@link AutoModerationRule#getRules} sames - * @param ruleId The optional rule id - * @returns - */ - fetchAutoModerationRules(ruleId?: Snowflake): Promise { - return AutoModerationRule.prototype.getRules.call({ session: this.session, guildId: this.id }, ruleId); - } - - /** - * gets the voice regions available for the guild. - * @see {@link DiscordVoiceRegion} - * @returns Promise that resolves to an array of voice regions. - */ - fetchVoiceRegions(): Promise { - return this.session.rest.get( - GUILD_VOICE_REGIONS(this.id) - ); - } - - /** - * Fetches user ban in the guild - * @param userId The user id - * @returns Resolves Discord Ban - */ - async fetchBan(userId: Snowflake): Promise { - const ban = await this.session.rest.get( - GUILD_BAN(this.id, userId) - ); - - return { - reason: ban.reason ?? undefined, - user: new User(this.session, ban.user), - }; - } - - /** - * Fetches bans in the guild - * @param options - * @returns Resolve with list of bans - */ - async fetchBans(options?: GetBans): Promise { - const bans = await this.session.rest.get( - GUILD_BANS(this.id, options) - ); - - return bans.map(x => { - return { - reason: x.reason ?? undefined, - user: new User(this.session, x.user), - }; - }); - } - - /** - * Fetches settings for {@link Widget} in the guild - * @returns Resolves with the settings - */ - async fetchWidgetSettings(): Promise { - const widget = await this.session.rest.get( - GUILD_WIDGET(this.id) - ); - - return { - enabled: !!widget.enabled, - channelId: widget.channel_id ?? undefined, - }; - } - - /** - * Fetches widget in the guild - * @returns Resolves with the Widget - */ - async fetchWidget(): Promise { - const widget = await this.session.rest.get( - GUILD_WIDGET(this.id, { get: 'json' }) - ); - - return new Widget(this.session, widget); - } - - /** - * Fetches vanity url invite - * @see {@link Invite} - * @returns Resolves a Invite - */ - async fetchVanityURL(): Promise> { - const vanity = await this.session.rest.get( - GUILD_VANITY(this.id) - ); - - return new Invite(this.session, vanity); - } - - /** - * Fetches preview of the guild - * @returns Resolves a Guild Preview object - */ - async fetchGuildPreview(): Promise { - const preview = await this.session.rest.get( - GUILD_PREVIEW(this.id) - ); - - return new GuildPreview(this.session, preview); - } - - async fetchChannel(channelId: string): Promise { - const channel = await this.session.rest.get(CHANNEL(channelId)); - - return ChannelFactory.fromGuildChannel(this.session, channel); - } - - async fetchChannels(): Promise { - const channels = await this.session.rest.get(GUILD_CHANNELS(this.id)); - - return channels.map(channel => ChannelFactory.fromGuildChannel(this.session, channel)); - } - - async fetchAuditLogs(options?: GetAuditLogs): Promise { - const auditLog = await this.session.rest.get(GUILD_AUDIT_LOGS(this.id, options)); - return { - auditLogEntries: auditLog.audit_log_entries.map(x => ({ - targetId: x.target_id, - changes: x.changes?.map(j => ({ - key: j.key, - oldValue: j.old_value - ? j.key === 'permission_overwrites' - ? (j.old_value as DiscordOverwrite[]) - : ['$add', '$remove'].includes(j.key) - ? (j.old_value as DiscordRole[]).map(j => new Role(this.session, { ...j, permissions: j.permissions || '0' }, this.id)) - : j.old_value as string | number | boolean - : null, - newValue: j.new_value - ? j.key === 'permission_overwrites' - ? (j.new_value as DiscordOverwrite[]) - : ['$add', '$remove'].includes(j.key) - ? (j.new_value as DiscordRole[]).map(j => new Role(this.session, { ...j, permissions: j.permissions || '0' }, this.id)) - : j.new_value as string | number | boolean - : null, - })), - userId: x.user_id, - id: x.id, - actionType: x.action_type, - options: x.options ? { - deleteMemberDays: x.options.delete_member_days, - membersRemoved: x.options.members_removed, - channelId: x.options.channel_id, - messageId: x.options.message_id, - count: x.options.count, - id: x.options.id, - type: x.options.type, - roleName: x.options.role_name, - applicationId: x.options.application_id - } : null, - reason: x.reason, - })), - autoModerationRules: auditLog.auto_moderation_rules?.map(x => new AutoModerationRule(this.session, x)), - guildScheduledEvents: auditLog.guild_scheduled_events?.map(x => new ScheduledEvent(this.session, x)), - integrations: auditLog.integrations.map(x => new Integration(this.session, { - guild_id: this.id, - ...x, - })), - threads: auditLog.threads.map(x => ChannelFactory.fromGuildChannel(this.session, x) as ThreadChannel), - users: auditLog.users.map(x => new User(this.session, x)), - webhooks: auditLog.webhooks.map(x => new Webhook(this.session, x)), - }; - } - - async fetchOwner(): Promise { - return this.fetchMember(this.ownerId); - } - - /** fetches a member */ - async fetchMember(memberId: Snowflake): Promise { - const member = await this.session.rest.get( - GUILD_MEMBER(this.id, memberId) - ); - - return new Member(this.session, member, this.id); - } - - /** fetches multiple members */ - async fetchMembers(options?: ListGuildMembers): Promise { - const members = await this.session.rest.get( - GUILD_MEMBERS(this.id, options) - ); - - return members.map(member => new Member(this.session, member, this.id)); - } -} diff --git a/packages/core/src/structures/integration.ts b/packages/core/src/structures/integration.ts deleted file mode 100644 index 46d44ac..0000000 --- a/packages/core/src/structures/integration.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { - DiscordIntegration, - IntegrationExpireBehaviors, -} from '@biscuitland/api-types'; -import { User } from './user'; - -export type IntegrationTypes = 'twitch' | 'youtube' | 'discord'; - -export interface IntegrationAccount { - id: Snowflake; - name: string; -} - -export interface IntegrationApplication { - id: Snowflake; - name: string; - icon?: string; - description: string; - bot?: User; -} - -export class Integration implements Model { - constructor( - session: Session, - data: DiscordIntegration & { guild_id?: Snowflake } - ) { - this.id = data.id; - this.session = session; - - data.guild_id ? (this.guildId = data.guild_id) : null; - - this.name = data.name; - this.type = data.type; - this.enabled = !!data.enabled; - this.syncing = !!data.syncing; - this.roleId = data.role_id; - this.enableEmoticons = !!data.enable_emoticons; - this.expireBehavior = data.expire_behavior; - this.expireGracePeriod = data.expire_grace_period; - this.syncedAt = data.synced_at; - this.subscriberCount = data.subscriber_count; - this.revoked = !!data.revoked; - - this.user = data.user ? new User(session, data.user) : undefined; - this.account = { - id: data.account.id, - name: data.account.name, - }; - - if (data.application) { - this.application = { - id: data.application.id, - name: data.application.name, - icon: data.application.icon ? data.application.icon : undefined, - description: data.application.description, - bot: data.application.bot - ? new User(session, data.application.bot) - : undefined, - }; - } - } - - readonly session: Session; - id: Snowflake; - guildId?: Snowflake; - - name: string; - type: IntegrationTypes; - enabled?: boolean; - syncing?: boolean; - roleId?: string; - enableEmoticons?: boolean; - expireBehavior?: IntegrationExpireBehaviors; - expireGracePeriod?: number; - syncedAt?: string; - subscriberCount?: number; - revoked?: boolean; - - user?: User; - account: IntegrationAccount; - application?: IntegrationApplication; -} - -export default Integration; diff --git a/packages/core/src/structures/interactions.ts b/packages/core/src/structures/interactions.ts deleted file mode 100644 index 4051aa4..0000000 --- a/packages/core/src/structures/interactions.ts +++ /dev/null @@ -1,669 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { - ApplicationCommandTypes, - DiscordInteraction, - DiscordMessage, - DiscordMessageComponents, - DiscordMemberWithUser, - DiscordMessageInteraction, - Locales -} from '@biscuitland/api-types'; -import type { CreateMessage } from './message'; -import type { MessageFlags } from '../utils/util'; -import type { EditWebhookMessage } from './webhook'; -import { - InteractionResponseTypes, - InteractionTypes, - MessageComponentTypes, - INTERACTION_ID_TOKEN, - WEBHOOK_MESSAGE, - WEBHOOK_MESSAGE_ORIGINAL -} from '@biscuitland/api-types'; - -import { Role } from './role'; -import { Attachment } from './attachment'; -import { Snowflake } from '../snowflakes'; -import { User } from './user'; -import { Member } from './members'; -import { Message } from './message'; -import { Permissions } from './special/permissions'; -import { Webhook } from './webhook'; -import { InteractionOptions } from './special/interaction-options'; -import { NewEmbed } from './embed'; - -export type InteractionResponseWith = { - with: InteractionApplicationCommandCallbackData; -}; -export type InteractionResponseWithData = - | InteractionResponse - | InteractionResponseWith; - -/** - * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response - */ -export interface InteractionResponse { - type: InteractionResponseTypes; - data?: InteractionApplicationCommandCallbackData; -} - -/** - * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata - */ -export interface InteractionApplicationCommandCallbackData - extends Pick< - CreateMessage, - 'allowedMentions' | 'content' | 'embeds' | 'files' - > { - customId?: string; - title?: string; - components?: DiscordMessageComponents; - flags?: MessageFlags; - choices?: ApplicationCommandOptionChoice[]; -} - -/** - * @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice - */ -export interface ApplicationCommandOptionChoice { - name: string; - value: string | number; -} - -export abstract class BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - this.session = session; - this.id = data.id; - this.token = data.token; - this.type = data.type; - this.guildId = data.guild_id; - this.channelId = data.channel_id; - this.applicationId = data.application_id; - this.version = data.version; - - this.locale = data.locale as Locales; - this.guildLocale = data.guild_locale as Locales; - - const perms = data.app_permissions; - - if (perms) { - this.appPermissions = new Permissions(BigInt(perms)); - } - - if (!data.guild_id) { - this.user = new User(session, data.user!); - } else { - this.member = new Member(session, data.member!, data.guild_id); - - // dangerous black magic be careful! - Object.defineProperty(this, 'user', { - get() { - return this.member.user; - } - }); - } - } - - readonly session: Session; - readonly id: Snowflake; - readonly token: string; - - type: InteractionTypes; - guildId?: Snowflake; - channelId?: Snowflake; - applicationId?: Snowflake; - user!: User; - member?: Member; - appPermissions?: Permissions; - - // must be implemented - locale: Locales; - guildLocale: Locales; - - // readonly property according to docs - readonly version: 1; - - responded = false; - - get createdTimestamp(): number { - return Snowflake.snowflakeToTimestamp(this.id); - } - - get createdAt(): Date { - return new Date(this.createdTimestamp); - } - - isCommand(): this is CommandInteraction { - return this.type === InteractionTypes.ApplicationCommand; - } - - isAutoComplete(): this is AutoCompleteInteraction { - return this.type === InteractionTypes.ApplicationCommandAutocomplete; - } - - isComponent(): this is ComponentInteraction { - return this.type === InteractionTypes.MessageComponent; - } - - isPing(): this is PingInteraction { - return this.type === InteractionTypes.Ping; - } - - isModalSubmit(): this is ModalSubmitInteraction { - return this.type === InteractionTypes.ModalSubmit; - } - - inGuild(): this is this & { guildId: Snowflake } { - return !!this.guildId; - } - - // webhooks methods: - - async editReply( - options: EditWebhookMessage & { messageId?: Snowflake } - ): Promise { - const message = await this.session.rest.patch< - DiscordMessage | undefined - >( - options.messageId - ? WEBHOOK_MESSAGE(this.session.applicationId, this.token, options.messageId) - : WEBHOOK_MESSAGE_ORIGINAL(this.session.applicationId, this.token), - { - content: options.content, - embeds: options.embeds?.map(NewEmbed), - file: options.files, - components: options.components, - allowed_mentions: options.allowedMentions && { - parse: options.allowedMentions.parse, - replied_user: options.allowedMentions.repliedUser, - users: options.allowedMentions.users, - roles: options.allowedMentions.roles, - }, - attachments: options.attachments?.map(attachment => { - return { - id: attachment.id, - filename: attachment.name, - content_type: attachment.contentType, - size: attachment.size, - url: attachment.attachment, - proxy_url: attachment.proxyUrl, - height: attachment.height, - width: attachment.width, - }; - }), - message_id: options.messageId, - } - ); - - if (!message || !options.messageId) { - return message as undefined; - } - - return new Message(this.session, message); - } - - async sendFollowUp( - options: InteractionApplicationCommandCallbackData - ): Promise { - const message = await Webhook.prototype.execute.call( - { - id: this.applicationId!, - token: this.token, - session: this.session, - }, - options - ); - - return message!; - } - - async editFollowUp( - messageId: Snowflake, - options?: { threadId: Snowflake } - ): Promise { - const message = await Webhook.prototype.editMessage.call( - { - id: this.session.applicationId, - session: this.session, - token: this.token, - }, - messageId, - options - ); - - return message; - } - - async deleteEphemeral(messageId?: Snowflake): Promise { - await Webhook.prototype.deleteFollowUp.call( - { - id: this.session.applicationId, - session: this.session, - token: this.token, - }, - messageId - ); - } - - async deleteFollowUp( - messageId: Snowflake, - threadId?: Snowflake - ): Promise { - await Webhook.prototype.deleteMessage.call( - { - id: this.session.applicationId, - session: this.session, - token: this.token, - }, - messageId, - threadId - ); - } - - async fetchFollowUp( - messageId: Snowflake, - threadId?: Snowflake - ): Promise { - const message = await Webhook.prototype.fetchMessage.call( - { - id: this.session.applicationId, - session: this.session, - token: this.token, - }, - messageId, - threadId - ); - - return message; - } - - // end webhook methods - - async respond(resp: InteractionResponse): Promise; - async respond(resp: InteractionResponseWith): Promise; - async respond( - resp: InteractionResponseWithData - ): Promise { - const options = 'with' in resp ? resp.with : resp.data; - const type = - 'type' in resp - ? resp.type - : InteractionResponseTypes.ChannelMessageWithSource; - - const data = { - content: options?.content, - custom_id: options?.customId, - file: options?.files, - allowed_mentions: options?.allowedMentions, - flags: options?.flags, - chocies: options?.choices, - embeds: options?.embeds?.map(NewEmbed), - title: options?.title, - components: options?.components, - }; - - if (!this.responded) { - await this.session.rest.post( - INTERACTION_ID_TOKEN(this.id, this.token), - { - file: options?.files, - type, - data, - } - ); - - this.responded = true; - return; - } - - return this.sendFollowUp(data); - } - - /** - * internal usage only, same as respond but doesn't tries to follow up - * */ - async respond_(resp: InteractionResponse): Promise { - if (!this.responded) return this.respond(resp) as Promise; - } - - // start custom methods - - async respondWith( - resp: InteractionApplicationCommandCallbackData - ): Promise { - const m = await this.respond({ with: resp }); - - return m; - } - - async defer() { - await this.respond({ - type: InteractionResponseTypes.DeferredChannelMessageWithSource, - }); - } - - async autocomplete() { - await this.respond({ - type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, - }); - } - - /** - * taken from Detritus - * try respond, try edit, try follow up - * */ - async editOrReply(resp: InteractionResponseWithData & EditWebhookMessage) { - if (this.responded) { - return this.editReply(resp); - } - - let type: InteractionResponseTypes = InteractionResponseTypes.ChannelMessageWithSource; - - switch (this.type) { - case InteractionTypes.ApplicationCommand: - type = InteractionResponseTypes.ChannelMessageWithSource; - break; - case InteractionTypes.MessageComponent: - type = InteractionResponseTypes.UpdateMessage; - break; - } - - const result = await this.respond({ type, data: resp }); - - return result; - } - - // end custom methods -} - -export class AutoCompleteInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.type = data.type as number; - this.commandId = data.data!.id; - this.commandName = data.data!.name; - this.commandType = data.data!.type; - this.commandGuildId = data.data!.guild_id; - this.options = new InteractionOptions( - data.data!.options ?? [] - ); - } - - override type: InteractionTypes.ApplicationCommandAutocomplete; - commandId: Snowflake; - commandName: string; - commandType: ApplicationCommandTypes; - commandGuildId?: Snowflake; - options: InteractionOptions; - - async respondWithChoices( - choices: ApplicationCommandOptionChoice[] - ): Promise { - await this.session.rest.post( - INTERACTION_ID_TOKEN(this.id, this.token), - { - data: { choices }, - type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, - } - ); - } -} - -export interface CommandInteractionDataResolved { - users: Map; - members: Map; - roles: Map; - messages: Map; - attachments: Map; -} - -export class CommandInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.type = data.type as number; - this.commandId = data.data!.id; - this.commandName = data.data!.name; - this.commandType = data.data!.type; - this.commandGuildId = data.data!.guild_id; - this.options = new InteractionOptions( - data.data!.options ?? [] - ); - - this.resolved = { - users: new Map(), - members: new Map(), - roles: new Map(), - attachments: new Map(), - messages: new Map(), - }; - - if (data.data!.resolved?.users) { - for (const [id, u] of Object.entries(data.data!.resolved.users)) { - this.resolved.users.set(id, new User(session, u)); - } - } - - if (data.data!.resolved?.members && !!super.guildId) { - for (const [id, m] of Object.entries(data.data!.resolved.members)) { - this.resolved.members.set( - id, - new Member( - session, - m as DiscordMemberWithUser, - super.guildId! - ) - ); - } - } - - if (data.data!.resolved?.roles && !!super.guildId) { - for (const [id, r] of Object.entries(data.data!.resolved.roles)) { - this.resolved.roles.set( - id, - new Role(session, r, super.guildId!) - ); - } - } - - if (data.data!.resolved?.attachments) { - for (const [id, a] of Object.entries( - data.data!.resolved.attachments - )) { - this.resolved.attachments.set(id, new Attachment(session, a)); - } - } - - if (data.data!.resolved?.messages) { - for (const [id, m] of Object.entries( - data.data!.resolved.messages - )) { - this.resolved.messages.set(id, new Message(session, m)); - } - } - } - - override type: InteractionTypes.ApplicationCommand; - commandId: Snowflake; - commandName: string; - commandType: ApplicationCommandTypes; - commandGuildId?: Snowflake; - resolved: CommandInteractionDataResolved; - options: InteractionOptions; -} - -export type ModalInMessage = ModalSubmitInteraction & { - message: Message; -}; - -export class ModalSubmitInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.type = data.type as number; - this.componentType = data.data!.component_type!; - this.customId = data.data!.custom_id; - this.targetId = data.data!.target_id; - this.values = data.data!.values; - - this.components = data.data?.components?.map( - ModalSubmitInteraction.transformComponent - ); - - if (data.message) { - this.message = new Message(session, data.message); - } - } - - override type: InteractionTypes.MessageComponent; - componentType: MessageComponentTypes; - customId?: string; - targetId?: Snowflake; - values?: string[]; - message?: Message; - components; - - static transformComponent(component: DiscordMessageComponents[number]) { - return { - type: component.type, - components: component.components.map(component => { - return { - customId: component.custom_id, - value: (component as typeof component & { value: string }) - .value, - }; - }), - }; - } - - inMessage(): this is ModalInMessage { - return !!this.message; - } -} - -export class PingInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.type = data.type as number; - this.commandId = data.data!.id; - this.commandName = data.data!.name; - this.commandType = data.data!.type; - this.commandGuildId = data.data!.guild_id; - } - - override type: InteractionTypes.Ping; - commandId: Snowflake; - commandName: string; - commandType: ApplicationCommandTypes; - commandGuildId?: Snowflake; - override locale = undefined as never; - override guildLocale = undefined as never; - - async pong(): Promise { - await this.session.rest.post( - INTERACTION_ID_TOKEN(this.id, this.token), - { - type: InteractionResponseTypes.Pong, - } - ); - } -} - -export class ComponentInteraction extends BaseInteraction implements Model { - constructor(session: Session, data: DiscordInteraction) { - super(session, data); - this.type = data.type as number; - this.componentType = data.data!.component_type!; - this.customId = data.data!.custom_id; - this.targetId = data.data!.target_id; - this.values = data.data!.values; - this.message = new Message(session, data.message!); - } - - override type: InteractionTypes.MessageComponent; - componentType: MessageComponentTypes; - customId?: string; - targetId?: Snowflake; - values?: string[]; - message: Message; - - isButton(): boolean { - return this.componentType === MessageComponentTypes.Button; - } - - isActionRow(): boolean { - return this.componentType === MessageComponentTypes.ActionRow; - } - - isTextInput(): boolean { - return this.componentType === MessageComponentTypes.InputText; - } - - isSelectMenu(): boolean { - return this.componentType === MessageComponentTypes.SelectMenu; - } - - async deferUpdate() { - await this.respond({ - type: InteractionResponseTypes.DeferredUpdateMessage, - }); - } -} - -/** - * @link https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure - */ -export interface MessageInteraction { - /** id of the interaction */ - id: Snowflake; - /** type of interaction */ - type: InteractionTypes; - /** name of the application command, including subcommands and subcommand groups */ - name: string; - /** user who invoked the interaction */ - user: User; - /** member who invoked the interaction in the guild */ - member?: Partial; -} - -export type Interaction = - | CommandInteraction - | ComponentInteraction - | PingInteraction - | AutoCompleteInteraction - | ModalSubmitInteraction; - -export class InteractionFactory { - static from( - session: Session, - interaction: DiscordInteraction - ): Interaction { - switch (interaction.type) { - case InteractionTypes.Ping: - return new PingInteraction(session, interaction); - case InteractionTypes.ApplicationCommand: - return new CommandInteraction(session, interaction); - case InteractionTypes.MessageComponent: - return new ComponentInteraction(session, interaction); - case InteractionTypes.ApplicationCommandAutocomplete: - return new AutoCompleteInteraction(session, interaction); - case InteractionTypes.ModalSubmit: - return new ModalSubmitInteraction(session, interaction); - } - } - - static fromMessage( - session: Session, - interaction: DiscordMessageInteraction, - _guildId?: Snowflake - ): MessageInteraction { - const obj = { - id: interaction.id, - type: interaction.type, - name: interaction.name, - user: new User(session, interaction.user), - // TODO: Parse member somehow with the guild id passed in message - }; - - return obj; - } -} - diff --git a/packages/core/src/structures/invite.ts b/packages/core/src/structures/invite.ts deleted file mode 100644 index abdd029..0000000 --- a/packages/core/src/structures/invite.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { - DiscordApplication, - DiscordChannel, - DiscordInvite, - DiscordInviteCreate, - DiscordMemberWithUser, - DiscordScheduledEventEntityMetadata, - ScheduledEventEntityType, - ScheduledEventPrivacyLevel, - ScheduledEventStatus, - TargetTypes, -} from '@biscuitland/api-types'; -import { GuildChannel } from './channels'; -import { Member } from './members'; -import { Guild, InviteGuild } from './guilds'; -import { User } from './user'; -import { Application } from './application'; - -export interface InviteStageInstance { - /** The members speaking in the Stage */ - members: Partial[]; - /** The number of users in the Stage */ - participantCount: number; - /** The number of users speaking in the Stage */ - speakerCount: number; - /** The topic of the Stage instance (1-120 characters) */ - topic: string; -} - -export interface InviteScheduledEvent { - id: Snowflake; - guildId: string; - channelId?: string; - creatorId?: string; - name: string; - description?: string; - scheduledStartTime: string; - scheduledEndTime?: string; - privacyLevel: ScheduledEventPrivacyLevel; - status: ScheduledEventStatus; - entityType: ScheduledEventEntityType; - entityId?: string; - entityMetadata?: DiscordScheduledEventEntityMetadata; - creator?: User; - userCount?: number; - image?: string; -} - -export interface InviteCreate { - channelId: string; - code: string; - createdAt: string; - guildId?: string; - inviter?: User; - maxAge: number; - maxUses: number; - targetType: TargetTypes; - targetUser?: User; - targetApplication?: Partial; - temporary: boolean; - uses: number; -} - -export function NewInviteCreate( - session: Session, - invite: DiscordInviteCreate -): InviteCreate { - return { - channelId: invite.channel_id, - code: invite.code, - createdAt: invite.created_at, - guildId: invite.guild_id, - inviter: invite.inviter ? new User(session, invite.inviter) : undefined, - maxAge: invite.max_age, - maxUses: invite.max_uses, - targetType: invite.target_type, - targetUser: invite.target_user - ? new User(session, invite.target_user) - : undefined, - targetApplication: - invite.target_application && - new Application( - session, - invite.target_application as DiscordApplication - ), - temporary: invite.temporary, - uses: invite.uses, - }; -} - -/** - * @link https://discord.com/developers/docs/resources/invite#invite-object - */ -export class Invite { - constructor(session: Session, data: DiscordInvite) { - this.session = session; - - this.guild = data.guild - ? new InviteGuild(session, data.guild) - : undefined; - this.approximateMemberCount = data.approximate_member_count - ? data.approximate_member_count - : undefined; - this.approximatePresenceCount = data.approximate_presence_count - ? data.approximate_presence_count - : undefined; - this.code = data.code; - this.expiresAt = data.expires_at - ? Date.parse(data.expires_at) - : undefined; - this.inviter = data.inviter - ? new User(session, data.inviter) - : undefined; - this.targetUser = data.target_user - ? new User(session, data.target_user) - : undefined; - this.targetApplication = data.target_application - ? new Application( - session, - data.target_application as DiscordApplication - ) - : undefined; - this.targetType = data.target_type; - - if (data.channel) { - const guildId = data.guild?.id ? data.guild.id : ''; - this.channel = new GuildChannel( - session, - data.channel as DiscordChannel, - guildId - ); - } - - if (data.guild_scheduled_event) { - this.guildScheduledEvent = { - id: data.guild_scheduled_event.id, - guildId: data.guild_scheduled_event.guild_id, - channelId: data.guild_scheduled_event.channel_id - ? data.guild_scheduled_event.channel_id - : undefined, - creatorId: data.guild_scheduled_event.creator_id - ? data.guild_scheduled_event.creator_id - : undefined, - name: data.guild_scheduled_event.name, - description: data.guild_scheduled_event.description - ? data.guild_scheduled_event.description - : undefined, - scheduledStartTime: - data.guild_scheduled_event.scheduled_start_time, - scheduledEndTime: data.guild_scheduled_event.scheduled_end_time - ? data.guild_scheduled_event.scheduled_end_time - : undefined, - privacyLevel: data.guild_scheduled_event.privacy_level, - status: data.guild_scheduled_event.status, - entityType: data.guild_scheduled_event.entity_type, - entityId: data.guild ? data.guild.id : undefined, - entityMetadata: data.guild_scheduled_event.entity_metadata - ? data.guild_scheduled_event.entity_metadata - : undefined, - creator: data.guild_scheduled_event.creator - ? new User(session, data.guild_scheduled_event.creator) - : undefined, - userCount: data.guild_scheduled_event.user_count - ? data.guild_scheduled_event.user_count - : undefined, - image: data.guild_scheduled_event.image - ? data.guild_scheduled_event.image - : undefined, - }; - } - - if (data.stage_instance) { - const guildId = data.guild?.id ? data.guild.id : ''; - this.stageInstance = { - members: data.stage_instance.members.map( - m => - new Member(session, m as DiscordMemberWithUser, guildId) - ), - participantCount: data.stage_instance.participant_count, - speakerCount: data.stage_instance.speaker_count, - topic: data.stage_instance.topic, - }; - } - } - - readonly session: Session; - guild?: InviteGuild; - approximateMemberCount?: number; - approximatePresenceCount?: number; - code: string; - expiresAt?: number; - inviter?: User; - targetUser?: User; - targetType?: TargetTypes; - channel?: Partial; - stageInstance?: InviteStageInstance; - guildScheduledEvent?: InviteScheduledEvent; - targetApplication?: Partial; - - async delete(): Promise { - await Guild.prototype.deleteInvite.call(this.guild, this.code); - return this; - } -} diff --git a/packages/core/src/structures/members.ts b/packages/core/src/structures/members.ts deleted file mode 100644 index 9a2d161..0000000 --- a/packages/core/src/structures/members.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { - DiscordMemberWithUser, - DiscordThreadMember, -} from '@biscuitland/api-types'; -import type { CreateGuildBan, ModifyGuildMember } from './guilds'; -import type { AvatarOptions } from './user'; -import { User } from './user'; -import { Guild } from './guilds'; -import { Util } from '../utils/util'; -import { USER_AVATAR, THREAD_USER } from '@biscuitland/api-types'; -import { Permissions } from './special/permissions'; - -/** - * Represents a guild member - * @link https://discord.com/developers/docs/resources/guild#guild-member-object - */ -export class Member implements Model { - constructor( - session: Session, - data: DiscordMemberWithUser, - guildId: Snowflake - ) { - this.session = session; - this.user = new User(session, data.user); - this.guildId = guildId; - this.avatarHash = data.avatar - ? data.avatar - : undefined; - - this.nickname = data.nick ? data.nick : undefined; - this.premiumSince = data.premium_since - ? Date.parse(data.premium_since) - : undefined; - - this.channelPermissions = data.permissions ? new Permissions(BigInt(data.permissions)) : undefined; - this.joinedTimestamp = Date.parse(data.joined_at); - this.roles = data.roles; - this.deaf = !!data.deaf; - this.mute = !!data.mute; - this.pending = !!data.pending; - this.communicationDisabledUntilTimestamp = - data.communication_disabled_until - ? Date.parse(data.communication_disabled_until) - : undefined; - } - - /** the session that instantiated this member */ - readonly session: Session; - - /** the user this guild member represents */ - user: User; - - /** the choosen guild id */ - guildId: Snowflake; - - /** the member's guild avatar hash optimized as a bigint */ - avatarHash?: string; - - /** this user's guild nickname */ - nickname?: string; - - /** when the user started boosting the guild */ - premiumSince?: number; - - /** total permissions of the member in the channel, including overwrites, returned when in the interaction object */ - channelPermissions?: Permissions; - - /** when the user joined the guild */ - joinedTimestamp: number; - - /** array of role object ids */ - roles: Snowflake[]; - - /** whether the user is deafened in voice channels */ - deaf: boolean; - - /** whether the user is muted in voice channels */ - mute: boolean; - - /** whether the user has not yet passed the guild's Membership Screening requirements */ - pending: boolean; - - /** when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */ - communicationDisabledUntilTimestamp?: number; - - /** shorthand to User.id */ - get id(): Snowflake { - return this.user.id; - } - - /** gets the nickname or the username */ - get nicknameOrUsername(): string { - return this.nickname ?? this.user.username; - } - - /** gets the joinedAt timestamp as a Date */ - get joinedAt(): Date { - return new Date(this.joinedTimestamp); - } - - /** bans a member from this guild and delete previous messages sent by the member */ - async ban(options: CreateGuildBan): Promise { - await Guild.prototype.banMember.call( - { id: this.guildId, session: this.session }, - this.user.id, - options - ); - - return this; - } - - /** kicks a member from this guild */ - async kick(options: { reason?: string }): Promise { - await Guild.prototype.kickMember.call( - { id: this.guildId, session: this.session }, - this.user.id, - options.reason - ); - - return this; - } - - /** unbans a member from this guild */ - async unban(): Promise { - await Guild.prototype.unbanMember.call( - { id: this.guildId, session: this.session }, - this.user.id - ); - } - - /** edits member's nickname, roles, etc */ - async edit(options: ModifyGuildMember): Promise { - const member = await Guild.prototype.editMember.call( - { id: this.guildId, session: this.session }, - this.user.id, - options - ); - - return member; - } - - /** calls {@link Member#edit} which calls {@link Guild#editMember} under the hood */ - async timeout(time: number | null) { - await this.edit({ communicationDisabledUntil: time }); - } - - /** adds a role to this member */ - async addRole(roleId: Snowflake, reason?: string): Promise { - await Guild.prototype.addRole.call( - { id: this.guildId, session: this.session }, - this.user.id, - roleId, - reason - ); - } - - /** removes a role from this member */ - async removeRole( - roleId: Snowflake, - options: { reason?: string } = {} - ): Promise { - await Guild.prototype.removeRole.call( - { id: this.guildId, session: this.session }, - this.user.id, - roleId, - options.reason - ); - } - - async fetch(): Promise { - const member = await Guild.prototype.fetchMember.call({ session: this.session, id: this.guildId }, this.id); - - return member; - } - - /** gets the members's guild avatar if the user has one, gets the user's avatar instead */ - avatarURL(options: AvatarOptions): string { - if (!this.avatarHash) { - return this.user.avatarURL(options); - } - - return Util.formatImageURL(USER_AVATAR( - this.user.id, - this.avatarHash - ), options.size ?? 128, options.format); - } - - /** - * Sets a new nickname for this member. Same as Member.edit({ nick: ... }) - * @param nick - The new nickname for the member. - */ - async setNickname(nick: string): Promise { - return this.edit({ nick }); - } - - toString(): string { - return `<@!${this.user.id}>`; - } -} - -/** - * A member that comes from a thread - * @link https://discord.com/developers/docs/resources/channel#thread-member-object - * **/ -export class ThreadMember implements Model { - constructor(session: Session, data: DiscordThreadMember) { - this.session = session; - this.id = data.id; - this.flags = data.flags; - this.timestamp = Date.parse(data.join_timestamp); - } - - readonly session: Session; - readonly id: Snowflake; - flags: number; - timestamp: number; - - get threadId(): Snowflake { - return this.id; - } - - async quitThread(memberId?: Snowflake): Promise { - await this.session.rest.delete( - THREAD_USER(this.id, memberId ?? this.session.botId), - {} - ); - } - - async fetchMember(memberId?: Snowflake): Promise { - const member = await this.session.rest.get( - THREAD_USER(this.id, memberId ?? this.session.botId) - ); - - return new ThreadMember(this.session, member); - } -} diff --git a/packages/core/src/structures/message-reaction.ts b/packages/core/src/structures/message-reaction.ts deleted file mode 100644 index de3e87a..0000000 --- a/packages/core/src/structures/message-reaction.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Session } from '../biscuit'; -import type { - DiscordMemberWithUser, - DiscordMessageReactionAdd, - DiscordReaction, -} from '@biscuitland/api-types'; -import { Emoji } from './emojis'; -import { Member } from './members'; - -/** - * Represents when a new reaction was added to a message. - * @link https://discord.com/developers/docs/topics/gateway#message-reaction-add - */ -export interface MessageReactionAdd { - userId: string; - channelId: string; - messageId: string; - guildId?: string; - member?: Member; - emoji: Partial; -} - -/** - * Represents when a reaction was removed from a message. - * Equal to MessageReactionAdd but without 'member' property. - * @see {@link MessageReactionAdd} - * @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-message-reaction-remove-event-fields - */ -export type MessageReactionRemove = Omit; - -/** - * Represents when all reactions were removed from a message. - * Equals to MessageReactionAdd but with 'channelId', 'messageId' and 'guildId' properties guaranteed. - * @see {@link MessageReactionAdd} - * @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all - */ -export type MessageReactionRemoveAll = Pick< - MessageReactionAdd, - 'channelId' | 'messageId' | 'guildId' ->; - -/** - * Represents when a reaction-emoji was removed from a message. - * Equals to MessageReactionAdd but with 'channelId', 'messageId', 'emoji' and 'guildId' properties guaranteed. - * @see {@link MessageReactionRemove} - * @see {@link Emoji} - * @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji - */ -export type MessageReactionRemoveEmoji = Pick< - MessageReactionAdd, - 'channelId' | 'guildId' | 'messageId' | 'emoji' ->; - -/** - * Creates a new MessageReactionAdd object. - * @param session - Current application session. - * @param data - Discord message reaction to parse. - */ -export function NewMessageReactionAdd( - session: Session, - data: DiscordMessageReactionAdd -): MessageReactionAdd { - return { - userId: data.user_id, - channelId: data.channel_id, - messageId: data.message_id, - guildId: data.guild_id, - member: data.member - ? new Member( - session, - data.member as DiscordMemberWithUser, - data.guild_id ?? '' - ) - : undefined, - emoji: new Emoji(session, data.emoji), - }; -} - -/** - * Represents a reaction - * @link https://discord.com/developers/docs/resources/channel#reaction-object - */ -export class MessageReaction { - constructor(session: Session, data: DiscordReaction) { - this.session = session; - this.me = data.me; - this.count = data.count; - this.emoji = new Emoji(session, data.emoji); - } - - readonly session: Session; - me: boolean; - count: number; - emoji: Emoji; -} - -export default MessageReaction; diff --git a/packages/core/src/structures/message.ts b/packages/core/src/structures/message.ts deleted file mode 100644 index 75e363e..0000000 --- a/packages/core/src/structures/message.ts +++ /dev/null @@ -1,644 +0,0 @@ -/* eslint-disable no-mixed-spaces-and-tabs */ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { - AllowedMentionsTypes, - DiscordMessage, - DiscordMessageComponents, - DiscordUser, - FileContent, - MessageActivityTypes, - MessageTypes, - GetReactions, -} from '@biscuitland/api-types'; -import type { Channel } from './channels'; -import type { Component } from './components'; -import type { MessageInteraction } from './interactions'; -import type { StickerItem } from './sticker'; -import type { Embed } from './embed'; -import { NewEmbed, NewEmbedR } from './embed'; -import { MessageFlags } from '../utils/util'; -import { Snowflake } from '../snowflakes'; -import { ChannelFactory, ThreadChannel } from './channels'; -import { User } from './user'; -import { Member } from './members'; -import { Attachment } from './attachment'; -import { ComponentFactory } from './components'; -import { MessageReaction } from './message-reaction'; -import { Application, NewTeam } from './application'; -import { InteractionFactory } from './interactions'; - -import { - CHANNEL_PIN, - CHANNEL_MESSAGE, - CHANNEL_MESSAGES, - CHANNEL_MESSAGE_REACTION_ME, - CHANNEL_MESSAGE_REACTION_USER, - CHANNEL_MESSAGE_REACTION, - CHANNEL_MESSAGE_REACTIONS, - CHANNEL_MESSAGE_CROSSPOST, -} from '@biscuitland/api-types'; - -export interface GuildMessage extends Message { - guildId: Snowflake; -} - -export type WebhookMessage = Message & { - author: Partial; - webhook: WebhookAuthor; - member: undefined; -}; - -export interface MessageActivity { - partyId?: Snowflake; - type: MessageActivityTypes; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#allowed-mentions-object - */ -export interface AllowedMentions { - parse?: AllowedMentionsTypes[]; - repliedUser?: boolean; - roles?: Snowflake[]; - users?: Snowflake[]; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#message-reference-object-message-reference-structure - * channelId is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. - */ -export interface CreateMessageReference { - messageId: Snowflake; - channelId?: Snowflake; - guildId?: Snowflake; - failIfNotExists?: boolean; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#create-message-json-params - * Posts a message to a guild text or DM channel. Returns a message object. Fires a Message Create Gateway event. - */ -export interface CreateMessage { - embeds?: Embed[]; - content?: string; - allowedMentions?: AllowedMentions; - files?: FileContent[]; - messageReference?: CreateMessageReference; - tts?: boolean; - components?: DiscordMessageComponents; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#edit-message-json-params - * Edit a previously sent message. - * Returns a {@link Message} object. Fires a Message Update Gateway event. - */ -export interface EditMessage extends Partial { - flags?: MessageFlags; - attachments?: Attachment[]; -} - -/** - * Represents a guild or unicode {@link Emoji} - */ -export type EmojiResolvable = - | string - | { - name: string; - id: Snowflake; - }; - -/** - * A partial {@link User} to represent the author of a message sent by a webhook - */ -export interface WebhookAuthor { - id: string; - username: string; - discriminator: string; - avatar?: string; -} - -/** - * @link https://discord.com/developers/docs/resources/channel#message-object - * Represents a message - */ -export class Message implements Model { - constructor(session: Session, data: DiscordMessage) { - this.session = session; - this.id = data.id; - - this.type = data.type; - this.channelId = data.channel_id; - this.guildId = data.guild_id; - this.applicationId = data.application_id; - - this.mentions = { - users: data.mentions?.map(user => new User(session, user)) ?? [], - roleIds: data.mention_roles ?? [], - channels: - data.mention_channels?.map(channel => - ChannelFactory.from(session, channel) - ) ?? [], - }; - - if (!data.webhook_id) { - this.author = new User(session, data.author); - } - - this.flags = data.flags; - this.pinned = !!data.pinned; - this.tts = !!data.tts; - this.content = data.content!; - this.nonce = data.nonce; - this.mentionEveryone = data.mention_everyone; - - this.timestamp = Date.parse(data.timestamp); - this.editedTimestamp = data.edited_timestamp - ? Date.parse(data.edited_timestamp) - : undefined; - - this.reactions = - data.reactions?.map(react => new MessageReaction(session, react)) ?? - []; - this.attachments = data.attachments.map( - attachment => new Attachment(session, attachment) - ); - this.embeds = data.embeds.map(NewEmbedR); - - if (data.position) { this.position = data.position; } - - if (data.interaction) { - this.interaction = InteractionFactory.fromMessage( - session, - data.interaction, - data.guild_id - ); - } - - if (data.thread && data.guild_id) { - this.thread = new ThreadChannel( - session, - data.thread, - data.guild_id - ); - } - - // webhook handling - if (data.webhook_id && data.author.discriminator === '0000') { - this.webhook = { - id: data.webhook_id!, - username: data.author.username, - discriminator: data.author.discriminator, - avatar: data.author.avatar ? data.author.avatar : undefined, - }; - } - - // user is always null on MessageCreate and its replaced with author - if (data.guild_id && data.member && !this.isWebhookMessage()) { - this.member = new Member( - session, - { ...data.member, user: data.author }, - data.guild_id - ); - } - - this.components = - data.components?.map(component => - ComponentFactory.from(session, component) - ) ?? []; - - if (data.activity) { - this.activity = { - partyId: data.activity.party_id, - type: data.activity.type, - }; - } - - if (data.sticker_items) { - this.stickers = data.sticker_items.map(si => { - return { - id: si.id, - name: si.name, - formatType: si.format_type, - }; - }); - } - - if (data.application) { - const application: Partial = { - id: data.application.id, - icon: data.application.icon ? data.application.icon : undefined, - name: data.application.name, - guildId: data.application.guild_id, - flags: data.application.flags, - botPublic: data.application.bot_public, - owner: data.application.owner - ? new User(session, data.application.owner as DiscordUser) - : undefined, - botRequireCodeGrant: data.application.bot_require_code_grant, - coverImage: data.application.cover_image, - customInstallURL: data.application.custom_install_url, - description: data.application.description, - installParams: data.application.install_params, - tags: data.application.tags, - verifyKey: data.application.verify_key, - team: data.application.team - ? NewTeam(session, data.application.team) - : undefined, - primarySkuId: data.application.primary_sku_id, - privacyPolicyURL: data.application.privacy_policy_url, - rpcOrigins: data.application.rpc_origins, - slug: data.application.slug, - }; - - Object.setPrototypeOf(application, Application.prototype); - - this.application = application; - } - } - - /** Reference to the client that instantiated this Message */ - readonly session: Session; - - /** id of the message */ - readonly id: Snowflake; - - /** type of message */ - type: MessageTypes; - - /** id of the channel the message was sent in */ - channelId: Snowflake; - - /** id of the guild the message was sent in, this should exist on MESSAGE_CREATE and MESSAGE_UPDATE events */ - guildId?: Snowflake; - - /** if the message is an Interaction or application-owned webhook, this is the id of the application */ - applicationId?: Snowflake; - - /** mentions if any */ - mentions: { - /** users specifically mentioned in the message */ - users: User[]; - - /** roles specifically mentioned in this message */ - roleIds: Snowflake[]; - - /** channels specifically mentioned in the message */ - channels: Channel[]; - }; - - /** sent if the message is a response to an Interaction */ - interaction?: MessageInteraction; - - /** the author of this message, this field is **not** sent on webhook messages */ - author!: User; - - /** message flags combined as a bitfield */ - flags?: MessageFlags; - - /** whether this message is pinned */ - pinned: boolean; - - /** whether this was a TTS message */ - tts: boolean; - - /** contents of the message */ - content: string; - - /** used for validating a message was sent */ - nonce?: string | number; - - /** whether this message mentions everyone */ - mentionEveryone: boolean; - - /** when this message was sent */ - timestamp: number; - - /** when this message was edited */ - editedTimestamp?: number; - - /** - * sent if the message contains stickers - * **this contains sticker items not stickers** - */ - stickers?: StickerItem[]; - - /** reactions to the message */ - reactions: MessageReaction[]; - - /** any attached files */ - attachments: Attachment[]; - - /** any embedded content */ - embeds: Embed[]; - - /** member properties for this message's author */ - member?: Member; - - /** the thread that was started from this message, includes {@link ThreadMember} */ - thread?: ThreadChannel; - - /** sent if the message contains components like buttons, action rows, or other interactive components */ - components: Component[]; - - /** if the message is generated by a webhook, this is the webhook's author data */ - webhook?: WebhookAuthor; - - /** sent with Rich Presence-related chat embeds */ - application?: Partial; - - /** sent with Rich Presence-related chat embeds */ - activity?: MessageActivity; - - /** Represents the approximate position of the message in a thread */ - position?: number; - - /** gets the timestamp of this message, this does not requires the timestamp field */ - get createdTimestamp(): number { - return Snowflake.snowflakeToTimestamp(this.id); - } - - /** gets the timestamp of this message as a Date */ - get createdAt(): Date { - return new Date(this.createdTimestamp); - } - - /** gets the timestamp of this message (sent by the API) */ - get sentAt(): Date { - return new Date(this.timestamp); - } - - /** gets the edited timestamp as a Date */ - get editedAt(): Date | undefined { - return this.editedTimestamp - ? new Date(this.editedTimestamp) - : undefined; - } - - /** whether this message was edited */ - get edited(): number | undefined { - return this.editedTimestamp; - } - - /** gets the url of the message that points to the message */ - get url(): string { - return `https://discord.com/channels/${this.guildId ?? '@me'}/${ - this.channelId - }/${this.id}`; - } - - /** - * Whether the author is bot. - * same as Message.author.bot - */ - get isBot(): boolean { - return this.author.bot; - } - - /** - * Pins this message - */ - async pin(): Promise { - await this.session.rest.put( - CHANNEL_PIN(this.channelId, this.id), - {} - ); - } - - /** - * Unpins this message - */ - async unpin(): Promise { - await this.session.rest.delete( - CHANNEL_PIN(this.channelId, this.id), - {} - ); - } - - /** Edits the current message */ - async edit(options: EditMessage): Promise { - const message = await this.session.rest.patch( - CHANNEL_MESSAGE(this.channelId, this.id), - { - content: options.content, - allowed_mentions: { - parse: options.allowedMentions?.parse, - roles: options.allowedMentions?.roles, - users: options.allowedMentions?.users, - replied_user: options.allowedMentions?.repliedUser, - }, - flags: options.flags, - embeds: options.embeds?.map(NewEmbed), - components: options.components, - files: options.files, - attachments: options.attachments - } - ); - - return new Message(this.session, message); - } - - /** edits the current message flags to supress its embeds */ - async suppressEmbeds(suppress: true): Promise; - async suppressEmbeds(suppress: false): Promise; - async suppressEmbeds(suppress = true) { - if (this.flags === MessageFlags.SupressEmbeds && suppress === false) { - return; - } - - const message = await this.edit({ flags: MessageFlags.SupressEmbeds }); - - return message; - } - - /** deletes this message */ - async delete(): Promise { - await this.session.rest.delete( - CHANNEL_MESSAGE(this.channelId, this.id) - ); - - return this; - } - - /** Replies directly in the channel where the message was sent */ - async reply(options: CreateMessage | string | Embed[]): Promise { - - // Options is plain content - if (typeof options === 'string') { - const message = await this.session.rest.post( - CHANNEL_MESSAGES(this.channelId), - { content: options } - ); - - return new Message(this.session, message); - } - - // Opptions are multiple embeds - if (Array.isArray(options)) { - const message = await this.session.rest.post( - CHANNEL_MESSAGES(this.channelId), - { embeds: options.map(NewEmbed) } - ); - - return new Message(this.session, message); - } - - // Options is of type CreateMessage - const message = await this.session.rest.post( - CHANNEL_MESSAGES(this.channelId), - { - content: options.content, - file: options.files, - allowed_mentions: { - parse: options.allowedMentions?.parse, - roles: options.allowedMentions?.roles, - users: options.allowedMentions?.users, - replied_user: options.allowedMentions?.repliedUser, - }, - message_reference: options.messageReference - ? { - message_id: options.messageReference.messageId, - channel_id: options.messageReference.channelId, - guild_id: options.messageReference.guildId, - fail_if_not_exists: - options.messageReference.failIfNotExists ?? - true, - } - : undefined, - embeds: options.embeds?.map(NewEmbed), - tts: options.tts, - components: options.components, - } - ); - - return new Message(this.session, message); - } - - /** alias for Message.addReaction */ - get react() { - return this.addReaction; - } - - /** adds a Reaction */ - async addReaction(reaction: EmojiResolvable): Promise { - const r = - typeof reaction === 'string' - ? reaction - : `${reaction.name}:${reaction.id}`; - - await this.session.rest.put( - CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r), - {} - ); - } - - /** removes a reaction from someone */ - async removeReaction( - reaction: EmojiResolvable, - options?: { userId: Snowflake } - ): Promise { - const r = - typeof reaction === 'string' - ? reaction - : `${reaction.name}:${reaction.id}`; - - await this.session.rest.delete( - options?.userId - ? CHANNEL_MESSAGE_REACTION_USER( - this.channelId, - this.id, - r, - options.userId - ) - : CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r), - {} - ); - } - - /** - * Get users who reacted with this emoji - * not recommended since the cache handles reactions already - */ - async fetchReactions( - reaction: EmojiResolvable, - options?: GetReactions - ): Promise { - const r = - typeof reaction === 'string' - ? reaction - : `${reaction.name}:${reaction.id}`; - - const users = await this.session.rest.get( - CHANNEL_MESSAGE_REACTION( - this.channelId, - this.id, - encodeURIComponent(r), - options - ) - ); - - return users.map(user => new User(this.session, user)); - } - - /** - * same as Message.removeReaction but removes using a unicode emoji - */ - async removeReactionEmoji(reaction: EmojiResolvable): Promise { - const r = - typeof reaction === 'string' - ? reaction - : `${reaction.name}:${reaction.id}`; - - await this.session.rest.delete( - CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r), - {} - ); - } - - /** nukes every reaction on the message */ - async nukeReactions(): Promise { - await this.session.rest.delete( - CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id), - {} - ); - } - - /** publishes/crossposts a message to all followers */ - async crosspost(): Promise { - const message = await this.session.rest.post( - CHANNEL_MESSAGE_CROSSPOST(this.channelId, this.id), - {} - ); - - return new Message(this.session, message); - } - - /** fetches this message, meant to be used with Function.call since its redundant */ - async fetch(): Promise { - const message = await this.session.rest.get( - CHANNEL_MESSAGE(this.channelId, this.id) - ); - - if (!message?.id) { - return; - } - - return new Message(this.session, message); - } - - /** alias of Message.crosspost */ - get publish() { - return this.crosspost; - } - - /** wheter the message comes from a guild **/ - inGuild(): this is GuildMessage { - return !!this.guildId; - } - - /** wheter the messages comes from a Webhook */ - isWebhookMessage(): this is WebhookMessage { - return !!this.webhook; - } -} diff --git a/packages/core/src/structures/presence.ts b/packages/core/src/structures/presence.ts deleted file mode 100644 index 2793644..0000000 --- a/packages/core/src/structures/presence.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { - ActivityTypes, - DiscordActivityButton, - DiscordActivitySecrets, - DiscordClientStatus, - DiscordPresenceUpdate, -} from '@biscuitland/api-types'; -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { ComponentEmoji } from '../utils/util'; -import { User } from './user'; - -export interface ActivityAssets { - largeImage?: string; - largeText?: string; - smallImage?: string; - smallText?: string; -} - -export interface Activities { - name: string; - type: ActivityTypes; - url?: string; - createdAt: number; - timestamps?: { - start?: number; - end?: number; - }; - applicationId?: Snowflake; - details?: string; - state?: string; - emoji?: ComponentEmoji; - party?: { - id?: string; - size?: number[]; - }; - assets?: ActivityAssets; - secrets?: DiscordActivitySecrets; - instance?: boolean; - flags?: number; - buttons?: DiscordActivityButton; -} - -export enum StatusTypes { - online = 0, - dnd = 1, - idle = 2, - invisible = 3, - offline = 4, -} - -export class Presence { - constructor(session: Session, data: DiscordPresenceUpdate) { - this.session = session; - this.user = new User(this.session, data.user); - this.guildId = data.guild_id; - this.status = StatusTypes[data.status]; - this.activities = data.activities.map(activity => - Object({ - name: activity.name, - type: activity.type, - url: activity.url ? activity.url : undefined, - createdAt: activity.created_at, - timestamps: activity.timestamps, - applicationId: activity.application_id, - details: activity.details ? activity.details : undefined, - state: activity.state, - emoji: activity.emoji ? activity.emoji : undefined, - party: activity.party ? activity.party : undefined, - assets: activity.assets - ? { - largeImage: activity.assets.large_image, - largeText: activity.assets.large_text, - smallImage: activity.assets.small_image, - smallText: activity.assets.small_text, - } - : null, - secrets: activity.secrets ? activity.secrets : undefined, - instance: !!activity.instance, - flags: activity.flags, - buttons: activity.buttons, - }) - ); - this.clientStatus = data.client_status; - } - - session: Session; - user: User; - guildId: Snowflake; - status: StatusTypes; - activities: Activities[]; - clientStatus: DiscordClientStatus; -} diff --git a/packages/core/src/structures/role.ts b/packages/core/src/structures/role.ts deleted file mode 100644 index e743260..0000000 --- a/packages/core/src/structures/role.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { DiscordRole } from '@biscuitland/api-types'; -import type { ModifyGuildRole } from './guilds'; -import { Snowflake } from '../snowflakes'; -import { Guild } from './guilds'; -import { Util } from '../utils/util'; -import { Permissions } from './special/permissions'; - -export class Role implements Model { - constructor(session: Session, data: DiscordRole, guildId: Snowflake) { - this.session = session; - this.id = data.id; - this.guildId = guildId; - this.hoist = data.hoist; - this.iconHash = data.icon - ? Util.iconHashToBigInt(data.icon) - : undefined; - this.color = data.color; - this.name = data.name; - this.unicodeEmoji = data.unicode_emoji; - this.mentionable = data.mentionable; - this.managed = data.managed; - this.permissions = new Permissions(BigInt(data.permissions)); - } - - readonly session: Session; - readonly id: Snowflake; - - readonly guildId: Snowflake; - - hoist: boolean; - iconHash?: bigint; - color: number; - name: string; - unicodeEmoji?: string; - mentionable: boolean; - managed: boolean; - permissions: Permissions; - - get createdTimestamp(): number { - return Snowflake.snowflakeToTimestamp(this.id); - } - - get createdAt(): Date { - return new Date(this.createdTimestamp); - } - - get hexColor(): string { - return `#${this.color.toString(16).padStart(6, '0')}`; - } - - async delete(): Promise { - await Guild.prototype.deleteRole.call( - { id: this.guildId, session: this.session }, - this.id - ); - } - - async edit(options: ModifyGuildRole): Promise { - const role = await Guild.prototype.editRole.call( - { id: this.guildId, session: this.session }, - this.id, - options - ); - return role; - } - - async add(memberId: Snowflake, reason?: string): Promise { - await Guild.prototype.addRole.call( - { id: this.guildId, session: this.session }, - memberId, - this.id, - reason - ); - } - - async remove(memberId: Snowflake, reason?: string): Promise { - await Guild.prototype.removeRole.call( - { id: this.guildId, session: this.session }, - memberId, - this.id, - reason - ); - } - - toString(): string { - switch (this.id) { - case this.guildId: - return '@everyone'; - default: - return `<@&${this.id}>`; - } - } -} diff --git a/packages/core/src/structures/scheduled-events.ts b/packages/core/src/structures/scheduled-events.ts deleted file mode 100644 index 4dbd33c..0000000 --- a/packages/core/src/structures/scheduled-events.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { - DiscordScheduledEvent, - DiscordScheduledEventEntityMetadata, - ScheduledEventEntityType, - ScheduledEventStatus, -} from '@biscuitland/api-types'; -import { PrivacyLevels } from './stage-instance'; -import { User } from './user'; - -export class ScheduledEvent implements Model { - constructor(session: Session, data: DiscordScheduledEvent) { - this.session = session; - this.id = data.id; - this.guildId = data.guild_id; - this.channelId = data.channel_id; - this.creatorId = data.creator_id ? data.creator_id : undefined; - this.name = data.name; - this.description = data.description; - this.scheduledStartTime = data.scheduled_start_time; - this.scheduledEndTime = data.scheduled_end_time; - this.privacyLevel = PrivacyLevels.GuildOnly; - this.status = data.status; - this.entityType = data.entity_type; - this.entityMetadata = data.entity_metadata - ? data.entity_metadata - : undefined; - this.creator = data.creator - ? new User(session, data.creator) - : undefined; - this.userCount = data.user_count; - this.image = data.image ? data.image : undefined; - } - - session: Session; - id: Snowflake; - guildId: Snowflake; - channelId: Snowflake | null; - creatorId?: Snowflake; - name: string; - description?: string; - scheduledStartTime: string; - scheduledEndTime: string | null; - privacyLevel: PrivacyLevels; - status: ScheduledEventStatus; - entityType: ScheduledEventEntityType; - entityMetadata?: DiscordScheduledEventEntityMetadata; - creator?: User; - userCount?: number; - image?: string; -} diff --git a/packages/core/src/structures/special/interaction-options.ts b/packages/core/src/structures/special/interaction-options.ts deleted file mode 100644 index 2708ca4..0000000 --- a/packages/core/src/structures/special/interaction-options.ts +++ /dev/null @@ -1,267 +0,0 @@ -import type { - DiscordInteractionDataOption, - DiscordInteractionDataResolved, - Snowflake, -} from '@biscuitland/api-types'; -import { ApplicationCommandOptionTypes } from '@biscuitland/api-types'; - -/** - * Utility class to get the resolved options for a command - * @example const option = ctx.options.getStringOption("name"); - */ -export class InteractionOptions { - private _subcommand?: string; - private _group?: string; - - hoistedOptions: DiscordInteractionDataOption[]; - resolved?: DiscordInteractionDataResolved; - - constructor( - options?: DiscordInteractionDataOption[], - resolved?: DiscordInteractionDataResolved - ) { - this.hoistedOptions = options ?? []; - - // warning: black magic do not edit and thank djs authors - - if ( - this.hoistedOptions[0]?.type === - ApplicationCommandOptionTypes.SubCommandGroup - ) { - this._group = this.hoistedOptions[0].name; - this.hoistedOptions = this.hoistedOptions[0].options ?? []; - } - - if ( - this.hoistedOptions[0]?.type === - ApplicationCommandOptionTypes.SubCommand - ) { - this._subcommand = this.hoistedOptions[0].name; - this.hoistedOptions = this.hoistedOptions[0].options ?? []; - } - - this.resolved = resolved; - } - - private getTypedOption( - name: string | number, - type: ApplicationCommandOptionTypes, - properties: (keyof DiscordInteractionDataOption)[], - required: boolean - ): DiscordInteractionDataOption | void { - const option: DiscordInteractionDataOption | undefined = this.get( - name, - required - ); - - if (!option) { - return; - } - - if (option.type !== type) { - // pass - } - - if ( - required === true && - properties.every(prop => typeof option[prop] === 'undefined') - ) { - throw new TypeError( - `Properties ${properties.join( - ', ' - )} are missing in option ${name}` - ); - } - - return option; - } - - get(name: string | number, required: true): DiscordInteractionDataOption; - get( - name: string | number, - required: boolean - ): DiscordInteractionDataOption | undefined; - - get(name: string | number, required?: boolean) { - const option: DiscordInteractionDataOption | undefined = - this.hoistedOptions.find(o => - typeof name === 'number' - ? o.name === name.toString() - : o.name === name - ); - - if (!option) { - if (required && name in this.hoistedOptions.map(o => o.name)) { - throw new TypeError('Option marked as required was undefined'); - } - - return; - } - - return option; - } - - /** searches for a string option */ - getString(name: string | number, required: true): string; - getString(name: string | number, required?: boolean): string | undefined; - getString(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.String, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a number option */ - getNumber(name: string | number, required: true): number; - getNumber(name: string | number, required?: boolean): number | undefined; - getNumber(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Number, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searhces for an integer option */ - getInteger(name: string | number, required: true): number; - getInteger(name: string | number, required?: boolean): number | undefined; - getInteger(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Integer, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a boolean option */ - getBoolean(name: string | number, required: true): boolean; - getBoolean(name: string | number, required?: boolean): boolean | undefined; - getBoolean(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Boolean, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a user option */ - getUser(name: string | number, required: true): Snowflake; - getUser(name: string | number, required?: boolean): Snowflake | undefined; - getUser(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.User, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a channel option */ - getChannel(name: string | number, required: true): Snowflake; - getChannel(name: string | number, required?: boolean): Snowflake | undefined; - getChannel(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Channel, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a mentionable-based option */ - getMentionable(name: string | number, required: true): string; - getMentionable( - name: string | number, - required?: boolean - ): string | undefined; - - getMentionable(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Mentionable, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for a mentionable-based option */ - getRole(name: string | number, required: true): Snowflake; - getRole(name: string | number, required?: boolean): Snowflake | undefined; - getRole(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Role, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for an attachment option */ - getAttachment(name: string | number, required: true): Snowflake; - getAttachment(name: string | number, required?: boolean): Snowflake | undefined; - getAttachment(name: string | number, required = false) { - const option: DiscordInteractionDataOption | void = this.getTypedOption( - name, - ApplicationCommandOptionTypes.Attachment, - ['value'], - required - ); - - return option?.value ?? undefined; - } - - /** searches for the focused option */ - getFocused(full: true): DiscordInteractionDataOption; - getFocused(full: false): DiscordInteractionDataOption['value']; - getFocused(full = false) { - const focusedOption: DiscordInteractionDataOption | void = - this.hoistedOptions.find(option => option.focused); - - if (!focusedOption) { - throw new TypeError('No option found'); - } - - return full ? focusedOption : focusedOption.value; - } - - getSubCommand( - required = true - ): [string | undefined, DiscordInteractionDataOption[]] { - if (required && !this._subcommand) { - throw new TypeError('Option marked as required was undefined'); - } - - return [this._subcommand, this.hoistedOptions]; - } - - getSubCommandGroup( - required = false - ): [string | undefined, DiscordInteractionDataOption[]] { - if (required && !this._group) { - throw new TypeError('Option marked as required was undefined'); - } - - return [this._group, this.hoistedOptions]; - } -} diff --git a/packages/core/src/structures/special/permissions.ts b/packages/core/src/structures/special/permissions.ts deleted file mode 100644 index 412bc95..0000000 --- a/packages/core/src/structures/special/permissions.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { BitwisePermissionFlags } from '@biscuitland/api-types'; - -export interface BitField { - add(...bits: T[]): this; - remove(...bits: T[]): this; - has(bit: T): boolean; - any(bit: T): boolean; - equals(bit: T): boolean; -} - -export type PermissionString = keyof typeof BitwisePermissionFlags; -export type PermissionResolvable = - | bigint - | PermissionString - | PermissionString[] - | BitwisePermissionFlags - | BitwisePermissionFlags[]; - -export class Permissions implements BitField { - /** Stores a reference to BitwisePermissionFlags */ - static Flags = BitwisePermissionFlags; - - /** Falsy; Stores the lack of permissions*/ - static None = 0n; - - /** Stores all entity permissions */ - bitfield: bigint; - - /** - * Wheter to grant all other permissions to the administrator - * **Not to get confused with Permissions#admin** - */ - __admin__ = true; - - constructor(bitfield: PermissionResolvable) { - this.bitfield = Permissions.resolve(bitfield); - } - - /** Wheter the bitfield has the administrator flag */ - get admin(): boolean { - return this.has(Permissions.Flags.ADMINISTRATOR); - } - - get array(): PermissionString[] { - // unsafe cast, do not edit - const permissions = Object.keys(Permissions.Flags) as PermissionString[]; - return permissions.filter(bit => this.has(bit)); - } - - add(...bits: PermissionResolvable[]): this { - let reduced = 0n; - for (const bit of bits) { - reduced |= Permissions.resolve(bit); - } - this.bitfield |= reduced; - return this; - } - - remove(...bits: PermissionResolvable[]): this { - let reduced = 0n; - for (const bit of bits) { - reduced |= Permissions.resolve(bit); - } - this.bitfield &= ~reduced; - return this; - } - - has(bit: PermissionResolvable): boolean { - const bbit = Permissions.resolve(bit); - - if (this.__admin__ && this.bitfield & BigInt(Permissions.Flags.ADMINISTRATOR)) { - return true; - } - - return (this.bitfield & bbit) === bbit; - } - - any(bit: PermissionResolvable): boolean { - const bbit = Permissions.resolve(bit); - - if (this.__admin__ && this.bitfield & BigInt(Permissions.Flags.ADMINISTRATOR)) { - return true; - } - - return (this.bitfield & bbit) !== Permissions.None; - } - - equals(bit: PermissionResolvable): boolean { - return !!(this.bitfield & Permissions.resolve(bit)); - } - - /** Gets all permissions */ - static get All(): bigint { - let reduced = 0n; - for (const key in BitwisePermissionFlags) { - const perm = BitwisePermissionFlags[key]; - - if (typeof perm === 'number') { - reduced += perm; - } - } - return reduced; - } - - static resolve(bit: PermissionResolvable): bigint { - switch (typeof bit) { - case 'bigint': - return bit; - case 'number': - return BigInt(bit); - case 'string': - return BigInt(Permissions.Flags[bit]); - case 'object': - return Permissions.resolve( - bit - .map(p => typeof p === 'string' ? BigInt(Permissions.Flags[p]) : BigInt(p)) - .reduce((acc, cur) => acc | cur, Permissions.None) - ); - default: - throw new TypeError(`Cannot resolve permission: ${bit}`); - } - } - - static sum(permissions: (bigint | number)[]) { - return permissions.reduce((y, x) => BigInt(y) | BigInt(x), Permissions.None); - } - - static reduce(permissions: PermissionResolvable[]): Permissions { - const solved = permissions.map(Permissions.resolve); - - return new Permissions(solved.reduce((y, x) => y | x, Permissions.None)); - } - - *[Symbol.iterator]() { - yield* this.array; - } - - valueOf() { - return this.bitfield; - } - - toJSON(): { fields: string[] } { - const fields = Object.keys(Permissions.Flags).filter(bit => typeof bit === 'number' && this.has(bit)); - - return { fields }; - } -} diff --git a/packages/core/src/structures/stage-instance.ts b/packages/core/src/structures/stage-instance.ts deleted file mode 100644 index 417111c..0000000 --- a/packages/core/src/structures/stage-instance.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { DiscordStageInstance as DiscordAutoClosingStageInstance } from '@biscuitland/api-types'; -import { STAGE_INSTANCE } from '@biscuitland/api-types'; - -export interface DiscordStageInstanceB extends DiscordAutoClosingStageInstance { - privacy_level: PrivacyLevels; - discoverable_disabled: boolean; - guild_scheduled_event_id: Snowflake; -} - -export enum PrivacyLevels { - Public = 1, - GuildOnly = 2, -} - -export type StageEditOptions = { - topic?: string; - privacy?: PrivacyLevels; -}; - -export class StageInstance implements Model { - constructor(session: Session, data: DiscordStageInstanceB) { - this.session = session; - this.id = data.id; - this.channelId = data.channel_id; - this.guildId = data.guild_id; - this.topic = data.topic; - this.privacyLevel = data.privacy_level; - this.discoverableDisabled = data.discoverable_disabled; - this.guildScheduledEventId = data.guild_scheduled_event_id; - } - - readonly session: Session; - readonly id: Snowflake; - - channelId: Snowflake; - guildId: Snowflake; - topic: string; - - // TODO: see if this works - privacyLevel: PrivacyLevels; - discoverableDisabled: boolean; - guildScheduledEventId: Snowflake; - - async edit(options: StageEditOptions): Promise { - const stageInstance = - await this.session.rest.patch( - STAGE_INSTANCE(this.id), - { - topic: options.topic, - privacy_level: options.privacy, - } - ); - - return new StageInstance(this.session, stageInstance); - } - - async delete(): Promise { - await this.session.rest.delete(STAGE_INSTANCE(this.id), {}); - } -} diff --git a/packages/core/src/structures/sticker.ts b/packages/core/src/structures/sticker.ts deleted file mode 100644 index 0996c8a..0000000 --- a/packages/core/src/structures/sticker.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { - DiscordSticker, - DiscordStickerPack, - StickerFormatTypes, - StickerTypes, -} from '@biscuitland/api-types'; -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import { User } from './user'; -import { STICKER_PACKS } from '@biscuitland/api-types'; - -export interface StickerItem { - id: Snowflake; - name: string; - formatType: StickerFormatTypes; -} - -export interface StickerPack { - id: Snowflake; - stickers: Sticker[]; - name: string; - skuId: Snowflake; - coverStickerId?: Snowflake; - description: string; - bannerAssetId?: Snowflake; -} - -export class Sticker implements Model { - constructor(session: Session, data: DiscordSticker) { - this.session = session; - this.id = data.id; - this.packId = data.pack_id; - this.name = data.name; - this.description = data.description; - this.tags = data.tags.split(','); - this.type = data.type; - this.formatType = data.format_type; - this.available = !!data.available; - this.guildId = data.guild_id; - this.user = data.user ? new User(this.session, data.user) : undefined; - this.sortValue = data.sort_value; - } - - session: Session; - id: Snowflake; - packId?: Snowflake; - name: string; - description?: string; - tags: string[]; - type: StickerTypes; - formatType: StickerFormatTypes; - available?: boolean; - guildId?: Snowflake; - user?: User; - sortValue?: number; - - async fetchPremiumPack(): Promise { - const data = await this.session.rest.get( - STICKER_PACKS() - ); - - return { - id: data.id, - stickers: data.stickers.map(st => new Sticker(this.session, st)), - name: data.name, - skuId: data.sku_id, - coverStickerId: data.cover_sticker_id, - description: data.description, - bannerAssetId: data.banner_asset_id, - }; - } -} diff --git a/packages/core/src/structures/user.ts b/packages/core/src/structures/user.ts deleted file mode 100644 index 2d526bf..0000000 --- a/packages/core/src/structures/user.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { Session } from '../biscuit'; -import type { DiscordUser, PremiumTypes, UserFlags } from '@biscuitland/api-types'; -import type { ImageFormat, ImageSize } from '../utils/util'; -import { USER, USER_AVATAR, USER_DEFAULT_AVATAR } from '@biscuitland/api-types'; -import { Util } from '../utils/util'; -import { DMChannel } from './channels'; - -export type AvatarOptions = { - format?: ImageFormat; - size?: ImageSize; -}; - -/** - * Represents a user - * @link https://discord.com/developers/docs/resources/user#user-object - */ -export class User implements Model { - constructor(session: Session, data: DiscordUser) { - this.session = session; - this.id = data.id; - - this.username = data.username; - this.discriminator = data.discriminator; - this.avatarHash = data.avatar - ? data.avatar - : undefined; - - this.accentColor = data.accent_color; - this.bot = !!data.bot; - this.system = !!data.system; - this.banner = data.banner - ? data.banner - : undefined; - - this.mfaEnabled = !!data.mfa_enabled; - this.locale = data.locale; - this.email = data.email ? data.email : undefined; - this.verified = !!data.verified; - this.flags = data.flags; - } - - /** the session that instantiated this User */ - readonly session: Session; - - /** the user's id */ - readonly id: Snowflake; - - /** the user's username, not unique across the platform */ - username: string; - - /** the user's 4-digit discord-tag */ - discriminator: string; - - /** the user's avatar hash */ - avatarHash?: string; - - /** the user's banner color encoded as an integer representation of hexadecimal color code */ - accentColor?: number; - - /** whether the user belongs to an OAuth2 application */ - bot: boolean; - - /** whether the user is an Official Discord System user (part of the urgent message system) */ - system: boolean; - - /** the user's banner hash */ - banner?: string; - - /** whether the user has two factor enabled on their account */ - mfaEnabled: boolean; - - /** the user's chosen language option */ - locale?: string; - - /** the user's email */ - email?: string; - - /** the flags on a user's account */ - flags?: UserFlags; - - /** whether the email on this account has been verified */ - verified: boolean; - - /** the type of Nitro subscription on a user's account */ - premiumType?: PremiumTypes; - - /** the public flags on a user's account */ - publicFlags?: UserFlags; - - /** gets the user's username#discriminator */ - get tag(): string { - return `${this.username}#${this.discriminator}`; - } - - /** fetches this user */ - async fetch(): Promise { - const user = await this.session.rest.get(USER(this.id)); - - return new User(this.session, user); - } - - /** gets the user's avatar */ - avatarURL(options: AvatarOptions): string { - if (!this.avatarHash) { - return USER_DEFAULT_AVATAR(Number(this.discriminator) % 5); - } - - return Util.formatImageURL(USER_AVATAR( - this.id, - this.avatarHash - ), options.size ?? 128, options.format); - } - - openDM(): Promise { - return DMChannel.prototype.open.call({ session: this.session }, this.id); - } - - toString(): string { - return `<@${this.id}>`; - } -} diff --git a/packages/core/src/structures/webhook.ts b/packages/core/src/structures/webhook.ts deleted file mode 100644 index 1680158..0000000 --- a/packages/core/src/structures/webhook.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { - DiscordEmbed, - DiscordMessage, - DiscordMessageComponents, - DiscordWebhook, - FileContent, - WebhookTypes, - WebhookOptions, -} from '@biscuitland/api-types'; -import type { Attachment } from './attachment'; -import type { AllowedMentions, CreateMessage } from './message'; -import { User } from './user'; -import { Message } from './message'; -import { Util } from '../utils/util'; -import { - WEBHOOK, - WEBHOOK_TOKEN, - WEBHOOK_MESSAGE, - WEBHOOK_MESSAGE_ORIGINAL, -} from '@biscuitland/api-types'; -import { NewEmbed } from './embed'; - -export type ExecuteWebhookOptions = WebhookOptions & - CreateMessage & { avatarUrl?: string; username?: string }; -export type EditMessageWithThread = EditWebhookMessage & { - threadId?: Snowflake; -}; - -/** - * @link https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params - */ -export interface EditWebhookMessage { - content?: string; - embeds?: DiscordEmbed[]; - files?: FileContent[]; - allowedMentions?: AllowedMentions; - attachments?: Attachment[]; - components?: DiscordMessageComponents; -} - -export class Webhook implements Model { - constructor(session: Session, data: DiscordWebhook) { - this.session = session; - this.id = data.id; - this.type = data.type; - this.token = data.token; - - if (data.avatar) { - this.avatar = Util.iconHashToBigInt(data.avatar); - } - - if (data.user) { - this.user = new User(session, data.user); - } - - if (data.guild_id) { - this.guildId = data.guild_id; - } - - if (data.channel_id) { - this.channelId = data.channel_id; - } - - if (data.application_id) { - this.applicationId = data.application_id; - } - } - - readonly session: Session; - readonly id: Snowflake; - type: WebhookTypes; - token?: string; - avatar?: bigint; - applicationId?: Snowflake; - channelId?: Snowflake; - guildId?: Snowflake; - user?: User; - - async execute( - options?: ExecuteWebhookOptions - ): Promise { - if (!this.token) { - return; - } - - const data = { - content: options?.content, - embeds: options?.embeds?.map(NewEmbed), - tts: options?.tts, - allowed_mentions: options?.allowedMentions, - components: options?.components, - file: options?.files, - }; - - const message = await this.session.rest.post( - WEBHOOK(this.id, this.token, { - wait: options?.wait, - threadId: options?.threadId, - }), - data - ); - - return options?.wait ?? true - ? new Message(this.session, message) - : undefined; - } - - async fetch(): Promise { - const message = await this.session.rest.get( - WEBHOOK_TOKEN(this.id, this.token) - ); - - return new Webhook(this.session, message); - } - - async fetchMessage( - messageId: Snowflake, - threadId?: Snowflake - ): Promise { - if (!this.token) { - return; - } - - const message = await this.session.rest.get( - WEBHOOK_MESSAGE(this.id, this.token, messageId, { threadId }) - ); - - return new Message(this.session, message); - } - - /** - * @deprecated you might want to delete an ephemeral message (deleteFollowUp) - * */ - deleteMessage( - messageId: Snowflake, - threadId?: Snowflake - ): Promise { - return this.deleteThreadMessage(messageId, threadId); - } - - async deleteThreadMessage( - messageId: Snowflake, - threadId?: Snowflake - ): Promise { - if (!this.token) { - throw new Error('No token found'); - } - - await this.session.rest.delete( - WEBHOOK_MESSAGE(this.id, this.token, messageId, { threadId }), - {} - ); - } - - async deleteFollowUp(messageId?: Snowflake): Promise { - if (!this.token) { - throw new Error('No token found'); - } - - await this.session.rest.delete( - messageId - ? WEBHOOK_MESSAGE(this.id, this.token, messageId) - : WEBHOOK_MESSAGE_ORIGINAL(this.id, this.token) - ); - } - - async editMessage( - messageId?: Snowflake, - options?: EditMessageWithThread - ): Promise { - if (!this.token) { - throw new Error('No token found'); - } - - const message = await this.session.rest.patch( - messageId - ? WEBHOOK_MESSAGE(this.id, this.token, messageId) - : WEBHOOK_MESSAGE_ORIGINAL(this.id, this.token), - { - content: options?.content, - embeds: options?.embeds?.map(NewEmbed), - file: options?.files, - components: options?.components, - allowed_mentions: options?.allowedMentions && { - parse: options?.allowedMentions.parse, - replied_user: options?.allowedMentions.repliedUser, - users: options?.allowedMentions.users, - roles: options?.allowedMentions.roles, - }, - attachments: options?.attachments?.map(attachment => { - return { - id: attachment.id, - filename: attachment.name, - content_type: attachment.contentType, - size: attachment.size, - url: attachment.attachment, - proxy_url: attachment.proxyUrl, - height: attachment.height, - width: attachment.width, - ephemeral: attachment.ephemeral, - }; - }), - } - ); - - return new Message(this.session, message); - } -} diff --git a/packages/core/src/structures/welcome.ts b/packages/core/src/structures/welcome.ts deleted file mode 100644 index 89519ee..0000000 --- a/packages/core/src/structures/welcome.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Session } from '../biscuit'; -import type { Model } from './base'; -import type { Snowflake } from '../snowflakes'; -import type { - DiscordWelcomeScreen, - DiscordWelcomeScreenChannel, -} from '@biscuitland/api-types'; -import { Emoji } from './emojis'; - -/** - * Not a channel - * @link https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure - */ -export class WelcomeChannel implements Model { - constructor(session: Session, data: DiscordWelcomeScreenChannel) { - this.session = session; - this.channelId = data.channel_id; - this.description = data.description; - this.emoji = new Emoji(session, { - name: data.emoji_name ? data.emoji_name : undefined, - id: data.emoji_id ? data.emoji_id : undefined, - }); - } - - session: Session; - channelId: Snowflake; - description: string; - emoji: Emoji; - - /** alias for WelcomeScreenChannel.channelId */ - get id(): Snowflake { - return this.channelId; - } -} - -/** - * @link https://discord.com/developers/docs/resources/guild#welcome-screen-object - */ -export class WelcomeScreen { - constructor(session: Session, data: DiscordWelcomeScreen) { - this.session = session; - this.welcomeChannels = data.welcome_channels.map( - welcomeChannel => new WelcomeChannel(session, welcomeChannel) - ); - - if (data.description) { - this.description = data.description; - } - } - - readonly session: Session; - - description?: string; - welcomeChannels: WelcomeChannel[]; -} diff --git a/packages/core/src/structures/widget.ts b/packages/core/src/structures/widget.ts deleted file mode 100644 index d0be0d3..0000000 --- a/packages/core/src/structures/widget.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Model } from './base'; -import type { Session } from '../biscuit'; -import type { Snowflake } from '../snowflakes'; -import type { DiscordGuildWidget } from '@biscuitland/api-types'; -import type { PartialChannel } from './channels'; - -export interface WidgetMember { - id?: string; - username: string; - avatar?: string | null; - status: string; - avatarURL: string; -} - -export class Widget implements Model { - constructor(session: Session, data: DiscordGuildWidget) { - this.session = session; - this.id = data.id; - this.name = data.name; - this.instantInvite = data.instant_invite; - this.channels = data.channels; - this.members = data.members.map(x => { - return { - id: x.id, - username: x.username, - avatar: x.avatar, - status: x.status, - avatarURL: x.avatar_url, - }; - }); - this.presenceCount = data.presence_count; - } - - session: Session; - id: Snowflake; - name: string; - instantInvite?: string; - channels: PartialChannel[]; - members: WidgetMember[]; - presenceCount: number; -} diff --git a/packages/core/src/utils/calculate-shard.ts b/packages/core/src/utils/calculate-shard.ts deleted file mode 100644 index 41fb290..0000000 --- a/packages/core/src/utils/calculate-shard.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function calculateShardId(totalShards: number, guildId: bigint) { - if (totalShards === 1) { return 0; } - - return Number((guildId >> 22n) % BigInt(totalShards - 1)); -} diff --git a/packages/core/src/utils/url-to-base-64.ts b/packages/core/src/utils/url-to-base-64.ts deleted file mode 100644 index d60d8d1..0000000 --- a/packages/core/src/utils/url-to-base-64.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** Converts a url to base 64. Useful for example, uploading/creating server emojis. */ -export async function urlToBase64(url: string): Promise { - const buffer = await fetch(url).then(res => res.arrayBuffer()); - const imageStr = encode(buffer); - const type = url.substring(url.lastIndexOf('.') + 1); - return `data:image/${type};base64,${imageStr}`; -} - -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -const base64abc: string[] = [ - 'A', 'B', 'C', - 'D', 'E', 'F', - 'G', 'H', 'I', - 'J', 'K', 'L', - 'M', 'N', 'O', - 'P', 'Q', 'R', - 'S', 'T', 'U', - 'V', 'W', 'X', - 'Y', 'Z', 'a', - 'b', 'c', 'd', - 'e', 'f', 'g', - 'h', 'i', 'j', - 'k', 'l', 'm', - 'n', 'o', 'p', - 'q', 'r', 's', - 't', 'u', 'v', - 'w', 'x', 'y', - 'z', '0', '1', - '2', '3', '4', - '5', '6', '7', - '8', '9', '+', '/', -]; - -/** - * CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 - * Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation - * @param data - */ -export function encode(data: ArrayBuffer | string): string { - const uint8: Uint8Array = typeof data === 'string' - ? new TextEncoder().encode(data) - : data instanceof Uint8Array - ? data - : new Uint8Array(data); - let result = '', - i; - const l: number = uint8.length; - for (i = 2; i < l; i += 3) { - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; - result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)]; - result += base64abc[uint8[i] & 0x3f]; - } - if (i === l + 1) { - // 1 octet yet to write - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[(uint8[i - 2] & 0x03) << 4]; - result += '=='; - } - if (i === l) { - // 2 octets yet to write - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; - result += base64abc[(uint8[i - 1] & 0x0f) << 2]; - result += '='; - } - return result; -} diff --git a/packages/core/src/utils/util.ts b/packages/core/src/utils/util.ts deleted file mode 100644 index 77a23d7..0000000 --- a/packages/core/src/utils/util.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { SelectMenuBuilder, InputTextBuilder, ButtonBuilder } from '@biscuitland/helpers'; -import type { Permissions } from '../structures/special/permissions'; -import type { Snowflake } from '../snowflakes'; -import type { DiscordMessage, MakeRequired } from '@biscuitland/api-types'; - -/* - * @link https://discord.com/developers/docs/resources/channel#message-object-message-flags - */ -export enum MessageFlags { - /** this message has been published to subscribed channels (via Channel Following) */ - CrossPosted = 1 << 0, - /** this message originated from a message in another channel (via Channel Following) */ - IsCrosspost = 1 << 1, - /** do not include any embeds when serializing this message */ - SupressEmbeds = 1 << 2, - /** the source message for this crosspost has been deleted (via Channel Following) */ - SourceMessageDeleted = 1 << 3, - /** this message came from the urgent message system */ - Urgent = 1 << 4, - /** this message has an associated thread, with the same id as the message */ - HasThread = 1 << 5, - /** this message is only visible to the user who invoked the Interaction */ - Ephemeral = 1 << 6, - /** this message is an Interaction Response and the bot is "thinking" */ - Loading = 1 << 7, - /** this message failed to mention some roles and add their members to the thread */ - FailedToMentionSomeRolesInThread = 1 << 8, -} - -export type ComponentBuilder = - | InputTextBuilder - | SelectMenuBuilder - | ButtonBuilder; - -/** * - * Utility type - */ -export type ComponentEmoji = { - id: Snowflake; - name: string; - animated?: boolean; -}; - -/** - * Utility type - */ -export interface PermissionsOverwrites { - id: Snowflake; - type: 0 | 1; - allow: Permissions; - deny: Permissions; -} - -/** - * @link https://discord.com/developers/docs/reference#image-formatting - */ -export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'gif' | 'json'; - -/** - * @link https://discord.com/developers/docs/reference#image-formatting - */ -export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096; - -/** - * Utility functions - */ -export abstract class Util { - static formatImageURL(url: string, size: ImageSize = 128, format?: ImageFormat): string { - return `${url}.${format ?? (url.includes('/a_') ? 'gif' : 'jpg')}?size=${size}`; - } - - static iconHashToBigInt(hash: string): bigint { - return BigInt('0x' + (hash.startsWith('a_') ? `a${hash.substring(2)}` : `b${hash}`)); - } - - static iconBigintToHash(icon: bigint): string { - const hash: string = icon.toString(16); - - return hash.startsWith('a') ? `a_${hash.substring(1)}` : hash.substring(1); - } - - /** Removes the Bot before the token. */ - static removeTokenPrefix(token?: string, type: 'GATEWAY' | 'REST' = 'REST'): string { - // If no token is provided, throw an error - if (!token) { throw new Error(`The ${type} was not given a token. Please provide a token and try again.`); } - - // If the token does not have a prefix just return token - if (!token.startsWith('Bot ')) { return token; } - - // Remove the prefix and return only the token. - return token.substring(token.indexOf(' ') + 1); - } - - /** Get the bot id from the bot token. WARNING: Discord staff has mentioned this may not be stable forever. Use at your own risk. However, note for over 5 years this has never broken. */ - static getBotIdFromToken(token: string): string { - return atob(token.split('.')[0]); - } - - static isFullMessage(m: Partial | DiscordMessage): m is DiscordMessage { - return !!m.edited_timestamp; - } - - static isPartialMessage(m: Partial | DiscordMessage): m is PartialMessage { - return !m.edited_timestamp; - } -} - -export type PartialMessage = MakeRequired; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 9b4f197..8b79d82 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "./dist" }, - "include": ["src/**/*"] + "include": ["src/**/*", "../../bot/Components", "../common/src/applyToClass.ts"] } diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 2e8c9eb..dfbea63 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], minify: isProduction, - sourcemap: false, + sourcemap: false }); diff --git a/packages/helpers/CHANGELOG.md b/packages/helpers/CHANGELOG.md deleted file mode 100644 index 815d3fb..0000000 --- a/packages/helpers/CHANGELOG.md +++ /dev/null @@ -1,101 +0,0 @@ -# @biscuitland/helpers - -## 2.3.0 - -### Minor Changes - -- fix TODO - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.3.0 - - @biscuitland/core@2.3.0 - -## 2.2.3 - -### Patch Changes - -- bug fixes -- Updated dependencies - - @biscuitland/api-types@2.2.3 - - @biscuitland/core@2.2.3 - -## 2.2.2 - -### Patch Changes - -- rename guildLocales to guildLocale in interactions -- Updated dependencies - - @biscuitland/api-types@2.2.2 - - @biscuitland/core@2.2.2 - -## 2.2.1 - -### Patch Changes - -- select menu options now can be empty since the latest Discord API update -- Updated dependencies - - @biscuitland/api-types@2.2.1 - - @biscuitland/core@2.2.1 - -## 2.2.0 - -### Minor Changes - -- Functionality to delete ephemeral messages added, select menus were updated - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.2.0 - - @biscuitland/core@2.2.0 - -## 2.1.2 - -### Patch Changes - -- minor changes -- Updated dependencies - - @biscuitland/api-types@2.1.2 - - @biscuitland/core@2.1.2 - -## 2.1.1 - -### Patch Changes - -- dumb hotfix that LH asked for (blame Yuzu) -- Updated dependencies - - @biscuitland/api-types@2.1.1 - - @biscuitland/core@2.1.1 - -## 2.1.0 - -### Minor Changes - -- Changes to cache and forum channels ✨ -- Forum channels and updates to @biscuitland/cache ✨ - -### Patch Changes - -- Updated dependencies -- Updated dependencies - - @biscuitland/api-types@2.1.0 - - @biscuitland/core@2.1.0 - -## 2.0.6 - -### Patch Changes - -- Minor fixes -- Updated dependencies - - @biscuitland/api-types@2.0.6 - - @biscuitland/core@2.0.6 - -## 2.0.5 - -### Patch Changes - -- Updated dependencies - - @biscuitland/api-types@2.0.5 - - @biscuitland/core@2.0.5 diff --git a/packages/helpers/README.md b/packages/helpers/README.md index 5aa1634..3765c96 100644 --- a/packages/helpers/README.md +++ b/packages/helpers/README.md @@ -1,6 +1,6 @@ # @biscuitland/helpers ## Most importantly, helpers is: -Collectors, builders and other helper functions such as setPresence. +Idk man [](https://github.com/oasisjs/biscuit) [](https://discord.gg/XNw2RZFzaP) @@ -18,4 +18,4 @@ yarn add @biscuitland/helpers * [Website](https://biscuitjs.com/) * [Documentation](https://docs.biscuitjs.com/) * [Discord](https://discord.gg/XNw2RZFzaP) -* [core](https://www.npmjs.com/package/@biscuitland/core) | [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) +* [core](https://www.npmjs.com/package/@biscuitland/core) | [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) \ No newline at end of file diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 100e6b0..c012823 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@biscuitland/helpers", - "version": "2.3.0", + "version": "3.0.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", @@ -23,8 +23,7 @@ } }, "dependencies": { - "@biscuitland/api-types": "^2.3.0", - "@biscuitland/core": "^2.3.0" + "@biscuitland/common": "^0.0.1" }, "devDependencies": { "@types/node": "^18.7.14", @@ -73,4 +72,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/helpers/src/MessageEmbed.ts b/packages/helpers/src/MessageEmbed.ts new file mode 100644 index 0000000..29b3604 --- /dev/null +++ b/packages/helpers/src/MessageEmbed.ts @@ -0,0 +1,70 @@ +import { + APIEmbed, + APIEmbedAuthor, + APIEmbedField, + APIEmbedFooter, + ObjectToLower, + TypeArray, + toSnakeCase +} from '@biscuitland/common'; + +export class MessageEmbed { + constructor(public data: Partial = {}) { + if (!data.fields) this.data.fields = []; + } + + setAuthor(author: ObjectToLower): this { + this.data.author = toSnakeCase(author); + return this; + } + + // TODO: Color resolve + setColor(color: number): this { + this.data.color = color; + return this; + } + + setDescription(desc: string): this { + this.data.description = desc; + return this; + } + + addFields(field: TypeArray): this { + this.data.fields?.concat(field); + return this; + } + + setFields(fields: APIEmbedField[]): this { + this.data.fields = fields; + return this; + } + + setFooter(footer: ObjectToLower): this { + this.data.footer = toSnakeCase(footer); + return this; + } + + setImage(url: string): this { + this.data.image = { url }; + return this; + } + + setTimestamp(time: string | number | Date = Date.now()): this { + this.data.timestamp = new Date(time).toISOString(); + return this; + } + + setTitle(title: string): this { + this.data.title = title; + return this; + } + + setURL(url: string): this { + this.data.url = url; + return this; + } + + toJSON(): APIEmbed { + return { ...this.data }; + } +} diff --git a/packages/helpers/src/Utils.ts b/packages/helpers/src/Utils.ts new file mode 100644 index 0000000..2c4c669 --- /dev/null +++ b/packages/helpers/src/Utils.ts @@ -0,0 +1,55 @@ +import { + APIMessageActionRowComponent, + APIModalActionRowComponent, + ComponentType, + PermissionFlagsBits +} from '@biscuitland/common'; +import { + ChannelSelectMenu, + MentionableSelectMenu, + MessageButton, + ModalTextInput, + RoleSelectMenu, + StringSelectMenu, + UserSelectMenu +} from './components'; +import { BaseComponent } from './components/BaseComponent'; + +export function createComponent(data: APIMessageActionRowComponent): HelperComponents; +export function createComponent(data: APIModalActionRowComponent): HelperComponents; +export function createComponent(data: HelperComponents): HelperComponents; +export function createComponent( + data: HelperComponents | APIMessageActionRowComponent | APIModalActionRowComponent +): HelperComponents { + if (data instanceof BaseComponent) { + return data; + } + + switch (data.type) { + case ComponentType.Button: + return new MessageButton(data); + case ComponentType.StringSelect: + return new StringSelectMenu(data); + case ComponentType.TextInput: + return new ModalTextInput(data); + case ComponentType.UserSelect: + return new UserSelectMenu(data); + case ComponentType.RoleSelect: + return new RoleSelectMenu(data); + case ComponentType.MentionableSelect: + return new MentionableSelectMenu(data); + case ComponentType.ChannelSelect: + return new ChannelSelectMenu(data); + } +} + +export type PermissionsStrings = `${keyof typeof PermissionFlagsBits}`; +export type OptionValuesLength = { max: number; min: number }; +export type MessageSelectMenus = + | RoleSelectMenu + | UserSelectMenu + | StringSelectMenu + | ChannelSelectMenu + | MentionableSelectMenu; +export type MessageComponents = MessageButton | MessageSelectMenus; +export type HelperComponents = MessageComponents | ModalTextInput; diff --git a/packages/helpers/src/builders/components/InputTextBuilder.ts b/packages/helpers/src/builders/components/InputTextBuilder.ts deleted file mode 100644 index fd61c7e..0000000 --- a/packages/helpers/src/builders/components/InputTextBuilder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { DiscordInputTextComponent, TextStyles } from '@biscuitland/api-types'; -import { MessageComponentTypes } from '@biscuitland/api-types'; - -export class InputTextBuilder { - constructor() { - this.#data = {} as DiscordInputTextComponent; - this.type = MessageComponentTypes.InputText; - } - - #data: DiscordInputTextComponent; - type: MessageComponentTypes.InputText; - - setStyle(style: TextStyles): this { - this.#data.style = style; - return this; - } - - setLabel(label: string): this { - this.#data.label = label; - return this; - } - - setPlaceholder(placeholder: string): this { - this.#data.placeholder = placeholder; - return this; - } - - setLength(max?: number, min?: number): this { - this.#data.max_length = max; - this.#data.min_length = min; - return this; - } - - setCustomId(id: string): this { - this.#data.custom_id = id; - return this; - } - - setValue(value: string): this { - this.#data.value = value; - return this; - } - - setRequired(required = true): this { - this.#data.required = required; - return this; - } - - toJSON(): DiscordInputTextComponent { - return { ...this.#data, type: this.type }; - } -} diff --git a/packages/helpers/src/builders/components/MessageActionRowBuilder.ts b/packages/helpers/src/builders/components/MessageActionRowBuilder.ts deleted file mode 100644 index 6399223..0000000 --- a/packages/helpers/src/builders/components/MessageActionRowBuilder.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { DiscordActionRow } from '@biscuitland/api-types'; -import { MessageComponentTypes } from '@biscuitland/api-types'; -import type { ComponentBuilder } from '@biscuitland/core'; - -export class ActionRowBuilder { - constructor() { - this.components = [] as T[]; - this.type = MessageComponentTypes.ActionRow; - } - - components: T[]; - type: MessageComponentTypes.ActionRow; - - addComponents(...components: T[]): this { - this.components.push(...components); - return this; - } - - setComponents(...components: T[]): this { - this.components.splice( - 0, - this.components.length, - ...components, - ); - return this; - } - - toJSON(): DiscordActionRow { - return { - type: this.type, - components: this.components.map(c => c.toJSON()) as DiscordActionRow['components'], - }; - } -} diff --git a/packages/helpers/src/builders/components/MessageButtonBuilder.ts b/packages/helpers/src/builders/components/MessageButtonBuilder.ts deleted file mode 100644 index 2424b59..0000000 --- a/packages/helpers/src/builders/components/MessageButtonBuilder.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ButtonStyles, DiscordButtonComponent } from '@biscuitland/api-types'; -import { MessageComponentTypes } from '@biscuitland/api-types'; -import type { ComponentEmoji } from '@biscuitland/core'; - -export class ButtonBuilder { - constructor() { - this.#data = {} as DiscordButtonComponent; - this.type = MessageComponentTypes.Button; - } - - #data: DiscordButtonComponent; - type: MessageComponentTypes.Button; - - setStyle(style: ButtonStyles): this { - this.#data.style = style; - return this; - } - - setLabel(label: string): this { - this.#data.label = label; - return this; - } - - setCustomId(id: string): this { - this.#data.custom_id = id; - return this; - } - - setEmoji(emoji: ComponentEmoji): this { - this.#data.emoji = emoji; - return this; - } - - setDisabled(disabled = true): this { - this.#data.disabled = disabled; - return this; - } - - setURL(url: string): this { - this.#data.url = url; - return this; - } - - toJSON(): DiscordButtonComponent { - return { ...this.#data, type: this.type }; - } -} diff --git a/packages/helpers/src/builders/components/MessageSelectMenuBuilder.ts b/packages/helpers/src/builders/components/MessageSelectMenuBuilder.ts deleted file mode 100644 index b691446..0000000 --- a/packages/helpers/src/builders/components/MessageSelectMenuBuilder.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { DiscordSelectOption, DiscordSelectMenuComponent, } from '@biscuitland/api-types'; -import type { ComponentEmoji } from '@biscuitland/core'; -import { MessageComponentTypes } from '@biscuitland/api-types'; - -export class SelectMenuOptionBuilder { - constructor() { - this.#data = {} as DiscordSelectOption; - } - - #data: DiscordSelectOption; - - setLabel(label: string): SelectMenuOptionBuilder { - this.#data.label = label; - return this; - } - - setValue(value: string): SelectMenuOptionBuilder { - this.#data.value = value; - return this; - } - - setDescription(description: string): SelectMenuOptionBuilder { - this.#data.description = description; - return this; - } - - setDefault(Default = true): SelectMenuOptionBuilder { - this.#data.default = Default; - return this; - } - - setEmoji(emoji: ComponentEmoji): SelectMenuOptionBuilder { - this.#data.emoji = emoji; - return this; - } - - toJSON(): DiscordSelectOption { - return { ...this.#data }; - } -} - -export class SelectMenuBuilder { - constructor() { - this.#data = {} as DiscordSelectMenuComponent; - this.type = MessageComponentTypes.SelectMenu; - this.options = []; - } - - #data: DiscordSelectMenuComponent; - type: MessageComponentTypes.SelectMenu | - MessageComponentTypes.RoleSelect | - MessageComponentTypes.UserSelect | - MessageComponentTypes.MentionableSelect | - MessageComponentTypes.ChannelSelect; - - options: SelectMenuOptionBuilder[]; - - setType(type: this['type']) { - this.type = type; - return this; - } - - setPlaceholder(placeholder: string): this { - this.#data.placeholder = placeholder; - return this; - } - - setValues(max?: number, min?: number): this { - this.#data.max_values = max; - this.#data.min_values = min; - return this; - } - - setDisabled(disabled = true): this { - this.#data.disabled = disabled; - return this; - } - - setCustomId(id: string): this { - this.#data.custom_id = id; - return this; - } - - setOptions(...options: SelectMenuOptionBuilder[]): this { - this.options.splice( - 0, - this.options.length, - ...options, - ); - return this; - } - - addOptions(...options: SelectMenuOptionBuilder[]): this { - this.options.push( - ...options, - ); - return this; - } - - toJSON(): DiscordSelectMenuComponent { - return { ...this.#data, type: this.type, options: this.options.map(option => option.toJSON()) }; - } -} diff --git a/packages/helpers/src/builders/embeds/embed-builder.ts b/packages/helpers/src/builders/embeds/embed-builder.ts deleted file mode 100644 index 67a509a..0000000 --- a/packages/helpers/src/builders/embeds/embed-builder.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { Embed } from '@biscuitland/core'; - -export interface EmbedFooter { - text: string; - iconUrl?: string; - proxyIconUrl?: string; -} - -export interface EmbedAuthor { - name: string; - text?: string; - url?: string; - iconUrl?: string; - proxyIconUrl?: string; -} - -export interface EmbedVideo { - height?: number; - proxyUrl?: string; - url?: string; - width?: number; -} - -export interface EmbedField { - name: string; - value: string; - inline?: boolean; -} - -export interface EmbedProvider { - url?: string; - name?: string; -} - -export class EmbedBuilder { - #data: Embed; - constructor(data: Embed = {}) { - this.#data = data; - if (!this.#data.fields) { this.#data.fields = []; } - } - - setAuthor(author: EmbedAuthor): EmbedBuilder { - this.#data.author = author; - return this; - } - - setColor(color: number): EmbedBuilder { - this.#data.color = color; - return this; - } - - setDescription(description: string): EmbedBuilder { - this.#data.description = description; - return this; - } - - addField(field: EmbedField): EmbedBuilder { - this.#data.fields!.push(field); - return this; - } - - setFooter(footer: EmbedFooter): EmbedBuilder { - this.#data.footer = footer; - return this; - } - - setImage(image: string): EmbedBuilder { - this.#data.image = { url: image }; - return this; - } - - setProvider(provider: EmbedProvider): EmbedBuilder { - this.#data.provider = provider; - return this; - } - - setThumbnail(thumbnail: string): EmbedBuilder { - this.#data.thumbnail = { url: thumbnail }; - return this; - } - - setTimestamp(timestamp: string | Date): EmbedBuilder { - this.#data.timestamp = timestamp instanceof Date ? timestamp.toISOString() : timestamp; - return this; - } - - setTitle(title: string, url?: string): EmbedBuilder { - this.#data.title = title; - if (url) { this.setUrl(url); } - return this; - } - - setUrl(url: string): EmbedBuilder { - this.#data.url = url; - return this; - } - - setVideo(video: EmbedVideo): EmbedBuilder { - this.#data.video = video; - return this; - } - - toJSON(): Embed { - return this.#data; - } -} diff --git a/packages/helpers/src/builders/slash/ApplicationCommand.ts b/packages/helpers/src/builders/slash/ApplicationCommand.ts deleted file mode 100644 index fde507f..0000000 --- a/packages/helpers/src/builders/slash/ApplicationCommand.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { - Localization, - PermissionStrings } from '@biscuitland/api-types'; -import { - ApplicationCommandTypes -} from '@biscuitland/api-types'; -import type { CreateApplicationCommand } from '@biscuitland/core'; -import { OptionBased } from './ApplicationCommandOption'; - -export abstract class ApplicationCommandBuilder { - constructor( - type: ApplicationCommandTypes = ApplicationCommandTypes.ChatInput, - name = '', - description = '', - defaultMemberPermissions?: PermissionStrings[], - nameLocalizations?: Localization, - descriptionLocalizations?: Localization, - dmPermission = true - ) { - this.type = type; - this.name = name; - this.description = description; - this.defaultMemberPermissions = defaultMemberPermissions; - this.nameLocalizations = nameLocalizations; - this.descriptionLocalizations = descriptionLocalizations; - this.dmPermission = dmPermission; - } - - type: ApplicationCommandTypes; - name: string; - description: string; - defaultMemberPermissions?: PermissionStrings[]; - nameLocalizations?: Localization; - descriptionLocalizations?: Localization; - dmPermission: boolean; - - setType(type: ApplicationCommandTypes): this { - return (this.type = type), this; - } - - setName(name: string): this { - return (this.name = name), this; - } - - setDescription(description: string): this { - return (this.description = description), this; - } - - setDefaultMemberPermission(perm: PermissionStrings[]): this { - return (this.defaultMemberPermissions = perm), this; - } - - setNameLocalizations(l: Localization): this { - return (this.nameLocalizations = l), this; - } - - setDescriptionLocalizations(l: Localization): this { - return (this.descriptionLocalizations = l), this; - } - - setDmPermission(perm: boolean): this { - return (this.dmPermission = perm), this; - } -} - -export type MessageApplicationCommandBuilderJSON = { - name: string; - type: ApplicationCommandTypes.Message; -}; - -export class MessageApplicationCommandBuilder { - type: ApplicationCommandTypes; - name?: string; - constructor(type?: ApplicationCommandTypes, name?: string) { - this.type = type ?? ApplicationCommandTypes.Message; - this.name = name; - } - - setName(name: string): this { - return (this.name = name), this; - } - - toJSON(): MessageApplicationCommandBuilderJSON { - if (!this.name) { throw new TypeError("Propety 'name' is required"); } - - return { - type: ApplicationCommandTypes.Message, - name: this.name - }; - } -} - -export class ChatInputApplicationCommandBuilder extends ApplicationCommandBuilder { - type: ApplicationCommandTypes.ChatInput = ApplicationCommandTypes.ChatInput; - - toJSON(): CreateApplicationCommand { - if (!this.type) { throw new TypeError("Propety 'type' is required"); } - if (!this.name) { throw new TypeError("Propety 'name' is required"); } - if (!this.description) { - throw new TypeError("Propety 'description' is required"); - } - - return { - type: ApplicationCommandTypes.ChatInput, - name: this.name, - description: this.description, - options: this.options?.map(o => o.toJSON()) ?? [], - default_member_permissions: this.defaultMemberPermissions, - name_localizations: this.nameLocalizations, - description_localizations: this.descriptionLocalizations, - dm_permission: this.dmPermission - }; - } -} - -OptionBased.applyTo(ChatInputApplicationCommandBuilder); - -export interface ChatInputApplicationCommandBuilder - extends ApplicationCommandBuilder, - OptionBased { - // pass -} diff --git a/packages/helpers/src/builders/slash/ApplicationCommandOption.ts b/packages/helpers/src/builders/slash/ApplicationCommandOption.ts deleted file mode 100644 index eb4e418..0000000 --- a/packages/helpers/src/builders/slash/ApplicationCommandOption.ts +++ /dev/null @@ -1,360 +0,0 @@ -import type { ChannelTypes, Localization, Locales } from '@biscuitland/api-types'; -import { ApplicationCommandOptionTypes } from '@biscuitland/api-types'; -import type { ApplicationCommandOptionChoice } from '@biscuitland/core'; - - -export type Localizations = typeof Locales[keyof typeof Locales] & string; - -export class ChoiceBuilder { - name?: string; - value?: string; - - setName(name: string): ChoiceBuilder { - this.name = name; - return this; - } - - setValue(value: string): this { - this.value = value; - return this; - } - - toJSON(): ApplicationCommandOptionChoice { - if (!this.name) { throw new TypeError('Property \'name\' is required'); } - if (!this.value) { throw new TypeError('Property \'value\' is required'); } - - return { - name: this.name, - value: this.value, - }; - } -} - -export class OptionBuilder { - required?: boolean; - autocomplete?: boolean; - type?: ApplicationCommandOptionTypes; - name?: string; - nameLocalization?: Record; - description?: string; - descriptionLocalization?: Record; - - constructor(type?: ApplicationCommandOptionTypes, name?: string, description?: string) { - this.type = type; - this.name = name; - this.description = description; - } - - setType(type: ApplicationCommandOptionTypes): this { - return (this.type = type), this; - } - - setName(name: string, localization?: Record): this { - this.name = name; - this.nameLocalization = localization; - - return this; - } - - setDescription(description: string, localization?: Record): this { - this.description = description; - this.descriptionLocalization = localization; - - return this; - } - - setRequired(required: boolean): this { - return (this.required = required), this; - } - - toJSON(): ApplicationCommandOption { - if (!this.type) { throw new TypeError('Property \'type\' is required'); } - if (!this.name) { throw new TypeError('Property \'name\' is required'); } - if (!this.description) { - throw new TypeError('Property \'description\' is required'); - } - - const applicationCommandOption: ApplicationCommandOption = { - type: this.type, - name: this.name, - name_localizations: this.nameLocalization, - description: this.description, - description_localizations: this.descriptionLocalization, - required: this.required ? true : false, - }; - - return applicationCommandOption; - } -} - -export class OptionBuilderLimitedValues extends OptionBuilder { - choices?: ChoiceBuilder[]; - minValue?: number; - maxValue?: number; - - constructor( - type?: ApplicationCommandOptionTypes.Integer | ApplicationCommandOptionTypes.Number, - name?: string, - description?: string, - ) { - super(); - this.type = type; - this.name = name; - this.description = description; - } - - setMinValue(n: number): this { - return (this.minValue = n), this; - } - - setMaxValue(n: number): this { - return (this.maxValue = n), this; - } - - addChoice(fn: (choice: ChoiceBuilder) => ChoiceBuilder): this { - const choice = fn(new ChoiceBuilder()); - this.choices ??= []; - this.choices.push(choice); - return this; - } - - override toJSON(): ApplicationCommandOption { - return { - ...super.toJSON(), - choices: this.choices?.map(c => c.toJSON()) ?? [], - min_value: this.minValue, - max_value: this.maxValue, - }; - } -} - -export class OptionBuilderString extends OptionBuilder { - choices?: ChoiceBuilder[]; - constructor( - type?: ApplicationCommandOptionTypes.String, - name?: string, - description?: string, - ) { - super(); - this.type = type; - this.name = name; - this.description = description; - this; - } - - addChoice(fn: (choice: ChoiceBuilder) => ChoiceBuilder): this { - const choice = fn(new ChoiceBuilder()); - this.choices ??= []; - this.choices.push(choice); - return this; - } - - override toJSON(): ApplicationCommandOption { - return { - ...super.toJSON(), - choices: this.choices?.map(c => c.toJSON()) ?? [], - }; - } -} - -export class OptionBuilderChannel extends OptionBuilder { - channelTypes?: ChannelTypes[]; - constructor( - type?: ApplicationCommandOptionTypes.Channel, - name?: string, - description?: string, - ) { - super(); - this.type = type; - this.name = name; - this.description = description; - this; - } - - addChannelTypes(...channels: ChannelTypes[]): this { - this.channelTypes ??= []; - this.channelTypes.push(...channels); - return this; - } - - override toJSON(): ApplicationCommandOption { - return { - ...super.toJSON(), - channel_types: this.channelTypes ?? [], - }; - } -} - -export interface OptionBuilderLike { - toJSON(): ApplicationCommandOption; -} - -export class OptionBased { - options?: - & ( - | OptionBuilder[] - | OptionBuilderString[] - | OptionBuilderLimitedValues[] - | OptionBuilderNested[] - | OptionBuilderChannel[] - ) - & OptionBuilderLike[]; - - addOption(fn: (option: OptionBuilder) => OptionBuilder, type?: ApplicationCommandOptionTypes): this { - const option = fn(new OptionBuilder(type)); - this.options ??= []; - this.options.push(option); - return this; - } - - addNestedOption(fn: (option: OptionBuilder) => OptionBuilder): this { - const option = fn(new OptionBuilder(ApplicationCommandOptionTypes.SubCommand)); - this.options ??= []; - this.options.push(option); - return this; - } - - addStringOption(fn: (option: OptionBuilderString) => OptionBuilderString): this { - const option = fn(new OptionBuilderString(ApplicationCommandOptionTypes.String)); - this.options ??= []; - this.options.push(option); - return this; - } - - addIntegerOption(fn: (option: OptionBuilderLimitedValues) => OptionBuilderLimitedValues): this { - const option = fn(new OptionBuilderLimitedValues(ApplicationCommandOptionTypes.Integer)); - this.options ??= []; - this.options.push(option); - return this; - } - - addNumberOption(fn: (option: OptionBuilderLimitedValues) => OptionBuilderLimitedValues): this { - const option = fn(new OptionBuilderLimitedValues(ApplicationCommandOptionTypes.Number)); - this.options ??= []; - this.options.push(option); - return this; - } - - addBooleanOption(fn: (option: OptionBuilder) => OptionBuilder): this { - return this.addOption(fn, ApplicationCommandOptionTypes.Boolean); - } - - addSubCommand(fn: (option: OptionBuilderNested) => OptionBuilderNested): this { - const option = fn(new OptionBuilderNested(ApplicationCommandOptionTypes.SubCommand)); - this.options ??= []; - this.options.push(option); - return this; - } - - addSubCommandGroup(fn: (option: OptionBuilderNested) => OptionBuilderNested): this { - const option = fn(new OptionBuilderNested(ApplicationCommandOptionTypes.SubCommandGroup)); - this.options ??= []; - this.options.push(option); - return this; - } - - addUserOption(fn: (option: OptionBuilder) => OptionBuilder): this { - return this.addOption(fn, ApplicationCommandOptionTypes.User); - } - - addChannelOption(fn: (option: OptionBuilderChannel) => OptionBuilderChannel): this { - const option = fn(new OptionBuilderChannel(ApplicationCommandOptionTypes.Channel)); - this.options ??= []; - this.options.push(option); - return this; - } - - addRoleOption(fn: (option: OptionBuilder) => OptionBuilder): this { - return this.addOption(fn, ApplicationCommandOptionTypes.Role); - } - - addMentionableOption(fn: (option: OptionBuilder) => OptionBuilder): this { - return this.addOption(fn, ApplicationCommandOptionTypes.Mentionable); - } - - // eslint-disable-next-line @typescript-eslint/ban-types - static applyTo(klass: Function, ignore: (keyof OptionBased)[] = []): void { - const methods: (keyof OptionBased)[] = [ - 'addOption', - 'addNestedOption', - 'addStringOption', - 'addIntegerOption', - 'addNumberOption', - 'addBooleanOption', - 'addSubCommand', - 'addSubCommandGroup', - 'addUserOption', - 'addChannelOption', - 'addRoleOption', - 'addMentionableOption', - ]; - - for (const method of methods) { - if (ignore.includes(method)) { continue; } - - klass.prototype[method] = OptionBased.prototype[method]; - } - } -} - -export class OptionBuilderNested extends OptionBuilder { - constructor( - type?: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup, - name?: string, - description?: string, - ) { - super(); - this.type = type; - this.name = name; - this.description = description; - } - - override toJSON(): ApplicationCommandOption { - if (!this.type) { throw new TypeError('Property \'type\' is required'); } - if (!this.name) { throw new TypeError('Property \'name\' is required'); } - if (!this.description) { - throw new TypeError('Property \'description\' is required'); - } - - return { - type: this.type, - name: this.name, - description: this.description, - options: this.options?.map(o => o.toJSON()) ?? [], - required: this.required ? true : false, - }; - } -} - -OptionBased.applyTo(OptionBuilderNested); - -export interface OptionBuilderNested extends OptionBuilder, OptionBased { - // pass -} - -export interface ApplicationCommandOption { - /** Value of Application Command Option Type */ - type: ApplicationCommandOptionTypes; - /** 1-32 character name matching lowercase `^[\w-]{1,32}$` */ - name: string; - /** Localization object for the `name` field. Values follow the same restrictions as `name` */ - name_localizations?: Localization; - /** 1-100 character description */ - description: string; - /** Localization object for the `description` field. Values follow the same restrictions as `description` */ - description_localizations?: Localization; - /** If the parameter is required or optional--default `false` */ - required?: boolean; - /** Choices for `string` and `int` types for the user to pick from */ - choices?: ApplicationCommandOptionChoice[]; - /** If the option is a subcommand or subcommand group type, this nested options will be the parameters */ - options?: ApplicationCommandOption[]; - /** if autocomplete interactions are enabled for this `String`, `Integer`, or `Number` type option */ - autocomplete?: boolean; - /** If the option is a channel type, the channels shown will be restricted to these types */ - channel_types?: ChannelTypes[]; - /** Minimum number desired. */ - min_value?: number; - /** Maximum number desired. */ - max_value?: number; -} diff --git a/packages/helpers/src/collectors.ts b/packages/helpers/src/collectors.ts deleted file mode 100644 index f003fdf..0000000 --- a/packages/helpers/src/collectors.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Session, Events } from '@biscuitland/core'; -import { EventEmitter } from 'node:events'; - - -export interface CollectorOptions { - event: E; - filter?(...args: Parameters): unknown; - max?: number; - time?: number; - idle?: number; -} - -export class Collector extends EventEmitter { - collected = new Set[0]>(); - ended = false; - - private timeout: NodeJS.Timeout; - - constructor(readonly session: Session, public options: CollectorOptions) { - super(); - - if (!('filter' in this.options)) { this.options.filter = (() => true); } - - if (!('max' in this.options)) { this.options.max = -1; } - - this.session.events.setMaxListeners(this.session.events.getMaxListeners() + 1); - - this.session.events.on(this.options.event, (...args: unknown[]) => this.collect(...args as Parameters)); - - this.timeout = setTimeout(() => this.stop('time'), this.options.idle ?? this.options.time); - } - - private collect(...args: Parameters) { - if (this.options.filter?.(...args)) { - this.collected.add(args[0]); - this.emit('collect', ...args); - } - - if (this.options.idle) { - clearTimeout(this.timeout); - this.timeout = setTimeout(() => this.stop('time'), this.options.idle); - } - - if (this.collected.size >= this.options.max!) { this.stop('max'); } - } - - stop(reason?: string) { - if (this.ended) { return; } - - clearTimeout(this.timeout); - - this.session.events.removeListener(this.options.event, (...args: unknown[]) => this.collect(...args as Parameters)); - this.session.events.setMaxListeners(this.session.events.getMaxListeners() - 1); - - this.ended = true; - this.emit('end', reason, this.collected); - } - - on(event: 'collect', listener: (...args: Parameters) => unknown): this; - on(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; - on(event: string, listener: unknown): this { - return super.on(event, listener as (() => unknown)); - } - - once(event: 'collect', listener: (...args: Parameters) => unknown): this; - once(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; - once(event: string, listener: unknown): this { - return super.once(event, listener as (() => unknown)); - } -} diff --git a/packages/helpers/src/commands/contextMenu/ContextCommand.ts b/packages/helpers/src/commands/contextMenu/ContextCommand.ts new file mode 100644 index 0000000..3f8b8f7 --- /dev/null +++ b/packages/helpers/src/commands/contextMenu/ContextCommand.ts @@ -0,0 +1,49 @@ +import { + LocalizationMap, + ApplicationCommandType, + Permissions, + PermissionFlagsBits, + RESTPostAPIContextMenuApplicationCommandsJSONBody +} from '@biscuitland/common'; +import { PermissionsStrings } from '../../Utils'; + +export type ContextCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; + +export class ContextCommand { + name: string = undefined!; + name_localizations?: LocalizationMap; + type: ContextCommandType = undefined!; + default_permission: boolean | undefined = undefined; + default_member_permissions: Permissions | null | undefined = undefined; + dm_permission: boolean | undefined = undefined; + + setName(name: string): this { + this.name = name; + return this; + } + + setType(type: ContextCommandType) { + this.type = type; + return this; + } + + addNameLocalizations(locals: LocalizationMap): this { + this.name_localizations = locals; + Reflect; + return this; + } + + setDMPermission(value = true): this { + this.dm_permission = value; + return this; + } + + setDefautlMemberPermissions(permissions: PermissionsStrings[]): this { + this.default_member_permissions = `$${permissions.reduce((y, x) => y | PermissionFlagsBits[x], 0n)}`; + return this; + } + + toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody { + return { ...this }; + } +} diff --git a/packages/helpers/src/commands/index.ts b/packages/helpers/src/commands/index.ts new file mode 100644 index 0000000..c1d66f2 --- /dev/null +++ b/packages/helpers/src/commands/index.ts @@ -0,0 +1 @@ +export * from './slash/SlashCommandOption'; diff --git a/packages/helpers/src/commands/slash/SlashCommand.ts b/packages/helpers/src/commands/slash/SlashCommand.ts new file mode 100644 index 0000000..a07b811 --- /dev/null +++ b/packages/helpers/src/commands/slash/SlashCommand.ts @@ -0,0 +1,57 @@ +import { + ApplicationCommandType, + PermissionFlagsBits, + RESTPostAPIChatInputApplicationCommandsJSONBody, + applyToClass +} from '@biscuitland/common'; +import { AllSlashOptions, SlashSubcommandGroupOption, SlashSubcommandOption } from './SlashCommandOption'; +import { PermissionsStrings } from '../../Utils'; + +class SlashCommandB { + constructor(public data: Partial = {}) {} + + setDMPermission(value = true): this { + this.data.dm_permission = value; + return this; + } + + setNSFW(value = true): this { + this.data.nsfw = value; + return this; + } + + setDefautlMemberPermissions(permissions: PermissionsStrings[]): this { + this.data.default_member_permissions = `$${permissions.reduce((y, x) => y | PermissionFlagsBits[x], 0n)}`; + return this; + } + + addSubcommandGroup(fn: (option: SlashSubcommandGroupOption) => SlashSubcommandGroupOption): this { + const option = fn(new SlashSubcommandGroupOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addSubcommand(fn: (option: SlashSubcommandOption) => SlashSubcommandOption): this { + const option = fn(new SlashSubcommandOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addRawOption(option: ReturnType) { + this.data.options ??= []; + // @ts-expect-error discord-api-types bad typing, again + this.data.options.push(option); + } + + toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody { + return { + ...this.data, + type: ApplicationCommandType.ChatInput + } as RESTPostAPIChatInputApplicationCommandsJSONBody & { + type: ApplicationCommandType.ChatInput; + }; + } +} + +export const SlashCommand = applyToClass(SlashSubcommandOption, SlashCommandB, ['toJSON']); +export type SlashCommand = InstanceType; diff --git a/packages/helpers/src/commands/slash/SlashCommandOption.ts b/packages/helpers/src/commands/slash/SlashCommandOption.ts new file mode 100644 index 0000000..b3a0bb8 --- /dev/null +++ b/packages/helpers/src/commands/slash/SlashCommandOption.ts @@ -0,0 +1,346 @@ +import { + APIApplicationCommandOption, + APIApplicationCommandOptionChoice, + ApplicationCommandOptionType, + APIApplicationCommandStringOption as AACSO, + APIApplicationCommandOptionBase, + LocalizationMap, + RestToKeys, + TypeArray, + When, + APIApplicationCommandNumberOption as AACNO, + APIApplicationCommandIntegerOption as AACIO, + APIApplicationCommandSubcommandGroupOption as AACSGO, + APIApplicationCommandSubcommandOption as AACSCO, + APIApplicationCommandUserOption, + APIApplicationCommandChannelOption, + ChannelType, + APIApplicationCommandRoleOption, + APIApplicationCommandMentionableOption, + APIApplicationCommandAttachmentOption, + APIApplicationCommandBooleanOption +} from '@biscuitland/common'; +import { OptionValuesLength } from '../../'; + +export type SlashBaseOptionTypes = + | Exclude + | APIApplicationCommandStringOption + | APIApplicationCommandNumberOption + | APIApplicationCommandIntegerOption + | APIApplicationCommandSubcommandOption; + +export type ApplicationCommandBasicOptions = + | APIApplicationCommandAttachmentOption + | APIApplicationCommandChannelOption + | APIApplicationCommandIntegerOption + | APIApplicationCommandMentionableOption + | APIApplicationCommandNumberOption + | APIApplicationCommandStringOption + | APIApplicationCommandRoleOption + | APIApplicationCommandBooleanOption + | APIApplicationCommandUserOption; + +export abstract class SlashBaseOption { + constructor(public data: Partial = {}) {} + + setName(name: string): this { + this.data.name = name; + return this; + } + + setDesciption(desc: string): this { + this.data.description = desc; + return this; + } + + addLocalizations(locals: RestToKeys<[LocalizationMap, 'name', 'description']>): this { + this.data.name_localizations = locals.name; + this.data.description_localizations = locals.description; + return this; + } + + toJSON(): DataType { + return { ...this.data } as DataType; + } +} + +export type SlashRequiredOptionTypes = Exclude; + +export class SlashRequiredOption extends SlashBaseOption { + setRequired(required = true): this { + this.data.required = required; + return this; + } +} + +/** + * Temporal fix type + * I didn't find the correct way to set this with discord-api-types + * Author: socram03 + */ +export interface APIApplicationCommandStringOption + extends APIApplicationCommandOptionBase { + /** + * For option type `STRING`, the minimum allowed length (minimum of `0`, maximum of `6000`). + */ + min_length?: number; + /** + * For option type `STRING`, the maximum allowed length (minimum of `1`, maximum of `6000`). + */ + max_length?: number; + + autocomplete: When; + + choices: When[], never>; +} + +export class SlashStringOption extends SlashRequiredOption< + APIApplicationCommandStringOption +> { + constructor(data: Partial> = {}) { + super({ ...data, type: ApplicationCommandOptionType.String }); + } + + addChoices(choices: TypeArray>): SlashStringOption { + const ctx = this as SlashStringOption; + ctx.data.choices ??= []; + ctx.data.choices.concat(choices); + return ctx; + } + + setAutocomplete(auto = true): SlashStringOption { + const ctx = this as SlashStringOption; + ctx.data.autocomplete = auto; + return ctx; + } + + setLength({ min, max }: OptionValuesLength): this { + this.data.min_length = min; + this.data.max_length = max; + return this; + } +} + +/** + * Temporal fix type + * I didn't find the correct way to set this with discord-api-types + * Author: socram03 + */ +interface APIApplicationCommandNumberOption + extends APIApplicationCommandOptionBase { + /** + * If the option is an `INTEGER` or `NUMBER` type, the minimum value permitted. + */ + min_value?: number; + /** + * If the option is an `INTEGER` or `NUMBER` type, the maximum value permitted. + */ + max_value?: number; + + autocomplete: When; + + choices: When[], never>; +} + +export class SlashNumberOption extends SlashRequiredOption< + APIApplicationCommandNumberOption +> { + constructor(data: Partial> = {}) { + super({ ...data, type: ApplicationCommandOptionType.Number }); + } + + addChoices(choices: TypeArray>): SlashNumberOption { + const ctx = this as SlashNumberOption; + ctx.data.choices ??= []; + ctx.data.choices.concat(choices); + return ctx; + } + + setValueRange({ min, max }: OptionValuesLength): this { + this.data.max_value = max; + this.data.min_value = min; + return this; + } + + setAutocomplete(auto = true): SlashNumberOption { + const ctx = this as SlashNumberOption; + ctx.data.autocomplete = auto; + return ctx; + } +} + +/** + * Temporal fix type + * I didn't find the correct way to set this with discord-api-types + * Author: socram03 + */ +interface APIApplicationCommandIntegerOption + extends APIApplicationCommandOptionBase { + /** + * If the option is an `INTEGER` or `NUMBER` type, the minimum value permitted. + */ + min_value?: number; + /** + * If the option is an `INTEGER` or `NUMBER` type, the maximum value permitted. + */ + max_value?: number; + + autocomplete: When; + + choices: When[], never>; +} + +export class SlashIntegerOption extends SlashRequiredOption< + APIApplicationCommandIntegerOption +> { + constructor(data: Partial> = {}) { + super({ ...data, type: ApplicationCommandOptionType.Integer }); + } +} + +export class SlashUserOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.User }); + } +} + +export type SlashChannelOptionChannelTypes = Exclude; + +export class SlashChannelOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Channel }); + } + + setChannelTypes(types: SlashChannelOptionChannelTypes[]): this { + this.data.channel_types = types; + return this; + } +} + +export class SlashRoleOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Role }); + } +} + +export class SlashMentionableOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Mentionable }); + } +} + +export class SlashAttachmentOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Attachment }); + } +} + +export class SlashBooleanOption extends SlashRequiredOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Boolean }); + } +} + +export type BasicSlashOptions = + | SlashStringOption + | SlashAttachmentOption + | SlashChannelOption + | SlashIntegerOption + | SlashNumberOption + | SlashRoleOption + | SlashMentionableOption + | SlashUserOption + | SlashBooleanOption; + +export type APIApplicationCommandSubcommandOption = AACSCO & { + options?: ApplicationCommandBasicOptions[]; +}; + +export class SlashSubcommandOption extends SlashBaseOption { + constructor(data: Partial = {}) { + super({ ...data, type: ApplicationCommandOptionType.Subcommand }); + } + + addStringOption(fn: (option: SlashStringOption) => SlashStringOption): this { + const option = fn(new SlashStringOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addNumberOption(fn: (option: SlashNumberOption) => SlashNumberOption): this { + const option = fn(new SlashNumberOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addIntegerOption(fn: (option: SlashIntegerOption) => SlashIntegerOption): this { + const option = fn(new SlashIntegerOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addChannelOption(fn: (option: SlashChannelOption) => SlashChannelOption): this { + const option = fn(new SlashChannelOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addUserOption(fn: (option: SlashUserOption) => SlashUserOption): this { + const option = fn(new SlashUserOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addRoleOption(fn: (option: SlashRoleOption) => SlashRoleOption): this { + const option = fn(new SlashRoleOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addMentionableOption(fn: (option: SlashMentionableOption) => SlashMentionableOption): this { + const option = fn(new SlashMentionableOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addAttachmentOption(fn: (option: SlashAttachmentOption) => SlashAttachmentOption): this { + const option = fn(new SlashAttachmentOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addBooleanOption(fn: (option: SlashBooleanOption) => SlashBooleanOption): this { + const option = fn(new SlashBooleanOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addRawOption(option: ApplicationCommandBasicOptions) { + this.data.options ??= []; + this.data.options.push(option); + } +} + +export type APIApplicationCommandSubcommandGroupOption = AACSGO & { + options?: APIApplicationCommandSubcommandOption[]; +}; + +export type AllSlashOptions = BasicSlashOptions | SlashSubcommandGroupOption | SlashSubcommandOption; + +export class SlashSubcommandGroupOption extends SlashBaseOption { + constructor(data: Partial = {}) { + if (!data.options) data.options = []; + super({ ...data, type: ApplicationCommandOptionType.SubcommandGroup }); + } + + addSubCommand(fn: (option: SlashSubcommandOption) => SlashSubcommandOption): this { + const option = fn(new SlashSubcommandOption()); + this.addRawOption(option.toJSON()); + return this; + } + + addRawOption(option: ReturnType) { + this.data.options ??= []; + this.data.options.push(option); + } +} diff --git a/packages/helpers/src/components/ActionRow.ts b/packages/helpers/src/components/ActionRow.ts new file mode 100644 index 0000000..58f2d03 --- /dev/null +++ b/packages/helpers/src/components/ActionRow.ts @@ -0,0 +1,30 @@ +import { APIActionRowComponent, APIMessageActionRowComponent, ComponentType, TypeArray } from '@biscuitland/common'; +import { MessageComponents, createComponent } from '../Utils'; +import { BaseComponent } from './BaseComponent'; + +export class MessageActionRow extends BaseComponent< + APIActionRowComponent +> { + constructor({ components, ...data }: Partial>) { + super({ ...data, type: ComponentType.ActionRow }); + this.components = (components?.map(createComponent) ?? []) as T[]; + } + components: T[]; + + addComponents(component: TypeArray): this { + this.components.concat(component); + return this; + } + + setComponents(component: T[]): this { + this.components = [...component]; + return this; + } + + toJSON(): APIActionRowComponent { + return { + ...this.data, + components: this.components.map((c) => c.toJSON()) + } as APIActionRowComponent>; + } +} diff --git a/packages/helpers/src/components/BaseComponent.ts b/packages/helpers/src/components/BaseComponent.ts new file mode 100644 index 0000000..d891e1b --- /dev/null +++ b/packages/helpers/src/components/BaseComponent.ts @@ -0,0 +1,11 @@ +import { APIBaseComponent, ComponentType } from '@biscuitland/common'; + +export abstract class BaseComponent< + TYPE extends Partial> = APIBaseComponent, +> { + constructor(public data: Partial) {} + + toJSON(): TYPE { + return { ...this.data } as TYPE; + } +} diff --git a/packages/helpers/src/components/MessageButton.ts b/packages/helpers/src/components/MessageButton.ts new file mode 100644 index 0000000..4d115d2 --- /dev/null +++ b/packages/helpers/src/components/MessageButton.ts @@ -0,0 +1,55 @@ +import { + APIButtonComponentBase, + APIMessageComponentEmoji, + ButtonStyle, + ComponentType, + When +} from '@biscuitland/common'; +import { BaseComponent } from './BaseComponent'; + +export type ButtonStylesForID = Exclude; + +export interface APIButtonComponent + extends APIButtonComponentBase> { + url: When; + + custom_id: When; +} + +export class MessageButton extends BaseComponent> { + constructor(data: Partial> = {}) { + super({ ...data, type: ComponentType.Button }); + } + + setLabel(label: string): this { + this.data.label = label; + return this; + } + + setEmoji(emoji: APIMessageComponentEmoji): this { + this.data.emoji = emoji; + return this; + } + + setDisabled(disabled = true): this { + this.data.disabled = disabled; + return this; + } + + setURL(url: string): MessageButton { + const ctx = this as MessageButton; + ctx.data.url = url; + return ctx; + } + + setStyle(style: When): this { + this.data.style = style; + return this; + } + + setCustomId(id: string): MessageButton { + const ctx = this as MessageButton; + ctx.data.custom_id = id; + return ctx; + } +} diff --git a/packages/helpers/src/components/SelectMenu.ts b/packages/helpers/src/components/SelectMenu.ts new file mode 100644 index 0000000..56c90a1 --- /dev/null +++ b/packages/helpers/src/components/SelectMenu.ts @@ -0,0 +1,117 @@ +import { + APIChannelSelectComponent, + APIMentionableSelectComponent, + APIMessageComponentEmoji, + APIRoleSelectComponent, + APISelectMenuComponent, + APISelectMenuOption, + APIStringSelectComponent, + APIUserSelectComponent, + ChannelType, + ComponentType, + TypeArray +} from '@biscuitland/common'; +import { BaseComponent } from './BaseComponent'; +import { OptionValuesLength } from '..'; + +class SelectMenu { + setCustomId(id: string): this { + this.data.custom_id = id; + return this; + } + + setPlaceholder(placeholder: string): this { + this.data.placeholder = placeholder; + return this; + } + + setValuesLength({ max, min }: Partial): this { + this.data.max_values = max; + this.data.min_values = min; + return this; + } + + setDisabled(disabled = true): this { + this.data.disabled = disabled; + return this; + } +} + +export class UserSelectMenu extends SelectMenu { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.UserSelect }); + } +} + +export class RoleSelectMenu extends SelectMenu { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.RoleSelect }); + } +} + +export class MentionableSelectMenu extends SelectMenu { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.MentionableSelect }); + } +} + +export class ChannelSelectMenu extends SelectMenu { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.ChannelSelect }); + } + + setChannelTypes(types: ChannelType[]): this { + this.data.channel_types = types; + return this; + } +} + +export class StringSelectMenu extends SelectMenu { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.StringSelect }); + } + + addOption(option: TypeArray): this { + this.data.options ??= []; + this.data.options.concat(option); + return this; + } + + setOptions(options: APISelectMenuOption[]): this { + this.data.options = options; + return this; + } +} + +export class StringSelectOption { + constructor(public data: Partial = {}) {} + + setLabel(label: string): this { + this.data.label = label; + return this; + } + + setValue(value: string): this { + this.data.value = value; + return this; + } + + setDescription(description: string): this { + this.data.description = description; + return this; + } + + setDefault(Default = true): this { + this.data.default = Default; + return this; + } + + setEmoji(emoji: APIMessageComponentEmoji): this { + this.data.emoji = emoji; + return this; + } + + toJSON(): APISelectMenuOption { + return { ...this.data } as APISelectMenuOption; + } +} diff --git a/packages/helpers/src/components/TextInput.ts b/packages/helpers/src/components/TextInput.ts new file mode 100644 index 0000000..10a8778 --- /dev/null +++ b/packages/helpers/src/components/TextInput.ts @@ -0,0 +1,45 @@ +import { APITextInputComponent, ComponentType, TextInputStyle } from '@biscuitland/common'; +import { BaseComponent } from './BaseComponent'; +import { OptionValuesLength } from '..'; + +export class ModalTextInput extends BaseComponent { + constructor(data: Partial = {}) { + super({ ...data, type: ComponentType.TextInput }); + } + + setStyle(style: TextInputStyle): this { + this.data.style = style; + return this; + } + + setLabel(label: string): this { + this.data.label = label; + return this; + } + + setPlaceholder(placeholder: string): this { + this.data.placeholder = placeholder; + return this; + } + + setLength({ max, min }: Partial): this { + this.data.max_length = max; + this.data.min_length = min; + return this; + } + + setCustomId(id: string): this { + this.data.custom_id = id; + return this; + } + + setValue(value: string): this { + this.data.value = value; + return this; + } + + setRequired(required = true): this { + this.data.required = required; + return this; + } +} diff --git a/packages/helpers/src/components/index.ts b/packages/helpers/src/components/index.ts new file mode 100644 index 0000000..93cc1d4 --- /dev/null +++ b/packages/helpers/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './ActionRow'; +export * from './MessageButton'; +export * from './SelectMenu'; +export * from './TextInput'; diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index 692a0b0..bbb9a93 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1,16 +1,4 @@ -export * from './collectors'; - -/** Builders */ - -// Components -export * from './builders/components/InputTextBuilder'; -export * from './builders/components/MessageActionRowBuilder'; -export * from './builders/components/MessageButtonBuilder'; -export * from './builders/components/MessageSelectMenuBuilder'; - -// Slash -export * from './builders/slash/ApplicationCommand'; -export * from './builders/slash/ApplicationCommandOption'; - -// Embed -export * from './builders/embeds/embed-builder'; +export * from './MessageEmbed'; +export * from './components/index'; +export * from './Utils'; +export * from './commands'; diff --git a/packages/helpers/tsup.config.ts b/packages/helpers/tsup.config.ts index 2e8c9eb..dfbea63 100644 --- a/packages/helpers/tsup.config.ts +++ b/packages/helpers/tsup.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], minify: isProduction, - sourcemap: false, + sourcemap: false }); diff --git a/packages/rest/README.md b/packages/rest/README.md index ec23717..dddf1bb 100644 --- a/packages/rest/README.md +++ b/packages/rest/README.md @@ -16,13 +16,13 @@ yarn add @biscuitland/rest ## Example (Standalone rest) ```ts -import { DefaultRestAdapter } from "@biscuitland/rest"; +import { BiscuitREST } from "@biscuitland/rest"; import Fastify from "fastify"; -const manager = new DefaultRestAdapter({ - url: "http://localhost:port...", - token: "your token goes here", +const manager = new BiscuitREST({ + api: "http://any.rest.proxy/", version: 10, + token: "your token goes here" }); const app = Fastify({}); @@ -58,10 +58,8 @@ app.all("*", (req, reply) => { app.listen({ port: "port..." }); ``` -This package can be delivered through [unpkg](https://unpkg.com/) however is not tested yet - ## Links * [Website](https://biscuitjs.com/) * [Documentation](https://docs.biscuitjs.com/) * [Discord](https://discord.gg/XNw2RZFzaP) -* [core](https://www.npmjs.com/package/@biscuitland/core) | [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) \ No newline at end of file +* [core](https://www.npmjs.com/package/@biscuitland/core) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) diff --git a/packages/rest/package.json b/packages/rest/package.json index 9e047e3..eb0c5d8 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,74 +1,74 @@ { - "name": "@biscuitland/rest", - "version": "2.3.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": [ - "dist/**" - ], - "scripts": { - "build": "tsup", - "clean": "rm -rf dist && rm -rf .turbo", - "dev": "tsup --watch" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "require": "./dist/index.js" - } - }, - "dependencies": { - "@biscuitland/api-types": "^2.3.0" - }, - "devDependencies": { - "tsup": "^6.1.3" - }, - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - }, - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "publishConfig": { - "access": "public" - } + "name": "@biscuitland/rest", + "version": "3.0.0", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist && rm -rf .turbo", + "dev": "tsup --watch" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": "./dist/index.js" + } + }, + "devDependencies": { + "tsup": "^6.1.3" + }, + "license": "Apache-2.0", + "author": "Yuzuru ", + "contributors": [ + { + "name": "Yuzuru", + "url": "https://github.com/yuzudev" + }, + { + "name": "miia", + "url": "https://github.com/dragurimu" + }, + { + "name": "n128", + "url": "https://github.com/nicolito128" + }, + { + "name": "socram03", + "url": "https://github.com/socram03", + "author": true + }, + { + "name": "Drylozu", + "url": "https://github.com/Drylozu" + } + ], + "homepage": "https://biscuitjs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/oasisjs/biscuit.git" + }, + "bugs": { + "url": "https://github.com/oasisjs/biscuit" + }, + "keywords": [ + "api", + "discord", + "bots", + "typescript", + "botdev" + ], + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@discordjs/rest": "^1.5.0" + } } diff --git a/packages/rest/src/CDN.ts b/packages/rest/src/CDN.ts new file mode 100644 index 0000000..1b078fb --- /dev/null +++ b/packages/rest/src/CDN.ts @@ -0,0 +1,68 @@ +export interface CDNRoutes { + embed: { + avatars: { + get(embed: UserAvatarDefault): string; + }; + }; + avatars(id: string): { + get(hash: string): string; + }; + icons(guildId: string): { + get(hash: string): string; + }; + splashes(guildId: string): { + get(hash: string): string; + }; + 'discovery-splashes'(guidId: string): { + get(hash: string): string; + }; + banners(id: string): { + get(hash: string): string; + }; + guilds(id: string): { + users(id: string): { + avatars(hash: string): { + get(): string; + }; + banners(hash: string): { + get(): string; + }; + }; + }; + emojis(id: string): { + get(): string; + }; + appIcons(appId: string): { + get(iconOrCover: string): string; + }; + 'app-assets'(appId: string): { + get(asset: string): string; + achievements(id: string): { + icons(hash: string): { + get(): string; + }; + }; + }; + 'team-icons'(teamId: string): { + get(hash: string): string; + }; + stickers(id: string): { + get(): string; + }; + 'role-icons'(roleId: string): { + get(icon: string): string; + }; + 'guild-events'(id: string): { + get(cover: string): string; + }; +} + +export interface CDNRoutes { + 'app-assets'(id: '710982414301790216'): { + store(packBannerId: string): { + get(): string; + }; + }; +} + +export type UserAvatarDefault = 1 | 2 | 3 | 4 | 5 | number; diff --git a/packages/rest/src/REST.ts b/packages/rest/src/REST.ts new file mode 100644 index 0000000..f558ffe --- /dev/null +++ b/packages/rest/src/REST.ts @@ -0,0 +1,82 @@ +import type { RawFile, RequestData } from '@discordjs/rest'; +import { REST } from '@discordjs/rest'; +import type { Identify } from '@biscuitland/common'; +import type { RequestMethod } from './Router'; +export class BiscuitREST { + api: REST; + constructor(public options: BiscuitRESTOptions) { + const { token, ...restOptions } = this.options; + this.api = new REST(restOptions).setToken(token); + } + + async get(route: string, options?: RequestObject): Promise { + const data = await this.api.get(route as `/${string}`, { + ...options, + query: options?.query ? new URLSearchParams(options.query) : undefined + }); + + return data as T; + } + + async post(route: string, body?: RequestObject): Promise { + const data = await this.api.post(route as `/${string}`, { + ...body, + body: body?.body, + query: body?.query ? new URLSearchParams(body.query) : undefined, + files: body?.files + }); + + return data as T; + } + + async put(route: string, body?: RequestObject): Promise { + const data = await this.api.put(route as `/${string}`, { + ...body, + body: body?.body, + query: body?.query ? new URLSearchParams(body.query) : undefined, + files: body?.files + }); + + return data as T; + } + + async patch(route: string, body?: RequestObject): Promise { + const data = await this.api.patch(route as `/${string}`, { + ...body, + body: body?.body, + query: body?.query ? new URLSearchParams(body.query) : undefined, + files: body?.files + }); + + return data as T; + } + + async delete(route: string, options?: RequestObject): Promise { + const data = await this.api.delete(route as `/${string}`, { + ...options, + query: options?.query ? new URLSearchParams(options.query) : undefined + }); + + return data as T; + } +} + +export type BiscuitRESTOptions = Identify[0] & { token: string }>; + +export type RequestOptions = Pick; + +export type RequestObject, Q = Record> = { + query?: Q; +} & RequestOptions & + (M extends `${RequestMethod.Get}` + ? unknown + : { + body?: B; + files?: RawFile[]; + }); + +export type RestArguments = any> = M extends RequestMethod.Get + ? Q extends never + ? RequestObject + : never + : RequestObject; diff --git a/packages/rest/src/Router.ts b/packages/rest/src/Router.ts new file mode 100644 index 0000000..509f6cf --- /dev/null +++ b/packages/rest/src/Router.ts @@ -0,0 +1,68 @@ +import { CDN_URL } from '@biscuitland/common'; +import type { CDNRoutes, Routes } from './'; +import { BiscuitREST } from './REST'; + +export enum RequestMethod { + Delete = 'delete', + Get = 'get', + Patch = 'patch', + Post = 'post', + Put = 'put' +} + +const ArrRequestsMethods = Object.values(RequestMethod) as string[]; + +export class Router { + noop = () => { + return; + }; + + constructor(private rest: BiscuitREST) {} + + createProxy(route = [] as string[]): Routes { + return new Proxy(this.noop, { + get: (_, key: string) => { + if (ArrRequestsMethods.includes(key)) { + return (...options: any[]) => this.rest[key](`/${route.join('/')}`, ...options); + } + route.push(key); + return this.createProxy(route); + }, + apply: (...[, _, args]) => { + route.push(...args.filter((x) => x != null)); + return this.createProxy(route); + } + }) as unknown as Routes; + } +} + +export class CDN { + static createProxy(route = [] as string[]): CDNRoutes { + const noop = () => { + return; + }; + return new Proxy(noop, { + get: (_, key: string) => { + if (key === 'get') { + return (value?: string) => { + const lastRoute = `${CDN_URL}/${route.join('/')}`; + if (value) { + if (typeof value !== 'string') { + // rome-ignore lint/style/noParameterAssign: ?! + value = String(value); + } + return `${lastRoute}/${value}`; + } + return lastRoute; + }; + } + route.push(key); + return this.createProxy(route); + }, + apply: (...[, _, args]) => { + route.push(...args.filter((x) => x != null)); + return this.createProxy(route); + } + }) as unknown as CDNRoutes; + } +} diff --git a/packages/rest/src/Routes/applications.ts b/packages/rest/src/Routes/applications.ts new file mode 100644 index 0000000..77ce2b9 --- /dev/null +++ b/packages/rest/src/Routes/applications.ts @@ -0,0 +1,89 @@ +import { + RESTGetAPIApplicationCommandResult, + RESTGetAPIApplicationCommandsQuery, + RESTGetAPIApplicationCommandsResult, + RESTGetAPIApplicationGuildCommandResult, + RESTGetAPIApplicationGuildCommandsQuery, + RESTGetAPIApplicationGuildCommandsResult, + RESTGetAPIApplicationRoleConnectionMetadataResult, + RESTGetAPIGuildApplicationCommandsPermissionsResult, + RESTPatchAPIApplicationCommandJSONBody, + RESTPatchAPIApplicationCommandResult, + RESTPatchAPIApplicationGuildCommandJSONBody, + RESTPatchAPIApplicationGuildCommandResult, + RESTPostAPIApplicationCommandsJSONBody, + RESTPostAPIApplicationCommandsResult, + RESTPostAPIApplicationGuildCommandsJSONBody, + RESTPostAPIApplicationGuildCommandsResult, + RESTPutAPIApplicationCommandPermissionsJSONBody, + RESTPutAPIApplicationCommandsJSONBody, + RESTPutAPIApplicationCommandsResult, + RESTPutAPIApplicationGuildCommandsJSONBody, + RESTPutAPIApplicationGuildCommandsResult, + RESTPutAPIApplicationRoleConnectionMetadataJSONBody, + RESTPutAPIApplicationRoleConnectionMetadataResult, + RESTPutAPIGuildApplicationCommandsPermissionsResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface ApplicationRoutes { + applications: { + (id: string): { + guilds: { + (id: string): { + commands: { + get( + args?: RestArguments + ): Promise; + post( + args: RestArguments + ): Promise; + put( + args: RestArguments + ): Promise; + permissions: { + get(args?: RestArguments): Promise; + // put(args: RestArguments): Promise + }; + (id: string): { + get(args?: RestArguments): Promise; + patch( + args: RestArguments + ): Promise; + delete(args?: RestArguments): Promise; + permissions: { + get(args?: RestArguments): Promise; + put( + args: RestArguments + ): Promise; + }; + }; + }; + }; + }; + commands: { + get(args?: RestArguments): Promise; + post( + args: RestArguments + ): Promise; + put(args: RestArguments): Promise; + (id: string): { + get(args?: RestArguments): Promise; + patch( + args: RestArguments + ): Promise; + delete(args?: RestArguments): Promise; + }; + }; + 'role-connections': { + metadata: { + get(args?: RestArguments): Promise; + put( + args: RestArguments + ): Promise; + }; + }; + }; + }; +} diff --git a/packages/rest/src/Routes/channels.ts b/packages/rest/src/Routes/channels.ts new file mode 100644 index 0000000..5ee0992 --- /dev/null +++ b/packages/rest/src/Routes/channels.ts @@ -0,0 +1,218 @@ +import { + RESTDeleteAPIChannelAllMessageReactionsResult, + RESTDeleteAPIChannelMessageReactionResult, + RESTDeleteAPIChannelMessageResult, + RESTDeleteAPIChannelPermissionResult, + RESTDeleteAPIChannelPinResult, + RESTDeleteAPIChannelRecipientResult, + RESTDeleteAPIChannelResult, + RESTDeleteAPIChannelThreadMembersResult, + RESTGetAPIChannelInvitesResult, + RESTGetAPIChannelMessageReactionUsersQuery, + RESTGetAPIChannelMessageReactionUsersResult, + RESTGetAPIChannelMessageResult, + RESTGetAPIChannelMessagesQuery, + RESTGetAPIChannelMessagesResult, + RESTGetAPIChannelPinsResult, + RESTGetAPIChannelResult, + RESTGetAPIChannelThreadMemberQuery, + RESTGetAPIChannelThreadMemberResult, + RESTGetAPIChannelThreadMembersQuery, + RESTGetAPIChannelThreadMembersResult, + RESTGetAPIChannelThreadsArchivedPrivateResult, + RESTGetAPIChannelThreadsArchivedPublicResult, + RESTGetAPIChannelThreadsArchivedQuery, + RESTGetAPIChannelUsersThreadsArchivedResult, + RESTGetAPIGuildWebhooksResult, + RESTPatchAPIChannelJSONBody, + RESTPatchAPIChannelMessageJSONBody, + RESTPatchAPIChannelMessageResult, + RESTPatchAPIChannelResult, + RESTPostAPIChannelFollowersJSONBody, + RESTPostAPIChannelFollowersResult, + RESTPostAPIChannelInviteJSONBody, + RESTPostAPIChannelInviteResult, + RESTPostAPIChannelMessageCrosspostResult, + RESTPostAPIChannelMessageJSONBody, + RESTPostAPIChannelMessageResult, + RESTPostAPIChannelMessagesBulkDeleteJSONBody, + RESTPostAPIChannelMessagesBulkDeleteResult, + RESTPostAPIChannelMessagesThreadsJSONBody, + RESTPostAPIChannelMessagesThreadsResult, + RESTPostAPIChannelThreadsJSONBody, + RESTPostAPIChannelThreadsResult, + RESTPostAPIChannelTypingResult, + RESTPostAPIChannelWebhookJSONBody, + RESTPostAPIChannelWebhookResult, + RESTPostAPIGuildForumThreadsJSONBody, + RESTPutAPIChannelMessageReactionResult, + RESTPutAPIChannelPermissionJSONBody, + RESTPutAPIChannelPermissionResult, + RESTPutAPIChannelPinResult, + RESTPutAPIChannelRecipientJSONBody, + RESTPutAPIChannelRecipientResult, + RESTPutAPIChannelThreadMembersResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface ChannelRoutes { + channels(id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + users: { + (id: '@me'): { + threads: { + archived: { + private: { + //. + get( + args?: RestArguments + ): Promise; + }; + }; + }; + }; + }; + 'thread-members': { + //. + get(args?: RestArguments): Promise; + (id: '@me'): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + threads: { + //. + post( + args: RestArguments + ): Promise; + archived: { + public: { + //. + get( + args?: RestArguments + ): Promise; + }; + private: { + //. + get( + args?: RestArguments + ): Promise; + }; + }; + }; + recipients: { + (id: string): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + pins: { + //. + get(args?: RestArguments): Promise; + (id: string): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + followers: { + //. + post(args: RestArguments): Promise; + }; + permissions: { + (id: string): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + invites: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + }; + messages: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + 'bulk-delete': { + //. + post( + args: RestArguments + ): Promise; + }; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + threads: { + //. + post( + args: RestArguments + ): Promise; + }; + crosspost: { + //. + post(args: RestArguments): Promise; + }; + reactions: { + //. + delete(args?: RestArguments): Promise; + (emoji: string): { + //. + get( + args?: RestArguments + ): Promise; + //. + delete(args?: RestArguments): Promise; + (id: '@me'): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + (id: string): { + //. + delete(args?: RestArguments): Promise; + }; + }; + }; + }; + }; + typing: { + //. + post(args?: RestArguments): Promise; + }; + webhooks: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + }; + }; +} diff --git a/packages/rest/src/Routes/gateway.ts b/packages/rest/src/Routes/gateway.ts new file mode 100644 index 0000000..68bf446 --- /dev/null +++ b/packages/rest/src/Routes/gateway.ts @@ -0,0 +1,12 @@ +import { RESTGetAPIGatewayResult, RESTGetAPIGatewayBotResult } from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface GatewayRoutes { + gateway: { + get(args?: RestArguments): Promise; + bot: { + get(args?: RestArguments): Promise; + }; + }; +} diff --git a/packages/rest/src/Routes/guilds.ts b/packages/rest/src/Routes/guilds.ts new file mode 100644 index 0000000..6b87e80 --- /dev/null +++ b/packages/rest/src/Routes/guilds.ts @@ -0,0 +1,381 @@ +import { + APIThreadChannel, + Identify, + RESTDeleteAPIAutoModerationRuleResult, + RESTDeleteAPIGuildBanResult, + RESTDeleteAPIGuildEmojiResult, + RESTDeleteAPIGuildIntegrationResult, + RESTDeleteAPIGuildMemberResult, + RESTDeleteAPIGuildMemberRoleResult, + RESTDeleteAPIGuildResult, + RESTDeleteAPIGuildRoleResult, + RESTDeleteAPIGuildScheduledEventResult, + RESTDeleteAPIGuildStickerResult, + RESTDeleteAPIGuildTemplateResult, + RESTGetAPIAuditLogQuery, + RESTGetAPIAuditLogResult, + RESTGetAPIAutoModerationRuleResult, + RESTGetAPIAutoModerationRulesResult, + RESTGetAPIGuildBanResult, + RESTGetAPIGuildBansQuery, + RESTGetAPIGuildBansResult, + RESTGetAPIGuildChannelsResult, + RESTGetAPIGuildEmojiResult, + RESTGetAPIGuildEmojisResult, + RESTGetAPIGuildIntegrationsResult, + RESTGetAPIGuildInvitesResult, + RESTGetAPIGuildMemberResult, + RESTGetAPIGuildMembersQuery, + RESTGetAPIGuildMembersResult, + RESTGetAPIGuildMembersSearchQuery, + RESTGetAPIGuildMembersSearchResult, + RESTGetAPIGuildPreviewResult, + RESTGetAPIGuildPruneCountQuery, + RESTGetAPIGuildPruneCountResult, + RESTGetAPIGuildQuery, + RESTGetAPIGuildResult, + RESTGetAPIGuildRolesResult, + RESTGetAPIGuildScheduledEventQuery, + RESTGetAPIGuildScheduledEventResult, + RESTGetAPIGuildScheduledEventUsersQuery, + RESTGetAPIGuildScheduledEventUsersResult, + RESTGetAPIGuildScheduledEventsQuery, + RESTGetAPIGuildScheduledEventsResult, + RESTGetAPIGuildStickerResult, + RESTGetAPIGuildStickersResult, + RESTGetAPIGuildTemplatesResult, + RESTGetAPIGuildThreadsResult, + RESTGetAPIGuildVanityUrlResult, + RESTGetAPIGuildVoiceRegionsResult, + RESTGetAPIGuildWebhooksResult, + RESTGetAPIGuildWelcomeScreenResult, + RESTGetAPIGuildWidgetImageQuery, + RESTGetAPIGuildWidgetImageResult, + RESTGetAPIGuildWidgetJSONResult, + RESTGetAPIGuildWidgetSettingsResult, + RESTGetAPITemplateResult, + RESTPatchAPIAutoModerationRuleJSONBody, + RESTPatchAPIAutoModerationRuleResult, + RESTPatchAPICurrentGuildMemberJSONBody, + RESTPatchAPIGuildChannelPositionsJSONBody, + RESTPatchAPIGuildChannelPositionsResult, + RESTPatchAPIGuildEmojiJSONBody, + RESTPatchAPIGuildEmojiResult, + RESTPatchAPIGuildJSONBody, + RESTPatchAPIGuildMemberJSONBody, + RESTPatchAPIGuildMemberResult, + RESTPatchAPIGuildResult, + RESTPatchAPIGuildRoleJSONBody, + RESTPatchAPIGuildRolePositionsJSONBody, + RESTPatchAPIGuildRolePositionsResult, + RESTPatchAPIGuildRoleResult, + RESTPatchAPIGuildScheduledEventJSONBody, + RESTPatchAPIGuildScheduledEventResult, + RESTPatchAPIGuildStickerJSONBody, + RESTPatchAPIGuildStickerResult, + RESTPatchAPIGuildTemplateJSONBody, + RESTPatchAPIGuildTemplateResult, + RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody, + RESTPatchAPIGuildVoiceStateCurrentMemberResult, + RESTPatchAPIGuildVoiceStateUserJSONBody, + RESTPatchAPIGuildVoiceStateUserResult, + RESTPatchAPIGuildWelcomeScreenJSONBody, + RESTPatchAPIGuildWelcomeScreenResult, + RESTPatchAPIGuildWidgetSettingsJSONBody, + RESTPatchAPIGuildWidgetSettingsResult, + RESTPostAPIAutoModerationRuleJSONBody, + RESTPostAPIAutoModerationRuleResult, + RESTPostAPIGuildChannelJSONBody, + RESTPostAPIGuildChannelResult, + RESTPostAPIGuildEmojiJSONBody, + RESTPostAPIGuildEmojiResult, + RESTPostAPIGuildPruneJSONBody, + RESTPostAPIGuildPruneResult, + RESTPostAPIGuildRoleJSONBody, + RESTPostAPIGuildRoleResult, + RESTPostAPIGuildScheduledEventJSONBody, + RESTPostAPIGuildScheduledEventResult, + RESTPostAPIGuildStickerFormDataBody, + RESTPostAPIGuildStickerResult, + RESTPostAPIGuildTemplatesJSONBody, + RESTPostAPIGuildTemplatesResult, + RESTPostAPIGuildsJSONBody, + RESTPostAPIGuildsMFAJSONBody, + RESTPostAPIGuildsMFAResult, + RESTPostAPIGuildsResult, + RESTPostAPITemplateCreateGuildJSONBody, + RESTPostAPITemplateCreateGuildResult, + RESTPutAPIGuildBanJSONBody, + RESTPutAPIGuildBanResult, + RESTPutAPIGuildMemberJSONBody, + RESTPutAPIGuildMemberResult, + RESTPutAPIGuildMemberRoleResult, + RESTPutAPIGuildTemplateSyncResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface GuildRoutes { + guilds: { + //. + post(args: RestArguments): Promise; + templates(code: string): { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + }; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + webhooks: { + //. + get(args?: RestArguments): Promise; + }; + preview: { + //. + get(args?: RestArguments): Promise; + }; + 'audit-logs': { + //. + get(args?: RestArguments): Promise; + }; + 'auto-moderation': { + rules: { + //. + get(args?: RestArguments): Promise; + //. + post( + args: RestArguments + ): Promise; + //. + patch( + args: RestArguments + ): Promise; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + }; + channels: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + //. + patch( + args: RestArguments + ): Promise; + }; + members: { + //. + get(args?: RestArguments): Promise; + search: { + //. + get(args: RestArguments): Promise; + }; + '@me': { + //. + patch(args: RestArguments): Promise; + }; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + put(args: RestArguments): Promise; + //. + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + roles(id: string): { + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + }; + threads: { + active: { + //. + get( + args?: RestArguments + ): Promise }>>; + }; + }; + roles: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + //. + patch( + args: RestArguments + ): Promise; + (id: string): { + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + bans: { + //. + get(args: RestArguments): Promise; + (userId: string): { + //. + get(args?: RestArguments): Promise; + //. + put(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + mfa: { + //. + post(args: RestArguments): Promise; + }; + prune: { + //. + get(args: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + }; + regions: { + //. + get(args?: RestArguments): Promise; + }; + invites: { + //. + get(args?: RestArguments): Promise; + }; + widget: { + //. + get(args?: RestArguments): Promise; + //. + patch( + args: RestArguments + ): Promise; + }; + 'widget.json': { + //. + get(args?: RestArguments): Promise; + }; + 'widget.png': { + //. + get(args?: RestArguments): Promise; + }; + integrations: { + //. + get(args?: RestArguments): Promise; + (id: string): { + //. + delete(args?: RestArguments): Promise; + }; + }; + 'vanity-url': { + //. + get(args?: RestArguments): Promise; + }; + 'welcome-screen': { + //. + get(args?: RestArguments): Promise; + //. + patch( + args: RestArguments + ): Promise; + }; + // onboarding: { + // get(args:RestArguments); + // } + emojis: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + 'voice-states': { + '@me': { + //. + patch( + args: RestArguments + ): Promise; + }; + (id: string): { + //. + patch( + args: RestArguments + ): Promise; + }; + }; + stickers: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + 'scheduled-events': { + //. + get(args?: RestArguments): Promise; + //. + post( + args: RestArguments + ): Promise; + (id: string): { + //. + get(args?: RestArguments): Promise; + //. + patch( + args: RestArguments + ): Promise; + //. + delete(args?: RestArguments): Promise; + users: { + //. + get( + args?: RestArguments + ): Promise; + }; + }; + }; + templates: { + //. + get(args?: RestArguments): Promise; + //. + post(args: RestArguments): Promise; + (code: string): { + //. + put(args: RestArguments): Promise; + //. + patch(args: RestArguments): Promise; + //. + delete(args?: RestArguments): Promise; + }; + }; + }; + }; +} diff --git a/packages/rest/src/Routes/index.ts b/packages/rest/src/Routes/index.ts new file mode 100644 index 0000000..260da9b --- /dev/null +++ b/packages/rest/src/Routes/index.ts @@ -0,0 +1,23 @@ +import { ApplicationRoutes } from './applications'; +import { ChannelRoutes } from './channels'; +import { GatewayRoutes } from './gateway'; +import { GuildRoutes } from './guilds'; +import { InteractionRoutes } from './interactions'; +import { InviteRoutes } from './invites'; +import { StageInstanceRoutes } from './stage-instances'; +import { StickerRoutes } from './stickers'; +import { UserRoutes } from './users'; +import { VoiceRoutes } from './voice'; +import { WebhookRoutes } from './webhooks'; + +export type Routes = ApplicationRoutes & + ChannelRoutes & + GatewayRoutes & + GuildRoutes & + InteractionRoutes & + InviteRoutes & + StageInstanceRoutes & + StickerRoutes & + UserRoutes & + VoiceRoutes & + WebhookRoutes; diff --git a/packages/rest/src/Routes/interactions.ts b/packages/rest/src/Routes/interactions.ts new file mode 100644 index 0000000..58ed063 --- /dev/null +++ b/packages/rest/src/Routes/interactions.ts @@ -0,0 +1,15 @@ +import { RESTPostAPIInteractionCallbackJSONBody } from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface InteractionRoutes { + interactions: { + (id: string): { + (token: string): { + callback: { + post(args: RestArguments): Promise; + }; + }; + }; + }; +} diff --git a/packages/rest/src/Routes/invites.ts b/packages/rest/src/Routes/invites.ts new file mode 100644 index 0000000..f8edea7 --- /dev/null +++ b/packages/rest/src/Routes/invites.ts @@ -0,0 +1,10 @@ +import { RESTDeleteAPIInviteResult, RESTGetAPIInviteQuery, RESTGetAPIInviteResult } from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface InviteRoutes { + invites(id: string): { + get(args?: RestArguments): Promise; + delete(args?: RestArguments): Promise; + }; +} diff --git a/packages/rest/src/Routes/stage-instances.ts b/packages/rest/src/Routes/stage-instances.ts new file mode 100644 index 0000000..77b5281 --- /dev/null +++ b/packages/rest/src/Routes/stage-instances.ts @@ -0,0 +1,21 @@ +import { + RESTDeleteAPIStageInstanceResult, + RESTGetAPIStageInstanceResult, + RESTPatchAPIStageInstanceJSONBody, + RESTPatchAPIStageInstanceResult, + RESTPostAPIStageInstanceJSONBody, + RESTPostAPIStageInstanceResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface StageInstanceRoutes { + 'stage-instances': { + post(args: RestArguments): Promise; + (id: string): { + get(args?: RestArguments): Promise; + patch(args: RestArguments): Promise; + delete(args?: RestArguments): Promise; + }; + }; +} diff --git a/packages/rest/src/Routes/stickers.ts b/packages/rest/src/Routes/stickers.ts new file mode 100644 index 0000000..9c0b980 --- /dev/null +++ b/packages/rest/src/Routes/stickers.ts @@ -0,0 +1,12 @@ +import { RESTGetAPIStickerResult, RESTGetNitroStickerPacksResult } from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface StickerRoutes { + stickers(id: string): { + get(args?: RestArguments): Promise; + }; + 'sticker-packs': { + get(args?: RestArguments): Promise; + }; +} diff --git a/packages/rest/src/Routes/users.ts b/packages/rest/src/Routes/users.ts new file mode 100644 index 0000000..b21780a --- /dev/null +++ b/packages/rest/src/Routes/users.ts @@ -0,0 +1,53 @@ +import { + APIDMChannel, + RESTDeleteAPIGuildResult, + RESTGetAPICurrentUserApplicationRoleConnectionResult, + RESTGetAPICurrentUserConnectionsResult, + RESTGetAPICurrentUserGuildsQuery, + RESTGetAPICurrentUserGuildsResult, + RESTGetAPICurrentUserResult, + RESTGetAPIUserResult, + RESTGetCurrentUserGuildMemberResult, + RESTPatchAPICurrentUserJSONBody, + RESTPatchAPICurrentUserResult, + RESTPostAPICurrentUserCreateDMChannelJSONBody, + RESTPutAPICurrentUserApplicationRoleConnectionJSONBody, + RESTPutAPICurrentUserApplicationRoleConnectionResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface UserRoutes { + users: { + (id: string): { + get(args?: RestArguments): Promise; + }; + (id: '@me'): { + get(args?: RestArguments): Promise; + patch(args: RestArguments): Promise; + guilds: { + get(args?: RestArguments): Promise; + (id: string): { + member: { + get(args?: RestArguments): Promise; + }; + delete(args?: RestArguments): Promise; + }; + }; + channels: { + post(args: RestArguments): Promise; + }; + connections: { + get(args?: RestArguments): Promise; + }; + applications(applicationId: string): { + 'role-connection': { + get(args?: RestArguments): Promise; + put( + args: RestArguments + ): Promise; + }; + }; + }; + }; +} diff --git a/packages/rest/src/Routes/voice.ts b/packages/rest/src/Routes/voice.ts new file mode 100644 index 0000000..adb4817 --- /dev/null +++ b/packages/rest/src/Routes/voice.ts @@ -0,0 +1,11 @@ +import { RESTGetAPIVoiceRegionsResult } from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface VoiceRoutes { + voice: { + region: { + get(args?: RestArguments): Promise; + }; + }; +} diff --git a/packages/rest/src/Routes/webhooks.ts b/packages/rest/src/Routes/webhooks.ts new file mode 100644 index 0000000..d82bc2f --- /dev/null +++ b/packages/rest/src/Routes/webhooks.ts @@ -0,0 +1,61 @@ +import { + RESTDeleteAPIWebhookResult, + RESTDeleteAPIWebhookWithTokenMessageResult, + RESTDeleteAPIWebhookWithTokenResult, + RESTGetAPIWebhookResult, + RESTGetAPIWebhookWithTokenMessageResult, + RESTGetAPIWebhookWithTokenResult, + RESTPatchAPIWebhookJSONBody, + RESTPatchAPIWebhookResult, + RESTPatchAPIWebhookWithTokenJSONBody, + RESTPatchAPIWebhookWithTokenMessageJSONBody, + RESTPatchAPIWebhookWithTokenMessageResult, + RESTPatchAPIWebhookWithTokenResult, + RESTPostAPIWebhookWithTokenGitHubQuery, + RESTPostAPIWebhookWithTokenGitHubResult, + RESTPostAPIWebhookWithTokenGitHubWaitResult, + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery, + RESTPostAPIWebhookWithTokenResult, + RESTPostAPIWebhookWithTokenSlackQuery, + RESTPostAPIWebhookWithTokenSlackResult, + RESTPostAPIWebhookWithTokenSlackWaitResult, + RESTPostAPIWebhookWithTokenWaitResult +} from '@biscuitland/common'; +import { RestArguments } from '../REST'; +import { RequestMethod } from '../Router'; + +export interface WebhookRoutes { + webhooks(id: string): { + get(args?: RestArguments): Promise; + patch(args: RestArguments): Promise; + delete(args?: RestArguments): Promise; + (token: string): { + get(args?: RestArguments): Promise; + patch(args: RestArguments): Promise; + delete(args?: RestArguments): Promise; + post( + args: RestArguments + ): Promise; + slack: { + post( + args: RestArguments + ): Promise; + }; + github: { + post( + args: RestArguments + ): Promise; + }; + messages: { + (id: string | '@original'): { + get(args?: RestArguments): Promise; + patch( + args: RestArguments + ): Promise; + delete(args?: RestArguments): Promise; + }; + }; + }; + }; +} diff --git a/packages/rest/src/adapters/default-rest-adapter.ts b/packages/rest/src/adapters/default-rest-adapter.ts deleted file mode 100644 index 09f99aa..0000000 --- a/packages/rest/src/adapters/default-rest-adapter.ts +++ /dev/null @@ -1,807 +0,0 @@ -import type { - CreateRequestBodyOptions, - RestAdapter, - RestPayload, - RestRateLimitedPath, - RestRequest, - RestRequestRejection, - RestRequestResponse, - RestSendRequestOptions, -} from './rest-adapter'; - -import type { FileContent } from '@biscuitland/api-types'; - -import { Constants, HTTPResponseCodes } from '@biscuitland/api-types'; - -export class DefaultRestAdapter implements RestAdapter { - static readonly DEFAULTS = { - url: '', - - version: Constants.API_VERSION, - - maxRetryCount: 10, - }; - - options: Options; - - /** current invalid amount */ - protected invalidRequests = 0; - - /** max invalid requests allowed until ban */ - protected maxInvalidRequests = 10000; - - /** 10 minutes */ - protected invalidRequestsInterval = 600000; - - /** timer to reset to 0 */ - protected invalidRequestsTimeoutId = 0; - - /** how safe to be from max */ - protected invalidRequestsSafetyAmount = 1; - - /** when first request in this period was made */ - protected invalidRequestErrorStatuses = [401, 403, 429]; - - protected processingQueue = false; - - protected processingRateLimitedPaths = false; - - protected globallyRateLimited = false; - - protected globalQueueProcessing = false; - - private rateLimitedPaths = new Map(); - - private globalQueue = [] as { - request: RestRequest; - payload: RestPayload; - basicURL: string; - urlToUse: string; - }[]; - - private pathQueues = new Map< - string, - { - isWaiting: boolean; - requests: { - request: RestRequest; - payload: RestPayload; - }[]; - } - >(); - - private url: string; - - constructor(options: DefaultRestOptions) { - this.options = Object.assign({}, DefaultRestAdapter.DEFAULTS, options); - - if (this.options.url) { - this.url = `${options.url}/v${this.options.version}`; - } else { - this.url = `${Constants.BASE_URL}/v${this.options.version}`; - } - } - - /** - * @inheritDoc - */ - - async get( - route: string, - body?: any, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise { - const url = route[0] === '/' ? `${this.url}${route}` : route; - - return new Promise((resolve, reject) => { - this.processRequest( - { - url, - method: 'GET', - reject: (data: RestRequestRejection) => { - const restError = this.convertRestError( - new Error('Location:'), - data - ); - reject(restError); - }, - resolve: (data: RestRequestResponse) => - resolve( - data.status !== 204 - ? JSON.parse(data.body ?? '{}') - : (undefined as unknown as T) - ), - }, - { - bucketId: options?.bucketId, - body: body as Record | undefined, - retryCount: options?.retryCount ?? 0, - headers: options?.headers, - } - ); - }); - } - - /** - * @inheritDoc - */ - - async put( - route: string, - body?: any, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise { - const url = route[0] === '/' ? `${this.url}${route}` : route; - - return new Promise((resolve, reject) => { - this.processRequest( - { - url, - method: 'PUT', - reject: (data: RestRequestRejection) => { - const restError = this.convertRestError( - new Error('Location:'), - data - ); - reject(restError); - }, - resolve: (data: RestRequestResponse) => - resolve( - data.status !== 204 - ? JSON.parse(data.body ?? '{}') - : (undefined as unknown as T) - ), - }, - { - bucketId: options?.bucketId, - body: body as Record | undefined, - retryCount: options?.retryCount ?? 0, - headers: options?.headers, - } - ); - }); - } - - /** - * @inheritDoc - */ - - async post( - route: string, - body?: any, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise { - const url = route[0] === '/' ? `${this.url}${route}` : route; - - return new Promise((resolve, reject) => { - this.processRequest( - { - url, - method: 'POST', - reject: err => reject(err), - resolve: (data: RestRequestResponse) => - resolve( - data.status !== 204 - ? JSON.parse(data.body ?? '{}') - : (undefined as unknown as T) - ), - }, - { - bucketId: options?.bucketId, - body: body as Record | undefined, - retryCount: options?.retryCount ?? 0, - headers: options?.headers, - } - ); - }); - } - - /** - * @inheritDoc - */ - - async patch( - route: string, - body?: any, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise { - const url = route[0] === '/' ? `${this.url}${route}` : route; - - return new Promise((resolve, reject) => { - this.processRequest( - { - url, - method: 'PATCH', - reject: (data: RestRequestRejection) => { - const restError = this.convertRestError( - new Error('Location:'), - data - ); - reject(restError); - }, - resolve: (data: RestRequestResponse) => - resolve( - data.status !== 204 - ? JSON.parse(data.body ?? '{}') - : (undefined as unknown as T) - ), - }, - { - bucketId: options?.bucketId, - body: body as Record | undefined, - retryCount: options?.retryCount ?? 0, - headers: options?.headers, - } - ); - }); - } - - /** - * @inheritDoc - */ - - async delete( - route: string, - body?: any, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise { - const url = route[0] === '/' ? `${this.url}${route}` : route; - return new Promise((resolve, reject) => { - this.processRequest( - { - url, - method: 'DELETE', - reject: (data: RestRequestRejection) => { - const restError = this.convertRestError( - new Error('Location:'), - data - ); - reject(restError); - }, - resolve: (data: any) => resolve(data.body ? JSON.parse(data.body) : {}), - }, - { - bucketId: options?.bucketId, - body: body as Record | undefined, - retryCount: options?.retryCount ?? 0, - headers: options?.headers, - } - ); - }); - } - - /** - * @inheritDoc - */ - - private async sendRequest(options: RestSendRequestOptions) { - try { - const response = await fetch( - new Request(options.url, { - method: options.method, - headers: options.payload?.headers, - body: options.payload?.body, - }) - ); - const bucketIdFromHeaders = this.processRequestHeaders( - options.url, - response.headers - ); - - if (bucketIdFromHeaders) { - options.bucketId = bucketIdFromHeaders; - } - - if (response.status === 204) { - options.resolve?.({ - ok: true, - status: 204, - }); - - return; - } - - if (response.status < 200 || response.status >= 400) { - let error = 'REQUEST_UNKNOWN_ERROR'; - - switch (response.status) { - case HTTPResponseCodes.BadRequest: - error = - "The options was improperly formatted, or the server couldn't understand it."; - break; - case HTTPResponseCodes.Unauthorized: - error = - 'The Authorization header was missing or invalid.'; - break; - case HTTPResponseCodes.Forbidden: - error = - 'The Authorization token you passed did not have permission to the resource.'; - break; - case HTTPResponseCodes.NotFound: - error = - "The resource at the location specified doesn't exist."; - break; - case HTTPResponseCodes.MethodNotAllowed: - error = - 'The HTTP method used is not valid for the location specified.'; - break; - case HTTPResponseCodes.GatewayUnavailable: - error = - 'There was not a gateway available to process your options. Wait a bit and retry.'; - break; - } - - if ( - this.invalidRequestErrorStatuses.includes( - response.status - ) && - !( - response.status === 429 && - response.headers.get('X-RateLimit-Scope') - ) - ) { - ++this.invalidRequests; - - if (!this.invalidRequestsTimeoutId) { - const it: any = setTimeout(() => { - this.invalidRequests = 0; - this.invalidRequestsTimeoutId = 0; - }, this.invalidRequestsInterval); - - this.invalidRequestsTimeoutId = it; - } - } - - if (response.status !== 429) { - options.reject?.({ - ok: false, - status: response.status, - error, - body: response.type - ? JSON.stringify(await response.json()) - : undefined, - }); - - throw new Error( - JSON.stringify({ - ok: false, - status: response.status, - error, - body: response.type - ? JSON.stringify(await response.json()) - : undefined, - }) - ); - } else { - if ( - options.retryCount && - options.retryCount++ >= this.options.maxRetryCount - ) { - options.reject?.({ - ok: false, - status: response.status, - error: 'The options was rate limited and it maxed out the retries limit.', - }); - - return; - } - } - } - - const json = JSON.stringify(await response.json()); - - options.resolve?.({ - ok: true, - status: 200, - body: json, - }); - - return JSON.parse(json); - } catch (error) { - options.reject?.({ - ok: false, - status: 599, - error: 'Internal Error', - }); - - throw new Error('Something went wrong in sendRequest'); - } - } - - /** - * @inheritDoc - */ - - private processRequest(request: RestRequest, payload: RestPayload) { - const queue = this.pathQueues.get(request.url); - - if (queue) { - queue.requests.push({ request, payload }); - } else { - this.pathQueues.set(request.url, { - isWaiting: false, - requests: [ - { - request, - payload, - }, - ], - }); - - this.processQueue(request.url); - } - } - - /** - * @inheritDoc - */ - - private processQueue(id: string) { - const queue = this.pathQueues.get(id); - - if (!queue) { - return; - } - - while (queue.requests.length) { - const request = queue.requests[0]; - - if (!request) { - break; - } - - const basicURL = request.request.url + '@' + request.request.method; - const urlResetIn = this.checkRateLimits(basicURL); - - if (urlResetIn) { - if (!queue.isWaiting) { - queue.isWaiting = true; - - setTimeout(() => { - queue.isWaiting = false; - - this.processQueue(id); - }, urlResetIn); - } - - break; - } - - const bucketResetIn = request.payload.bucketId - ? this.checkRateLimits(request.payload.bucketId) - : false; - - if (bucketResetIn) { - continue; - } - - this.globalQueue.push({ - ...request, - urlToUse: request.request.url, - basicURL, - }); - - this.processGlobalQueue(); - queue.requests.shift(); - } - - this.cleanupQueues(); - } - - /** - * @inheritDoc - */ - - private async processGlobalQueue() { - if (!this.globalQueue.length) { - return; - } - - if (this.globalQueueProcessing) { - return; - } - - this.globalQueueProcessing = true; - - while (this.globalQueue.length) { - if (this.globallyRateLimited) { - setTimeout(() => { - this.processGlobalQueue(); - }, 1000); - - break; - } - - if ( - this.invalidRequests === - this.maxInvalidRequests - this.invalidRequestsSafetyAmount - ) { - setTimeout(() => { - this.processGlobalQueue(); - }, 1000); - - break; - } - - const request = this.globalQueue.shift(); - - if (!request) { - continue; - } - - const urlResetIn = this.checkRateLimits(request.basicURL); - - const bucketResetIn = request.payload.bucketId - ? this.checkRateLimits(request.payload.bucketId) - : false; - - if (urlResetIn || bucketResetIn) { - setTimeout(() => { - this.globalQueue.unshift(request); - this.processGlobalQueue(); - }, urlResetIn || (bucketResetIn as number)); - - continue; - } - - await this.sendRequest({ - url: request.urlToUse, - method: request.request.method, - bucketId: request.payload.bucketId, - reject: request.request.reject, - resolve: request.request.resolve, - retryCount: request.payload.retryCount ?? 0, - payload: this.createRequestBody({ - method: request.request.method, - body: request.payload.body, - }), - }).catch(() => null); - } - - this.globalQueueProcessing = false; - } - - /** - * @inheritDoc - */ - - private processRequestHeaders(url: string, headers: Headers) { - let rateLimited = false; - - const remaining = headers.get('x-ratelimit-remaining'); - const retryAfter = headers.get('x-ratelimit-reset-after'); - const reset = Date.now() + Number(retryAfter) * 1000; - const global = headers.get('x-ratelimit-global'); - - const bucketId = headers.get('x-ratelimit-bucket') || undefined; - - if (remaining === '0') { - rateLimited = true; - - this.rateLimitedPaths.set(url, { - url, - resetTimestamp: reset, - bucketId, - }); - - if (bucketId) { - this.rateLimitedPaths.set(bucketId, { - url, - resetTimestamp: reset, - bucketId, - }); - } - } - - if (global) { - const retryAfter = headers.get('retry-after'); - const globalReset = Date.now() + Number(retryAfter) * 1000; - - this.globallyRateLimited = true; - rateLimited = true; - - this.rateLimitedPaths.set('global', { - url: 'global', - resetTimestamp: globalReset, - bucketId, - }); - - if (bucketId) { - this.rateLimitedPaths.set(bucketId, { - url: 'global', - resetTimestamp: globalReset, - bucketId, - }); - } - } - - if (rateLimited && !this.processingRateLimitedPaths) { - this.processRateLimitedPaths(); - } - - return rateLimited ? bucketId : undefined; - } - - /** - * @inheritDoc - */ - - private processRateLimitedPaths() { - const now = Date.now(); - - for (const [key, value] of this.rateLimitedPaths.entries()) { - if (value.resetTimestamp > now) { - continue; - } - - this.rateLimitedPaths.delete(key); - - if (key === 'global') { - this.globallyRateLimited = false; - } - } - - if (!this.rateLimitedPaths.size) { - this.processingRateLimitedPaths = false; - } else { - this.processingRateLimitedPaths = true; - - setTimeout(() => { - this.processRateLimitedPaths(); - }, 1000); - } - } - - /** - * @inheritDoc - */ - - private createRequestBody(options: CreateRequestBodyOptions) { - const headers: Record = { - 'user-agent': Constants.USER_AGENT, - }; - - if (!options.unauthorized) { - headers.authorization = `Bot ${this.options.token}`; - } - - if (options.headers) { - for (const key in options.headers) { - headers[key.toLowerCase()] = options.headers[key]; - } - } - - if (options.method === 'GET') { - options.body = undefined; - } - - if (options.body?.reason) { - headers['X-Audit-Log-Reason'] = encodeURIComponent( - options.body.reason as string - ); - options.body.reason = undefined; - } - - if (options.body?.file) { - if (!Array.isArray(options.body.file)) { - options.body.file = [options.body.file]; - } - - const form = new FormData(); - - for ( - let i = 0; - i < (options.body.file as FileContent[]).length; - i++ - ) { - form.append( - `file${i}`, - (options.body.file as FileContent[])[i].blob, - (options.body.file as FileContent[])[i].name - ); - } - - form.append( - 'payload_json', - JSON.stringify({ ...options.body, file: undefined }) - ); - options.body.file = form; - } else if ( - options.body && - !['GET', 'DELETE'].includes(options.method) - ) { - headers['Content-Type'] = 'application/json'; - } - - return { - headers, - body: (options.body?.file ?? JSON.stringify(options.body)) as - | FormData - | string, - method: options.method, - }; - } - - /** - * @inheritDoc - */ - - private checkRateLimits(url: string) { - const limited = this.rateLimitedPaths.get(url); - const global = this.rateLimitedPaths.get('global'); - - const now = Date.now(); - - if (limited && now < limited.resetTimestamp) { - return limited.resetTimestamp - now; - } - - if (global && now < global.resetTimestamp) { - return global.resetTimestamp - now; - } - - return false; - } - - /** - * @inheritDoc - */ - - private convertRestError(errorStack: Error, data: RestRequestRejection) { - errorStack.message = `[${data.status}] ${data.error}\n${data.body}`; - return errorStack; - } - - /** - * @inheritDoc - */ - - private cleanupQueues() { - for (const [key, queue] of this.pathQueues) { - if (queue.requests.length) { - continue; - } - - this.pathQueues.delete(key); - } - - if (!this.pathQueues.size) { - this.processingQueue = false; - } - } -} - -export type DefaultRestOptions = Pick< - Options, - Exclude -> & - Partial; - -export interface Options { - url: string; - - token: string; - version: number; - - maxRetryCount: number; -} diff --git a/packages/rest/src/adapters/rest-adapter.ts b/packages/rest/src/adapters/rest-adapter.ts deleted file mode 100644 index 95cedec..0000000 --- a/packages/rest/src/adapters/rest-adapter.ts +++ /dev/null @@ -1,125 +0,0 @@ -export interface RestRequest { - url: string; - method: RequestMethod; - reject: (payload: RestRequestRejection) => unknown; - resolve: (payload: RestRequestResponse) => unknown; -} - -export interface RestRequestResponse { - ok: boolean; - status: number; - body?: string; -} - -export interface RestRequestRejection extends RestRequestResponse { - error: string; -} - -export interface RestPayload { - bucketId?: string; - body?: Record; - retryCount: number; - headers?: Record; -} - -export interface RestRateLimitedPath { - url: string; - resetTimestamp: number; - bucketId?: string; -} - -export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - -export interface RestSendRequestOptions { - url: string; - method: RequestMethod; - bucketId?: string; - reject?: CallableFunction; - resolve?: CallableFunction; - retryCount?: number; - payload?: { - headers: Record; - body: string | FormData; - }; -} - -export interface CreateRequestBodyOptions { - headers?: Record; - method: RequestMethod; - body?: Record; - unauthorized?: boolean; -} - -export interface RestAdapter { - options: any; - - /** - * @inheritDoc - */ - - get( - route: string, - data?: unknown, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise; - - /** - * @inheritDoc - */ - - put( - router: string, - data: unknown, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise; - - /** - * @inheritDoc - */ - - post( - router: string, - data: unknown, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise; - - /** - * @inheritDoc - */ - - patch( - router: string, - data: unknown, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise; - - /** - * @inheritDoc - */ - - delete( - router: string, - data?: unknown, - options?: { - retryCount?: number; - bucketId?: string; - headers?: Record; - } - ): Promise; -} diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 9911566..747b44a 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -1,3 +1,5 @@ -export { RestAdapter } from './adapters/rest-adapter'; - -export { DefaultRestAdapter } from './adapters/default-rest-adapter'; +export * from './Routes/'; +export * from './Router'; +export * from './REST'; +export * from './CDN'; +export { type RawFile, REST } from '@discordjs/rest'; diff --git a/packages/rest/tsup.config.ts b/packages/rest/tsup.config.ts index 2e8c9eb..dfbea63 100644 --- a/packages/rest/tsup.config.ts +++ b/packages/rest/tsup.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], minify: isProduction, - sourcemap: false, + sourcemap: false }); diff --git a/packages/ws/README.md b/packages/ws/README.md index d920013..5392428 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -1,5 +1,11 @@ # @biscuitland/ws + +## Advice + +This version of @biscuitland/ws is a **fork** of @discordeno/gateway, all credits go to them. However it has been heavily modified for proper use within biscuit. + ## Most importantly, biscuit's ws is: + A standalone gateway to interface Discord, it is meant to be used with a rest manager to send fetch requests to Discord [](https://github.com/oasisjs/biscuit) @@ -14,37 +20,29 @@ npm install @biscuitland/ws yarn add @biscuitland/ws ``` -## Example (GW proxy) +## Example + ```ts -import { ShardManager } from '@biscuitland/ws'; -import { DefaultRestAdapter } from '@biscuitland/rest'; -import { GatewayIntents } from '@biscuitland/api-types'; -import type { DiscordGetGatewayBot, DiscordReady } from '@biscuitland/api-types'; +import { GatewayManager } from '@biscuitland/ws'; +import { BiscuitREST } from '@biscuitland/rest'; +import { GatewayIntentBits, Routes} from '@biscuitland/common'; -const intents = GatewayIntents.Guilds; +const intents = GatewayIntentBits.Guilds; const token = 'your token goes here'; -const rest = new DefaultRestAdapter({ token }); +const rest = new BiscuitREST({ token }); -const gateway = await rest.get('/gateway/bot'); +(async () => { + const connection = await rest.get(Routes.gatewayBot()) -// gateway bot code ↓ -const ws = new ShardManager({ - gateway, - config: { intents, token }, - handleDiscordPayload(shard, payload) { - if (payload.t === "READY") { - const data = payload.d as DiscordReady; - console.log("logged in as:", data.user.username); - console.log("shard: %d", shard.options.id); - } - }, -}); + // gateway bot code ↓ + const ws = new GatewayManager({ token, intents, connection }); -await ws.spawns(); + await ws.spawnShards(); +})(); ``` ## Links * [Website](https://biscuitjs.com/) * [Documentation](https://docs.biscuitjs.com/) * [Discord](https://discord.gg/XNw2RZFzaP) -* [core](https://www.npmjs.com/package/@biscuitland/core) | [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [cache](https://www.npmjs.com/package/@biscuitland/cache) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) +* [rest](https://www.npmjs.com/package/@biscuitland/rest) | [core](https://www.npmjs.com/package/@biscuitland/core) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) diff --git a/packages/ws/package.json b/packages/ws/package.json index 60156df..3781cf8 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -1,76 +1,82 @@ { - "name": "@biscuitland/ws", - "version": "2.3.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": [ - "dist/**" - ], - "scripts": { - "build": "tsup", - "clean": "rm -rf dist && rm -rf .turbo", - "dev": "tsup --watch" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "require": "./dist/index.js" - } - }, - "dependencies": { - "@biscuitland/api-types": "^2.3.0", - "ws": "^8.8.1" - }, - "devDependencies": { - "@types/ws": "^8.5.3", - "tsup": "^6.1.3" - }, - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - }, - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "publishConfig": { - "access": "public" - } + "name": "@biscuitland/ws", + "version": "3.0.0", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist && rm -rf .turbo", + "dev": "tsup --watch", + "test": "vitest run" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": "./dist/index.js" + } + }, + "dependencies": { + "@biscuitland/common": "^0.0.1", + "@biscuitland/rest": "^3.0.0", + "ws": "^8.13.0" + }, + "devDependencies": { + "@types/node": "^18.14.0", + "@types/ws": "^8.5.4", + "tsup": "^6.1.3" + }, + "directories": { + "lib": "src" + }, + "license": "Apache-2.0", + "author": "Yuzuru ", + "contributors": [ + { + "name": "Yuzuru", + "url": "https://github.com/yuzudev" + }, + { + "name": "miia", + "url": "https://github.com/dragurimu" + }, + { + "name": "n128", + "url": "https://github.com/nicolito128" + }, + { + "name": "socram03", + "url": "https://github.com/socram03", + "author": true + }, + { + "name": "Drylozu", + "url": "https://github.com/Drylozu" + } + ], + "homepage": "https://biscuitjs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/oasisjs/biscuit.git" + }, + "bugs": { + "url": "https://github.com/oasisjs/biscuit" + }, + "keywords": [ + "api", + "discord", + "bots", + "typescript", + "botdev" + ], + "publishConfig": { + "access": "public" + } } diff --git a/packages/ws/src/SharedTypes.ts b/packages/ws/src/SharedTypes.ts new file mode 100644 index 0000000..11fa0ad --- /dev/null +++ b/packages/ws/src/SharedTypes.ts @@ -0,0 +1,238 @@ +import type { + GatewayPresenceUpdateData, + PresenceUpdateStatus, + GatewayActivity, + APIGuildMember, + GatewayRequestGuildMembersDataWithQuery, + GatewayRequestGuildMembersDataWithUserIds, + APIAuditLogEntry, + APIAutoModerationRule, + APIChannel, + APIGuild, + APIGuildScheduledEvent, + APIStageInstance, + GatewayAutoModerationActionExecutionDispatchData, + GatewayChannelPinsUpdateDispatchData, + GatewayChannelUpdateDispatchData, + GatewayDispatchEvents, + GatewayGuildBanAddDispatchData, + GatewayGuildBanRemoveDispatchData, + GatewayGuildCreateDispatchData, + GatewayGuildDeleteDispatchData, + GatewayGuildEmojisUpdateDispatchData, + GatewayGuildIntegrationsUpdateDispatchData, + GatewayGuildMemberAddDispatchData, + GatewayGuildMemberRemoveDispatchData, + GatewayGuildMemberUpdateDispatchData, + GatewayGuildMembersChunkDispatchData, + GatewayGuildRoleCreateDispatchData, + GatewayGuildRoleDeleteDispatchData, + GatewayGuildRoleUpdateDispatchData, + GatewayGuildScheduledEventUserRemoveDispatchData, + GatewayGuildStickersUpdateDispatchData, + GatewayIntegrationCreateDispatchData, + GatewayIntegrationDeleteDispatchData, + GatewayInteractionCreateDispatchData, + GatewayInviteCreateDispatchData, + GatewayInviteDeleteDispatchData, + GatewayMessageCreateDispatchData, + GatewayMessageDeleteBulkDispatchData, + GatewayMessageDeleteDispatchData, + GatewayMessageReactionAddDispatchData, + GatewayMessageReactionRemoveAllDispatchData, + GatewayMessageReactionRemoveDispatchData, + GatewayMessageReactionRemoveEmojiDispatchData, + GatewayMessageUpdateDispatchData, + GatewayPresenceUpdateDispatchData, + GatewayReadyDispatchData, + GatewayThreadCreateDispatchData, + GatewayThreadDeleteDispatchData, + GatewayThreadListSyncDispatchData, + GatewayThreadMemberUpdateDispatchData, + GatewayThreadMembersUpdateDispatchData, + GatewayTypingStartDispatchData, + GatewayUserUpdateDispatchData, + GatewayVoiceServerUpdateDispatchData, + GatewayVoiceStateUpdateData, + GatewayWebhooksUpdateDispatchData, + RestToKeys, + APIUser +} from '@biscuitland/common'; + +export enum ShardState { + /** Shard is fully connected to the gateway and receiving events from Discord. */ + Connected = 0, + /** Shard started to connect to the gateway. This is only used if the shard is not currently trying to identify or resume. */ + Connecting = 1, + /** Shard got disconnected and reconnection actions have been started. */ + Disconnected = 2, + /** The shard is connected to the gateway but only heartbeating. At this state the shard has not been identified with discord. */ + Unidentified = 3, + /** Shard is trying to identify with the gateway to create a new session. */ + Identifying = 4, + /** Shard is trying to resume a session with the gateway. */ + Resuming = 5, + /** Shard got shut down studied or due to a not (self) fixable error and may not attempt to reconnect on its own. */ + Offline = 6 +} + +export enum ShardSocketCloseCodes { + /** A regular Shard shutdown. */ + Shutdown = 3000, + /** A resume has been requested and therefore the old connection needs to be closed. */ + ResumeClosingOldConnection = 3024, + /** Did not receive a heartbeat ACK in time. + * Closing the shard and creating a new session. + */ + ZombiedConnection = 3010, + /** Discordeno's gateway tests hae been finished, therefore the Shard can be turned off. */ + TestingFinished = 3064, + /** Special close code reserved for Discordeno's zero-downtime resharding system. */ + Resharded = 3065, + /** Shard is re-identifying therefore the old connection needs to be closed. */ + ReIdentifying = 3066 +} + +/** https://discord.com/developers/docs/topics/gateway-events#update-presence */ +export interface StatusUpdate { + /** The user's activities */ + activities?: Omit[]; + /** The user's new status */ + status: PresenceUpdateStatus; +} + +/** https://discord.com/developers/docs/topics/gateway#update-voice-state */ +export interface UpdateVoiceState { + /** id of the guild */ + guild_id: string; + /** id of the voice channel client wants to join (null if disconnecting) */ + channel_id: string | null; + /** Is the client muted */ + self_mute: boolean; + /** Is the client deafened */ + self_deaf: boolean; +} + +export type ShardStatusUpdate = Pick; + +export interface RequestGuildMembersOptions extends GatewayRequestGuildMembersDataWithQuery, GatewayRequestGuildMembersDataWithUserIds {} + +export interface GatewayMemberRequest { + /** The unique nonce for this request. */ + nonce: string; + /** The resolver handler to run when all members arrive. */ + resolve: (value: APIGuildMember[] | PromiseLike) => void; + /** The members that have already arrived for this request. */ + members: APIGuildMember[]; +} + +export type AtLeastOne< + T, + U = { + [K in keyof T]: Pick; + } +> = Partial & U[keyof U]; + +export type ClientUser = { bot: true } & APIUser; + +export interface Events { + [GatewayDispatchEvents.Ready]: GatewayReadyDispatchData & { + user: ClientUser; + }; + [GatewayDispatchEvents.ChannelUpdate]: GatewayChannelUpdateDispatchData; + [GatewayDispatchEvents.AutoModerationActionExecution]: GatewayAutoModerationActionExecutionDispatchData; + [GatewayDispatchEvents.ThreadCreate]: GatewayThreadCreateDispatchData; + [GatewayDispatchEvents.ThreadDelete]: GatewayThreadDeleteDispatchData; + [GatewayDispatchEvents.ThreadUpdate]: GatewayThreadDeleteDispatchData; + [GatewayDispatchEvents.ThreadListSync]: GatewayThreadListSyncDispatchData; + [GatewayDispatchEvents.ThreadMemberUpdate]: GatewayThreadMemberUpdateDispatchData; + [GatewayDispatchEvents.ThreadMembersUpdate]: GatewayThreadMembersUpdateDispatchData; + [GatewayDispatchEvents.ChannelPinsUpdate]: GatewayChannelPinsUpdateDispatchData; + [GatewayDispatchEvents.GuildCreate]: GatewayGuildCreateDispatchData; + [GatewayDispatchEvents.GuildUpdate]: APIGuild; + [GatewayDispatchEvents.GuildDelete]: GatewayGuildDeleteDispatchData; + [GatewayDispatchEvents.GuildAuditLogEntryCreate]: APIAuditLogEntry; + [GatewayDispatchEvents.GuildBanAdd]: GatewayGuildBanAddDispatchData; + [GatewayDispatchEvents.GuildBanRemove]: GatewayGuildBanRemoveDispatchData; + [GatewayDispatchEvents.GuildEmojisUpdate]: GatewayGuildEmojisUpdateDispatchData; + [GatewayDispatchEvents.GuildStickersUpdate]: GatewayGuildStickersUpdateDispatchData; + [GatewayDispatchEvents.GuildIntegrationsUpdate]: GatewayGuildIntegrationsUpdateDispatchData; + [GatewayDispatchEvents.GuildMemberAdd]: GatewayGuildMemberAddDispatchData; + [GatewayDispatchEvents.GuildMemberRemove]: GatewayGuildMemberRemoveDispatchData; + [GatewayDispatchEvents.GuildMemberUpdate]: GatewayGuildMemberUpdateDispatchData; + [GatewayDispatchEvents.GuildMembersChunk]: GatewayGuildMembersChunkDispatchData; + [GatewayDispatchEvents.GuildRoleCreate]: GatewayGuildRoleCreateDispatchData; + [GatewayDispatchEvents.GuildRoleUpdate]: GatewayGuildRoleUpdateDispatchData; + [GatewayDispatchEvents.GuildRoleDelete]: GatewayGuildRoleDeleteDispatchData; + [GatewayDispatchEvents.IntegrationDelete]: GatewayIntegrationDeleteDispatchData; + [GatewayDispatchEvents.InviteCreate]: GatewayInviteCreateDispatchData; + [GatewayDispatchEvents.InviteDelete]: GatewayInviteDeleteDispatchData; + [GatewayDispatchEvents.MessageCreate]: GatewayMessageCreateDispatchData; + [GatewayDispatchEvents.MessageUpdate]: GatewayMessageUpdateDispatchData; + [GatewayDispatchEvents.MessageDelete]: GatewayMessageDeleteDispatchData; + [GatewayDispatchEvents.MessageDeleteBulk]: GatewayMessageDeleteBulkDispatchData; + [GatewayDispatchEvents.MessageReactionAdd]: GatewayMessageReactionAddDispatchData; + [GatewayDispatchEvents.MessageReactionRemove]: GatewayMessageReactionRemoveDispatchData; + [GatewayDispatchEvents.MessageReactionRemoveAll]: GatewayMessageReactionRemoveAllDispatchData; + [GatewayDispatchEvents.MessageReactionRemoveEmoji]: GatewayMessageReactionRemoveEmojiDispatchData; + [GatewayDispatchEvents.PresenceUpdate]: GatewayPresenceUpdateDispatchData; + [GatewayDispatchEvents.TypingStart]: GatewayTypingStartDispatchData; + [GatewayDispatchEvents.UserUpdate]: GatewayUserUpdateDispatchData; + [GatewayDispatchEvents.VoiceStateUpdate]: GatewayVoiceStateUpdateData; + [GatewayDispatchEvents.VoiceServerUpdate]: GatewayVoiceServerUpdateDispatchData; + [GatewayDispatchEvents.WebhooksUpdate]: GatewayWebhooksUpdateDispatchData; + [GatewayDispatchEvents.InteractionCreate]: GatewayInteractionCreateDispatchData; +} + +export type StageSameEvents = RestToKeys< + [ + APIStageInstance, + GatewayDispatchEvents.StageInstanceCreate, + GatewayDispatchEvents.StageInstanceUpdate, + GatewayDispatchEvents.StageInstanceDelete + ] +>; + +export type IntegrationSameEvents = RestToKeys< + [GatewayIntegrationCreateDispatchData, GatewayDispatchEvents.IntegrationCreate, GatewayDispatchEvents.IntegrationUpdate] +>; + +export type GuildScheduledUserSameEvents = RestToKeys< + [ + GatewayGuildScheduledEventUserRemoveDispatchData, + GatewayDispatchEvents.GuildScheduledEventUserRemove, + GatewayDispatchEvents.GuildScheduledEventUserAdd + ] +>; + +export type GuildScheduledSameEvents = RestToKeys< + [ + APIGuildScheduledEvent, + GatewayDispatchEvents.GuildScheduledEventCreate, + GatewayDispatchEvents.GuildScheduledEventDelete, + GatewayDispatchEvents.GuildScheduledEventUpdate + ] +>; + +export type ChannelSameEvents = RestToKeys< + [APIChannel, GatewayDispatchEvents.ChannelCreate, GatewayDispatchEvents.ChannelDelete, GatewayDispatchEvents.ChannelUpdate] +>; + +export type AutoModetaractionRuleEvents = RestToKeys< + [ + APIAutoModerationRule, + GatewayDispatchEvents.AutoModerationRuleCreate, + GatewayDispatchEvents.AutoModerationRuleDelete, + GatewayDispatchEvents.AutoModerationRuleUpdate + ] +>; + +export type NormalizeEvents = Events & + AutoModetaractionRuleEvents & + ChannelSameEvents & + GuildScheduledSameEvents & + GuildScheduledUserSameEvents & + IntegrationSameEvents & + StageSameEvents; + +export type GatewayEvents = { [x in keyof Events]: Events[x] }; diff --git a/packages/ws/src/defaults.ts b/packages/ws/src/defaults.ts new file mode 100644 index 0000000..25179c4 --- /dev/null +++ b/packages/ws/src/defaults.ts @@ -0,0 +1,19 @@ +export const GatewayManagerDefaultOptions = { + compress: false, + intents: 0, + properties: { + os: process.platform, + browser: 'Biscuit', + device: 'Biscuit' + }, + url: 'wss://gateway.discord.gg', + version: 10, + totalShards: 1, + lastShardId: 0, + firstShardId: 0, + totalWorkers: 4, + shardsPerWorker: 25, + spawnShardDelay: 5300, + debug: false, + cache: false +}; diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts index 25273e1..1b70e71 100644 --- a/packages/ws/src/index.ts +++ b/packages/ws/src/index.ts @@ -1,3 +1,7 @@ -export { ShardManager } from './services/shard-manager'; -export { Shard } from './services/shard'; -export { Options } from './utils/options'; +export * from './manager/GatewayManager'; +export * from './manager/GatewayManagerTypes'; +export * from './shard/Shard'; +export * from './shard/ShardTypes'; +export * from './defaults'; +export * from './SharedTypes'; +export * from './utils/Bucket'; diff --git a/packages/ws/src/manager/GatewayManager.ts b/packages/ws/src/manager/GatewayManager.ts new file mode 100644 index 0000000..2d73c34 --- /dev/null +++ b/packages/ws/src/manager/GatewayManager.ts @@ -0,0 +1,182 @@ +import { Options, Collection, Logger, delay } from '@biscuitland/common'; +import { + Shard, + GatewayMemberRequest, + BucketData, + CreateGatewayManagerOptions, + JoinVoiceOptions, + GatewayManagerDefaultOptions +} from '../index'; +export class GatewayManager { + buckets = new Map(); + shards = new Map(); + cache: Collection | null = null; + options: Required; + logger: Logger; + constructor(options: CreateGatewayManagerOptions) { + this.options = Options>(GatewayManagerDefaultOptions, { + ...options, + lastShardId: + options.lastShardId ?? (options.totalShards ? options.totalShards - 1 : options.connection ? options.connection.shards - 1 : 0) + }); + if (this.options.cache) this.cache = new Collection(); + this.logger = new Logger({ + name: '[GatewayManager]', + active: options.debug + }); + } + + calculateTotalShards(): number { + if (this.options.totalShards < 100) { + this.logger.info(`Calculating total shards: ${this.options.totalShards}`); + return this.options.totalShards; + } + this.logger.info('Calculating total shards', this.options.totalShards, this.options.connection.session_start_limit.max_concurrency); + + // Calculate a multiple of `maxConcurrency` which can be used to connect to the gateway. + return ( + Math.ceil( + this.options.totalShards / + // If `maxConcurrency` is 1 we can safely use 16. + (this.options.connection.session_start_limit.max_concurrency === 1 + ? 16 + : this.options.connection.session_start_limit.max_concurrency) + ) * this.options.connection.session_start_limit.max_concurrency + ); + } + + calculateWorekId(shardId: number): number { + const workerId = shardId % this.options.shardsPerWorker; + this.logger.info( + `Calculating workerId: Shard: ${shardId} -> Worker: ${workerId} -> Per Worker: ${this.options.shardsPerWorker} -> Total: ${this.options.totalWorkers}` + ); + return workerId; + } + + prepareBuckets(): void { + for (let i = 0; i < this.options.connection.session_start_limit.max_concurrency; ++i) { + this.logger.info(`Preparing buckets for concurrency: ${i}`); + this.buckets.set(i, { workers: [], identifyRequest: [] }); + } + + for (let shardId = this.options.firstShardId; shardId <= this.options.lastShardId; ++shardId) { + this.logger.info(`Preparing bucket for shard: ${shardId}`); + if (shardId >= this.options.totalShards) { + throw new Error(`Shard (id: ${shardId}) is bigger or equal to the used amount of used shards which is ${this.options.totalShards}`); + } + + const bucketId = shardId % this.options.connection.session_start_limit.max_concurrency; + const bucket = this.buckets.get(bucketId); + if (!bucket) + throw new Error( + `Shard (id: ${shardId}) got assigned to an illegal bucket id: ${bucketId}, expected a bucket id between 0 and ${ + this.options.connection.session_start_limit.max_concurrency - 1 + }` + ); + + const workerId = this.calculateWorekId(shardId); + const worker = bucket.workers.find((w) => w.id === workerId); + + // IF THE QUEUE HAS SPACE JUST ADD IT TO THIS QUEUE + worker ? worker.queue.push(shardId) : bucket.workers.push({ id: workerId, queue: [shardId] }); + + for (const bucket of this.buckets.values()) { + for (const worker of bucket.workers.values()) { + worker.queue = worker.queue.sort(); + } + } + } + } + + async spawnShards() { + this.prepareBuckets(); + + await Promise.all( + [...this.buckets.entries()].map(async ([bucketId, bucket]) => { + for (const worker of bucket.workers) { + worker.queue.forEach(async (shardId) => await this.tellWorkerIdentify(worker.id, shardId, bucketId)); + } + }) + ); + } + + async tellWorkerIdentify(...[workerId, shardId, bucketId]: number[]) { + this.logger.info(`tell worker to identify (${workerId}, ${shardId}, ${bucketId})`); + await this.identify(shardId); + } + + async identify(shardId: number, bId?: number) { + const bucketId = bId ?? shardId % this.options.connection.session_start_limit.max_concurrency; + let shard = this.shards.get(shardId); + this.logger.info(`identifying ${shard ? 'existing' : 'new'} shard (${shardId})`); + if (!shard) { + shard = new Shard({ + id: shardId, + connection: { + intents: this.options.intents, + url: this.options.url, + version: this.options.version, + token: this.options.token, + totalShards: this.options.totalShards, + properties: this.options.properties + }, + logger: this.logger, + // @ts-ignore + handlePayload: this.options.handlePayload, + requestIdentify: async () => await this.identify(shardId), + shardIsReady: async () => { + this.logger.info(` Shard #${shardId} is ready`); + await delay(this.options.spawnShardDelay); + this.logger.info(' Resolving shard identify request'); + this.buckets.get(bucketId)?.identifyRequest.shift()?.(); + } + }); + + this.shards.set(shardId, shard); + } + + const bucket = this.buckets.get(bucketId); + if (!bucket) return; + + return new Promise((resolve) => { + // Mark that we are making an identify request so another is not made. + bucket.identifyRequest.push(resolve); + this.logger.info(`identifiying shard #${shardId}`); + shard!.identify(); + }); + } + + async shutdown(code: number, reason: string) { + this.shards.forEach((shard) => shard.close(code, reason)); + await delay(5000); + } + + async kill(shardId: number) { + const shard = this.shards.get(shardId); + if (!shard) { + this.logger.info(`kill shard but not found ${shardId}`); + return; + } + this.logger.info(`kill shard ${shardId}`); + this.shards.delete(shardId); + await shard.shutdown(); + } + + calculateShardId(guildId: string, totalShards = this.options.totalShards) { + if (totalShards === 1) { + return 0; + } + + this.logger.info(`calculateShardId (guildId: ${guildId}, totalShards: ${totalShards})`); + return Number((BigInt(guildId) >> 22n) % BigInt(totalShards)); + } + + async joinVoice(options: JoinVoiceOptions) { + const shardId = this.calculateShardId(options.guild_id); + const shard = this.shards.get(shardId); + if (!shard) throw new Error(`Shard #${shardId}`); + + this.logger.info(`joinVoice guildId ${options.guild_id} channelId ${options.channel_id}`); + shard.joinVoiceChannel(options); + } +} diff --git a/packages/ws/src/manager/GatewayManagerTypes.ts b/packages/ws/src/manager/GatewayManagerTypes.ts new file mode 100644 index 0000000..5e99236 --- /dev/null +++ b/packages/ws/src/manager/GatewayManagerTypes.ts @@ -0,0 +1,88 @@ +import type { APIGatewayBotInfo, Identify, MakeRequired } from '@biscuitland/common'; +import type { GatewayEvents, UpdateVoiceState } from '../SharedTypes'; + +export interface CreateGatewayManagerOptions { + /** Important data which is used by the manager to connect shards to the gateway. */ + connection: APIGatewayBotInfo; + /** + * Id of the first Shard which should get controlled by this manager. + * @default 0 + */ + firstShardId?: number; + /** + * Id of the last Shard which should get controlled by this manager. + * @default 0 + */ + lastShardId?: number; + /** + * Delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 5100. YOU DON'T WANT TO HIT THE RATE LIMIT!!! + * @default 5300 + */ + spawnShardDelay?: number; + /** + * Total amount of shards your bot uses. Useful for zero-downtime updates or resharding. + * @default 1 + */ + totalShards?: number; + /** + * The amount of shards to load per worker. + * @default 25 + */ + shardsPerWorker?: number; + /** + * The total amount of workers to use for your bot. + * @default 4 + */ + totalWorkers?: number; + /** The calculated intent value of the events which the shard should receive. + * + * @default 0 + */ + intents?: number; + /** Identify properties to use */ + properties?: { + /** Operating system the shard runs on. + * + * @default "darwin" | "linux" | "windows" + */ + os: string; + /** The "browser" where this shard is running on. + * + * @default "Discordeno" + */ + browser: string; + /** The device on which the shard is running. + * + * @default "Discordeno" + */ + device: string; + }; + /** Bot token which is used to connect to Discord */ + token: string; + /** The URL of the gateway which should be connected to. + * + * @default "wss://gateway.discord.gg" + */ + url?: string; + /** The gateway version which should be used. + * + * @default 10 + */ + version?: number; + /** The payload handlers for messages on the shard. */ + handlePayload: (shardId: number, data: { t: K; d: GatewayEvents[K] }) => Promise; + /** This managers cache related settings. */ + cache?: boolean; + debug?: boolean; +} + +export interface BucketData { + workers: { id: number; queue: number[] }[]; + identifyRequest: ((value: void | PromiseLike) => void)[]; +} + +export type JoinVoiceOptions = Identify< + Omit, 'guild_id'>, 'channel_id'> & { + channel_id: string; + } +>; diff --git a/packages/ws/src/services/shard-manager.ts b/packages/ws/src/services/shard-manager.ts deleted file mode 100644 index 0a54342..0000000 --- a/packages/ws/src/services/shard-manager.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { ShardManagerOptions, SMO } from '../types'; -import type { LeakyBucket } from '../utils/bucket'; - -import { Shard } from './shard'; - -import { createLeakyBucket } from '../utils/bucket'; -import { Options } from '../utils/options'; - -export class ShardManager { - static readonly DEFAULTS = { - workers: { - shards: 25, - amount: 5, - delay: 5000 - }, - - shards: { - timeout: 15000, - delay: 5000 - } - }; - - readonly options: SMO; - - readonly buckets = new Map< - number, - { - workers: { id: number; queue: number[] }[]; - leak: LeakyBucket; - } - >(); - - readonly shards = new Map(); - - constructor(options: ShardManagerOptions) { - this.options = Options(ShardManager.DEFAULTS, options); - } - - /** Invokes internal processing and respawns shards */ - async respawns(): Promise { - // - } - - /** Invoke internal processing and spawns shards */ - async spawns(): Promise { - const { gateway, workers } = this.options; - - /** Creates the necessary buckets according to concurrency */ - for (let i = 0; i < gateway.session_start_limit.max_concurrency; i++) { - this.buckets.set(i, { - workers: [], - leak: createLeakyBucket({ - max: 1, - refillAmount: 1, - refillInterval: workers.delay, - }), - }); - } - - /** Create the start sequence of the shards inside the buckets. */ - for (let i = 0; i < gateway.shards; i++) { - const bucketID = i % gateway.session_start_limit.max_concurrency; - const bucket = this.buckets.get(bucketID); - - if (bucket) { - const workerID = Math.floor(i / workers.shards); - const worker = bucket.workers.find(w => w.id === workerID); - - if (worker) { - worker.queue.push(i); - } else { - bucket.workers.push({ id: workerID, queue: [i] }); - } - } - } - - /** Route all shards to workers */ - this.buckets.forEach(async bucket => { - for (const worker of bucket.workers) { - - for (const id of worker.queue) { - await this.connect(id); - } - - } - }); - } - - /** Invokes the bucket to prepare the connection to the shard */ - private async connect(id: number): Promise { - const { gateway } = this.options; - - let shard = this.shards.get(id); - - if (!shard) { - shard = new Shard({ - id, - - gateway: this.options.gateway, - - shards: this.options.shards, - - config: this.options.config, - - presence: this.options.makePresence, - - handlePayloads: async (shard, payload) => { - await this.options.handleDiscordPayload(shard, payload); // remove await? - }, - - handleIdentify: async (id: number) => { - await this.buckets.get(id % gateway.session_start_limit.max_concurrency)!.leak.acquire(1); // remove await? - } - }); - - this.shards.set(id, shard); - } - - await shard.connect(); - - return shard; - } -} diff --git a/packages/ws/src/services/shard.ts b/packages/ws/src/services/shard.ts deleted file mode 100644 index 0f9418f..0000000 --- a/packages/ws/src/services/shard.ts +++ /dev/null @@ -1,446 +0,0 @@ -import type { DiscordGatewayPayload } from '@biscuitland/api-types'; -import type { ShardOptions, SO, ShardStatus } from '../types'; - -import type { LeakyBucket } from '../utils/bucket'; - -import { GatewayOpcodes } from '@biscuitland/api-types'; -import { createLeakyBucket } from '../utils/bucket'; - -import { WebSocket } from 'ws'; -import { Options } from '../utils/options'; - -const textDecoder = new TextDecoder(); - -export class Shard { - static readonly DEFAULTS = { - // - }; - - readonly options: SO; - - heartbeatInterval: any | null = null; - heartbeatAck = false; - - heartbeatAt = -1; - interval = 45000; - - resumeURL: string | null = null; - sessionID: string | null = null; - - sequence = 0; - - resolves: Map void> = new Map(); - - status: ShardStatus = 'Disconnected'; - - bucket: LeakyBucket; - - trace: any = null; - - ws: WebSocket | null = null; - - constructor(options: ShardOptions) { - this.options = Options(Shard.DEFAULTS, options); - - this.bucket = createLeakyBucket({ - max: 120, - refillInterval: 60000, - refillAmount: 120, - }); - } - - resume() { - this.status = 'Resuming'; - - this.send({ - op: GatewayOpcodes.Resume, - d: { - token: `Bot ${this.options.config.token}`, - session_id: this.sessionID, - seq: this.sequence, - } - }); - } - - destroy() { - this.ws = null; - - this.bucket = createLeakyBucket({ - max: 120, - refillInterval: 60000, - refillAmount: 120, - }); - - this.sequence = 0; - this.resumeURL = null; - this.sessionID = null; - - this.heartbeatInterval = null; - } - - connect() { - if (this.ws && this.ws.readyState !== WebSocket.CLOSED) { - return; - } - - this.status = 'Connecting'; - - if (this.sessionID && this.resumeURL) { - this.ws = new WebSocket(this.resumeURL); - } else { - this.ws = new WebSocket('wss://gateway.discord.gg/?v=10&encoding=json'); - } - - this.ws.on('message', this.onMessage.bind(this)); - this.ws.on('close', this.onClose.bind(this)); - this.ws.on('error', this.onError.bind(this)); - this.ws.on('open', this.onOpen.bind(this)); - - return new Promise(resolve => { - this.resolves.set('READY', () => { - setTimeout(() => resolve(true), this.options.shards.timeout); - }); - }); - } - - identify() { - this.status = 'Identifying'; - - this.send({ - op: GatewayOpcodes.Identify, - d: { - token: `Bot ${this.options.config.token}`, - compress: false, - properties: { - os: 'linux', - device: 'Biscuit', - browser: 'Biscuit' - }, - intents: this.options.config.intents, - shard: [this.options.id, this.options.gateway.shards], - presence: this.options.presence - } - }); - } - - heartbeat(requested = false) { - if (this.status === 'Resuming' || this.status === 'Identifying') { - return; - } - - if (!requested) { - if (!this.heartbeatAt) { - // eslint-disable-next-line no-console - console.log(JSON.stringify({ - heartbeatInterval: this.heartbeatInterval, - heartbeatAck: this.heartbeatAck, - timestamp: Date.now(), - status: this.status - })); - - this.disconnect(); - return; - } - - this.heartbeatAck = false; - } - - this.heartbeatAt = Date.now(); - - this.send({ - op: GatewayOpcodes.Heartbeat, - d: this.sequence, - }, true); - } - - disconnect(reconnect = false) { - if (!this.ws) { - return; - } - - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - - if (this.ws.readyState !== WebSocket.CLOSED) { - this.ws.removeAllListeners(); - - if (this.sessionID && reconnect) { - if (this.ws.readyState !== WebSocket.OPEN) { - this.ws.close(4999, 'Reconnect'); - } else { - this.ws.terminate(); - } - } else { - this.ws.close(1000, 'Normal Close'); - } - } - - this.ws = null; - - this.status = 'Disconnected'; - - this.resolves = new Map(); - this.heartbeatAck = true; - - if (reconnect) { - if (this.sessionID) { - this.connect(); - } else { - // this.connect(); - } - } else { - this.destroy(); - } - } - - async send(payload: Partial, priority = false) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - await this.bucket.acquire(1, priority); - - this.ws.send(JSON.stringify(payload)); - } - } - - private async onMessage(data: any, isBinary: boolean) { - const payload = this.pack(data as Buffer | ArrayBuffer, isBinary); - - if (payload.s != null) { - this.sequence = payload.s; - } - - switch (payload.op) { - case GatewayOpcodes.Dispatch: - - switch (payload.t) { - case 'READY': - this.debug([`[READY] shard id: ${this.options.id}`]); - - this.status = 'Ready'; - - // @ts-ignore - this.resumeURL = `${payload.d.resume_gateway_url}/?v=10&encoding=json`; - - // @ts-ignore - this.sessionID = payload.d.session_id; - - // @ts-ignore - this.sequence = 0; - - this.resolves.get('READY')?.(payload); - this.resolves.delete('READY'); - - break; - - case 'RESUMED': - this.status = 'Ready'; - - this.resolves.get('RESUMED')?.(payload); - this.resolves.delete('RESUMED'); - - break; - } - - break; - - case GatewayOpcodes.Heartbeat: - this.heartbeat(true); - - break; - - case GatewayOpcodes.Reconnect: - this.disconnect(true); - break; - - case GatewayOpcodes.InvalidSession: - - if (payload.d) { - this.resume(); - } else { - this.sessionID = null; - this.sequence = 0; - - this.identify(); - } - - break; - - case GatewayOpcodes.Hello: - - // @ts-ignore - if (payload.d.heartbeat_interval > 0) { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - } - - // @ts-ignore - this.heartbeatInterval = setInterval(() => this.heartbeat(), payload.d.heartbeat_interval); - - // @ts-ignore - this.interval = payload.d.heartbeat_interval; - } - - if (this.status !== 'Resuming') { - this.bucket = createLeakyBucket({ - max: this.safe(), - refillInterval: 60000, - refillAmount: this.safe(), - waiting: this.bucket.waiting, - }); - } - - if (this.sessionID) { - this.resume(); - } else { - this.identify(); - this.heartbeat(); - } - - break; - case GatewayOpcodes.HeartbeatACK: - this.heartbeatAck = true; - - break; - - } - - // @ts-ignore - if (payload?.d?._trace) { - // @ts-ignore - this.trace = JSON.parse(payload.d._trace); - } - - this.options.handlePayloads(this, payload); - } - - private async onClose(code: number) { - this.debug([`[onClose] shard id: ${this.options.id}`, code]); - - switch (code) { - case 1001: - // Discord WebSocket requesting client reconnect - this.disconnect(true); - break; - - case 1006: - // problems with connections - this.disconnect(true); - break; - - case 4000: - // Unknown error - this.disconnect(); - break; - - case 4001: - // Unknown opcode - this.disconnect(); - break; - - case 4002: - // Decode error - this.disconnect(); - break; - - case 4003: - // Not authenticated - this.sessionID = null; - this.disconnect(); - break; - - case 4004: - // Authentication failed - this.sessionID = null; - this.disconnect(); - break; - - case 4005: - // Already authenticated - this.sessionID = null; - this.disconnect(); - break; - - case 4007: - // Invalid sequence - this.sequence = 0; - this.disconnect(); - break; - - case 4008: - // Rate limited - this.disconnect(); - break; - - case 4009: - // Session timed out - this.disconnect(); - break; - - case 4010: - // Invalid shard - this.sessionID = null; - this.disconnect(); - break; - - case 4011: - // Sharding required - this.sessionID = null; - this.disconnect(); - break; - - case 4012: - // Invalid API version - this.sessionID = null; - this.disconnect(); - break; - - case 4013: - // Invalid intent(s) - this.sessionID = null; - this.disconnect(); - break; - - case 4014: - // Disallowed intent(s) - this.sessionID = null; - this.disconnect(); - break; - - default: - this.disconnect(); - break; - } - } - - private async onError(error: Error) { - this.debug([`[onError] shard id: ${this.options.id}`, error]); - } - - private async onOpen() { - this.status = 'Handshaking'; - this.heartbeatAck = true; - } - - /** temporal */ - debug(_messages: unknown[]) { - // for (let index = 0; index < messages.length; index++) { - // const message = messages[index]; - - // // eslint-disable-next-line no-console - // console.log(message); - // } - } - - /** temporal */ - pack(data: Buffer | ArrayBuffer, _isBinary: boolean): DiscordGatewayPayload { - return JSON.parse(textDecoder.decode(new Uint8Array(data))) as DiscordGatewayPayload; - } - - /** temporal */ - safe() { - const requests = 120 - Math.ceil(60000 / this.interval) * 2; - - return requests < 0 ? 0 : requests; - } -} diff --git a/packages/ws/src/shard/Shard.ts b/packages/ws/src/shard/Shard.ts new file mode 100644 index 0000000..1997a9f --- /dev/null +++ b/packages/ws/src/shard/Shard.ts @@ -0,0 +1,582 @@ +import { LeakyBucket } from '../index'; +import { ShardGatewayConfig, ShardHeart, ShardCreateOptions } from './ShardTypes'; +import { GatewayMemberRequest, RequestGuildMembersOptions, ShardSocketCloseCodes, ShardState, StatusUpdate } from '../SharedTypes'; +import { + APIGuildMember, + GatewayCloseCodes, + GatewayDispatchPayload, + GatewayIntentBits, + GatewayOpcodes, + GatewayReceivePayload, + GatewaySendPayload, + Collection, + Logger +} from '@biscuitland/common'; +import { WebSocket, CloseEvent, MessageEvent } from 'ws'; +import { JoinVoiceOptions } from '../manager/GatewayManagerTypes'; + +export class Shard { + /** The id of the shard */ + id: number; + /** The connection config details that this shard will used to connect to discord. */ + connection: ShardGatewayConfig; + /** This contains all the heartbeat information */ + heart: ShardHeart; + /** The maximum of requests which can be send to discord per rate limit tick. Typically this value should not be changed. */ + maxRequestsPerRateLimitTick = 120; + /** The previous payload sequence number. */ + previousSequenceNumber: number | null = null; + /** In which interval (in milliseconds) the gateway resets it's rate limit. */ + rateLimitResetInterval = 60000; + /** Current session id of the shard if present. */ + sessionId?: string; + /** This contains the WebSocket connection to Discord, if currently connected. */ + socket?: WebSocket; + /** Current internal state of the this. */ + state = ShardState.Offline; + /** The url provided by discord to use when resuming a connection for this this. */ + resumeGatewayUrl = ''; + /** Cache for pending gateway requests which should have been send while the gateway went offline. */ + offlineSendQueue: ((_?: unknown) => void)[] = []; + /** Resolve internal waiting states. Mapped by SelectedEvents => ResolveFunction */ + resolves = new Map<'READY' | 'RESUMED' | 'INVALID_SESSION', (payload: GatewayReceivePayload) => void>(); + /** Shard bucket. Only access this if you know what you are doing. Bucket for handling shard request rate limits. */ + bucket: LeakyBucket; + logger: Logger; + /** The payload handlers for messages on the shard. */ + handlePayload: (shardId: number, data: GatewayReceivePayload) => Promise; + cache = { + requestMembers: { + /** + * Whether or not request member requests should be cached. + * @default false + */ + enabled: false, + /** The pending requests. */ + pending: new Collection() + } + }; + constructor(options: ShardCreateOptions) { + this.id = options.id; + this.connection = options.connection; + this.logger = options.logger; + this.heart = { + acknowledged: false, + interval: 45e3 + }; + + if (options.requestIdentify) this.requestIdentify = options.requestIdentify; + if (options.shardIsReady) this.shardIsReady = options.shardIsReady; + + this.handlePayload = options.handlePayload; + + const safe = this.calculateSafeRequests(); + + this.bucket = new LeakyBucket({ + max: safe, + refillAmount: safe, + refillInterval: 6e4 + }); + } + + /** The url to connect to. Intially this is the discord gateway url, and then is switched to resume gateway url once a READY is received. */ + get connectionUrl(): string { + return this.resumeGatewayUrl || this.connection.url; + } + + /** Check whether the connection to Discord is currently open. */ + get isOpen(): boolean { + return this.socket?.readyState === WebSocket.OPEN; + } + + /** Calculate the amount of requests which can safely be made per rate limit interval, before the gateway gets disconnected due to an exceeded rate limit. */ + calculateSafeRequests(): number { + // * 2 adds extra safety layer for discords OP 1 requests that we need to respond to + const safeRequests = this.maxRequestsPerRateLimitTick - Math.ceil(this.rateLimitResetInterval / this.heart.interval) * 2; + + return safeRequests < 0 ? 0 : safeRequests; + } + + async checkOffline(highPriority: boolean) { + if (!this.isOpen) { + return new Promise((resolve) => { + // Higher priority requests get added at the beginning of the array. + if (highPriority) this.offlineSendQueue.unshift(resolve); + else this.offlineSendQueue.push(resolve); + }); + } + } + + /** Close the socket connection to discord if present. */ + close(code: number, reason: string): void { + if (this.socket?.readyState !== WebSocket.OPEN) return; + + this.socket?.close(code, reason); + } + + /** Connect the shard with the gateway and start heartbeating. This will not identify the shard to the gateway. */ + async connect(): Promise { + // Only set the shard to `Connecting` state, + // if the connection request does not come from an identify or resume action. + if (![ShardState.Identifying, ShardState.Resuming].includes(this.state)) { + this.state = ShardState.Connecting; + } + + const url = new URL(this.connectionUrl); + url.searchParams.set('v', this.connection.version.toString()); + url.searchParams.set('enconding', 'json'); + + this.socket = new WebSocket(url.toString()); + + this.socket.onerror = (event) => this.logger.info({ error: event, shardId: this.id }); + this.socket.onclose = async (event) => await this.handleClose(event); + this.socket.onmessage = async (message) => await this.handleMessage(message); + + return new Promise((resolve) => { + this.socket!.onopen = () => { + // Only set the shard to `Unidentified` state, + // if the connection request does not come from an identify or resume action. + if (![ShardState.Identifying, ShardState.Resuming].includes(this.state)) { + this.state = ShardState.Unidentified; + } + // event connected + resolve(this); + }; + }); + } + + /** Identify the shard to the gateway. If not connected, this will also connect the shard to the gateway. */ + async identify() { + // A new identify has been requested even though there is already a connection open. + // Therefore we need to close the old connection and heartbeating before creating a new one. + if (this.isOpen) { + this.logger.info(`Closing existing shard: #${this.id}`); + this.close(ShardSocketCloseCodes.ReIdentifying, 'Re-identifying closure of old connection'); + } + this.state = ShardState.Identifying; + // identifying + // It is possible that the shard is in Heartbeating state but not identified, + // so check whether there is already a gateway connection existing. + // If not we need to create one before we identify. + if (!this.isOpen) await this.connect(); + + this.send( + { + op: GatewayOpcodes.Identify, + d: { + token: `Bot ${this.connection.token}`, + properties: this.connection.properties, + intents: this.connection.intents, + shard: [this.id, this.connection.totalShards], + presence: this.connection.presence + } + }, + true + ); + + return new Promise((resolve) => { + this.resolves.set('READY', () => { + // event idenfity + this.shardIsReady(); + resolve(); + }); + + // When identifying too fast, + // Discord sends an invalid session payload. + // This can safely be ignored though and the shard starts a new identify action. + this.resolves.set('INVALID_SESSION', () => { + this.resolves.delete('READY'); + resolve(); + }); + }); + } + + async resume() { + this.logger.info(`Resuming Shard #${this.id}`); + // It has been requested to resume the Shards session. + // It's possible that the shard is still connected with Discord's gateway therefore we need to forcefully close it. + if (this.isOpen) { + this.logger.info(`Resuming Shard #${this.id} in isOpen`); + this.close(ShardSocketCloseCodes.ResumeClosingOldConnection, 'Reconnecting the shard, closing old connection.'); + } + + // Shard has never identified, so we cannot resume. + if (!this.sessionId) { + this.logger.info(`Trying to resume a shard #${this.id} that was NOT first identified. (No session id found)`); + + return await this.identify(); + } + + this.state = ShardState.Resuming; + + this.logger.info(`Resuming Shard #${this.id}, before connecting`); + // Before we can resume, we need to create a new connection with Discord's gateway. + await this.connect(); + this.logger.info( + `Resuming Shard #${this.id}, after connecting. ${this.connection.token} | ${this.sessionId} | ${this.previousSequenceNumber}` + ); + + this.send( + { + op: GatewayOpcodes.Resume, + d: { + token: `Bot ${this.connection.token}`, + session_id: this.sessionId, + seq: this.previousSequenceNumber ?? 0 + } + }, + true + ); + this.logger.info(`Resuming Shard #${this.id} after send resumg`); + + return new Promise((resolve) => { + this.resolves.set('RESUMED', () => resolve()); + // If it is attempted to resume with an invalid session id, + // Discord sends an invalid session payload + // Not erroring here since it is easy that this happens, also it would be not catchable + this.resolves.set('INVALID_SESSION', () => { + this.resolves.delete('RESUMED'); + resolve(); + }); + }); + } + + /** + * Send a message to Discord Gateway. + * @param highPriority [highPriority=false] - Whether this message should be send asap. + */ + async send(message: GatewaySendPayload, highPriority = false) { + // Before acquiring a token from the bucket, check whether the shard is currently offline or not. + // Else bucket and token wait time just get wasted. + await this.checkOffline(highPriority); + + await this.bucket.acquire(highPriority); + + // It's possible, that the shard went offline after a token has been acquired from the bucket. + await this.checkOffline(highPriority); + + this.socket?.send(JSON.stringify(message)); + } + + /** Shutdown the this. Forcefully disconnect the shard from Discord. The shard may not attempt to reconnect with Discord. */ + async shutdown(): Promise { + this.close(ShardSocketCloseCodes.Shutdown, 'Shard shutting down.'); + this.state = ShardState.Offline; + } + + /** Handle a gateway connection close. */ + async handleClose(close: CloseEvent) { + this.stopHeartbeating(); + + switch (close.code) { + case ShardSocketCloseCodes.TestingFinished: + this.state = ShardState.Offline; + // disconnected event + return; + // On these codes a manual start will be done. + case ShardSocketCloseCodes.Shutdown: + case ShardSocketCloseCodes.ReIdentifying: + case ShardSocketCloseCodes.Resharded: + case ShardSocketCloseCodes.ResumeClosingOldConnection: + case ShardSocketCloseCodes.ZombiedConnection: + this.state = ShardState.Offline; + // disconnected event + return; + // Gateway connection closes which require a new identify. + case GatewayCloseCodes.UnknownOpcode: + case GatewayCloseCodes.NotAuthenticated: + case GatewayCloseCodes.InvalidSeq: + case GatewayCloseCodes.RateLimited: + case GatewayCloseCodes.SessionTimedOut: + this.logger.info(`Gateway connection closing requiring re-identify. Code: ${close.code}`); + this.state = ShardState.Identifying; + // disconnected event + // @ts-expect-error identify + return this.idenfity(); + // When these codes are received something went really wrong. + // On those we cannot start a reconnect attempt. + case GatewayCloseCodes.AuthenticationFailed: + case GatewayCloseCodes.InvalidShard: + case GatewayCloseCodes.ShardingRequired: + case GatewayCloseCodes.InvalidAPIVersion: + case GatewayCloseCodes.InvalidIntents: + case GatewayCloseCodes.DisallowedIntents: + this.state = ShardState.Offline; + // disconnected event + throw new Error(close.reason || 'Discord gave no reason! GG! You broke Discord!'); + default: + this.logger.info(`Closed shard #${this.id}. Resuming...`); + // disconnected event + return this.resume(); + } + } + + /** Handles a incoming gateway packet. */ + async handleDiscordPacket(packet: GatewayReceivePayload) { + // Edge case start: https://github.com/discordeno/discordeno/issues/2311 + this.heart.lastAck = Date.now(); + + // Manually calculating the round trip time for users who need it. + if (this.heart.lastBeat && !this.heart.acknowledged) { + this.heart.rtt = this.heart.lastAck - this.heart.lastBeat; + } + + this.heart.acknowledged = true; + + switch (packet.op) { + case GatewayOpcodes.Heartbeat: + if (!this.isOpen) return await this.resume(); + this.heart.lastBeat = Date.now(); + // Discord randomly sends this requiring an immediate heartbeat back. + // Using a direct socket.send call here because heartbeat requests are reserved by us. + this.socket?.send( + JSON.stringify({ + op: GatewayOpcodes.Heartbeat, + d: this.previousSequenceNumber + }) + ); + // hearbeat event + break; + case GatewayOpcodes.Hello: { + const interval = packet.d.heartbeat_interval; + this.logger.info(`Hello on Shard #${this.id}`); + this.startHeartbeating(interval); + break; + } + } + this.handlePayload(this.id, packet); + } + + /** Handle an incoming gateway message. */ + async handleMessage(message: MessageEvent) { + const preProcessMessage = message.data; + + if (typeof preProcessMessage !== 'string') return; + + return await this.handleDiscordPacket(JSON.parse(preProcessMessage) as GatewayDispatchPayload); + } + + /** Start sending heartbeat payloads to Discord in the provided interval. */ + startHeartbeating(interval: number) { + this.logger.info(`Start heartbeating shard #${this.id}`); + // If old heartbeast exist like after resume, clear the old ones. + if (this.heart.intervalId) clearInterval(this.heart.intervalId); + if (this.heart.timeoutId) clearTimeout(this.heart.timeoutId); + + this.heart.interval = interval; + + // Only set the shard's state to `Unidentified` + // if heartbeating has not been started due to an identify or resume action. + if ([ShardState.Disconnected, ShardState.Offline].includes(this.state)) { + this.logger.info(`[tart Heartbeating Shard #${this.id} a`); + this.state = ShardState.Unidentified; + } + + // The first heartbeat needs to be send with a random delay between `0` and `interval` + // Using a `setTimeout(_, jitter)` here to accomplish that. + // `Math.random()` can be `0` so we use `0.5` if this happens + // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating + const jitter = Math.ceil(this.heart.interval * (Math.random() || 0.5)); + this.heart.timeoutId = setTimeout(() => { + this.logger.info(`start hearting shard #${this.id} b`); + if (!this.isOpen) return; + this.logger.info(`start heartbeting shard #${this.id} c ${this.previousSequenceNumber}`); + + // Using a direct socket.send call here because heartbeat requests are reserved by us. + this.socket?.send( + JSON.stringify({ + op: GatewayOpcodes.Heartbeat, + d: this.previousSequenceNumber + }) + ); + + this.logger.info(`start hearting shard #${this.id} d`); + this.heart.lastBeat = Date.now(); + this.heart.acknowledged = false; + + // After the random heartbeat jitter we can start a normal interval. + this.heart.intervalId = setInterval(async () => { + this.logger.info(`start heartbeating shard #${this.id} e`); + if (!this.isOpen) return; + this.logger.info(`start heartbeating shard #${this.id} f`); + + // The Shard did not receive a heartbeat ACK from Discord in time, + // therefore we have to assume that the connection has failed or got "zombied". + // The Shard needs to start a re-identify action accordingly. + // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating-example-gateway-heartbeat-ack + if (!this.heart.acknowledged) { + this.logger.info(`heartbeat not acknowledged for shard #${this.id}.`); + this.close(ShardSocketCloseCodes.ZombiedConnection, 'Zombied connection, did not receive an heartbeat ACK in time.'); + return await this.identify(); + } + + this.heart.acknowledged = false; + + this.logger.info(`start Heartbeating Shard #${this.id} g`); + // Using a direct socket.send call here because heartbeat requests are reserved by us. + this.socket?.send( + JSON.stringify({ + op: GatewayOpcodes.Heartbeat, + d: this.previousSequenceNumber + }) + ); + this.heart.lastBeat = Date.now(); + + // heartbeat event + }, this.heart.interval); + }, jitter); + } + + /** Stop the heartbeating process with discord. */ + stopHeartbeating(): void { + // Clear the regular heartbeat interval. + clearInterval(this.heart.intervalId); + // It's possible that the Shard got closed before the first jittered heartbeat. + // To go safe we should clear the related timeout too. + clearTimeout(this.heart.timeoutId); + } + + /** + * Fetches the list of members for a guild over the gateway. + * + * @param options - The parameters for the fetching of the members. + * + * @remarks + * If requesting the entire member list: + * - Requires the `GUILD_MEMBERS` intent. + * + * Fires a _Guild Members Chunk_ gateway event for every 1000 members fetched. + * + * @see {@link https://discord.com/developers/docs/topics/gateway#request-guild-members} + */ + async requestMembers(options: { guild_id: string } & Partial): Promise { + // You can request 1 member without the intent + // Check if intents is not 0 as proxy ws won't set intents in other instances + if (this.connection.intents && (!options.limit || options.limit > 1) && !(this.connection.intents & GatewayIntentBits.GuildMembers)) + throw new Error('MISSING_INTENT_GUILD_MEMBERS'); + if (options.user_ids?.length) { + this.logger.info( + `requestMembers guildId: ${options.guild_id} -> setting user limit based on userIds length: ${options.user_ids.length}` + ); + options.limit = options.user_ids.length; + } + + const nonce = `${options.guild_id}-${Date.now()}`; + + // Gateway does not require caching these requests so directly send and return + if (!this.cache.requestMembers.enabled) { + this.logger.info(`requestMembers guildId: ${options.guild_id} -> skipping cache -> options ${JSON.stringify(options)}`); + + await this.send({ + op: GatewayOpcodes.RequestGuildMembers, + d: { + guild_id: options.guild_id, + // @ts-expect-error + // If a query is provided use it, OR if a limit is NOT provided use "" + query: options.query ?? (options.limit ? undefined : ''), + limit: options.limit ?? 0, + presences: options.presences ?? false, + user_ids: options.user_ids, + nonce + } + }); + return []; + } + return new Promise((resolve) => { + this.cache.requestMembers.pending.set(nonce, { + nonce, + resolve, + members: [] + }); + this.logger.info(`requestMembers options.guild_id: ${options.guild_id} -> requesting members -> data: ${JSON.stringify(options)}`); + this.send({ + op: GatewayOpcodes.RequestGuildMembers, + d: { + guild_id: options.guild_id, + // @ts-expect-error + // If a query is provided use it, OR if a limit is NOT provided use "" + query: options?.query ?? (options?.limit ? undefined : ''), + limit: options?.limit ?? 0, + presences: options?.presences ?? false, + user_ids: options?.user_ids, + nonce + } + }); + }); + } + + /** + * Connects the bot user to a voice or stage channel. + * + * This function sends the _Update Voice State_ gateway command over the gateway behind the scenes. + * + * @remarks + * Requires the `CONNECT` permission. + * + * Fires a _Voice State Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/topics/gateway#update-voice-state} + */ + async joinVoiceChannel({ guild_id, channel_id, self_deaf, self_mute }: JoinVoiceOptions) { + this.logger.info(`joinVoiceChannel guildId: ${guild_id} channelId: ${channel_id}`); + await this.send({ + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id, + channel_id, + self_mute: Boolean(self_mute), + self_deaf: self_deaf ?? true + } + }); + } + + /** + * Leaves the voice channel the bot user is currently in. + * + * This function sends the _Update Voice State_ gateway command over the gateway behind the scenes. + * + * @param guildId - The ID of the guild the voice channel to leave is in. + * + * @remarks + * Fires a _Voice State Update_ gateway event. + * + * @see {@link https://discord.com/developers/docs/topics/gateway#update-voice-state} + */ + async leaveVoiceChannel(guild_id: string) { + this.logger.info(`leaveVoiceChannel guildId: ${guild_id} Shard ${this.id}`); + await this.send({ + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id, + channel_id: null, + self_deaf: false, + self_mute: false + } + }); + } + + /** + * Edits the bot's status on one shard. + * + * @param shardId The shard id to edit the status for. + * @param data The status data to set the bots status to. + */ + async editShardStatus(data: Required) { + this.logger.info(`editShardStatus shardId: ${this.id} -> data: ${JSON.stringify(data)}`); + await this.send({ + op: GatewayOpcodes.PresenceUpdate, + d: { + since: null, + afk: false, + activities: data.activities, + status: data.status + } + }); + } + + /** This function communicates with the management process, in order to know whether its free to identify. When this function resolves, this means that the shard is allowed to send an identify payload to discord. */ + async requestIdentify(): Promise {} + + /** This function communicates with the management process, in order to tell it can identify the next shard. */ + async shardIsReady(): Promise {} +} diff --git a/packages/ws/src/shard/ShardTypes.ts b/packages/ws/src/shard/ShardTypes.ts new file mode 100644 index 0000000..cbeaae7 --- /dev/null +++ b/packages/ws/src/shard/ShardTypes.ts @@ -0,0 +1,80 @@ +import type { GatewayPresenceUpdateData, GatewayReceivePayload, Logger } from '@biscuitland/common'; + +export interface ShardGatewayConfig { + /** The calculated intent value of the events which the shard should receive. + * + * @default 0 + */ + intents: number; + /** Identify properties to use */ + properties: { + /** Operating system the shard runs on. + * + * @default "darwin" | "linux" | "windows" + */ + os: string; + /** The "browser" where this shard is running on. + * + * @default "Biscuit" + */ + browser: string; + /** The device on which the shard is running. + * + * @default "Biscuit" + */ + device: string; + }; + /** Bot token which is used to connect to Discord */ + token: string; + /** The URL of the gateway which should be connected to. + * + * @default "wss://gateway.discord.gg" + */ + url: string; + /** The gateway version which should be used. + * + * @default 10 + */ + version: number; + /** + * The total number of shards to connect to across the entire bot. + * @default 1 + */ + totalShards: number; + + presence?: GatewayPresenceUpdateData; +} + +export interface ShardCreateOptions { + /** The shard id */ + id: number; + /** The connection details */ + connection: ShardGatewayConfig; + /** The payload handlers for messages on the shard. */ + handlePayload: (shardId: number, data: GatewayReceivePayload) => Promise; + /** The handler to request a space to make an identify request. */ + requestIdentify?: () => Promise; + /** The handler to alert the gateway manager that this shard has received a READY event. */ + shardIsReady?: () => Promise; + logger: Logger; +} + +export interface ShardHeart { + /** Whether or not the heartbeat was acknowledged by Discord in time. */ + acknowledged: boolean; + /** Interval between heartbeats requested by Discord. */ + interval: number; + /** Id of the interval, which is used for sending the heartbeats. */ + intervalId?: NodeJS.Timer; + /** Unix (in milliseconds) timestamp when the last heartbeat ACK was received from Discord. */ + lastAck?: number; + /** Unix timestamp (in milliseconds) when the last heartbeat was sent. */ + lastBeat?: number; + /** Round trip time (in milliseconds) from Shard to Discord and back. + * Calculated using the heartbeat system. + * Note: this value is undefined until the first heartbeat to Discord has happened. + */ + rtt?: number; + /** Id of the timeout which is used for sending the first heartbeat to Discord since it's "special". */ + timeoutId?: NodeJS.Timeout; +} diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts deleted file mode 100644 index c39c570..0000000 --- a/packages/ws/src/types.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { DiscordGatewayPayload, DiscordGetGatewayBot, GatewayIntents, DiscordActivity } from '@biscuitland/api-types'; - -import type { ShardManager } from './services/shard-manager'; -import type { Shard } from './services/shard'; - -/** ShardManager */ - -export type ShardManagerOptions = Pick> & Partial; - -export interface SMO { - /** Function for interpretation of messages from discord */ - handleDiscordPayload: (shard: Shard, payload: DiscordGatewayPayload) => unknown; - - /** Based on the information in Get Gateway */ - gateway: DiscordGetGatewayBot; - - /** Workers options */ - workers: ShardManagerWorkersOptions; - - /** Authentication */ - config: { - intents?: GatewayIntents; - token: string; - }; - - /** Presence on identify */ - makePresence?: { - status: 'idle' | 'dnd' | 'online' | 'offline'; - afk: boolean; - since: number | null; - activities: DiscordActivity[]; - }; - - /** Options shards */ - shards: ShardManagerShardsOptions; -} - -export interface ShardManagerWorkersOptions { - /** - * Number of shards per worker - * @default 25 - */ - shards: number; - - /** - * Number of workers - * @default 5 - */ - amount: number; - - /** - * Waiting time between workers - * @default 5000 - */ - delay: number; -} - -export interface ShardManagerShardsOptions { - /** - * Waiting time to receive the ready event. - * @default 15000 - */ - timeout: number; - - /** - * Waiting time between shards - * @default 5000 - */ - delay: number; -} - -/** Shard */ - -export type ShardOptions = Pick> & Partial; - -export interface SO { - /** Shard Id */ - id: number; - - /** Based on the information in Get Gateway */ - gateway: DiscordGetGatewayBot; - - /** Options shards */ - shards: ShardManagerShardsOptions; - - /** Authentication */ - config: { - intents?: GatewayIntents; - token: string; - }; - - /** Presence on identify */ - presence?: ShardManagerOptions['makePresence']; - - /** Function for interpretation of messages from discord */ - handlePayloads: (shard: Shard, data: DiscordGatewayPayload) => Promise; - - /** Notify the manager if the shard is ready. */ - handleIdentify: (id: number) => Promise; -} - -export type ShardStatus = 'Disconnected' | 'Handshaking' | 'Connecting' | 'Heartbeating' | 'Identifying' | 'Resuming' | 'Ready'; diff --git a/packages/ws/src/utils/Bucket.ts b/packages/ws/src/utils/Bucket.ts new file mode 100644 index 0000000..b435ce1 --- /dev/null +++ b/packages/ws/src/utils/Bucket.ts @@ -0,0 +1,122 @@ +import { delay, Logger } from '@biscuitland/common'; + +export class LeakyBucket implements LeakyBucketOptions { + max: number; + refillInterval: number; + refillAmount: number; + + /** The amount of requests that have been used up already. */ + used = 0; + /** The queue of requests to acquire an available request. Mapped by */ + queue: Array<(value: void | PromiseLike) => void> = []; + /** Whether or not the queue is already processing. */ + processing = false; + /** The timeout id for the timer to reduce the used amount by the refill amount. */ + timeoutId?: NodeJS.Timeout; + /** The timestamp in milliseconds when the next refill is scheduled. */ + refillsAt?: number; + + logger = new Logger({ name: 'BiscuitWS' }); + + constructor(options?: LeakyBucketOptions) { + this.max = options?.max ?? 1; + this.refillAmount = options?.refillAmount ? (options.refillAmount > this.max ? this.max : options.refillAmount) : 1; + this.refillInterval = options?.refillInterval ?? 5000; + } + + /** The amount of requests that still remain. */ + get remaining(): number { + return this.max < this.used ? 0 : this.max - this.used; + } + + /** Refills the bucket as needed. */ + refillBucket(): void { + this.logger.debug('[LeakyBucket] Timeout for leaky bucket requests executed. Refilling bucket.'); + // Lower the used amount by the refill amount + this.used = this.refillAmount > this.used ? 0 : this.used - this.refillAmount; + // Reset the refillsAt timestamp since it just got refilled + this.refillsAt = undefined; + + if (this.used > 0) { + if (this.timeoutId) clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(() => { + this.refillBucket(); + }, this.refillInterval); + this.refillsAt = Date.now() + this.refillInterval; + } + } + + /** Begin processing the queue. */ + async processQueue(): Promise { + this.logger.debug('[Gateway] Processing queue'); + // There is already a queue that is processing + if (this.processing) return this.logger.debug('[Gateway] Queue is already processing.'); + + // Begin going through the queue. + while (this.queue.length) { + if (this.remaining) { + this.logger.debug(`[LeakyBucket] Processing queue. Remaining: ${this.remaining} Length: ${this.queue.length}`); + // Resolves the promise allowing the paused execution of this request to resolve and continue. + this.queue.shift()?.(); + // A request can be made + this.used++; + + // Create a new timeout for this request if none exists. + if (!this.timeoutId) { + this.logger.debug('[LeakyBucket] Creating new timeout for leaky bucket requests.'); + + this.timeoutId = setTimeout(() => { + this.refillBucket(); + }, this.refillInterval); + // Set the time for when this refill will occur. + this.refillsAt = Date.now() + this.refillInterval; + } + } + + // Check if a refill is scheduled, since we have used up all available requests + else if (this.refillsAt) { + const now = Date.now(); + // If there is time left until next refill, just delay execution. + if (this.refillsAt > now) { + this.logger.debug(`[LeakyBucket] Delaying execution of leaky bucket requests for ${this.refillsAt - now}ms`); + await delay(this.refillsAt - now); + this.logger.debug('[LeakyBucket] Resuming execution'); + } + } + } + + // Loop has ended mark false so it can restart later when needed + this.processing = false; + } + + /** Pauses the execution until the request is available to be made. */ + async acquire(highPriority?: boolean): Promise { + return await new Promise((resolve) => { + // High priority requests get added to the start of the queue + if (highPriority) this.queue.unshift(resolve); + // All other requests get pushed to the end. + else this.queue.push(resolve); + + // Each request should trigger the queue to be processesd. + void this.processQueue(); + }); + } +} + +export interface LeakyBucketOptions { + /** + * Max requests allowed at once. + * @default 1 + */ + max?: number; + /** + * Interval in milliseconds between refills. + * @default 5000 + */ + refillInterval?: number; + /** + * Amount of requests to refill at each interval. + * @default 1 + */ + refillAmount?: number; +} diff --git a/packages/ws/src/utils/bucket.ts b/packages/ws/src/utils/bucket.ts deleted file mode 100644 index 2c44fb2..0000000 --- a/packages/ws/src/utils/bucket.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** Create from scratch */ - -import type { PickPartial } from '@biscuitland/api-types'; - -export interface LeakyBucket { - /** How many tokens this bucket can hold. */ - max: number; - /** Amount of tokens gained per interval */ - refillAmount: number; - /** Interval at which the bucket gains tokens. */ - refillInterval: number; - - /** Acquire tokens from the bucket. */ - acquire(amount: number, highPriority?: boolean): Promise; - - /** Returns the number of milliseconds until the next refill. */ - nextRefill(): number; - - /** Current tokens in the bucket. */ - tokens(): number; - - /** @private Internal track of when the last refill of tokens was. - * DO NOT TOUCH THIS! Unless you know what you are doing ofc :P - */ - lastRefill: number; - - /** @private Internal state of whether currently it is allowed to acquire tokens. - * DO NOT TOUCH THIS! Unless you know what you are doing ofc :P - */ - allowAcquire: boolean; - - /** @private Internal number of currently available tokens. - * DO NOT TOUCH THIS! Unless you know what you are doing ofc :P - */ - tokensState: number; - - /** @private Internal array of promises necessary to guarantee no race conditions. - * DO NOT TOUCH THIS! Unless you know what you are doing ofc :P - */ - waiting: ((_?: unknown) => void)[]; -} - -export function delay(ms: number): Promise { - return new Promise(result => - setTimeout(() => { - result(); - }, ms) - ); -} - -/** Update the tokens of that bucket. - * @returns {number} The amount of current available tokens. - */ -function updateTokens(bucket: LeakyBucket): number { - const timePassed = performance.now() - bucket.lastRefill; - const missedRefills = Math.floor(timePassed / bucket.refillInterval); - - // The refill shall not exceed the max amount of tokens. - bucket.tokensState = Math.min( - bucket.tokensState + bucket.refillAmount * missedRefills, - bucket.max - ); - bucket.lastRefill += bucket.refillInterval * missedRefills; - - return bucket.tokensState; -} - -function nextRefill(bucket: LeakyBucket): number { - // Since this bucket is lazy update the tokens before calculating the next refill. - updateTokens(bucket); - - return performance.now() - bucket.lastRefill + bucket.refillInterval; -} - -async function acquire( - bucket: LeakyBucket, - amount: number, - highPriority = false -): Promise { - // To prevent the race condition of 2 acquires happening at once, - // check whether its currently allowed to acquire. - if (!bucket.allowAcquire) { - // create, push, and wait until the current running acquiring is finished. - await new Promise(resolve => { - if (highPriority) { - bucket.waiting.unshift(resolve); - } else { - bucket.waiting.push(resolve); - } - }); - - // Somehow another acquire has started, - // so need to wait again. - if (!bucket.allowAcquire) { - return await acquire(bucket, amount); - } - } - - bucket.allowAcquire = false; - // Since the bucket is lazy update the tokens now, - // and also get the current amount of available tokens - const currentTokens = updateTokens(bucket); - - // It's possible that more than available tokens have been acquired, - // so calculate the amount of milliseconds to wait until this acquire is good to go. - if (currentTokens < amount) { - const tokensNeeded = amount - currentTokens; - const refillsNeeded = Math.ceil(tokensNeeded / bucket.refillAmount); - - const waitTime = bucket.refillInterval * refillsNeeded; - await delay(waitTime); - - // Update the tokens again to ensure nothing has been missed. - updateTokens(bucket); - } - - // In order to not subtract too much from the tokens, - // calculate what is actually needed to subtract. - const toSubtract = amount % bucket.refillAmount || amount; - bucket.tokensState -= toSubtract; - - // Allow the next acquire to happen. - bucket.allowAcquire = true; - // If there is an acquire waiting, let it continue. - bucket.waiting.shift()?.(); -} - -export function createLeakyBucket({ - max, - refillInterval, - refillAmount, - tokens, - waiting, - ...rest -}: Omit< - PickPartial, - 'tokens' -> & { - /** Current tokens in the bucket. - * @default max - */ - tokens?: number; -}): LeakyBucket { - return { - max, - refillInterval, - refillAmount: refillAmount > max ? max : refillAmount, - lastRefill: performance.now(), - allowAcquire: true, - - nextRefill() { - return nextRefill(this); - }, - - tokens() { - return updateTokens(this); - }, - - async acquire(amount, highPriority) { - return await acquire(this, amount, highPriority); - }, - - tokensState: tokens ?? max, - waiting: waiting ?? [], - - ...rest, - }; -} diff --git a/packages/ws/src/utils/options.ts b/packages/ws/src/utils/options.ts deleted file mode 100644 index bf43dbf..0000000 --- a/packages/ws/src/utils/options.ts +++ /dev/null @@ -1,42 +0,0 @@ -const isPlainObject = (value: any) => { - return ( - value !== null - && typeof value === 'object' - && typeof value.constructor === 'function' - // eslint-disable-next-line no-prototype-builtins - && (value.constructor.prototype.hasOwnProperty('isPrototypeOf') || Object.getPrototypeOf(value.constructor.prototype) === null) - ) - || (value && Object.getPrototypeOf(value) === null); -}; - -const isObject = (o: any) => { - return !!o && typeof o === 'object' && !Array.isArray(o); -}; - -export const Options = (defaults: any, ...options: any[]): any => { - if (!options.length) { - return defaults; - } - - const source = options.shift(); - - if (isObject(defaults) && isPlainObject(source)) { - Object.entries(source).forEach(([key, value]) => { - if (typeof value === 'undefined') { - return; - } - - if (isPlainObject(value)) { - if (!(key in defaults)) { - Object.assign(defaults, { [key]: {} }); - } - - Options(defaults[key], value); - } else { - Object.assign(defaults, { [key]: value }); - } - }); - } - - return Options(defaults, ...options); -}; diff --git a/packages/ws/tsconfig.json b/packages/ws/tsconfig.json index 9b4f197..eb9dc47 100644 --- a/packages/ws/tsconfig.json +++ b/packages/ws/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "./dist" }, - "include": ["src/**/*"] + "include": ["src/**/*", "../common/src/Collection.ts"] } diff --git a/packages/ws/tsconfig.test.json b/packages/ws/tsconfig.test.json new file mode 100644 index 0000000..93e768d --- /dev/null +++ b/packages/ws/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": ["./tsconfig.json"], + "compilerOptions": { + "outDir": "./tests" + }, + "include": ["./tests/connection.spec.ts"] +} diff --git a/packages/ws/tsup.config.ts b/packages/ws/tsup.config.ts index 2e8c9eb..dfbea63 100644 --- a/packages/ws/tsup.config.ts +++ b/packages/ws/tsup.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], minify: isProduction, - sourcemap: false, + sourcemap: false }); diff --git a/rome.json b/rome.json new file mode 100644 index 0000000..0fc5b63 --- /dev/null +++ b/rome.json @@ -0,0 +1,34 @@ +{ + "linter": { + "enabled": true, + "rules": { + "all": true, + "style": { + "useWhile": "off", + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off" + }, + "nursery": { + "useCamelCase": "off" + } + } + }, + "formatter": { + "enabled": true, + "indentSize": 2, + "indentStyle": "space", + "lineWidth": 140, + "formatWithErrors": true + }, + "files": { + "ignore": [ + "node_modules/", + "build", + "dist", + "tsup.config.ts", + "__test__" + ] + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 7dd50c8..6113337 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,9 +5,9 @@ "lib": ["ESNext", "WebWorker"], "moduleResolution": "node", "declaration": true, + "suppressImplicitAnyIndexErrors": true, "sourceMap": false, "strict": true, - "suppressImplicitAnyIndexErrors": true, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/tsconfig.json b/tsconfig.json index 57be0d7..008a7fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "baseUrl": ".", "paths": { "@biscuitland/*": ["packages/*/src"] - } + }, + "ignoreDeprecations": "5.0", } } diff --git a/turbo.json b/turbo.json index 86254ca..bc3adc8 100644 --- a/turbo.json +++ b/turbo.json @@ -1,33 +1,32 @@ { "$schema": "https://turborepo.org/schema.json", - "baseBranch": "origin/main", "pipeline": { - "@biscuitland/api-types#build": { + "@biscuitland/rest#build": { + "dependsOn": [ + "@biscuitland/common#build" + ] + }, + "@biscuitland/common#build": { "dependsOn": [] }, - "@biscuitland/cache#build": { - "dependsOn": ["@biscuitland/api-types#build"] - }, - "@biscuitland/rest#build": { - "dependsOn": ["@biscuitland/api-types#build"] - }, "@biscuitland/ws#build": { - "dependsOn": ["@biscuitland/api-types#build"] + "dependsOn": [ + "@biscuitland/rest#build", + "@biscuitland/common#build" + ] }, "@biscuitland/core#build": { - "dependsOn": ["@biscuitland/api-types#build", "@biscuitland/rest#build", "@biscuitland/ws#build"] - }, - "@biscuitland/helpers#build": { - "dependsOn": ["@biscuitland/api-types#build", "@biscuitland/core#build"] + "dependsOn": [ + "@biscuitland/rest#build", + "@biscuitland/ws#build", + "@biscuitland/common#build" + ] }, "clean": { "cache": false }, - "lint": { - "outputs": [] - }, "dev": { "cache": false } } -} +} \ No newline at end of file