From f6c307844f32e3f25fb4b1499d64e024255689eb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 5 Dec 2020 14:26:43 +0530 Subject: [PATCH] feat(voice): add Gateway#updateVoiceState, VoiceChannel#join, VoiceChannel#leave --- src/gateway/handlers/voiceStateUpdate.ts | 1 - src/gateway/index.ts | 28 +++++++++++++ src/models/rest.ts | 51 +++++++++++++++++++----- src/structures/guildVoiceChannel.ts | 47 ++++++++++++++++++++++ src/structures/voiceState.ts | 3 ++ src/test/cmd.ts | 15 ++----- src/test/cmds/eval.ts | 21 ++++++++++ src/test/cmds/join.ts | 17 ++++++++ src/test/cmds/leave.ts | 19 +++++++++ 9 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 src/test/cmds/eval.ts create mode 100644 src/test/cmds/join.ts create mode 100644 src/test/cmds/leave.ts diff --git a/src/gateway/handlers/voiceStateUpdate.ts b/src/gateway/handlers/voiceStateUpdate.ts index 3e016d3..8a537b5 100644 --- a/src/gateway/handlers/voiceStateUpdate.ts +++ b/src/gateway/handlers/voiceStateUpdate.ts @@ -8,7 +8,6 @@ export const voiceStateUpdate: GatewayEventHandler = async ( gateway: Gateway, d: VoiceStatePayload ) => { - // TODO(DjDeveloperr): Support self-bot here; they can be in DMs (Call) if (d.guild_id === undefined) return const guild = ((await gateway.client.guilds.get( d.guild_id diff --git a/src/gateway/index.ts b/src/gateway/index.ts index f6f24d8..6311ee9 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -16,6 +16,8 @@ import { gatewayHandlers } from './handlers/index.ts' import { GATEWAY_BOT } from '../types/endpoint.ts' import { GatewayCache } from '../managers/gatewayCache.ts' import { delay } from '../utils/delay.ts' +import { VoiceChannel } from '../structures/guildVoiceChannel.ts' +import { Guild } from '../structures/guild.ts' export interface RequestMembersOptions { limit?: number @@ -24,6 +26,11 @@ export interface RequestMembersOptions { users?: string[] } +export interface VoiceStateOptions { + mute?: boolean + deaf?: boolean +} + export const RECONNECT_REASON = 'harmony-reconnect' /** @@ -308,6 +315,27 @@ class Gateway { return nonce } + updateVoiceState( + guild: Guild | string, + channel?: VoiceChannel | string, + voiceOptions: VoiceStateOptions = {} + ): void { + this.send({ + op: GatewayOpcodes.VOICE_STATE_UPDATE, + d: { + guild_id: typeof guild === 'string' ? guild : guild.id, + channel_id: + channel === undefined + ? null + : typeof channel === 'string' + ? channel + : channel?.id, + self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute, + self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf + } + }) + } + debug(msg: string): void { this.client.debug('Gateway', msg) } diff --git a/src/models/rest.ts b/src/models/rest.ts index 6a57321..093472b 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -244,7 +244,8 @@ export class RESTManager { private async handleStatusCode( response: Response, body: any, - data: { [key: string]: any } + data: { [key: string]: any }, + reject: CallableFunction ): Promise { const status = response.status @@ -261,18 +262,48 @@ export class RESTManager { if (text === 'undefined') text = undefined if (status === HttpResponseCode.Unauthorized) - throw new DiscordAPIError( - `Request was not successful (Unauthorized). Invalid Token.\n${text}` + reject( + new DiscordAPIError( + `Request was not successful (Unauthorized). Invalid Token.\n${text}` + ) ) // At this point we know it is error - let error = { + const error: { [name: string]: any } = { url: response.url, status, method: data.method, - body: data.body + code: body?.code, + message: body?.message, + errors: Object.fromEntries( + Object.entries( + body?.errors as { + [name: string]: { + _errors: Array<{ code: string; message: string }> + } + } + ).map((entry) => { + return [entry[0], entry[1]._errors] + }) + ) } - if (body !== undefined) error = Object.assign(error, body) + + // if (typeof error.errors === 'object') { + // const errors = error.errors as { + // [name: string]: { _errors: Array<{ code: string; message: string }> } + // } + // console.log(`%cREST Error:`, 'color: #F14C39;') + // Object.entries(errors).forEach((entry) => { + // console.log(` %c${entry[0]}:`, 'color: #12BC79;') + // entry[1]._errors.forEach((e) => { + // console.log( + // ` %c${e.code}: %c${e.message}`, + // 'color: skyblue;', + // 'color: #CECECE;' + // ) + // }) + // }) + // } if ( [ @@ -282,10 +313,10 @@ export class RESTManager { HttpResponseCode.MethodNotAllowed ].includes(status) ) { - throw new DiscordAPIError(Deno.inspect(error)) + reject(new DiscordAPIError(Deno.inspect(error))) } else if (status === HttpResponseCode.GatewayUnavailable) { - throw new DiscordAPIError(Deno.inspect(error)) - } else throw new DiscordAPIError('Request - Unknown Error') + reject(new DiscordAPIError(Deno.inspect(error))) + } else reject(new DiscordAPIError('Request - Unknown Error')) } /** @@ -347,7 +378,7 @@ export class RESTManager { ) const json: any = await response.json() - await this.handleStatusCode(response, json, requestData) + await this.handleStatusCode(response, json, requestData, reject) if ( json.retry_after !== undefined || diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index 7eebdf4..d0177ab 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -1,7 +1,10 @@ +import { VoiceServerUpdateData } from '../gateway/handlers/index.ts' +import { VoiceStateOptions } from '../gateway/index.ts' import { Client } from '../models/client.ts' import { GuildVoiceChannelPayload, Overwrite } from '../types/channel.ts' import { Channel } from './channel.ts' import { Guild } from './guild.ts' +import { VoiceState } from './voiceState.ts' export class VoiceChannel extends Channel { bitrate: string @@ -29,6 +32,50 @@ export class VoiceChannel extends Channel { // cache.set('guildvoicechannel', this.id, this) } + async join(options?: VoiceStateOptions): Promise { + return await new Promise((resolve, reject) => { + let vcdata: VoiceServerUpdateData | undefined + let done = 0 + + const onVoiceStateAdd = (state: VoiceState): void => { + if (state.user.id !== this.client.user?.id) return + if (state.channel?.id !== this.id) return + this.client.removeListener('voiceStateAdd', onVoiceStateAdd) + done++ + if (done >= 2) resolve(vcdata) + } + + const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { + if (data.guild.id !== this.guild.id) return + vcdata = data + this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) + done++ + if (done >= 2) resolve(vcdata) + } + + this.client.gateway?.updateVoiceState(this.guild.id, this.id, options) + + this.client.on('voiceStateAdd', onVoiceStateAdd) + this.client.on('voiceServerUpdate', onVoiceServerUpdate) + + setTimeout(() => { + if (done < 2) { + this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) + this.client.removeListener('voiceStateAdd', onVoiceStateAdd) + reject( + new Error( + "Connection timed out - couldn't connect to Voice Channel" + ) + ) + } + }, 1000 * 60) + }) + } + + leave(): void { + this.client.gateway?.updateVoiceState(this.guild.id, undefined) + } + readFromData(data: GuildVoiceChannelPayload): void { super.readFromData(data) this.bitrate = data.bitrate ?? this.bitrate diff --git a/src/structures/voiceState.ts b/src/structures/voiceState.ts index 0633b13..92e58b5 100644 --- a/src/structures/voiceState.ts +++ b/src/structures/voiceState.ts @@ -8,6 +8,7 @@ import { User } from './user.ts' export class VoiceState extends Base { guild?: Guild + channelID: string | null channel: VoiceChannel | null user: User member?: Member @@ -29,6 +30,7 @@ export class VoiceState extends Base { } ) { super(client, data) + this.channelID = data.channel_id this.channel = _data.channel this.sessionID = data.session_id this.user = _data.user @@ -46,6 +48,7 @@ export class VoiceState extends Base { readFromData(data: VoiceStatePayload): void { this.sessionID = data.session_id ?? this.sessionID this.deaf = data.deaf ?? this.deaf + this.channelID = data.channel_id ?? this.channelID this.mute = data.mute ?? this.mute this.deaf = data.self_deaf ?? this.deaf this.mute = data.self_mute ?? this.mute diff --git a/src/test/cmd.ts b/src/test/cmd.ts index ea2fec1..75050eb 100644 --- a/src/test/cmd.ts +++ b/src/test/cmd.ts @@ -12,7 +12,8 @@ import { TOKEN } from './config.ts' const client = new CommandClient({ prefix: ['pls', '!'], spacesAfterPrefix: true, - mentionPrefix: true + mentionPrefix: true, + owners: ['422957901716652033'] }) client.on('debug', console.log) @@ -116,20 +117,12 @@ client.on('channelUpdate', (before, after) => { ) }) -client.on('typingStart', (user, channel, at, guildData) => { - console.log( - `${user.tag} started typing in ${channel.id} at ${at}${ - guildData !== undefined ? `\nGuild: ${guildData.guild.name}` : '' - }` - ) -}) - client.on('voiceStateAdd', (state) => { - console.log('VC Join', state) + console.log('VC Join', state.user.tag) }) client.on('voiceStateRemove', (state) => { - console.log('VC Leave', state) + console.log('VC Leave', state.user.tag) }) client.on('messageReactionAdd', (reaction, user) => { diff --git a/src/test/cmds/eval.ts b/src/test/cmds/eval.ts new file mode 100644 index 0000000..efd46e9 --- /dev/null +++ b/src/test/cmds/eval.ts @@ -0,0 +1,21 @@ +import { Command } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' + +export default class EvalCommand extends Command { + name = 'eval' + ownerOnly = true + + async execute(ctx: CommandContext): Promise { + try { + // eslint-disable-next-line no-eval + let evaled = eval(ctx.argString) + if (evaled instanceof Promise) evaled = await evaled + if (typeof evaled === 'object') evaled = Deno.inspect(evaled) + await ctx.message.reply( + `\`\`\`js\n${`${evaled}`.substring(0, 1990)}\n\`\`\`` + ) + } catch (e) { + ctx.message.reply(`\`\`\`js\n${e.stack}\n\`\`\``) + } + } +} diff --git a/src/test/cmds/join.ts b/src/test/cmds/join.ts new file mode 100644 index 0000000..6438d9e --- /dev/null +++ b/src/test/cmds/join.ts @@ -0,0 +1,17 @@ +import { Command } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' + +export default class JoinCommand extends Command { + name = 'join' + guildOnly = true + + async execute(ctx: CommandContext): Promise { + const userVS = await ctx.guild?.voiceStates.get(ctx.author.id) + if (userVS === undefined) { + ctx.message.reply("You're not in VC.") + return + } + await userVS.channel?.join() + ctx.message.reply(`Joined VC channel - ${userVS.channel?.name}!`) + } +} diff --git a/src/test/cmds/leave.ts b/src/test/cmds/leave.ts new file mode 100644 index 0000000..abf5d61 --- /dev/null +++ b/src/test/cmds/leave.ts @@ -0,0 +1,19 @@ +import { Command } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' + +export default class LeaveCommand extends Command { + name = 'leave' + guildOnly = true + + async execute(ctx: CommandContext): Promise { + const userVS = await ctx.guild?.voiceStates.get( + (ctx.client.user?.id as unknown) as string + ) + if (userVS === undefined) { + ctx.message.reply("I'm not in VC.") + return + } + userVS.channel?.leave() + ctx.message.reply(`Left VC channel - ${userVS.channel?.name}!`) + } +}