From b4a1ae389d35d950a7c954cb26c5a332ad4ac129 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 01:43:01 +0900 Subject: [PATCH 1/7] :sparkles: Add some few things that are useful to voiceState --- src/structures/member.ts | 39 +++++++++++++++++-- src/structures/voiceState.ts | 74 +++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/structures/member.ts b/src/structures/member.ts index fe491d4..ee60bf7 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -5,6 +5,7 @@ import { MemberPayload } from '../types/guild.ts' import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' import { Guild } from './guild.ts' +import { VoiceChannel } from './guildVoiceChannel.ts' import { Role } from './role.ts' import { User } from './user.ts' @@ -13,6 +14,7 @@ export interface MemberData { roles?: Array deaf?: boolean mute?: boolean + channel?: string | VoiceChannel | null } export class Member extends SnowflakeBase { @@ -84,8 +86,11 @@ export class Member extends SnowflakeBase { nick: data.nick, roles: data.roles?.map((e) => (typeof e === 'string' ? e : e.id)), deaf: data.deaf, - mute: data.mute + mute: data.mute, + channel_id: + typeof data.channel === 'string' ? data.channel : data.channel?.id } + const res = await this.client.rest.patch( GUILD_MEMBER(this.guild.id, this.id), payload, @@ -125,7 +130,7 @@ export class Member extends SnowflakeBase { */ async setMute(mute?: boolean): Promise { return await this.edit({ - mute: mute === undefined ? false : mute + mute: mute ?? false }) } @@ -135,7 +140,28 @@ export class Member extends SnowflakeBase { */ async setDeaf(deaf?: boolean): Promise { return await this.edit({ - deaf: deaf === undefined ? false : deaf + deaf: deaf ?? false + }) + } + + /** + * Moves a Member to another VC + * @param channel Channel to move(null or undefined to disconnect) + */ + async moveVoiceChannel( + channel?: string | VoiceChannel | null + ): Promise { + return await this.edit({ + channel: channel ?? null + }) + } + + /** + * Disconnects a Member from connected VC + */ + async disconnectVoice(): Promise { + return await this.edit({ + channel: null }) } @@ -146,6 +172,13 @@ export class Member extends SnowflakeBase { return await this.setMute(false) } + /** + * Undeafs the Member from VC. + */ + async undeaf(): Promise { + return await this.setDeaf(false) + } + /** * Kicks the member. */ diff --git a/src/structures/voiceState.ts b/src/structures/voiceState.ts index cd3cc7f..45c2a4b 100644 --- a/src/structures/voiceState.ts +++ b/src/structures/voiceState.ts @@ -1,4 +1,5 @@ import { Client } from '../models/client.ts' +import { ChannelTypes } from '../types/channel.ts' import { VoiceStatePayload } from '../types/voice.ts' import { Base } from './base.ts' import { Guild } from './guild.ts' @@ -52,12 +53,81 @@ export class VoiceState extends Base { 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 this.selfDeaf = data.self_deaf ?? this.selfDeaf this.selfMute = data.self_mute ?? this.selfMute this.stream = data.self_stream ?? this.stream this.video = data.self_video ?? this.video this.suppress = data.suppress ?? this.suppress } + + /** + * Disconnects a Member from connected VC + */ + async disconnect(): Promise { + const result = this.member?.disconnectVoice() + if (result !== undefined) { + this.channelID = null + this.channel = null + } + return result + } + + /** + * Moves a Member to another VC + * @param channel Channel to move(null or undefined to disconnect) + */ + async moveChannel( + channel?: string | VoiceChannel | null + ): Promise { + const result = this.member?.moveVoiceChannel(channel) + if (result !== undefined) { + let channelFetched: VoiceChannel | null + let channelID: string | null + if (typeof channel === 'string') { + channelID = channel + const channelCached = await this.guild?.channels.fetch(channel) + if (channelCached?.type === ChannelTypes.GUILD_VOICE) { + channelFetched = channelCached as VoiceChannel + } else { + throw new Error(`Channel ${channel} is not a VoiceChannel.`) + } + } else { + channelID = channel?.id ?? null + channelFetched = channel ?? null + } + this.channelID = channelID + this.channel = channelFetched + } + return result + } + + /** + * Sets a Member mute in VC + * @param mute Value to set + */ + async setMute(mute?: boolean): Promise { + return this.member?.setMute(mute) + } + + /** + * Sets a Member deaf in VC + * @param deaf Value to set + */ + async setDeaf(deaf?: boolean): Promise { + return this.member?.setDeaf(deaf) + } + + /** + * Unmutes the Member from VC. + */ + async unmute(): Promise { + return await this.setMute(false) + } + + /** + * Undeafs the Member from VC. + */ + async undeaf(): Promise { + return await this.setDeaf(false) + } } From 9c5aea1ef8544eeeca8d90bd1388401941c1c541 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 01:54:21 +0900 Subject: [PATCH 2/7] :bug: Replace the guild as the manager's guild if it's undefined --- src/managers/guildVoiceStates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/guildVoiceStates.ts b/src/managers/guildVoiceStates.ts index 4b1c5bd..baefcf7 100644 --- a/src/managers/guildVoiceStates.ts +++ b/src/managers/guildVoiceStates.ts @@ -30,7 +30,7 @@ export class GuildVoiceStatesManager extends BaseManager< const guild = raw.guild_id === undefined - ? undefined + ? this.guild : await this.client.guilds.get(raw.guild_id) return new VoiceState(this.client, raw, { @@ -57,7 +57,7 @@ export class GuildVoiceStatesManager extends BaseManager< arr.map(async (raw) => { const guild = raw.guild_id === undefined - ? undefined + ? this.guild : await this.client.guilds.get(raw.guild_id) return new VoiceState(this.client, raw, { From 064c3a6d765adf223ad3d42f844f7a9853ebd6a9 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 02:00:25 +0900 Subject: [PATCH 3/7] :bug: Make .edit check channel type more precisely --- src/structures/member.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/member.ts b/src/structures/member.ts index ee60bf7..eb2593a 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -88,7 +88,7 @@ export class Member extends SnowflakeBase { deaf: data.deaf, mute: data.mute, channel_id: - typeof data.channel === 'string' ? data.channel : data.channel?.id + data.channel instanceof VoiceChannel ? data.channel.id : data.channel } const res = await this.client.rest.patch( From d2cedeceb13ded109f4a17f5725a89662ce5dd6b Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 02:01:19 +0900 Subject: [PATCH 4/7] :white_check_mark: Add voice kick test --- src/test/cmds/kickFromVoice.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/test/cmds/kickFromVoice.ts diff --git a/src/test/cmds/kickFromVoice.ts b/src/test/cmds/kickFromVoice.ts new file mode 100644 index 0000000..5730703 --- /dev/null +++ b/src/test/cmds/kickFromVoice.ts @@ -0,0 +1,20 @@ +import { Command } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' + +export default class KickFromVoiceCommand extends Command { + name = 'kickFromVoice' + + async execute(ctx: CommandContext): Promise { + if (ctx.guild !== undefined) { + const voiceStates = await ctx.guild.voiceStates.array() + if (voiceStates !== undefined) { + voiceStates.forEach(async (voiceState) => { + const member = await voiceState.disconnect() + if (member !== undefined) { + ctx.channel.send(`Kicked member ${member.id}`) + } + }) + } + } + } +} From 51b1d71e9a9053372191a0751017c0ca13eea8e2 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 02:18:24 +0900 Subject: [PATCH 5/7] :sparkles: Add guild channel voice states manager --- src/managers/guildChannelVoiceStates.ts | 39 +++++++++++++++++++++++++ src/structures/guildVoiceChannel.ts | 6 ++++ src/test/cmds/kickFromSpecificVoice.ts | 28 ++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/managers/guildChannelVoiceStates.ts create mode 100644 src/test/cmds/kickFromSpecificVoice.ts diff --git a/src/managers/guildChannelVoiceStates.ts b/src/managers/guildChannelVoiceStates.ts new file mode 100644 index 0000000..2dc9ac5 --- /dev/null +++ b/src/managers/guildChannelVoiceStates.ts @@ -0,0 +1,39 @@ +import { Client } from '../models/client.ts' +import { BaseChildManager } from './baseChild.ts' +import { VoiceStatePayload } from '../types/voice.ts' +import { VoiceState } from '../structures/voiceState.ts' +import { GuildVoiceStatesManager } from './guildVoiceStates.ts' +import { VoiceChannel } from '../structures/guildVoiceChannel.ts' + +export class GuildChannelVoiceStatesManager extends BaseChildManager< + VoiceStatePayload, + VoiceState +> { + channel: VoiceChannel + + constructor( + client: Client, + parent: GuildVoiceStatesManager, + channel: VoiceChannel + ) { + super(client, parent as any) + this.channel = channel + } + + async get(id: string): Promise { + const res = await this.parent.get(id) + if (res !== undefined && res.channel?.id === this.channel.id) return res + else return undefined + } + + async array(): Promise { + const arr = (await this.parent.array()) as VoiceState[] + return arr.filter((c: any) => c.channel?.id === this.channel.id) as any + } + + async fromPayload(d: VoiceStatePayload[]): Promise { + for (const data of d) { + await this.set(data.user_id, data) + } + } +} diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index d1dc287..0f2764a 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -10,6 +10,7 @@ import { CHANNEL } from '../types/endpoint.ts' import { GuildChannel } from './channel.ts' import { Guild } from './guild.ts' import { VoiceState } from './voiceState.ts' +import { GuildChannelVoiceStatesManager } from '../managers/guildChannelVoiceStates.ts' export interface VoiceServerData extends VoiceServerUpdateData { sessionID: string @@ -18,6 +19,11 @@ export interface VoiceServerData extends VoiceServerUpdateData { export class VoiceChannel extends GuildChannel { bitrate: string userLimit: number + voiceStates = new GuildChannelVoiceStatesManager( + this.client, + this.guild.voiceStates, + this + ) constructor(client: Client, data: GuildVoiceChannelPayload, guild: Guild) { super(client, data, guild) diff --git a/src/test/cmds/kickFromSpecificVoice.ts b/src/test/cmds/kickFromSpecificVoice.ts new file mode 100644 index 0000000..5fb8499 --- /dev/null +++ b/src/test/cmds/kickFromSpecificVoice.ts @@ -0,0 +1,28 @@ +import { Command, VoiceChannel } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' +import { ChannelTypes } from '../../types/channel.ts' + +export default class KickFromSpecificVoiceCommand extends Command { + name = 'kickFromSpecificVoice' + + async execute(ctx: CommandContext): Promise { + if (ctx.guild !== undefined) { + const channel = await ctx.guild.channels.get('YOUR VOICE CHANNEL ID') + if (channel === undefined || channel.type !== ChannelTypes.GUILD_VOICE) { + ctx.channel.send('The channel is either not a voice or not available.') + return + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const voiceStates = await (channel as VoiceChannel).voiceStates.array() + if (voiceStates !== undefined) { + voiceStates.forEach(async (voiceState) => { + const member = await voiceState.disconnect() + if (member !== undefined) { + ctx.channel.send(`Kicked member ${member.id}`) + } + }) + } + } + } +} From 96b273ed047a77e92627b8711ca0d19e248f4c6c Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 11:05:21 +0900 Subject: [PATCH 6/7] :sparkles: Add disconnection functions to voice channel --- src/structures/guildVoiceChannel.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index 0f2764a..759fb71 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -11,6 +11,8 @@ import { GuildChannel } from './channel.ts' import { Guild } from './guild.ts' import { VoiceState } from './voiceState.ts' import { GuildChannelVoiceStatesManager } from '../managers/guildChannelVoiceStates.ts' +import { User } from './user.ts' +import { Member } from './member.ts' export interface VoiceServerData extends VoiceServerUpdateData { sessionID: string @@ -113,4 +115,25 @@ export class VoiceChannel extends GuildChannel { return new VoiceChannel(this.client, resp, this.guild) } + + async disconnectMember( + member: User | Member | string + ): Promise { + const memberID = typeof member === 'string' ? member : member.id + const memberVoiceState = await this.voiceStates.get(memberID) + + return memberVoiceState?.disconnect() + } + + async disconnectAll(): Promise { + const members: Member[] = [] + for await (const memberVoiceState of this.voiceStates) { + const member = await memberVoiceState.disconnect() + if (member !== undefined) { + members.push(member) + } + } + + return members + } } From 3fe5ce3063e31d29cab8e2d1d7cd9aa92ec74112 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Mar 2021 11:29:07 +0900 Subject: [PATCH 7/7] :white_check_mark: Add voice channel disconnect test --- src/test/cmds/kickFromSpecificVoice.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/test/cmds/kickFromSpecificVoice.ts b/src/test/cmds/kickFromSpecificVoice.ts index 5fb8499..f60089b 100644 --- a/src/test/cmds/kickFromSpecificVoice.ts +++ b/src/test/cmds/kickFromSpecificVoice.ts @@ -14,15 +14,10 @@ export default class KickFromSpecificVoiceCommand extends Command { } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const voiceStates = await (channel as VoiceChannel).voiceStates.array() - if (voiceStates !== undefined) { - voiceStates.forEach(async (voiceState) => { - const member = await voiceState.disconnect() - if (member !== undefined) { - ctx.channel.send(`Kicked member ${member.id}`) - } - }) - } + const members = await (channel as VoiceChannel).disconnectAll() + members.forEach((member) => { + ctx.channel.send(`Kicked member ${member.id}`) + }) } } }