├── .github └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── esm.mjs ├── examples ├── applicationCommands.js ├── components-v2.js ├── components.js ├── embed.js ├── intent.js ├── pingpong.js ├── playFile.js └── sharding.js ├── index.d.ts ├── index.js ├── lib ├── Client.js ├── Constants.js ├── errors │ ├── DiscordHTTPError.js │ └── DiscordRESTError.js ├── gateway │ ├── Shard.js │ └── ShardManager.js ├── rest │ ├── Endpoints.js │ └── RequestHandler.js ├── structures │ ├── ApplicationCommand.js │ ├── Attachment.js │ ├── AutoModerationRule.js │ ├── AutocompleteInteraction.js │ ├── Base.js │ ├── CategoryChannel.js │ ├── Channel.js │ ├── CommandInteraction.js │ ├── ComponentInteraction.js │ ├── Entitlement.js │ ├── ExtendedUser.js │ ├── ForumChannel.js │ ├── GroupChannel.js │ ├── Guild.js │ ├── GuildAuditLogEntry.js │ ├── GuildChannel.js │ ├── GuildIntegration.js │ ├── GuildPreview.js │ ├── GuildScheduledEvent.js │ ├── GuildTemplate.js │ ├── Interaction.js │ ├── InteractionMetadata.js │ ├── Invite.js │ ├── MediaChannel.js │ ├── Member.js │ ├── Message.js │ ├── ModalSubmitInteraction.js │ ├── NewsChannel.js │ ├── NewsThreadChannel.js │ ├── Permission.js │ ├── PermissionOverwrite.js │ ├── PingInteraction.js │ ├── PrivateChannel.js │ ├── PrivateThreadChannel.js │ ├── PublicThreadChannel.js │ ├── Role.js │ ├── SKU.js │ ├── SoundboardSound.js │ ├── StageChannel.js │ ├── StageInstance.js │ ├── Subscription.js │ ├── TextChannel.js │ ├── TextVoiceChannel.js │ ├── ThreadChannel.js │ ├── ThreadMember.js │ ├── UnavailableGuild.js │ ├── User.js │ ├── VoiceChannel.js │ └── VoiceState.js ├── util │ ├── BrowserWebSocket.js │ ├── Bucket.js │ ├── Collection.js │ ├── MultipartData.js │ ├── Opus.js │ ├── SequentialBucket.js │ └── emitDeprecation.js └── voice │ ├── Piper.js │ ├── SharedStream.js │ ├── VoiceConnection.js │ ├── VoiceConnectionManager.js │ ├── VoiceDataStream.js │ └── streams │ ├── BaseTransformer.js │ ├── DCAOpusTransformer.js │ ├── FFmpegDuplex.js │ ├── FFmpegOggTransformer.js │ ├── FFmpegPCMTransformer.js │ ├── OggOpusTransformer.js │ ├── PCMOpusTransformer.js │ ├── VolumeTransformer.js │ └── WebmOpusTransformer.js ├── package.json └── tsconfig.json /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: JavaScript/TypeScript linting 2 | on: [ pull_request ] 3 | 4 | jobs: 5 | lint: 6 | name: Lint all files 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Setup Node.js 11 | uses: actions/setup-node@v4 12 | with: 13 | node-version: lts/* 14 | - name: Checkout the repository 15 | uses: actions/checkout@v4 16 | - name: Install dependencies 17 | run: npm i --omit=optional 18 | - name: Lint all files 19 | run: "npm run lint" 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [released] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | release: 9 | name: Release to NPM 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: lts/* 19 | registry-url: 'https://registry.npmjs.org' 20 | - name: Checkout the repository 21 | uses: actions/checkout@v4 22 | - name: Package the project 23 | run: | 24 | mkdir -p ${{ runner.temp }}/gha-dist 25 | npm pack . --pack-destination ${{ runner.temp }}/gha-dist/ 26 | - name: Upload the built package as an artifact 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: npm-package 30 | path: 31 | ${{ runner.temp }}/gha-dist/*.tgz 32 | - name: Publish the package to NPM 33 | if: github.event_name == 'release' 34 | env: 35 | NODE_AUTH_TOKEN: ${{ github.event_name == 'release' && secrets.NPM_TOKEN || '' }} 36 | run: npm publish --provenance --access public 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | docgen 4 | docs 5 | node_modules 6 | test 7 | npm-debug.log 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintrc*.yml 3 | *.log 4 | *.tar.gz 5 | CONTRIBUTING.md 6 | examples 7 | docgen 8 | docs 9 | node_modules 10 | test 11 | .github 12 | eslint.config.mjs 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering contributing! 4 | 5 | A few rules of thumb: 6 | - Code contributions should match the existing code style. 7 | - `npm run lint` to check the formatting 8 | - `npm run lint:fix` to fix some of the issues 9 | - Discuss additions/changes with us [on Discord](https://discord.gg/2uUvgJzgCE) before working on them 10 | - Check the dev branch to make sure someone hasn't worked on the same thing already 11 | - Pull requests should point to the dev branch. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 abalabahaha 4 | Copyright (c) 2022- Project Dysnomia Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project Dysnomia 2 | ==== 3 | 4 | A fork of [Eris](https://github.com/abalabahaha/eris), a Node.js wrapper for interfacing with Discord, focused on keeping up with the latest Discord API changes. 5 | 6 | Installing 7 | ---------- 8 | 9 | You will need Node.js 18+. Voice support requires [additional software](https://github.com/nodejs/node-gyp#installation). 10 | 11 | ``` 12 | npm install --omit=optional @projectdysnomia/dysnomia 13 | ``` 14 | 15 | If you'd like to install the development versions of the library, use the following command instead: 16 | ``` 17 | npm install --omit=optional "github:projectdysnomia/dysnomia#dev" 18 | ``` 19 | 20 | If you need voice support, remove the `--omit=optional`. 21 | 22 | Ping Pong Example 23 | ----------------- 24 | 25 | ```js 26 | const Dysnomia = require("@projectdysnomia/dysnomia"); 27 | 28 | // Replace TOKEN with your bot account's token 29 | const bot = new Dysnomia.Client("Bot TOKEN", { 30 | gateway: { 31 | intents: [ 32 | "guildMessages" 33 | ] 34 | } 35 | }); 36 | 37 | bot.on("ready", () => { // When the bot is ready 38 | console.log("Ready!"); // Log "Ready!" 39 | }); 40 | 41 | bot.on("error", (err) => { 42 | console.error(err); // or your preferred logger 43 | }); 44 | 45 | bot.on("messageCreate", (msg) => { // When a message is created 46 | if(msg.content === "!ping") { // If the message content is "!ping" 47 | bot.createMessage(msg.channel.id, "Pong!"); 48 | // Send a message in the same channel with "Pong!" 49 | } else if(msg.content === "!pong") { // Otherwise, if the message is "!pong" 50 | bot.createMessage(msg.channel.id, "Ping!"); 51 | // Respond with "Ping!" 52 | } 53 | }); 54 | 55 | bot.connect(); // Get the bot to connect to Discord 56 | ``` 57 | 58 | More examples can be found in [the examples folder](examples). 59 | 60 | Useful Links 61 | ------------ 62 | 63 | - [The official Project Dysnomia server](https://discord.gg/2uUvgJzgCE) is the best place to get support. 64 | - [The GitHub repo](https://github.com/projectdysnomia/dysnomia) is where development primarily happens. 65 | - [The NPM package webpage](https://npmjs.com/package/@projectdysnomia/dysnomia) is, well, the webpage for the NPM package. 66 | 67 | License 68 | ------- 69 | 70 | Refer to the [LICENSE](LICENSE) file. 71 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import stylistic from "@stylistic/eslint-plugin"; 3 | import ts from "typescript-eslint"; 4 | import globals from "globals"; 5 | import sortClassMembers from "eslint-plugin-sort-class-members"; 6 | import jsdoc from "eslint-plugin-jsdoc"; 7 | 8 | const cjsFiles = [ 9 | "lib/**/*.js", 10 | "examples/**/*.js", 11 | "*.js" 12 | ]; 13 | 14 | const jsFiles = [ 15 | ...cjsFiles, 16 | "lib/**/*.mjs", 17 | "examples/**/*.mjs", 18 | "*.mjs" 19 | ]; 20 | 21 | const tsFiles = [ 22 | "lib/**/*.ts", 23 | "examples/**/*.ts", 24 | "*.ts" 25 | ]; 26 | 27 | const classSortCommon = { 28 | groups: { 29 | "alphabetical-getters": [ 30 | { 31 | kind: "get", 32 | sort: "alphabetical", 33 | static: false 34 | } 35 | ], 36 | "alphabetical-methods": [ 37 | { 38 | type: "method", 39 | sort: "alphabetical", 40 | static: false 41 | } 42 | ], 43 | "alphabetical-properties": [ 44 | { 45 | type: "property", 46 | sort: "alphabetical", 47 | static: false 48 | } 49 | ], 50 | "alphabetical-private-properties": [ 51 | { 52 | type: "property", 53 | sort: "alphabetical", 54 | private: true 55 | } 56 | ], 57 | "alphabetical-conventional-private-methods": [ 58 | { 59 | name: "/_.+/", 60 | type: "method", 61 | sort: "alphabetical" 62 | } 63 | ], 64 | "alphabetical-private-methods": [ 65 | { 66 | type: "method", 67 | sort: "alphabetical", 68 | private: true 69 | } 70 | ], 71 | "custom-inspect-method": [ 72 | { 73 | name: "[util.inspect.custom]", 74 | type: "method" 75 | } 76 | ], 77 | "screaming-snake-case-static-properties": [ 78 | { 79 | name: "/^[A-Z_0-9]+$/", 80 | type: "property", 81 | static: true 82 | } 83 | ], 84 | "alphabetical-static-properties": [ 85 | { 86 | type: "property", 87 | sort: "alphabetical", 88 | static: true 89 | } 90 | ], 91 | "alphabetical-static-methods": [ 92 | { 93 | type: "method", 94 | sort: "alphabetical", 95 | static: true 96 | } 97 | ] 98 | } 99 | }; 100 | 101 | export default ts.config( 102 | { 103 | files: jsFiles, 104 | extends: [ 105 | js.configs.recommended 106 | ], 107 | plugins: { 108 | "sort-class-members": sortClassMembers 109 | }, 110 | languageOptions: { 111 | globals: { 112 | window: true, 113 | ...globals.node 114 | } 115 | }, 116 | rules: { 117 | "curly": "error", 118 | "prefer-object-has-own": "error", 119 | "no-trailing-spaces": "error", 120 | "no-var": "error", 121 | "object-shorthand": [ 122 | "error", 123 | "consistent-as-needed" 124 | ], 125 | "prefer-const": "error", 126 | "require-atomic-updates": "warn", 127 | "eqeqeq": [ 128 | "error", 129 | "allow-null" 130 | ], 131 | "sort-class-members/sort-class-members": [ 132 | "error", 133 | { 134 | ...classSortCommon, 135 | order: [ 136 | "[alphabetical-properties]", 137 | "[alphabetical-private-properties]", 138 | "constructor", 139 | "update", 140 | "[alphabetical-getters]", 141 | "[alphabetical-methods]", 142 | "[alphabetical-conventional-private-methods]", 143 | "[alphabetical-private-methods]", 144 | "[everything-else]", 145 | "[custom-inspect-method]", 146 | "toString", 147 | "toJSON" 148 | ] 149 | } 150 | ] 151 | } 152 | }, 153 | { 154 | files: cjsFiles, 155 | languageOptions: { 156 | sourceType: "commonjs" 157 | }, 158 | plugins: { 159 | jsdoc 160 | }, 161 | settings: { 162 | jsdoc: { 163 | preferredTypes: { 164 | bigint: "BigInt", 165 | boolean: "Boolean", 166 | number: "Number", 167 | object: "Object", 168 | string: "String" 169 | }, 170 | tagNamePreference: { 171 | property: "prop", 172 | augments: "extends" 173 | } 174 | } 175 | }, 176 | rules: { 177 | "jsdoc/check-types": "error", 178 | "jsdoc/check-tag-names": "error", 179 | "jsdoc/check-alignment": "error" 180 | } 181 | }, 182 | { 183 | files: tsFiles, 184 | extends: [ 185 | ...ts.configs.recommended 186 | ], 187 | rules: { 188 | "@typescript-eslint/consistent-type-definitions": "error" 189 | } 190 | }, 191 | { 192 | files: [ 193 | ...jsFiles, 194 | ...tsFiles 195 | ], 196 | extends: [ 197 | stylistic.configs.customize({ 198 | arrowParens: true, 199 | braceStyle: "1tbs", 200 | indent: 4, 201 | quotes: "double", 202 | semi: true, 203 | commaDangle: "never" 204 | }) 205 | ], 206 | rules: { 207 | "@stylistic/keyword-spacing": [ 208 | "error", 209 | { 210 | after: true, 211 | overrides: { 212 | catch: {after: false}, 213 | for: {after: false}, 214 | if: {after: false}, 215 | switch: {after: false}, 216 | while: {after: false} 217 | } 218 | } 219 | ], 220 | "@stylistic/object-curly-spacing": [ 221 | "error", 222 | "never" 223 | ], 224 | "@stylistic/space-before-function-paren": "off" 225 | } 226 | }, 227 | { 228 | files: ["index.d.ts"], 229 | plugins: { 230 | "sort-class-members": sortClassMembers 231 | }, 232 | rules: { 233 | "@stylistic/indent": ["error", 2], 234 | "@stylistic/object-curly-spacing": ["error", "always"], 235 | "@typescript-eslint/no-explicit-any": "off", 236 | "@typescript-eslint/ban-ts-comment": ["error", { 237 | "ts-expect-error": "allow-with-description", 238 | "ts-ignore": "allow-with-description" 239 | }], 240 | "@typescript-eslint/no-require-imports": "off", 241 | "sort-class-members/sort-class-members": ["error", { 242 | ...classSortCommon, 243 | order: [ 244 | "[screaming-snake-case-static-properties]", 245 | "[alphabetical-static-properties]", 246 | "[alphabetical-properties]", 247 | "constructor", 248 | "[alphabetical-static-methods]", 249 | "[alphabetical-methods]", 250 | "on", 251 | "[everything-else]", 252 | "[custom-inspect-method]", 253 | "toString", 254 | "toJSON" 255 | ] 256 | }] 257 | } 258 | } 259 | ); 260 | -------------------------------------------------------------------------------- /esm.mjs: -------------------------------------------------------------------------------- 1 | import Dysnomia from "./index.js"; 2 | 3 | export const { 4 | ApplicationCommand, 5 | Attachment, 6 | AutocompleteInteraction, 7 | AutoModerationRule, 8 | Base, 9 | Bucket, 10 | CategoryChannel, 11 | Channel, 12 | Client, 13 | Collection, 14 | CommandInteraction, 15 | ComponentInteraction, 16 | Constants, 17 | DiscordHTTPError, 18 | DiscordRESTError, 19 | Entitlement, 20 | ExtendedUser, 21 | ForumChannel, 22 | GroupChannel, 23 | Guild, 24 | GuildChannel, 25 | GuildIntegration, 26 | GuildPreview, 27 | GuildScheduledEvent, 28 | GuildTemplate, 29 | Interaction, 30 | InteractionMetadata, 31 | Invite, 32 | MediaChannel, 33 | Member, 34 | Message, 35 | ModalSubmitInteraction, 36 | NewsChannel, 37 | NewsThreadChannel, 38 | Permission, 39 | PermissionOverwrite, 40 | PingInteraction, 41 | PrivateChannel, 42 | PrivateThreadChannel, 43 | PublicThreadChannel, 44 | RequestHandler, 45 | Role, 46 | SequentialBucket, 47 | Shard, 48 | SharedStream, 49 | SKU, 50 | SoundboardSound, 51 | StageChannel, 52 | StageInstance, 53 | Subscription, 54 | TextChannel, 55 | TextVoiceChannel, 56 | ThreadChannel, 57 | ThreadMember, 58 | UnavailableGuild, 59 | User, 60 | VERSION, 61 | VoiceChannel, 62 | VoiceConnection, 63 | VoiceConnectionManager, 64 | VoiceState 65 | } = Dysnomia; 66 | -------------------------------------------------------------------------------- /examples/applicationCommands.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | const Constants = Dysnomia.Constants; 4 | 5 | // Replace TOKEN with your bot account's token 6 | const bot = new Dysnomia.Client("BOT TOKEN", { 7 | gateway: { 8 | intents: [] // No intents are needed for interactions, but you still need to specify either an empty array or 0 9 | } 10 | }); 11 | 12 | bot.on("ready", async () => { // When the bot is ready 13 | console.log("Ready!"); // Log "Ready!" 14 | 15 | const commands = await bot.getCommands(); 16 | 17 | if(!commands.length) { 18 | bot.createCommand({ 19 | name: "test_chat_input", 20 | description: "Test command to show how to make commands", 21 | options: [ // An array of Chat Input options https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 22 | { 23 | name: "animal", // The name of the option 24 | description: "The type of animal", 25 | type: Constants.ApplicationCommandOptionTypes.STRING, // This is the type of string, see the types here https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type 26 | required: true, 27 | choices: [ // The possible choices for the options 28 | { 29 | name: "Dog", 30 | value: "animal_dog" 31 | }, 32 | { 33 | name: "Cat", 34 | value: "animal_cat" 35 | }, 36 | { 37 | name: "Penguin", 38 | value: "animal_penguin" 39 | } 40 | ] 41 | }, 42 | { 43 | name: "only_smol", 44 | description: "Whether to show only baby animals", 45 | type: Constants.ApplicationCommandOptionTypes.BOOLEAN, 46 | required: false 47 | } 48 | ], 49 | type: Constants.ApplicationCommandTypes.CHAT_INPUT // Not required for Chat input type, but recommended 50 | }); // Create a chat input command 51 | 52 | bot.createCommand({ 53 | name: "Test User Menu", 54 | type: Constants.ApplicationCommandTypes.USER 55 | }); // Create a user context menu 56 | 57 | bot.createCommand({ 58 | name: "Test Message Menu", 59 | type: Constants.ApplicationCommandTypes.MESSAGE 60 | }); // Create a message context menu 61 | 62 | bot.createCommand({ 63 | name: "test_edit_command", 64 | description: "Test command to show off how to edit commands", 65 | type: Constants.ApplicationCommandTypes.CHAT_INPUT // Not required for Chat input type, but recommended 66 | }); // Create a chat input command 67 | 68 | bot.createCommand({ 69 | name: "test_delete_command", 70 | description: "Test command to show off how to delete commands", 71 | type: Constants.ApplicationCommandTypes.CHAT_INPUT // Not required for Chat input type, but recommended 72 | }); // Create a chat input command 73 | 74 | // In practice, you should use bulkEditCommands if you need to create multiple commands 75 | } 76 | }); 77 | 78 | bot.on("error", (err) => { 79 | console.error(err); // or your preferred logger 80 | }); 81 | 82 | bot.on("interactionCreate", (interaction) => { 83 | if(interaction instanceof Dysnomia.CommandInteraction) { 84 | switch(interaction.data.name) { 85 | case "test_edit_command": 86 | interaction.createMessage("interaction recieved"); 87 | return bot.editCommand(interaction.data.id, { 88 | name: "edited_test_command", 89 | description: "Test command that was edited by running test_edit_command" 90 | }); 91 | case "test_delete_command": 92 | interaction.createMessage("interaction received"); 93 | return bot.deleteCommand(interaction.data.id); 94 | default: { 95 | return interaction.createMessage("interaction received"); 96 | } 97 | } 98 | } 99 | }); 100 | 101 | bot.connect(); // Get the bot to connect to Discord 102 | -------------------------------------------------------------------------------- /examples/components-v2.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | const {Client, Constants} = Dysnomia; 4 | 5 | // Replace TOKEN with your bot account's token 6 | const bot = new Client("Bot TOKEN"); 7 | 8 | bot.on("ready", async () => { // When the bot is ready 9 | console.log("Ready!"); // Log "Ready!" 10 | 11 | // Register the commands 12 | await bot.bulkEditCommands([ 13 | { 14 | name: "components-v2", 15 | description: "Shows an example message featuring v2 components", 16 | type: Constants.ApplicationCommandTypes.CHAT_INPUT 17 | } 18 | ]); 19 | }); 20 | 21 | bot.on("error", (err) => { 22 | console.error(err); // or your preferred logger 23 | }); 24 | 25 | bot.on("interactionCreate", async (interaction) => { // When an interaction is created 26 | if(interaction instanceof Dysnomia.CommandInteraction) { // If the interaction is a command interaction 27 | if(interaction.data.name === "components-v2") { // If the command name is "components-v2" 28 | // Send a message containing v2 components 29 | // Note that you cannot use "content" and "embeds" when sending v2 components 30 | await interaction.createMessage({ 31 | flags: Constants.MessageFlags.IS_COMPONENTS_V2, // This flag is required to be able to send v2 components 32 | components: [ 33 | // A text display component displays text 34 | { 35 | type: Constants.ComponentTypes.TEXT_DISPLAY, 36 | content: "# Welcome to Components v2!" 37 | }, 38 | // A container component groups components together in a box, similar to an embed 39 | { 40 | type: Constants.ComponentTypes.CONTAINER, 41 | accent_color: 0x008000, 42 | components: [ 43 | { 44 | type: Constants.ComponentTypes.TEXT_DISPLAY, 45 | content: "A container groups content together, similar to an embed. It can have an accent color and various components included in it. You can find some files and images below." 46 | }, 47 | // A media gallery components displays a bunch of media items (images, videos, etc.) in a grid 48 | { 49 | type: Constants.ComponentTypes.MEDIA_GALLERY, 50 | items: [ 51 | { 52 | media: { 53 | url: interaction.user.avatarURL // The URL of the media item. attachment:// URLs can also be used 54 | }, 55 | description: `${interaction.user.username}'s avatar` // A media gallery item can have alt text attached to it 56 | } 57 | ] 58 | }, 59 | // A separator component creates a horizontal line in the message 60 | { 61 | type: Constants.ComponentTypes.SEPARATOR, 62 | divider: true, 63 | spacing: Constants.SeparatorSpacingSize.LARGE 64 | }, 65 | // A section component displays text content with an optional accessory 66 | { 67 | type: Constants.ComponentTypes.SECTION, 68 | components: [ 69 | { 70 | type: Constants.ComponentTypes.TEXT_DISPLAY, 71 | content: "Above is a divider with large spacing, and your avatar is to the right of this text. v1 components are still supported in v2 messages. For example, here's an user select component:" 72 | } 73 | ], 74 | accessory: { // A thumbnail accessory displays an image to the right of the section 75 | type: Constants.ComponentTypes.THUMBNAIL, 76 | media: { 77 | url: interaction.user.avatarURL 78 | } 79 | } 80 | }, 81 | // An action row (v1 component) 82 | { 83 | type: Constants.ComponentTypes.ACTION_ROW, 84 | components: [ 85 | // A user select component allows the user to select a user 86 | { 87 | type: Constants.ComponentTypes.USER_SELECT, 88 | custom_id: "user_select", 89 | placeholder: "Select a user" 90 | } 91 | ] 92 | } 93 | ] 94 | }, 95 | // A file component displays a file attachment 96 | { 97 | type: Constants.ComponentTypes.FILE, 98 | file: { 99 | url: "attachment://hello_world.txt" 100 | }, 101 | spoiler: true 102 | }, 103 | { 104 | type: Constants.ComponentTypes.SECTION, 105 | components: [ 106 | { 107 | type: Constants.ComponentTypes.TEXT_DISPLAY, 108 | content: "A section can have a button displayed next to it." 109 | } 110 | ], 111 | accessory: { 112 | type: Constants.ComponentTypes.BUTTON, 113 | style: Constants.ButtonStyles.PRIMARY, 114 | custom_id: "click_me", 115 | label: "Click me!" 116 | } 117 | } 118 | ], 119 | attachments: [{ 120 | filename: "hello_world.txt", 121 | file: Buffer.from("Hello, world!") 122 | }] 123 | }); 124 | } 125 | } else if(interaction instanceof Dysnomia.ComponentInteraction) { // If the interaction is a component interaction 126 | await interaction.createMessage({ 127 | content: "A component interaction was received!", 128 | flags: Constants.MessageFlags.EPHEMERAL 129 | }); 130 | } 131 | }); 132 | 133 | bot.connect(); // Get the bot to connect to Discord 134 | -------------------------------------------------------------------------------- /examples/components.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | const Constants = Dysnomia.Constants; 4 | 5 | // Replace TOKEN with your bot account's token 6 | const bot = new Dysnomia.Client("BOT TOKEN", { 7 | gateway: { 8 | intents: ["guildMessages"] 9 | } 10 | }); 11 | 12 | bot.on("ready", async () => { // When the bot is ready 13 | console.log("Ready!"); // Log "Ready!" 14 | }); 15 | 16 | bot.on("error", (err) => { 17 | console.error(err); // or your preferred logger 18 | }); 19 | 20 | bot.on("messageCreate", (msg) => { // When a message is created 21 | if(msg.content === "!button") { // If the message content is "!button" 22 | bot.createMessage(msg.channel.id, { 23 | content: "Button Example", 24 | components: [ 25 | { 26 | type: Constants.ComponentTypes.ACTION_ROW, // You can have up to 5 action rows, and 1 select menu per action row 27 | components: [ 28 | { 29 | type: Constants.ComponentTypes.BUTTON, // https://discord.com/developers/docs/interactions/message-components#buttons 30 | style: Constants.ButtonStyles.PRIMARY, // This is the style of the button https://discord.com/developers/docs/interactions/message-components#button-object-button-styles 31 | custom_id: "click_one", 32 | label: "Click me!", 33 | disabled: false // Whether or not the button is disabled, is false by default 34 | } 35 | ] 36 | } 37 | ] 38 | }); 39 | // Send a message in the same channel with a Button 40 | } else if(msg.content === "!select") { // Otherwise, if the message is "!select" 41 | bot.createMessage(msg.channel.id, { 42 | content: "Select Menu Example", 43 | components: [ 44 | { 45 | type: Constants.ComponentTypes.ACTION_ROW, // You can have up to 5 action rows, and 5 buttons per action row 46 | components: [ 47 | { 48 | type: Constants.ComponentTypes.STRING_SELECT, // https://discord.com/developers/docs/interactions/message-components#select-menus 49 | custom_id: "select_one", 50 | placeholder: "Select an option", 51 | options: [ // The options to select from https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure 52 | { 53 | label: "Option 1", 54 | value: "option_1", 55 | description: "[Insert description here]" 56 | }, 57 | { 58 | label: "Option 2", 59 | value: "option_2", 60 | description: "This is only here to show off picking one" 61 | } 62 | ], 63 | min_values: 1, 64 | max_values: 1, 65 | disabled: false // Whether or not the select menu is disabled, is false by default 66 | } 67 | ] 68 | } 69 | ] 70 | }); 71 | // Send a message in the same channel with a Select Menu 72 | } 73 | }); 74 | 75 | bot.on("interactionCreate", (interaction) => { 76 | if(interaction instanceof Dysnomia.ComponentInteraction) { 77 | return interaction.createMessage({ 78 | content: "Interaction Received", 79 | flags: 64 80 | }); 81 | } 82 | }); 83 | 84 | bot.connect(); // Get the bot to connect to Discord 85 | -------------------------------------------------------------------------------- /examples/embed.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | // Replace TOKEN with your bot account's token 4 | const bot = new Dysnomia.Client("Bot TOKEN"); 5 | 6 | bot.on("ready", () => { // When the bot is ready 7 | console.log("Ready!"); // Log "Ready!" 8 | }); 9 | 10 | bot.on("error", (err) => { 11 | console.error(err); // or your preferred logger 12 | }); 13 | 14 | bot.on("messageCreate", (msg) => { // When a message is created 15 | if(msg.content === "!embed") { // If the message content is "!embed" 16 | bot.createMessage(msg.channel.id, { 17 | embeds: [{ 18 | title: "I'm an embed!", // Title of the embed 19 | description: "Here is some more info, with **awesome** formatting.\nPretty *neat*, huh?", 20 | author: { // Author property 21 | name: msg.author.username, 22 | icon_url: msg.author.avatarURL 23 | }, 24 | color: 0x008000, // Color, either in hex (show), or a base-10 integer 25 | fields: [ // Array of field objects 26 | { 27 | name: "Some extra info.", // Field title 28 | value: "Some extra value.", // Field 29 | inline: true // Whether you want multiple fields in same line 30 | }, 31 | { 32 | name: "Some more extra info.", 33 | value: "Another extra value.", 34 | inline: true 35 | } 36 | ], 37 | footer: { // Footer text 38 | text: "Created with Dysnomia." 39 | } 40 | }] 41 | }); 42 | } 43 | }); 44 | 45 | bot.connect(); // Get the bot to connect to Discord 46 | -------------------------------------------------------------------------------- /examples/intent.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | // Replace TOKEN with your bot account's token 4 | const bot = new Dysnomia.Client("Bot TOKEN", { 5 | gateway: { 6 | intents: [ 7 | "guilds", 8 | "guildMessages" 9 | ] 10 | } 11 | }); 12 | 13 | bot.on("ready", () => { // When the bot is ready 14 | console.log("Ready!"); // Log "Ready!" 15 | }); 16 | 17 | bot.on("error", (err) => { 18 | console.error(err); // or your preferred logger 19 | }); 20 | 21 | bot.on("guildCreate", (guild) => { // When the client joins a new guild 22 | console.log(`New guild: ${guild.name}`); 23 | }); 24 | 25 | bot.on("messageCreate", (msg) => { // When a message is created 26 | console.log(`New message: ${msg.cleanContent}`); 27 | }); 28 | 29 | // This event will never fire since the client did 30 | // not specify `guildMessageTyping` intent 31 | bot.on("typingStart", (channel, user) => { // When a user starts typing 32 | console.log(`${user.username} is typing in ${channel.name}`); 33 | }); 34 | 35 | bot.connect(); // Get the bot to connect to Discord 36 | -------------------------------------------------------------------------------- /examples/pingpong.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | // Replace TOKEN with your bot account's token 4 | const bot = new Dysnomia.Client("Bot TOKEN"); 5 | 6 | bot.on("ready", () => { // When the bot is ready 7 | console.log("Ready!"); // Log "Ready!" 8 | }); 9 | 10 | bot.on("error", (err) => { 11 | console.error(err); // or your preferred logger 12 | }); 13 | 14 | bot.on("messageCreate", (msg) => { // When a message is created 15 | if(msg.content === "!ping") { // If the message content is "!ping" 16 | bot.createMessage(msg.channel.id, "Pong!"); 17 | // Send a message in the same channel with "Pong!" 18 | } else if(msg.content === "!pong") { // Otherwise, if the message is "!pong" 19 | bot.createMessage(msg.channel.id, "Ping!"); 20 | // Respond with "Ping!" 21 | } 22 | }); 23 | 24 | bot.connect(); // Get the bot to connect to Discord 25 | -------------------------------------------------------------------------------- /examples/playFile.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | // Replace TOKEN with your bot account's token 4 | const bot = new Dysnomia.Client("Bot TOKEN"); 5 | 6 | const playCommand = "!play"; 7 | 8 | bot.on("ready", () => { // When the bot is ready 9 | console.log("Ready!"); // Log "Ready!" 10 | }); 11 | 12 | bot.on("error", (err) => { 13 | console.error(err); // or your preferred logger 14 | }); 15 | 16 | bot.on("messageCreate", (msg) => { // When a message is created 17 | if(msg.content.startsWith(playCommand)) { // If the message content starts with "!play " 18 | if(msg.content.length <= playCommand.length + 1) { // Check if a filename was specified 19 | bot.createMessage(msg.channel.id, "Please specify a filename."); 20 | return; 21 | } 22 | if(!msg.channel.guild) { // Check if the message was sent in a guild 23 | bot.createMessage(msg.channel.id, "This command can only be run in a server."); 24 | return; 25 | } 26 | if(!msg.member.voiceState.channelID) { // Check if the user is in a voice channel 27 | bot.createMessage(msg.channel.id, "You are not in a voice channel."); 28 | return; 29 | } 30 | const filename = msg.content.substring(playCommand.length + 1); // Get the filename 31 | bot.joinVoiceChannel(msg.member.voiceState.channelID).catch((err) => { // Join the user's voice channel 32 | bot.createMessage(msg.channel.id, "Error joining voice channel: " + err.message); // Notify the user if there is an error 33 | console.log(err); // Log the error 34 | }).then((connection) => { 35 | if(connection.playing) { // Stop playing if the connection is playing something 36 | connection.stopPlaying(); 37 | } 38 | connection.play(filename); // Play the file and notify the user 39 | bot.createMessage(msg.channel.id, `Now playing **${filename}**`); 40 | connection.once("end", () => { 41 | bot.createMessage(msg.channel.id, `Finished **${filename}**`); // Say when the file has finished playing 42 | }); 43 | }); 44 | } 45 | }); 46 | 47 | bot.connect(); // Get the bot to connect to Discord 48 | -------------------------------------------------------------------------------- /examples/sharding.js: -------------------------------------------------------------------------------- 1 | const Dysnomia = require("@projectdysnomia/dysnomia"); 2 | 3 | // Replace TOKEN with your bot account's token 4 | const bot = new Dysnomia.Client("Bot TOKEN", { 5 | gateway: { 6 | firstShardID: 0, 7 | lastShardID: 15, 8 | maxShards: 16, 9 | getAllUsers: false, 10 | intents: ["guilds", "guildMembers", "guildPresences"] 11 | } 12 | }); 13 | 14 | bot.on("ready", () => { // When the bot is ready 15 | console.log("Ready!"); // Log "Ready!" 16 | console.timeEnd("ready"); 17 | }); 18 | 19 | bot.on("error", (err) => { 20 | console.error(err); // or your preferred logger 21 | }); 22 | 23 | bot.on("shardReady", (id) => { 24 | console.log(`Shard ${id} ready!`); 25 | }); 26 | 27 | console.time("ready"); 28 | bot.connect(); // Get the bot to connect to Discord 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Import the Client eagerly to allow some of the circularly 4 | * imported modules to correctly initialize. 5 | */ 6 | const Client = require("./lib/Client.js"); 7 | 8 | const Dysnomia = {}; 9 | 10 | Dysnomia.ApplicationCommand = require("./lib/structures/ApplicationCommand"); 11 | Dysnomia.Attachment = require("./lib/structures/Attachment"); 12 | Dysnomia.AutocompleteInteraction = require("./lib/structures/AutocompleteInteraction"); 13 | Dysnomia.AutoModerationRule = require("./lib/structures/AutoModerationRule"); 14 | Dysnomia.Base = require("./lib/structures/Base"); 15 | Dysnomia.Bucket = require("./lib/util/Bucket"); 16 | Dysnomia.CategoryChannel = require("./lib/structures/CategoryChannel"); 17 | Dysnomia.Channel = require("./lib/structures/Channel"); 18 | Dysnomia.CommandInteraction = require("./lib/structures/CommandInteraction"); 19 | Dysnomia.ComponentInteraction = require("./lib/structures/ComponentInteraction"); 20 | Dysnomia.Client = Client; 21 | Dysnomia.Collection = require("./lib/util/Collection"); 22 | Dysnomia.Constants = require("./lib/Constants"); 23 | Dysnomia.DiscordHTTPError = require("./lib/errors/DiscordHTTPError"); 24 | Dysnomia.DiscordRESTError = require("./lib/errors/DiscordRESTError"); 25 | Dysnomia.Entitlement = require("./lib/structures/Entitlement"); 26 | Dysnomia.ExtendedUser = require("./lib/structures/ExtendedUser"); 27 | Dysnomia.ForumChannel = require("./lib/structures/ForumChannel"); 28 | Dysnomia.GroupChannel = require("./lib/structures/GroupChannel.js"); 29 | Dysnomia.Guild = require("./lib/structures/Guild"); 30 | Dysnomia.GuildChannel = require("./lib/structures/GuildChannel"); 31 | Dysnomia.GuildIntegration = require("./lib/structures/GuildIntegration"); 32 | Dysnomia.GuildPreview = require("./lib/structures/GuildPreview"); 33 | Dysnomia.GuildScheduledEvent = require("./lib/structures/GuildScheduledEvent"); 34 | Dysnomia.GuildTemplate = require("./lib/structures/GuildTemplate"); 35 | Dysnomia.Interaction = require("./lib/structures/Interaction"); 36 | Dysnomia.InteractionMetadata = require("./lib/structures/InteractionMetadata.js"); 37 | Dysnomia.Invite = require("./lib/structures/Invite"); 38 | Dysnomia.MediaChannel = require("./lib/structures/MediaChannel"); 39 | Dysnomia.Member = require("./lib/structures/Member"); 40 | Dysnomia.Message = require("./lib/structures/Message"); 41 | Dysnomia.ModalSubmitInteraction = require("./lib/structures/ModalSubmitInteraction.js"); 42 | Dysnomia.NewsChannel = require("./lib/structures/NewsChannel"); 43 | Dysnomia.NewsThreadChannel = require("./lib/structures/NewsThreadChannel"); 44 | Dysnomia.Permission = require("./lib/structures/Permission"); 45 | Dysnomia.PermissionOverwrite = require("./lib/structures/PermissionOverwrite"); 46 | Dysnomia.PingInteraction = require("./lib/structures/PingInteraction"); 47 | Dysnomia.PrivateChannel = require("./lib/structures/PrivateChannel"); 48 | Dysnomia.PrivateThreadChannel = require("./lib/structures/PrivateThreadChannel"); 49 | Dysnomia.PublicThreadChannel = require("./lib/structures/PublicThreadChannel"); 50 | Dysnomia.RequestHandler = require("./lib/rest/RequestHandler"); 51 | Dysnomia.Role = require("./lib/structures/Role"); 52 | Dysnomia.SequentialBucket = require("./lib/util/SequentialBucket"); 53 | Dysnomia.Shard = require("./lib/gateway/Shard"); 54 | Dysnomia.SharedStream = require("./lib/voice/SharedStream"); 55 | Dysnomia.SKU = require("./lib/structures/SKU.js"); 56 | Dysnomia.SoundboardSound = require("./lib/structures/SoundboardSound"); 57 | Dysnomia.StageChannel = require("./lib/structures/StageChannel"); 58 | Dysnomia.StageInstance = require("./lib/structures/StageInstance"); 59 | Dysnomia.Subscription = require("./lib/structures/Subscription.js"); 60 | Dysnomia.TextChannel = require("./lib/structures/TextChannel"); 61 | Dysnomia.TextVoiceChannel = require("./lib/structures/TextVoiceChannel"); 62 | Dysnomia.ThreadChannel = require("./lib/structures/ThreadChannel"); 63 | Dysnomia.ThreadMember = require("./lib/structures/ThreadMember"); 64 | Dysnomia.UnavailableGuild = require("./lib/structures/UnavailableGuild"); 65 | Dysnomia.User = require("./lib/structures/User"); 66 | Dysnomia.VERSION = require("./package.json").version; 67 | Dysnomia.VoiceChannel = require("./lib/structures/VoiceChannel"); 68 | Dysnomia.VoiceConnection = require("./lib/voice/VoiceConnection"); 69 | Dysnomia.VoiceConnectionManager = require("./lib/voice/VoiceConnectionManager"); 70 | Dysnomia.VoiceState = require("./lib/structures/VoiceState"); 71 | 72 | module.exports = Dysnomia; 73 | -------------------------------------------------------------------------------- /lib/errors/DiscordHTTPError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class DiscordHTTPError extends Error { 4 | constructor(req, res, response, stack) { 5 | super(); 6 | 7 | Object.defineProperty(this, "req", { 8 | enumerable: false, 9 | value: req 10 | }); 11 | Object.defineProperty(this, "res", { 12 | enumerable: false, 13 | value: res 14 | }); 15 | Object.defineProperty(this, "response", { 16 | enumerable: false, 17 | value: response 18 | }); 19 | 20 | Object.defineProperty(this, "code", { 21 | enumerable: false, 22 | value: res.statusCode 23 | }); 24 | let message = `${res.statusCode} ${res.statusMessage} on ${req.method} ${req.path}`; 25 | const errors = this.flattenErrors(response); 26 | if(errors.length > 0) { 27 | message += "\n " + errors.join("\n "); 28 | } 29 | Object.defineProperty(this, "message", { 30 | enumerable: false, 31 | value: message 32 | }); 33 | 34 | if(stack) { 35 | this.stack = this.name + ": " + this.message + "\n" + stack; 36 | } else { 37 | Error.captureStackTrace(this, DiscordHTTPError); 38 | } 39 | } 40 | 41 | get headers() { 42 | return this.response.headers; 43 | } 44 | 45 | get name() { 46 | return this.constructor.name; 47 | } 48 | 49 | flattenErrors(errors, keyPrefix = "") { 50 | let messages = []; 51 | for(const fieldName in errors) { 52 | if(!Object.hasOwn(errors, fieldName) || fieldName === "message" || fieldName === "code") { 53 | continue; 54 | } 55 | if(Array.isArray(errors[fieldName])) { 56 | messages = messages.concat(errors[fieldName].map((str) => `${keyPrefix + fieldName}: ${str}`)); 57 | } 58 | } 59 | return messages; 60 | } 61 | } 62 | 63 | module.exports = DiscordHTTPError; 64 | -------------------------------------------------------------------------------- /lib/errors/DiscordRESTError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class DiscordRESTError extends Error { 4 | constructor(req, res, response, stack) { 5 | super(); 6 | 7 | Object.defineProperty(this, "req", { 8 | enumerable: false, 9 | value: req 10 | }); 11 | Object.defineProperty(this, "res", { 12 | enumerable: false, 13 | value: res 14 | }); 15 | Object.defineProperty(this, "response", { 16 | enumerable: false, 17 | value: response 18 | }); 19 | 20 | Object.defineProperty(this, "code", { 21 | enumerable: false, 22 | value: +response.code || -1 23 | }); 24 | let message = response.message || "Unknown error"; 25 | if(response.errors) { 26 | message += "\n " + this.flattenErrors(response.errors).join("\n "); 27 | } else { 28 | const errors = this.flattenErrors(response); 29 | if(errors.length > 0) { 30 | message += "\n " + errors.join("\n "); 31 | } 32 | } 33 | Object.defineProperty(this, "message", { 34 | enumerable: false, 35 | value: message 36 | }); 37 | 38 | if(stack) { 39 | this.stack = this.name + ": " + this.message + "\n" + stack; 40 | } else { 41 | Error.captureStackTrace(this, DiscordRESTError); 42 | } 43 | } 44 | 45 | get headers() { 46 | return this.response.headers; 47 | } 48 | 49 | get name() { 50 | return `${this.constructor.name} [${this.code}]`; 51 | } 52 | 53 | flattenErrors(errors, keyPrefix = "") { 54 | let messages = []; 55 | for(const fieldName in errors) { 56 | if(!Object.hasOwn(errors, fieldName) || fieldName === "message" || fieldName === "code") { 57 | continue; 58 | } 59 | if(fieldName === "_errors") { 60 | messages = messages.concat( 61 | errors._errors.map((obj) => `${keyPrefix ? `${keyPrefix}: ` : ""}${obj.message}`) 62 | ); 63 | } else if(errors[fieldName]._errors) { 64 | messages = messages.concat(errors[fieldName]._errors.map((obj) => `${keyPrefix + fieldName}: ${obj.message}`)); 65 | } else if(Array.isArray(errors[fieldName])) { 66 | messages = messages.concat(errors[fieldName].map((str) => `${keyPrefix + fieldName}: ${str}`)); 67 | } else if(typeof errors[fieldName] === "object") { 68 | messages = messages.concat(this.flattenErrors(errors[fieldName], keyPrefix + fieldName + ".")); 69 | } 70 | } 71 | return messages; 72 | } 73 | } 74 | 75 | module.exports = DiscordRESTError; 76 | -------------------------------------------------------------------------------- /lib/gateway/ShardManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("../structures/Base"); 4 | const Collection = require("../util/Collection"); 5 | const Shard = require("./Shard"); 6 | const Constants = require("../Constants"); 7 | let ZlibSync; 8 | try { 9 | ZlibSync = require("zlib-sync"); 10 | } catch{ 11 | try { 12 | ZlibSync = require("pako"); 13 | } catch{ // eslint-disable no-empty 14 | } 15 | } 16 | class ShardManager extends Collection { 17 | #client; 18 | buckets = new Map(); 19 | connectQueue = []; 20 | connectTimeout = null; 21 | 22 | constructor(client, options = {}) { 23 | super(Shard); 24 | this.#client = client; 25 | /* eslint-disable @stylistic/key-spacing -- this is spaced differently to the rest */ 26 | this.options = Object.assign({ 27 | autoreconnect: true, 28 | compress: false, 29 | connectionTimeout: 30000, 30 | disableEvents: {}, 31 | firstShardID: 0, 32 | getAllUsers: false, 33 | guildCreateTimeout: 2000, 34 | intents: Constants.Intents.allNonPrivileged, 35 | largeThreshold: 250, 36 | maxReconnectAttempts: Infinity, 37 | maxResumeAttempts: 10, 38 | maxConcurrency: 1, 39 | maxShards: 1, 40 | seedVoiceConnections: false, 41 | requestTimeout: 15000, 42 | reconnectDelay: (lastDelay, attempts) => Math.pow(attempts + 1, 0.7) * 20000 43 | }, options); 44 | /* eslint-enable @stylistic/key-spacing */ 45 | 46 | if(typeof this.options.intents !== "undefined") { 47 | // Resolve intents option to the proper integer 48 | if(Array.isArray(this.options.intents)) { 49 | let bitmask = 0; 50 | for(const intent of this.options.intents) { 51 | if(Constants.Intents[intent]) { 52 | bitmask |= Constants.Intents[intent]; 53 | } else if(typeof intent === "number") { 54 | bitmask |= intent; 55 | } else { 56 | this.#client.emit("warn", `Unknown intent: ${intent}`); 57 | } 58 | } 59 | this.options.intents = bitmask; 60 | } 61 | 62 | // Ensure requesting all guild members isn't destined to fail 63 | if(this.options.getAllUsers && !(this.options.intents & Constants.Intents.guildMembers)) { 64 | throw new Error("Cannot request all members without guildMembers intent"); 65 | } 66 | } 67 | 68 | if(this.options.lastShardID === undefined && this.options.maxShards !== "auto") { 69 | this.options.lastShardID = this.options.maxShards - 1; 70 | } 71 | 72 | if(typeof window !== "undefined" || !ZlibSync) { 73 | this.options.compress = false; // zlib does not like Blobs, Pako is not here 74 | } 75 | } 76 | 77 | connect(shard) { 78 | this.connectQueue.push(shard); 79 | this.tryConnect(); 80 | } 81 | 82 | spawn(id) { 83 | let shard = this.get(id); 84 | if(!shard) { 85 | shard = this.add(new Shard(id, this.#client)); 86 | shard.on("ready", () => { 87 | /** 88 | * Fired when a shard turns ready 89 | * @event Client#shardReady 90 | * @prop {Number} id The ID of the shard 91 | */ 92 | this.#client.emit("shardReady", shard.id); 93 | if(this.#client.ready) { 94 | return; 95 | } 96 | for(const other of this.values()) { 97 | if(!other.ready) { 98 | return; 99 | } 100 | } 101 | this.#client.ready = true; 102 | this.#client.startTime = Date.now(); 103 | /** 104 | * Fired when all shards turn ready 105 | * @event Client#ready 106 | */ 107 | this.#client.emit("ready"); 108 | }).on("resume", () => { 109 | /** 110 | * Fired when a shard resumes 111 | * @event Client#shardResume 112 | * @prop {Number} id The ID of the shard 113 | */ 114 | this.#client.emit("shardResume", shard.id); 115 | if(this.#client.ready) { 116 | return; 117 | } 118 | for(const other of this.values()) { 119 | if(!other.ready) { 120 | return; 121 | } 122 | } 123 | this.#client.ready = true; 124 | this.#client.startTime = Date.now(); 125 | this.#client.emit("ready"); 126 | }).on("disconnect", (error) => { 127 | /** 128 | * Fired when a shard disconnects 129 | * @event Client#shardDisconnect 130 | * @prop {Error?} error The error, if any 131 | * @prop {Number} id The ID of the shard 132 | */ 133 | this.#client.emit("shardDisconnect", error, shard.id); 134 | for(const other of this.values()) { 135 | if(other.ready) { 136 | return; 137 | } 138 | } 139 | this.#client.ready = false; 140 | this.#client.startTime = 0; 141 | /** 142 | * Fired when all shards disconnect 143 | * @event Client#disconnect 144 | */ 145 | this.#client.emit("disconnect"); 146 | }); 147 | } 148 | if(shard.status === "disconnected") { 149 | return this.connect(shard); 150 | } 151 | } 152 | 153 | tryConnect() { 154 | // nothing in queue 155 | if(this.connectQueue.length === 0) { 156 | return; 157 | } 158 | 159 | // loop over the connectQueue 160 | for(const shard of this.connectQueue) { 161 | // find the bucket for our shard 162 | const rateLimitKey = (shard.id % this.options.maxConcurrency) || 0; 163 | const lastConnect = this.buckets.get(rateLimitKey) || 0; 164 | 165 | // has enough time passed since the last connect for this bucket (5s/bucket)? 166 | // alternatively if we have a sessionID, we can skip this check 167 | if(!shard.sessionID && Date.now() - lastConnect < 5000) { 168 | continue; 169 | } 170 | 171 | // Are there any connecting shards in the same bucket we should wait on? 172 | if(this.some((s) => s.connecting && ((s.id % this.options.maxConcurrency) || 0) === rateLimitKey)) { 173 | continue; 174 | } 175 | 176 | // connect the shard 177 | shard.connect(); 178 | this.buckets.set(rateLimitKey, Date.now()); 179 | 180 | // remove the shard from the queue 181 | const index = this.connectQueue.findIndex((s) => s.id === shard.id); 182 | this.connectQueue.splice(index, 1); 183 | } 184 | 185 | // set the next timeout if we have more shards to connect 186 | if(!this.connectTimeout && this.connectQueue.length > 0) { 187 | this.connectTimeout = setTimeout(() => { 188 | this.connectTimeout = null; 189 | this.tryConnect(); 190 | }, 500); 191 | } 192 | } 193 | 194 | _readyPacketCB(shardID) { 195 | const rateLimitKey = (shardID % this.options.maxConcurrency) || 0; 196 | this.buckets.set(rateLimitKey, Date.now()); 197 | 198 | this.tryConnect(); 199 | } 200 | 201 | toString() { 202 | return `[ShardManager ${this.size}]`; 203 | } 204 | 205 | toJSON(props = []) { 206 | return Base.prototype.toJSON.call(this, [ 207 | "buckets", 208 | "connectQueue", 209 | "connectTimeout", 210 | "options", 211 | ...props 212 | ]); 213 | } 214 | } 215 | 216 | module.exports = ShardManager; 217 | -------------------------------------------------------------------------------- /lib/structures/ApplicationCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents an application command 7 | * @extends Base 8 | */ 9 | class ApplicationCommand extends Base { 10 | /** 11 | * The ID of the application command 12 | * @override 13 | * @member {String} ApplicationCommand#id 14 | */ 15 | 16 | #client; 17 | constructor(data, client) { 18 | super(data.id); 19 | this.#client = client; 20 | 21 | /** 22 | * The ID of the application that this command belongs to 23 | * @type {String} 24 | */ 25 | this.applicationID = data.application_id; 26 | /** 27 | * The name of the command 28 | * @type {String} 29 | */ 30 | this.name = data.name; 31 | /** 32 | * The description of the command (empty for user & message commands) 33 | * @type {String} 34 | */ 35 | this.description = data.description; 36 | /** 37 | * The [command type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types) 38 | * @type {Number} 39 | */ 40 | this.type = data.type; 41 | /** 42 | * The id of the version of this command 43 | * @type {String} 44 | */ 45 | this.version = data.version; 46 | 47 | if(data.guild_id !== undefined) { 48 | /** 49 | * The ID of the guild associated with this command (guild commands only) 50 | * @type {String?} 51 | */ 52 | this.guildID = data.guild_id; 53 | } 54 | 55 | if(data.name_localizations !== undefined) { 56 | /** 57 | * A map of [locales](https://discord.com/developers/docs/reference#locales) to names for that locale 58 | * @type {Object} 59 | */ 60 | this.nameLocalizations = data.name_localizations; 61 | } 62 | 63 | if(data.description_localizations !== undefined) { 64 | /** 65 | * A map of [locales](https://discord.com/developers/docs/reference#locales) to descriptions for that locale 66 | * @type {Object} 67 | */ 68 | this.descriptionLocalizations = data.description_localizations; 69 | } 70 | 71 | if(data.options !== undefined) { 72 | /** 73 | * The [options](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure) associated with this command 74 | * @type {Object[]?} 75 | */ 76 | this.options = data.options; 77 | } 78 | 79 | if(data.default_member_permissions !== undefined) { 80 | /** 81 | * The [permissions](https://discord.com/developers/docs/topics/permissions) required by default for this command to be usable 82 | * @type {String?} 83 | */ 84 | this.defaultMemberPermissions = data.default_member_permissions; 85 | } 86 | 87 | if(data.dm_permission !== undefined) { 88 | /** 89 | * If this command can be used in direct messages (global commands only) 90 | * @deprecated Use {@link ApplicationCommand#contexts} instead 91 | * @type {Boolean?} 92 | */ 93 | this.dmPermission = data.dm_permission; 94 | } 95 | 96 | if(data.nsfw !== undefined) { 97 | /** 98 | * Whether this command is age-restricted or not 99 | * @type {Boolean} 100 | */ 101 | this.nsfw = data.nsfw; 102 | } 103 | 104 | if(data.handler !== undefined) { 105 | /** 106 | * The [handler type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-entry-point-command-handler-types) for primary entry point commands 107 | * @type {Number?} 108 | */ 109 | this.handler = data.handler; 110 | } 111 | 112 | if(data.integration_types !== undefined) { 113 | /** 114 | * A list of installation contexts where the command is available 115 | * @type {Array} 116 | */ 117 | this.integrationTypes = data.integration_types; 118 | } 119 | 120 | if(data.contexts !== undefined) { 121 | /** 122 | * A list of interaction contexts where the command can be run 123 | * @type {Array} 124 | */ 125 | this.contexts = data.contexts; 126 | } 127 | } 128 | 129 | /** 130 | * Delete this command 131 | * @returns {Promise} 132 | */ 133 | delete() { 134 | return this.guildID === undefined ? this.#client.deleteCommand.call(this.#client, this.id) : this.#client.deleteGuildCommand.call(this.#client, this.guildID, this.id); 135 | } 136 | 137 | /** 138 | * Edit this application command 139 | * @param {Object} options The properties to edit 140 | * @param {String} [options.name] The command name 141 | * @param {Object} [options.nameLocalizations] A map of [locales](https://discord.com/developers/docs/reference#locales) to names for that locale 142 | * @param {String} [options.description] The command description (chat input commands only) 143 | * @param {Object} [options.descriptionLocalizations] A map of [locales](https://discord.com/developers/docs/reference#locales) to descriptions for that locale 144 | * @param {Array} [options.options] An array of [command options](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure) 145 | * @param {String} [options.defaultMemberPermissions] The [permissions](https://discord.com/developers/docs/topics/permissions) required by default for this command to be usable 146 | * @param {Boolean} [options.dmPermission] If this command can be used in direct messages (global commands only) 147 | * @returns {Promise} 148 | */ 149 | edit(options) { 150 | return this.guildID === undefined ? this.#client.editCommand.call(this.#client, this.id, options) : this.#client.editGuildCommand.call(this.#client, this.id, this.guildID, options); 151 | } 152 | 153 | toJSON(props = []) { 154 | return super.toJSON([ 155 | "applicationID", 156 | "contexts", 157 | "defaultMemberPermissions", 158 | "description", 159 | "descriptionLocalizations", 160 | "dmPermission", 161 | "guildID", 162 | "handler", 163 | "integrationTypes", 164 | "name", 165 | "nameLocalizations", 166 | "nsfw", 167 | "options", 168 | "type", 169 | "version", 170 | ...props 171 | ]); 172 | } 173 | } 174 | 175 | module.exports = ApplicationCommand; 176 | -------------------------------------------------------------------------------- /lib/structures/Attachment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents an attachment 7 | * @extends Base 8 | */ 9 | class Attachment extends Base { 10 | /** 11 | * The attachment ID 12 | * @override 13 | * @member {String} Attachment#id 14 | */ 15 | 16 | constructor(data) { 17 | super(data.id); 18 | 19 | /** 20 | * The filename of the attachment 21 | * @type {String} 22 | */ 23 | this.filename = data.filename; 24 | /** 25 | * The size of the attachment 26 | * @type {Number} 27 | */ 28 | this.size = data.size; 29 | /** 30 | * The URL of the attachment 31 | * @type {String} 32 | */ 33 | this.url = data.url; 34 | /** 35 | * The proxy URL of the attachment 36 | * @type {String} 37 | */ 38 | this.proxyURL = data.proxy_url; 39 | /** 40 | * The duration of the audio file (voice messages only) 41 | * @type {Number?} 42 | */ 43 | this.durationSecs = data.duration_secs; 44 | /** 45 | * A Base64-encoded byte array representing the sampled waveform of the audio file (voice messages only) 46 | * @type {String?} 47 | */ 48 | this.waveform = data.waveform; 49 | this.update(data); 50 | } 51 | 52 | update(data) { 53 | if(data.title !== undefined) { 54 | /** 55 | * The title of the attachment 56 | * @type {String?} 57 | */ 58 | this.title = data.title; 59 | } 60 | if(data.description !== undefined) { 61 | /** 62 | * The description of the attachment 63 | * @type {String?} 64 | */ 65 | this.description = data.description; 66 | } 67 | if(data.content_type !== undefined) { 68 | /** 69 | * The content type of the attachment 70 | * @type {String?} 71 | */ 72 | this.contentType = data.content_type; 73 | } 74 | if(data.height !== undefined) { 75 | /** 76 | * The height of the attachment 77 | * @type {Number?} 78 | */ 79 | this.height = data.height; 80 | } 81 | if(data.width !== undefined) { 82 | /** 83 | * The width of the attachment 84 | * @type {Number?} 85 | */ 86 | this.width = data.width; 87 | } 88 | if(data.ephemeral !== undefined) { 89 | /** 90 | * Whether the attachment is ephemeral 91 | * @type {Boolean?} 92 | */ 93 | this.ephemeral = data.ephemeral; 94 | } 95 | if(data.flags !== undefined) { 96 | /** 97 | * Attachment flags. See [Discord's documentation](https://discord.com/developers/docs/resources/channel#attachment-object-attachment-flags) for a list of them 98 | * @type {Number?} 99 | */ 100 | this.flags = data.flags; 101 | } 102 | } 103 | 104 | toJSON(props = []) { 105 | return super.toJSON([ 106 | "filename", 107 | "description", 108 | "contentType", 109 | "size", 110 | "url", 111 | "proxyURL", 112 | "height", 113 | "width", 114 | "ephemeral", 115 | "durationSecs", 116 | "waveform", 117 | "flags", 118 | ...props 119 | ]); 120 | } 121 | } 122 | 123 | module.exports = Attachment; 124 | -------------------------------------------------------------------------------- /lib/structures/AutoModerationRule.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents an auto moderation rule 7 | */ 8 | class AutoModerationRule extends Base { 9 | /** 10 | * The ID of the auto moderation rule 11 | * @member {String} AutoModerationRule#id 12 | */ 13 | #client; 14 | constructor(data, client) { 15 | super(data.id); 16 | this.#client = client; 17 | 18 | /** 19 | * An array of [auto moderation action objects](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object) 20 | * @type {Array} 21 | */ 22 | this.actions = data.actions.map((action) => ({ 23 | type: action.type, 24 | metadata: action.metadata 25 | })); 26 | /** 27 | * The ID of the user who created this auto moderation rule 28 | * @type {String} 29 | */ 30 | this.creatorID = data.creator_id; 31 | /** 32 | * Whether this auto moderation rule is enabled or not 33 | * @type {Boolean} 34 | */ 35 | this.enabled = data.enabled; 36 | /** 37 | * The rule [event type](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-event-types) 38 | * @type {Number} 39 | */ 40 | this.eventType = data.event_type; 41 | /** 42 | * An array of role IDs exempt from this rule 43 | * @type {Array} 44 | */ 45 | this.exemptRoles = data.exempt_roles; 46 | /** 47 | * An array of channel IDs exempt from this rule 48 | * @type {Array} 49 | */ 50 | this.exemptChannels = data.exempt_channels; 51 | /** 52 | * The ID of the guild which this rule belongs to 53 | * @type {String} 54 | */ 55 | this.guildID = data.guild_id; 56 | /** 57 | * The name of the rule 58 | * @type {String} 59 | */ 60 | this.name = data.name; 61 | /** 62 | * The [metadata](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata) tied with this rule 63 | * @type {Object} 64 | */ 65 | this.triggerMetadata = data.trigger_metadata; 66 | /** 67 | * The rule [trigger type](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-types) 68 | * @type {Number} 69 | */ 70 | this.triggerType = data.trigger_type; 71 | } 72 | 73 | /** 74 | * Deletes this auto moderation rule 75 | * @returns {Promise} 76 | */ 77 | delete() { 78 | return this.#client.deleteAutoModerationRule.call(this.#client, this.guildID, this.id); 79 | } 80 | 81 | /** 82 | * Edits this auto moderation rule 83 | * @param {Object} options The new rule options 84 | * @param {Object[]} [options.actions] The [actions](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object) done when the rule is violated 85 | * @param {Boolean} [options.enabled=false] If the rule is enabled, false by default 86 | * @param {Number} [options.eventType] The [event type](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-event-types) for the rule 87 | * @param {String[]} [options.exemptChannels] Any channels where this rule does not apply 88 | * @param {String[]} [options.exemptRoles] Any roles to which this rule does not apply 89 | * @param {String} [options.name] The name of the rule 90 | * @param {String} [options.reason] The reason to be displayed in audit logs 91 | * @param {Object} [options.triggerMetadata] The [trigger metadata](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata) for the rule 92 | * @returns {Promise} 93 | */ 94 | edit(options) { 95 | return this.#client.editAutoModerationRule.call(this.#client, this.guildID, this.id, options); 96 | } 97 | 98 | toJSON(props = []) { 99 | return super.toJSON([ 100 | "actions", 101 | "creatorID", 102 | "enabled", 103 | "eventType", 104 | "exemptRoles", 105 | "exemptChannels", 106 | "guildID", 107 | "name", 108 | "triggerMetadata", 109 | "triggerType", 110 | ...props 111 | ]); 112 | } 113 | } 114 | 115 | module.exports = AutoModerationRule; 116 | -------------------------------------------------------------------------------- /lib/structures/AutocompleteInteraction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Interaction = require("./Interaction"); 4 | const {InteractionResponseTypes} = require("../Constants"); 5 | 6 | /** 7 | * Represents an application command autocomplete interaction. See Interaction for more properties. 8 | * @extends Interaction 9 | */ 10 | class AutocompleteInteraction extends Interaction { 11 | /** 12 | * @override 13 | * @member {!(PrivateChannel | TextChannel | NewsChannel)} AutocompleteInteraction#channel 14 | */ 15 | /** 16 | * The data attached to the interaction 17 | * @override 18 | * @member {AutocompleteInteraction.InteractionData} AutocompleteInteraction#data 19 | */ 20 | #client; 21 | constructor(data, client) { 22 | super(data, client); 23 | this.#client = client; 24 | } 25 | 26 | /** 27 | * Acknowledges the autocomplete interaction with a result of choices 28 | * Note: You can **not** use more than 1 initial interaction response per interaction 29 | * @param {Array} choices The autocomplete choices to return to the user 30 | * @param {String | Number} choices[].name The choice display name 31 | * @param {String} choices[].value The choice value to return to the bot 32 | * @returns {Promise} 33 | */ 34 | acknowledge(choices) { 35 | return this.result(choices); 36 | } 37 | 38 | /** 39 | * Acknowledges the autocomplete interaction with a result of choices 40 | * Note: You can **not** use more than 1 initial interaction response per interaction. 41 | * @param {Array} choices The autocomplete choices to return to the user 42 | * @param {String | Number} choices[].name The choice display name 43 | * @param {String} choices[].value The choice value to return to the bot 44 | * @returns {Promise} 45 | */ 46 | result(choices) { 47 | if(this.acknowledged === true) { 48 | throw new Error("You have already acknowledged this interaction."); 49 | } 50 | return this.#client.createInteractionResponse.call(this.#client, this.id, this.token, { 51 | type: InteractionResponseTypes.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, 52 | data: {choices} 53 | }).then(() => this.update()); 54 | } 55 | } 56 | 57 | module.exports = AutocompleteInteraction; 58 | 59 | /** 60 | * The data attached to the interaction 61 | * @typedef AutocompleteInteraction.InteractionData 62 | * @prop {String} id The ID of the Application Command 63 | * @prop {String} name The command name 64 | * @prop {Number} type The [command type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types) 65 | * @prop {String?} target_id The id the of user or message targetted by a context menu command 66 | * @prop {Array?} options The run Application Command options 67 | */ 68 | 69 | /** 70 | * The run Application Command options 71 | * @typedef AutocompleteInteraction.CommandOptions 72 | * @prop {String} name The name of the Application Command option 73 | * @prop {Number} type The [option type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type) 74 | * @prop {(String | Number | Boolean)?} value The option value (Mutually exclusive with options) 75 | * @prop {Boolean?} focused If the option is focused 76 | * @prop {Array?} options Sub-options (Mutually exclusive with value, subcommand/subcommandgroup) 77 | */ 78 | -------------------------------------------------------------------------------- /lib/structures/Base.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("node:util"); 4 | 5 | /** 6 | * Provides utilities for working with many Discord structures 7 | */ 8 | class Base { 9 | constructor(id) { 10 | if(id) { 11 | /** 12 | * A Discord snowflake identifying the object 13 | * @type {String} 14 | */ 15 | this.id = id; 16 | } 17 | } 18 | 19 | /** 20 | * Timestamp of structure creation 21 | * @type {Number} 22 | */ 23 | get createdAt() { 24 | return Base.getCreatedAt(this.id); 25 | } 26 | 27 | /** 28 | * Calculates the timestamp in milliseconds associated with a Discord ID/snowflake 29 | * @param {String} id The ID of a structure 30 | * @returns {Number} 31 | */ 32 | static getCreatedAt(id) { 33 | return Base.getDiscordEpoch(id) + 1420070400000; 34 | } 35 | 36 | /** 37 | * Gets the number of milliseconds since epoch represented by an ID/snowflake 38 | * @param {String} id The ID of a structure 39 | * @returns {Number} 40 | */ 41 | static getDiscordEpoch(id) { 42 | return Math.floor(id / 4194304); 43 | } 44 | 45 | [util.inspect.custom]() { 46 | // http://stackoverflow.com/questions/5905492/dynamic-function-name-in-javascript 47 | const copy = new {[this.constructor.name]: class {}}[this.constructor.name](); 48 | for(const key in this) { 49 | if(Object.hasOwn(this, key) && !key.startsWith("_") && this[key] !== undefined) { 50 | copy[key] = this[key]; 51 | } 52 | } 53 | return copy; 54 | } 55 | 56 | toString() { 57 | return `[${this.constructor.name} ${this.id}]`; 58 | } 59 | 60 | toJSON(props = []) { 61 | const json = {}; 62 | if(this.id) { 63 | json.id = this.id; 64 | json.createdAt = this.createdAt; 65 | } 66 | for(const prop of props) { 67 | const value = this[prop]; 68 | const type = typeof value; 69 | if(value === undefined) { 70 | continue; 71 | } else if((type !== "object" && type !== "function" && type !== "bigint") || value === null) { 72 | json[prop] = value; 73 | } else if(value.toJSON !== undefined) { 74 | json[prop] = value.toJSON(); 75 | } else if(value.values !== undefined) { 76 | json[prop] = [...value.values()]; 77 | } else if(type === "bigint") { 78 | json[prop] = value.toString(); 79 | } else if(type === "object") { 80 | json[prop] = value; 81 | } 82 | } 83 | return json; 84 | } 85 | } 86 | 87 | module.exports = Base; 88 | -------------------------------------------------------------------------------- /lib/structures/CategoryChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Collection = require("../util/Collection"); 4 | const GuildChannel = require("./GuildChannel"); 5 | const PermissionOverwrite = require("./PermissionOverwrite"); 6 | 7 | /** 8 | * Represents a guild category channel. See GuildChannel for more properties and methods. 9 | * @extends GuildChannel 10 | */ 11 | class CategoryChannel extends GuildChannel { 12 | constructor(data, client) { 13 | super(data, client); 14 | } 15 | 16 | update(data, client) { 17 | super.update(data, client); 18 | if(data.permission_overwrites) { 19 | /** 20 | * Collection of PermissionOverwrites in this channel 21 | * @type {Collection} 22 | */ 23 | this.permissionOverwrites = new Collection(PermissionOverwrite); 24 | data.permission_overwrites.forEach((overwrite) => { 25 | this.permissionOverwrites.add(overwrite); 26 | }); 27 | } 28 | if(data.position !== undefined) { 29 | /** 30 | * The position of the channel 31 | * @type {Number} 32 | */ 33 | this.position = data.position; 34 | } 35 | } 36 | 37 | /** 38 | * A collection of guild channels that are part of this category 39 | * @type {Collection} 40 | */ 41 | get channels() { 42 | const channels = new Collection(GuildChannel); 43 | if(this.guild?.channels) { 44 | for(const channel of this.guild.channels.values()) { 45 | if(channel.parentID === this.id) { 46 | channels.add(channel); 47 | } 48 | } 49 | } 50 | return channels; 51 | } 52 | 53 | toJSON(props = []) { 54 | return super.toJSON([ 55 | "permissionOverwrites", 56 | "position", 57 | ...props 58 | ]); 59 | } 60 | } 61 | 62 | module.exports = CategoryChannel; 63 | -------------------------------------------------------------------------------- /lib/structures/Channel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const {ChannelTypes} = require("../Constants"); 5 | const emitDeprecation = require("../util/emitDeprecation"); 6 | 7 | /** 8 | * Represents a channel. You also probably want to look at CategoryChannel, NewsChannel, PrivateChannel, TextChannel, and TextVoiceChannel. 9 | */ 10 | class Channel extends Base { 11 | /** 12 | * The ID of the channel 13 | * @member {String} Channel#id 14 | */ 15 | 16 | /** 17 | * Timestamp of the channel's creation 18 | * @member {Number} Channel#createdAt 19 | */ 20 | #client; 21 | constructor(data, client) { 22 | super(data.id); 23 | this.#client = client; 24 | /** 25 | * The type of the channel 26 | * @type {Number} 27 | */ 28 | this.type = data.type; 29 | } 30 | 31 | /** 32 | * The client that initialized the channel 33 | * @deprecated Accessing the client reference via Channel#client is deprecated and is going to be removed in the next release. Please use your own client reference instead. 34 | * @type {Client} 35 | */ 36 | get client() { 37 | emitDeprecation("CHANNEL_CLIENT"); 38 | return this.#client; 39 | } 40 | 41 | /** 42 | * A string that mentions the channel 43 | * @type {String} 44 | */ 45 | get mention() { 46 | return `<#${this.id}>`; 47 | } 48 | 49 | static from(data, client) { 50 | switch(data.type) { 51 | case ChannelTypes.GUILD_TEXT: { 52 | return new TextChannel(data, client); 53 | } 54 | case ChannelTypes.DM: { 55 | return new PrivateChannel(data, client); 56 | } 57 | case ChannelTypes.GROUP_DM: { 58 | return new GroupChannel(data, client); 59 | } 60 | case ChannelTypes.GUILD_VOICE: { 61 | return new TextVoiceChannel(data, client); 62 | } 63 | case ChannelTypes.GUILD_CATEGORY: { 64 | return new CategoryChannel(data, client); 65 | } 66 | case ChannelTypes.GUILD_ANNOUNCEMENT: { 67 | return new NewsChannel(data, client); 68 | } 69 | case ChannelTypes.ANNOUNCEMENT_THREAD: { 70 | return new NewsThreadChannel(data, client); 71 | } 72 | case ChannelTypes.PUBLIC_THREAD: { 73 | return new PublicThreadChannel(data, client); 74 | } 75 | case ChannelTypes.PRIVATE_THREAD: { 76 | return new PrivateThreadChannel(data, client); 77 | } 78 | case ChannelTypes.GUILD_STAGE_VOICE: { 79 | return new StageChannel(data, client); 80 | } 81 | case ChannelTypes.GUILD_FORUM: { 82 | return new ForumChannel(data, client); 83 | } 84 | case ChannelTypes.GUILD_MEDIA: { 85 | return new MediaChannel(data, client); 86 | } 87 | } 88 | if(data.guild_id) { 89 | if(data.last_message_id !== undefined) { 90 | client.emit("warn", new Error(`Unknown guild text channel type: ${data.type}\n${JSON.stringify(data)}`)); 91 | return new TextChannel(data, client); 92 | } 93 | client.emit("warn", new Error(`Unknown guild channel type: ${data.type}\n${JSON.stringify(data)}`)); 94 | return new GuildChannel(data, client); 95 | } 96 | client.emit("warn", new Error(`Unknown channel type: ${data.type}\n${JSON.stringify(data)}`)); 97 | return new Channel(data, client); 98 | } 99 | 100 | toJSON(props = []) { 101 | return super.toJSON([ 102 | "type", 103 | ...props 104 | ]); 105 | } 106 | } 107 | 108 | module.exports = Channel; 109 | 110 | // Circular import 111 | const CategoryChannel = require("./CategoryChannel"); 112 | const ForumChannel = require("./ForumChannel"); 113 | const GroupChannel = require("./GroupChannel"); 114 | const GuildChannel = require("./GuildChannel"); 115 | const MediaChannel = require("./MediaChannel"); 116 | const NewsChannel = require("./NewsChannel"); 117 | const NewsThreadChannel = require("./NewsThreadChannel"); 118 | const PrivateChannel = require("./PrivateChannel"); 119 | const PrivateThreadChannel = require("./PrivateThreadChannel"); 120 | const PublicThreadChannel = require("./PublicThreadChannel"); 121 | const StageChannel = require("./StageChannel"); 122 | const TextChannel = require("./TextChannel"); 123 | const TextVoiceChannel = require("./TextVoiceChannel"); 124 | -------------------------------------------------------------------------------- /lib/structures/Entitlement.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents an entitlement 7 | * @extends Base 8 | */ 9 | class Entitlement extends Base { 10 | /** 11 | * The ID of the entitlement 12 | * @member {String} Entitlement#id 13 | */ 14 | #client; 15 | constructor(data, client) { 16 | super(data.id); 17 | 18 | this.#client = client; 19 | 20 | /** 21 | * The ID of the SKU associated with this entitlement 22 | * @type {String} 23 | */ 24 | this.skuID = data.sku_id; 25 | 26 | /** 27 | * The ID of the user that is granted access to the SKU 28 | * @type {String?} 29 | */ 30 | this.userID = data.user_id; 31 | 32 | /** 33 | * The ID of the guild that is granted access to the SKU 34 | * @type {String?} 35 | */ 36 | this.guildID = data.guild_id; 37 | 38 | /** 39 | * The ID of the application associated with this entitlement 40 | * @type {String} 41 | */ 42 | this.applicationID = data.application_id; 43 | 44 | /** 45 | * The type of the entitlement 46 | * @type {Number} 47 | */ 48 | this.type = data.type; 49 | 50 | /** 51 | * The timestamp at which the entitlement starts to be valid. `null` if the entitlement is a test entitlement. 52 | * @type {Number?} 53 | */ 54 | this.startsAt = data.starts_at != null ? Date.parse(data.starts_at) : null; 55 | /** 56 | * The timestamp at which the entitlement is no longer valid. `null` if the entitlement is a test entitlement. 57 | * @type {Number?} 58 | */ 59 | this.endsAt = data.ends_at != null ? Date.parse(data.ends_at) : null; 60 | /** 61 | * Whether this entitlement has been deleted or not 62 | * @type {Boolean} 63 | */ 64 | this.deleted = !!data.deleted; 65 | /** 66 | * Whether this entitlement was consumed or not 67 | * @type {Boolean} 68 | */ 69 | this.consumed = !!data.consumed; 70 | } 71 | 72 | /** 73 | * Consumes this entitlement 74 | * @returns {Promise} 75 | */ 76 | consume() { 77 | return this.#client.consumeEntitlement.call(this.#client, this.id); 78 | } 79 | 80 | toJSON(props = []) { 81 | return super.toJSON([ 82 | "skuID", 83 | "userID", 84 | "guildID", 85 | "applicationID", 86 | "type", 87 | "startsAt", 88 | "endsAt", 89 | "deleted", 90 | "consumed", 91 | ...props 92 | ]); 93 | } 94 | } 95 | 96 | module.exports = Entitlement; 97 | -------------------------------------------------------------------------------- /lib/structures/ExtendedUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const User = require("./User"); 4 | 5 | /** 6 | * Represents an extended user 7 | * @extends User 8 | */ 9 | class ExtendedUser extends User { 10 | constructor(data, client) { 11 | super(data, client); 12 | } 13 | 14 | update(data) { 15 | super.update(data); 16 | if(data.email !== undefined) { 17 | /** 18 | * The email of the user 19 | * @type {String?} 20 | */ 21 | this.email = data.email; 22 | } 23 | if(data.verified !== undefined) { 24 | /** 25 | * Whether the account email has been verified 26 | * @type {Boolean?} 27 | */ 28 | this.verified = data.verified; 29 | } 30 | if(data.mfa_enabled !== undefined) { 31 | /** 32 | * Whether the user has enabled two-factor authentication 33 | * @type {Boolean?} 34 | */ 35 | this.mfaEnabled = data.mfa_enabled; 36 | } 37 | if(data.premium_type !== undefined) { 38 | /** 39 | * The type of Nitro subscription on the user's account 40 | * @type {Number?} 41 | */ 42 | this.premiumType = data.premium_type; 43 | } 44 | } 45 | 46 | toJSON(props = []) { 47 | return super.toJSON([ 48 | "email", 49 | "mfaEnabled", 50 | "premium", 51 | "verified", 52 | ...props 53 | ]); 54 | } 55 | } 56 | 57 | module.exports = ExtendedUser; 58 | -------------------------------------------------------------------------------- /lib/structures/GroupChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Endpoints = require("../rest/Endpoints"); 4 | const PrivateChannel = require("./PrivateChannel"); 5 | 6 | /** 7 | * Represents a group channel 8 | * @extends PrivateChannel 9 | */ 10 | class GroupChannel extends PrivateChannel { // (╯°□°)╯︵ ┻━┻ 11 | #client; 12 | constructor(data, client) { 13 | super(data, client); 14 | this.#client = client; 15 | 16 | if(data.name !== undefined) { 17 | /** 18 | * The name of this group channel 19 | * @type {String} 20 | */ 21 | this.name = data.name; 22 | } 23 | if(data.owner_id !== undefined) { 24 | /** 25 | * The ID of the owner of this group channel 26 | * @type {String} 27 | */ 28 | this.ownerID = data.owner_id; 29 | } 30 | if(data.icon !== undefined) { 31 | /** 32 | * The hash of the group channel icon 33 | * @type {String?} 34 | */ 35 | this.icon = data.icon; 36 | } 37 | } 38 | 39 | /** 40 | * The URL of the group channel icon 41 | * @type {String?} 42 | */ 43 | get iconURL() { 44 | return this.icon ? this.#client._formatImage(Endpoints.CHANNEL_ICON(this.id, this.icon)) : null; 45 | } 46 | 47 | /** 48 | * Get the group's icon with the given format and size 49 | * @param {String} [format] The filetype of the icon ("jpg", "jpeg", "png", "gif", or "webp") 50 | * @param {Number} [size] The size of the icon (any power of two between 16 and 4096) 51 | * @returns {String?} 52 | */ 53 | dynamicIconURL(format, size) { 54 | return this.icon ? this.#client._formatImage(Endpoints.CHANNEL_ICON(this.id, this.icon), format, size) : null; 55 | } 56 | 57 | toJSON(props = []) { 58 | return super.toJSON([ 59 | "icon", 60 | "name", 61 | "ownerID", 62 | ...props 63 | ]); 64 | } 65 | } 66 | 67 | module.exports = GroupChannel; 68 | -------------------------------------------------------------------------------- /lib/structures/GuildIntegration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a guild integration 7 | * @extends Base 8 | */ 9 | class GuildIntegration extends Base { 10 | /** 11 | * The ID of the integration 12 | * @member {String} GuildIntegration#id 13 | */ 14 | /** 15 | * Timestamp of the guild integration's creation 16 | * @member {Number} GuildIntegration#createdAt 17 | */ 18 | constructor(data, guild) { 19 | super(data.id); 20 | /** 21 | * The guild this integration belongs to 22 | * @type {Guild} 23 | */ 24 | this.guild = guild; 25 | /** 26 | * The name of the integration 27 | * @type {String} 28 | */ 29 | this.name = data.name; 30 | /** 31 | * The type of the integration 32 | * @type {String} 33 | */ 34 | this.type = data.type; 35 | if(data.role_id !== undefined) { 36 | /** 37 | * The ID of the role connected to the integration 38 | * @type {String?} 39 | */ 40 | this.roleID = data.role_id; 41 | } 42 | if(data.user) { 43 | /** 44 | * The user connected to the integration 45 | * @type {User?} 46 | */ 47 | this.user = guild.shard.client.users.add(data.user, guild.shard.client); 48 | } 49 | /** 50 | * Info on the integration account 51 | * @type {GuildIntegration.AccountData} 52 | */ 53 | this.account = data.account; // not worth making a class for 54 | this.update(data); 55 | } 56 | 57 | update(data) { 58 | /** 59 | * Whether the integration is enabled or not 60 | * @type {Boolean} 61 | */ 62 | this.enabled = data.enabled; 63 | if(data.syncing !== undefined) { 64 | /** 65 | * Whether the integration is syncing or not 66 | * @type {Boolean?} 67 | */ 68 | this.syncing = data.syncing; 69 | } 70 | if(data.expire_behavior !== undefined) { 71 | /** 72 | * The behavior of expired subscriptions 73 | * @type {Number?} 74 | */ 75 | this.expireBehavior = data.expire_behavior; 76 | } 77 | if(data.expire_behavior !== undefined) { 78 | /** 79 | * The grace period for expired subscriptions 80 | * @type {Number?} 81 | */ 82 | this.expireGracePeriod = data.expire_grace_period; 83 | } 84 | if(data.enable_emoticons !== undefined) { 85 | /** 86 | * Whether integration emoticons are enabled or not 87 | * @type {Boolean?} 88 | */ 89 | this.enableEmoticons = data.enable_emoticons; 90 | } 91 | if(data.subscriber_count !== undefined) { 92 | /** 93 | * The amount of subscribers 94 | * @type {Number?} 95 | */ 96 | this.subscriberCount = data.subscriber_count; 97 | } 98 | if(data.synced_at !== undefined) { 99 | /** 100 | * Unix timestamp of last integration sync 101 | * @type {Number?} 102 | */ 103 | this.syncedAt = data.synced_at; 104 | } 105 | if(data.revoked !== undefined) { 106 | /** 107 | * Whether or not the application was revoked 108 | * @type {Boolean?} 109 | */ 110 | this.revoked = data.revoked; 111 | } 112 | if(data.application !== undefined) { 113 | /** 114 | * The bot/OAuth2 application for Discord integrations. See [the Discord docs](https://discord.com/developers/docs/resources/guild#integration-application-object) 115 | * @type {Object?} 116 | */ 117 | this.application = data.application; 118 | } 119 | if(data.scopes !== undefined) { 120 | /** 121 | * The scope the application is authorized for 122 | * @type {Array?} 123 | */ 124 | this.scopes = data.scopes; 125 | } 126 | } 127 | 128 | /** 129 | * Delete the guild integration 130 | * @returns {Promise} 131 | */ 132 | delete() { 133 | return this.guild.shard.client.deleteGuildIntegration.call(this.guild.shard.client, this.guild.id, this.id); 134 | } 135 | 136 | toJSON(props = []) { 137 | return super.toJSON([ 138 | "account", 139 | "application", 140 | "enabled", 141 | "enableEmoticons", 142 | "expireBehavior", 143 | "expireGracePeriod", 144 | "name", 145 | "revoked", 146 | "roleID", 147 | "scopes", 148 | "subscriberCount", 149 | "syncedAt", 150 | "syncing", 151 | "type", 152 | "user", 153 | ...props 154 | ]); 155 | } 156 | } 157 | 158 | module.exports = GuildIntegration; 159 | 160 | /** 161 | * Info on the integration account 162 | * @typedef GuildIntegration.AccountData 163 | * @prop {String} id The ID of the integration account 164 | * @prop {String} name The name of the integration account 165 | */ 166 | -------------------------------------------------------------------------------- /lib/structures/GuildPreview.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Endpoints = require("../rest/Endpoints.js"); 5 | 6 | /** 7 | * Represents a GuildPreview structure 8 | * @extends Base 9 | */ 10 | class GuildPreview extends Base { 11 | #client; 12 | /** 13 | * The ID of the guild 14 | * @member {String} GuildPreview#id 15 | */ 16 | constructor(data, client) { 17 | super(data.id); 18 | this.#client = client; 19 | 20 | /** 21 | * The name of the guild 22 | * @type {String} 23 | */ 24 | this.name = data.name; 25 | /** 26 | * The hash of the guild icon, or null if no icon 27 | * @type {String?} 28 | */ 29 | this.icon = data.icon; 30 | /** 31 | * The description for the guild 32 | * @type {String?} 33 | */ 34 | this.description = data.description; 35 | /** 36 | * The hash of the guild splash image, or null if no splash (VIP only) 37 | * @type {String?} 38 | */ 39 | this.splash = data.splash; 40 | /** 41 | * The hash of the guild discovery splash image, or null if no splash 42 | * @type {String?} 43 | */ 44 | this.discoverySplash = data.discovery_splash; 45 | /** 46 | * An array of guild feature strings 47 | * @type {Array} 48 | */ 49 | this.features = data.features; 50 | /** 51 | * The **approximate** number of members in the guild 52 | * @type {Number} 53 | */ 54 | this.approximateMemberCount = data.approximate_member_count; 55 | /** 56 | * The **approximate** number of presences in the guild 57 | * @type {Number} 58 | */ 59 | this.approximatePresenceCount = data.approximate_presence_count; 60 | /** 61 | * An array of guild emoji objects 62 | * @type {Array} 63 | */ 64 | this.emojis = data.emojis; 65 | /** 66 | * An array of guild sticker objects 67 | * @type {Array} 68 | */ 69 | this.stickers = data.stickers; 70 | } 71 | 72 | /** 73 | * The URL of the guild's icon 74 | * @type {String?} 75 | */ 76 | get iconURL() { 77 | return this.icon ? this.#client._formatImage(Endpoints.GUILD_ICON(this.id, this.icon)) : null; 78 | } 79 | 80 | /** 81 | * The URL of the guild's splash image 82 | * @type {String?} 83 | */ 84 | get splashURL() { 85 | return this.splash ? this.#client._formatImage(Endpoints.GUILD_SPLASH(this.id, this.splash)) : null; 86 | } 87 | 88 | /** 89 | * The URL of the guild's discovery splash image 90 | * @type {String?} 91 | */ 92 | get discoverySplashURL() { 93 | return this.discoverySplash ? this.#client._formatImage(Endpoints.GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash)) : null; 94 | } 95 | 96 | /** 97 | * Get the guild's splash with the given format and size 98 | * @param {String} [format] The filetype of the icon ("jpg", "jpeg", "png", "gif", or "webp") 99 | * @param {Number} [size] The size of the icon (any power of two between 16 and 4096) 100 | * @returns {String?} 101 | */ 102 | dynamicDiscoverySplashURL(format, size) { 103 | return this.discoverySplash ? this.#client._formatImage(Endpoints.GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash), format, size) : null; 104 | } 105 | 106 | /** 107 | * Get the guild's icon with the given format and size 108 | * @param {String} [format] The filetype of the icon ("jpg", "jpeg", "png", "gif", or "webp") 109 | * @param {Number} [size] The size of the icon (any power of two between 16 and 4096) 110 | * @returns {String?} 111 | */ 112 | dynamicIconURL(format, size) { 113 | return this.icon ? this.#client._formatImage(Endpoints.GUILD_ICON(this.id, this.icon), format, size) : null; 114 | } 115 | 116 | /** 117 | * Get the guild's splash with the given format and size 118 | * @param {String} [format] The filetype of the icon ("jpg", "jpeg", "png", "gif", or "webp") 119 | * @param {Number} [size] The size of the icon (any power of two between 16 and 4096) 120 | * @returns {String?} 121 | */ 122 | dynamicSplashURL(format, size) { 123 | return this.splash ? this.#client._formatImage(Endpoints.GUILD_SPLASH(this.id, this.splash), format, size) : null; 124 | } 125 | 126 | toJSON(props = []) { 127 | return super.toJSON([ 128 | "approximateMemberCount", 129 | "approximatePresenceCount", 130 | "description", 131 | "discoverySplash", 132 | "emojis", 133 | "features", 134 | "icon", 135 | "name", 136 | "splash", 137 | ...props 138 | ]); 139 | } 140 | } 141 | 142 | module.exports = GuildPreview; 143 | -------------------------------------------------------------------------------- /lib/structures/GuildTemplate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Guild = require("./Guild"); 5 | 6 | /** 7 | * Represents a guild template 8 | */ 9 | class GuildTemplate { 10 | #client; 11 | constructor(data, client) { 12 | this.#client = client; 13 | /** 14 | * The template code 15 | * @type {String} 16 | */ 17 | this.code = data.code; 18 | /** 19 | * Timestamp of template creation 20 | * @type {Number} 21 | */ 22 | this.createdAt = Date.parse(data.created_at); 23 | /** 24 | * The user that created the template 25 | * @type {User} 26 | */ 27 | this.creator = client.users.update(data.creator, client); 28 | /** 29 | * The template description 30 | * @type {String?} 31 | */ 32 | this.description = data.description; 33 | /** 34 | * Whether the template has unsynced changes 35 | * @type {Boolean?} 36 | */ 37 | this.isDirty = data.is_dirty; 38 | /** 39 | * The template name 40 | * @type {String} 41 | */ 42 | this.name = data.name; 43 | /** 44 | * The guild snapshot this template contains 45 | * @type {Guild} 46 | */ 47 | this.serializedSourceGuild = new Guild(data.serialized_source_guild, client); 48 | /** 49 | * The guild this template is based on. If the guild is not cached, this will be an object with `id` key. No other property is guaranteed 50 | * @type {Guild | Object} 51 | */ 52 | this.sourceGuild = client.guilds.get(data.source_guild_id) || {id: data.source_guild_id}; 53 | /** 54 | * Timestamp of template update 55 | * @type {Number} 56 | */ 57 | this.updatedAt = Date.parse(data.updated_at); 58 | /** 59 | * The number of times this template has been used 60 | * @type {Number} 61 | */ 62 | this.usageCount = data.usage_count; 63 | } 64 | 65 | /** 66 | * Create a guild based on this template. This can only be used with bots in less than 10 guilds 67 | * @param {String} name The name of the guild 68 | * @param {String} [icon] The 128x128 icon as a base64 data URI 69 | * @returns {Promise} 70 | */ 71 | createGuild(name, icon) { 72 | return this.#client.createGuildFromTemplate.call(this.#client, this.code, name, icon); 73 | } 74 | 75 | /** 76 | * Delete this template 77 | * @returns {Promise} 78 | */ 79 | delete() { 80 | return this.#client.deleteGuildTemplate.call(this.#client, this.sourceGuild.id, this.code); 81 | } 82 | 83 | /** 84 | * Edit this template 85 | * @param {Object} options The properties to edit 86 | * @param {String} [options.name] The name of the template 87 | * @param {String?} [options.description] The description for the template. Set to `null` to remove the description 88 | * @returns {Promise} 89 | */ 90 | edit(options) { 91 | return this.#client.editGuildTemplate.call(this.#client, this.sourceGuild.id, this.code, options); 92 | } 93 | 94 | /** 95 | * Force this template to sync to the guild's current state 96 | * @returns {Promise} 97 | */ 98 | sync() { 99 | return this.#client.syncGuildTemplate.call(this.#client, this.sourceGuild.id, this.code); 100 | } 101 | 102 | toJSON(props = []) { 103 | return Base.prototype.toJSON.call(this, [ 104 | "code", 105 | "createdAt", 106 | "creator", 107 | "description", 108 | "isDirty", 109 | "name", 110 | "serializedSourceGuild", 111 | "sourceGuild", 112 | "updatedAt", 113 | "usageCount", 114 | ...props 115 | ]); 116 | } 117 | } 118 | 119 | module.exports = GuildTemplate; 120 | -------------------------------------------------------------------------------- /lib/structures/InteractionMetadata.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Base = require("./Base"); 3 | const User = require("./User"); 4 | 5 | /** 6 | * Represents metadata about the interaction in a {@link Message} object 7 | * @extends Base 8 | */ 9 | class InteractionMetadata extends Base { 10 | /** 11 | * The ID of the interaction 12 | * @member {String} InteractionMetadata#id 13 | */ 14 | 15 | constructor(data, client) { 16 | super(data.id); 17 | 18 | /** 19 | * The type of the interaction 20 | * @type {Number} 21 | */ 22 | this.type = data.type; 23 | 24 | /** 25 | * The user who triggered the interaction 26 | * @type {User} 27 | */ 28 | this.user = client ? client.users.update(data.user, client) : new User(data.user, client); 29 | 30 | /** 31 | * A mapping of installation contexts that the app was authorized for to respective guild/user IDs 32 | * @type {Object} 33 | */ 34 | this.authorizingIntegrationOwners = data.authorizing_integration_owners; 35 | 36 | /** 37 | * The ID of the original response message (present only on follow-up messages) 38 | * @type {String?} 39 | */ 40 | this.originalResponseMessageID = data.original_response_message_id; 41 | 42 | /** 43 | * The ID of the message which contained the interactive component (present only on messages created from component interactions) 44 | * @type {String?} 45 | */ 46 | this.interactedMessageID = data.interacted_message_id; 47 | 48 | if(data.triggering_interaction_metadata !== undefined) { 49 | /** 50 | * The metadata for the interaction that was used to open the modal (present only on modal submit interactions) 51 | * @type {InteractionMetadata?} 52 | */ 53 | this.triggeringInteractionMetadata = new InteractionMetadata(data.triggering_interaction_metadata, client); 54 | } 55 | 56 | if(data.target_user !== undefined) { 57 | /** 58 | * The user an interaction command was run on, present only on user command interactions 59 | * @type {User?} 60 | */ 61 | this.targetUser = client ? client.users.update(data.target_user, client) : new User(data.target_user, client); 62 | } 63 | 64 | /** 65 | * The ID of the message an interaction command was run on, present only on message command interactions 66 | * @type {String?} 67 | */ 68 | this.targetMessageID = data.target_message_id; 69 | } 70 | 71 | toJSON(props = []) { 72 | return super.toJSON([ 73 | "type", 74 | "user", 75 | "authorizingIntegrationOwners", 76 | "originalResponseMessageID", 77 | "interactedMessageID", 78 | "triggeringInteractionMetadata", 79 | "targetUser", 80 | "targetMessageID", 81 | ...props 82 | ]); 83 | } 84 | } 85 | 86 | module.exports = InteractionMetadata; 87 | -------------------------------------------------------------------------------- /lib/structures/Invite.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Guild = require("./Guild"); 5 | const GuildScheduledEvent = require("./GuildScheduledEvent"); 6 | 7 | /** 8 | * Represents an invite. Some properties are only available when fetching invites from channels, which requires the Manage Channel permission. 9 | * @extends Base 10 | */ 11 | class Invite extends Base { 12 | /** 13 | * Invites don't have an ID. 14 | * @private 15 | * @override 16 | * @member {undefined} Invite#id 17 | */ 18 | 19 | #client; 20 | #createdAt; 21 | constructor(data, client) { 22 | super(); 23 | this.#client = client; 24 | /** 25 | * The invite code 26 | * @type {String} 27 | */ 28 | this.code = data.code; 29 | /** 30 | * The invite type 31 | * @type {Number} 32 | */ 33 | this.type = data.type; 34 | if(data.guild && client.guilds.has(data.guild.id)) { 35 | /** 36 | * Info on the invite channel 37 | * @type {TextChannel | NewsChannel | TextVoiceChannel | StageChannel | Invite.UncachedInviteChannel} 38 | */ 39 | this.channel = client.guilds.get(data.guild.id).channels.update(data.channel, client); 40 | } else { 41 | this.channel = data.channel; 42 | } 43 | if(data.guild) { 44 | if(client.guilds.has(data.guild.id)) { 45 | /** 46 | * Info on the invite guild 47 | * @type {Guild?} 48 | */ 49 | this.guild = client.guilds.update(data.guild, client); 50 | } else { 51 | this.guild = new Guild(data.guild, client); 52 | } 53 | } 54 | if(data.inviter) { 55 | /** 56 | * The invite creator 57 | * @type {User?} 58 | */ 59 | this.inviter = client.users.add(data.inviter, client); 60 | } 61 | /** 62 | * The number of invite uses 63 | * @type {Number?} 64 | */ 65 | this.uses = data.uses !== undefined ? data.uses : null; 66 | /** 67 | * The max number of invite uses 68 | * @type {Number?} 69 | */ 70 | this.maxUses = data.max_uses !== undefined ? data.max_uses : null; 71 | /** 72 | * How long the invite lasts in seconds 73 | * @type {Number?} 74 | */ 75 | this.maxAge = data.max_age !== undefined ? data.max_age : null; 76 | /** 77 | * Whether the invite grants temporary membership or not 78 | * @type {Boolean?} 79 | */ 80 | this.temporary = data.temporary !== undefined ? data.temporary : null; 81 | this.#createdAt = data.created_at !== undefined ? data.created_at : null; 82 | /** 83 | * The **approximate** presence count for the guild 84 | * @type {Number?} 85 | */ 86 | this.presenceCount = data.approximate_presence_count !== undefined ? data.approximate_presence_count : null; 87 | /** 88 | * The **approximate** member count for the guild 89 | * @type {Number?} 90 | */ 91 | this.memberCount = data.approximate_member_count !== undefined ? data.approximate_member_count : null; 92 | if(data.stage_instance !== undefined) { 93 | data.stage_instance.members = data.stage_instance.members.map((m) => { 94 | m.id = m.user.id; 95 | return m; 96 | }); 97 | /** 98 | * The active public stage instance data for the stage channel this invite is for 99 | * @deprecated Deprecated in Discord's API 100 | * @type {Invite.StageInstance} 101 | */ 102 | this.stageInstance = { 103 | members: data.stage_instance.members.map((m) => this.guild.members.update(m, this.guild)), 104 | participantCount: data.stage_instance.participant_count, 105 | speakerCount: data.stage_instance.speaker_count, 106 | topic: data.stage_instance.topic 107 | }; 108 | } else { 109 | this.stageInstance = null; 110 | } 111 | if(data.target_application !== undefined) { 112 | /** 113 | * The target application 114 | * @type {Object?} 115 | */ 116 | this.targetApplication = data.target_application; 117 | } 118 | if(data.target_type !== undefined) { 119 | /** 120 | * The type of the target application 121 | * @type {Number?} 122 | */ 123 | this.targetType = data.target_type; 124 | } 125 | if(data.target_user !== undefined) { 126 | /** 127 | * The user whose stream is displayed for the invite (voice channel only) 128 | * @type {User?} 129 | */ 130 | this.targetUser = client.users.update(data.target_user, this.#client); 131 | } 132 | if(data.expires_at !== undefined) { 133 | /** 134 | * Timestamp of invite expiration 135 | * @type {Number?} 136 | */ 137 | this.expiresAt = Date.parse(data.expires_at); 138 | } 139 | if(data.guild_scheduled_event !== undefined) { 140 | /** 141 | * The guild scheduled event associated with the invite 142 | * @type {GuildScheduledEvent?} 143 | */ 144 | this.guildScheduledEvent = new GuildScheduledEvent(data.guild_scheduled_event, client); 145 | } 146 | } 147 | 148 | /** 149 | * Timestamp of invite creation 150 | * @type {Number?} 151 | */ 152 | get createdAt() { 153 | return Date.parse(this.#createdAt); 154 | } 155 | 156 | /** 157 | * Delete the invite 158 | * @param {String} [reason] The reason to be displayed in audit logs 159 | * @returns {Promise} 160 | */ 161 | delete(reason) { 162 | return this.#client.deleteInvite.call(this.#client, this.code, reason); 163 | } 164 | 165 | toString() { 166 | return `[Invite ${this.code}]`; 167 | } 168 | 169 | toJSON(props = []) { 170 | return super.toJSON([ 171 | "channel", 172 | "code", 173 | "createdAt", 174 | "guild", 175 | "maxAge", 176 | "maxUses", 177 | "memberCount", 178 | "presenceCount", 179 | "revoked", 180 | "temporary", 181 | "uses", 182 | ...props 183 | ]); 184 | } 185 | } 186 | 187 | module.exports = Invite; 188 | 189 | /** 190 | * Information about an uncached invite 191 | * @typedef Invite.UncachedInviteChannel 192 | * @prop {String} id The ID of the invite's channel 193 | * @prop {String?} name The name of the invite's channel 194 | * @prop {Number} type The type of the invite's channel 195 | * @prop {String?} icon The icon of a channel (group dm) 196 | */ 197 | 198 | /** 199 | * Information about the active public stage instance 200 | * @deprecated Deprecated in Discord's API 201 | * @typedef Invite.StageInstance 202 | * @prop {Member[]} members The members in the stage instance 203 | * @prop {Number} participantCount The number of participants in the stage instance 204 | * @prop {Number} speakerCount The number of speakers in the stage instance 205 | * @prop {String} topic The topic of the stage instance 206 | */ 207 | -------------------------------------------------------------------------------- /lib/structures/MediaChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ForumChannel = require("./ForumChannel"); 4 | 5 | /** 6 | * Represents a media channel. 7 | * @extends ForumChannel 8 | */ 9 | class MediaChannel extends ForumChannel {} 10 | 11 | module.exports = MediaChannel; 12 | -------------------------------------------------------------------------------- /lib/structures/NewsChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TextChannel = require("./TextChannel"); 4 | 5 | /** 6 | * Represents a guild news channel. 7 | * @extends TextChannel 8 | */ 9 | class NewsChannel extends TextChannel { 10 | #client; 11 | constructor(data, client, messageLimit) { 12 | super(data, client, messageLimit); 13 | this.#client = client; 14 | this.rateLimitPerUser = 0; 15 | } 16 | 17 | /** 18 | * Crosspost (publish) a message to subscribed channels 19 | * @param {String} messageID The ID of the message 20 | * @returns {Promise} 21 | */ 22 | crosspostMessage(messageID) { 23 | return this.#client.crosspostMessage.call(this.#client, this.id, messageID); 24 | } 25 | 26 | /** 27 | * Follow this channel in another channel. This creates a webhook in the target channel 28 | * @param {String} webhookChannelID The ID of the target channel 29 | * @param {String} [reason] The reason to be displayed in audit logs 30 | * @returns {Object} An object containing this channel's ID and the new webhook's ID 31 | */ 32 | follow(webhookChannelID, reason) { 33 | return this.#client.followChannel.call(this.#client, this.id, webhookChannelID, reason); 34 | } 35 | } 36 | 37 | module.exports = NewsChannel; 38 | -------------------------------------------------------------------------------- /lib/structures/NewsThreadChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ThreadChannel = require("./ThreadChannel"); 4 | 5 | /** 6 | * Represents a news thread channel. 7 | * @extends ThreadChannel 8 | */ 9 | class NewsThreadChannel extends ThreadChannel { 10 | constructor(data, client, messageLimit) { 11 | super(data, client, messageLimit); 12 | } 13 | } 14 | 15 | module.exports = NewsThreadChannel; 16 | -------------------------------------------------------------------------------- /lib/structures/Permission.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const {Permissions} = require("../Constants"); 5 | 6 | /** 7 | * Represents a calculated permissions number 8 | */ 9 | class Permission extends Base { 10 | #json; 11 | constructor(allow, deny = 0) { 12 | super(); 13 | /** 14 | * The allowed permissions number 15 | * @type {BigInt} 16 | */ 17 | this.allow = BigInt(allow); 18 | /** 19 | * The denied permissions number 20 | * @type {BigInt} 21 | */ 22 | this.deny = BigInt(deny); 23 | } 24 | 25 | /** 26 | * A JSON representation of the permissions number. 27 | * If a permission key isn't there, it is not set by this permission. 28 | * If a permission key is false, it is denied by the permission. 29 | * If a permission key is true, it is allowed by the permission. 30 | * i.e.: 31 | * ```json 32 | * { 33 | * "readMessages": true, 34 | * "sendMessages": true, 35 | * "manageMessages": false 36 | * } 37 | * ``` 38 | * In the above example, readMessages and sendMessages are allowed permissions, and manageMessages is denied. Everything else is not explicitly set. 39 | * [A full list of permission nodes can be found in Constants](https://github.com/projectdysnomia/dysnomia/blob/dev/lib/Constants.js#L442) 40 | * @type {Object} 41 | */ 42 | get json() { 43 | if(!this.#json) { 44 | this.#json = {}; 45 | for(const perm of Object.keys(Permissions)) { 46 | if(!perm.startsWith("all")) { 47 | if(this.allow & Permissions[perm]) { 48 | this.#json[perm] = true; 49 | } else if(this.deny & Permissions[perm]) { 50 | this.#json[perm] = false; 51 | } 52 | } 53 | } 54 | } 55 | return this.#json; 56 | } 57 | 58 | /** 59 | * Check if this permission allows a specific permission 60 | * @param {String | BigInt} permission The name of the permission, or bit of permissions. [A full list of permission nodes can be found on the docs reference page](/Eris/docs/reference). Pass a BigInt if you want to check multiple permissions. 61 | * @returns {Boolean} Whether the permission allows the specified permission 62 | */ 63 | has(permission) { 64 | if(typeof permission === "bigint") { 65 | return (this.allow & permission) === permission; 66 | } 67 | return !!(this.allow & Permissions[permission]); 68 | } 69 | 70 | toString() { 71 | return `[${this.constructor.name} +${this.allow} -${this.deny}]`; 72 | } 73 | 74 | toJSON(props = []) { 75 | return super.toJSON([ 76 | "allow", 77 | "deny", 78 | ...props 79 | ]); 80 | } 81 | } 82 | 83 | module.exports = Permission; 84 | -------------------------------------------------------------------------------- /lib/structures/PermissionOverwrite.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Permission = require("./Permission"); 4 | 5 | /** 6 | * Represents a permission overwrite 7 | * @extends Permission 8 | */ 9 | class PermissionOverwrite extends Permission { 10 | constructor(data) { 11 | super(data.allow, data.deny); 12 | /** 13 | * The ID of the overwrite 14 | * @type {String} 15 | */ 16 | this.id = data.id; 17 | /** 18 | * The type of the overwrite, either 1 for "member" or 0 for "role" 19 | * @type {Number} 20 | */ 21 | this.type = data.type; 22 | } 23 | 24 | toJSON(props = []) { 25 | return super.toJSON([ 26 | "type", 27 | ...props 28 | ]); 29 | } 30 | } 31 | 32 | module.exports = PermissionOverwrite; 33 | -------------------------------------------------------------------------------- /lib/structures/PingInteraction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Interaction = require("./Interaction"); 4 | const {InteractionResponseTypes} = require("../Constants"); 5 | 6 | /** 7 | * Represents a ping interaction. See Interaction for more properties. 8 | * @extends Interaction 9 | */ 10 | class PingInteraction extends Interaction { 11 | #client; 12 | constructor(data, client) { 13 | super(data, client); 14 | this.#client = client; 15 | } 16 | 17 | /** 18 | * Acknowledges the ping interaction with a pong response. 19 | * Note: You can **not** use more than 1 initial interaction response per interaction. 20 | * @returns {Promise} 21 | */ 22 | acknowledge() { 23 | return this.pong(); 24 | } 25 | 26 | /** 27 | * Acknowledges the ping interaction with a pong response. 28 | * Note: You can **not** use more than 1 initial interaction response per interaction. 29 | * @returns {Promise} 30 | */ 31 | pong() { 32 | if(this.acknowledged === true) { 33 | throw new Error("You have already acknowledged this interaction."); 34 | } 35 | return this.#client.createInteractionResponse.call(this.#client, this.id, this.token, { 36 | type: InteractionResponseTypes.PONG 37 | }).then(() => this.update()); 38 | } 39 | } 40 | 41 | module.exports = PingInteraction; 42 | -------------------------------------------------------------------------------- /lib/structures/PrivateThreadChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ThreadChannel = require("./ThreadChannel"); 4 | 5 | /** 6 | * Represents a private thread channel. See ThreadChannel for extra properties. 7 | * @extends ThreadChannel 8 | */ 9 | class PrivateThreadChannel extends ThreadChannel { 10 | constructor(data, client, messageLimit) { 11 | super(data, client, messageLimit); 12 | } 13 | 14 | update(data, client) { 15 | super.update(data, client); 16 | if(data.thread_metadata !== undefined) { 17 | /** 18 | * Metadata for the thread 19 | * @override 20 | * @type {ThreadChannel.ThreadMetadata} 21 | */ 22 | this.threadMetadata = { 23 | archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), 24 | archived: data.thread_metadata.archived, 25 | autoArchiveDuration: data.thread_metadata.auto_archive_duration, 26 | createTimestamp: !data.thread_metadata.create_timestamp ? null : Date.parse(data.thread_metadata.create_timestamp), 27 | invitable: data.thread_metadata.invitable, 28 | locked: data.thread_metadata.locked 29 | }; 30 | } 31 | } 32 | } 33 | 34 | module.exports = PrivateThreadChannel; 35 | -------------------------------------------------------------------------------- /lib/structures/PublicThreadChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ThreadChannel = require("./ThreadChannel"); 4 | 5 | /** 6 | * Represents a public thread channel. See ThreadChannel for extra properties. 7 | * @extends ThreadChannel 8 | */ 9 | class PublicThreadChannel extends ThreadChannel { 10 | constructor(data, client, messageLimit) { 11 | super(data, client, messageLimit); 12 | } 13 | 14 | update(data, client) { 15 | super.update(data, client); 16 | if(data.applied_tags !== undefined) { 17 | /** 18 | * An array of applied tag IDs for the thread (available only in threads in thread-only channels) 19 | * @type {Array?} 20 | */ 21 | this.appliedTags = data.applied_tags; 22 | } 23 | } 24 | } 25 | 26 | module.exports = PublicThreadChannel; 27 | -------------------------------------------------------------------------------- /lib/structures/Role.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Endpoints = require("../rest/Endpoints"); 5 | const Permission = require("./Permission"); 6 | 7 | /** 8 | * Represents a role 9 | * @extends Base 10 | */ 11 | class Role extends Base { 12 | /** 13 | * The ID of the role 14 | * @member {String} Role#id 15 | */ 16 | /** 17 | * Timestamp of the role's creation 18 | * @member {Number} Role#createdAt 19 | */ 20 | constructor(data, guild) { 21 | super(data.id); 22 | /** 23 | * The guild that owns the role 24 | * @type {Guild} 25 | */ 26 | this.guild = guild; 27 | this.update(data); 28 | } 29 | 30 | update(data) { 31 | if(data.name !== undefined) { 32 | /** 33 | * The name of the role 34 | * @type {String} 35 | */ 36 | this.name = data.name; 37 | } 38 | if(data.mentionable !== undefined) { 39 | /** 40 | * Whether the role is mentionable or not 41 | * @type {Boolean} 42 | */ 43 | this.mentionable = data.mentionable; 44 | } 45 | if(data.managed !== undefined) { 46 | /** 47 | * Whether a guild integration manages this role or not 48 | * @type {Boolean} 49 | */ 50 | this.managed = data.managed; 51 | } 52 | if(data.hoist !== undefined) { 53 | /** 54 | * Whether users with this role are hoisted in the user list or not 55 | * @type {Boolean} 56 | */ 57 | this.hoist = data.hoist; 58 | } 59 | if(data.color !== undefined) { 60 | /** 61 | * The hex color of the role in base 10 62 | * @type {Number} 63 | */ 64 | this.color = data.color; 65 | } 66 | if(data.position !== undefined) { 67 | /** 68 | * The position of the role 69 | * @type {Number} 70 | */ 71 | this.position = data.position; 72 | } 73 | if(data.permissions !== undefined) { 74 | /** 75 | * The permissions representation of the role 76 | * @type {Permission} 77 | */ 78 | this.permissions = new Permission(data.permissions); 79 | } 80 | if(data.tags !== undefined) { 81 | /** 82 | * The tags of the role 83 | * @type {Object} 84 | * @prop {String?} bot_id The ID of the bot associated with the role 85 | * @prop {String?} integration_id The ID of the integration associated with the role 86 | * @prop {Boolean?} premium_subscriber Whether the role is the guild's premium subscriber role 87 | */ 88 | this.tags = data.tags; 89 | if(this.tags.guild_connections === null) { 90 | this.tags.guild_connections = true; 91 | } 92 | if(this.tags.premium_subscriber === null) { 93 | this.tags.premium_subscriber = true; 94 | } 95 | if(this.tags.available_for_purchase === null) { 96 | this.tags.available_for_purchase = true; 97 | } 98 | } 99 | if(data.icon !== undefined) { 100 | /** 101 | * The hash of the role's icon, or null if no icon 102 | * @type {String?} 103 | */ 104 | this.icon = data.icon; 105 | } 106 | if(data.unicode_emoji !== undefined) { 107 | /** 108 | * Unicode emoji for the role 109 | * @type {String?} 110 | */ 111 | this.unicodeEmoji = data.unicode_emoji; 112 | } 113 | if(data.flags !== undefined) { 114 | /** 115 | * Role flags. See [Discord's documentation](https://discord.com/developers/docs/topics/permissions#role-object-role-flags) for a list of them 116 | * @type {Number} 117 | */ 118 | this.flags = data.flags; 119 | } 120 | } 121 | 122 | /** 123 | * The URL of the role's icon 124 | * @type {String} 125 | */ 126 | get iconURL() { 127 | return this.icon ? this.guild.shard.client._formatImage(Endpoints.ROLE_ICON(this.id, this.icon)) : null; 128 | } 129 | 130 | /** 131 | * Generates a JSON representation of the role permissions 132 | * @type {Object} 133 | */ 134 | get json() { 135 | return this.permissions.json; 136 | } 137 | 138 | /** 139 | * A string that mentions the role 140 | * @type {String} 141 | */ 142 | get mention() { 143 | return `<@&${this.id}>`; 144 | } 145 | 146 | /** 147 | * Delete the role 148 | * @param {String} [reason] The reason to be displayed in audit logs 149 | * @returns {Promise} 150 | */ 151 | delete(reason) { 152 | return this.guild.shard.client.deleteRole.call(this.guild.shard.client, this.guild.id, this.id, reason); 153 | } 154 | 155 | /** 156 | * Edit the guild role 157 | * @param {Object} options The properties to edit 158 | * @param {Number} [options.color] The hex color of the role, in number form (ex: 0x3da5b3 or 4040115) 159 | * @param {Boolean} [options.hoist] Whether to hoist the role in the user list or not 160 | * @param {String} [options.icon] The role icon as a base64 data URI 161 | * @param {Boolean} [options.mentionable] Whether the role is mentionable or not 162 | * @param {String} [options.name] The name of the role 163 | * @param {BigInt | Number} [options.permissions] The role permissions number 164 | * @param {String?} [options.unicodeEmoji] The role's unicode emoji 165 | * @param {String} [reason] The reason to be displayed in audit logs 166 | * @returns {Promise} 167 | */ 168 | edit(options, reason) { 169 | return this.guild.shard.client.editRole.call(this.guild.shard.client, this.guild.id, this.id, options, reason); 170 | } 171 | 172 | /** 173 | * Edit the role's position. Note that role position numbers are highest on top and lowest at the bottom. 174 | * @param {Number} position The new position of the role 175 | * @returns {Promise} 176 | */ 177 | editPosition(position) { 178 | return this.guild.shard.client.editRolePosition.call(this.guild.shard.client, this.guild.id, this.id, position); 179 | } 180 | 181 | toJSON(props = []) { 182 | return super.toJSON([ 183 | "color", 184 | "hoist", 185 | "icon", 186 | "flags", 187 | "managed", 188 | "mentionable", 189 | "name", 190 | "permissions", 191 | "position", 192 | "tags", 193 | "unicodeEmoji", 194 | ...props 195 | ]); 196 | } 197 | } 198 | 199 | module.exports = Role; 200 | -------------------------------------------------------------------------------- /lib/structures/SKU.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Base = require("./Base"); 3 | 4 | /** 5 | * Represents a SKU 6 | * @extends Base 7 | */ 8 | class SKU extends Base { 9 | /** 10 | * The ID of the SKU 11 | * @member {String} SKU#id 12 | */ 13 | 14 | #client; 15 | constructor(data, client) { 16 | super(data.id); 17 | this.#client = client; 18 | 19 | /** 20 | * The type of the SKU 21 | * @type {Number} 22 | */ 23 | this.type = data.type; 24 | 25 | /** 26 | * The ID of the application that owns this SKU 27 | * @type {String} 28 | */ 29 | this.applicationID = data.application_id; 30 | 31 | /** 32 | * The customer-facing name of the SKU 33 | * @type {String} 34 | */ 35 | this.name = data.name; 36 | 37 | /** 38 | * A system-generated URL slug for the SKU 39 | * @type {String} 40 | */ 41 | this.slug = data.slug; 42 | 43 | /** 44 | * The SKU flag bitfield 45 | * @type {Number} 46 | */ 47 | this.flags = data.flags; 48 | } 49 | 50 | /** 51 | * Gets a subscription to this SKU 52 | * @param {String} subscriptionID The ID of the subscription 53 | * @returns {Promise} 54 | */ 55 | getSubscription(subscriptionID) { 56 | return this.#client.getSKUSubscription.call(this.#client, this.id, subscriptionID); 57 | } 58 | 59 | /** 60 | * Gets the list of subscriptions to this SKU 61 | * @param {Object} options The options for the request 62 | * @param {String} [options.after] Get subscriptions after this subscription ID 63 | * @param {String} [options.before] Get subscriptions before this subscription ID 64 | * @param {Number} [options.limit] The maximum number of subscriptions to get 65 | * @param {String} options.userID The ID of the user to get subscriptions for (can be omitted only if requesting with an OAuth token) 66 | * @returns {Promise>} 67 | */ 68 | getSubscriptions(options) { 69 | return this.#client.getSKUSubscriptions.call(this.#client, this.id, options); 70 | } 71 | 72 | toJSON(props = []) { 73 | return super.toJSON([ 74 | "type", 75 | "applicationID", 76 | "name", 77 | "slug", 78 | "flags", 79 | ...props 80 | ]); 81 | } 82 | } 83 | 84 | module.exports = SKU; 85 | -------------------------------------------------------------------------------- /lib/structures/SoundboardSound.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a Soundboard Sound 7 | * @extends Base 8 | */ 9 | class SoundboardSound extends Base { 10 | /** 11 | * The ID of the soundboard sound 12 | * @member {String} SoundboardSound#id 13 | */ 14 | 15 | #client; 16 | constructor(data, client) { 17 | super(data.id); 18 | this.#client = client; 19 | if(data.guild_id !== undefined) { 20 | /** 21 | * The guild where the soundboard sound was created in (not present for default soundboard sounds). If the guild is uncached, this will be an object with an `id` key. No other keys are guaranteed 22 | * @type {Guild?} 23 | */ 24 | this.guild = client.guilds.get(data.guild_id) || {id: data.guild_id}; 25 | } 26 | } 27 | 28 | update(data) { 29 | if(data.name !== undefined) { 30 | /** 31 | * The name of the soundboard sound 32 | * @type {String} 33 | */ 34 | this.name = data.name; 35 | } 36 | if(data.volume !== undefined) { 37 | /** 38 | * The volume of the soundboard sound, between 0 and 1 39 | * @type {Number} 40 | */ 41 | this.volume = data.volume; 42 | } 43 | if(data.emoji_id !== undefined) { 44 | /** 45 | * The ID of the relating custom emoji (will always be null for default soundboard sounds) 46 | * @type {String?} 47 | */ 48 | this.emojiID = data.emoji_id; 49 | } 50 | if(data.emoji_name !== undefined) { 51 | /** 52 | * The name of the relating default emoji 53 | * @type {String?} 54 | */ 55 | this.emojiName = data.emoji_name; 56 | } 57 | if(data.available !== undefined) { 58 | /** 59 | * Whether the soundboard sound is available or not (will always be true for default soundboard sounds) 60 | * @type {Boolean} 61 | */ 62 | this.available = data.available; 63 | } 64 | if(data.user !== undefined) { 65 | /** 66 | * The user that created the soundboard sound (not present for default soundboard sounds, or if the bot doesn't have either create/editGuildExpressions permissions) 67 | * @type {User?} 68 | */ 69 | this.user = this.#client.users.update(data.user, this.#client); 70 | } 71 | } 72 | 73 | /** 74 | * Delete the soundboard sound (not available for default soundboard sounds) 75 | * @param {String} [reason] The reason to be displayed in audit logs 76 | * @returns {Promise} 77 | */ 78 | delete(reason) { 79 | return this.#client.deleteGuildSoundboardSound.call(this.#client, this.guild.id, this.id, reason); 80 | } 81 | 82 | /** 83 | * Edit the soundboard sound (not available for default soundboard sounds) 84 | * @param {Object} options The properties to edit 85 | * @param {String?} [options.emojiID] The ID of the relating custom emoji (mutually exclusive with options.emojiName) 86 | * @param {String?} [options.emojiName] The name of the relating default emoji (mutually exclusive with options.emojiID) 87 | * @param {String} [options.name] The name of the soundboard sound (2-32 characters) 88 | * @param {Number?} [options.volume] The volume of the soundboard sound, between 0 and 1 89 | * @param {String} [options.reason] The reason to be displayed in audit logs 90 | * @returns {Promise} 91 | */ 92 | edit(options) { 93 | return this.#client.editGuildSoundboardSound.call(this.#client, this.guild.id, this.id, options); 94 | } 95 | 96 | /** 97 | * Send the soundboard sound to a connected voice channel 98 | * @param {String} channelID The ID of the connected voice channel 99 | * @returns {Promise} 100 | */ 101 | send(channelID) { 102 | return this.#client.sendSoundboardSound.call(this.#client, channelID, {soundID: this.id, sourceGuildID: this.guild.id}); 103 | } 104 | 105 | toJSON(props = []) { 106 | return super.toJSON([ 107 | "available", 108 | "emojiID", 109 | "emojiName", 110 | "guild", 111 | "name", 112 | "user", 113 | "volume", 114 | ...props 115 | ]); 116 | } 117 | } 118 | 119 | module.exports = SoundboardSound; 120 | -------------------------------------------------------------------------------- /lib/structures/StageChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TextVoiceChannel = require("./TextVoiceChannel"); 4 | 5 | /** 6 | * Represents a guild stage channel. 7 | * @extends TextVoiceChannel 8 | */ 9 | class StageChannel extends TextVoiceChannel { 10 | #client; 11 | constructor(data, client, messageLimit) { 12 | super(data, client, messageLimit); 13 | this.#client = client; 14 | } 15 | 16 | update(data, client) { 17 | super.update(data, client); 18 | if(data.topic !== undefined) { 19 | this.topic = data.topic; 20 | } 21 | } 22 | 23 | /** 24 | * Create a stage instance 25 | * @param {Object} options The stage instance options 26 | * @param {String} [options.guildScheduledEventID] The ID of the guild scheduled event associated with the stage instance 27 | * @param {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public (deprecated), 2 is guild only 28 | * @param {Boolean} [options.sendStartNotification] Whether to notify @everyone that a stage instance has started or not 29 | * @param {String} options.topic The stage instance topic 30 | * @returns {Promise} 31 | */ 32 | createInstance(options) { 33 | return this.#client.createStageInstance.call(this.#client, this.id, options); 34 | } 35 | 36 | /** 37 | * Delete the stage instance for this channel 38 | * @returns {Promise} 39 | */ 40 | deleteInstance() { 41 | return this.#client.deleteStageInstance.call(this.#client, this.id); 42 | } 43 | 44 | /** 45 | * Update the stage instance for this channel 46 | * @param {Object} options The properties to edit 47 | * @param {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only 48 | * @param {String} [options.topic] The stage instance topic 49 | * @returns {Promise} 50 | */ 51 | editInstance(options) { 52 | return this.#client.editStageInstance.call(this.#client, this.id, options); 53 | } 54 | 55 | /** 56 | * Get the stage instance for this channel 57 | * @returns {Promise} 58 | */ 59 | getInstance() { 60 | return this.#client.getStageInstance.call(this.#client, this.id); 61 | } 62 | } 63 | 64 | module.exports = StageChannel; 65 | -------------------------------------------------------------------------------- /lib/structures/StageInstance.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a stage instance 7 | */ 8 | class StageInstance extends Base { 9 | /** 10 | * The ID of the stage instance 11 | * @member {String} StageInstance#id 12 | */ 13 | #client; 14 | constructor(data, client) { 15 | super(data.id); 16 | this.#client = client; 17 | /** 18 | * The associated stage channel 19 | * @type {StageChannel} 20 | */ 21 | this.channel = client.getChannel(data.channel_id) || {id: data.channel_id}; 22 | /** 23 | * The guild of the associated stage channel 24 | * @type {Guild} 25 | */ 26 | this.guild = client.guilds.get(data.guild_id) || {id: data.guild_id}; 27 | /** 28 | * The event associated with this instance 29 | * @type {GuildScheduledEvent} 30 | */ 31 | this.guildScheduledEvent = this.guild.events?.get(data.guild_scheduled_event_id) || {id: data.guild_scheduled_event_id}; 32 | this.update(data); 33 | } 34 | 35 | update(data) { 36 | if(data.discoverable_disabled !== undefined) { 37 | /** 38 | * Whether or not stage discovery is disabled 39 | * @deprecated Deprecated in Discord's API 40 | * @type {Boolean} 41 | */ 42 | this.discoverableDisabled = data.discoverable_disabled; 43 | } 44 | if(data.privacy_level !== undefined) { 45 | /** 46 | * The privacy level of the stage instance. 1 is public (deprecated), 2 is guild only 47 | * @type {Number} 48 | */ 49 | this.privacyLevel = data.privacy_level; 50 | } 51 | if(data.topic !== undefined) { 52 | /** 53 | * The stage instance topic 54 | * @type {String} 55 | */ 56 | this.topic = data.topic; 57 | } 58 | } 59 | 60 | /** 61 | * Delete this stage instance 62 | * @returns {Promise} 63 | */ 64 | delete() { 65 | return this.#client.deleteStageInstance.call(this.#client, this.channel.id); 66 | } 67 | 68 | /** 69 | * Update this stage instance 70 | * @param {Object} options The properties to edit 71 | * @param {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public (deprecated), 2 is guild only 72 | * @param {String} [options.topic] The stage instance topic 73 | * @returns {Promise} 74 | */ 75 | edit(options) { 76 | return this.#client.editStageInstance.call(this.#client, this.channel.id, options); 77 | } 78 | 79 | toJSON(props = []) { 80 | return super.toJSON([ 81 | "channel", 82 | "guild", 83 | "guildScheduledEvent", 84 | "privacyLevel", 85 | "topic", 86 | ...props 87 | ]); 88 | } 89 | } 90 | 91 | module.exports = StageInstance; 92 | -------------------------------------------------------------------------------- /lib/structures/Subscription.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Base = require("./Base"); 3 | 4 | /** 5 | * Represents a subscription for one or more SKUs 6 | * @extends Base 7 | */ 8 | class Subscription extends Base { 9 | /** 10 | * The ID of the subscription 11 | * @member {String} Subscription#id 12 | */ 13 | 14 | constructor(data) { 15 | super(data.id); 16 | 17 | /** 18 | * The ID of the user who subscribed 19 | * @type {String} 20 | */ 21 | this.userID = data.user_id; 22 | 23 | /** 24 | * An array of SKU IDs that the user is subscribed to 25 | * @type {Array} 26 | */ 27 | this.skuIDs = data.sku_ids; 28 | 29 | /** 30 | * An array of entitlement IDs that this subscription grants 31 | * @type {Array} 32 | */ 33 | this.entitlementIDs = data.entitlement_ids; 34 | 35 | /** 36 | * The start timestamp of the current subscription period 37 | * @type {Number} 38 | */ 39 | this.currentPeriodStart = Date.parse(data.current_period_start); 40 | 41 | /** 42 | * The end timestamp of the current subscription period 43 | * @type {Number} 44 | */ 45 | this.currentPeriodEnd = Date.parse(data.current_period_end); 46 | 47 | /** 48 | * The status of the subscription 49 | * @type {Number} 50 | */ 51 | this.status = data.status; 52 | 53 | /** 54 | * A timestamp of when the subscription was canceled 55 | * @type {Number?} 56 | */ 57 | this.canceledAt = data.canceled_at != null ? Date.parse(data.canceled_at) : null; 58 | 59 | /** 60 | * An ISO 3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing if not queried with a private OAuth2 scope. 61 | * @type {String?} 62 | */ 63 | this.country = data.country; 64 | 65 | /** 66 | * An array of SKU IDs that the user will be subscribed to after renewal 67 | * @type {Array?} 68 | */ 69 | this.renewalSKUIDs = data.renewal_sku_ids; 70 | } 71 | 72 | toJSON(props = []) { 73 | return super.toJSON([ 74 | "userID", 75 | "skuIDs", 76 | "entitlementIDs", 77 | "currentPeriodStart", 78 | "currentPeriodEnd", 79 | "status", 80 | "canceledAt", 81 | "country", 82 | ...props 83 | ]); 84 | } 85 | } 86 | 87 | module.exports = Subscription; 88 | -------------------------------------------------------------------------------- /lib/structures/ThreadMember.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Member = require("./Member"); 5 | 6 | /** 7 | * Represents a thread member 8 | * @extends Base 9 | */ 10 | class ThreadMember extends Base { 11 | /** 12 | * The ID of the thread member 13 | * @member {String} ThreadMember#id 14 | */ 15 | #client; 16 | constructor(data, client) { 17 | super(data.user_id); 18 | this.#client = client; 19 | /** 20 | * The user-thread settings of this member 21 | * @type {Number} 22 | */ 23 | this.flags = data.flags; 24 | /** 25 | * The ID of the thread this member is a part of 26 | * @type {String} 27 | */ 28 | this.threadID = data.thread_id || data.id; // Thanks Discord 29 | /** 30 | * Timestamp of when the member joined the thread 31 | * @type {Number} 32 | */ 33 | this.joinTimestamp = Date.parse(data.join_timestamp); 34 | 35 | if(data.member !== undefined) { 36 | if(data.member.id === undefined) { 37 | data.member.id = this.id; 38 | } 39 | 40 | const guild = this.#client.guilds.get(this.#client.threadGuildMap[this.threadID]); 41 | /** 42 | * The guild member that this thread member belongs to 43 | * @type {Member?} 44 | */ 45 | this.guildMember = guild ? guild.members.update(data.member, guild) : new Member(data.member, guild, client); 46 | if(data.presence) { 47 | this.guildMember.update(data.presence); 48 | } 49 | } 50 | 51 | this.update(data); 52 | } 53 | 54 | update(data) { 55 | if(data.flags !== undefined) { 56 | this.flags = data.flags; 57 | } 58 | } 59 | 60 | /** 61 | * Remove the member from the thread 62 | * @returns {Promise} 63 | */ 64 | leave() { 65 | return this.#client.leaveThread.call(this.#client, this.threadID, this.id); 66 | } 67 | 68 | toJSON(props = []) { 69 | return super.toJSON([ 70 | "threadID", 71 | "joinTimestamp", 72 | ...props 73 | ]); 74 | } 75 | } 76 | 77 | module.exports = ThreadMember; 78 | -------------------------------------------------------------------------------- /lib/structures/UnavailableGuild.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a guild 7 | * @extends Base 8 | */ 9 | class UnavailableGuild extends Base { 10 | /** 11 | * The ID of the guild 12 | * @member {String} UnavailableGuild#id 13 | */ 14 | constructor(data, client) { 15 | super(data.id); 16 | /** 17 | * The shard that owns this guild 18 | * @type {Shard} 19 | */ 20 | this.shard = client.shards.get(client.guildShardMap[this.id]); 21 | /** 22 | * Whether the guild is unavailable or not 23 | * @type {Boolean} 24 | */ 25 | this.unavailable = !!data.unavailable; 26 | } 27 | 28 | toJSON(props = []) { 29 | return super.toJSON([ 30 | "unavailable", 31 | ...props 32 | ]); 33 | } 34 | } 35 | 36 | module.exports = UnavailableGuild; 37 | -------------------------------------------------------------------------------- /lib/structures/User.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Endpoints = require("../rest/Endpoints"); 5 | 6 | /** 7 | * Represents a user 8 | * @extends Base 9 | */ 10 | class User extends Base { 11 | /** 12 | * The ID of the user 13 | * @member {String} User#id 14 | */ 15 | /** 16 | * Timestamp of the user's creation 17 | * @member {Number} User#createdAt 18 | */ 19 | 20 | #client; 21 | #missingClientError; 22 | constructor(data, client) { 23 | super(data.id); 24 | if(!client) { 25 | this.#missingClientError = new Error("Missing client in constructor"); // Preserve constructor callstack 26 | } 27 | this.#client = client; 28 | /** 29 | * Whether the user is an OAuth bot or not 30 | * @type {Boolean} 31 | */ 32 | this.bot = !!data.bot; 33 | /** 34 | * Whether the user is an official Discord system user (e.g. urgent messages) 35 | * @type {Boolean} 36 | */ 37 | this.system = !!data.system; 38 | this.update(data); 39 | } 40 | 41 | update(data) { 42 | if(data.avatar !== undefined) { 43 | /** 44 | * The hash of the user's avatar, or null if no avatar 45 | * @type {String?} 46 | */ 47 | this.avatar = data.avatar; 48 | } 49 | if(data.avatar_decoration_data !== undefined) { 50 | /** 51 | * The data of the user's avatar decoration, or null if no avatar decoration 52 | * @type {Object?} 53 | */ 54 | this.avatarDecorationData = data.avatar_decoration_data; 55 | } 56 | if(data.username !== undefined) { 57 | /** 58 | * The username of the user 59 | * @type {String} 60 | */ 61 | this.username = data.username; 62 | } 63 | if(data.discriminator !== undefined) { 64 | /** 65 | * The discriminator of the user - if a single zero digit ("0"), the user is using the unique username system 66 | * @type {String} 67 | */ 68 | this.discriminator = data.discriminator; 69 | } 70 | if(data.public_flags !== undefined) { 71 | /** 72 | * Publicly visible flags for this user 73 | * @type {Number?} 74 | */ 75 | this.publicFlags = data.public_flags; 76 | } 77 | if(data.banner !== undefined) { 78 | /** 79 | * The hash of the user's banner, or null if no banner (REST only) 80 | * @type {String?} 81 | */ 82 | this.banner = data.banner; 83 | } 84 | if(data.accent_color !== undefined) { 85 | /** 86 | * The user's banner color, or null if no banner color (REST only) 87 | * @type {Number?} 88 | */ 89 | this.accentColor = data.accent_color; 90 | } 91 | if(data.global_name !== undefined) { 92 | /** 93 | * The globally visible display name of the user 94 | * @type {String?} 95 | */ 96 | this.globalName = data.global_name; 97 | } 98 | } 99 | 100 | /** 101 | * The URL of the user's avatar which can be either a JPG or GIF 102 | * @type {String} 103 | */ 104 | get avatarURL() { 105 | if(this.#missingClientError) { 106 | throw this.#missingClientError; 107 | } 108 | return this.avatar ? this.#client._formatImage(Endpoints.USER_AVATAR(this.id, this.avatar)) : this.defaultAvatarURL; 109 | } 110 | 111 | /** 112 | * The URL of the user's avatar decoration, which is usually an animated PNG 113 | * @type {String?} 114 | */ 115 | get avatarDecorationURL() { 116 | if(!this.avatarDecorationData) { 117 | return null; 118 | } 119 | const url = Endpoints.USER_AVATAR_DECORATION_PRESET(this.avatarDecorationData.asset); 120 | return `${Endpoints.CDN_URL}${url}.png`; 121 | } 122 | 123 | /** 124 | * The URL of the user's banner 125 | * @type {String?} 126 | */ 127 | get bannerURL() { 128 | if(!this.banner) { 129 | return null; 130 | } 131 | if(this.#missingClientError) { 132 | throw this.#missingClientError; 133 | } 134 | return this.#client._formatImage(Endpoints.BANNER(this.id, this.banner)); 135 | } 136 | 137 | /** 138 | * The hash for the default avatar of a user if there is no avatar set 139 | * @type {String} 140 | */ 141 | get defaultAvatar() { 142 | if(this.discriminator === "0") { 143 | return Base.getDiscordEpoch(this.id) % 6; 144 | } 145 | return this.discriminator % 5; 146 | } 147 | 148 | /** 149 | * The URL of the user's default avatar 150 | * @type {String} 151 | */ 152 | get defaultAvatarURL() { 153 | return `${Endpoints.CDN_URL}${Endpoints.DEFAULT_USER_AVATAR(this.defaultAvatar)}.png`; 154 | } 155 | 156 | /** 157 | * A string that mentions the user 158 | * @type {String} 159 | */ 160 | get mention() { 161 | return `<@${this.id}>`; 162 | } 163 | 164 | /** 165 | * The URL of the user's avatar (always a JPG) 166 | * @type {String} 167 | */ 168 | get staticAvatarURL() { 169 | if(this.#missingClientError) { 170 | throw this.#missingClientError; 171 | } 172 | return this.avatar ? this.#client._formatImage(Endpoints.USER_AVATAR(this.id, this.avatar), "jpg") : this.defaultAvatarURL; 173 | } 174 | 175 | /** 176 | * Get the user's avatar with the given format and size 177 | * @param {String} [format] The filetype of the avatar ("jpg", "jpeg", "png", "gif", or "webp") 178 | * @param {Number} [size] The size of the avatar (any power of two between 16 and 4096) 179 | * @returns {String} 180 | */ 181 | dynamicAvatarURL(format, size) { 182 | if(!this.avatar) { 183 | return this.defaultAvatarURL; 184 | } 185 | if(this.#missingClientError) { 186 | throw this.#missingClientError; 187 | } 188 | return this.#client._formatImage(Endpoints.USER_AVATAR(this.id, this.avatar), format, size); 189 | } 190 | 191 | /** 192 | * Get the user's banner with the given format and size 193 | * @param {String} [format] The filetype of the banner ("jpg", "jpeg", "png", "gif", or "webp") 194 | * @param {Number} [size] The size of the banner (any power of two between 16 and 4096) 195 | * @returns {String?} 196 | */ 197 | dynamicBannerURL(format, size) { 198 | if(!this.banner) { 199 | return null; 200 | } 201 | if(this.#missingClientError) { 202 | throw this.#missingClientError; 203 | } 204 | return this.#client._formatImage(Endpoints.BANNER(this.id, this.banner), format, size); 205 | } 206 | 207 | /** 208 | * Get a DM channel with the user, or create one if it does not exist 209 | * @returns {Promise} 210 | */ 211 | getDMChannel() { 212 | return this.#client.getDMChannel.call(this.#client, this.id); 213 | } 214 | 215 | toJSON(props = []) { 216 | return super.toJSON([ 217 | "accentColor", 218 | "avatar", 219 | "banner", 220 | "bot", 221 | "discriminator", 222 | "globalName", 223 | "publicFlags", 224 | "system", 225 | "username", 226 | ...props 227 | ]); 228 | } 229 | } 230 | 231 | module.exports = User; 232 | -------------------------------------------------------------------------------- /lib/structures/VoiceChannel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Collection = require("../util/Collection"); 4 | const GuildChannel = require("./GuildChannel"); 5 | const Member = require("./Member"); 6 | const PermissionOverwrite = require("./PermissionOverwrite"); 7 | 8 | /** 9 | * Represents a guild voice channel. See GuildChannel for more properties and methods. 10 | * @extends GuildChannel 11 | */ 12 | class VoiceChannel extends GuildChannel { 13 | #client; 14 | constructor(data, client) { 15 | super(data, client); 16 | this.#client = client; 17 | /** 18 | * Collection of Members in this channel 19 | * @type {Collection} 20 | */ 21 | this.voiceMembers = new Collection(Member); 22 | } 23 | 24 | update(data, client) { 25 | super.update(data, client); 26 | 27 | if(data.bitrate !== undefined) { 28 | /** 29 | * The bitrate of the channel 30 | * @type {Number?} 31 | */ 32 | this.bitrate = data.bitrate; 33 | } 34 | if(data.rtc_region !== undefined) { 35 | /** 36 | * The RTC region ID of the channel (automatic when `null`) 37 | * @type {String?} 38 | */ 39 | this.rtcRegion = data.rtc_region; 40 | } 41 | if(data.permission_overwrites) { 42 | /** 43 | * Collection of PermissionOverwrites in this channel 44 | * @type {Collection} 45 | */ 46 | this.permissionOverwrites = new Collection(PermissionOverwrite); 47 | data.permission_overwrites.forEach((overwrite) => { 48 | this.permissionOverwrites.add(overwrite); 49 | }); 50 | } 51 | if(data.position !== undefined) { 52 | /** 53 | * The position of the channel 54 | * @type {Number} 55 | */ 56 | this.position = data.position; 57 | } 58 | if(data.nsfw !== undefined) { 59 | /** 60 | * Whether the channel is an NSFW channel or not 61 | * @type {Boolean} 62 | */ 63 | this.nsfw = data.nsfw; 64 | } 65 | } 66 | 67 | /** 68 | * Create an invite for the channel 69 | * @param {Object} [options] Invite generation options 70 | * @param {Number} [options.maxAge] How long the invite should last in seconds 71 | * @param {Number} [options.maxUses] How many uses the invite should last for 72 | * @param {Boolean} [options.temporary] Whether the invite grants temporary membership or not 73 | * @param {Boolean} [options.unique] Whether the invite is unique or not 74 | * @param {String} [reason] The reason to be displayed in audit logs 75 | * @returns {Promise} 76 | */ 77 | createInvite(options, reason) { 78 | return this.#client.createChannelInvite.call(this.#client, this.id, options, reason); 79 | } 80 | 81 | /** 82 | * Get all invites in the channel 83 | * @returns {Promise>} 84 | */ 85 | getInvites() { 86 | return this.#client.getChannelInvites.call(this.#client, this.id); 87 | } 88 | 89 | /** 90 | * Joins the channel. 91 | * @param {Object} [options] VoiceConnection constructor options 92 | * @param {Object} [options.opusOnly] Skip opus encoder initialization. You should not enable this unless you know what you are doing 93 | * @param {Object} [options.shared] Whether the VoiceConnection will be part of a SharedStream or not 94 | * @param {Boolean} [options.selfMute] Whether the bot joins the channel muted or not 95 | * @param {Boolean} [options.selfDeaf] Whether the bot joins the channel deafened or not 96 | * @returns {Promise} Resolves with a VoiceConnection 97 | */ 98 | join(options) { 99 | return this.#client.joinVoiceChannel.call(this.#client, this.id, options); 100 | } 101 | 102 | /** 103 | * Leaves the channel. 104 | */ 105 | leave() { 106 | return this.#client.leaveVoiceChannel.call(this.#client, this.id); 107 | } 108 | 109 | /** 110 | * Send a soundboard sound to the voice channel 111 | * @param {Object} options The soundboard sound options 112 | * @param {String} options.soundID The ID of the soundboard sound 113 | * @param {String} [options.sourceGuildID] The ID of the guild where the soundboard sound was created, if not in the same guild 114 | * @returns {Promise} 115 | */ 116 | sendSoundboardSound(options) { 117 | return this.#client.sendSoundboardSound.call(this.#client, this.id, options); 118 | } 119 | 120 | toJSON(props = []) { 121 | return super.toJSON([ 122 | "bitrate", 123 | "permissionOverwrites", 124 | "position", 125 | "rtcRegion", 126 | "voiceMembers", 127 | ...props 128 | ]); 129 | } 130 | } 131 | 132 | module.exports = VoiceChannel; 133 | -------------------------------------------------------------------------------- /lib/structures/VoiceState.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a member's voice state in a guild 7 | * @extends Base 8 | */ 9 | class VoiceState extends Base { 10 | /** 11 | * The ID of the member 12 | * @member {String} VoiceState#id 13 | */ 14 | constructor(data) { 15 | super(data.id); 16 | /** 17 | * Whether the member is server muted or not 18 | * @type {Boolean} 19 | */ 20 | this.mute = false; 21 | /** 22 | * Whether the member is server deafened or not 23 | * @type {Boolean} 24 | */ 25 | this.deaf = false; 26 | /** 27 | * Timestamp of the member's latest request to speak 28 | * @type {Number?} 29 | */ 30 | this.requestToSpeakTimestamp = null; 31 | /** 32 | * Whether the member is self muted or not 33 | * @type {Boolean} 34 | */ 35 | this.selfMute = false; 36 | /** 37 | * Whether the member is self deafened or not 38 | * @type {Boolean} 39 | */ 40 | this.selfDeaf = false; 41 | /** 42 | * Whether the member is streaming using "Go Live" 43 | * @type {Boolean} 44 | */ 45 | this.selfStream = false; 46 | /** 47 | * Whether the member's camera is enabled 48 | * @type {Boolean} 49 | */ 50 | this.selfVideo = false; 51 | /** 52 | * Whether the member is suppressed or not 53 | * @type {Boolean} 54 | */ 55 | this.suppress = false; 56 | this.update(data); 57 | } 58 | 59 | update(data) { 60 | if(data.channel_id !== undefined) { 61 | /** 62 | * The ID of the member's current voice channel 63 | * @type {String?} 64 | */ 65 | this.channelID = data.channel_id; 66 | /** 67 | * The ID of the member's current voice session 68 | * @type {String?} 69 | */ 70 | this.sessionID = data.channel_id === null ? null : data.session_id; 71 | } else if(this.channelID === undefined) { 72 | this.channelID = this.sessionID = null; 73 | } 74 | if(data.mute !== undefined) { 75 | this.mute = data.mute; 76 | } 77 | if(data.deaf !== undefined) { 78 | this.deaf = data.deaf; 79 | } 80 | if(data.request_to_speak_timestamp !== undefined) { 81 | this.requestToSpeakTimestamp = data.request_to_speak_timestamp == null ? null : Date.parse(data.request_to_speak_timestamp); 82 | } 83 | if(data.self_mute !== undefined) { 84 | this.selfMute = data.self_mute; 85 | } 86 | if(data.self_deaf !== undefined) { 87 | this.selfDeaf = data.self_deaf; 88 | } 89 | if(data.self_video !== undefined) { 90 | this.selfVideo = data.self_video; 91 | } 92 | if(data.self_stream !== undefined) { 93 | this.selfStream = data.self_stream; 94 | } 95 | if(data.suppress !== undefined) { // Bots ignore this 96 | this.suppress = data.suppress; 97 | } 98 | } 99 | 100 | toJSON(props = []) { 101 | return super.toJSON([ 102 | "channelID", 103 | "deaf", 104 | "mute", 105 | "requestToSpeakTimestamp", 106 | "selfDeaf", 107 | "selfMute", 108 | "selfStream", 109 | "selfVideo", 110 | "sessionID", 111 | "suppress", 112 | ...props 113 | ]); 114 | } 115 | } 116 | 117 | module.exports = VoiceState; 118 | -------------------------------------------------------------------------------- /lib/util/BrowserWebSocket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("node:util"); 4 | const Base = require("../structures/Base"); 5 | 6 | let EventEmitter; 7 | try { 8 | EventEmitter = require("eventemitter3"); 9 | } catch{ 10 | EventEmitter = require("node:events").EventEmitter; 11 | } 12 | 13 | class BrowserWebSocketError extends Error { 14 | constructor(message, event) { 15 | super(message); 16 | this.event = event; 17 | } 18 | } 19 | 20 | /** 21 | * Represents a browser's websocket usable by Dysnomia 22 | * @extends EventEmitter 23 | */ 24 | class BrowserWebSocket extends EventEmitter { 25 | #ws; 26 | /** 27 | * Creates a browser web socket 28 | * @param {String} url The URL to connect to 29 | */ 30 | constructor(url) { 31 | super(); 32 | 33 | if(typeof window === "undefined") { 34 | throw new Error("BrowserWebSocket cannot be used outside of a browser environment"); 35 | } 36 | 37 | this.#ws = new window.WebSocket(url); 38 | this.#ws.onopen = () => this.emit("open"); 39 | this.#ws.onmessage = this.#onMessage.bind(this); 40 | this.#ws.onerror = (event) => this.emit("error", new BrowserWebSocketError("Unknown error", event)); 41 | this.#ws.onclose = (event) => this.emit("close", event.code, event.reason); 42 | } 43 | 44 | get readyState() { 45 | return this.#ws.readyState; 46 | } 47 | 48 | close(code, reason) { 49 | return this.#ws.close(code, reason); 50 | } 51 | 52 | removeEventListener(type, listener) { 53 | return this.removeListener(type, listener); 54 | } 55 | 56 | send(data) { 57 | return this.#ws.send(data); 58 | } 59 | 60 | terminate() { 61 | return this.#ws.close(); 62 | } 63 | 64 | async #onMessage(event) { 65 | if(event.data instanceof window.Blob) { 66 | this.emit("message", await event.data.arrayBuffer()); 67 | } else { 68 | this.emit("message", event.data); 69 | } 70 | } 71 | 72 | [util.inspect.custom]() { 73 | return Base.prototype[util.inspect.custom].call(this); 74 | } 75 | } 76 | 77 | BrowserWebSocket.CONNECTING = 0; 78 | BrowserWebSocket.OPEN = 1; 79 | BrowserWebSocket.CLOSING = 2; 80 | BrowserWebSocket.CLOSED = 3; 81 | 82 | module.exports = BrowserWebSocket; 83 | -------------------------------------------------------------------------------- /lib/util/Bucket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("node:util"); 4 | const Base = require("../structures/Base"); 5 | 6 | /** 7 | * Handle ratelimiting something 8 | */ 9 | class Bucket { 10 | #queue = []; 11 | /** 12 | * Timestamp of last token clearing 13 | * @type {Number} 14 | */ 15 | lastReset = 0; 16 | /** 17 | * Timestamp of last token consumption 18 | * @type {Number} 19 | */ 20 | lastSend = 0; 21 | /** 22 | * How many tokens the bucket has consumed in this interval 23 | * @type {Number} 24 | */ 25 | tokens = 0; 26 | /** 27 | * Construct a Bucket 28 | * @param {Number} tokenLimit The max number of tokens the bucket can consume per interval 29 | * @param {Number} interval How long (in ms) to wait between clearing used tokens 30 | * @param {Object} [options] Optional parameters 31 | * @param {Object} options.latencyRef A latency reference object 32 | * @param {Number} options.latencyRef.latency Interval between consuming tokens 33 | * @param {Number} options.reservedTokens How many tokens to reserve for priority operations 34 | */ 35 | constructor(tokenLimit, interval, options = {}) { 36 | /** 37 | * The max number tokens the bucket can consume per interval 38 | * @type {Number} 39 | */ 40 | this.tokenLimit = tokenLimit; 41 | /** 42 | * How long (in ms) to wait between clearing used tokens 43 | * @type {Number} 44 | */ 45 | this.interval = interval; 46 | this.latencyRef = options.latencyRef || {latency: 0}; 47 | this.reservedTokens = options.reservedTokens || 0; 48 | } 49 | 50 | check() { 51 | if(this.timeout || this.#queue.length === 0) { 52 | return; 53 | } 54 | if(this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency < Date.now()) { 55 | this.lastReset = Date.now(); 56 | this.tokens = Math.max(0, this.tokens - this.tokenLimit); 57 | } 58 | 59 | let val; 60 | let tokensAvailable = this.tokens < this.tokenLimit; 61 | let unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens); 62 | while(this.#queue.length > 0 && (unreservedTokensAvailable || (tokensAvailable && this.#queue[0].priority))) { 63 | this.tokens++; 64 | tokensAvailable = this.tokens < this.tokenLimit; 65 | unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens); 66 | const item = this.#queue.shift(); 67 | val = this.latencyRef.latency - Date.now() + this.lastSend; 68 | if(this.latencyRef.latency === 0 || val <= 0) { 69 | item.func(); 70 | this.lastSend = Date.now(); 71 | } else { 72 | setTimeout(() => { 73 | item.func(); 74 | }, val); 75 | this.lastSend = Date.now() + val; 76 | } 77 | } 78 | 79 | if(this.#queue.length > 0 && !this.timeout) { 80 | this.timeout = setTimeout(() => { 81 | this.timeout = null; 82 | this.check(); 83 | }, this.tokens < this.tokenLimit ? this.latencyRef.latency : Math.max(0, this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency - Date.now())); 84 | } 85 | } 86 | 87 | /** 88 | * Queue something in the Bucket 89 | * @param {Function} func A callback to call when a token can be consumed 90 | * @param {Boolean} [priority=false] Whether or not the callback should use reserved tokens 91 | */ 92 | queue(func, priority = false) { 93 | if(priority) { 94 | this.#queue.unshift({func, priority}); 95 | } else { 96 | this.#queue.push({func, priority}); 97 | } 98 | this.check(); 99 | } 100 | 101 | [util.inspect.custom]() { 102 | return Base.prototype[util.inspect.custom].call(this); 103 | } 104 | } 105 | 106 | module.exports = Bucket; 107 | -------------------------------------------------------------------------------- /lib/util/Collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Hold a bunch of something 5 | * @template Class 6 | * @extends Map 7 | */ 8 | class Collection extends Map { 9 | /** 10 | * Construct a Collection 11 | * @param {Class} baseObject The base class for all items 12 | * @param {Number} [limit] Max number of items to hold 13 | */ 14 | constructor(baseObject, limit) { 15 | super(); 16 | /** 17 | * The base class for all items 18 | * @type {Class} 19 | */ 20 | this.baseObject = baseObject; 21 | /** 22 | * Max number of items to hold 23 | * @type {Number?} 24 | */ 25 | this.limit = limit; 26 | } 27 | 28 | /** 29 | * Update an object 30 | * @param {Object} obj The updated object data 31 | * @param {String} obj.id The ID of the object 32 | * @param {Class} [extra] An extra parameter the constructor may need 33 | * @param {Boolean} [replace] Whether to replace an existing object with the same ID 34 | * @returns {Class} The updated object 35 | */ 36 | update(obj, extra, replace) { 37 | if(!obj.id && obj.id !== 0) { 38 | throw new Error("Missing object id"); 39 | } 40 | const item = this.get(obj.id); 41 | if(!item) { 42 | return this.add(obj, extra, replace); 43 | } 44 | item.update(obj, extra); 45 | return item; 46 | } 47 | 48 | /** 49 | * Add an object 50 | * @param {Object} obj The object data 51 | * @param {String} obj.id The ID of the object 52 | * @param {Class} [extra] An extra parameter the constructor may need 53 | * @param {Boolean} [replace] Whether to replace an existing object with the same ID 54 | * @returns {Class} The existing or newly created object 55 | */ 56 | add(obj, extra, replace) { 57 | if(this.limit === 0) { 58 | return (obj instanceof this.baseObject || obj.constructor.name === this.baseObject.name) ? obj : new this.baseObject(obj, extra); 59 | } 60 | if(obj.id == null) { 61 | throw new Error("Missing object id"); 62 | } 63 | const existing = this.get(obj.id); 64 | if(existing && !replace) { 65 | return existing; 66 | } 67 | if(!(obj instanceof this.baseObject || obj.constructor.name === this.baseObject.name)) { 68 | obj = new this.baseObject(obj, extra); 69 | } 70 | 71 | this.set(obj.id, obj); 72 | 73 | if(this.limit && this.size > this.limit) { 74 | const iter = this.keys(); 75 | while(this.size > this.limit) { 76 | this.delete(iter.next().value); 77 | } 78 | } 79 | return obj; 80 | } 81 | 82 | /** 83 | * Returns true if all elements satisfy the condition 84 | * @param {Function} func A function that takes an object and returns true or false 85 | * @returns {Boolean} Whether or not all elements satisfied the condition 86 | */ 87 | every(func) { 88 | for(const item of this.values()) { 89 | if(!func(item)) { 90 | return false; 91 | } 92 | } 93 | return true; 94 | } 95 | 96 | /** 97 | * Return all the objects that make the function evaluate true 98 | * @param {Function} func A function that takes an object and returns true if it matches 99 | * @returns {Array} An array containing all the objects that matched 100 | */ 101 | filter(func) { 102 | const arr = []; 103 | for(const item of this.values()) { 104 | if(func(item)) { 105 | arr.push(item); 106 | } 107 | } 108 | return arr; 109 | } 110 | 111 | /** 112 | * Return the first object to make the function evaluate true 113 | * @param {Function} func A function that takes an object and returns true if it matches 114 | * @returns {Class?} The first matching object, or undefined if no match 115 | */ 116 | find(func) { 117 | for(const item of this.values()) { 118 | if(func(item)) { 119 | return item; 120 | } 121 | } 122 | return undefined; 123 | } 124 | 125 | /** 126 | * Return an array with the results of applying the given function to each element 127 | * @param {Function} func A function that takes an object and returns something 128 | * @returns {Array} An array containing the results 129 | */ 130 | map(func) { 131 | const arr = []; 132 | for(const item of this.values()) { 133 | arr.push(func(item)); 134 | } 135 | return arr; 136 | } 137 | 138 | /** 139 | * Get a random object from the Collection 140 | * @returns {Class?} The random object, or undefined if there is no match 141 | */ 142 | random() { 143 | const index = Math.floor(Math.random() * this.size); 144 | const iter = this.values(); 145 | for(let i = 0; i < index; ++i) { 146 | iter.next(); 147 | } 148 | return iter.next().value; 149 | } 150 | 151 | /** 152 | * Returns a value resulting from applying a function to every element of the collection 153 | * @param {Function} func A function that takes the previous value and the next item and returns a new value 154 | * @param {any} [initialValue] The initial value passed to the function 155 | * @returns {any} The final result 156 | */ 157 | reduce(func, initialValue) { 158 | const iter = this.values(); 159 | let val; 160 | let result = initialValue === undefined ? iter.next().value : initialValue; 161 | while((val = iter.next().value) !== undefined) { 162 | result = func(result, val); 163 | } 164 | return result; 165 | } 166 | 167 | /** 168 | * Remove an object 169 | * @param {Object} obj The object 170 | * @param {String} obj.id The ID of the object 171 | * @returns {Class?} The removed object, or null if nothing was removed 172 | */ 173 | remove(obj) { 174 | const item = this.get(obj.id); 175 | if(!item) { 176 | return null; 177 | } 178 | this.delete(obj.id); 179 | return item; 180 | } 181 | 182 | /** 183 | * Returns true if at least one element satisfies the condition 184 | * @param {Function} func A function that takes an object and returns true or false 185 | * @returns {Boolean} Whether or not at least one element satisfied the condition 186 | */ 187 | some(func) { 188 | for(const item of this.values()) { 189 | if(func(item)) { 190 | return true; 191 | } 192 | } 193 | return false; 194 | } 195 | 196 | toString() { 197 | return `[Collection<${this.baseObject.name}>]`; 198 | } 199 | 200 | toJSON() { 201 | const json = {}; 202 | for(const item of this.values()) { 203 | json[item.id] = item; 204 | } 205 | return json; 206 | } 207 | } 208 | 209 | module.exports = Collection; 210 | -------------------------------------------------------------------------------- /lib/util/MultipartData.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class MultipartData { 4 | boundary = "----------------Dysnomia"; 5 | bufs = []; 6 | 7 | attach(fieldName, data, filename) { 8 | if(data === undefined) { 9 | return; 10 | } 11 | let str = "\r\n--" + this.boundary + "\r\nContent-Disposition: form-data; name=\"" + fieldName + "\""; 12 | let contentType; 13 | if(filename) { 14 | str += "; filename=\"" + filename + "\""; 15 | const extension = filename.match(/\.(png|apng|gif|jpg|jpeg|webp|svg|json)$/i); 16 | if(extension) { 17 | let ext = extension[1].toLowerCase(); 18 | switch(ext) { 19 | case "png": 20 | case "apng": 21 | case "gif": 22 | case "jpg": 23 | case "jpeg": 24 | case "webp": 25 | case "svg": { 26 | if(ext === "svg") { 27 | ext = "svg+xml"; 28 | } 29 | contentType = "image/"; 30 | break; 31 | } 32 | case "json": { 33 | contentType = "application/"; 34 | break; 35 | } 36 | } 37 | contentType += ext; 38 | } 39 | } 40 | 41 | if(contentType) { 42 | str += `\r\nContent-Type: ${contentType}`; 43 | } else if(ArrayBuffer.isView(data)) { 44 | str += "\r\nContent-Type: application/octet-stream"; 45 | if(!(data instanceof Uint8Array)) { 46 | data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); 47 | } 48 | } else if(typeof data === "object") { 49 | str += "\r\nContent-Type: application/json"; 50 | data = Buffer.from(JSON.stringify(data)); 51 | } else { 52 | data = Buffer.from("" + data); 53 | } 54 | this.bufs.push(Buffer.from(str + "\r\n\r\n")); 55 | this.bufs.push(data); 56 | } 57 | 58 | finish() { 59 | this.bufs.push(Buffer.from("\r\n--" + this.boundary + "--")); 60 | return this.bufs; 61 | } 62 | } 63 | 64 | module.exports = MultipartData; 65 | -------------------------------------------------------------------------------- /lib/util/Opus.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let NativeOpus; 4 | let OpusScript; 5 | 6 | module.exports.createOpus = function createOpus(samplingRate, channels, bitrate) { 7 | if(!NativeOpus && !OpusScript) { 8 | try { 9 | NativeOpus = require("@discordjs/opus"); 10 | } catch{ 11 | try { 12 | OpusScript = require("opusscript"); 13 | } catch{ // eslint-disable no-empty 14 | } 15 | } 16 | } 17 | 18 | let opus; 19 | if(NativeOpus) { 20 | opus = new NativeOpus.OpusEncoder(samplingRate, channels); 21 | } else if(OpusScript) { 22 | opus = new OpusScript(samplingRate, channels, OpusScript.Application.AUDIO); 23 | } else { 24 | throw new Error("No opus encoder found, playing non-opus audio will not work."); 25 | } 26 | 27 | if(opus.setBitrate) { 28 | opus.setBitrate(bitrate); 29 | } else if(opus.encoderCTL) { 30 | opus.encoderCTL(4002, bitrate); 31 | } 32 | 33 | return opus; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/util/SequentialBucket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("node:util"); 4 | const Base = require("../structures/Base"); 5 | 6 | /** 7 | * Ratelimit requests and release in sequence 8 | * TODO: add latencyref 9 | */ 10 | class SequentialBucket { 11 | #queue = []; 12 | /** 13 | * Whether the queue is being processed 14 | * @type {Boolean} 15 | */ 16 | processing = false; 17 | /** 18 | * Timestamp of next reset 19 | * @type {Number} 20 | */ 21 | reset = 0; 22 | /** 23 | * How many tokens the bucket has left in the current interval 24 | * @member {Number} SequentialBucket#remaining 25 | */ 26 | 27 | /** 28 | * Construct a SequentialBucket 29 | * @param {Number} limit The max number of tokens the bucket can consume per interval 30 | * @param {Object} [latencyRef] An object 31 | * @param {Number} latencyRef.latency Interval between consuming tokens 32 | */ 33 | constructor(limit, latencyRef = {latency: 0}) { 34 | /** 35 | * How many tokens the bucket can consume in the current interval 36 | * @type {Number} 37 | */ 38 | this.limit = this.remaining = limit; 39 | this.latencyRef = latencyRef; 40 | } 41 | 42 | check(override) { 43 | if(this.#queue.length === 0) { 44 | if(this.processing) { 45 | clearTimeout(this.processing); 46 | this.processing = false; 47 | } 48 | return; 49 | } 50 | if(this.processing && !override) { 51 | return; 52 | } 53 | const now = Date.now(); 54 | const offset = this.latencyRef.latency; 55 | if(!this.reset || this.reset < now - offset) { 56 | this.reset = now - offset; 57 | this.remaining = this.limit; 58 | } 59 | this.last = now; 60 | if(this.remaining <= 0) { 61 | this.processing = setTimeout(() => { 62 | this.processing = false; 63 | this.check(true); 64 | }, Math.max(0, (this.reset || 0) - now + offset) + 1); 65 | return; 66 | } 67 | --this.remaining; 68 | this.processing = true; 69 | this.#queue.shift()(() => { 70 | if(this.#queue.length > 0) { 71 | this.check(true); 72 | } else { 73 | this.processing = false; 74 | } 75 | }); 76 | } 77 | 78 | /** 79 | * Queue something in the SequentialBucket 80 | * @param {Function} func A function to call when a token can be consumed. The function will be passed a callback argument, which must be called to allow the bucket to continue to work 81 | */ 82 | queue(func, short) { 83 | if(short) { 84 | this.#queue.unshift(func); 85 | } else { 86 | this.#queue.push(func); 87 | } 88 | this.check(); 89 | } 90 | 91 | [util.inspect.custom]() { 92 | return Base.prototype[util.inspect.custom].call(this); 93 | } 94 | } 95 | 96 | module.exports = SequentialBucket; 97 | -------------------------------------------------------------------------------- /lib/util/emitDeprecation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const warningMessages = { 4 | CHANNEL_CLIENT: "Accessing the client reference via Channel#client is deprecated and is going to be removed in the next release. Please use your own client reference instead.", 5 | NITRO_STICKER_PACKS: "Client#getNitroStickerPacks is deprecated as built-in sticker packs are free for everyone. Please use Client#getStickerPacks instead.", 6 | INTERACTIONS_REQUIRE_PREMIUM: "Interaction#requirePremium is deprecated by Discord. Please use premium buttons instead.", 7 | PRIVATE_CHANNEL_RECIPIENT: "Accessing a private channel recipient via PrivateChannel#recipient is deprecated. Use PrivateChannel#recipients instead." 8 | }; 9 | const unknownCodeMessage = "You have triggered a deprecated behavior whose warning was implemented improperly. Please report this issue."; 10 | 11 | const emittedCodes = []; 12 | 13 | /** 14 | * @param {keyof typeof warningMessages} code 15 | */ 16 | module.exports = function emitDeprecation(code) { 17 | if(emittedCodes.includes(code)) { 18 | return; 19 | } 20 | emittedCodes.push(code); 21 | process.emitWarning(warningMessages[code] || unknownCodeMessage, "DeprecationWarning", `dysnomia:${code}`); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/voice/VoiceConnectionManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("../structures/Base"); 4 | const Collection = require("../util/Collection"); 5 | 6 | class VoiceConnectionManager extends Collection { 7 | pendingGuilds = {}; 8 | constructor(vcObject) { 9 | super(vcObject || require("./VoiceConnection")); 10 | } 11 | 12 | join(guildID, channelID, options) { 13 | const connection = this.get(guildID); 14 | if(connection?.ws) { 15 | connection.switchChannel(channelID); 16 | if(connection.ready) { 17 | return Promise.resolve(connection); 18 | } else { 19 | return new Promise((res, rej) => { 20 | const disconnectHandler = () => { 21 | connection.removeListener("ready", readyHandler); 22 | connection.removeListener("error", errorHandler); 23 | rej(new Error("Disconnected")); 24 | }; 25 | const readyHandler = () => { 26 | connection.removeListener("disconnect", disconnectHandler); 27 | connection.removeListener("error", errorHandler); 28 | res(connection); 29 | }; 30 | const errorHandler = (err) => { 31 | connection.removeListener("disconnect", disconnectHandler); 32 | connection.removeListener("ready", readyHandler); 33 | connection.disconnect(); 34 | rej(err); 35 | }; 36 | connection.once("ready", readyHandler).once("disconnect", disconnectHandler).once("error", errorHandler); 37 | }); 38 | } 39 | } 40 | return new Promise((res, rej) => { 41 | this.pendingGuilds[guildID] = { 42 | channelID: channelID, 43 | options: options || {}, 44 | res: res, 45 | rej: rej, 46 | timeout: setTimeout(() => { 47 | delete this.pendingGuilds[guildID]; 48 | rej(new Error("Voice connection timeout")); 49 | }, 10000) 50 | }; 51 | }); 52 | } 53 | 54 | leave(guildID) { 55 | const connection = this.get(guildID); 56 | if(!connection) { 57 | return; 58 | } 59 | connection.disconnect(); 60 | connection._destroy(); 61 | this.remove(connection); 62 | } 63 | 64 | switch(guildID, channelID) { 65 | const connection = this.get(guildID); 66 | connection?.switch(channelID); 67 | } 68 | 69 | voiceServerUpdate(data) { 70 | if(this.pendingGuilds[data.guild_id]?.timeout) { 71 | clearTimeout(this.pendingGuilds[data.guild_id].timeout); 72 | this.pendingGuilds[data.guild_id].timeout = null; 73 | } 74 | let connection = this.get(data.guild_id); 75 | if(!connection) { 76 | if(!this.pendingGuilds[data.guild_id]) { 77 | return; 78 | } 79 | connection = this.add(new this.baseObject(data.guild_id, { 80 | shard: data.shard, 81 | opusOnly: this.pendingGuilds[data.guild_id].options.opusOnly, 82 | shared: this.pendingGuilds[data.guild_id].options.shared 83 | })); 84 | } 85 | connection.connect({ 86 | channel_id: (this.pendingGuilds[data.guild_id] || connection).channelID, 87 | endpoint: data.endpoint, 88 | token: data.token, 89 | session_id: data.session_id, 90 | user_id: data.user_id 91 | }); 92 | if(!this.pendingGuilds[data.guild_id] || this.pendingGuilds[data.guild_id].waiting) { 93 | return; 94 | } 95 | this.pendingGuilds[data.guild_id].waiting = true; 96 | const disconnectHandler = () => { 97 | connection = this.get(data.guild_id); 98 | if(connection) { 99 | connection.removeListener("ready", readyHandler); 100 | connection.removeListener("error", errorHandler); 101 | } 102 | if(this.pendingGuilds[data.guild_id]) { 103 | this.pendingGuilds[data.guild_id].rej(new Error("Disconnected")); 104 | delete this.pendingGuilds[data.guild_id]; 105 | } 106 | }; 107 | const readyHandler = () => { 108 | connection = this.get(data.guild_id); 109 | if(connection) { 110 | connection.removeListener("disconnect", disconnectHandler); 111 | connection.removeListener("error", errorHandler); 112 | } 113 | if(this.pendingGuilds[data.guild_id]) { 114 | this.pendingGuilds[data.guild_id].res(connection); 115 | delete this.pendingGuilds[data.guild_id]; 116 | } 117 | }; 118 | const errorHandler = (err) => { 119 | connection = this.get(data.guild_id); 120 | if(connection) { 121 | connection.removeListener("disconnect", disconnectHandler); 122 | connection.removeListener("ready", readyHandler); 123 | connection.disconnect(); 124 | } 125 | if(this.pendingGuilds[data.guild_id]) { 126 | this.pendingGuilds[data.guild_id].rej(err); 127 | delete this.pendingGuilds[data.guild_id]; 128 | } 129 | }; 130 | connection.once("ready", readyHandler).once("disconnect", disconnectHandler).once("error", errorHandler); 131 | } 132 | 133 | toString() { 134 | return "[VoiceConnectionManager]"; 135 | } 136 | 137 | toJSON(props = []) { 138 | return Base.prototype.toJSON.call(this, [ 139 | "pendingGuilds", 140 | ...props 141 | ]); 142 | } 143 | } 144 | 145 | module.exports = VoiceConnectionManager; 146 | -------------------------------------------------------------------------------- /lib/voice/VoiceDataStream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let EventEmitter; 4 | try { 5 | EventEmitter = require("eventemitter3"); 6 | } catch{ 7 | EventEmitter = require("node:events").EventEmitter; 8 | } 9 | 10 | /** 11 | * Represents a voice data stream 12 | * @extends EventEmitter 13 | */ 14 | class VoiceDataStream extends EventEmitter { 15 | constructor(type) { 16 | super(); 17 | /** 18 | * The targeted voice data type for the stream, either "opus" or "pcm" 19 | * @type {String} 20 | */ 21 | this.type = type; 22 | } 23 | } 24 | 25 | module.exports = VoiceDataStream; 26 | -------------------------------------------------------------------------------- /lib/voice/streams/BaseTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("node:util"); 4 | const Base = require("../../structures/Base"); 5 | const TransformStream = require("node:stream").Transform; 6 | 7 | class BaseTransformer extends TransformStream { 8 | #transformCB; 9 | manualCB = false; 10 | constructor(options = {}) { 11 | options.allowHalfOpen ??= true; 12 | options.highWaterMark ??= 0; 13 | super(options); 14 | } 15 | 16 | setTransformCB(cb) { 17 | if(this.manualCB) { 18 | this.transformCB(); 19 | this.#transformCB = cb; 20 | } else { 21 | cb(); 22 | } 23 | } 24 | 25 | transformCB() { 26 | if(this.#transformCB) { 27 | this.#transformCB(); 28 | this.#transformCB = null; 29 | } 30 | } 31 | 32 | [util.inspect.custom]() { 33 | return Base.prototype[util.inspect.custom].call(this); 34 | } 35 | } 36 | 37 | module.exports = BaseTransformer; 38 | -------------------------------------------------------------------------------- /lib/voice/streams/DCAOpusTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseTransformer = require("./BaseTransformer"); 4 | 5 | class DCAOpusTransformer extends BaseTransformer { 6 | #remainder = null; 7 | 8 | process(buffer) { 9 | if(buffer.length - buffer._index < 2) { 10 | return true; 11 | } 12 | 13 | const opusLen = buffer.readInt16LE(buffer._index); 14 | buffer._index += 2; 15 | 16 | if(buffer.length - buffer._index < opusLen) { 17 | return true; 18 | } 19 | 20 | buffer._index += opusLen; 21 | this.push(buffer.subarray(buffer._index - opusLen, buffer._index)); 22 | } 23 | 24 | _transform(chunk, enc, cb) { 25 | if(this.#remainder) { 26 | chunk = Buffer.concat([this.#remainder, chunk]); 27 | this.#remainder = null; 28 | } 29 | 30 | if(!this.head) { 31 | if(chunk.length < 4) { 32 | this.#remainder = chunk; 33 | return cb(); 34 | } else { 35 | const dcaVersion = chunk.subarray(0, 4); 36 | if(dcaVersion[0] !== 68 || dcaVersion[1] !== 67 || dcaVersion[2] !== 65) { // DCA0 or invalid 37 | this.head = true; // Attempt to play as if it were a DCA0 file 38 | } else if(dcaVersion[3] === 49) { // DCA1 39 | if(chunk.length < 8) { 40 | this.#remainder = chunk; 41 | return cb(); 42 | } 43 | const jsonLength = chunk.subarray(4, 8).readInt32LE(0); 44 | if(chunk.length < 8 + jsonLength) { 45 | this.#remainder = chunk; 46 | return cb(); 47 | } 48 | const jsonMetadata = chunk.subarray(8, 8 + jsonLength); 49 | this.emit("debug", jsonMetadata); 50 | chunk = chunk.subarray(8 + jsonLength); 51 | this.head = true; 52 | } else { 53 | this.emit("error", new Error("Unsupported DCA version: " + dcaVersion.toString())); 54 | } 55 | } 56 | } 57 | 58 | chunk._index = 0; 59 | 60 | while(chunk._index < chunk.length) { 61 | const offset = chunk._index; 62 | const ret = this.process(chunk); 63 | if(ret) { 64 | this.#remainder = chunk.subarray(offset); 65 | cb(); 66 | return; 67 | } 68 | } 69 | 70 | this.setTransformCB(cb); 71 | } 72 | } 73 | 74 | module.exports = DCAOpusTransformer; 75 | -------------------------------------------------------------------------------- /lib/voice/streams/FFmpegDuplex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const util = require("node:util"); 3 | const Base = require("../../structures/Base"); 4 | const ChildProcess = require("node:child_process"); 5 | const DuplexStream = require("node:stream").Duplex; 6 | const PassThroughStream = require("node:stream").PassThrough; 7 | 8 | const delegateEvents = { 9 | readable: "_reader", 10 | data: "_reader", 11 | end: "_reader", 12 | drain: "_writer", 13 | finish: "_writer" 14 | }; 15 | 16 | class FFmpegDuplex extends DuplexStream { 17 | #onError; 18 | #process; 19 | #stderr; 20 | #stdin; 21 | #stdout; 22 | constructor(command, options = {}) { 23 | options.highWaterMark ??= 0; 24 | super(options); 25 | 26 | this.command = command; 27 | this._reader = new PassThroughStream(options); 28 | this._writer = new PassThroughStream(options); 29 | 30 | this.#onError = this.emit.bind(this, "error"); 31 | 32 | this._reader.on("error", this.#onError); 33 | this._writer.on("error", this.#onError); 34 | 35 | this._readableState = this._reader._readableState; 36 | this._writableState = this._writer._writableState; 37 | 38 | ["on", "once", "removeListener", "removeListeners", "listeners"].forEach((method) => { 39 | const og = DuplexStream.prototype[method]; 40 | 41 | this[method] = function(ev, fn) { 42 | const substream = delegateEvents[ev]; 43 | if(substream) { 44 | return this[substream][method](ev, fn); 45 | } else { 46 | return og.call(this, ev, fn); 47 | } 48 | }; 49 | }); 50 | } 51 | 52 | destroy() { 53 | } 54 | 55 | end(chunk, enc, cb) { 56 | return this._writer.end(chunk, enc, cb); 57 | } 58 | 59 | kill() { 60 | } 61 | 62 | noop() { 63 | } 64 | 65 | pipe(dest, opts) { 66 | return this._reader.pipe(dest, opts); 67 | } 68 | 69 | read(size) { 70 | return this._reader.read(size); 71 | } 72 | 73 | setEncoding(enc) { 74 | return this._reader.setEncoding(enc); 75 | } 76 | 77 | spawn(args, options = {}) { 78 | let ex, exited, killed, ended; 79 | let stderr = []; 80 | 81 | const onStdoutEnd = () => { 82 | if(exited && !ended) { 83 | ended = true; 84 | this._reader.end(); 85 | setImmediate(this.emit.bind(this, "close")); 86 | } 87 | }; 88 | 89 | const onStderrData = (chunk) => { 90 | stderr.push(chunk); 91 | }; 92 | 93 | const cleanup = () => { 94 | this.#process 95 | = this.#stderr 96 | = this.#stdout 97 | = this.#stdin 98 | = stderr 99 | = ex 100 | = killed = null; 101 | 102 | this.kill 103 | = this.destroy = this.noop; 104 | }; 105 | 106 | const onExit = (code, signal) => { 107 | if(exited) { 108 | return; 109 | } 110 | exited = true; 111 | 112 | if(killed) { 113 | if(ex) { 114 | this.emit("error", ex); 115 | } 116 | this.emit("close"); 117 | } else if(code === 0 && signal == null) { 118 | // All is well 119 | onStdoutEnd(); 120 | } else { 121 | // Everything else 122 | ex = new Error("Command failed: " + Buffer.concat(stderr).toString("utf8")); 123 | ex.killed = this.#process.killed || killed; 124 | ex.code = code; 125 | ex.signal = signal; 126 | this.emit("error", ex); 127 | this.emit("close"); 128 | } 129 | 130 | cleanup(); 131 | }; 132 | 133 | const onError = (err) => { 134 | ex = err; 135 | this.#stdout.destroy(); 136 | this.#stderr.destroy(); 137 | onExit(); 138 | }; 139 | 140 | const kill = () => { 141 | if(killed) { 142 | return; 143 | } 144 | this.#stdout.destroy(); 145 | this.#stderr.destroy(); 146 | 147 | killed = true; 148 | 149 | try { 150 | this.#process.kill(options.killSignal || "SIGTERM"); 151 | setTimeout(() => this.#process && this.#process.kill("SIGKILL"), 2000); 152 | } catch(e) { 153 | ex = e; 154 | onExit(); 155 | } 156 | }; 157 | 158 | this.#process = ChildProcess.spawn(this.command, args, options); 159 | this.#stdin = this.#process.stdin; 160 | this.#stdout = this.#process.stdout; 161 | this.#stderr = this.#process.stderr; 162 | this._writer.pipe(this.#stdin); 163 | this.#stdout.pipe(this._reader, { 164 | end: false 165 | }); 166 | this.kill = this.destroy = kill; 167 | 168 | this.#stderr.on("data", onStderrData); 169 | 170 | // In some cases ECONNRESET can be emitted by stdin because the process is not interested in any 171 | // more data but the _writer is still piping. Forget about errors emitted on stdin and stdout 172 | this.#stdin.on("error", this.noop); 173 | this.#stdout.on("error", this.noop); 174 | 175 | this.#stdout.on("end", onStdoutEnd); 176 | 177 | this.#process.once("close", onExit); 178 | this.#process.once("error", onError); 179 | 180 | return this; 181 | } 182 | 183 | unpipe(dest) { 184 | return this._reader.unpipe(dest) || this.kill(); 185 | } 186 | 187 | write(chunk, enc, cb) { 188 | return this._writer.write(chunk, enc, cb); 189 | } 190 | 191 | [util.inspect.custom]() { 192 | return Base.prototype[util.inspect.custom].call(this); 193 | } 194 | } 195 | 196 | FFmpegDuplex.prototype.addListener = FFmpegDuplex.prototype.on; 197 | 198 | FFmpegDuplex.spawn = function(connection, args, options) { 199 | return new FFmpegDuplex(connection, options).spawn(args, options); 200 | }; 201 | 202 | module.exports = FFmpegDuplex; 203 | -------------------------------------------------------------------------------- /lib/voice/streams/FFmpegOggTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const FFmpegDuplex = require("./FFmpegDuplex"); 4 | 5 | module.exports = function(options = {}) { 6 | if(!options.command) { 7 | throw new Error("Invalid converter command"); 8 | } 9 | options.frameDuration ??= 60; 10 | let inputArgs = [ 11 | "-analyzeduration", "0", 12 | "-loglevel", "24" 13 | ].concat(options.inputArgs || []); 14 | if(options.format === "pcm") { 15 | inputArgs = inputArgs.concat( 16 | "-f", "s16le", 17 | "-ar", "48000", 18 | "-ac", "2" 19 | ); 20 | } 21 | inputArgs = inputArgs.concat( 22 | "-i", options.input || "-", 23 | "-vn" 24 | ); 25 | const outputArgs = [ 26 | "-c:a", "libopus", 27 | "-vbr", "on", 28 | "-frame_duration", "" + options.frameDuration, 29 | "-f", "ogg", 30 | "-" 31 | ]; 32 | return FFmpegDuplex.spawn(options.command, inputArgs.concat(options.encoderArgs || [], outputArgs)); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/voice/streams/FFmpegPCMTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const FFmpegDuplex = require("./FFmpegDuplex"); 4 | 5 | module.exports = function(options = {}) { 6 | if(!options.command) { 7 | throw new Error("Invalid converter command"); 8 | } 9 | options.samplingRate ??= 48_000; 10 | const inputArgs = [ 11 | "-analyzeduration", "0", 12 | "-loglevel", "24" 13 | ].concat(options.inputArgs || [], 14 | "-i", options.input || "-", 15 | "-vn" 16 | ); 17 | const outputArgs = [ 18 | "-f", "s16le", 19 | "-ar", "" + options.samplingRate, 20 | "-ac", "2", 21 | "-" 22 | ]; 23 | return FFmpegDuplex.spawn(options.command, inputArgs.concat(options.encoderArgs || [], outputArgs)); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/voice/streams/OggOpusTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseTransformer = require("./BaseTransformer"); 4 | 5 | class OggOpusTransformer extends BaseTransformer { 6 | #bitstream = null; 7 | #remainder = null; 8 | 9 | process(buffer) { 10 | if(buffer.length - buffer._index <= 26) { 11 | return true; 12 | } 13 | 14 | if(buffer.toString("utf8", buffer._index, buffer._index + 4) !== "OggS") { 15 | return new Error("Invalid OGG magic string: " + buffer.toString("utf8", buffer._index, buffer._index + 4)); 16 | } 17 | 18 | const typeFlag = buffer.readUInt8(buffer._index + 5); 19 | if(typeFlag === 1) { 20 | return new Error("OGG continued page not supported"); 21 | } 22 | 23 | const bitstream = buffer.readUInt32BE(buffer._index + 14); 24 | 25 | buffer._index += 26; 26 | 27 | const segmentCount = buffer.readUInt8(buffer._index); 28 | if(buffer.length - buffer._index - 1 < segmentCount) { 29 | return true; 30 | } 31 | 32 | const segments = []; 33 | let size = 0; 34 | let byte = 0; 35 | let total = 0; 36 | let i = 0; 37 | for(; i < segmentCount; i++) { 38 | byte = buffer.readUInt8(++buffer._index); 39 | if(byte < 255) { 40 | segments.push(size + byte); 41 | size = 0; 42 | } else { 43 | size += byte; 44 | } 45 | total += byte; 46 | } 47 | 48 | ++buffer._index; 49 | 50 | if(buffer.length - buffer._index < total) { 51 | return true; 52 | } 53 | 54 | for(let segment of segments) { 55 | buffer._index += segment; 56 | byte = (segment = buffer.subarray(buffer._index - segment, buffer._index)).toString("utf8", 0, 8); 57 | if(this.head) { 58 | if(byte === "OpusTags") { 59 | this.emit("debug", segment.toString()); 60 | } else if(bitstream === this.#bitstream) { 61 | this.push(segment); 62 | } 63 | } else if(byte === "OpusHead") { 64 | this.#bitstream = bitstream; 65 | this.emit("debug", (this.head = segment.toString())); 66 | } else { 67 | this.emit("debug", "Invalid codec: " + byte); 68 | } 69 | } 70 | } 71 | 72 | _final() { 73 | if(!this.#bitstream) { 74 | this.emit("error", new Error("No Opus stream was found")); 75 | } 76 | } 77 | 78 | _transform(chunk, enc, cb) { 79 | if(this.#remainder) { 80 | chunk = Buffer.concat([this.#remainder, chunk]); 81 | this.#remainder = null; 82 | } 83 | 84 | chunk._index = 0; 85 | 86 | while(chunk._index < chunk.length) { 87 | const offset = chunk._index; 88 | const ret = this.process(chunk); 89 | if(ret) { 90 | this.#remainder = chunk.subarray(offset); 91 | if(ret instanceof Error) { 92 | this.emit("error", ret); 93 | } 94 | cb(); 95 | return; 96 | } 97 | } 98 | 99 | this.setTransformCB(cb); 100 | } 101 | } 102 | 103 | module.exports = OggOpusTransformer; 104 | -------------------------------------------------------------------------------- /lib/voice/streams/PCMOpusTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseTransformer = require("./BaseTransformer"); 4 | 5 | class PCMOpusTransformer extends BaseTransformer { 6 | #remainder = null; 7 | constructor(options = {}) { 8 | super(options); 9 | 10 | this.opus = options.opusFactory(); 11 | this.frameSize = options.frameSize || 2880; 12 | this.pcmSize = options.pcmSize || 11520; 13 | } 14 | 15 | _destroy(...args) { 16 | this.opus.delete?.(); 17 | 18 | return super._destroy(...args); 19 | } 20 | 21 | _flush(cb) { 22 | if(this.#remainder) { 23 | const buf = Buffer.allocUnsafe(this.pcmSize); 24 | this.#remainder.copy(buf); 25 | buf.fill(0, this.#remainder.length); 26 | this.push(this.opus.encode(buf, this.frameSize)); 27 | this.#remainder = null; 28 | } 29 | cb(); 30 | } 31 | 32 | _transform(chunk, enc, cb) { 33 | if(this.#remainder) { 34 | chunk = Buffer.concat([this.#remainder, chunk]); 35 | this.#remainder = null; 36 | } 37 | 38 | if(chunk.length < this.pcmSize) { 39 | this.#remainder = chunk; 40 | return cb(); 41 | } 42 | 43 | chunk._index = 0; 44 | 45 | while(chunk._index + this.pcmSize < chunk.length) { 46 | chunk._index += this.pcmSize; 47 | this.push(this.opus.encode(chunk.subarray(chunk._index - this.pcmSize, chunk._index), this.frameSize)); 48 | } 49 | 50 | if(chunk._index < chunk.length) { 51 | this.#remainder = chunk.subarray(chunk._index); 52 | } 53 | 54 | this.setTransformCB(cb); 55 | } 56 | } 57 | 58 | module.exports = PCMOpusTransformer; 59 | -------------------------------------------------------------------------------- /lib/voice/streams/VolumeTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseTransformer = require("./BaseTransformer"); 4 | 5 | class VolumeTransformer extends BaseTransformer { 6 | #remainder = null; 7 | constructor(options = {}) { 8 | super(options); 9 | 10 | this.setVolume(1.0); 11 | } 12 | 13 | setVolume(volume) { 14 | if(isNaN(volume) || (volume = +volume) < 0) { 15 | throw new Error("Invalid volume level: " + volume); 16 | } 17 | this.volume = volume; 18 | this.db = 10 * Math.log(1 + this.volume) / 6.931471805599453; 19 | } 20 | 21 | _transform(chunk, enc, cb) { 22 | if(this.#remainder) { 23 | chunk = Buffer.concat([this.#remainder, chunk]); 24 | this.#remainder = null; 25 | } 26 | 27 | if(chunk.length < 2) { 28 | return cb(); 29 | } 30 | 31 | let buf; 32 | if(chunk.length & 1) { 33 | this.#remainder = chunk.subarray(chunk.length - 1); 34 | buf = Buffer.allocUnsafe(chunk.length - 1); 35 | } else { 36 | buf = Buffer.allocUnsafe(chunk.length); 37 | } 38 | 39 | for(let i = 0, num; i < buf.length - 1; i += 2) { 40 | // Bind transformed chunk to to 16 bit 41 | num = ~~(this.db * chunk.readInt16LE(i)); 42 | buf.writeInt16LE(num >= 32767 ? 32767 : num <= -32767 ? -32767 : num, i); 43 | } 44 | 45 | this.push(buf); 46 | this.setTransformCB(cb); 47 | } 48 | } 49 | 50 | module.exports = VolumeTransformer; 51 | -------------------------------------------------------------------------------- /lib/voice/streams/WebmOpusTransformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseTransformer = require("./BaseTransformer"); 4 | 5 | // EBML VInt max value is (2 ^ 56 - 2), but JS only supports 2^53 6 | // 45 = 53 - 8 - check before last 8 bytes 7 | const MAX_SHIFTED_VINT = Math.pow(2, 45); 8 | 9 | const STATE_CONTENT = 0; 10 | const STATE_TAG = 1; 11 | 12 | const TAG_TYPE_END = 0; 13 | const TAG_TYPE_START = 1; 14 | const TAG_TYPE_TAG = 2; 15 | 16 | const TRACKTYPE_AUDIO = 2; // EBML spec: https://www.matroska.org/technical/specs/index.html#TrackType 17 | 18 | class WebmOpusTransformer extends BaseTransformer { 19 | #remainder; 20 | #state = STATE_TAG; 21 | #tag_stack = []; 22 | #total = 0; 23 | 24 | getVIntLength(buffer, index) { 25 | let length = 1; 26 | for(; length <= 8; ++length) { 27 | if(buffer[index] & (1 << (8 - length))) { 28 | break; 29 | } 30 | } 31 | if(length > 8) { 32 | this.emit("debug", new Error(`VInt length ${length} | ${buffer.toString("hex", index, index + length)}`)); 33 | return null; 34 | } 35 | if(index + length > buffer.length) { 36 | return null; 37 | } 38 | return length; 39 | } 40 | 41 | process(type, info) { 42 | if(type === TAG_TYPE_TAG) { 43 | if(info.name === "SimpleBlock" && (info.data.readUInt8(0) & 0xF) === this.firstAudioTrack.TrackNumber) { 44 | this.push(info.data.subarray(4)); 45 | return; 46 | } 47 | if(info.name === "CodecPrivate") { 48 | const head = info.data.toString("utf8", 0, 8); 49 | if(head !== "OpusHead") { 50 | this.emit("error", new Error("Invalid codec: " + head)); 51 | return; 52 | } 53 | 54 | this.codecData = { 55 | version: info.data.readUInt8(8), 56 | channelCount: info.data.readUInt8(9), 57 | preSkip: info.data.readUInt16LE(10), 58 | inputSampleRate: info.data.readUInt32LE(12), 59 | outputGain: info.data.readUInt16LE(16), 60 | mappingFamily: info.data.readUInt8(18) 61 | }; 62 | return; 63 | } 64 | } 65 | 66 | if(!this.firstAudioTrack) { 67 | if(info.name === "TrackEntry") { 68 | if(type === TAG_TYPE_START) { 69 | this.parsingTrack = {}; 70 | } else if(type === TAG_TYPE_END) { 71 | if(this.parsingTrack.TrackNumber && this.parsingTrack.TrackType === TRACKTYPE_AUDIO) { 72 | this.firstAudioTrack = this.parsingTrack; 73 | } 74 | delete this.parsingTrack; 75 | } 76 | return; 77 | } 78 | if(this.parsingTrack) { 79 | if(info.name === "TrackNumber") { 80 | this.parsingTrack.TrackNumber = info.data[0]; 81 | return; 82 | } 83 | if(info.name === "TrackType") { 84 | this.parsingTrack.TrackType = info.data[0]; 85 | return; 86 | } 87 | } 88 | if(type === TAG_TYPE_END && info.name === "Tracks") { 89 | this.emit("error", new Error("No audio track")); 90 | return; 91 | } 92 | return; 93 | } 94 | } 95 | 96 | readContent(buffer) { 97 | const tagObj = this.#tag_stack[this.#tag_stack.length - 1]; 98 | 99 | if(tagObj.type === "m") { 100 | this.process(TAG_TYPE_START, tagObj); 101 | this.#state = STATE_TAG; 102 | return true; 103 | } 104 | 105 | if(buffer.length < buffer._index + tagObj.size) { 106 | return false; 107 | } 108 | 109 | tagObj.data = buffer.subarray(buffer._index, buffer._index + tagObj.size); 110 | buffer._index += tagObj.size; 111 | this.#total += tagObj.size; 112 | this.#state = STATE_TAG; 113 | 114 | this.#tag_stack.pop(); 115 | 116 | this.process(TAG_TYPE_TAG, tagObj); 117 | 118 | while(this.#tag_stack.length > 0) { 119 | if(this.#total < this.#tag_stack[this.#tag_stack.length - 1].end) { 120 | break; 121 | } 122 | this.process(TAG_TYPE_END, this.#tag_stack.pop()); 123 | } 124 | 125 | return true; 126 | } 127 | 128 | readTag(buffer) { 129 | const tagSize = this.getVIntLength(buffer, buffer._index); 130 | if(tagSize === null) { 131 | return false; 132 | } 133 | 134 | const size = this.getVIntLength(buffer, buffer._index + tagSize); 135 | if(size === null) { 136 | return false; 137 | } 138 | 139 | const tagStr = buffer.toString("hex", buffer._index, buffer._index + tagSize); 140 | 141 | const tagObj = { 142 | type: "unknown", 143 | name: "unknown", 144 | end: this.#total + tagSize 145 | }; 146 | if(schema[tagStr]) { 147 | tagObj.type = schema[tagStr].type; 148 | tagObj.name = schema[tagStr].name; 149 | } 150 | 151 | buffer._index += tagSize; 152 | 153 | let value = buffer[buffer._index] & (1 << (8 - size)) - 1; 154 | for(let i = 1; i < size; ++i) { 155 | if(i === 7 && value >= MAX_SHIFTED_VINT && buffer[buffer._index + 7] > 0) { 156 | tagObj.end = -1; // Special livestreaming int 0x1FFFFFFFFFFFFFF 157 | break; 158 | } 159 | value = (value << 8) + buffer[buffer._index + i]; 160 | } 161 | if(tagObj.end !== -1) { 162 | tagObj.end += value + size; 163 | } 164 | tagObj.size = value; 165 | 166 | buffer._index += size; 167 | this.#total += tagSize + size; 168 | this.#state = STATE_CONTENT; 169 | 170 | this.#tag_stack.push(tagObj); 171 | 172 | return true; 173 | } 174 | 175 | _transform(chunk, enc, cb) { 176 | if(this.#remainder) { 177 | chunk = Buffer.concat([this.#remainder, chunk]); 178 | this.#remainder = null; 179 | } 180 | 181 | chunk._index = 0; 182 | 183 | while(chunk._index < chunk.length) { 184 | if(this.#state === STATE_TAG && !this.readTag(chunk)) { 185 | break; 186 | } 187 | if(this.#state === STATE_CONTENT && !this.readContent(chunk)) { 188 | break; 189 | } 190 | } 191 | 192 | if(chunk._index < chunk.length) { 193 | this.#remainder = chunk.subarray(chunk._index); 194 | } 195 | 196 | this.setTransformCB(cb); 197 | } 198 | } 199 | 200 | module.exports = WebmOpusTransformer; 201 | 202 | const schema = { 203 | "ae": { 204 | name: "TrackEntry", 205 | type: "m" 206 | }, 207 | "d7": { 208 | name: "TrackNumber", 209 | type: "u" 210 | }, 211 | "86": { 212 | name: "CodecID", 213 | type: "s" 214 | }, 215 | "83": { 216 | name: "TrackType", 217 | type: "u" 218 | }, 219 | "1654ae6b": { 220 | name: "Tracks", 221 | type: "m" 222 | }, 223 | "63a2": { 224 | name: "CodecPrivate", 225 | type: "b" 226 | }, 227 | "a3": { 228 | name: "SimpleBlock", 229 | type: "b" 230 | }, 231 | "1a45dfa3": { 232 | name: "EBML", 233 | type: "m" 234 | }, 235 | "18538067": { 236 | name: "Segment", 237 | type: "m" 238 | }, 239 | "114d9b74": { 240 | name: "SeekHead", 241 | type: "m" 242 | }, 243 | "1549a966": { 244 | name: "Info", 245 | type: "m" 246 | }, 247 | "e1": { 248 | name: "Audio", 249 | type: "m" 250 | }, 251 | "1f43b675": { 252 | name: "Cluster", 253 | type: "m" 254 | } 255 | }; 256 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@projectdysnomia/dysnomia", 3 | "version": "0.2.3", 4 | "description": "A fork of Eris focused on keeping up with the latest Discord API changes.", 5 | "main": "./index.js", 6 | "exports": { 7 | ".": [ 8 | { 9 | "require": "./index.js", 10 | "import": "./esm.mjs" 11 | }, 12 | "./index.js" 13 | ], 14 | "./*": "./*", 15 | "./esm": "./esm.mjs" 16 | }, 17 | "typings": "./index.d.ts", 18 | "engines": { 19 | "node": ">=18.0.0" 20 | }, 21 | "scripts": { 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix && echo \"\u001b[1m\u001b[32mOK\u001b[39m\u001b[22m\" || echo \"\u001b[1m\u001b[31mNot OK\u001b[39m\u001b[22m\"" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/projectdysnomia/dysnomia.git" 28 | }, 29 | "keywords": [ 30 | "api", 31 | "discord", 32 | "discordapp", 33 | "dysnomia", 34 | "eris", 35 | "wrapper" 36 | ], 37 | "author": "Project Dysnomia Contributors", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/projectdysnomia/dysnomia/issues" 41 | }, 42 | "dependencies": { 43 | "ws": "^8.18.0" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "^9.15.0", 47 | "@stylistic/eslint-plugin": "^2.10.1", 48 | "@types/node": "^18.19.64", 49 | "@types/ws": "^8.5.13", 50 | "eslint": "^9.15.0", 51 | "eslint-plugin-jsdoc": "^50.5.0", 52 | "eslint-plugin-sort-class-members": "^1.21.0", 53 | "globals": "^15.12.0", 54 | "typescript": "^5.6.3", 55 | "typescript-eslint": "^8.14.0" 56 | }, 57 | "optionalDependencies": { 58 | "@stablelib/xchacha20poly1305": "~1.0.1", 59 | "opusscript": "^0.1.1" 60 | }, 61 | "browser": { 62 | "@discordjs/opus": false, 63 | "child_process": false, 64 | "dgram": false, 65 | "dns": false, 66 | "fs": false, 67 | "tls": false, 68 | "ws": false 69 | }, 70 | "peerDependencies": { 71 | "@discordjs/opus": "^0.9.0", 72 | "erlpack": "github:discord/erlpack", 73 | "eventemitter3": "^5.0.1", 74 | "pako": "^2.1.0", 75 | "sodium-native": "^4.1.1", 76 | "zlib-sync": "^0.1.9" 77 | }, 78 | "peerDependenciesMeta": { 79 | "@discordjs/opus": { 80 | "optional": true 81 | }, 82 | "eventemitter3": { 83 | "optional": true 84 | }, 85 | "erlpack": { 86 | "optional": true 87 | }, 88 | "pako": { 89 | "optional": true 90 | }, 91 | "sodium-native": { 92 | "optional": true 93 | }, 94 | "zlib-sync": { 95 | "optional": true 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "strictNullChecks": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "moduleResolution": "node", 15 | "lib": ["es6"] 16 | }, 17 | "files": ["index.d.ts"] 18 | } 19 | --------------------------------------------------------------------------------