feat(voice): add Gateway#updateVoiceState, VoiceChannel#join, VoiceChannel#leave

This commit is contained in:
DjDeveloperr 2020-12-05 14:26:43 +05:30
parent a66a18cdc0
commit f6c307844f
9 changed files with 180 additions and 22 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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 ||

View file

@ -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

View file

@ -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

View file

@ -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
View 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
View 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
View 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}!`)
}
}