From d9fd8a88ffbc56e787b047048c212d307866d4ce Mon Sep 17 00:00:00 2001 From: Skick Date: Tue, 28 Dec 2021 17:55:43 +0700 Subject: [PATCH 1/3] fix(Util): remove deprecated stuff Resolve #222 --- package.json | 40 ++++++++++++++++++++------------------- src/__test__/util.test.ts | 2 ++ src/util.ts | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index a1c26a88..f15b3bb1 100644 --- a/package.json +++ b/package.json @@ -46,36 +46,38 @@ "homepage": "https://distube.js.org/", "dependencies": { "@distube/youtube-dl": "^2.2.4", - "@distube/ytdl-core": "^4.9.3", + "@distube/ytdl-core": "^4.9.4", "@distube/ytpl": "^1.1.1", "@distube/ytsr": "^1.1.5", "prism-media": "https://codeload.github.com/distubejs/prism-media/tar.gz/main", "tiny-typed-emitter": "^2.1.0" }, "devDependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-object-rest-spread": "^7.16.0", - "@babel/preset-env": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@commitlint/cli": "^14.1.0", - "@commitlint/config-conventional": "^14.1.0", - "@discordjs/voice": "^0.7.2", + "@babel/core": "^7.16.5", + "@babel/plugin-proposal-class-properties": "^7.16.5", + "@babel/plugin-proposal-object-rest-spread": "^7.16.5", + "@babel/preset-env": "^7.16.5", + "@babel/preset-typescript": "^7.16.5", + "@commitlint/cli": "^16.0.1", + "@commitlint/config-conventional": "^16.0.0", + "@discordjs/voice": "^0.7.5", "@distube/docgen": "github:distubejs/docgen", - "@types/jest": "^27.0.2", + "@types/jest": "^27.0.3", "@types/node": "^16.11.7", - "babel-jest": "^27.3.1", - "discord.js": "^13.3.1", - "eslint": "^7.32.0", - "eslint-config-distube": "^1.4.0", + "@typescript-eslint/eslint-plugin": "^5.8.1", + "babel-jest": "^27.4.5", + "discord.js": "^13.4.0", + "eslint": "^8.5.0", + "eslint-config-distube": "^1.5.0", + "eslint-plugin-jsdoc": "^37.4.0", "husky": "^7.0.4", - "jest": "^27.3.1", + "jest": "^27.4.5", "jsdoc-babel": "^0.5.0", - "lint-staged": "^11.2.6", - "npm-check-updates": "^12.0.2", + "lint-staged": "^12.1.4", + "npm-check-updates": "^12.0.5", "pinst": "^2.1.6", - "prettier": "^2.4.1", - "typescript": "^4.4.4" + "prettier": "^2.5.1", + "typescript": "^4.5.4" }, "peerDependencies": { "@discordjs/opus": "*", diff --git a/src/__test__/util.test.ts b/src/__test__/util.test.ts index dcd859db..1acfc299 100644 --- a/src/__test__/util.test.ts +++ b/src/__test__/util.test.ts @@ -28,7 +28,9 @@ client.user = new ClientUser(client, rawClientUser); const guild = new Guild(client, rawGuild); const textChannel = guild.channels.cache.get("737499503384461325") as TextChannel; const voiceChannel = guild.channels.cache.get("853225781604646933") as VoiceChannel; +Object.defineProperty(voiceChannel, "joinable", { value: true, writable: false }); const stageChannel = guild.channels.cache.get("835876864458489857") as StageChannel; +Object.defineProperty(stageChannel, "joinable", { value: false, writable: false }); const botVoiceState = new VoiceState(guild, rawBotVoiceState); const userVoiceState = new VoiceState(guild, rawUserVoiceState); const message = new Message(client, rawMessage); diff --git a/src/util.ts b/src/util.ts index 8537634f..e144afb3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -146,7 +146,7 @@ export function isMessageInstance(message: any): message is Message { export function isSupportedVoiceChannel(channel: any): channel is VoiceChannel | StageChannel { return ( !!channel && - channel.deleted === false && + typeof channel.joinable === "boolean" && isSnowflake(channel.id) && isSnowflake(channel.guild?.id) && typeof channel.full === "boolean" && From 9985a1ae6c06b1c0dcbee21f2103bdab892ad7be Mon Sep 17 00:00:00 2001 From: Skick Date: Tue, 28 Dec 2021 18:02:10 +0700 Subject: [PATCH 2/3] feat(Util): add `entersState` method --- src/util.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/util.ts b/src/util.ts index e144afb3..fb9a807f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,6 +2,8 @@ import { URL } from "url"; import { DisTubeError, DisTubeVoice, Queue } from "."; import { Intents, SnowflakeUtil } from "discord.js"; import type { GuildIDResolvable } from "."; +import type { EventEmitter } from "node:events"; +import type { AudioPlayer, AudioPlayerStatus, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"; import type { BitFieldResolvable, Client, @@ -194,3 +196,31 @@ export function checkInvalidKey( const invalidKey = Object.keys(target).find(key => !sourceKeys.includes(key)); if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey); } + +async function waitEvent(target: EventEmitter, status: string, maxTime: number) { + let cleanup = () => undefined as any; + try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`Didn't trigger ${status} within ${maxTime}ms`)), maxTime); + target.once(status, resolve); + target.once("error", reject); + cleanup = () => { + clearTimeout(timeout); + target.off(status, resolve); + target.off("error", reject); + }; + }); + return target; + } finally { + cleanup(); + } +} + +export async function entersState( + target: T, + status: T extends VoiceConnection ? VoiceConnectionStatus : AudioPlayerStatus, + maxTime: number, +) { + if (target.state.status === status) return target; + return waitEvent(target, status, maxTime) as Promise; +} From 24100ad00ace078d7323351c96fde15072bc6d7b Mon Sep 17 00:00:00 2001 From: Skick Date: Tue, 28 Dec 2021 18:02:38 +0700 Subject: [PATCH 3/3] feat(Voice): use `Util#entersState` method Resolve #217 --- src/core/voice/DisTubeVoice.ts | 11 ++---- src/core/voice/__test__/DisTubeVoice.test.ts | 36 ++++++-------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/core/voice/DisTubeVoice.ts b/src/core/voice/DisTubeVoice.ts index 657c17b1..f5d3eec8 100644 --- a/src/core/voice/DisTubeVoice.ts +++ b/src/core/voice/DisTubeVoice.ts @@ -1,12 +1,11 @@ import { TypedEmitter } from "tiny-typed-emitter"; -import { DisTubeError, createDiscordJSAdapter, isSupportedVoiceChannel } from "../.."; +import { DisTubeError, createDiscordJSAdapter, entersState, isSupportedVoiceChannel } from "../.."; import { AudioPlayerStatus, VoiceConnectionDisconnectReason, VoiceConnectionStatus, createAudioPlayer, createAudioResource, - entersState, joinVoiceChannel, } from "@discordjs/voice"; import type { DisTubeStream, DisTubeVoiceEvents, DisTubeVoiceManager } from "../.."; @@ -125,16 +124,12 @@ export class DisTubeVoice extends TypedEmitter { * @param {Error} [error] Optional, an error to emit with 'error' event. */ leave(error?: Error) { - this.stop(); + this.stop(true); if (!this.isDisconnected) { this.emit("disconnect", error); this.isDisconnected = true; } - entersState(this.audioPlayer, AudioPlayerStatus.Idle, (this.audioResource?.silencePaddingFrames ?? 5) * 20) - .catch(() => this.stop(true)) - .finally(() => { - if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy(); - }); + if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy(); this.voices.delete(this.id); } /** diff --git a/src/core/voice/__test__/DisTubeVoice.test.ts b/src/core/voice/__test__/DisTubeVoice.test.ts index 9404d645..bd5b9313 100644 --- a/src/core/voice/__test__/DisTubeVoice.test.ts +++ b/src/core/voice/__test__/DisTubeVoice.test.ts @@ -10,8 +10,6 @@ jest.mock("@discordjs/voice"); const Util = _Util as unknown as jest.Mocked; const DiscordVoice = _DiscordVoice as unknown as jest.Mocked; -const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate); - const voiceManager = { add: jest.fn(), delete: jest.fn(), @@ -128,7 +126,7 @@ describe("Constructor", () => { (voice.emit as jest.Mock).mockClear(); expect(connection.on).nthCalledWith(1, DiscordVoice.VoiceConnectionStatus.Disconnected, expect.any(Function)); const catchFn = jest.fn(); - DiscordVoice.entersState.mockReturnValue({ catch: catchFn } as any); + Util.entersState.mockReturnValue({ catch: catchFn } as any); connection.on.mock.calls[0][1]( {}, { reason: DiscordVoice.VoiceConnectionDisconnectReason.WebSocketClose, closeCode: 4014 }, @@ -241,9 +239,9 @@ describe("Methods", () => { const TIMEOUT = 30e3; test("Timeout when signalling connection", async () => { - DiscordVoice.entersState.mockRejectedValue(undefined); + Util.entersState.mockRejectedValue(undefined); await expect(voice.join()).rejects.toThrow(new DisTubeError("VOICE_CONNECT_FAILED", TIMEOUT / 1e3)); - expect(DiscordVoice.entersState).toBeCalledWith(connection, DiscordVoice.VoiceConnectionStatus.Ready, TIMEOUT); + expect(Util.entersState).toBeCalledWith(connection, DiscordVoice.VoiceConnectionStatus.Ready, TIMEOUT); expect(connection.destroy).toBeCalledTimes(1); expect(voiceManager.delete).toBeCalledWith(voice.id); }); @@ -251,7 +249,7 @@ describe("Methods", () => { test("Timeout when connection destroyed", async () => { const newVC = { guild: { id: 2 } }; Util.isSupportedVoiceChannel.mockReturnValue(true); - DiscordVoice.entersState.mockRejectedValue(undefined); + Util.entersState.mockRejectedValue(undefined); connection.state.status = DiscordVoice.VoiceConnectionStatus.Destroyed; await expect(voice.join(newVC as any)).rejects.toThrow(new DisTubeError("VOICE_CONNECT_FAILED", TIMEOUT / 1e3)); expect(voice.channel).toBe(newVC); @@ -262,9 +260,9 @@ describe("Methods", () => { test("Joined a voice channel", async () => { Util.isSupportedVoiceChannel.mockReturnValue(true); - DiscordVoice.entersState.mockResolvedValue(undefined); + Util.entersState.mockResolvedValue(undefined); await expect(voice.join(voiceChannel as any)).resolves.toBe(voice); - expect(DiscordVoice.entersState).toBeCalledWith(connection, DiscordVoice.VoiceConnectionStatus.Ready, TIMEOUT); + expect(Util.entersState).toBeCalledWith(connection, DiscordVoice.VoiceConnectionStatus.Ready, TIMEOUT); expect(connection.destroy).not.toBeCalled(); expect(voiceManager.delete).not.toBeCalled(); expect(voice.channel).toBe(voiceChannel); @@ -274,35 +272,21 @@ describe("Methods", () => { describe("DisTubeVoice#leave()", () => { describe("Destroy the connection", () => { - test("Without error", async () => { - DiscordVoice.entersState.mockResolvedValueOnce(voice.audioPlayer); + test("Without error", () => { expect(voice.leave()).toBeUndefined(); - await flushPromises(); expect(audioPlayer.stop).toBeCalledTimes(1); + expect(audioPlayer.stop).toBeCalledWith(true); expect(connection.destroy).toBeCalledTimes(1); expect(voice.emit).toBeCalledWith("disconnect", undefined); expect(voiceManager.delete).toBeCalledWith(voice.id); }); - test("With error", async () => { - DiscordVoice.entersState.mockRejectedValueOnce(undefined); - voice.isDisconnected = false; - const err: any = {}; - expect(voice.leave(err)).toBeUndefined(); - await flushPromises(); - expect(audioPlayer.stop).toBeCalledTimes(2); - expect(audioPlayer.stop).nthCalledWith(2, true); - expect(connection.destroy).toBeCalledTimes(1); - expect(voice.emit).toBeCalledWith("disconnect", err); - expect(voiceManager.delete).toBeCalledWith(voice.id); - }); }); - test("Leave the destroyed connection", async () => { - DiscordVoice.entersState.mockResolvedValueOnce(voice.audioPlayer); + test("Leave the destroyed connection", () => { connection.state.status = DiscordVoice.VoiceConnectionStatus.Destroyed; expect(voice.leave()).toBeUndefined(); - await flushPromises(); expect(audioPlayer.stop).toBeCalledTimes(1); + expect(audioPlayer.stop).toBeCalledWith(true); expect(voice.emit).not.toBeCalled(); expect(connection.destroy).not.toBeCalled(); expect(voiceManager.delete).toBeCalledWith(voice.id);