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, | ||||
|   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 | ||||
|  |  | |||
|  | @ -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) | ||||
|   } | ||||
|  |  | |||
|  | @ -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<undefined> { | ||||
|     const status = response.status | ||||
| 
 | ||||
|  | @ -261,18 +262,48 @@ export class RESTManager { | |||
|     if (text === 'undefined') text = undefined | ||||
| 
 | ||||
|     if (status === HttpResponseCode.Unauthorized) | ||||
|       throw new DiscordAPIError( | ||||
|       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 }> | ||||
|             } | ||||
|     if (body !== undefined) error = Object.assign(error, body) | ||||
|           } | ||||
|         ).map((entry) => { | ||||
|           return [entry[0], entry[1]._errors] | ||||
|         }) | ||||
|       ) | ||||
|     } | ||||
| 
 | ||||
|     // 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 || | ||||
|  |  | |||
|  | @ -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<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 { | ||||
|     super.readFromData(data) | ||||
|     this.bitrate = data.bitrate ?? this.bitrate | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) => { | ||||
|  |  | |||
							
								
								
									
										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