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(
|
||||||
|
new DiscordAPIError(
|
||||||
`Request was not successful (Unauthorized). Invalid Token.\n${text}`
|
`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 }>
|
||||||
}
|
}
|
||||||
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 (
|
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…
Reference in a new issue