feat(voice): add Gateway#updateVoiceState, VoiceChannel#join, VoiceChannel#leave
This commit is contained in:
		
							parent
							
								
									a66a18cdc0
								
							
						
					
					
						commit
						f6c307844f
					
				
					 9 changed files with 180 additions and 22 deletions
				
			
		|  | @ -8,7 +8,6 @@ export const voiceStateUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|   d: VoiceStatePayload |   d: VoiceStatePayload | ||||||
| ) => { | ) => { | ||||||
|   // TODO(DjDeveloperr): Support self-bot here; they can be in DMs (Call)
 |  | ||||||
|   if (d.guild_id === undefined) return |   if (d.guild_id === undefined) return | ||||||
|   const guild = ((await gateway.client.guilds.get( |   const guild = ((await gateway.client.guilds.get( | ||||||
|     d.guild_id |     d.guild_id | ||||||
|  |  | ||||||
|  | @ -16,6 +16,8 @@ import { gatewayHandlers } from './handlers/index.ts' | ||||||
| import { GATEWAY_BOT } from '../types/endpoint.ts' | import { GATEWAY_BOT } from '../types/endpoint.ts' | ||||||
| import { GatewayCache } from '../managers/gatewayCache.ts' | import { GatewayCache } from '../managers/gatewayCache.ts' | ||||||
| import { delay } from '../utils/delay.ts' | import { delay } from '../utils/delay.ts' | ||||||
|  | import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
|  | import { Guild } from '../structures/guild.ts' | ||||||
| 
 | 
 | ||||||
| export interface RequestMembersOptions { | export interface RequestMembersOptions { | ||||||
|   limit?: number |   limit?: number | ||||||
|  | @ -24,6 +26,11 @@ export interface RequestMembersOptions { | ||||||
|   users?: string[] |   users?: string[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface VoiceStateOptions { | ||||||
|  |   mute?: boolean | ||||||
|  |   deaf?: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const RECONNECT_REASON = 'harmony-reconnect' | export const RECONNECT_REASON = 'harmony-reconnect' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -308,6 +315,27 @@ class Gateway { | ||||||
|     return nonce |     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 { |   debug(msg: string): void { | ||||||
|     this.client.debug('Gateway', msg) |     this.client.debug('Gateway', msg) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -244,7 +244,8 @@ export class RESTManager { | ||||||
|   private async handleStatusCode( |   private async handleStatusCode( | ||||||
|     response: Response, |     response: Response, | ||||||
|     body: any, |     body: any, | ||||||
|     data: { [key: string]: any } |     data: { [key: string]: any }, | ||||||
|  |     reject: CallableFunction | ||||||
|   ): Promise<undefined> { |   ): Promise<undefined> { | ||||||
|     const status = response.status |     const status = response.status | ||||||
| 
 | 
 | ||||||
|  | @ -261,18 +262,48 @@ export class RESTManager { | ||||||
|     if (text === 'undefined') text = undefined |     if (text === 'undefined') text = undefined | ||||||
| 
 | 
 | ||||||
|     if (status === HttpResponseCode.Unauthorized) |     if (status === HttpResponseCode.Unauthorized) | ||||||
|       throw new DiscordAPIError( |       reject( | ||||||
|         `Request was not successful (Unauthorized). Invalid Token.\n${text}` |         new DiscordAPIError( | ||||||
|  |           `Request was not successful (Unauthorized). Invalid Token.\n${text}` | ||||||
|  |         ) | ||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
|     // At this point we know it is error
 |     // At this point we know it is error
 | ||||||
|     let error = { |     const error: { [name: string]: any } = { | ||||||
|       url: response.url, |       url: response.url, | ||||||
|       status, |       status, | ||||||
|       method: data.method, |       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 ( |     if ( | ||||||
|       [ |       [ | ||||||
|  | @ -282,10 +313,10 @@ export class RESTManager { | ||||||
|         HttpResponseCode.MethodNotAllowed |         HttpResponseCode.MethodNotAllowed | ||||||
|       ].includes(status) |       ].includes(status) | ||||||
|     ) { |     ) { | ||||||
|       throw new DiscordAPIError(Deno.inspect(error)) |       reject(new DiscordAPIError(Deno.inspect(error))) | ||||||
|     } else if (status === HttpResponseCode.GatewayUnavailable) { |     } else if (status === HttpResponseCode.GatewayUnavailable) { | ||||||
|       throw new DiscordAPIError(Deno.inspect(error)) |       reject(new DiscordAPIError(Deno.inspect(error))) | ||||||
|     } else throw new DiscordAPIError('Request - Unknown Error') |     } else reject(new DiscordAPIError('Request - Unknown Error')) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | @ -347,7 +378,7 @@ export class RESTManager { | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|           const json: any = await response.json() |           const json: any = await response.json() | ||||||
|           await this.handleStatusCode(response, json, requestData) |           await this.handleStatusCode(response, json, requestData, reject) | ||||||
| 
 | 
 | ||||||
|           if ( |           if ( | ||||||
|             json.retry_after !== undefined || |             json.retry_after !== undefined || | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
|  | import { VoiceServerUpdateData } from '../gateway/handlers/index.ts' | ||||||
|  | import { VoiceStateOptions } from '../gateway/index.ts' | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { GuildVoiceChannelPayload, Overwrite } from '../types/channel.ts' | import { GuildVoiceChannelPayload, Overwrite } from '../types/channel.ts' | ||||||
| import { Channel } from './channel.ts' | import { Channel } from './channel.ts' | ||||||
| import { Guild } from './guild.ts' | import { Guild } from './guild.ts' | ||||||
|  | import { VoiceState } from './voiceState.ts' | ||||||
| 
 | 
 | ||||||
| export class VoiceChannel extends Channel { | export class VoiceChannel extends Channel { | ||||||
|   bitrate: string |   bitrate: string | ||||||
|  | @ -29,6 +32,50 @@ export class VoiceChannel extends Channel { | ||||||
|     // cache.set('guildvoicechannel', this.id, this)
 |     // cache.set('guildvoicechannel', this.id, this)
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async join(options?: VoiceStateOptions): Promise<VoiceServerUpdateData> { | ||||||
|  |     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 { |   readFromData(data: GuildVoiceChannelPayload): void { | ||||||
|     super.readFromData(data) |     super.readFromData(data) | ||||||
|     this.bitrate = data.bitrate ?? this.bitrate |     this.bitrate = data.bitrate ?? this.bitrate | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { User } from './user.ts' | ||||||
| 
 | 
 | ||||||
| export class VoiceState extends Base { | export class VoiceState extends Base { | ||||||
|   guild?: Guild |   guild?: Guild | ||||||
|  |   channelID: string | null | ||||||
|   channel: VoiceChannel | null |   channel: VoiceChannel | null | ||||||
|   user: User |   user: User | ||||||
|   member?: Member |   member?: Member | ||||||
|  | @ -29,6 +30,7 @@ export class VoiceState extends Base { | ||||||
|     } |     } | ||||||
|   ) { |   ) { | ||||||
|     super(client, data) |     super(client, data) | ||||||
|  |     this.channelID = data.channel_id | ||||||
|     this.channel = _data.channel |     this.channel = _data.channel | ||||||
|     this.sessionID = data.session_id |     this.sessionID = data.session_id | ||||||
|     this.user = _data.user |     this.user = _data.user | ||||||
|  | @ -46,6 +48,7 @@ export class VoiceState extends Base { | ||||||
|   readFromData(data: VoiceStatePayload): void { |   readFromData(data: VoiceStatePayload): void { | ||||||
|     this.sessionID = data.session_id ?? this.sessionID |     this.sessionID = data.session_id ?? this.sessionID | ||||||
|     this.deaf = data.deaf ?? this.deaf |     this.deaf = data.deaf ?? this.deaf | ||||||
|  |     this.channelID = data.channel_id ?? this.channelID | ||||||
|     this.mute = data.mute ?? this.mute |     this.mute = data.mute ?? this.mute | ||||||
|     this.deaf = data.self_deaf ?? this.deaf |     this.deaf = data.self_deaf ?? this.deaf | ||||||
|     this.mute = data.self_mute ?? this.mute |     this.mute = data.self_mute ?? this.mute | ||||||
|  |  | ||||||
|  | @ -12,7 +12,8 @@ import { TOKEN } from './config.ts' | ||||||
| const client = new CommandClient({ | const client = new CommandClient({ | ||||||
|   prefix: ['pls', '!'], |   prefix: ['pls', '!'], | ||||||
|   spacesAfterPrefix: true, |   spacesAfterPrefix: true, | ||||||
|   mentionPrefix: true |   mentionPrefix: true, | ||||||
|  |   owners: ['422957901716652033'] | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| client.on('debug', console.log) | 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) => { | client.on('voiceStateAdd', (state) => { | ||||||
|   console.log('VC Join', state) |   console.log('VC Join', state.user.tag) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| client.on('voiceStateRemove', (state) => { | client.on('voiceStateRemove', (state) => { | ||||||
|   console.log('VC Leave', state) |   console.log('VC Leave', state.user.tag) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| client.on('messageReactionAdd', (reaction, user) => { | client.on('messageReactionAdd', (reaction, user) => { | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								src/test/cmds/eval.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/test/cmds/eval.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<void> { | ||||||
|  |     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\`\`\``) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/test/cmds/join.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/test/cmds/join.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<void> { | ||||||
|  |     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}!`) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								src/test/cmds/leave.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/test/cmds/leave.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<void> { | ||||||
|  |     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}!`) | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue