diff --git a/packages/builders/package.json b/packages/builders/package.json index a796be733..d91f2262f 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -54,7 +54,7 @@ "dependencies": { "@sapphire/shapeshift": "^3.0.0", "@sindresorhus/is": "^4.6.0", - "discord-api-types": "^0.32.1", + "discord-api-types": "^0.33.0", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.1", "tslib": "^2.3.1" diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 35eedd910..8592762a0 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -52,7 +52,7 @@ "@discordjs/rest": "workspace:^", "@sapphire/snowflake": "^3.2.1", "@types/ws": "^8.5.3", - "discord-api-types": "^0.32.1", + "discord-api-types": "^0.33.0", "fast-deep-equal": "^3.1.3", "lodash.snakecase": "^4.1.1", "tslib": "^2.3.1", diff --git a/packages/discord.js/test/random.js b/packages/discord.js/test/random.js index 99ec1f69a..e33af44d7 100644 --- a/packages/discord.js/test/random.js +++ b/packages/discord.js/test/random.js @@ -2,9 +2,7 @@ 'use strict'; -const request = require('superagent'); -const ytdl = require('ytdl-core'); -const { token, song } = require('./auth.js'); +const { token } = require('./auth.js'); const { Client } = require('../src'); const { ChannelType, GatewayIntentBits } = require('discord-api-types/v10'); @@ -32,7 +30,7 @@ client.on('guildCreate', guild => // Fetch all members in a newly available guild client.on('guildUpdate', (oldGuild, newGuild) => !oldGuild.available && newGuild.available - ? guild.members.fetch().catch(err => console.log(`Failed to fetch all members: ${err}\n${err.stack}`)) + ? newGuild.members.fetch().catch(err => console.log(`Failed to fetch all members: ${err}\n${err.stack}`)) : Promise.resolve(), ); @@ -99,12 +97,10 @@ client.on('messageCreate', message => { } if (message.content.startsWith('botavatar')) { - request.get('url').end((err, res) => { - client.user - .setAvatar(res.body) - .catch(console.error) - .then(user => message.channel.send('Done!')); - }); + fetch('url') + .then(result => result.arrayBuffer()) + .then(buffer => client.user.setAvatar(buffer)) + .then(() => message.channel.send('Done!'), console.error); } if (message.content.startsWith('gn')) { @@ -200,34 +196,6 @@ client.on('messageCreate', msg => { } }); -let disp, con; - -client.on('messageCreate', msg => { - if (msg.content.startsWith('/play')) { - console.log('I am now going to play', msg.content); - const chan = msg.content.split(' ').slice(1).join(' '); - const s = ytdl(chan, { filter: 'audioonly' }, { passes: 3 }); - s.on('error', e => console.log(`e w stream 1 ${e}`)); - con.play(s); - } - if (msg.content.startsWith('/join')) { - const chan = msg.content.split(' ').slice(1).join(' '); - msg.channel.guild.channels.cache - .get(chan) - .join() - .then(conn => { - con = conn; - msg.channel.send('done'); - const s = ytdl(song, { filter: 'audioonly' }, { passes: 3 }); - s.on('error', e => console.log(`e w stream 2 ${e}`)); - disp = conn.playStream(s); - conn.player.on('debug', console.log); - conn.player.on('error', err => console.log(123, err)); - }) - .catch(console.error); - } -}); - client.on('messageReactionAdd', (reaction, user) => { if (reaction.message.channelId !== '222086648706498562') return; reaction.message.channel.send(`${user.username} added reaction ${reaction.emoji}, count is now ${reaction.count}`); diff --git a/packages/rest/package.json b/packages/rest/package.json index 447a9a081..3026219af 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -53,7 +53,7 @@ "@discordjs/collection": "workspace:^", "@sapphire/async-queue": "^1.3.1", "@sapphire/snowflake": "^3.2.1", - "discord-api-types": "^0.32.1", + "discord-api-types": "^0.33.0", "tslib": "^2.3.1", "undici": "^5.2.0" }, diff --git a/packages/voice/examples/basic/adapter.ts b/packages/voice/examples/basic/adapter.ts index 6a34e5fcc..6e7b1bf0c 100644 --- a/packages/voice/examples/basic/adapter.ts +++ b/packages/voice/examples/basic/adapter.ts @@ -1,6 +1,10 @@ import { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '../../'; -import { VoiceChannel, Snowflake, Client, Constants, Guild } from 'discord.js'; -import { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; +import { Snowflake, Client, Guild, VoiceBasedChannel, Status, Events } from 'discord.js'; +import { + GatewayDispatchEvents, + GatewayVoiceServerUpdateDispatchData, + GatewayVoiceStateUpdateDispatchData, +} from 'discord-api-types/v9'; const adapters = new Map(); const trackedClients = new Set(); @@ -13,32 +17,32 @@ const trackedClients = new Set(); function trackClient(client: Client) { if (trackedClients.has(client)) return; trackedClients.add(client); - client.ws.on(Constants.WSEvents.VOICE_SERVER_UPDATE, (payload: GatewayVoiceServerUpdateDispatchData) => { + client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (payload: GatewayVoiceServerUpdateDispatchData) => { adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload); }); - client.ws.on(Constants.WSEvents.VOICE_STATE_UPDATE, (payload: GatewayVoiceStateUpdateDispatchData) => { + client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (payload: GatewayVoiceStateUpdateDispatchData) => { if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) { adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload); } }); - client.on(Constants.Events.SHARD_DISCONNECT, (_, shardID) => { - const guilds = trackedShards.get(shardID); + client.on(Events.ShardDisconnect, (_, shardId) => { + const guilds = trackedShards.get(shardId); if (guilds) { for (const guildID of guilds.values()) { adapters.get(guildID)?.destroy(); } } - trackedShards.delete(shardID); + trackedShards.delete(shardId); }); } const trackedShards = new Map>(); function trackGuild(guild: Guild) { - let guilds = trackedShards.get(guild.shardID); + let guilds = trackedShards.get(guild.shardId); if (!guilds) { guilds = new Set(); - trackedShards.set(guild.shardID, guilds); + trackedShards.set(guild.shardId, guilds); } guilds.add(guild.id); } @@ -48,14 +52,14 @@ function trackGuild(guild: Guild) { * * @param channel - The channel to create the adapter for */ -export function createDiscordJSAdapter(channel: VoiceChannel): DiscordGatewayAdapterCreator { +export function createDiscordJSAdapter(channel: VoiceBasedChannel): DiscordGatewayAdapterCreator { return (methods) => { adapters.set(channel.guild.id, methods); trackClient(channel.client); trackGuild(channel.guild); return { sendPayload(data) { - if (channel.guild.shard.status === Constants.Status.READY) { + if (channel.guild.shard.status === Status.Ready) { channel.guild.shard.send(data); return true; } diff --git a/packages/voice/examples/basic/basic-example.ts b/packages/voice/examples/basic/basic-example.ts index def75222c..00222e956 100644 --- a/packages/voice/examples/basic/basic-example.ts +++ b/packages/voice/examples/basic/basic-example.ts @@ -1,4 +1,4 @@ -import { Client, VoiceChannel, Intents } from 'discord.js'; +import { Client, VoiceBasedChannel, VoiceChannel } from 'discord.js'; import { joinVoiceChannel, createAudioPlayer, @@ -9,6 +9,7 @@ import { VoiceConnectionStatus, } from '@discordjs/voice'; import { createDiscordJSAdapter } from './adapter'; +import { GatewayIntentBits } from 'discord-api-types/v9'; /** * In this example, we are creating a single audio player that plays to a number of voice channels. @@ -48,7 +49,7 @@ function playSong() { return entersState(player, AudioPlayerStatus.Playing, 5e3); } -async function connectToChannel(channel: VoiceChannel) { +async function connectToChannel(channel: VoiceBasedChannel) { /** * Here, we try to establish a connection to a voice channel. If we're already connected * to this voice channel, @discordjs/voice will just return the existing connection for us! @@ -94,7 +95,7 @@ async function connectToChannel(channel: VoiceChannel) { */ const client = new Client({ - ws: { intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] }, + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates], }); void client.login('token here'); diff --git a/packages/voice/examples/music-bot/.eslintrc.json b/packages/voice/examples/music-bot/.eslintrc.json deleted file mode 100644 index ab8cc9fa2..000000000 --- a/packages/voice/examples/music-bot/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "root": true, - "extends": "../../.eslintrc.json", - "parserOptions": { - "project": "./tsconfig.eslint.json" - } -} diff --git a/packages/voice/examples/music-bot/.gitignore b/packages/voice/examples/music-bot/.gitignore deleted file mode 100644 index 97fb878ea..000000000 --- a/packages/voice/examples/music-bot/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -package-lock.json -auth.json -tsconfig.tsbuildinfo diff --git a/packages/voice/examples/music-bot/README.md b/packages/voice/examples/music-bot/README.md deleted file mode 100644 index 22be695fe..000000000 --- a/packages/voice/examples/music-bot/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Music Bot Example - -This is an example of how to create a music bot using @discordjs/voice alongside [discord.js](https://github.com/discordjs/discord.js). - -The focus of this example is on how to create a robust music system using this library. The example explores error recovery, reconnection logic and implementation of a queue that won't lock up. - -If you're looking to make your own music bot that is fairly simple, this example is a great place to start. - -## Usage - -```bash -# Clone the main repository, and then run: -$ npm install -$ npm run build - -# Open this example and install dependencies -$ cd examples/music-bot -$ npm install - -# Set a bot token (see auth.example.json) -$ nano auth.json - -# Start the bot! -$ npm start -``` - -## Code structure - -The bot code has been separated from the code that is specific to @discordjs/voice as much as possible. Within `src/music`, you will find code that is specific to this library and you can take inspiration from this when building your own music system. - -On the other hand, `src/bot.ts` is discord.js-specific code that interacts with the music system above, as well as handling user commands given on Discord. This example uses a development build of Discord.js that supports slash commands. diff --git a/packages/voice/examples/music-bot/auth.example.json b/packages/voice/examples/music-bot/auth.example.json deleted file mode 100644 index 34e3fca00..000000000 --- a/packages/voice/examples/music-bot/auth.example.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "token": "Your Discord bot token here" -} diff --git a/packages/voice/examples/music-bot/package.json b/packages/voice/examples/music-bot/package.json deleted file mode 100644 index 9279a60d4..000000000 --- a/packages/voice/examples/music-bot/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "music-bot", - "version": "0.0.1", - "description": "An example music bot written using @discordjs/voice", - "scripts": { - "start": "npm run build && node -r tsconfig-paths/register dist/bot", - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint src --ext .ts", - "lint:fix": "eslint src --ext .ts --fix", - "prettier": "prettier --write .", - "build": "tsc", - "build:check": "tsc --noEmit --incremental false" - }, - "author": "Amish Shah ", - "license": "MIT", - "dependencies": { - "@discordjs/opus": "^0.5.0", - "discord-api-types": "^0.19.0", - "discord.js": "^13.0.0-dev.328501b.1626912223", - "libsodium-wrappers": "^0.7.9", - "youtube-dl-exec": "^1.2.4", - "ytdl-core": "^4.8.3" - }, - "devDependencies": { - "tsconfig-paths": "^3.9.0", - "typescript": "~4.2.2" - } -} diff --git a/packages/voice/examples/music-bot/src/bot.ts b/packages/voice/examples/music-bot/src/bot.ts deleted file mode 100644 index 5bdd01558..000000000 --- a/packages/voice/examples/music-bot/src/bot.ts +++ /dev/null @@ -1,188 +0,0 @@ -import Discord, { Interaction, GuildMember, Snowflake } from 'discord.js'; -import { - AudioPlayerStatus, - AudioResource, - entersState, - joinVoiceChannel, - VoiceConnectionStatus, -} from '@discordjs/voice'; -import { Track } from './music/track'; -import { MusicSubscription } from './music/subscription'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports -const { token } = require('../auth.json'); - -const client = new Discord.Client({ intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS'] }); - -client.on('ready', () => console.log('Ready!')); - -// This contains the setup code for creating slash commands in a guild. The owner of the bot can send "!deploy" to create them. -client.on('messageCreate', async (message) => { - if (!message.guild) return; - if (!client.application?.owner) await client.application?.fetch(); - - if (message.content.toLowerCase() === '!deploy' && message.author.id === client.application?.owner?.id) { - await message.guild.commands.set([ - { - name: 'play', - description: 'Plays a song', - options: [ - { - name: 'song', - type: 'STRING' as const, - description: 'The URL of the song to play', - required: true, - }, - ], - }, - { - name: 'skip', - description: 'Skip to the next song in the queue', - }, - { - name: 'queue', - description: 'See the music queue', - }, - { - name: 'pause', - description: 'Pauses the song that is currently playing', - }, - { - name: 'resume', - description: 'Resume playback of the current song', - }, - { - name: 'leave', - description: 'Leave the voice channel', - }, - ]); - - await message.reply('Deployed!'); - } -}); - -/** - * Maps guild IDs to music subscriptions, which exist if the bot has an active VoiceConnection to the guild. - */ -const subscriptions = new Map(); - -// Handles slash command interactions -client.on('interactionCreate', async (interaction: Interaction) => { - if (!interaction.isCommand() || !interaction.guildId) return; - let subscription = subscriptions.get(interaction.guildId); - - if (interaction.commandName === 'play') { - await interaction.defer(); - // Extract the video URL from the command - const url = interaction.options.get('song')!.value! as string; - - // If a connection to the guild doesn't already exist and the user is in a voice channel, join that channel - // and create a subscription. - if (!subscription) { - if (interaction.member instanceof GuildMember && interaction.member.voice.channel) { - const channel = interaction.member.voice.channel; - subscription = new MusicSubscription( - joinVoiceChannel({ - channelId: channel.id, - guildId: channel.guild.id, - adapterCreator: channel.guild.voiceAdapterCreator, - }), - ); - subscription.voiceConnection.on('error', console.warn); - subscriptions.set(interaction.guildId, subscription); - } - } - - // If there is no subscription, tell the user they need to join a channel. - if (!subscription) { - await interaction.followUp('Join a voice channel and then try that again!'); - return; - } - - // Make sure the connection is ready before processing the user's request - try { - await entersState(subscription.voiceConnection, VoiceConnectionStatus.Ready, 20e3); - } catch (error) { - console.warn(error); - await interaction.followUp('Failed to join voice channel within 20 seconds, please try again later!'); - return; - } - - try { - // Attempt to create a Track from the user's video URL - const track = await Track.from(url, { - onStart() { - interaction.followUp({ content: 'Now playing!', ephemeral: true }).catch(console.warn); - }, - onFinish() { - interaction.followUp({ content: 'Now finished!', ephemeral: true }).catch(console.warn); - }, - onError(error) { - console.warn(error); - interaction.followUp({ content: `Error: ${error.message}`, ephemeral: true }).catch(console.warn); - }, - }); - // Enqueue the track and reply a success message to the user - subscription.enqueue(track); - await interaction.followUp(`Enqueued **${track.title}**`); - } catch (error) { - console.warn(error); - await interaction.followUp('Failed to play track, please try again later!'); - } - } else if (interaction.commandName === 'skip') { - if (subscription) { - // Calling .stop() on an AudioPlayer causes it to transition into the Idle state. Because of a state transition - // listener defined in music/subscription.ts, transitions into the Idle state mean the next track from the queue - // will be loaded and played. - subscription.audioPlayer.stop(); - await interaction.reply('Skipped song!'); - } else { - await interaction.reply('Not playing in this server!'); - } - } else if (interaction.commandName === 'queue') { - // Print out the current queue, including up to the next 5 tracks to be played. - if (subscription) { - const current = - subscription.audioPlayer.state.status === AudioPlayerStatus.Idle - ? `Nothing is currently playing!` - : `Playing **${(subscription.audioPlayer.state.resource as AudioResource).metadata.title}**`; - - const queue = subscription.queue - .slice(0, 5) - .map((track, index) => `${index + 1}) ${track.title}`) - .join('\n'); - - await interaction.reply(`${current}\n\n${queue}`); - } else { - await interaction.reply('Not playing in this server!'); - } - } else if (interaction.commandName === 'pause') { - if (subscription) { - subscription.audioPlayer.pause(); - await interaction.reply({ content: `Paused!`, ephemeral: true }); - } else { - await interaction.reply('Not playing in this server!'); - } - } else if (interaction.commandName === 'resume') { - if (subscription) { - subscription.audioPlayer.unpause(); - await interaction.reply({ content: `Unpaused!`, ephemeral: true }); - } else { - await interaction.reply('Not playing in this server!'); - } - } else if (interaction.commandName === 'leave') { - if (subscription) { - subscription.voiceConnection.destroy(); - subscriptions.delete(interaction.guildId); - await interaction.reply({ content: `Left channel!`, ephemeral: true }); - } else { - await interaction.reply('Not playing in this server!'); - } - } else { - await interaction.reply('Unknown command'); - } -}); - -client.on('error', console.warn); - -void client.login(token); diff --git a/packages/voice/examples/music-bot/src/music/subscription.ts b/packages/voice/examples/music-bot/src/music/subscription.ts deleted file mode 100644 index e17091900..000000000 --- a/packages/voice/examples/music-bot/src/music/subscription.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { - AudioPlayer, - AudioPlayerStatus, - AudioResource, - createAudioPlayer, - entersState, - VoiceConnection, - VoiceConnectionDisconnectReason, - VoiceConnectionStatus, -} from '@discordjs/voice'; -import type { Track } from './track'; -import { promisify } from 'node:util'; - -const wait = promisify(setTimeout); - -/** - * A MusicSubscription exists for each active VoiceConnection. Each subscription has its own audio player and queue, - * and it also attaches logic to the audio player and voice connection for error handling and reconnection logic. - */ -export class MusicSubscription { - public readonly voiceConnection: VoiceConnection; - public readonly audioPlayer: AudioPlayer; - public queue: Track[]; - public queueLock = false; - public readyLock = false; - - public constructor(voiceConnection: VoiceConnection) { - this.voiceConnection = voiceConnection; - this.audioPlayer = createAudioPlayer(); - this.queue = []; - - this.voiceConnection.on( - 'stateChange', - async (_: any, newState: { status: any; reason: any; closeCode: number }) => { - if (newState.status === VoiceConnectionStatus.Disconnected) { - if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { - /** - * If the WebSocket closed with a 4014 code, this means that we should not manually attempt to reconnect, - * but there is a chance the connection will recover itself if the reason of the disconnect was due to - * switching voice channels. This is also the same code for the bot being kicked from the voice channel, - * so we allow 5 seconds to figure out which scenario it is. If the bot has been kicked, we should destroy - * the voice connection. - */ - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, 5_000); - // Probably moved voice channel - } catch { - this.voiceConnection.destroy(); - // Probably removed from voice channel - } - } else if (this.voiceConnection.rejoinAttempts < 5) { - /** - * The disconnect in this case is recoverable, and we also have <5 repeated attempts so we will reconnect. - */ - await wait((this.voiceConnection.rejoinAttempts + 1) * 5_000); - this.voiceConnection.rejoin(); - } else { - /** - * The disconnect in this case may be recoverable, but we have no more remaining attempts - destroy. - */ - this.voiceConnection.destroy(); - } - } else if (newState.status === VoiceConnectionStatus.Destroyed) { - /** - * Once destroyed, stop the subscription. - */ - this.stop(); - } else if ( - !this.readyLock && - (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling) - ) { - /** - * In the Signalling or Connecting states, we set a 20 second time limit for the connection to become ready - * before destroying the voice connection. This stops the voice connection permanently existing in one of these - * states. - */ - this.readyLock = true; - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20_000); - } catch { - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy(); - } finally { - this.readyLock = false; - } - } - }, - ); - - // Configure audio player - this.audioPlayer.on( - 'stateChange', - (oldState: { status: any; resource: any }, newState: { status: any; resource: any }) => { - if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { - // If the Idle state is entered from a non-Idle state, it means that an audio resource has finished playing. - // The queue is then processed to start playing the next track, if one is available. - (oldState.resource as AudioResource).metadata.onFinish(); - void this.processQueue(); - } else if (newState.status === AudioPlayerStatus.Playing) { - // If the Playing state has been entered, then a new track has started playback. - (newState.resource as AudioResource).metadata.onStart(); - } - }, - ); - - this.audioPlayer.on('error', (error: { resource: any }) => - (error.resource as AudioResource).metadata.onError(error), - ); - - voiceConnection.subscribe(this.audioPlayer); - } - - /** - * Adds a new Track to the queue. - * - * @param track The track to add to the queue - */ - public enqueue(track: Track) { - this.queue.push(track); - void this.processQueue(); - } - - /** - * Stops audio playback and empties the queue. - */ - public stop() { - this.queueLock = true; - this.queue = []; - this.audioPlayer.stop(true); - } - - /** - * Attempts to play a Track from the queue. - */ - private async processQueue(): Promise { - // If the queue is locked (already being processed), is empty, or the audio player is already playing something, return - if (this.queueLock || this.audioPlayer.state.status !== AudioPlayerStatus.Idle || this.queue.length === 0) { - return; - } - // Lock the queue to guarantee safe access - this.queueLock = true; - - // Take the first item from the queue. This is guaranteed to exist due to the non-empty check above. - const nextTrack = this.queue.shift()!; - try { - // Attempt to convert the Track into an AudioResource (i.e. start streaming the video) - const resource = await nextTrack.createAudioResource(); - this.audioPlayer.play(resource); - this.queueLock = false; - } catch (error) { - // If an error occurred, try the next item of the queue instead - nextTrack.onError(error as Error); - this.queueLock = false; - return this.processQueue(); - } - } -} diff --git a/packages/voice/examples/music-bot/src/music/track.ts b/packages/voice/examples/music-bot/src/music/track.ts deleted file mode 100644 index 9ad09e2d5..000000000 --- a/packages/voice/examples/music-bot/src/music/track.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { getInfo } from 'ytdl-core'; -import { AudioResource, createAudioResource, demuxProbe } from '@discordjs/voice'; -import { raw as ytdl } from 'youtube-dl-exec'; - -/** - * This is the data required to create a Track object. - */ -export interface TrackData { - url: string; - title: string; - onStart: () => void; - onFinish: () => void; - onError: (error: Error) => void; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; - -/** - * A Track represents information about a YouTube video (in this context) that can be added to a queue. - * It contains the title and URL of the video, as well as functions onStart, onFinish, onError, that act - * as callbacks that are triggered at certain points during the track's lifecycle. - * - * Rather than creating an AudioResource for each video immediately and then keeping those in a queue, - * we use tracks as they don't pre-emptively load the videos. Instead, once a Track is taken from the - * queue, it is converted into an AudioResource just in time for playback. - */ -export class Track implements TrackData { - public readonly url: string; - public readonly title: string; - public readonly onStart: () => void; - public readonly onFinish: () => void; - public readonly onError: (error: Error) => void; - - private constructor({ url, title, onStart, onFinish, onError }: TrackData) { - this.url = url; - this.title = title; - this.onStart = onStart; - this.onFinish = onFinish; - this.onError = onError; - } - - /** - * Creates an AudioResource from this Track. - */ - public createAudioResource(): Promise> { - return new Promise((resolve, reject) => { - const process = ytdl( - this.url, - { - o: '-', - q: '', - f: 'bestaudio[ext=webm+acodec=opus+asr=48000]/bestaudio', - r: '100K', - }, - { stdio: ['ignore', 'pipe', 'ignore'] }, - ); - if (!process.stdout) { - reject(new Error('No stdout')); - return; - } - const stream = process.stdout; - const onError = (error: Error) => { - if (!process.killed) process.kill(); - stream.resume(); - reject(error); - }; - process - .once('spawn', () => { - demuxProbe(stream) - .then((probe: { stream: any; type: any }) => - resolve(createAudioResource(probe.stream, { metadata: this, inputType: probe.type })), - ) - .catch(onError); - }) - .catch(onError); - }); - } - - /** - * Creates a Track from a video URL and lifecycle callback methods. - * - * @param url The URL of the video - * @param methods Lifecycle callbacks - * - * @returns The created Track - */ - public static async from(url: string, methods: Pick): Promise { - const info = await getInfo(url); - - // The methods are wrapped so that we can ensure that they are only called once. - const wrappedMethods = { - onStart() { - wrappedMethods.onStart = noop; - methods.onStart(); - }, - onFinish() { - wrappedMethods.onFinish = noop; - methods.onFinish(); - }, - onError(error: Error) { - wrappedMethods.onError = noop; - methods.onError(error); - }, - }; - - return new Track({ - title: info.videoDetails.title, - url, - ...wrappedMethods, - }); - } -} diff --git a/packages/voice/examples/music-bot/tsconfig.eslint.json b/packages/voice/examples/music-bot/tsconfig.eslint.json deleted file mode 100644 index ea6be8e9a..000000000 --- a/packages/voice/examples/music-bot/tsconfig.eslint.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./tsconfig.json" -} diff --git a/packages/voice/examples/music-bot/tsconfig.json b/packages/voice/examples/music-bot/tsconfig.json deleted file mode 100644 index 97782611a..000000000 --- a/packages/voice/examples/music-bot/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": ".", - "outDir": "dist", - "rootDir": "src", - "paths": { - "@discordjs/voice": ["../../"], - "@discordjs/opus": ["./node_modules/@discordjs/opus"], - "sodium": ["./node_modules/sodium"], - "libsodium-wrappers": ["./node_modules/libsodium-wrappers"], - "tweetnacl": ["./node_modules/tweetnacl"] - } - }, - "include": ["src/**/*.ts"], - "exclude": [""] -} diff --git a/packages/voice/examples/radio-bot/package.json b/packages/voice/examples/radio-bot/package.json index aaa10da2f..1cafafad5 100644 --- a/packages/voice/examples/radio-bot/package.json +++ b/packages/voice/examples/radio-bot/package.json @@ -19,7 +19,7 @@ "license": "MIT", "dependencies": { "@discordjs/voice": "file:../../", - "discord.js": "^13.0.0-dev.f7eeccba4b7015496df811f10cc2da2b0fab0630", + "discord.js": "^13.7.0", "libsodium-wrappers": "^0.7.9", "module-alias": "^2.2.2", "prism-media": "^1.3.1" diff --git a/packages/voice/examples/recorder/package.json b/packages/voice/examples/recorder/package.json index 20b87f5c6..336388063 100644 --- a/packages/voice/examples/recorder/package.json +++ b/packages/voice/examples/recorder/package.json @@ -15,8 +15,8 @@ "license": "MIT", "dependencies": { "@discordjs/opus": "^0.5.3", - "discord-api-types": "^0.22.0", - "discord.js": "^13.0.1", + "discord-api-types": "^0.30.0", + "discord.js": "^13.7.0", "libsodium-wrappers": "^0.7.9", "node-crc": "^1.3.2", "prism-media": "^2.0.0-alpha.0" diff --git a/packages/voice/examples/recorder/src/bot.ts b/packages/voice/examples/recorder/src/bot.ts index 498dacaf6..a012f17e9 100644 --- a/packages/voice/examples/recorder/src/bot.ts +++ b/packages/voice/examples/recorder/src/bot.ts @@ -1,16 +1,19 @@ -import Discord, { Interaction } from 'discord.js'; +import Discord, { Events, Interaction } from 'discord.js'; import { getVoiceConnection } from '@discordjs/voice'; import { deploy } from './deploy'; import { interactionHandlers } from './interactions'; +import { GatewayIntentBits } from 'discord-api-types/v9'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const { token } = require('../auth.json'); -const client = new Discord.Client({ intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS'] }); +const client = new Discord.Client({ + intents: [GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds], +}); -client.on('ready', () => console.log('Ready!')); +client.on(Events.ClientReady, () => console.log('Ready!')); -client.on('messageCreate', async (message) => { +client.on(Events.MessageCreate, async (message) => { if (!message.guild) return; if (!client.application?.owner) await client.application?.fetch(); @@ -25,7 +28,7 @@ client.on('messageCreate', async (message) => { */ const recordable = new Set(); -client.on('interactionCreate', async (interaction: Interaction) => { +client.on(Events.InteractionCreate, async (interaction: Interaction) => { if (!interaction.isCommand() || !interaction.guildId) return; const handler = interactionHandlers.get(interaction.commandName); @@ -41,6 +44,6 @@ client.on('interactionCreate', async (interaction: Interaction) => { } }); -client.on('error', console.warn); +client.on(Events.Error, console.warn); void client.login(token); diff --git a/packages/voice/examples/recorder/src/createListeningStream.ts b/packages/voice/examples/recorder/src/createListeningStream.ts index 7232ffdc0..63bb6af73 100644 --- a/packages/voice/examples/recorder/src/createListeningStream.ts +++ b/packages/voice/examples/recorder/src/createListeningStream.ts @@ -1,5 +1,5 @@ import { EndBehaviorType, VoiceReceiver } from '@discordjs/voice'; -import { User } from 'discord.js'; +import type { User } from 'discord.js'; import { createWriteStream } from 'node:fs'; import prism from 'prism-media'; import { pipeline } from 'node:stream'; diff --git a/packages/voice/examples/recorder/src/deploy.ts b/packages/voice/examples/recorder/src/deploy.ts index b8c414e74..ce1166ae9 100644 --- a/packages/voice/examples/recorder/src/deploy.ts +++ b/packages/voice/examples/recorder/src/deploy.ts @@ -1,4 +1,5 @@ -import { Guild } from 'discord.js'; +import { ApplicationCommandOptionType } from 'discord-api-types/v9'; +import type { Guild } from 'discord.js'; export const deploy = async (guild: Guild) => { await guild.commands.set([ @@ -12,7 +13,7 @@ export const deploy = async (guild: Guild) => { options: [ { name: 'speaker', - type: 'USER' as const, + type: ApplicationCommandOptionType.User, description: 'The user to record', required: true, }, diff --git a/packages/voice/examples/recorder/src/interactions.ts b/packages/voice/examples/recorder/src/interactions.ts index d27bcd385..55610ca2b 100644 --- a/packages/voice/examples/recorder/src/interactions.ts +++ b/packages/voice/examples/recorder/src/interactions.ts @@ -66,7 +66,7 @@ async function record( async function leave( interaction: CommandInteraction, recordable: Set, - client: Client, + _client: Client, connection?: VoiceConnection, ) { if (connection) { diff --git a/packages/voice/package.json b/packages/voice/package.json index 86b705734..beec881ba 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -51,7 +51,7 @@ "homepage": "https://discord.js.org", "dependencies": { "@types/ws": "^8.5.3", - "discord-api-types": "^0.32.1", + "discord-api-types": "^0.33.0", "prism-media": "^1.3.2", "tiny-typed-emitter": "^2.1.0", "tslib": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index 0b8180c5a..7c0c4e474 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1830,7 +1830,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.19.0 "@typescript-eslint/parser": ^5.19.0 babel-plugin-transform-typescript-metadata: ^0.3.2 - discord-api-types: ^0.32.1 + discord-api-types: ^0.33.0 eslint: ^8.13.0 eslint-config-marine: ^9.4.1 eslint-config-prettier: ^8.5.0 @@ -1916,7 +1916,7 @@ __metadata: "@typescript-eslint/parser": ^5.19.0 babel-plugin-const-enum: ^1.2.0 babel-plugin-transform-typescript-metadata: ^0.3.2 - discord-api-types: ^0.32.1 + discord-api-types: ^0.33.0 eslint: ^8.13.0 eslint-config-marine: ^9.4.1 eslint-config-prettier: ^8.5.0 @@ -1954,7 +1954,7 @@ __metadata: "@types/ws": ^8.5.3 "@typescript-eslint/eslint-plugin": ^5.19.0 "@typescript-eslint/parser": ^5.19.0 - discord-api-types: ^0.32.1 + discord-api-types: ^0.33.0 eslint: ^8.13.0 eslint-config-marine: ^9.4.1 eslint-config-prettier: ^8.5.0 @@ -4490,10 +4490,10 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.32.1": - version: 0.32.1 - resolution: "discord-api-types@npm:0.32.1" - checksum: cce4fa81a4c8311b72cf9f23ff9a3d16e40ba7d862a7634717d9437e0e5918209a8eeb75ea9974f24e3761b06e4599757e7544343b702a23f04b07f31884c304 +"discord-api-types@npm:^0.33.0": + version: 0.33.0 + resolution: "discord-api-types@npm:0.33.0" + checksum: 8ae2c7e36c34e1d250acab18f8d2941be6135b4e08e48f9c0f584b23a59e8b310ec33f78a46e4aa8294639ec0b5703162f1895eb0f446c70559ae4e69cd26b16 languageName: node linkType: hard @@ -4508,7 +4508,7 @@ __metadata: "@sapphire/snowflake": ^3.2.1 "@types/node": ^16.11.27 "@types/ws": ^8.5.3 - discord-api-types: ^0.32.1 + discord-api-types: ^0.33.0 dtslint: ^4.2.1 eslint: ^8.13.0 eslint-config-prettier: ^8.5.0