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(
|
||||
`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 ||
|
||||
|
|
|
@ -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…
Reference in a new issue