chore(deps): update discord-api-types and /voice (#7934)

This commit is contained in:
A. Román
2022-05-18 19:54:35 +02:00
committed by GitHub
parent dfe449c253
commit 993eb74475
24 changed files with 54 additions and 626 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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}`);

View File

@@ -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"
},

View File

@@ -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<Snowflake, DiscordGatewayAdapterLibraryMethods>();
const trackedClients = new Set<Client>();
@@ -13,32 +17,32 @@ const trackedClients = new Set<Client>();
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<number, Set<Snowflake>>();
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;
}

View File

@@ -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');

View File

@@ -1,7 +0,0 @@
{
"root": true,
"extends": "../../.eslintrc.json",
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

View File

@@ -1,3 +0,0 @@
package-lock.json
auth.json
tsconfig.tsbuildinfo

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
{
"token": "Your Discord bot token here"
}

View File

@@ -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 <contact@shah.gg>",
"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"
}
}

View File

@@ -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<Snowflake, MusicSubscription>();
// 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<Track>).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);

View File

@@ -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<Track>).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<Track>).metadata.onStart();
}
},
);
this.audioPlayer.on('error', (error: { resource: any }) =>
(error.resource as AudioResource<Track>).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<void> {
// 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();
}
}
}

View File

@@ -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<AudioResource<Track>> {
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<Track, 'onStart' | 'onFinish' | 'onError'>): Promise<Track> {
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,
});
}
}

View File

@@ -1,3 +0,0 @@
{
"extends": "./tsconfig.json"
}

View File

@@ -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": [""]
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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<string>();
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);

View File

@@ -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';

View File

@@ -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,
},

View File

@@ -66,7 +66,7 @@ async function record(
async function leave(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
_client: Client,
connection?: VoiceConnection,
) {
if (connection) {

View File

@@ -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",

View File

@@ -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