From 2127b32d26dedeb44ec43d16ec2e2046919f9bb0 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:23:13 -0500 Subject: [PATCH] feat: Add `@discordjs/core` (#8736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add @discordjs/core * chore: lint * chore: add all gateway events * chore: add the rest of the rest routes * chore: cleanup gateway * chore: rename gateway to client * chore: rename gateway to client * fix: don't spread unless we need to * refactor: use classes and make requested changes * chore: show shardId on emit * chore: add interface for intrinsic props * refactor: scope dispatch data instead of spreading * chore: add utility for uploading files for messages and interactions * feat: finish up form data handling * chore: add readme * chore: update api-extractor stuff * chore: bump deps * chore: make requested changes * chore: make requested changes * Update package.json * chore: make requested changes * fix: add missing interaction responses * chore: make some requested changes * chore: remove `return await` * chore: use autoModeration instead of automod * refactor: use snowflakes and -types results * chore: sort imports, fix return type on editUserVoiceState * chore: rename bots to users * feat: add automod dispatch events * refactor: move templates and members into guild * fix: use users instead of bots in api class * chore: imports * chore: make requested changes * fix: don't make files required on interaction replies * fix: rename sendMessage to createMessage * feat: add application command routes * feat: add webhook.execute overloads and options to invites.get * chore: use create prefixes * chore: seperate interaction params * chore: use Id * chore: make requested changes * chore: make requested changes * chore: make requested changes * chore: for -> from * Apply suggestions from code review Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * Update packages/core/README.md Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * chore: make requested changes * chore: update -types * chore: bump vitest * fix: sticker uploading * fix: lockfile * chore: make requested changes * chore: make requested changes * Update packages/core/src/api/applicationCommands.ts Co-authored-by: Almeida * Apply suggestions from code review Co-authored-by: Aura Román * Update packages/core/README.md Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: almeidx Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Aura Román Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .github/labeler.yml | 3 + .github/labels.yml | 2 + packages/core/.cliff-jumperrc.json | 5 + packages/core/.eslintrc.json | 6 + packages/core/.gitignore | 27 + packages/core/.lintstagedrc.js | 1 + packages/core/.prettierignore | 8 + packages/core/.prettierrc.js | 1 + packages/core/LICENSE | 191 ++++ packages/core/README.md | 115 +++ packages/core/api-extractor.json | 3 + packages/core/cliff.toml | 63 ++ packages/core/package.json | 73 ++ packages/core/src/api/applicationCommands.ts | 240 +++++ packages/core/src/api/channel.ts | 326 +++++++ packages/core/src/api/guild.ts | 901 +++++++++++++++++++ packages/core/src/api/index.ts | 57 ++ packages/core/src/api/interactions.ts | 181 ++++ packages/core/src/api/invite.ts | 27 + packages/core/src/api/sticker.ts | 27 + packages/core/src/api/thread.ts | 117 +++ packages/core/src/api/user.ts | 112 +++ packages/core/src/api/voice.ts | 13 + packages/core/src/api/webhook.ts | 214 +++++ packages/core/src/client.ts | 179 ++++ packages/core/src/index.ts | 5 + packages/core/src/util/files.ts | 29 + packages/core/src/util/index.ts | 1 + packages/core/tsconfig.eslint.json | 20 + packages/core/tsconfig.json | 4 + packages/core/tsup.config.js | 3 + packages/rest/src/lib/RequestManager.ts | 2 +- yarn.lock | 25 +- 33 files changed, 2979 insertions(+), 2 deletions(-) create mode 100644 packages/core/.cliff-jumperrc.json create mode 100644 packages/core/.eslintrc.json create mode 100644 packages/core/.gitignore create mode 100644 packages/core/.lintstagedrc.js create mode 100644 packages/core/.prettierignore create mode 100644 packages/core/.prettierrc.js create mode 100644 packages/core/LICENSE create mode 100644 packages/core/README.md create mode 100644 packages/core/api-extractor.json create mode 100644 packages/core/cliff.toml create mode 100644 packages/core/package.json create mode 100644 packages/core/src/api/applicationCommands.ts create mode 100644 packages/core/src/api/channel.ts create mode 100644 packages/core/src/api/guild.ts create mode 100644 packages/core/src/api/index.ts create mode 100644 packages/core/src/api/interactions.ts create mode 100644 packages/core/src/api/invite.ts create mode 100644 packages/core/src/api/sticker.ts create mode 100644 packages/core/src/api/thread.ts create mode 100644 packages/core/src/api/user.ts create mode 100644 packages/core/src/api/voice.ts create mode 100644 packages/core/src/api/webhook.ts create mode 100644 packages/core/src/client.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/util/files.ts create mode 100644 packages/core/src/util/index.ts create mode 100644 packages/core/tsconfig.eslint.json create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsup.config.js diff --git a/.github/labeler.yml b/.github/labeler.yml index 9424ed10655a..c2b67c66aecb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -14,6 +14,9 @@ packages:builders: packages:collection: - packages/collection/* - packages/collection/**/* +packages/core: + - packages:core/* + - packages:core/**/* packages:discord.js: - packages/discord.js/* - packages/discord.js/**/* diff --git a/.github/labels.yml b/.github/labels.yml index 8bc0a7d3b3f9..7396a351199b 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -56,6 +56,8 @@ color: fbca04 - name: packages:collection color: fbca04 +- name: packages:core + color: fbca04 - name: packages:discord.js color: fbca04 - name: packages:docgen diff --git a/packages/core/.cliff-jumperrc.json b/packages/core/.cliff-jumperrc.json new file mode 100644 index 000000000000..2d962203a9be --- /dev/null +++ b/packages/core/.cliff-jumperrc.json @@ -0,0 +1,5 @@ +{ + "name": "core", + "org": "discordjs", + "packagePath": "packages/core" +} diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json new file mode 100644 index 000000000000..216ef39dfa4c --- /dev/null +++ b/packages/core/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../../.eslintrc.json", + "rules": { + "jsdoc/check-param-names": "off" + } +} diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 000000000000..86b93e929ae6 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1,27 @@ +# Packages +node_modules/ + +# Log files +logs/ +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Env +.env + +# Dist +dist/ +typings/ +docs/**/* +!docs/index.json +!docs/README.md + +# Miscellaneous +.tmp/ +coverage/ +tsconfig.tsbuildinfo diff --git a/packages/core/.lintstagedrc.js b/packages/core/.lintstagedrc.js new file mode 100644 index 000000000000..dc17706a55ac --- /dev/null +++ b/packages/core/.lintstagedrc.js @@ -0,0 +1 @@ +module.exports = require('../../.lintstagedrc.json'); diff --git a/packages/core/.prettierignore b/packages/core/.prettierignore new file mode 100644 index 000000000000..553e0ea6c783 --- /dev/null +++ b/packages/core/.prettierignore @@ -0,0 +1,8 @@ +# Autogenerated +CHANGELOG.md +.turbo +dist/ +docs/**/* +!docs/index.yml +!docs/README.md +coverage/ diff --git a/packages/core/.prettierrc.js b/packages/core/.prettierrc.js new file mode 100644 index 000000000000..f004026c7647 --- /dev/null +++ b/packages/core/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../.prettierrc.json'); diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 000000000000..fd65c1f0bd66 --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 Noel Buechler + Copyright 2022 Suneet Tipirneni + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 000000000000..46540e2fd8fe --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,115 @@ +
+
+

+ discord.js +

+
+

+ Discord server + npm version + npm downloads + Build status + Code coverage +

+

+ Vercel +

+
+ +## About + +`@discordjs/core` is a thinly abstracted wrapper around the "core" components of the Discord API: REST, and gateway. + +## Installation + +**Node.js 16.9.0 or newer is required.** + +```sh-session +npm install @discordjs/core +yarn add @discordjs/core +pnpm add @discordjs/core +``` + +## Example usage + +```ts +import { REST } from '@discordjs/rest'; +import { WebSocketManager } from '@discordjs/ws'; +import { GatewayIntentBits, InteractionType, MessageFlags, createClient } from '@discordjs/core'; + +// Create REST and WebSocket managers directly +const rest = new REST({ version: '10' }).setToken(token); +const ws = new WebSocketManager({ + token, + intents: GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent, + rest, +}); + +// Create a client to emit relevant events. +const client = createClient({ rest, ws }); + +// Listen for interactions +// Each event contains an `api` prop along with the event data that allows you to interface with the Discord REST API +client.on('interactionCreate', async ({ interaction, api }) => { + if (!(interaction.type === InteractionType.ApplicationCommand) || interaction.data.name !== 'ping') { + return; + } + + await api.interactions.reply(interaction.id, interaction.token, { content: 'Pong!', flags: MessageFlags.Ephemeral }); +}); + +// Listen for the ready event +client.on('ready', () => console.log('Ready!')); + +// Start the WebSocket connection. +ws.connect(); +``` + +## Independent REST API Usage + +```ts +// Create REST instance +const rest = new REST({ version: '10' }).setToken(token); + +// Pass into API +const api = new API(rest); + +// Fetch a guild using the API wrapper +const guild = await api.guilds.get('1234567891011'); +``` + +## Links + +- [Website][website] ([source][website-source]) +- [Documentation][documentation] +- [Guide][guide] ([source][guide-source]) + See also the [Update Guide][guide-update], including updated and removed items in the library. +- [discord.js Discord server][discord] +- [Discord API Discord server][discord-api] +- [GitHub][source] +- [npm][npm] +- [Related libraries][related-libs] + +## Contributing + +Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the +[documentation][documentation]. +See [the contribution guide][contributing] if you'd like to submit a PR. + +## Help + +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle +nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord]. + +[website]: https://discord.js.org/ +[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website +[documentation]: https://discord.js.org/ +[guide]: https://discordjs.guide/ +[guide-source]: https://github.com/discordjs/guide +[guide-update]: https://discordjs.guide/additional-info/changes-in-v14.html +[discord]: https://discord.gg/djs +[discord-api]: https://discord.gg/discord-api +[source]: https://github.com/discordjs/discord.js/tree/main/packages/core +[npm]: https://www.npmjs.com/package/@discordjs/core +[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries +[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md diff --git a/packages/core/api-extractor.json b/packages/core/api-extractor.json new file mode 100644 index 000000000000..bc73f2cc022e --- /dev/null +++ b/packages/core/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../api-extractor.json" +} diff --git a/packages/core/cliff.toml b/packages/core/cliff.toml new file mode 100644 index 000000000000..66d9194efbcb --- /dev/null +++ b/packages/core/cliff.toml @@ -0,0 +1,63 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + (https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + (https://github.com/discordjs/discord.js/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/discordjs/discord.js/commit/{{ commit.id }}))\ + {% if commit.breaking %}\ + {% for breakingChange in commit.footers %}\ + \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^typings", group = "Typings"}, + { message = "^types", group = "Typings"}, + { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"}, + { message = "^revert", skip = true}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore", skip = true}, + { message = "^ci", skip = true}, + { message = "^build", skip = true}, + { body = ".*security", group = "Security"}, +] +filter_commits = true +tag_pattern = "@discordjs/core@[0-9]*" +ignore_tags = "" +date_order = true +sort_commits = "newest" diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 000000000000..38f3fc0cf9e1 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,73 @@ +{ + "name": "@discordjs/core", + "version": "0.1.0", + "description": "A thinly abstracted wrapper around the rest API, and gateway.", + "scripts": { + "test": "vitest run", + "build": "tsup", + "lint": "prettier --check . && cross-env TIMING=1 eslint src --ext .mjs,.js,.ts --format=pretty", + "format": "prettier --write . && cross-env TIMING=1 eslint src --ext .mjs,.js,.ts --fix --format=pretty", + "docs": "api-extractor run --local", + "prepack": "yarn build && yarn lint", + "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/core/*'", + "release": "cliff-jumper" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "typings": "./dist/index.d.ts", + "exports": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "directories": { + "lib": "src", + "test": "__tests__" + }, + "files": [ + "dist" + ], + "contributors": [ + "Crawl ", + "SpaceEEC ", + "Vlad Frangu ", + "Aura Román ", + "Suneet Tipirneni " + ], + "license": "Apache-2.0", + "keywords": [], + "repository": { + "type": "git", + "url": "git+https://github.com/discordjs/discord.js.git" + }, + "bugs": { + "url": "https://github.com/discordjs/discord.js/issues" + }, + "homepage": "https://discord.js.org", + "dependencies": { + "@discordjs/rest": "workspace:^", + "@discordjs/ws": "workspace:^", + "@vladfrangu/async_event_emitter": "^2.1.2", + "discord-api-types": "^0.37.20" + }, + "devDependencies": { + "@favware/cliff-jumper": "^1.9.0", + "@microsoft/api-extractor": "^7.33.6", + "@types/node": "16.18.3", + "@vitest/coverage-c8": "^0.25.3", + "cross-env": "^7.0.3", + "eslint": "^8.28.0", + "eslint-config-neon": "^0.1.40", + "eslint-formatter-pretty": "^4.1.0", + "prettier": "^2.8.0", + "tsup": "^6.5.0", + "typescript": "^4.9.3", + "vitest": "^0.25.3" + }, + "engines": { + "node": ">=16.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/core/src/api/applicationCommands.ts b/packages/core/src/api/applicationCommands.ts new file mode 100644 index 000000000000..1f66c1a45711 --- /dev/null +++ b/packages/core/src/api/applicationCommands.ts @@ -0,0 +1,240 @@ +import { makeURLSearchParams, type REST } from '@discordjs/rest'; +import { + Routes, + type RESTGetAPIApplicationCommandPermissionsResult, + type RESTGetAPIApplicationCommandResult, + type RESTGetAPIApplicationCommandsResult, + type RESTGetAPIGuildApplicationCommandsPermissionsResult, + type RESTPatchAPIApplicationCommandJSONBody, + type RESTPatchAPIApplicationCommandResult, + type RESTPostAPIApplicationCommandsJSONBody, + type RESTPostAPIApplicationCommandsResult, + type RESTPutAPIApplicationCommandPermissionsJSONBody, + type RESTPutAPIApplicationCommandPermissionsResult, + type RESTPutAPIApplicationCommandsJSONBody, + type RESTGetAPIApplicationCommandsQuery, + type RESTPutAPIApplicationCommandsResult, + type RESTGetAPIApplicationGuildCommandsQuery, + type Snowflake, +} from 'discord-api-types/v10'; + +export class ApplicationCommandsAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches all global commands for a application + * + * @param applicationId - The application id to fetch commands for + * @param options - The options to use when fetching commands + */ + public async getGlobalCommands(applicationId: Snowflake, options: RESTGetAPIApplicationCommandsQuery = {}) { + return this.rest.get(Routes.applicationCommands(applicationId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Creates a new global command + * + * @param applicationId - The application id to create the command for + * @param data - The data to use when creating the command + */ + public async createGlobalCommand(applicationId: Snowflake, data: RESTPostAPIApplicationCommandsJSONBody) { + return this.rest.post(Routes.applicationCommands(applicationId), { + body: data, + }) as Promise; + } + + /** + * Fetches a global command + * + * @param applicationId - The application id to fetch the command from + * @param commandId - The command id to fetch + */ + public async getGlobalCommand(applicationId: Snowflake, commandId: Snowflake) { + return this.rest.get( + Routes.applicationCommand(applicationId, commandId), + ) as Promise; + } + + /** + * Edits a global command + * + * @param applicationId - The application id of the command + * @param commandId - The id of the command to edit + * @param data - The data to use when editing the command + */ + public async editGlobalCommand( + applicationId: Snowflake, + commandId: Snowflake, + data: RESTPatchAPIApplicationCommandJSONBody, + ) { + return this.rest.patch(Routes.applicationCommand(applicationId, commandId), { + body: data, + }) as Promise; + } + + /** + * Deletes a global command + * + * @param applicationId - The application id of the command + * @param commandId - The id of the command to delete + */ + public async deleteGlobalCommand(applicationId: Snowflake, commandId: Snowflake) { + await this.rest.delete(Routes.applicationCommand(applicationId, commandId)); + } + + /** + * Overwrites global commands + * + * @param applicationId - The application id to overwrite commands for + * @param data - The data to use when overwriting commands + */ + public async bulkOverwriteGlobalCommands(applicationId: Snowflake, data: RESTPutAPIApplicationCommandsJSONBody) { + return this.rest.put(Routes.applicationCommands(applicationId), { + body: data, + }) as Promise; + } + + /** + * Fetches all commands for a guild + * + * @param applicationId - The application id to fetch commands for + * @param guildId - The guild id to fetch commands for + * @param data - The data to use when fetching commands + */ + public async getGuildCommands( + applicationId: Snowflake, + guildId: Snowflake, + data: RESTGetAPIApplicationGuildCommandsQuery = {}, + ) { + return this.rest.get(Routes.applicationGuildCommands(applicationId, guildId), { + query: makeURLSearchParams(data as Record), + }) as Promise; + } + + /** + * Creates a new command for a guild + * + * @param applicationId - The application id to create the command for + * @param guildId - The guild id to create the command for + * @param data - The data to use when creating the command + */ + public async createGuildCommand( + applicationId: Snowflake, + guildId: Snowflake, + data: RESTPostAPIApplicationCommandsJSONBody, + ) { + return this.rest.post(Routes.applicationGuildCommands(applicationId, guildId), { + body: data, + }) as Promise; + } + + /** + * Fetches a guild command + * + * @param applicationId - The application id to fetch the command from + * @param guildId - The guild id to fetch the command from + * @param commandId - The command id to fetch + */ + public async getGuildCommand(applicationId: Snowflake, guildId: Snowflake, commandId: Snowflake) { + return this.rest.get( + Routes.applicationGuildCommand(applicationId, guildId, commandId), + ) as Promise; + } + + /** + * Edits a guild command + * + * @param applicationId - The application id of the command + * @param guildId - The guild id of the command + * @param commandId - The command id to edit + * @param data - The data to use when editing the command + */ + public async editGuildCommand( + applicationId: Snowflake, + guildId: Snowflake, + commandId: Snowflake, + data: RESTPatchAPIApplicationCommandJSONBody, + ) { + return this.rest.patch(Routes.applicationGuildCommand(applicationId, guildId, commandId), { + body: data, + }) as Promise; + } + + /** + * Deletes a guild command + * + * @param applicationId - The application id of the command + * @param guildId - The guild id of the command + * @param commandId - The id of the command to delete + */ + public async deleteGuildCommand(applicationId: Snowflake, guildId: Snowflake, commandId: Snowflake) { + await this.rest.delete(Routes.applicationGuildCommand(applicationId, guildId, commandId)); + } + + /** + * Bulk overwrites guild commands + * + * @param applicationId - The application id to overwrite commands for + * @param guildId - The guild id to overwrite commands for + * @param data - The data to use when overwriting commands + */ + public async bulkOverwriteGuildCommands( + applicationId: Snowflake, + guildId: Snowflake, + data: RESTPutAPIApplicationCommandsJSONBody, + ) { + return this.rest.put(Routes.applicationGuildCommands(applicationId, guildId), { + body: data, + }) as Promise; + } + + /** + * Fetches the permissions for a guild command + * + * @param applicationId - The application id to get the permissions for + * @param guildId - The guild id of the command + * @param commandId - The command id to get the permissions for + */ + public async getGuildCommandPermissions(applicationId: Snowflake, guildId: Snowflake, commandId: Snowflake) { + return this.rest.get( + Routes.applicationCommandPermissions(applicationId, guildId, commandId), + ) as Promise; + } + + /** + * Fetches all permissions for all commands in a guild + * + * @param applicationId - The application id to get the permissions for + * @param guildId - The guild id to get the permissions for + */ + public async getGuildCommandsPermissions(applicationId: Snowflake, guildId: Snowflake) { + return this.rest.get( + Routes.guildApplicationCommandsPermissions(applicationId, guildId), + ) as Promise; + } + + /** + * Edits the permissions for a guild command + * + * @param userToken - The token of the user to edit permissions on behalf of + * @param applicationId - The application id to edit the permissions for + * @param guildId - The guild id to edit the permissions for + * @param commandId - The id of the command to edit the permissions for + * @param data - The data to use when editing the permissions + */ + public async editGuildCommandPermissions( + userToken: string, + applicationId: Snowflake, + guildId: Snowflake, + commandId: Snowflake, + data: RESTPutAPIApplicationCommandPermissionsJSONBody, + ) { + return this.rest.put(Routes.applicationCommandPermissions(applicationId, guildId, commandId), { + headers: { Authorization: `Bearer ${userToken.replace('Bearer ', '')}` }, + auth: false, + body: data, + }) as Promise; + } +} diff --git a/packages/core/src/api/channel.ts b/packages/core/src/api/channel.ts new file mode 100644 index 000000000000..ea1b125884af --- /dev/null +++ b/packages/core/src/api/channel.ts @@ -0,0 +1,326 @@ +import { makeURLSearchParams, type RawFile, type REST } from '@discordjs/rest'; +import { + Routes, + type RESTDeleteAPIChannelResult, + type RESTGetAPIChannelInvitesResult, + type RESTGetAPIChannelMessageReactionUsersQuery, + type RESTGetAPIChannelMessageReactionUsersResult, + type RESTGetAPIChannelMessageResult, + type RESTGetAPIChannelMessagesQuery, + type RESTGetAPIChannelMessagesResult, + type RESTGetAPIChannelPinsResult, + type RESTGetAPIChannelResult, + type RESTGetAPIChannelThreadsArchivedQuery, + type RESTGetAPIChannelUsersThreadsArchivedResult, + type RESTPatchAPIChannelJSONBody, + type RESTPatchAPIChannelMessageResult, + type RESTPatchAPIChannelResult, + type RESTPostAPIChannelFollowersResult, + type RESTPostAPIChannelInviteJSONBody, + type RESTPostAPIChannelInviteResult, + type RESTPostAPIChannelMessageCrosspostResult, + type RESTPostAPIChannelMessageJSONBody, + type RESTPostAPIChannelMessageResult, + type Snowflake, +} from 'discord-api-types/v10'; + +export class ChannelsAPI { + public constructor(private readonly rest: REST) {} + + /** + * Sends a message in a channel + * + * @param channelId - The id of the channel to send the message in + * @param data - The data to use when sending the message + */ + public async createMessage( + channelId: Snowflake, + { files, ...body }: RESTPostAPIChannelMessageJSONBody & { files?: RawFile[] }, + ) { + return this.rest.post(Routes.channelMessages(channelId), { + files, + body, + }) as Promise; + } + + /** + * Edits a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to edit + * @param data - The data to use when editing the message + */ + public async editMessage( + channelId: Snowflake, + messageId: Snowflake, + { files, ...body }: RESTPostAPIChannelMessageJSONBody & { files?: RawFile[] }, + ) { + return this.rest.patch(Routes.channelMessage(channelId, messageId), { + files, + body, + }) as Promise; + } + + /** + * Fetches the reactions for a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to get the reactions for + * @param emoji - The emoji to get the reactions for + * @param options - The options to use when fetching the reactions + */ + public async getMessageReactions( + channelId: Snowflake, + messageId: Snowflake, + emoji: string, + options: RESTGetAPIChannelMessageReactionUsersQuery = {}, + ) { + return this.rest.get(Routes.channelMessageReaction(channelId, messageId, encodeURIComponent(emoji)), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Deletes a reaction for the current user + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to delete the reaction for + * @param emoji - The emoji to delete the reaction for + */ + public async deleteOwnMessageReaction(channelId: Snowflake, messageId: Snowflake, emoji: string) { + await this.rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encodeURIComponent(emoji))); + } + + /** + * Deletes a reaction for a user + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to delete the reaction for + * @param emoji - The emoji to delete the reaction for + * @param userId - The id of the user to delete the reaction for + */ + public async deleteUserMessageReaction(channelId: Snowflake, messageId: Snowflake, emoji: string, userId: Snowflake) { + await this.rest.delete(Routes.channelMessageUserReaction(channelId, messageId, encodeURIComponent(emoji), userId)); + } + + /** + * Deletes all reactions for a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to delete the reactions for + */ + public async deleteAllMessageReactions(channelId: Snowflake, messageId: Snowflake) { + await this.rest.delete(Routes.channelMessageAllReactions(channelId, messageId)); + } + + /** + * Deletes all reactions of an emoji for a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to delete the reactions for + * @param emoji - The emoji to delete the reactions for + */ + public async deleteAllMessageReactionsForEmoji(channelId: Snowflake, messageId: Snowflake, emoji: string) { + await this.rest.delete(Routes.channelMessageReaction(channelId, messageId, encodeURIComponent(emoji))); + } + + /** + * Adds a reaction to a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to add the reaction to + * @param emoji - The emoji to add the reaction with + */ + public async addMessageReaction(channelId: Snowflake, messageId: Snowflake, emoji: string) { + await this.rest.put(Routes.channelMessageOwnReaction(channelId, messageId, encodeURIComponent(emoji))); + } + + /** + * Fetches a channel + * + * @param channelId - The id of the channel + */ + public async get(channelId: Snowflake) { + return this.rest.get(Routes.channel(channelId)) as Promise; + } + + /** + * Edits a channel + * + * @param channelId - The id of the channel to edit + * @param data - The new channel data + */ + public async edit(channelId: Snowflake, data: RESTPatchAPIChannelJSONBody) { + return this.rest.patch(Routes.channel(channelId), { body: data }) as Promise; + } + + /** + * Deletes a channel + * + * @param channelId - The id of the channel to delete + */ + public async delete(channelId: Snowflake) { + return this.rest.delete(Routes.channel(channelId)) as Promise; + } + + /** + * Fetches the messages of a channel + * + * @param channelId - The id of the channel to fetch messages from + * @param options - The options to use when fetching messages + */ + public async getMessages(channelId: Snowflake, options: RESTGetAPIChannelMessagesQuery = {}) { + return this.rest.get(Routes.channelMessages(channelId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Shows a typing indicator in a channel + * + * @param channelId - The id of the channel to show the typing indicator in + */ + public async showTyping(channelId: Snowflake) { + await this.rest.post(Routes.channelTyping(channelId)); + } + + /** + * Fetches the pinned messages of a channel + * + * @param channelId - The id of the channel to fetch pinned messages from + */ + public async getPins(channelId: Snowflake) { + return this.rest.get(Routes.channelPins(channelId)) as Promise; + } + + /** + * Pins a message in a channel + * + * @param channelId - The id of the channel to pin the message in + * @param messageId - The id of the message to pin + * @param reason - The reason for pinning the message + */ + public async pinMessage(channelId: Snowflake, messageId: Snowflake, reason?: string) { + await this.rest.put(Routes.channelPin(channelId, messageId), { reason }); + } + + /** + * Deletes a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to delete + * @param reason - The reason for deleting the message + */ + public async deleteMessage(channelId: Snowflake, messageId: Snowflake, reason?: string) { + await this.rest.delete(Routes.channelMessage(channelId, messageId), { reason }); + } + + /** + * Bulk deletes messages + * + * @param channelId - The id of the channel the messages are in + * @param messageIds - The ids of the messages to delete + */ + public async bulkDeleteMessages(channelId: Snowflake, messageIds: Snowflake[], reason?: string): Promise { + await this.rest.post(Routes.channelBulkDelete(channelId), { reason, body: { messages: messageIds } }); + } + + /** + * Fetches a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to fetch + */ + public async getMessage(channelId: Snowflake, messageId: Snowflake) { + return this.rest.get(Routes.channelMessage(channelId, messageId)) as Promise; + } + + /** + * Crossposts a message + * + * @param channelId - The id of the channel the message is in + * @param messageId - The id of the message to crosspost + */ + public async crosspostMessage(channelId: Snowflake, messageId: Snowflake) { + return this.rest.post( + Routes.channelMessageCrosspost(channelId, messageId), + ) as Promise; + } + + /** + * Unpins a message in a channel + * + * @param channelId - The id of the channel to unpin the message in + * @param messageId - The id of the message to unpin + * @param reason - The reason for unpinning the message + */ + public async unpinMessage(channelId: Snowflake, messageId: Snowflake, reason?: string) { + await this.rest.delete(Routes.channelPin(channelId, messageId), { reason }); + } + + /** + * Follows an announcement channel + * + * @param channelId - The id of the announcement channel to follow + * @param webhookChannelId - The id of the webhook channel to follow the announcements in + */ + public async followAnnouncements(channelId: Snowflake, webhookChannelId: Snowflake) { + return this.rest.post(Routes.channelFollowers(channelId), { + body: { webhook_channel_id: webhookChannelId }, + }) as Promise; + } + + /** + * Creates a new invite for a channel + * + * @param channelId - The id of the channel to create an invite for + * @param data - The data to use when creating the invite + */ + public async createInvite(channelId: Snowflake, data: RESTPostAPIChannelInviteJSONBody, reason?: string) { + return this.rest.post(Routes.channelInvites(channelId), { + reason, + body: data, + }) as Promise; + } + + /** + * Fetches the invites of a channel + * + * @param channelId - The id of the channel to fetch invites from + */ + public async getInvites(channelId: Snowflake) { + return this.rest.get(Routes.channelInvites(channelId)) as Promise; + } + + /** + * Fetches the archived threads of a channel + * + * @param channelId - The id of the channel to fetch archived threads from + * @param archivedStatus - The archived status of the threads to fetch + * @param options - The options to use when fetching archived threads + */ + public async getArchivedThreads( + channelId: Snowflake, + archivedStatus: 'private' | 'public', + options: RESTGetAPIChannelThreadsArchivedQuery = {}, + ) { + return this.rest.get(Routes.channelThreads(channelId, archivedStatus), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Fetches the private joined archived threads of a channel + * + * @param channelId - The id of the channel to fetch joined archived threads from + * @param options - The options to use when fetching joined archived threads + */ + public async getJoinedPrivateArchivedThreads( + channelId: Snowflake, + options: RESTGetAPIChannelThreadsArchivedQuery = {}, + ) { + return this.rest.get(Routes.channelJoinedArchivedThreads(channelId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } +} diff --git a/packages/core/src/api/guild.ts b/packages/core/src/api/guild.ts new file mode 100644 index 000000000000..609e9b13b8d4 --- /dev/null +++ b/packages/core/src/api/guild.ts @@ -0,0 +1,901 @@ +import type { RawFile } from '@discordjs/rest'; +import { makeURLSearchParams, type REST } from '@discordjs/rest'; +import { + Routes, + type GuildMFALevel, + type GuildWidgetStyle, + type RESTGetAPIAuditLogQuery, + type RESTGetAPIAuditLogResult, + type RESTGetAPIAutoModerationRuleResult, + type RESTGetAPIAutoModerationRulesResult, + type RESTGetAPIGuildBansResult, + type RESTGetAPIGuildChannelsResult, + type RESTGetAPIGuildEmojiResult, + type RESTGetAPIGuildEmojisResult, + type RESTGetAPIGuildIntegrationsResult, + type RESTGetAPIGuildInvitesResult, + type RESTGetAPIGuildMemberResult, + type RESTGetAPIGuildMembersQuery, + type RESTGetAPIGuildMembersSearchResult, + type RESTGetAPIGuildPreviewResult, + type RESTGetAPIGuildPruneCountResult, + type RESTGetAPIGuildResult, + type RESTGetAPIGuildRolesResult, + type RESTGetAPIGuildScheduledEventQuery, + type RESTGetAPIGuildScheduledEventResult, + type RESTGetAPIGuildScheduledEventsQuery, + type RESTGetAPIGuildScheduledEventsResult, + type RESTGetAPIGuildScheduledEventUsersQuery, + type RESTGetAPIGuildScheduledEventUsersResult, + type RESTGetAPIGuildStickerResult, + type RESTGetAPIGuildStickersResult, + type RESTGetAPIGuildTemplatesResult, + type RESTGetAPIGuildThreadsResult, + type RESTGetAPIGuildVanityUrlResult, + type RESTGetAPIGuildVoiceRegionsResult, + type RESTGetAPIGuildPruneCountQuery, + type RESTPostAPIGuildStickerFormDataBody, + type RESTPostAPIGuildStickerResult, + type RESTGetAPIGuildMembersSearchQuery, + type RESTGetAPIGuildWelcomeScreenResult, + type RESTGetAPIGuildWidgetImageResult, + type RESTGetAPIGuildWidgetJSONResult, + type RESTGetAPITemplateResult, + type RESTPatchAPIAutoModerationRuleJSONBody, + type RESTPatchAPIGuildChannelPositionsJSONBody, + type RESTPatchAPIGuildEmojiJSONBody, + type RESTPatchAPIGuildEmojiResult, + type RESTPatchAPIGuildJSONBody, + type RESTPatchAPIGuildMemberJSONBody, + type RESTPatchAPIGuildMemberResult, + type RESTPatchAPIGuildResult, + type RESTPatchAPIGuildRoleJSONBody, + type RESTPatchAPIGuildRolePositionsJSONBody, + type RESTPatchAPIGuildRolePositionsResult, + type RESTPatchAPIGuildRoleResult, + type RESTPatchAPIGuildScheduledEventJSONBody, + type RESTPatchAPIGuildScheduledEventResult, + type RESTPatchAPIGuildStickerJSONBody, + type RESTPatchAPIGuildStickerResult, + type RESTPatchAPIGuildTemplateJSONBody, + type RESTPatchAPIGuildTemplateResult, + type RESTPatchAPIGuildVoiceStateUserJSONBody, + type RESTPatchAPIGuildWelcomeScreenJSONBody, + type RESTPatchAPIGuildWelcomeScreenResult, + type RESTPatchAPIGuildWidgetSettingsJSONBody, + type RESTPatchAPIGuildWidgetSettingsResult, + type RESTPostAPIAutoModerationRuleJSONBody, + type RESTPostAPIAutoModerationRuleResult, + type RESTPostAPIGuildChannelJSONBody, + type RESTPostAPIGuildChannelResult, + type RESTPostAPIGuildEmojiJSONBody, + type RESTPostAPIGuildEmojiResult, + type RESTPostAPIGuildPruneJSONBody, + type RESTPostAPIGuildRoleJSONBody, + type RESTPostAPIGuildRoleResult, + type RESTPostAPIGuildScheduledEventJSONBody, + type RESTPostAPIGuildScheduledEventResult, + type RESTPostAPIGuildsJSONBody, + type RESTPostAPIGuildsMFAResult, + type RESTPostAPIGuildsResult, + type RESTPostAPIGuildTemplatesResult, + type RESTPostAPITemplateCreateGuildJSONBody, + type RESTPutAPIGuildBanJSONBody, + type RESTPutAPIGuildTemplateSyncResult, + type Snowflake, +} from 'discord-api-types/v10'; + +export class GuildsAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches a guild + * + * @param guildId - The id of the guild + */ + public async get(guildId: string) { + return this.rest.get(Routes.guild(guildId)) as Promise; + } + + /** + * Fetches a guild preview + * + * @param guildId - The id of the guild to fetch the preview from + */ + public async getPreview(guildId: Snowflake) { + return this.rest.get(Routes.guildPreview(guildId)) as Promise; + } + + /** + * Creates a guild + * + * @param data - The guild to create + */ + public async create(data: RESTPostAPIGuildsJSONBody) { + return this.rest.post(Routes.guilds(), { body: data }) as Promise; + } + + /** + * Edits a guild + * + * @param guildId - The id of the guild to edit + * @param data - The new guild data + * @param reason - The reason for editing this guild + */ + public async edit(guildId: Snowflake, data: RESTPatchAPIGuildJSONBody, reason?: string) { + return this.rest.patch(Routes.guild(guildId), { reason, body: data }) as Promise; + } + + /** + * Deletes a guild + * + * @param guildId - The id of the guild to delete + * @param reason - The reason for deleting this guild + */ + public async delete(guildId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guild(guildId), { reason }); + } + + /** + * Fetches all the members of a guild + * + * @param guildId - The id of the guild + * @param options - The options to use when fetching the guild members + */ + public async getMembers(guildId: Snowflake, options: RESTGetAPIGuildMembersQuery = {}) { + return this.rest.get(Routes.guildMembers(guildId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Fetches a guild's channels + * + * @param guildId - The id of the guild to fetch the channels from + */ + public async getChannels(guildId: Snowflake) { + return this.rest.get(Routes.guildChannels(guildId)) as Promise; + } + + /** + * Creates a guild channel + * + * @param guildId - The id of the guild to create the channel in + * @param data - The data to create the new channel + * @param reason - The reason for creating this channel + */ + public async createChannel(guildId: Snowflake, data: RESTPostAPIGuildChannelJSONBody, reason?: string) { + return this.rest.post(Routes.guildChannels(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits a guild channel's positions + * + * @param guildId - The id of the guild to edit the channel positions from + * @param data - The data to edit the channel positions with + * @param reason - The reason for editing the channel positions + */ + public async setChannelPositions( + guildId: Snowflake, + data: RESTPatchAPIGuildChannelPositionsJSONBody, + reason?: string, + ) { + await this.rest.patch(Routes.guildChannels(guildId), { reason, body: data }); + } + + /** + * Fetches the active threads in a guild + * + * @param guildId - The id of the guild to fetch the active threads from + */ + public async getActiveThreads(guildId: Snowflake) { + return this.rest.get(Routes.guildActiveThreads(guildId)) as Promise; + } + + /** + * Fetches a guild member ban + * + * @param guildId - The id of the guild to fetch the ban from + */ + public async getMemberBans(guildId: Snowflake) { + return this.rest.get(Routes.guildBans(guildId)) as Promise; + } + + /** + * Bans a user from a guild + * + * @param guildId - The id of the guild to ban the member in + * @param userId - The id of the user to ban + * @param options - Options for banning the user + * @param reason - The reason for banning the user + */ + public async banUser( + guildId: Snowflake, + userId: Snowflake, + options: RESTPutAPIGuildBanJSONBody = {}, + reason?: string, + ) { + await this.rest.put(Routes.guildBan(guildId, userId), { reason, body: options }); + } + + /** + * Unbans a user from a guild + * + * @param guildId - The id of the guild to unban the member in + * @param userId - The id of the user to unban + * @param reason - The reason for unbanning the user + */ + public async unbanUser(guildId: Snowflake, userId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildBan(guildId, userId), { reason }); + } + + /** + * Gets all the roles in a guild + * + * @param guildId - The id of the guild to fetch the roles from + */ + public async getRoles(guildId: Snowflake) { + return this.rest.get(Routes.guildRoles(guildId)) as Promise; + } + + /** + * Creates a guild role + * + * @param guildId - The id of the guild to create the role in + * @param data - The data to create the role with + * @param reason - The reason for creating the role + */ + public async createRole(guildId: Snowflake, data: RESTPostAPIGuildRoleJSONBody, reason?: string) { + return this.rest.post(Routes.guildRoles(guildId), { reason, body: data }) as Promise; + } + + /** + * Sets role positions in a guild + * + * @param guildId - The id of the guild to set role positions for + * @param data - The data for setting a role position + * @param reason - The reason for setting the role position + */ + public async setRolePositions(guildId: Snowflake, data: RESTPatchAPIGuildRolePositionsJSONBody, reason?: string) { + return this.rest.patch(Routes.guildRoles(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits a guild role + * + * @param guildId - The id of the guild to edit the role in + * @param roleId - The id of the role to edit + * @param data - data for editing the role + * @param reason - The reason for editing the role + */ + public async editRole(guildId: Snowflake, roleId: Snowflake, data: RESTPatchAPIGuildRoleJSONBody, reason?: string) { + return this.rest.patch(Routes.guildRole(guildId, roleId), { + reason, + body: data, + }) as Promise; + } + + /** + * Deletes a guild role + * + * @param guildId - The id of the guild to delete the role in + * @param roleId - The id of the role to delete + * @param reason - The reason for deleting the role + */ + public async deleteRole(guildId: Snowflake, roleId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildRole(guildId, roleId), { reason }); + } + + /** + * Edits the multi-factor-authentication (MFA) level of a guild + * + * @param guildId - The id of the guild to edit the MFA level for + * @param level - The new MFA level + * @param reason - The reason for editing the MFA level + */ + public async editMFALevel(guildId: Snowflake, level: GuildMFALevel, reason?: string) { + return this.rest.post(Routes.guildMFA(guildId), { + reason, + body: { mfa_level: level }, + }) as Promise; + } + + /** + * Fetch the number of members that can be pruned from a guild + * + * @param guildId - The id of the guild to fetch the number of pruned members from + * @param options - The options for fetching the number of pruned members + */ + public async getPruneCount(guildId: Snowflake, options: RESTGetAPIGuildPruneCountQuery = {}) { + return this.rest.get(Routes.guildPrune(guildId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Prunes members in a guild + * + * @param guildId - The id of the guild to prune members in + * @param options - The options for pruning members + * @param reason - The reason for pruning members + */ + public async beginPrune(guildId: Snowflake, options: RESTPostAPIGuildPruneJSONBody = {}, reason?: string) { + return this.rest.post(Routes.guildPrune(guildId), { + body: options, + reason, + }) as Promise; + } + + /** + * Fetches voice regions for a guild + * + * @param guildId - The id of the guild to fetch the voice regions from + */ + public async getVoiceRegions(guildId: Snowflake) { + return this.rest.get(Routes.guildVoiceRegions(guildId)) as Promise; + } + + /** + * Fetches the invites for a guild + * + * @param guildId - The id of the guild to fetch the invites from + */ + public async getInvites(guildId: Snowflake) { + return this.rest.get(Routes.guildInvites(guildId)) as Promise; + } + + /** + * Fetches the integrations for a guild + * + * @param guildId - The id of the guild to fetch the integrations from + */ + public async getIntegrations(guildId: Snowflake) { + return this.rest.get(Routes.guildIntegrations(guildId)) as Promise; + } + + /** + * Deletes an integration from a guild + * + * @param guildId - The id of the guild to delete the integration from + * @param integrationId - The id of the integration to delete + * @param reason - The reason for deleting the integration + */ + public async deleteIntegration(guildId: Snowflake, integrationId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildIntegration(guildId, integrationId), { reason }); + } + + /** + * Fetches the widget settings for a guild + * + * @param guildId - The id of the guild to fetch the widget settings from + */ + public async getWidgetSettings(guildId: Snowflake) { + return this.rest.get(Routes.guildWidgetSettings(guildId)) as Promise; + } + + /** + * Edits the widget settings for a guild + * + * @param guildId - The id of the guild to edit the widget settings from + * @param data - The new widget settings data + * @param reason - The reason for editing the widget settings + */ + public async editWidgetSettings(guildId: Snowflake, data: RESTPatchAPIGuildWidgetSettingsJSONBody, reason?: string) { + return this.rest.patch(Routes.guildWidgetSettings(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Fetches the widget for a guild + * + * @param guildId - The id of the guild to fetch the widget from + */ + public async getWidget(guildId: Snowflake) { + return this.rest.get(Routes.guildWidgetJSON(guildId)) as Promise; + } + + /** + * Fetches the vanity url for a guild + * + * @param guildId - The id of the guild to fetch the vanity url from + */ + public async getVanityURL(guildId: Snowflake) { + return this.rest.get(Routes.guildVanityUrl(guildId)) as Promise; + } + + /** + * Fetches the widget image for a guild + * + * @param guildId - The id of the guild to fetch the widget image from + * @param style - The style of the widget image + */ + public async getWidgetImage(guildId: Snowflake, style?: GuildWidgetStyle) { + return this.rest.get(Routes.guildWidgetImage(guildId), { + query: makeURLSearchParams({ style }), + }) as Promise; + } + + /** + * Fetches the welcome screen for a guild + * + * @param guildId - The id of the guild to fetch the welcome screen from + */ + public async getWelcomeScreen(guildId: Snowflake) { + return this.rest.get(Routes.guildWelcomeScreen(guildId)) as Promise; + } + + /** + * Edits the welcome screen for a guild + * + * @param guildId - The id of the guild to edit the welcome screen for + * @param data - The new welcome screen data + * @param reason - The reason for editing the welcome screen + */ + public async editWelcomeScreen(guildId: Snowflake, data?: RESTPatchAPIGuildWelcomeScreenJSONBody, reason?: string) { + return this.rest.patch(Routes.guildWelcomeScreen(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits a user's voice state in a guild + * + * @param guildId - The id of the guild to edit the current user's voice state in + * @param userId - The id of the user to edit the voice state for + * @param data - The data for editing the voice state + * @param reason - The reason for editing the voice state + */ + public async editUserVoiceState( + guildId: Snowflake, + userId: Snowflake, + data: RESTPatchAPIGuildVoiceStateUserJSONBody, + reason?: string, + ) { + await this.rest.patch(Routes.guildVoiceState(guildId, userId), { reason, body: data }); + } + + /** + * Fetches all emojis for a guild + * + * @param guildId - The id of the guild to fetch the emojis from + */ + public async getEmojis(guildId: Snowflake) { + return this.rest.get(Routes.guildEmojis(guildId)) as Promise; + } + + /** + * Fetches an emoji for a guild + * + * @param guildId - The id of the guild to fetch the emoji from + * @param emojiId - The id of the emoji to fetch + */ + public async getEmoji(guildId: Snowflake, emojiId: Snowflake) { + return this.rest.get(Routes.guildEmoji(guildId, emojiId)) as Promise; + } + + /** + * Creates a new emoji for a guild + * + * @param guildId - The id of the guild to create the emoji from + * @param data - The data for creating the emoji + * @param reason - The reason for creating the emoji + */ + public async createEmoji(guildId: Snowflake, data: RESTPostAPIGuildEmojiJSONBody, reason?: string) { + return this.rest.post(Routes.guildEmojis(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits an emoji for a guild + * + * @param guildId - The id of the guild to edit the emoji from + * @param emojiId - The id of the emoji to edit + * @param data - The data for editing the emoji + * @param reason - The reason for editing the emoji + */ + public async editEmoji( + guildId: Snowflake, + emojiId: Snowflake, + data: RESTPatchAPIGuildEmojiJSONBody, + reason?: string, + ) { + return this.rest.patch(Routes.guildEmoji(guildId, emojiId), { + reason, + body: data, + }) as Promise; + } + + /** + * Deletes an emoji for a guild + * + * @param guildId - The id of the guild to delete the emoji from + * @param emojiId - The id of the emoji to delete + * @param reason - The reason for deleting the emoji + */ + public async deleteEmoji(guildId: Snowflake, emojiId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildEmoji(guildId, emojiId), { reason }); + } + + /** + * Fetches all scheduled events for a guild + * + * @param guildId - The id of the guild to fetch the scheduled events from + * @param options - The options for fetching the scheduled events + */ + public async getScheduledEvents(guildId: Snowflake, options: RESTGetAPIGuildScheduledEventsQuery = {}) { + return this.rest.get(Routes.guildScheduledEvents(guildId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Creates a new scheduled event for a guild + * + * @param guildId - The id of the guild to create the scheduled event from + * @param data - The data to create the event with + * @param reason - The reason for creating the scheduled event + */ + public async createScheduledEvent(guildId: Snowflake, data: RESTPostAPIGuildScheduledEventJSONBody, reason?: string) { + return this.rest.post(Routes.guildScheduledEvents(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Fetches a scheduled event for a guild + * + * @param guildId - The id of the guild to fetch the scheduled event from + * @param eventId - The id of the scheduled event to fetch + * @param options - The options for fetching the scheduled event + */ + public async getScheduledEvent( + guildId: Snowflake, + eventId: Snowflake, + options: RESTGetAPIGuildScheduledEventQuery = {}, + ) { + return this.rest.get(Routes.guildScheduledEvent(guildId, eventId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Edits a scheduled event for a guild + * + * @param guildId - The id of the guild to edit the scheduled event from + * @param eventId - The id of the scheduled event to edit + * @param data - The new event data + * @param reason - The reason for editing the scheduled event + */ + public async editScheduledEvent( + guildId: Snowflake, + eventId: Snowflake, + data: RESTPatchAPIGuildScheduledEventJSONBody, + reason?: string, + ) { + return this.rest.patch(Routes.guildScheduledEvent(guildId, eventId), { + reason, + body: data, + }) as Promise; + } + + /** + * Deletes a scheduled event for a guild + * + * @param guildId - The id of the guild to delete the scheduled event from + * @param eventId - The id of the scheduled event to delete + * @param reason - The reason for deleting the scheduled event + */ + public async deleteScheduledEvent(guildId: Snowflake, eventId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildScheduledEvent(guildId, eventId), { reason }); + } + + /** + * Gets all users that are interested in a scheduled event + * + * @param guildId - The id of the guild to fetch the scheduled event users from + * @param eventId - The id of the scheduled event to fetch the users for + * @param options - The options for fetching the scheduled event users + */ + public async getScheduledEventUsers( + guildId: Snowflake, + eventId: Snowflake, + options: RESTGetAPIGuildScheduledEventUsersQuery = {}, + ) { + return this.rest.get(Routes.guildScheduledEventUsers(guildId, eventId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Fetches all the templates for a guild + * + * @param guildId - The id of the guild to fetch the templates from + */ + public async getTemplates(guildId: Snowflake) { + return this.rest.get(Routes.guildTemplates(guildId)) as Promise; + } + + /** + * Syncs a template for a guild + * + * @param guildId - The id of the guild to sync the template from + * @param templateCode - The code of the template to sync + */ + public async syncTemplate(guildId: Snowflake, templateCode: string) { + return this.rest.put(Routes.guildTemplate(guildId, templateCode)) as Promise; + } + + /** + * Edits a template for a guild + * + * @param guildId - The id of the guild to edit the template from + * @param templateCode - The code of the template to edit + * @param data - The data for editing the template + */ + public async editTemplate(guildId: Snowflake, templateCode: string, data: RESTPatchAPIGuildTemplateJSONBody) { + return this.rest.patch(Routes.guildTemplate(guildId, templateCode), { + body: data, + }) as Promise; + } + + /** + * Deletes a template for a guild + * + * @param guildId - The id of the guild to delete the template from + * @param templateCode - The code of the template to delete + */ + public async deleteTemplate(guildId: Snowflake, templateCode: string) { + await this.rest.delete(Routes.guildTemplate(guildId, templateCode)); + } + + /** + * Fetches all the stickers for a guild + * + * @param guildId - The id of the guild to fetch the stickers from + */ + public async getStickers(guildId: Snowflake) { + return this.rest.get(Routes.guildStickers(guildId)) as Promise; + } + + /** + * Fetches a sticker for a guild + * + * @param guildId - The id of the guild to fetch the sticker from + * @param stickerId - The id of the sticker to fetch + */ + public async getSticker(guildId: Snowflake, stickerId: Snowflake) { + return this.rest.get(Routes.guildSticker(guildId, stickerId)) as Promise; + } + + /** + * Creates a sticker for a guild + * + * @param guildId - The id of the guild to create the sticker for + * @param data - The data for creating the sticker + * @param reason - The reason for creating the sticker + */ + public async createSticker( + guildId: Snowflake, + { file, ...body }: Omit & { file: RawFile }, + reason?: string, + ) { + const fileData = { ...file, key: 'file' }; + + return this.rest.post(Routes.guildStickers(guildId), { + appendToFormData: true, + body, + files: [fileData], + reason, + }) as Promise; + } + + /** + * Edits a sticker for a guild + * + * @param guildId - The id of the guild to edit the sticker from + * @param stickerId - The id of the sticker to edit + * @param data - The data for editing the sticker + * @param reason - The reason for editing the sticker + */ + public async editSticker( + guildId: Snowflake, + stickerId: Snowflake, + data: RESTPatchAPIGuildStickerJSONBody, + reason?: string, + ) { + return this.rest.patch(Routes.guildSticker(guildId, stickerId), { + reason, + body: data, + }) as Promise; + } + + /** + * Deletes a sticker for a guild + * + * @param guildId - The id of the guild to delete the sticker from + * @param stickerId - The id of the sticker to delete + * @param reason - The reason for deleting the sticker + */ + public async deleteSticker(guildId: Snowflake, stickerId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildSticker(guildId, stickerId), { reason }); + } + + /** + * Fetches the audit logs for a guild + * + * @param guildId - The id of the guild to fetch the audit logs from + * @param options - The options for fetching the audit logs + */ + public async getAuditLogs(guildId: Snowflake, options: RESTGetAPIAuditLogQuery = {}) { + return this.rest.get(Routes.guildAuditLog(guildId), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Fetches all auto moderation rules for a guild + * + * @param guildId - The id of the guild to fetch the auto moderation rules from + */ + public async getAutoModerationRules(guildId: Snowflake) { + return this.rest.get(Routes.guildAutoModerationRules(guildId)) as Promise; + } + + /** + * Fetches an auto moderation rule for a guild + * + * @param guildId - The id of the guild to fetch the auto moderation rule from + * @param ruleId - The id of the auto moderation rule to fetch + */ + public async getAutoModerationRule(guildId: Snowflake, ruleId: Snowflake) { + return this.rest.get( + Routes.guildAutoModerationRule(guildId, ruleId), + ) as Promise; + } + + /** + * Creates a new auto moderation rule for a guild + * + * @param guildId - The id of the guild to create the auto moderation rule from + * @param data - The data for creating the auto moderation rule + */ + public async createAutoModerationRule( + guildId: Snowflake, + data: RESTPostAPIAutoModerationRuleJSONBody, + reason?: string, + ) { + return this.rest.post(Routes.guildAutoModerationRules(guildId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits an auto moderation rule for a guild + * + * @param guildId - The id of the guild to edit the auto moderation rule from + * @param ruleId - The id of the auto moderation rule to edit + * @param data - The data for editing the auto moderation rule + * @param reason - The reason for editing the auto moderation rule + */ + public async editAutoModerationRule( + guildId: Snowflake, + ruleId: Snowflake, + data: RESTPatchAPIAutoModerationRuleJSONBody, + reason?: string, + ) { + return this.rest.patch(Routes.guildAutoModerationRule(guildId, ruleId), { + reason, + body: data, + }) as Promise; + } + + /** + * Deletes an auto moderation rule for a guild + * + * @param guildId - The id of the guild to delete the auto moderation rule from + * @param ruleId - The id of the auto moderation rule to delete + * @param reason - The reason for deleting the auto moderation rule + */ + public async deleteAutoModerationRule(guildId: Snowflake, ruleId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildAutoModerationRule(guildId, ruleId), { reason }); + } + + /** + * Fetches a guild member + * + * @param guildId - The id of the guild + * @param userId - The id of the user + */ + public async getMember(guildId: Snowflake, userId: Snowflake) { + return this.rest.get(Routes.guildMember(guildId, userId)) as Promise; + } + + /** + * Searches for guild members + * + * @param guildId - The id of the guild to search in + * @param query - The query to search for + * @param limit - The maximum number of members to return + */ + public async searchForMembers(guildId: Snowflake, options: RESTGetAPIGuildMembersSearchQuery) { + return this.rest.get(Routes.guildMembersSearch(guildId), { + query: makeURLSearchParams(options as unknown as Record), + }) as Promise; + } + + /** + * Edits a guild member + * + * @param guildId - The id of the guild + * @param userId - The id of the user + * @param data - The data to use when editing the guild member + * @param reason - The reason for editing this guild member + */ + public async editMember( + guildId: Snowflake, + userId: Snowflake, + data: RESTPatchAPIGuildMemberJSONBody = {}, + reason?: string, + ) { + return this.rest.patch(Routes.guildMember(guildId, userId), { + reason, + body: data, + }) as Promise; + } + + /** + * Adds a role to a guild member + * + * @param guildId - The id of the guild + * @param userId - The id of the user + * @param roleId - The id of the role + * @param reason - The reason for adding this role to the guild member + */ + public async addRoleToMember(guildId: Snowflake, userId: Snowflake, roleId: Snowflake, reason?: string) { + await this.rest.put(Routes.guildMemberRole(guildId, userId, roleId), { reason }); + } + + /** + * Removes a role from a guild member + * + * @param guildId - The id of the guild + * @param userId - The id of the user + * @param roleId - The id of the role + * @param reason - The reason for removing this role from the guild member + */ + public async removeRoleFromMember(guildId: Snowflake, userId: Snowflake, roleId: Snowflake, reason?: string) { + await this.rest.delete(Routes.guildMemberRole(guildId, userId, roleId), { reason }); + } + + /** + * Fetches a guild template + * + * @param templateCode - The code of the template + */ + public async getTemplate(templateCode: string) { + return this.rest.get(Routes.template(templateCode)) as Promise; + } + + /** + * Creates a new template + * + * @param templateCode - The code of the template + * @param data - The data to use when creating the template + */ + public async createTemplate(templateCode: string, data: RESTPostAPITemplateCreateGuildJSONBody) { + return this.rest.post(Routes.template(templateCode), { body: data }) as Promise; + } +} diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts new file mode 100644 index 000000000000..a8282fd234cd --- /dev/null +++ b/packages/core/src/api/index.ts @@ -0,0 +1,57 @@ +import type { REST } from '@discordjs/rest'; +import { ApplicationCommandsAPI } from './applicationCommands.js'; +import { ChannelsAPI } from './channel.js'; +import { GuildsAPI } from './guild.js'; +import { InteractionsAPI } from './interactions.js'; +import { InvitesAPI } from './invite.js'; +import { StickersAPI } from './sticker.js'; +import { ThreadsAPI } from './thread.js'; +import { UsersAPI } from './user.js'; +import { VoiceAPI } from './voice.js'; +import { WebhooksAPI } from './webhook.js'; + +export * from './applicationCommands.js'; +export * from './channel.js'; +export * from './guild.js'; +export * from './interactions.js'; +export * from './invite.js'; +export * from './sticker.js'; +export * from './thread.js'; +export * from './user.js'; +export * from './voice.js'; +export * from './webhook.js'; + +export class API { + public readonly applicationCommands: ApplicationCommandsAPI; + + public readonly channels: ChannelsAPI; + + public readonly guilds: GuildsAPI; + + public readonly interactions: InteractionsAPI; + + public readonly invites: InvitesAPI; + + public readonly stickers: StickersAPI; + + public readonly threads: ThreadsAPI; + + public readonly users: UsersAPI; + + public readonly voice: VoiceAPI; + + public readonly webhooks: WebhooksAPI; + + public constructor(public readonly rest: REST) { + this.applicationCommands = new ApplicationCommandsAPI(rest); + this.channels = new ChannelsAPI(rest); + this.guilds = new GuildsAPI(rest); + this.invites = new InvitesAPI(rest); + this.stickers = new StickersAPI(rest); + this.threads = new ThreadsAPI(rest); + this.users = new UsersAPI(rest); + this.voice = new VoiceAPI(rest); + this.webhooks = new WebhooksAPI(rest); + this.interactions = new InteractionsAPI(rest, this.webhooks); + } +} diff --git a/packages/core/src/api/interactions.ts b/packages/core/src/api/interactions.ts new file mode 100644 index 000000000000..5db65e471650 --- /dev/null +++ b/packages/core/src/api/interactions.ts @@ -0,0 +1,181 @@ +import type { RawFile, REST } from '@discordjs/rest'; +import type { Snowflake } from 'discord-api-types/v10'; +import { + InteractionResponseType, + Routes, + type APICommandAutocompleteInteractionResponseCallbackData, + type APIInteractionResponseCallbackData, + type APIModalInteractionResponseCallbackData, + type RESTGetAPIWebhookWithTokenMessageResult, +} from 'discord-api-types/v10'; +import type { WebhooksAPI } from './webhook.js'; + +export class InteractionsAPI { + public constructor(private readonly rest: REST, private readonly webhooks: WebhooksAPI) {} + + /** + * Replies to an interaction + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + * @param data - The data to use when replying + */ + public async reply( + interactionId: Snowflake, + interactionToken: string, + { files, ...data }: APIInteractionResponseCallbackData & { files?: RawFile[] }, + ) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + files, + body: { + type: InteractionResponseType.ChannelMessageWithSource, + data, + }, + }); + } + + /** + * Defers the reply to an interaction + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + */ + public async defer(interactionId: Snowflake, interactionToken: string) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + body: { + type: InteractionResponseType.DeferredChannelMessageWithSource, + }, + }); + } + + /** + * Defers an update from a message component interaction + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + */ + public async deferMessageUpdate(interactionId: Snowflake, interactionToken: string) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + body: { + type: InteractionResponseType.DeferredMessageUpdate, + }, + }); + } + + /** + * Reply to a deferred interaction + * + * @param applicationId - The application id of the interaction + * @param interactionToken - The token of the interaction + * @param data - The data to use when replying + */ + public async followUp( + applicationId: Snowflake, + interactionToken: string, + data: APIInteractionResponseCallbackData & { files?: RawFile[] }, + ) { + await this.webhooks.execute(applicationId, interactionToken, data); + } + + /** + * Edits the initial reply to an interaction + * + * @param applicationId - The application id of the interaction + * @param interactionToken - The token of the interaction + * @param data - The data to use when editing the reply + * @param messageId - The id of the message to edit. If omitted, the original reply will be edited + */ + public async editReply( + applicationId: Snowflake, + interactionToken: string, + data: APIInteractionResponseCallbackData & { files?: RawFile[] }, + messageId?: string, + ) { + return this.webhooks.editMessage(applicationId, interactionToken, messageId ?? '@original', data); + } + + /** + * Fetches the initial reply to an interaction + * + * @param applicationId - The application id of the interaction + * @param interactionToken - The token of the interaction + */ + public async getOriginalReply(applicationId: Snowflake, interactionToken: string) { + return this.webhooks.getMessage( + applicationId, + interactionToken, + '@original', + ) as Promise; + } + + /** + * Deletes the initial reply to an interaction + * + * @param applicationId - The application id of the interaction + * @param interactionToken - The token of the interaction + */ + public async deleteReply(applicationId: Snowflake, interactionToken: string) { + await this.webhooks.deleteMessage(applicationId, interactionToken, '@original'); + } + + /** + * Updates the the message the component interaction was triggered on + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + * @param data - The data to use when updating the interaction + */ + public async updateMessage( + interactionId: Snowflake, + interactionToken: string, + { files, ...data }: APIInteractionResponseCallbackData & { files?: RawFile[] }, + ) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + files, + body: { + type: InteractionResponseType.UpdateMessage, + data, + }, + }); + } + + /** + * Sends an autocomplete response to an interaction + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + * @param data - Data for the autocomplete response + */ + public async createAutocompleteResponse( + interactionId: Snowflake, + interactionToken: string, + data: APICommandAutocompleteInteractionResponseCallbackData, + ) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + body: { + type: InteractionResponseType.ApplicationCommandAutocompleteResult, + data, + }, + }); + } + + /** + * Sends a modal response to an interaction + * + * @param interactionId - The id of the interaction + * @param interactionToken - The token of the interaction + * @param data - The modal to send + */ + public async createModal( + interactionId: Snowflake, + interactionToken: string, + data: APIModalInteractionResponseCallbackData, + ) { + await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), { + body: { + type: InteractionResponseType.Modal, + data, + }, + }); + } +} diff --git a/packages/core/src/api/invite.ts b/packages/core/src/api/invite.ts new file mode 100644 index 000000000000..44da9150685e --- /dev/null +++ b/packages/core/src/api/invite.ts @@ -0,0 +1,27 @@ +import { makeURLSearchParams, type REST } from '@discordjs/rest'; +import { Routes, type RESTGetAPIInviteQuery, type RESTGetAPIInviteResult } from 'discord-api-types/v10'; + +export class InvitesAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches an invite + * + * @param code - The invite code + */ + public async get(code: string, options: RESTGetAPIInviteQuery = {}) { + return this.rest.get(Routes.invite(code), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Deletes an invite + * + * @param code - The invite code + * @param reason - The reason for deleting the invite + */ + public async delete(code: string, reason?: string) { + await this.rest.delete(Routes.invite(code), { reason }); + } +} diff --git a/packages/core/src/api/sticker.ts b/packages/core/src/api/sticker.ts new file mode 100644 index 000000000000..7a01dc9b03b2 --- /dev/null +++ b/packages/core/src/api/sticker.ts @@ -0,0 +1,27 @@ +import type { REST } from '@discordjs/rest'; +import { + Routes, + type RESTGetAPIStickerResult, + type RESTGetNitroStickerPacksResult, + type Snowflake, +} from 'discord-api-types/v10'; + +export class StickersAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches all of the nitro sticker packs + */ + public async getNitroStickers() { + return this.rest.get(Routes.nitroStickerPacks()) as Promise; + } + + /** + * Fetches a sticker + * + * @param stickerId - The id of the sticker + */ + public async get(stickerId: Snowflake) { + return this.rest.get(Routes.sticker(stickerId)) as Promise; + } +} diff --git a/packages/core/src/api/thread.ts b/packages/core/src/api/thread.ts new file mode 100644 index 000000000000..94f08f1a5f3b --- /dev/null +++ b/packages/core/src/api/thread.ts @@ -0,0 +1,117 @@ +import type { RawFile, REST } from '@discordjs/rest'; +import { + Routes, + type APIThreadChannel, + type APIThreadMember, + type RESTGetAPIChannelThreadMembersResult, + type RESTPostAPIChannelThreadsJSONBody, + type RESTPostAPIChannelThreadsResult, + type RESTPostAPIGuildForumThreadsJSONBody, + type Snowflake, +} from 'discord-api-types/v10'; + +export interface StartThreadOptions extends RESTPostAPIChannelThreadsJSONBody { + message_id?: string; +} + +export interface StartForumThreadOptions extends RESTPostAPIGuildForumThreadsJSONBody { + message: RESTPostAPIGuildForumThreadsJSONBody['message'] & { files?: RawFile[] }; +} + +export class ThreadsAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches a thread + * + * @param channelId - The id of the channel to fetch the thread from + * @param threadId - The id of the thread + */ + public async get(channelId: Snowflake, threadId: Snowflake) { + return this.rest.get(Routes.threads(channelId, threadId)) as Promise; + } + + /** + * Creates a new thread + * + * @param channelId - The id of the channel to start the thread in + * @param data - The data to use when starting the thread + */ + public async create(channelId: Snowflake, { message_id, ...body }: StartThreadOptions) { + return this.rest.post(Routes.threads(channelId, message_id), { body }) as Promise; + } + + /** + * Creates a new forum post + * + * @param channelId - The id of the forum channel to start the thread in + * @param data - The data to use when starting the thread + */ + public async createForumThread(channelId: Snowflake, { message, ...optionsBody }: StartForumThreadOptions) { + const { files, ...messageBody } = message; + + const body = { + ...optionsBody, + message: messageBody, + }; + + return this.rest.post(Routes.threads(channelId), { files, body }) as Promise; + } + + /** + * Adds the current user to a thread + * + * @param threadId - The id of the thread to join + */ + public async join(threadId: Snowflake) { + await this.rest.put(Routes.threadMembers(threadId, '@me')); + } + + /** + * Adds a member to a thread + * + * @param threadId - The id of the thread to add the member to + * @param userId - The id of the user to add to the thread + */ + public async addMember(threadId: Snowflake, userId: Snowflake) { + await this.rest.put(Routes.threadMembers(threadId, userId)); + } + + /** + * Removes the current user from a thread + * + * @param threadId - The id of the thread to leave + */ + public async leave(threadId: Snowflake) { + await this.rest.delete(Routes.threadMembers(threadId, '@me')); + } + + /** + * Removes a member from a thread + * + * @param threadId - The id of the thread to remove the member from + * @param userId - The id of the user to remove from the thread + */ + public async removeMember(threadId: Snowflake, userId: Snowflake) { + await this.rest.delete(Routes.threadMembers(threadId, userId)); + } + + /** + * Fetches a member of a thread + * + * @param threadId - The id of the thread to fetch the member from + * @param userId - The id of the user + */ + public async getMember(threadId: Snowflake, userId: Snowflake) { + return this.rest.get(Routes.threadMembers(threadId, userId)) as Promise; + } + + /** + * Fetches all members of a thread + * + * @param threadId - The id of the thread to fetch the members from + */ + public async getAllMembers(threadId: Snowflake) { + return this.rest.get(Routes.threadMembers(threadId)) as Promise; + } +} diff --git a/packages/core/src/api/user.ts b/packages/core/src/api/user.ts new file mode 100644 index 000000000000..5f03bb1b49fc --- /dev/null +++ b/packages/core/src/api/user.ts @@ -0,0 +1,112 @@ +import { makeURLSearchParams, type REST } from '@discordjs/rest'; +import { + Routes, + type RESTGetAPICurrentUserGuildsQuery, + type RESTGetAPICurrentUserGuildsResult, + type RESTGetAPICurrentUserResult, + type RESTGetAPIUserResult, + type RESTGetCurrentUserGuildMemberResult, + type RESTPatchAPICurrentUserJSONBody, + type RESTPatchAPICurrentUserResult, + type RESTPatchAPIGuildMemberJSONBody, + type RESTPatchAPIGuildMemberResult, + type RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody, + type RESTPatchAPIGuildVoiceStateCurrentMemberResult, + type RESTPostAPICurrentUserCreateDMChannelResult, + type Snowflake, +} from 'discord-api-types/v10'; + +export class UsersAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches a user by their id + * + * @param userId - The id of the user to fetch + */ + public async get(userId: Snowflake) { + return this.rest.get(Routes.user(userId)) as Promise; + } + + /** + * Returns the user object of the requester's account + */ + public async getCurrent() { + return this.rest.get(Routes.user('@me')) as Promise; + } + + /** + * Returns a list of partial guild objects the current user is a member of + * + * @param options - The options to use when fetching the current user's guilds + */ + public async getGuilds(options: RESTGetAPICurrentUserGuildsQuery = {}) { + return this.rest.get(Routes.userGuilds(), { + query: makeURLSearchParams(options as Record), + }) as Promise; + } + + /** + * Leaves the guild with the given id + * + * @param guildId - The id of the guild + */ + public async leaveGuild(guildId: Snowflake) { + await this.rest.delete(Routes.userGuild(guildId)); + } + + /** + * Edits the current user + * + * @param user - The new data for the current user + */ + public async edit(user: RESTPatchAPICurrentUserJSONBody) { + return this.rest.patch(Routes.user('@me'), { body: user }) as Promise; + } + + /** + * Fetches the guild member for the current user + * + * @param guildId - The id of the guild + */ + public async getGuildMember(guildId: Snowflake) { + return this.rest.get(Routes.userGuildMember(guildId)) as Promise; + } + + /** + * Edits the guild member for the current user + * + * @param guildId - The id of the guild + * @param member - The new data for the guild member + * @param reason - The reason for editing this guild member + */ + public async editGuildMember(guildId: Snowflake, member: RESTPatchAPIGuildMemberJSONBody = {}, reason?: string) { + return this.rest.patch(Routes.guildMember(guildId, '@me'), { + reason, + body: member, + }) as Promise; + } + + /** + * Sets the voice state for the current user + * + * @param guildId - The id of the guild + * @param options - The options to use when setting the voice state + */ + public async setVoiceState(guildId: Snowflake, options: RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody = {}) { + return this.rest.patch(Routes.guildVoiceState(guildId, '@me'), { + body: options, + }) as Promise; + } + + /** + * Opens a new DM channel with a user + * + * @param userId - The id of the user to open a DM channel with + */ + public async createDM(userId: Snowflake) { + return this.rest.post(Routes.userChannels(), { + body: { recipient_id: userId }, + }) as Promise; + } +} diff --git a/packages/core/src/api/voice.ts b/packages/core/src/api/voice.ts new file mode 100644 index 000000000000..e04441822db8 --- /dev/null +++ b/packages/core/src/api/voice.ts @@ -0,0 +1,13 @@ +import type { REST } from '@discordjs/rest'; +import { Routes, type GetAPIVoiceRegionsResult } from 'discord-api-types/v10'; + +export class VoiceAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches all voice regions + */ + public async getVoiceRegions() { + return this.rest.get(Routes.voiceRegions()) as Promise; + } +} diff --git a/packages/core/src/api/webhook.ts b/packages/core/src/api/webhook.ts new file mode 100644 index 000000000000..b4da4997d649 --- /dev/null +++ b/packages/core/src/api/webhook.ts @@ -0,0 +1,214 @@ +import { makeURLSearchParams, type RawFile, type REST } from '@discordjs/rest'; +import { + Routes, + type RESTGetAPIChannelMessageResult, + type RESTGetAPIWebhookResult, + type RESTPatchAPIWebhookJSONBody, + type RESTPatchAPIWebhookResult, + type RESTPatchAPIWebhookWithTokenMessageJSONBody, + type RESTPatchAPIWebhookWithTokenMessageResult, + type RESTPostAPIChannelWebhookJSONBody, + type RESTPostAPIWebhookWithTokenGitHubQuery, + type RESTPostAPIWebhookWithTokenJSONBody, + type RESTPostAPIWebhookWithTokenQuery, + type RESTPostAPIWebhookWithTokenResult, + type RESTPostAPIWebhookWithTokenSlackQuery, + type RESTPostAPIWebhookWithTokenWaitResult, + type Snowflake, +} from 'discord-api-types/v10'; + +export class WebhooksAPI { + public constructor(private readonly rest: REST) {} + + /** + * Fetches a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + */ + public async get(id: Snowflake, token?: string) { + return this.rest.get(Routes.webhook(id, token)) as Promise; + } + + /** + * Creates a new webhook + * + * @param channelId - The id of the channel to create the webhook in + * @param data - The data to use when creating the webhook + * @param reason - The reason for creating the webhook + */ + public async create(channelId: Snowflake, data: RESTPostAPIChannelWebhookJSONBody, reason?: string) { + return this.rest.post(Routes.channelWebhooks(channelId), { + reason, + body: data, + }) as Promise; + } + + /** + * Edits a webhook + * + * @param id - The id of the webhook to edit + * @param webhook - The new webhook data + * @param options - The options to use when editing the webhook + */ + public async edit( + id: Snowflake, + webhook: RESTPatchAPIWebhookJSONBody, + { token, reason }: { reason?: string; token?: string } = {}, + ) { + return this.rest.patch(Routes.webhook(id, token), { reason, body: webhook }) as Promise; + } + + /** + * Deletes a webhook + * + * @param id - The id of the webhook to delete + * @param options - The options to use when deleting the webhook + */ + public async delete(id: Snowflake, { token, reason }: { reason?: string; token?: string } = {}) { + await this.rest.delete(Routes.webhook(id, token), { reason }); + } + + /** + * Executes a webhook and returns the created message + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param data - The data to use when executing the webhook + */ + public async execute( + id: Snowflake, + token: string, + data: RESTPostAPIWebhookWithTokenJSONBody & RESTPostAPIWebhookWithTokenQuery & { files?: RawFile[]; wait: true }, + ): Promise; + + /** + * Executes a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param data - The data to use when executing the webhook + */ + public async execute( + id: Snowflake, + token: string, + data: RESTPostAPIWebhookWithTokenJSONBody & RESTPostAPIWebhookWithTokenQuery & { files?: RawFile[]; wait?: false }, + ): Promise; + + /** + * Executes a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param data - The data to use when executing the webhook + */ + public async execute( + id: Snowflake, + token: string, + { + wait, + thread_id, + files, + ...body + }: RESTPostAPIWebhookWithTokenJSONBody & RESTPostAPIWebhookWithTokenQuery & { files?: RawFile[] }, + ) { + return this.rest.post(Routes.webhook(id, token), { + query: makeURLSearchParams({ wait, thread_id }), + files, + body, + auth: false, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + }) as Promise; + } + + /** + * Executes a slack webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param options - The options to use when executing the webhook + */ + public async executeSlack( + id: Snowflake, + token: string, + body: unknown, + options: RESTPostAPIWebhookWithTokenSlackQuery = {}, + ) { + await this.rest.post(Routes.webhookPlatform(id, token, 'slack'), { + query: makeURLSearchParams(options as Record), + body, + auth: false, + }); + } + + /** + * Executes a github webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param options - The options to use when executing the webhook + */ + public async executeGitHub( + id: Snowflake, + token: string, + body: unknown, + options: RESTPostAPIWebhookWithTokenGitHubQuery = {}, + ) { + await this.rest.post(Routes.webhookPlatform(id, token, 'github'), { + query: makeURLSearchParams(options as Record), + body, + auth: false, + }); + } + + /** + * Fetches an associated message from a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param messageId - The id of the message to fetch + * @param options - The options to use when fetching the message + */ + public async getMessage(id: Snowflake, token: string, messageId: Snowflake, options: { thread_id?: string } = {}) { + return this.rest.get(Routes.webhookMessage(id, token, messageId), { + query: makeURLSearchParams(options as Record), + auth: false, + }) as Promise; + } + + /** + * Edits an associated message from a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param messageId - The id of the message to edit + * @param data - The data to use when editing the message + */ + public async editMessage( + id: Snowflake, + token: string, + messageId: Snowflake, + { thread_id, ...body }: RESTPatchAPIWebhookWithTokenMessageJSONBody & { thread_id?: string }, + ) { + return this.rest.patch(Routes.webhookMessage(id, token, messageId), { + query: makeURLSearchParams({ thread_id }), + auth: false, + body, + }) as Promise; + } + + /** + * Deletes an associated message from a webhook + * + * @param id - The id of the webhook + * @param token - The token of the webhook + * @param messageId - The id of the message to delete + * @param options - The options to use when deleting the message + */ + public async deleteMessage(id: Snowflake, token: string, messageId: Snowflake, options: { thread_id?: string } = {}) { + await this.rest.delete(Routes.webhookMessage(id, token, messageId), { + query: makeURLSearchParams(options as Record), + auth: false, + }); + } +} diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts new file mode 100644 index 000000000000..04652a7304ea --- /dev/null +++ b/packages/core/src/client.ts @@ -0,0 +1,179 @@ +import type { REST } from '@discordjs/rest'; +import { WebSocketShardEvents, type WebSocketManager } from '@discordjs/ws'; +import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; +import type { + GatewayAutoModerationActionExecutionDispatchData, + GatewayAutoModerationRuleCreateDispatchData, + GatewayAutoModerationRuleDeleteDispatchData, + GatewayAutoModerationRuleUpdateDispatchData, + GatewayChannelCreateDispatchData, + GatewayChannelDeleteDispatchData, + GatewayChannelPinsUpdateDispatchData, + GatewayChannelUpdateDispatchData, + GatewayDispatchEvents, + GatewayGuildBanAddDispatchData, + GatewayGuildBanRemoveDispatchData, + GatewayGuildCreateDispatchData, + GatewayGuildDeleteDispatchData, + GatewayGuildEmojisUpdateDispatchData, + GatewayGuildIntegrationsUpdateDispatchData, + GatewayGuildMemberAddDispatchData, + GatewayGuildMemberRemoveDispatchData, + GatewayGuildMembersChunkDispatchData, + GatewayGuildMemberUpdateDispatchData, + GatewayGuildRoleCreateDispatchData, + GatewayGuildRoleDeleteDispatchData, + GatewayGuildRoleUpdateDispatchData, + GatewayGuildScheduledEventCreateDispatchData, + GatewayGuildScheduledEventDeleteDispatchData, + GatewayGuildScheduledEventUpdateDispatchData, + GatewayGuildScheduledEventUserAddDispatchData, + GatewayGuildScheduledEventUserRemoveDispatchData, + GatewayGuildStickersUpdateDispatchData, + GatewayGuildUpdateDispatchData, + GatewayIntegrationCreateDispatchData, + GatewayIntegrationDeleteDispatchData, + GatewayIntegrationUpdateDispatchData, + GatewayInteractionCreateDispatchData, + GatewayInviteCreateDispatchData, + GatewayInviteDeleteDispatchData, + GatewayMessageCreateDispatchData, + GatewayMessageDeleteBulkDispatchData, + GatewayMessageDeleteDispatchData, + GatewayMessageReactionAddDispatchData, + GatewayMessageReactionRemoveAllDispatchData, + GatewayMessageReactionRemoveDispatchData, + GatewayMessageReactionRemoveEmojiDispatchData, + GatewayMessageUpdateDispatchData, + GatewayPresenceUpdateDispatchData, + GatewayReadyDispatchData, + GatewayStageInstanceCreateDispatchData, + GatewayStageInstanceDeleteDispatchData, + GatewayStageInstanceUpdateDispatchData, + GatewayThreadCreateDispatchData, + GatewayThreadDeleteDispatchData, + GatewayThreadListSyncDispatchData, + GatewayThreadMembersUpdateDispatchData, + GatewayThreadMemberUpdateDispatchData, + GatewayThreadUpdateDispatchData, + GatewayTypingStartDispatchData, + GatewayUserUpdateDispatchData, + GatewayVoiceServerUpdateDispatchData, + GatewayVoiceStateUpdateDispatchData, + GatewayWebhooksUpdateDispatchData, +} from 'discord-api-types/v10'; +import { API } from './api/index.js'; + +export interface IntrinsicProps { + /** + * The REST API + */ + api: API; + /** + * The id of the shard that emitted the event + */ + shardId: number; +} + +export interface WithIntrinsicProps extends IntrinsicProps { + data: T; +} + +export interface MappedEvents { + [GatewayDispatchEvents.ChannelCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ChannelDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ChannelPinsUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ChannelUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildBanAdd]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildBanRemove]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildEmojisUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildIntegrationsUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildMemberAdd]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildMemberRemove]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildMemberUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildMembersChunk]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildRoleCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildRoleDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildRoleUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildScheduledEventCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildScheduledEventDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildScheduledEventUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildScheduledEventUserAdd]: [ + WithIntrinsicProps, + ]; + [GatewayDispatchEvents.GuildScheduledEventUserRemove]: [ + WithIntrinsicProps, + ]; + [GatewayDispatchEvents.GuildStickersUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.GuildUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.IntegrationCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.IntegrationDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.IntegrationUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.InteractionCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.InviteCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.InviteDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageDeleteBulk]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageReactionAdd]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageReactionRemove]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageReactionRemoveAll]: [WithIntrinsicProps]; + [GatewayDispatchEvents.MessageReactionRemoveEmoji]: [ + WithIntrinsicProps, + ]; + [GatewayDispatchEvents.MessageUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.PresenceUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.Ready]: [WithIntrinsicProps]; + [GatewayDispatchEvents.StageInstanceCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.StageInstanceDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.StageInstanceUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadListSync]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadMemberUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadMembersUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.ThreadUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.UserUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.VoiceServerUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.VoiceStateUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.WebhooksUpdate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.Resumed]: [WithIntrinsicProps]; + [GatewayDispatchEvents.TypingStart]: [WithIntrinsicProps]; + [GatewayDispatchEvents.AutoModerationActionExecution]: [ + WithIntrinsicProps, + ]; + [GatewayDispatchEvents.AutoModerationRuleCreate]: [WithIntrinsicProps]; + [GatewayDispatchEvents.AutoModerationRuleDelete]: [WithIntrinsicProps]; + [GatewayDispatchEvents.AutoModerationRuleUpdate]: [WithIntrinsicProps]; +} + +export type ManagerShardEventsMap = { + [K in keyof MappedEvents]: MappedEvents[K]; +}; + +export interface ClientOptions { + rest: REST; + ws: WebSocketManager; +} + +export function createClient({ rest, ws }: ClientOptions) { + const api = new API(rest); + const emitter = new AsyncEventEmitter(); + + function wrapIntrinsicProps(obj: T, shardId: number): WithIntrinsicProps { + return { + api, + shardId, + data: obj, + }; + } + + ws.on(WebSocketShardEvents.Dispatch, ({ data: dispatch, shardId }) => { + // @ts-expect-error event props can't be resolved properly, but they are correct + emitter.emit(dispatch.t, wrapIntrinsicProps(dispatch.d, shardId)); + }); + + return emitter; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 000000000000..86f5c52478f6 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,5 @@ +export * from './api/index.js'; +export * from './client.js'; +export * from './util/index.js'; + +export * from 'discord-api-types/v10'; diff --git a/packages/core/src/util/files.ts b/packages/core/src/util/files.ts new file mode 100644 index 000000000000..7f22fb07434c --- /dev/null +++ b/packages/core/src/util/files.ts @@ -0,0 +1,29 @@ +import type { RawFile } from '@discordjs/rest'; +import type { APIInteractionResponseCallbackData } from 'discord-api-types/v10'; + +export interface DescriptiveRawFile extends RawFile { + description?: string; +} + +/** + * A utility function to create a form data payload given an array of file buffers + * + * @param files - The files to create a form data payload for + * @param options - The additional options for the form data payload + */ +export function withFiles(files: DescriptiveRawFile[], options: APIInteractionResponseCallbackData) { + const body = { + ...options, + attachments: files.map((file, index) => ({ + id: index.toString(), + description: file.description, + })), + }; + + const outputFiles = files.map((file, index) => ({ + name: file.name ?? index.toString(), + data: file.data, + })); + + return { body, files: outputFiles }; +} diff --git a/packages/core/src/util/index.ts b/packages/core/src/util/index.ts new file mode 100644 index 000000000000..f2891a7c9727 --- /dev/null +++ b/packages/core/src/util/index.ts @@ -0,0 +1 @@ +export * from './files.js'; diff --git a/packages/core/tsconfig.eslint.json b/packages/core/tsconfig.eslint.json new file mode 100644 index 000000000000..d04d4be3aedc --- /dev/null +++ b/packages/core/tsconfig.eslint.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.mjs", + "**/*.jsx", + "**/*.test.ts", + "**/*.test.js", + "**/*.test.mjs", + "**/*.spec.ts", + "**/*.spec.js", + "**/*.spec.mjs" + ], + "exclude": [] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 000000000000..fd8b5e417b9f --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/core/tsup.config.js b/packages/core/tsup.config.js new file mode 100644 index 000000000000..3d4480d6d4aa --- /dev/null +++ b/packages/core/tsup.config.js @@ -0,0 +1,3 @@ +import { createTsupConfig } from '../../tsup.config.js'; + +export default createTsupConfig({}); diff --git a/packages/rest/src/lib/RequestManager.ts b/packages/rest/src/lib/RequestManager.ts index 4fa8dab221bf..18d03ae3d657 100644 --- a/packages/rest/src/lib/RequestManager.ts +++ b/packages/rest/src/lib/RequestManager.ts @@ -88,7 +88,7 @@ export interface RequestData { /** * Reason to show in the audit logs */ - reason?: string; + reason?: string | undefined; /** * The signal to abort the queue entry or the REST call, where applicable */ diff --git a/yarn.lock b/yarn.lock index 244c4d197364..e795e37070f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,6 +2089,29 @@ __metadata: languageName: unknown linkType: soft +"@discordjs/core@workspace:packages/core": + version: 0.0.0-use.local + resolution: "@discordjs/core@workspace:packages/core" + dependencies: + "@discordjs/rest": "workspace:^" + "@discordjs/ws": "workspace:^" + "@favware/cliff-jumper": ^1.9.0 + "@microsoft/api-extractor": ^7.33.6 + "@types/node": 16.18.3 + "@vitest/coverage-c8": ^0.25.3 + "@vladfrangu/async_event_emitter": ^2.1.2 + cross-env: ^7.0.3 + discord-api-types: ^0.37.20 + eslint: ^8.28.0 + eslint-config-neon: ^0.1.40 + eslint-formatter-pretty: ^4.1.0 + prettier: ^2.8.0 + tsup: ^6.5.0 + typescript: ^4.9.3 + vitest: ^0.25.3 + languageName: unknown + linkType: soft + "@discordjs/discord.js@workspace:.": version: 0.0.0-use.local resolution: "@discordjs/discord.js@workspace:." @@ -2420,7 +2443,7 @@ __metadata: languageName: unknown linkType: soft -"@discordjs/ws@workspace:packages/ws": +"@discordjs/ws@workspace:^, @discordjs/ws@workspace:packages/ws": version: 0.0.0-use.local resolution: "@discordjs/ws@workspace:packages/ws" dependencies: