Merge pull request #3 from BiRabittoh/typescript Migrate to TypeScript
jump to
@@ -7,15 +7,20 @@ "configurations": [
{ "type": "node", "request": "launch", - "name": "Launch Program", - "skipFiles": [ - "<node_internals>/**" - ], - "program": "${workspaceFolder}/index.js" + "name": "Launch", + "program": "${workspaceFolder}/build/index.js", + }, + { + "type": "node", + "request": "launch", + "name": "Build & launch", + "program": "${workspaceFolder}/src/index.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/build/**/*.js"] }, { "name": "Deploy commands", - "program": "${workspaceFolder}/tools/deploy-commands.js", + "program": "${workspaceFolder}/build/tools/deploy-commands.js", "request": "launch", "skipFiles": [ "<node_internals>/**"@@ -24,7 +29,7 @@ "type": "node"
}, { "name": "Delete commands", - "program": "${workspaceFolder}/tools/delete-commands.js", + "program": "${workspaceFolder}/build/tools/delete-commands.js", "request": "launch", "skipFiles": [ "<node_internals>/**"
@@ -0,0 +1,17 @@
+{ + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": [ + "$tsc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "label": "tsc: build - tsconfig.json" + } + ] +}
@@ -0,0 +1,21 @@
+MIT License + +Copyright (c) 2024 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
@@ -1,7 +1,7 @@
# Simple Discord Music Bot ### Why? -I wanted to experiment with [discord.js](). +I wanted to experiment with [discord.js](https://discord.js.org/). ### Will you add \<feature>? Nah, this is just a proof of concept.@@ -22,6 +22,11 @@ ```
npm run deploy-commands ``` This will also give you a link to invite the bot to any server. + +Build the project: +``` +npm run build +``` Finally, type this to start the bot: ```
@@ -1,19 +0,0 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { getChannel, clearQueue } = require('../functions/music'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('clear') - .setDescription('Clear the queue.'), - - async execute(interaction) { - const channel = await getChannel(interaction); - if (typeof channel == 'string') - return await interaction.reply({ content: channel, ephemeral: true }); - - if (clearQueue()) - return await interaction.reply({ content: 'Queue cleared.' }); - - return await interaction.reply({ content: 'Error.' }); - }, -};
@@ -1,9 +1,9 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { playOutro, getChannel } = require('../functions/music'); -const path = require('node:path'); +import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { playOutro, getChannel } from '../functions/music'; +import path from 'node:path'; const { outros } = require(path.join(process.cwd(), 'config.json')); -function getOutroUrl(outro) { +function getOutroUrl(outro: string): string { if (outro) return outro; const randomIndex = Math.floor(Math.random() * (outros.length)); return outros[randomIndex].value;@@ -22,9 +22,9 @@ .addStringOption(option =>
option.setName('kick') .setDescription('Do you actually want to log off?') .setRequired(false) - .addChoices({ name: 'Yes', value: 'true', default: 'true' }, { name: 'No', value: 'false' })), + .addChoices({ name: 'Yes', value: 'true' }, { name: 'No', value: 'false' })), - async execute(interaction) { + async execute(interaction: ChatInputCommandInteraction) { const channel = await getChannel(interaction); if (typeof channel == 'string') return await interaction.reply({ content: channel, ephemeral: true });@@ -35,7 +35,9 @@ const outroUrl = getOutroUrl(outro);
await playOutro(outroUrl, channel); if (kick !== 'false') { - setTimeout(() => interaction.member.voice.disconnect(), 20_000); + const member = interaction.member; + if ("voice" in member) + setTimeout(() => member.voice.disconnect(), 20_000); return await interaction.reply({ content: 'Prepare for takeoff!', ephemeral: true }); } return await interaction.reply({ content: 'Playing outro.', ephemeral: true });
@@ -1,52 +0,0 @@
-const { SlashCommandBuilder } = require('discord.js'); -const play = require('play-dl'); -const { playUrls, getChannel } = require('../functions/music'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('play') - .setDescription('Play something off YouTube.') - .addStringOption((option) => option - .setName('query') - .setDescription('YouTube URL or search query') - .setRequired(true), - ), - - async execute(interaction) { - const channel = await getChannel(interaction); - if (typeof channel == 'string') - return await interaction.reply({ content: channel, ephemeral: true }); - - await interaction.deferReply(); - const url = interaction.options.getString('query'); - - let video, yt_info; - switch (play.yt_validate(url)) { - case 'video': - playUrls([url], channel); - return await interaction.editReply(`Added ${url} to queue.`); - - case 'search': - yt_info = await play.search(url, { source: { youtube: 'video' }, limit: 1 }); - - if (yt_info.length === 0) - return await interaction.editReply('No results found.'); - - video = yt_info[0]; - playUrls([video.url], channel); - return await interaction.editReply(`Added ${video.url} to queue.`); - - case 'playlist': - const playlist = await play.playlist_info(url, { incomplete : true }); - const videos = await playlist.all_videos(); - const urls = videos.map((e) => e.url); - result = await playUrls(urls, channel); - if (result) - return await interaction.editReply(`Added ${urls.length} videos from the following playlist: ${playlist.title}.`); - else - return await interaction.editReply(`Could not add playlist.`); - default: - return await interaction.editReply('Not supported.'); - } - }, -};
@@ -1,21 +1,22 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { getChannel, getQueue } = require('../functions/music'); +import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { formatTitle, getChannel, getQueue } from '../functions/music'; +import { YouTubeVideo } from 'play-dl'; const CHARACTER_LIMIT_API = 2000; -function getReply(result) { - const nowPlaying = "Now playing: " + result.shift() +function getReply(result: YouTubeVideo[]): string { + const nowPlaying = "Now playing: " + formatTitle(result.shift()); if (!result.length) { return nowPlaying; } let reply = "Queue:" - let new_string = ""; const characterLimit = CHARACTER_LIMIT_API - nowPlaying.length - 6; // 4 chars for "\n...", 2 chars for "\n\n" - for (r in result) { - new_string = "\n" + (r + 1) + ". <" + result[r] + ">"; + for (let r in result) { + const video = result[r]; + const new_string = `\n${r + 1}. ${formatTitle(video)}`; if (reply.length + new_string.length > characterLimit) { reply += "\n..."; break;@@ -30,14 +31,14 @@ data: new SlashCommandBuilder()
.setName('queue') .setDescription('Show current queue status.'), - async execute(interaction) { + async execute(interaction: ChatInputCommandInteraction) { const channel = await getChannel(interaction); if (typeof channel == 'string') return await interaction.reply({ content: channel, ephemeral: true }); const result = await getQueue(); if (result) { - reply = getReply(result); + const reply = getReply(result); return await interaction.reply({ content: reply }); } return await interaction.reply({ content: 'Queue is empty.' });
@@ -1,29 +0,0 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { playStream, getChannel } = require('../functions/music'); -const path = require('node:path'); -const { radios } = require(path.join(process.cwd(), 'config.json')); - -module.exports = { - data: new SlashCommandBuilder() - .setName('radio') - .setDescription('Play a custom-defined webradio URL.') - .addStringOption(option => - option.setName('which') - .setDescription('Select which radio to play') - .setRequired(false) - .addChoices(...radios)), - - async execute(interaction) { - const channel = await getChannel(interaction); - if (typeof channel == 'string') - return await interaction.reply({ content: channel, ephemeral: true }); - - await interaction.deferReply(); - - const radio = interaction.options.getString('which'); - const streamUrl = radio ? radio : radios[0].value; - - playStream(streamUrl, channel); - return await interaction.editReply('Playing web radio.'); - }, -};
@@ -1,12 +1,12 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { getChannel, skipMusic } = require('../functions/music'); +import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { getChannel, skipMusic } from '../functions/music'; module.exports = { data: new SlashCommandBuilder() .setName('skip') .setDescription('Skip current track.'), - async execute(interaction) { + async execute(interaction: ChatInputCommandInteraction) { const channel = await getChannel(interaction); if (typeof channel == 'string') return await interaction.reply({ content: channel, ephemeral: true });
@@ -1,12 +1,12 @@
-const { SlashCommandBuilder } = require('discord.js'); -const { getChannel, stopMusic } = require('../functions/music'); +import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { getChannel, stopMusic } from '../functions/music'; module.exports = { data: new SlashCommandBuilder() .setName('stop') .setDescription('Stop the music.'), - async execute(interaction) { + async execute(interaction: ChatInputCommandInteraction) { const channel = await getChannel(interaction); if (typeof channel == 'string') return await interaction.reply({ content: channel, ephemeral: true });
@@ -7,12 +7,5 @@ { "name": "TheFatRat - Xenogenesis", "value": "https://www.youtube.com/watch?v=6N8zvi1VNSc" },
{ "name": "OMFG - Hello", "value": "https://www.youtube.com/watch?v=5nYVNTX0Ib8" }, { "name": "Pegboard Nerds - Disconnected", "value": "https://www.youtube.com/watch?v=YdBtx8qG68w" }, { "name": "Gym Class Heroes - Stereo Hearts", "value": "https://www.youtube.com/watch?v=ThctmvQ3NGk" } - ], - "radios": - [ - { "name": "EusariRadio", "value": "https://onair7.xdevel.com/proxy/xautocloud_cnou_1049?mp=/;stream/" }, - { "name": "Radio 24", "value": "https://shoutcast3.radio24.ilsole24ore.com/stream.mp3" }, - { "name": "Radio Delfino", "value": "https://nr8.newradio.it/proxy/emaamo00?mp=/stream?ext=.mp3" }, - { "name": "Radio 105", "value": "https://icy.unitedradio.it/Radio105.mp3" } ] }
@@ -1,69 +0,0 @@
-const { - createAudioResource, - joinVoiceChannel, - AudioPlayerStatus, -} = require('@discordjs/voice'); -const play = require('play-dl'); -const { MyQueue } = require('./myqueue') - -const q = new MyQueue(); - -function getChannelConnection(channel) { - const guild = channel.guild; - return joinVoiceChannel({ - channelId: channel.id, - guildId: guild.id, - adapterCreator: guild.voiceAdapterCreator - }) -} - -module.exports = { - async playUrls(urls, channel) { - if (!channel) { - console.log('Channel error:', channel); - return; - } - - q.connection = getChannelConnection(channel); - return q.addArray(urls); - }, - async playStream(url, channel) { - if (!channel) { - console.log('Channel error:', channel); - return; - } - q.connection = getChannelConnection(channel); - q.add(createAudioResource(url, { inputType: 'mp3' })); - }, - async playOutro(url, channel) { - if (!channel) { - console.log('Channel error:', channel); - return; - } - q.connection = getChannelConnection(channel); - q.outro(url); - }, - async getChannel(interaction) { - const member = interaction.member; - if (!member) - return 'Please use this in your current server.'; - - const channel = member.voice.channel; - if (!channel) - return 'You\'re not in a voice channel.'; - - return channel; - }, - async stopMusic() { - return q.stop(); - }, - async skipMusic() { - return q.next(); - }, - async getQueue() { - return q.queue; - }, - clearQueue() { - return q.clear(); - } -};
@@ -1,18 +1,17 @@
-const { - createAudioResource, - createAudioPlayer, - AudioPlayerStatus -} = require('@discordjs/voice'); -const play = require('play-dl'); +import { createAudioResource, createAudioPlayer, NoSubscriberBehavior, AudioPlayerStatus, VoiceConnection, AudioPlayer } from '@discordjs/voice'; +import play, { YouTubeVideo } from 'play-dl'; -async function resourceFromUrl(url) { +async function resourceFromYTUrl(url: string) { const stream = await play.stream(url); return createAudioResource(stream.stream, { inputType: stream.type }) } -class MyQueue { - #nowPlaying = null; - #queue = Array(); +export default class MyQueue { + #nowPlaying: YouTubeVideo; + #queue: Array<YouTubeVideo>; + connection: VoiceConnection; + player: AudioPlayer; + static _instance: MyQueue; get queue() { if (this.#nowPlaying) return [this.#nowPlaying].concat(this.#queue);@@ -25,19 +24,19 @@ return MyQueue._instance
} MyQueue._instance = this; - this.#nowPlaying = ""; + this.#nowPlaying = null; this.connection = null; - this.#queue = Array(); - this.player = createAudioPlayer(); + this.#queue = Array<YouTubeVideo>(); + this.player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Stop } }); this.player.on(AudioPlayerStatus.Idle, () => { if (this.#queue.length > 0) return this.next(); - this.player.subscribers.forEach((e) => e.connection.disconnect()); + this.connection.disconnect(); }); } clear() { - this.#queue = Array(); + this.#queue = Array<YouTubeVideo>(); } stop() {@@ -51,23 +50,23 @@ async next() {
if (this.#queue.length == 0) return this.stop(); this.#nowPlaying = this.#queue.shift(); - const resource = await resourceFromUrl(this.#nowPlaying); + const resource = await resourceFromYTUrl(this.#nowPlaying.url); this.player.play(resource); this.connection.subscribe(this.player); } - add(url, position=this.#queue.length) { + add(video: YouTubeVideo, position=this.#queue.length) { const l = this.#queue.length; const normalizedPosition = position % (l + 1); - this.#queue.splice(normalizedPosition, 0, url); + this.#queue.splice(normalizedPosition, 0, video); if (l == 0) this.next(); } - async addArray(urls) { + async addArray(urls: YouTubeVideo[]) { const l = this.#queue.length; this.#queue.push(...urls); if (l == 0 && this.player.state.status == AudioPlayerStatus.Idle) this.next(); - return l != this.#queue.length; + return urls; } pause() {@@ -79,9 +78,9 @@ this.player.unpause();
this.connection.subscribe(this.player); } - async outro(url) { + async outro(url: string) { this.player.pause(); - const resource = await resourceFromUrl(url); + const resource = await resourceFromYTUrl(url); const p = createAudioPlayer(); p.on(AudioPlayerStatus.Idle, () => {@@ -89,7 +88,7 @@ if (this.player.state.status == AudioPlayerStatus.Paused) {
this.resume(); return } - p.subscribers.forEach((e) => e.connection.disconnect()); + this.connection.disconnect(); }); p.play(resource);@@ -97,4 +96,4 @@ this.connection.subscribe(p);
} } -module.exports = { MyQueue } +(module).exports = MyQueue;
@@ -1,10 +1,11 @@
-const fs = require('node:fs'); -const path = require('node:path'); -const { Client, Collection, GatewayIntentBits, ActivityType } = require('discord.js'); +import fs from 'node:fs'; +import path from 'node:path'; +import { Client, Collection, GatewayIntentBits, ActivityType, BaseInteraction } from 'discord.js'; +import { SlashCommand } from "./types"; const { token } = require(path.join(process.cwd(), 'config.json')); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] }); -client.commands = new Collection(); +client.slashCommands = new Collection<string, SlashCommand>(); const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));@@ -12,7 +13,7 @@ for (const file of commandFiles) {
const filePath = path.join(commandsPath, file); const command = require(filePath); if ('data' in command && 'execute' in command) { - client.commands.set(command.data.name, command); + client.slashCommands.set(command.data.name, command); } else { console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); }@@ -23,9 +24,9 @@ client.user.setActivity('FOSS', { type: ActivityType.Competing });
console.log('Bot online!'); }); -client.on('interactionCreate', async interaction => { +client.on('interactionCreate', async (interaction: BaseInteraction) => { if (!interaction.isChatInputCommand()) return; - const command = client.commands.get(interaction.commandName); + const command = client.slashCommands.get(interaction.commandName); if (!command) return; try { await command.execute(interaction);
@@ -9,14 +9,14 @@ "name": "simple-discord-music-bot",
"version": "1.0.0", "license": "GPL3", "dependencies": { - "@discordjs/rest": "^1.5.0", "@discordjs/voice": "^0.16.0", - "discord.js": "^14.3.0", + "discord.js": "^14.14.1", "libsodium-wrappers": "^0.7.10", "play-dl": "^1.9.6" }, "devDependencies": { - "eslint": "^8.23.1" + "eslint": "^8.23.1", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": {@@ -45,19 +45,6 @@ "engines": {
"node": ">=16.11.0" } }, - "node_modules/@discordjs/builders/node_modules/@discordjs/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", - "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/builders/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/collection": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",@@ -77,35 +64,39 @@ "engines": {
"node": ">=16.11.0" } }, - "node_modules/@discordjs/formatters/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/rest": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.1.tgz", - "integrity": "sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", + "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", "dependencies": { - "@discordjs/collection": "^1.5.1", - "@discordjs/util": "^0.3.0", + "@discordjs/collection": "^2.0.0", + "@discordjs/util": "^1.0.2", "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.4.2", - "discord-api-types": "^0.37.41", - "file-type": "^18.3.0", - "tslib": "^2.5.0", - "undici": "^5.22.0" + "@sapphire/snowflake": "^3.5.1", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "magic-bytes.js": "^1.5.0", + "tslib": "^2.6.2", + "undici": "5.27.2" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", + "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "engines": { + "node": ">=18" } }, "node_modules/@discordjs/util": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.3.1.tgz", - "integrity": "sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", + "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/@discordjs/voice": {@@ -123,11 +114,6 @@ "engines": {
"node": ">=16.11.0" } }, - "node_modules/@discordjs/voice/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/ws": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz",@@ -155,49 +141,6 @@ "engines": {
"node": ">=18" } }, - "node_modules/@discordjs/ws/node_modules/@discordjs/rest": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", - "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", - "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", - "magic-bytes.js": "^1.5.0", - "tslib": "^2.6.2", - "undici": "5.27.2" - }, - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", - "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/ws/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, - "node_modules/@discordjs/ws/node_modules/undici": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", - "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",@@ -352,23 +295,18 @@ "node": ">=v18"
} }, "node_modules/@sapphire/snowflake": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.2.tgz", - "integrity": "sha512-FTm9RdyELF21PQN5dS/HLRs90XqWclHa+p0gkonc+BA2X2QKfFySHSjUbO65rmArd/ghR9Ahj2fMfedTZEqzOw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", + "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", + "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", "dependencies": { "undici-types": "~5.26.4" }@@ -566,9 +504,9 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true }, "node_modules/discord-api-types": { - "version": "0.37.67", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.67.tgz", - "integrity": "sha512-4HEzUEmwGPXWJdVhGZ/K+9eWs8kurdn5r/I8qD3/0hb14I0MZvx8K/JOyLhKLgcR4/8/jcr6Xej820BNgNXN7A==" + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" }, "node_modules/discord.js": { "version": "14.14.1",@@ -592,50 +530,6 @@ "ws": "8.14.2"
}, "engines": { "node": ">=16.11.0" - } - }, - "node_modules/discord.js/node_modules/@discordjs/rest": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", - "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", - "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", - "magic-bytes.js": "^1.5.0", - "tslib": "^2.6.2", - "undici": "5.27.2" - }, - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/discord.js/node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", - "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", - "engines": { - "node": ">=18" - } - }, - "node_modules/discord.js/node_modules/@discordjs/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", - "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/discord.js/node_modules/@sapphire/snowflake": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", - "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" } }, "node_modules/discord.js/node_modules/@types/ws": {@@ -646,22 +540,6 @@ "dependencies": {
"@types/node": "*" } }, - "node_modules/discord.js/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, - "node_modules/discord.js/node_modules/undici": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", - "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/discord.js/node_modules/ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",@@ -886,22 +764,6 @@ "engines": {
"node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-type": { - "version": "18.7.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", - "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",@@ -1006,25 +868,6 @@ "engines": {
"node": ">=8" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",@@ -1072,7 +915,8 @@ },
"node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/is-extglob": { "version": "2.1.1",@@ -1330,18 +1174,6 @@ "engines": {
"node": ">=8" } }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/play-audio": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",@@ -1421,34 +1253,6 @@ "url": "https://feross.org/support"
} ] }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",@@ -1506,25 +1310,6 @@ "dependencies": {
"queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",@@ -1546,14 +1331,6 @@ "engines": {
"node": ">=8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",@@ -1578,22 +1355,6 @@ "funding": {
"url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",@@ -1612,22 +1373,6 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/ts-mixer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",@@ -1662,10 +1407,23 @@ "funding": {
"url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { - "version": "5.28.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", - "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", "dependencies": { "@fastify/busboy": "^2.0.0" },@@ -1686,11 +1444,6 @@ "dev": true,
"dependencies": { "punycode": "^2.1.0" } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/which": { "version": "2.0.2",
@@ -4,20 +4,21 @@ "version": "1.0.0",
"description": "A music-focused Discord bot.", "main": "index.js", "scripts": { - "start": "node index.js", + "build": "npx tsc", + "start": "node build/index.js", "deploy-commands": "node tools/deploy-commands.js", "delete-commands": "node tools/delete-commands.js" }, "author": "Marco Andronaco", "license": "GPL3", "dependencies": { - "@discordjs/rest": "^1.5.0", "@discordjs/voice": "^0.16.0", - "discord.js": "^14.3.0", + "discord.js": "^14.14.1", "libsodium-wrappers": "^0.7.10", "play-dl": "^1.9.6" }, "devDependencies": { - "eslint": "^8.23.1" + "eslint": "^8.23.1", + "typescript": "^5.3.3" } }
@@ -0,0 +1,17 @@
+import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import * as music from '../functions/music'; + +module.exports = { + data: new SlashCommandBuilder() + .setName('clear') + .setDescription('Clear the queue.'), + + async execute(interaction: ChatInputCommandInteraction) { + const channel = await music.getChannel(interaction); + if (typeof channel == 'string') + return await interaction.reply({ content: channel, ephemeral: true }); + + music.clearQueue() + return await interaction.reply({ content: 'Queue cleared.' }); + }, +};
@@ -0,0 +1,57 @@
+import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import play, { YouTubeVideo } from 'play-dl'; +import { playUrls, getChannel, formatTitle } from '../functions/music'; + +async function handleUserInput(input: string): Promise<YouTubeVideo[]> { + switch (play.yt_validate(input)) { + case 'video': + const info = await play.video_basic_info(input); + return [info.video_details]; + case 'search': + const results = await play.search(input, { source: { youtube: 'video' }, limit: 1 }); + + if (results.length == 0) + return []; + + const firstResult = results[0]; + return [firstResult]; + case 'playlist': + const playlist = await play.playlist_info(input, { incomplete: true }); + return await playlist.all_videos(); + default: + return []; + } +} + +module.exports = { + data: new SlashCommandBuilder() + .setName('play') + .setDescription('Play something off YouTube.') + .addStringOption((option) => option + .setName('query') + .setDescription('YouTube URL or search query') + .setRequired(true), + ), + + async execute(interaction: ChatInputCommandInteraction) { + const channel = await getChannel(interaction); + if (typeof channel == 'string') + return await interaction.reply({ content: channel, ephemeral: true }); + + await interaction.deferReply(); + const opt = interaction.options; + const input = opt.getString('query'); + + const yt_videos = await handleUserInput(input); + const added = await playUrls(yt_videos, channel); + + switch (added.length) { + case 0: + return await interaction.editReply('No videos were added to the queue.'); + case 1: + return await interaction.editReply(`Added ${formatTitle(added[0])} to queue.`); + default: + return await interaction.editReply(`Added ${added.length} videos to queue.`); + } + }, +};
@@ -0,0 +1,76 @@
+import { joinVoiceChannel } from '@discordjs/voice'; +import { ChatInputCommandInteraction, VoiceBasedChannel } from 'discord.js' +import MyQueue from './myqueue'; +import { YouTubeVideo } from 'play-dl'; + +const q = new MyQueue(); + +export function formatTitle(video: YouTubeVideo): string { + return `**${video.title}** (\`${video.durationRaw}\`)` +} + +export function getChannelConnection(channel: VoiceBasedChannel) { + const guild = channel.guild; + return joinVoiceChannel({ + channelId: channel.id, + guildId: guild.id, + adapterCreator: guild.voiceAdapterCreator + }); +} + +export async function playUrls(videos: YouTubeVideo[], channel: VoiceBasedChannel): Promise<YouTubeVideo[]> { + if (!channel) { + console.log('Channel error:', channel); + return; + } + + q.connection = getChannelConnection(channel); + return await q.addArray(videos); +} +/* Only useful for radio +export async function playStream(url: string, channel: VoiceBasedChannel) { + if (!channel) { + console.log('Channel error:', channel); + return; + } + q.connection = getChannelConnection(channel); + q.add(createAudioResource(url, { inputType: StreamType.Opus })); +} +*/ + +export async function playOutro(url: string, channel: VoiceBasedChannel) { + if (!channel) { + console.log('Channel error:', channel); + return; + } + q.connection = getChannelConnection(channel); + q.outro(url); +} + +export async function getChannel(interaction: ChatInputCommandInteraction): Promise<string | VoiceBasedChannel>{ + const member = interaction.member; + if (!member) + return 'Please use this in your current server.'; + + const vc_error = 'You\'re not in a voice channel.'; + + if (!("voice" in member)) return vc_error; + const channel: VoiceBasedChannel = member.voice.channel; + return channel ? channel : vc_error; +} + +export async function stopMusic() { + return q.stop(); +} + +export async function skipMusic() { + return q.next(); +} + +export async function getQueue() { + return q.queue; +} + +export function clearQueue() { + return q.clear(); +}
@@ -0,0 +1,21 @@
+import { SlashCommandBuilder, Collection, AutocompleteInteraction, ChatInputCommandInteraction } from "discord.js" +import { YouTubeStream } from "play-dl" + +export interface SlashCommand { + command: SlashCommandBuilder, + execute: (interaction : ChatInputCommandInteraction) => void, + autocomplete?: (interaction: AutocompleteInteraction) => void, + cooldown?: number // in seconds +} + +export interface YTVideo { + name: string, + url: string, + stream?: YouTubeStream, +} + +declare module "discord.js" { + export interface Client { + slashCommands: Collection<string, SlashCommand> + } +}
@@ -0,0 +1,11 @@
+{ + "compilerOptions": { + "outDir": "build", + "allowJs": true, + "target": "ESNext", + "moduleResolution": "NodeNext", + "module": "NodeNext", + "sourceMap": true + }, + "include": ["./src/**/*"] + }