From f9ec41c9b6ceeed545f8e1a1ff804b6755c4e633 Mon Sep 17 00:00:00 2001 From: March 7th <71698422+aiko-chan-ai@users.noreply.github.com> Date: Sat, 21 May 2022 20:58:33 +0700 Subject: [PATCH] Calling support (1) --- package-lock.json | 83 +++++++++++++++++++++++++ package.json | 1 + src/client/Client.js | 14 ++++- src/client/actions/VoiceStateUpdate.js | 20 ++++++ src/client/voice/ClientVoiceManager.js | 11 +++- src/client/websocket/handlers/READY.js | 37 ++++++++++- src/structures/DMChannel.js | 60 +++++++++++++++--- src/structures/PartialGroupDMChannel.js | 60 +++++++++++++++--- src/structures/VoiceState.js | 46 +++++++++----- src/util/Options.js | 2 + typings/index.d.ts | 8 +++ 11 files changed, 303 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba39813..549f72c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@discordjs/builders": "^0.13.0", "@discordjs/collection": "^0.6.0", + "@discordjs/voice": "^0.9.0", "@sapphire/async-queue": "^1.3.1", "@sapphire/snowflake": "^3.2.2", "@types/node-fetch": "^2.6.1", @@ -1665,6 +1666,27 @@ "decamelize": "^1.2.0" } }, + "node_modules/@discordjs/voice": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.9.0.tgz", + "integrity": "sha512-fSAYtPCEfIyG56hC2cRJuyfvQGr+aawSssLCqYg70vZ51dKO4spEKOB8V6vNMP5HnEplbhmxkB3YbshFKtnCQQ==", + "dependencies": { + "@types/ws": "^8.5.3", + "discord-api-types": "^0.29.0", + "prism-media": "^1.3.2", + "tiny-typed-emitter": "^2.1.0", + "tslib": "^2.3.1", + "ws": "^8.5.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/voice/node_modules/discord-api-types": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.29.0.tgz", + "integrity": "sha512-Ekq1ICNpOTVajXKZguNFrsDeTmam+ZeA38txsNLZnANdXUjU6QBPIZLUQTC6MzigFGb0Tt8vk4xLnXmzv0shNg==" + }, "node_modules/@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -12067,6 +12089,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prism-media": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz", + "integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==", + "peerDependencies": { + "@discordjs/opus": "^0.5.0", + "ffmpeg-static": "^4.2.7 || ^3.0.0 || ^2.4.0", + "node-opus": "^0.3.3", + "opusscript": "^0.0.8" + }, + "peerDependenciesMeta": { + "@discordjs/opus": { + "optional": true + }, + "ffmpeg-static": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + } + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -13562,6 +13609,11 @@ "readable-stream": "3" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -15910,6 +15962,26 @@ } } }, + "@discordjs/voice": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.9.0.tgz", + "integrity": "sha512-fSAYtPCEfIyG56hC2cRJuyfvQGr+aawSssLCqYg70vZ51dKO4spEKOB8V6vNMP5HnEplbhmxkB3YbshFKtnCQQ==", + "requires": { + "@types/ws": "^8.5.3", + "discord-api-types": "^0.29.0", + "prism-media": "^1.3.2", + "tiny-typed-emitter": "^2.1.0", + "tslib": "^2.3.1", + "ws": "^8.5.0" + }, + "dependencies": { + "discord-api-types": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.29.0.tgz", + "integrity": "sha512-Ekq1ICNpOTVajXKZguNFrsDeTmam+ZeA38txsNLZnANdXUjU6QBPIZLUQTC6MzigFGb0Tt8vk4xLnXmzv0shNg==" + } + } + }, "@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -23716,6 +23788,12 @@ } } }, + "prism-media": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz", + "integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==", + "requires": {} + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -24856,6 +24934,11 @@ "readable-stream": "3" } }, + "tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", diff --git a/package.json b/package.json index 1cb554b..3b4cabb 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dependencies": { "@discordjs/builders": "^0.13.0", "@discordjs/collection": "^0.6.0", + "@discordjs/voice": "^0.9.0", "@sapphire/async-queue": "^1.3.1", "@sapphire/snowflake": "^3.2.2", "@types/node-fetch": "^2.6.1", diff --git a/src/client/Client.js b/src/client/Client.js index 966a7b4..8d364b9 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -3,6 +3,7 @@ const process = require('node:process'); const { setInterval, setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); +const { getVoiceConnection } = require('@discordjs/voice'); const RichPresence = require('discord-rpc-contructor'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); @@ -15,6 +16,7 @@ const ClientUserSettingManager = require('../managers/ClientUserSettingManager') const GuildManager = require('../managers/GuildManager'); const RelationshipsManager = require('../managers/RelationshipsManager'); const UserManager = require('../managers/UserManager'); +const VoiceStateManager = require('../managers/VoiceStateManager'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const ClientPresence = require('../structures/ClientPresence'); const GuildPreview = require('../structures/GuildPreview'); @@ -113,6 +115,8 @@ class Client extends BaseClient { */ this.voice = new ClientVoiceManager(this); + this.voiceStates = new VoiceStateManager({ client: this }); + /** * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} @@ -248,6 +252,15 @@ class Client extends BaseClient { return this.readyAt ? Date.now() - this.readyAt : null; } + /** + * Get connection to current call + * @type {?VoiceConnection} + * @readonly + */ + get callVoice() { + return getVoiceConnection(null); + } + /** * Update Cloudflare Cookie and Discord Fingerprint */ @@ -335,7 +348,6 @@ class Client extends BaseClient { * client.QRLogin(); */ QRLogin(debug = false) { - console.log(this.options); const QR = new DiscordAuthWebsocket(this, debug); this.emit(Events.DEBUG, `Preparing to connect to the gateway`, QR); return QR; diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index f750e04..19131ae 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -29,6 +29,26 @@ class VoiceStateUpdate extends Action { client.voice.onVoiceStateUpdate(data); } + /** + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update + */ + client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); + } else { + // Update the state + const oldState = + client.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState({ client }, { user_id: data.user_id }); + + const newState = client.voiceStates._add(data); + + // Emit event + if (data.user_id === client.user.id) { + client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); + client.voice.onVoiceStateUpdate(data); + } + /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index e6ea6ea..af23bae 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -24,19 +24,26 @@ class ClientVoiceManager { client.on(Events.SHARD_DISCONNECT, (_, shardId) => { for (const [guildId, adapter] of this.adapters.entries()) { if (client.guilds.cache.get(guildId)?.shardId === shardId) { - adapter.destroy(); + // Because it has 1 shard => adapter.destroy(); } + adapter.destroy(); } }); } onVoiceServer(payload) { - this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload); + if (payload.guild_id) { + this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload); + } else { + this.adapters.get(payload.channel_id)?.onVoiceServerUpdate(payload); + } } onVoiceStateUpdate(payload) { if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) { this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload); + } else if (payload.channel_id && payload.session_id && payload.user_id === this.client.user?.id) { + this.adapters.get(payload.channel_id)?.onVoiceStateUpdate(payload); } } } diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index ad7a1fd..93ac063 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -1,10 +1,12 @@ 'use strict'; let ClientUser; +const { VoiceConnection, VoiceConnectionStatus } = require('@discordjs/voice'); const axios = require('axios'); const chalk = require('chalk'); const Discord = require('../../../index'); const { Events, Opcodes } = require('../../../util/Constants'); +const { Networking } = require('../../../util/Voice'); async function checkUpdate() { const res_ = await axios.get(`https://registry.npmjs.com/${encodeURIComponent('discord.js-selfbot-v13')}`); @@ -29,9 +31,42 @@ module.exports = (client, { d: data }, shard) => { try { checkUpdate(); } catch (e) { - console.log(e); + console.log(`${chalk.redBright('[Fail]')} Check Update error:`, e.message); } } + + if (client.options.patchVoice) { + /* eslint-disable */ + VoiceConnection.prototype.configureNetworking = function () { + const { server, state } = this.packets; + if (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return; + const networking = new Networking( + { + endpoint: server.endpoint, + serverId: server.guild_id ?? server.channel_id, + token: server.token, + sessionId: state.session_id, + userId: state.user_id, + }, + Boolean(this.debug), + ); + networking.once('close', this.onNetworkingClose); + networking.on('stateChange', this.onNetworkingStateChange); + networking.on('error', this.onNetworkingError); + networking.on('debug', this.onNetworkingDebug); + this.state = { + ...this.state, + status: VoiceConnectionStatus.Connecting, + networking, + }; + }; + client.emit( + Events.DEBUG, + `${chalk.greenBright('[OK]')} Patched VoiceConnection.prototype.configureNetworking [@discordjs/voice]`, + ); + /* eslint-enable */ + } + client.session_id = data.session_id; if (client.user) { client.user._patch(data.user); diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 7466332..96952fa 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,8 +1,10 @@ 'use strict'; +const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice'); const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageManager = require('../managers/MessageManager'); +const { Status } = require('../util/Constants'); /** * Represents a direct message channel between two users. @@ -97,18 +99,58 @@ class DMChannel extends Channel { // Testing feature: Call // URL: https://discord.com/api/v9/channels/DMchannelId/call/ring /** - * Call this DMChannel. [TEST only] - * @returns {Promise} + * Call this DMChannel. Return discordjs/voice VoiceConnection + * @param {Object} options Options for the call (selfDeaf, selfMute: Boolean) + * @returns {Promise} */ - call() { - console.log('TEST only, and not working !'); - return this.client.api.channels(this.id).call.ring.post({ - usingApplicationJson: true, - data: { - recipients: null, - }, + call(options = {}) { + return new Promise((resolve, reject) => { + this.client.api.channels(this.id).call.ring.post({ + usingApplicationJson: true, + data: { + recipients: null, + }, + }); + const connection = joinVoiceChannel({ + channelId: this.id, + guildId: null, + adapterCreator: this.voiceAdapterCreator, + selfDeaf: options.selfDeaf ?? false, + selfMute: options.selfMute ?? false, + }); + entersState(connection, VoiceConnectionStatus.Ready, 30000) + .then(connection => { + resolve(connection); + }) + .catch(err => { + connection.destroy(); + reject(err); + }); }); } + get shard() { + return this.client.ws.shards.first(); + } + get voiceAdapterCreator() { + return methods => { + this.client.voice.adapters.set(this.id, methods); + return { + sendPayload: data => { + data.d = { + ...data.d, + self_video: false, + }; + if (this.shard.status !== Status.READY) return false; + console.log('DM channel send payload', data); + this.shard.send(data); + return true; + }, + destroy: () => { + this.client.voice.adapters.delete(this.id); + }, + }; + }; + } } TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js index a20bb9d..4d931e4 100644 --- a/src/structures/PartialGroupDMChannel.js +++ b/src/structures/PartialGroupDMChannel.js @@ -1,12 +1,14 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice'); const { Channel } = require('./Channel'); const Invite = require('./Invite'); const User = require('./User'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Error } = require('../errors'); const MessageManager = require('../managers/MessageManager'); +const { Status } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); /** @@ -204,18 +206,58 @@ class PartialGroupDMChannel extends Channel { // Testing feature: Call // URL: https://discord.com/api/v9/channels/DMchannelId/call/ring /** - * Call this DMChannel. [TEST only] - * @returns {Promise} + * Call this Group DMChannel. Return discordjs/voice VoiceConnection + * @param {Object} options Options for the call (selfDeaf, selfMute: Boolean) + * @returns {Promise} */ - call() { - console.log('TEST only, and not working !'); - return this.client.api.channels(this.id).call.ring.post({ - usingApplicationJson: true, - data: { - recipients: null, - }, + call(options = {}) { + return new Promise((resolve, reject) => { + this.client.api.channels(this.id).call.ring.post({ + usingApplicationJson: true, + data: { + recipients: null, + }, + }); + const connection = joinVoiceChannel({ + channelId: this.id, + guildId: null, + adapterCreator: this.voiceAdapterCreator, + selfDeaf: options.selfDeaf ?? false, + selfMute: options.selfMute ?? false, + }); + entersState(connection, VoiceConnectionStatus.Ready, 30000) + .then(connection => { + resolve(connection); + }) + .catch(err => { + connection.destroy(); + reject(err); + }); }); } + get shard() { + return this.client.ws.shards.first(); + } + get voiceAdapterCreator() { + return methods => { + this.client.voice.adapters.set(this.id, methods); + return { + sendPayload: data => { + data.d = { + ...data.d, + self_video: false, + }; + if (this.shard.status !== Status.READY) return false; + console.log('DM channel send payload', data); + this.shard.send(data); + return true; + }, + destroy: () => { + this.client.voice.adapters.delete(this.id); + }, + }; + }; + } } TextBasedChannel.applyToClass(PartialGroupDMChannel, false); diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index d2185d5..7916aaa 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -132,6 +132,7 @@ class VoiceState extends Base { * @readonly */ get member() { + if (!this.guild?.id) return null; return this.guild.members.cache.get(this.id) ?? null; } @@ -141,6 +142,7 @@ class VoiceState extends Base { * @readonly */ get channel() { + if (!this.guild?.id) return null; return this.guild.channels.cache.get(this.channelId) ?? null; } @@ -169,6 +171,7 @@ class VoiceState extends Base { * @returns {Promise} */ setMute(mute = true, reason) { + if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { mute }, reason); } @@ -179,6 +182,7 @@ class VoiceState extends Base { * @returns {Promise} */ setDeaf(deaf = true, reason) { + if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { deaf }, reason); } @@ -188,6 +192,7 @@ class VoiceState extends Base { * @returns {Promise} */ disconnect(reason) { + if (!this.guild?.id) return this.callVoice?.disconnect(); return this.setChannel(null, reason); } @@ -199,6 +204,7 @@ class VoiceState extends Base { * @returns {Promise} */ setChannel(channel, reason) { + if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { channel }, reason); } @@ -215,16 +221,18 @@ class VoiceState extends Base { * @returns {Promise} */ async setRequestToSpeak(request = true) { - if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); + if (this.guild?.id) { + if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); - if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN'); + if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN'); - await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({ - data: { - channel_id: this.channelId, - request_to_speak_timestamp: request ? new Date().toISOString() : null, - }, - }); + await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({ + data: { + channel_id: this.channelId, + request_to_speak_timestamp: request ? new Date().toISOString() : null, + }, + }); + } } /** @@ -245,18 +253,20 @@ class VoiceState extends Base { * @returns {Promise} */ async setSuppressed(suppressed = true) { - if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); + if (this.guild?.id) { + if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); - if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); + if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); - const target = this.client.user.id === this.id ? '@me' : this.id; + const target = this.client.user.id === this.id ? '@me' : this.id; - await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({ - data: { - channel_id: this.channelId, - suppress: suppressed, - }, - }); + await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({ + data: { + channel_id: this.channelId, + suppress: suppressed, + }, + }); + } } /** @@ -264,6 +274,8 @@ class VoiceState extends Base { * @returns {string} URL Image of the user's streaming video */ async getPreview() { + if (!this.guild?.id) return null; + if (!this.streaming) throw new Error('USER_NOT_STREAMING'); // URL: https://discord.com/api/v9/streams/guild:guildid:voicechannelid:userid/preview const data = await this.client.api.streams[ diff --git a/src/util/Options.js b/src/util/Options.js index adca9d3..39390cb 100644 --- a/src/util/Options.js +++ b/src/util/Options.js @@ -37,6 +37,7 @@ const JSONBig = require('json-bigint'); * @property {boolean} [checkUpdate=true] Check for module updates at startup * @property {boolean} [readyStatus=true] Sync state with Discord Client * @property {boolean} [autoCookie=true] Automatically add Cookies to Request on startup + * @property {boolean} [patchVoice=true] Automatically patch @discordjs/voice module (support for call) * @property {number} [shardCount=1] The total amount of shards used by all processes of this bot * (e.g. recommended shard count, shard count of the ShardingManager) * @property {CacheFactory} [makeCache] Function to create a cache. @@ -140,6 +141,7 @@ class Options extends null { checkUpdate: true, readyStatus: true, autoCookie: true, + patchVoice: true, waitGuildTimeout: 15_000, shardCount: 1, makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings), diff --git a/typings/index.d.ts b/typings/index.d.ts index e907868..e96b189 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -625,6 +625,7 @@ export class Client extends BaseClient { public generateInvite(options?: InviteGenerationOptions): string; public login(token?: string): Promise; public QRLogin(debug?: boolean): DiscordAuthWebsocket; + public readonly callVoice: VoiceConnection | undefined; public isReady(): this is Client; /** @deprecated Use {@link Sweepers#sweepMessages} instead */ public sweepMessages(lifetime?: number): number; @@ -940,6 +941,9 @@ export class DMChannel extends TextBasedChannelMixin(Channel, ['bulkDelete']) { public recipient: User; public type: 'DM'; public fetch(force?: boolean): Promise; + public readonly voiceAdapterCreator: InternalDiscordGatewayAdapterCreator; + public call(options?: object): Promise; + public readonly shard: WebSocketShard; } export class Emoji extends Base { @@ -2068,6 +2072,9 @@ export class PartialGroupDMChannel extends TextBasedChannelMixin(Channel, ['bulk public getInvite(): Promise; public fetchInvite(force: boolean): Promise; public removeInvite(invite: Invite): Promise; + public readonly voiceAdapterCreator: InternalDiscordGatewayAdapterCreator; + public call(options?: object): Promise; + public readonly shard: WebSocketShard; } export class PermissionOverwrites extends Base { @@ -4208,6 +4215,7 @@ export interface ClientOptions { checkUpdate?: boolean; readyStatus?: boolean; autoCookie?: boolean; + patchVoice?: boolean; } // end copy