diff --git a/deploy.ts b/deploy.ts index 9e5ad9a..396a664 100644 --- a/deploy.ts +++ b/deploy.ts @@ -1,4 +1,4 @@ -import { Interaction } from './src/structures/interactions.ts ' +import { Interaction } from './src/structures/interactions.ts' import { SlashCommandsManager, SlashClient, diff --git a/src/client/client.ts b/src/client/client.ts index e8ea5ef..2790074 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -23,6 +23,7 @@ import type { VoiceRegion } from '../types/voice.ts' import { fetchAuto } from '../../deps.ts' import type { DMChannel } from '../structures/dmChannel.ts' import { Template } from '../structures/template.ts' +import { VoiceManager } from './voice.ts' /** OS related properties sent with Gateway Identify */ export interface ClientProperties { @@ -119,6 +120,9 @@ export class Client extends HarmonyEventEmitter { /** Whether to fetch Gateway info or not */ fetchGatewayInfo: boolean = true + /** Voice Connections Manager */ + readonly voice = new VoiceManager(this) + /** Users Manager, containing all Users cached */ readonly users: UsersManager = new UsersManager(this) /** Guilds Manager, providing cache & API interface to Guilds */ diff --git a/src/client/voice.ts b/src/client/voice.ts new file mode 100644 index 0000000..ebb5a6b --- /dev/null +++ b/src/client/voice.ts @@ -0,0 +1,116 @@ +import type { VoiceServerUpdateData } from '../gateway/handlers/mod.ts' +import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' +import type { VoiceStateOptions } from '../gateway/mod.ts' +import { VoiceState } from '../structures/voiceState.ts' +import { ChannelTypes } from '../types/channel.ts' +import type { Guild } from '../structures/guild.ts' +import { HarmonyEventEmitter } from '../utils/events.ts' +import type { Client } from './client.ts' + +export interface VoiceServerData extends VoiceServerUpdateData { + userID: string + sessionID: string +} + +export interface VoiceChannelJoinOptions extends VoiceStateOptions { + timeout?: number +} + +export class VoiceManager extends HarmonyEventEmitter<{ + voiceStateUpdate: [VoiceState] +}> { + #pending = new Map() + + readonly client!: Client + + constructor(client: Client) { + super() + Object.defineProperty(this, 'client', { + value: client, + enumerable: false + }) + } + + async join( + channel: string | VoiceChannel, + options?: VoiceChannelJoinOptions + ): Promise { + const id = typeof channel === 'string' ? channel : channel.id + const chan = await this.client.channels.get(id) + if (chan === undefined) throw new Error('Voice Channel not cached') + if ( + chan.type !== ChannelTypes.GUILD_VOICE && + chan.type !== ChannelTypes.GUILD_STAGE_VOICE + ) + throw new Error('Cannot join non-voice channel') + + const pending = this.#pending.get(chan.guild.id) + if (pending !== undefined) { + clearTimeout(pending[0]) + pending[1](new Error('Voice Connection timed out')) + this.#pending.delete(chan.guild.id) + } + + return await new Promise((resolve, reject) => { + let vcdata: VoiceServerData + let done = 0 + + const onVoiceStateAdd = (state: VoiceState): void => { + if (state.user.id !== this.client.user?.id) return + if (state.channel?.id !== id) return + this.client.off('voiceStateAdd', onVoiceStateAdd) + done++ + vcdata = vcdata ?? {} + vcdata.sessionID = state.sessionID + vcdata.userID = state.user.id + if (done >= 2) { + this.#pending.delete(chan.guild.id) + resolve(vcdata) + } + } + + const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { + if (data.guild.id !== chan.guild.id) return + vcdata = Object.assign(vcdata ?? {}, data) + this.client.off('voiceServerUpdate', onVoiceServerUpdate) + done++ + if (done >= 2) { + this.#pending.delete(chan.guild.id) + resolve(vcdata) + } + } + + this.client.shards + .get(chan.guild.shardID)! + .updateVoiceState(chan.guild.id, chan.id, options) + + this.on('voiceStateUpdate', onVoiceStateAdd) + this.client.on('voiceServerUpdate', onVoiceServerUpdate) + + const timer = setTimeout(() => { + if (done < 2) { + this.client.off('voiceServerUpdate', onVoiceServerUpdate) + this.client.off('voiceStateAdd', onVoiceStateAdd) + reject( + new Error( + "Connection timed out - couldn't connect to Voice Channel" + ) + ) + } + }, options?.timeout ?? 1000 * 30) + + this.#pending.set(chan.guild.id, [timer, reject]) + }) + } + + async leave(guildOrID: Guild | string): Promise { + const id = typeof guildOrID === 'string' ? guildOrID : guildOrID.id + const guild = await this.client.guilds.get(id) + if (guild === undefined) throw new Error('Guild not cached') + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const vcs = await guild.voiceStates.get(this.client.user?.id!) + if (vcs === undefined) throw new Error('Not in Voice Channel') + + this.client.shards.get(guild.shardID)!.updateVoiceState(guild, undefined) + } +} diff --git a/src/gateway/handlers/voiceStateUpdate.ts b/src/gateway/handlers/voiceStateUpdate.ts index 7babda2..5a7310e 100644 --- a/src/gateway/handlers/voiceStateUpdate.ts +++ b/src/gateway/handlers/voiceStateUpdate.ts @@ -33,17 +33,15 @@ export const voiceStateUpdate: GatewayEventHandler = async ( } await guild.voiceStates.set(d.user_id, d) - const newVoiceState = await guild.voiceStates.get(d.user_id) + const newVoiceState = (await guild.voiceStates.get(d.user_id))! + + if (d.user_id === gateway.client.user!.id) { + gateway.client.voice.emit('voiceStateUpdate', newVoiceState) + } + if (voiceState === undefined) { - gateway.client.emit( - 'voiceStateAdd', - (newVoiceState as unknown) as VoiceState - ) + gateway.client.emit('voiceStateAdd', newVoiceState) } else { - gateway.client.emit( - 'voiceStateUpdate', - voiceState, - (newVoiceState as unknown) as VoiceState - ) + gateway.client.emit('voiceStateUpdate', voiceState, newVoiceState) } } diff --git a/src/gateway/mod.ts b/src/gateway/mod.ts index 81aa16a..7eb9cb1 100644 --- a/src/gateway/mod.ts +++ b/src/gateway/mod.ts @@ -371,13 +371,13 @@ export class Gateway extends HarmonyEventEmitter { : channel?.id, self_mute: channel === undefined - ? undefined + ? false : voiceOptions.mute === undefined ? false : voiceOptions.mute, self_deaf: channel === undefined - ? undefined + ? false : voiceOptions.deaf === undefined ? false : voiceOptions.deaf diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index 0d2058f..dade3df 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -1,5 +1,3 @@ -import type { VoiceServerUpdateData } from '../gateway/handlers/mod.ts' -import type { VoiceStateOptions } from '../gateway/mod.ts' import type { Client } from '../client/mod.ts' import type { GuildVoiceChannelPayload, @@ -9,14 +7,13 @@ import type { import { CHANNEL } from '../types/endpoint.ts' import { GuildChannel } from './channel.ts' import type { Guild } from './guild.ts' -import type { VoiceState } from './voiceState.ts' import { GuildChannelVoiceStatesManager } from '../managers/guildChannelVoiceStates.ts' import type { User } from './user.ts' import type { Member } from './member.ts' - -export interface VoiceServerData extends VoiceServerUpdateData { - sessionID: string -} +import type { + VoiceChannelJoinOptions, + VoiceServerData +} from '../client/voice.ts' export class VoiceChannel extends GuildChannel { bitrate: string @@ -34,65 +31,13 @@ export class VoiceChannel extends GuildChannel { } /** Join the Voice Channel */ - async join( - options?: VoiceStateOptions & { onlyJoin?: boolean } - ): Promise { - return await new Promise((resolve, reject) => { - let vcdata: VoiceServerData - let sessionID: string - 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.off('voiceStateAdd', onVoiceStateAdd) - done++ - sessionID = state.sessionID - if (done >= 2) { - vcdata.sessionID = sessionID - if (options?.onlyJoin !== true) { - } - resolve(vcdata) - } - } - - const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { - if (data.guild.id !== this.guild.id) return - vcdata = (data as unknown) as VoiceServerData - this.client.off('voiceServerUpdate', onVoiceServerUpdate) - done++ - if (done >= 2) { - vcdata.sessionID = sessionID - resolve(vcdata) - } - } - - this.client.shards - .get(this.guild.shardID) - ?.updateVoiceState(this.guild.id, this.id, options) - - this.client.on('voiceStateAdd', onVoiceStateAdd) - this.client.on('voiceServerUpdate', onVoiceServerUpdate) - - setTimeout(() => { - if (done < 2) { - this.client.off('voiceServerUpdate', onVoiceServerUpdate) - this.client.off('voiceStateAdd', onVoiceStateAdd) - reject( - new Error( - "Connection timed out - couldn't connect to Voice Channel" - ) - ) - } - }, 1000 * 60) - }) + async join(options?: VoiceChannelJoinOptions): Promise { + return this.client.voice.join(this.id, options) } /** Leave the Voice Channel */ - leave(): void { - this.client.shards - .get(this.guild.shardID) - ?.updateVoiceState(this.guild.id, undefined) + async leave(): Promise { + return this.client.voice.leave(this.guild) } readFromData(data: GuildVoiceChannelPayload): void { @@ -116,6 +61,14 @@ export class VoiceChannel extends GuildChannel { return new VoiceChannel(this.client, resp, this.guild) } + async setBitrate(rate: number | undefined): Promise { + return await this.edit({ bitrate: rate }) + } + + async setUserLimit(limit: number | undefined): Promise { + return await this.edit({ userLimit: limit }) + } + async disconnectMember( member: User | Member | string ): Promise { diff --git a/test/vc.ts b/test/vc.ts new file mode 100644 index 0000000..12bb7bb --- /dev/null +++ b/test/vc.ts @@ -0,0 +1,26 @@ +import * as discord from '../mod.ts' +import { TOKEN } from './config.ts' + +const client = new discord.Client({ + token: TOKEN, + intents: ['GUILDS', 'GUILD_VOICE_STATES', 'GUILD_MESSAGES'] +}) + +client.on('messageCreate', async (msg) => { + if (msg.author.bot === true || msg.guild === undefined) return + + if (msg.content === '!join') { + const vs = await msg.guild.voiceStates.get(msg.author.id) + if (vs === undefined) return msg.reply("You're not in Voice Channel!") + const data = await vs.channel!.join() + console.log(data) + msg.reply('Joined voice channel.') + } else if (msg.content === '!leave') { + const vs = await msg.guild.voiceStates.get(msg.client.user!.id!) + if (vs === undefined) return msg.reply("I'm not in Voice Channel!") + await vs.channel!.leave() + msg.reply('Left voice channel.') + } +}) + +client.connect().then(() => console.log('Ready!'))