diff --git a/package.json b/package.json index 7f8d376..870eb11 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@discordjs/builders": "^1.3.0", "@discordjs/opus": "^0.9.0", - "@discordjs/voice": "^0.13.0", + "@discordjs/voice": "0.11.0", "@discordx/importer": "^1.1.10", "@discordx/music": "^4.1.0", "@discordx/pagination": "^3.3.1", @@ -30,8 +30,11 @@ "discord-api-types": "^0.37.14", "discord.js": "~14.6.0", "discordx": "^11.4.0", + "ffmpeg-static": "^5.1.0", "ms": "^2.1.3", - "prisma": "^4.5.0" + "prism-media": "^1.3.4", + "prisma": "^4.5.0", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@types/ms": "^0.7.31", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a84aa6e..66b0ce0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: 5.4 specifiers: '@discordjs/builders': ^1.3.0 '@discordjs/opus': ^0.9.0 - '@discordjs/voice': ^0.13.0 + '@discordjs/voice': 0.11.0 '@discordx/importer': ^1.1.10 '@discordx/music': ^4.1.0 '@discordx/pagination': ^3.3.1 @@ -19,24 +19,30 @@ specifiers: discordx: ^11.4.0 dotenv: ^16.0.3 eslint: '8.22' + ffmpeg-static: ^5.1.0 ms: ^2.1.3 + prism-media: ^1.3.4 prisma: ^4.5.0 + tweetnacl: ^1.0.3 typescript: ^4.8.4 dependencies: '@discordjs/builders': 1.3.0 '@discordjs/opus': 0.9.0 - '@discordjs/voice': 0.13.0_@discordjs+opus@0.9.0 + '@discordjs/voice': 0.11.0_bfd7ybjr4u3cxk5yt76u2mrpt4 '@discordx/importer': 1.1.10 - '@discordx/music': 4.1.0_ngocjugu66ysfl7kd6xufp6iae + '@discordx/music': 4.1.0_4b6w7lzznzhzdiacs74od732lm '@discordx/pagination': 3.3.1_discord.js@14.6.0 '@discordx/utilities': 5.1.0_q3osisuh25yz2fyejdvcqk24au axios: 1.1.3 discord-api-types: 0.37.14 discord.js: 14.6.0 discordx: 11.4.0_discord.js@14.6.0 + ffmpeg-static: 5.1.0 ms: 2.1.3 + prism-media: 1.3.4_bfd7ybjr4u3cxk5yt76u2mrpt4 prisma: 4.5.0 + tweetnacl: 1.0.3 devDependencies: '@types/ms': 0.7.31 @@ -50,6 +56,16 @@ devDependencies: packages: + /@derhuerst/http-basic/8.2.4: + resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} + engines: {node: '>=6.0.0'} + dependencies: + caseless: 0.12.0 + concat-stream: 2.0.0 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + dev: false + /@discordjs/builders/1.3.0: resolution: {integrity: sha512-Pvca6Nw8Hp+n3N+Wp17xjygXmMvggbh5ywUsOYE2Et4xkwwVRwgzxDJiMUuYapPtnYt4w/8aKlf5khc8ipLvhg==} engines: {node: '>=16.9.0'} @@ -116,13 +132,13 @@ packages: engines: {node: '>=16.9.0'} dev: false - /@discordjs/voice/0.13.0_@discordjs+opus@0.9.0: - resolution: {integrity: sha512-ZzwDmVINaLgkoDUeTJfpN9TkjINMLfTVoLMtEygm0YC5jTTw7AvKGqhc+Ae/2kNLywd0joyFVNrLp94yCkQ9SA==} + /@discordjs/voice/0.11.0_bfd7ybjr4u3cxk5yt76u2mrpt4: + resolution: {integrity: sha512-6+9cj1dxzBJm7WJ9qyG2XZZQ8rcLl6x2caW0C0OxuTtMLAaEDntpb6lqMTFiBg/rDc4Rd59g1w0gJmib33CuHw==} engines: {node: '>=16.9.0'} dependencies: '@types/ws': 8.5.3 - discord-api-types: 0.37.14 - prism-media: 1.3.4_@discordjs+opus@0.9.0 + discord-api-types: 0.36.3 + prism-media: 1.3.4_bfd7ybjr4u3cxk5yt76u2mrpt4 tslib: 2.4.0 ws: 8.9.0 transitivePeerDependencies: @@ -156,7 +172,7 @@ packages: tslib: 2.4.0 dev: false - /@discordx/music/4.1.0_ngocjugu66ysfl7kd6xufp6iae: + /@discordx/music/4.1.0_4b6w7lzznzhzdiacs74od732lm: resolution: {integrity: sha512-5qLj/rg/m/sS8LYTtiwwd7JNjfU37QMwL0erAO6kQDRJFkV3EF/tICxr47tzo+6RJMvJOpiQpLq97ZsVn1Iyzw==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} peerDependencies: @@ -165,10 +181,10 @@ packages: discord.js: '>=14 || ^14.0.0-dev' dependencies: '@discordjs/opus': 0.9.0 - '@discordjs/voice': 0.13.0_@discordjs+opus@0.9.0 + '@discordjs/voice': 0.11.0_bfd7ybjr4u3cxk5yt76u2mrpt4 discord.js: 14.6.0 lodash: 4.17.21 - prism-media: 1.3.4_@discordjs+opus@0.9.0 + prism-media: 1.3.4_bfd7ybjr4u3cxk5yt76u2mrpt4 tslib: 2.4.0 ytdl-core: 4.11.2 ytpl: 2.3.0 @@ -294,6 +310,10 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/node/10.17.60: + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + dev: false + /@types/node/18.11.3: resolution: {integrity: sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==} @@ -539,6 +559,10 @@ packages: fill-range: 7.0.1 dev: true + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + /busboy/1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -551,6 +575,10 @@ packages: engines: {node: '>=6'} dev: true + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: false + /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -599,6 +627,16 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /concat-stream/2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.0 + typedarray: 0.0.6 + dev: false + /concurrently/7.5.0: resolution: {integrity: sha512-5E3mwiS+i2JYBzr5BpXkFxOnleZTMsG+WnE/dCG4/P+oiVXrbmrBwJ2ozn4SxwB2EZDrKR568X+puVohxz3/Mg==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} @@ -669,6 +707,10 @@ packages: path-type: 4.0.0 dev: true + /discord-api-types/0.36.3: + resolution: {integrity: sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==} + dev: false + /discord-api-types/0.37.14: resolution: {integrity: sha512-byBH7SfDCMJwxdqeS8k5sihltH88/YPhuwx+vF2cftSxFLdxyHyU/ZxDL3bq+LB2c4ls/TymE76/ISlLfniUXg==} dev: false @@ -722,6 +764,11 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /env-paths/2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -882,6 +929,19 @@ packages: reusify: 1.0.4 dev: true + /ffmpeg-static/5.1.0: + resolution: {integrity: sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg==} + engines: {node: '>=16'} + requiresBuild: true + dependencies: + '@derhuerst/http-basic': 8.2.4 + env-paths: 2.2.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: false + /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1045,6 +1105,12 @@ packages: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false + /http-response-object/3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + dependencies: + '@types/node': 10.17.60 + dev: false + /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -1327,6 +1393,10 @@ packages: callsites: 3.1.0 dev: true + /parse-cache-control/1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + dev: false + /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1361,7 +1431,7 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prism-media/1.3.4_@discordjs+opus@0.9.0: + /prism-media/1.3.4_bfd7ybjr4u3cxk5yt76u2mrpt4: resolution: {integrity: sha512-eW7LXORkTCQznZs+eqe9VjGOrLBxcBPXgNyHXMTSRVhphvd/RrxgIR7WaWt4fkLuhshcdT5KHL88LAfcvS3f5g==} peerDependencies: '@discordjs/opus': ^0.8.0 @@ -1379,6 +1449,7 @@ packages: optional: true dependencies: '@discordjs/opus': 0.9.0 + ffmpeg-static: 5.1.0 dev: false /prisma/4.5.0: @@ -1390,6 +1461,11 @@ packages: '@prisma/engines': 4.5.0 dev: false + /progress/2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: false + /proxy-from-env/1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -1629,6 +1705,10 @@ packages: tslib: 1.14.1 dev: false + /tweetnacl/1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + dev: false + /type-check/0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1641,6 +1721,10 @@ packages: engines: {node: '>=10'} dev: true + /typedarray/0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + /typedi/0.10.0: resolution: {integrity: sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==} dev: false diff --git a/src/commands/Music Category/music.ts b/src/commands/Music Category/music.ts new file mode 100644 index 0000000..3bcc602 --- /dev/null +++ b/src/commands/Music Category/music.ts @@ -0,0 +1,518 @@ +import { Discord, Slash, SlashGroup, SlashOption } from "discordx"; +import { Category } from "@discordx/utilities"; +import { CommandInteraction, EmbedBuilder, Guild, GuildMember, TextBasedChannel } from "discord.js"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { Player, Queue } from "@discordx/music"; + +@Discord() +@Category("Music") +@SlashGroup({ + description: "Music commands group", + name: "music", +}) +@SlashGroup("music") +export class MusicCommand { + musicPlayer: Player; + channel: TextBasedChannel | undefined; + + constructor() { + this.musicPlayer = new Player(); + + this.musicPlayer.on("onStart", ([, track]) => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle(`🎧 Playing ${track.title}`).setURL(track.url || "")], + }); + } + }); + + this.musicPlayer.on("onFinishPlayback", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🎧 Finished playback")], + }); + } + }); + + this.musicPlayer.on("onPause", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("⏸ Paused playback")], + }); + } + }); + + this.musicPlayer.on("onResume", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("▶ Resumed playback")], + }); + } + }); + + this.musicPlayer.on("onError", ([, err, track]) => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("❌ Error").setDescription(`Error: ${err} | Track: ${track.title}`)], + }); + } + }); + + this.musicPlayer.on("onFinish", ([, track]) => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🎧 Finished").setDescription(`Finished ${track.title}`)], + }); + } + }); + + this.musicPlayer.on("onLoop", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Looping")], + }); + } + }); + + this.musicPlayer.on("onRepeat", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Repeating")], + }); + } + }); + + this.musicPlayer.on("onSkip", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("⏭ Skipped")], + }); + } + }); + + this.musicPlayer.on("onTrackAdd", ([, track]) => { + if (this.channel) { + this.channel.send({ + embeds: [ + new EmbedBuilder().setTitle("🎧 Track added").setDescription(`Added ${track[track.length - 1].title}`), + ], + }); + } + }); + + this.musicPlayer.on("onLoopEnabled", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Loop enabled")], + }); + } + }); + + this.musicPlayer.on("onLoopDisabled", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Loop disabled")], + }); + } + }); + + this.musicPlayer.on("onRepeatEnabled", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Repeat enabled")], + }); + } + }); + + this.musicPlayer.on("onRepeatDisabled", () => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Repeat disabled")], + }); + } + }); + + this.musicPlayer.on("onMix", ([, tracks]) => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔁 Mix").setDescription(`Added ${tracks.length} tracks`)], + }); + } + }); + + this.musicPlayer.on("onVolumeUpdate", ([, volume]) => { + if (this.channel) { + this.channel.send({ + embeds: [new EmbedBuilder().setTitle("🔊 Volume").setDescription(`Volume: ${volume}`)], + }); + } + }); + } + + queue(guild: Guild): Queue { + return this.musicPlayer.queue(guild); + } + + @Slash({ description: "Play a song" }) + async play( + @SlashOption({ + description: "Song name or url", + name: "song", + type: ApplicationCommandOptionType.String, + }) + songName: string, + interaction: CommandInteraction, + ): Promise { + if (!interaction.guild) return; + + if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("You must be in a voice channel to use this command") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + await interaction.deferReply(); + const queue = this.queue(interaction.guild); + if (!queue.isReady) { + this.channel = interaction.channel ?? undefined; + await queue.join(interaction.member.voice.channel); + } + const status = await queue.play(songName); + if (!status) { + await interaction.followUp({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("No songs found") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } else { + await interaction.followUp({ + embeds: [ + new EmbedBuilder() + .setTitle("🎧 Playing") + .setDescription(`Playing ${status.title}`) + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } + } + + @Slash({ description: "Play songs from a playlist" }) + async playlist( + @SlashOption({ + description: "Playlist name or url", + name: "playlist", + type: ApplicationCommandOptionType.String, + }) + playlistName: string, + interaction: CommandInteraction, + ): Promise { + if (!interaction.guild) { + return; + } + + if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("You must be in a voice channel to use this command") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + await interaction.deferReply(); + const queue = this.queue(interaction.guild); + if (!queue.isReady) { + this.channel = interaction.channel ?? undefined; + await queue.join(interaction.member.voice.channel); + } + const status = await queue.playlist(playlistName); + if (!status) { + await interaction.followUp({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("No songs found") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } else { + await interaction.followUp({ + embeds: [ + new EmbedBuilder() + .setTitle("🎧 Playing") + .setDescription("The requested playlist has been added to the queue") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } + } + + async validateInteraction( + interaction: CommandInteraction, + ): Promise { + if (!interaction.guild || !(interaction.member instanceof GuildMember)) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("This command can only be used in a server") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + const queue = this.queue(interaction.guild); + + if (!queue.isReady) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("I am not in a voice channel") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + if (interaction.member.voice.channel!.id !== queue.voiceChannelId) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("You are not in the same voice channel as me") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + return { guild: interaction.guild, member: interaction.member, queue }; + } + + @Slash({ description: "Skip current track" }) + async skip(interaction: CommandInteraction): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + queue.skip(); + + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("⏭ Skipped current track") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } + + @Slash({ description: "Mix tracks" }) + async mix(interaction: CommandInteraction): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + queue.mix(); + + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("🔀 Mixed tracks") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } + + @Slash({ description: "Pause track" }) + async pause(interaction: CommandInteraction): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + + if (queue.isPause) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("The queue is already paused") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + queue.pause(); + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("⏸ Paused current track") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } + + @Slash({ description: "resume music" }) + async resume(interaction: CommandInteraction): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + + if (queue.isPlaying) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("The queue is already playing") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + queue.resume(); + await interaction.reply("resumed music"); + } + + @Slash({ description: "Seek track" }) + async seek( + @SlashOption({ + description: "Seek time in seconds", + name: "time", + type: ApplicationCommandOptionType.Number, + }) + time: number, + interaction: CommandInteraction, + ): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + + if (!queue.isPlaying || !queue.currentTrack) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("There is no track playing") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + + const state = queue.seek(time * 1e3); + if (!state) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("❌ Error") + .setDescription("Invalid seek time") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + return; + } + await interaction.reply("current music seeked"); + } + + @Slash({ description: "Leave voice channel" }) + async leave(interaction: CommandInteraction): Promise { + const validate = await this.validateInteraction(interaction); + if (!validate) return; + + const { queue } = validate; + queue.leave(); + + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("👋 Left voice channel") + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(), + ], + }); + } +} diff --git a/src/commands/Search Category/dictionary.ts b/src/commands/Search Category/dictionary.ts index 07fea94..bbfe877 100644 --- a/src/commands/Search Category/dictionary.ts +++ b/src/commands/Search Category/dictionary.ts @@ -79,7 +79,7 @@ export class DictionaryCommand { type: ApplicationCommandOptionType.String, }) word: string, - interaction: CommandInteraction + interaction: CommandInteraction, ): Promise { const response = await get(`https://api.urbandictionary.com/v0/define?term=${word}`); const { data } = response; diff --git a/src/commands/Search Category/reddit.ts b/src/commands/Search Category/reddit.ts index a7610f0..cd2036f 100644 --- a/src/commands/Search Category/reddit.ts +++ b/src/commands/Search Category/reddit.ts @@ -34,7 +34,7 @@ export class RedditCommand { .setTimestamp(new Date(post.created_utc * 1000)) .setImage(post.url || post.thumbnail) .setColor(Colors.Orange), - ] + ], }); } catch (error) { await interaction.editReply({ @@ -52,4 +52,4 @@ export class RedditCommand { }); } } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 347ac24..6a1cbbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,11 @@ bot.once("ready", async () => { }); bot.on("interactionCreate", (interaction: Interaction) => { + if (interaction.isButton() || interaction.isSelectMenu()) { + if (interaction.customId.startsWith("discordx@pagination@")) { + return; + } + } bot.executeInteraction(interaction); });