diff --git a/.gcloudignore b/.gcloudignore deleted file mode 100644 index a3f0c76..0000000 --- a/.gcloudignore +++ /dev/null @@ -1,17 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Node.js dependencies: -node_modules/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0c7ac32..af48262 100644 --- a/.gitignore +++ b/.gitignore @@ -363,7 +363,4 @@ MigrationBackup/ FodyWeavers.xsd # typescript restrictions -dist/ - -# safety -env.yaml \ No newline at end of file +dist/ \ No newline at end of file diff --git a/5a0b-bot.njsproj b/5a0b-bot.njsproj index 7f750d8..6b711e4 100644 --- a/5a0b-bot.njsproj +++ b/5a0b-bot.njsproj @@ -4,6 +4,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 5a0b-bot NodejsConsoleApp1 + True @@ -17,11 +18,11 @@ . - . + ./dist v4.0 {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} true - false + False true @@ -38,24 +39,38 @@ + + - - - - - - - - - - - + + + + + + Code + + + + + + + + + + Code + + + Code + + + Code + - + diff --git a/package-lock.json b/package-lock.json index 2e4ed92..5e1da0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,28 +19,39 @@ "ytdl-core": "4.9.1" }, "devDependencies": { + "@discordjs/builders": "0.8.2", "@types/yt-search": "^2.3.2" }, "engines": { - "node": "16.9.1" + "node": "16.x.x" } }, "node_modules/@discordjs/builders": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz", - "integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.2.tgz", + "integrity": "sha512-/YRd11SrcluqXkKppq/FAVzLIPRVlIVmc6X8ZklspzMIHDtJ+A4W37D43SHvLdH//+NnK+SHW/WeOF4Ts54PeQ==", + "dev": true, "dependencies": { - "@sindresorhus/is": "^4.0.1", - "discord-api-types": "^0.22.0", + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.24.0", "ow": "^0.27.0", "ts-mixer": "^6.0.0", "tslib": "^2.3.1" }, "engines": { - "node": ">=14.0.0", + "node": ">=16.0.0", "npm": ">=7.0.0" } }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@discordjs/collection": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz", @@ -515,6 +526,30 @@ "npm": ">=7.0.0" } }, + "node_modules/discord.js/node_modules/@discordjs/builders": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz", + "integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==", + "dependencies": { + "@sindresorhus/is": "^4.0.1", + "discord-api-types": "^0.22.0", + "ow": "^0.27.0", + "ts-mixer": "^6.0.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/discord.js/node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz", + "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==", + "engines": { + "node": ">=12" + } + }, "node_modules/discord.js/node_modules/@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", @@ -1451,15 +1486,24 @@ }, "dependencies": { "@discordjs/builders": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz", - "integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.2.tgz", + "integrity": "sha512-/YRd11SrcluqXkKppq/FAVzLIPRVlIVmc6X8ZklspzMIHDtJ+A4W37D43SHvLdH//+NnK+SHW/WeOF4Ts54PeQ==", + "dev": true, "requires": { - "@sindresorhus/is": "^4.0.1", - "discord-api-types": "^0.22.0", + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.24.0", "ow": "^0.27.0", "ts-mixer": "^6.0.0", "tslib": "^2.3.1" + }, + "dependencies": { + "discord-api-types": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==", + "dev": true + } } }, "@discordjs/collection": { @@ -1842,6 +1886,25 @@ "ws": "^8.2.3" }, "dependencies": { + "@discordjs/builders": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz", + "integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==", + "requires": { + "@sindresorhus/is": "^4.0.1", + "discord-api-types": "^0.22.0", + "ow": "^0.27.0", + "ts-mixer": "^6.0.0", + "tslib": "^2.3.1" + }, + "dependencies": { + "discord-api-types": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz", + "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==" + } + } + }, "@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", diff --git a/package.json b/package.json index 32de1f0..13e77f3 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ "scripts": { "build": "tsc --build", "clean": "tsc --build --clean", - "start": "node dist/app.js", - "gcp-build": "tsc -p tsconfig.json" + "start": "node dist/app.js" }, "dependencies": { "@discordjs/opus": "^0.6.0", diff --git a/src/app.ts b/src/app.ts index 116db19..fdc2ee3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,27 +2,8 @@ import { MyClient } from "./types/Client"; -const token = process.env.DISCORD_TOKEN; +const token = "NzIyMjE0MTA3MjM5MDIyNjcy.Xuf0TQ.9uJbIhlTUDCsKnt0_UcuHOG7i94"; const client = new MyClient("./commands", "./events"); -client.on("ready", () => { - if (!client.user) - return; - console.info(`Logged in as ${client.user.tag}!`); - client.user.setActivity("on sound waves.", { type: "PLAYING" }); -}); - -client.on("messageCreate", async (message) => { - if (message.guild) - await message.guild.commands.set(client.restCommands); -}); - -client.on("interactionCreate", interaction => { - if (interaction.isCommand() && client.commands.has(interaction.commandName)) { - const command = client.commands.get(interaction.commandName); - command.run(client, interaction); - } -}); - client.login(token); \ No newline at end of file diff --git a/src/commands/clear.ts b/src/commands/clear.ts deleted file mode 100644 index 223ae92..0000000 --- a/src/commands/clear.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { design } from "../config"; - -import { ButtonInteraction, ColorResolvable, CommandInteraction, Message, MessageActionRow, MessageButton, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { generateQueue } from "../types/Misc"; -import { EmojiIdentifierResolvable } from "discord.js"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) - return; - - const player = client.players.getPlayer(ctx.guildId); - - if (player) { - player.clear(); - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Queue has been cleared."); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } -} - -const data = { - name: "clear", - description: "Clears queue.", -} - -export { data } \ No newline at end of file diff --git a/src/commands/connect.ts b/src/commands/connect.ts deleted file mode 100644 index 6ff837c..0000000 --- a/src/commands/connect.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { design } from "../config"; - -import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { VoiceConnectionStatus } from "@discordjs/voice"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) - return; - - - let player = client.players.getPlayer(ctx.guildId); - - if (!player) - player = client.players.genPlayer(ctx.guildId); - - if (!player.connection || player.connection.state.status === VoiceConnectionStatus.Disconnected) { - const channel = (ctx.member as GuildMember).voice.channel; - if (channel) { - await player.connect(channel); - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle(`Successfully connected to ${channel.name}.`) - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } - else { - client.players.delPlayer(ctx.guildId); - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Please connect to a voice channel.") - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } - } - else { - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Already connected.") - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } -} - -const data = { - name: "connect", - description: "Connect to voice channel." -} - -export { data } \ No newline at end of file diff --git a/src/commands/insert.ts b/src/commands/insert.ts deleted file mode 100644 index 986ff6b..0000000 --- a/src/commands/insert.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { design } from "../config"; - -import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - const pos = ctx.options.getString("position"); - const query = ctx.options.getString("query"); - - if (ctx.guildId === null || query === null) - return; - - - let player = client.players.getPlayer(ctx.guildId); - - if (!player) - return; - - let state: boolean; - - if (!isNaN(Number(query))) { - await player.addTrack(query, ctx.user, Number(query) - 1); - } - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Inserted."); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - -} - -const data = { - name: "insert", - description: "Insert new track to specific position.", - options: [ - { - name: "query", - description: "Title or URL of the song.", - type: "STRING", - required: true - }, - { - name: "position", - description: "Position to insert.", - type: "STRING", - required: true - } - ] -} - -export { data } \ No newline at end of file diff --git a/src/commands/music/clear.ts b/src/commands/music/clear.ts new file mode 100644 index 0000000..f52229b --- /dev/null +++ b/src/commands/music/clear.ts @@ -0,0 +1,32 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + await ctx.deferReply(); + if (ctx.guild === null) + return; + + const player = client.players.getPlayer(ctx.guild.id); + + if (player) { + player.clear(); + + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Queue has been cleared."); + + await ctx.editReply({ embeds: [embed] }); + } +} + +const data = { + name: "clear", + description: "Clears queue.", +} + +export { data } \ No newline at end of file diff --git a/src/commands/music/connect.ts b/src/commands/music/connect.ts new file mode 100644 index 0000000..6fe0988 --- /dev/null +++ b/src/commands/music/connect.ts @@ -0,0 +1,49 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed, TextChannel } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + +export async function run(client: MyClient, ctx: CommandInteraction, reply = true) { + if (ctx.guild === null) + return; + + if (reply) + await ctx.deferReply(); + + const player = client.players.genPlayer(ctx.guild, client.players, ctx.channel as TextChannel); + + let embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Mistake in connection.") + + if (!player.connection) { + const channel = (ctx.member as GuildMember).voice.channel; + if (channel) { + await player.connect(channel); + + embed.setTitle(`Successfully connected to ${channel.name}.`) + } + else { + client.players.delPlayer(ctx.guild.id); + + embed.setTitle("Please connect to a voice channel.") + } + } + else { + embed.setTitle("Already connected to ${channel.name}..") + } + + if (reply) + await ctx.editReply({ embeds: [embed] }); + else + return player; +} + +const data = { + name: "connect", + description: "Connect to voice channel." +} + +export { data } \ No newline at end of file diff --git a/src/commands/music/insert.ts b/src/commands/music/insert.ts new file mode 100644 index 0000000..f2563ce --- /dev/null +++ b/src/commands/music/insert.ts @@ -0,0 +1,56 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + const pos = ctx.options.getInteger("position"); + const query = ctx.options.getString("query"); + + if (!ctx.guild || !query || pos === null) + return; + + const player = client.players.getPlayer(ctx.guild.id); + + if (!player) + return; + + await ctx.deferReply() + + const track = await player.addTrack(query, ctx.user, pos - 1); + + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle(track.title) + .setDescription("Has been inserted to queue.") + .setURL(track.url) + .setThumbnail(track.thumbnail) + .setAuthor(`@${ctx.user.tag}`, ctx.user.avatarURL() as string); + + console.log(`${track.title} added to ${ctx.guildId}`); + + await ctx.editReply({ embeds: [embed] }); +} + +const data = { + name: "insert", + description: "Insert new track to specific position.", + options: [ + { + name: "query", + description: "Title or URL of the song.", + type: "STRING", + required: true + }, + { + name: "position", + description: "Position to insert.", + type: "INTEGER", + required: true + } + ] +} + +export { data } \ No newline at end of file diff --git a/src/commands/jump.ts b/src/commands/music/jump.ts similarity index 59% rename from src/commands/jump.ts rename to src/commands/music/jump.ts index 094cff0..e000243 100644 --- a/src/commands/jump.ts +++ b/src/commands/music/jump.ts @@ -1,22 +1,24 @@ -import { design } from "../config"; +import { design } from "../../config"; -import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; export async function run(client: MyClient, ctx: CommandInteraction) { const query = ctx.options.getString("position"); - if (ctx.guildId === null || query === null) + if (ctx.guild === null || query === null) return; - let player = client.players.getPlayer(ctx.guildId); + let player = client.players.getPlayer(ctx.guild.id); if (!player) return; + await ctx.deferReply(); + let state: boolean; if (!isNaN(Number(query))) { @@ -25,16 +27,17 @@ export async function run(client: MyClient, ctx: CommandInteraction) { state = await player.jump(query); } + let embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Could not find track specified."); + if (state) { - const embed = new MessageEmbed() + embed = new MessageEmbed() .setColor(design.color as ColorResolvable) .setTitle("Jumped."); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } } + await ctx.editReply({ embeds: [embed] }); } const data = { diff --git a/src/commands/music/loop.ts b/src/commands/music/loop.ts new file mode 100644 index 0000000..3d3b145 --- /dev/null +++ b/src/commands/music/loop.ts @@ -0,0 +1,61 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + const query = ctx.options.getString("position"); + + if (ctx.guild === null || query === null) + return; + + + let player = client.players.getPlayer(ctx.guild.id); + + if (!player) + return; + + await ctx.deferReply(); + + let state: boolean; + + if (!isNaN(Number(query))) { + state = await player.jump(Number(query) - 1); + } else { + state = await player.jump(query); + } + + let embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Could not find track specified."); + + if (state) { + embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Jumped."); + } + + await ctx.editReply({ embeds: [embed] }); +} + +const data = { + name: "loop", + description: "Set player loop option.", + options: [ + { + name: "option", + description: "Set player to...", + type: "STRING", + required: true, + choices: [ + { name: "loop queue", value: "LOOP" }, + { name: "loop current track", value: "SONG" }, + { name: "Destroy player on end", value: "NONE" } + ] + } + ] +} + +export { data } \ No newline at end of file diff --git a/src/commands/next.ts b/src/commands/music/next.ts similarity index 50% rename from src/commands/next.ts rename to src/commands/music/next.ts index 6bb6a80..07e549a 100644 --- a/src/commands/next.ts +++ b/src/commands/music/next.ts @@ -1,27 +1,26 @@ -import { design } from "../config"; +import { design } from "../../config"; import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; +import { MyClient } from "../../types/Client"; export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) + if (ctx.guild === null) return; - const player = client.players.getPlayer(ctx.guildId); + const player = client.players.getPlayer(ctx.guild.id); - if (player) + if (player) { player.skip(); - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Skipped."); + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Skipped."); - try { await ctx.reply({ embeds: [embed] }); - } catch { } + } } const data = { diff --git a/src/commands/pause.ts b/src/commands/music/pause.ts similarity index 59% rename from src/commands/pause.ts rename to src/commands/music/pause.ts index 7f2d44e..4f413dd 100644 --- a/src/commands/pause.ts +++ b/src/commands/music/pause.ts @@ -1,28 +1,26 @@ -import { design } from "../config"; +import { design } from "../../config"; import { ButtonInteraction, ColorResolvable, CommandInteraction, Message, MessageActionRow, MessageButton, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { generateQueue } from "../types/Misc"; +import { MyClient } from "../../types/Client"; +import { generateQueue } from "../../types/Misc"; import { EmojiIdentifierResolvable } from "discord.js"; export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) + if (ctx.guild === null) return; - const player = client.players.getPlayer(ctx.guildId); + const player = client.players.getPlayer(ctx.guild.id); if (player) { const state = player.pause(); const embed = new MessageEmbed() .setColor(design.color as ColorResolvable) - .setTitle(`Pause is now ${state ? "on" : "off"}.`); + .setTitle(`Pause is now ${state ? "resumed" : "paused"}.`); - try { - await ctx.reply({ embeds: [embed] }); - } catch { } + await ctx.reply({ embeds: [embed] }); } diff --git a/src/commands/music/play.ts b/src/commands/music/play.ts new file mode 100644 index 0000000..fbf13fc --- /dev/null +++ b/src/commands/music/play.ts @@ -0,0 +1,57 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; +import { run as connect } from "./connect"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + const query = ctx.options.getString("query"); + + const player = await connect(client, ctx, false); + + if (!player) + return; + + await ctx.deferReply(); + + let embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Now playing.") + .setAuthor(`@${ctx.user.tag}`, ctx.user.avatarURL() as string); + + if (query) { + const track = await player.addTrack(query, ctx.user); + embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle(track.title) + .setDescription("Has been pushed to queue.") + .setURL(track.url) + .setThumbnail(track.thumbnail) + .setAuthor(`@${ctx.user.tag}`, ctx.user.avatarURL() as string); + + console.log(`${track.title} added to ${ctx.guildId}`); + } + + await ctx.editReply({ embeds: [embed] }); + + if (!player.nowPlaying) { + await player.play(); + } +} + +const data = { + name: "play", + description: "Play a song or add it to queue.", + options: [ + { + name: "query", + description: "Title or URL of the song.", + type: "STRING", + required: true + } + ] +} + +export { data } \ No newline at end of file diff --git a/src/commands/music/queue.ts b/src/commands/music/queue.ts new file mode 100644 index 0000000..051a458 --- /dev/null +++ b/src/commands/music/queue.ts @@ -0,0 +1,85 @@ +import { design } from "../../config"; + +import { ButtonInteraction, CollectorFilter, ColorResolvable, CommandInteraction, Message, MessageActionRow, MessageButton, MessageComponentInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; +import { generateQueue } from "../../types/Misc"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + if (ctx.guild === null) + return; + + const player = client.players.getPlayer(ctx.guild.id); + + if (!player) + return; + + await ctx.deferReply(); + const queue = player.queue; + + if (queue.length === 0) { + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Queue is empty."); + + await ctx.editReply({ embeds: [embed] }); + return; + } + + let page = 0; + const maxPage = Math.floor(queue.length / 11); + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId("QueueTop") + .setLabel("TOP") + .setStyle("SECONDARY"), + new MessageButton() + .setCustomId("QueueUp") + .setLabel("UP") + .setStyle("SECONDARY"), + new MessageButton() + .setCustomId("QueueDown") + .setLabel("DOWN") + .setStyle("SECONDARY"), + new MessageButton() + .setCustomId("QueueEnd") + .setLabel("END") + .setStyle("SECONDARY") + ); + + await ctx.editReply({ + content: generateQueue(queue, page, player.queuePosition), + components: [row] + }); + + const filter: CollectorFilter = (btn) => { + return btn.message === ctx.fetchReply() as unknown as Message; + }; + + if (ctx.channel) { + const collector = ctx.channel.createMessageComponentCollector({ filter, time: 15000 }); + + collector.on("collect", async (btn: ButtonInteraction) => { + switch (btn.customId) { + case "QueueTop": page = 0; + case "QueueUp": page = Math.max(page - 1, 0); + case "QueueDown": page = Math.min(page + 1, maxPage); + case "QueueEnd": page = maxPage; + } + + await btn.update({ + content: generateQueue(queue, page, player.queuePosition) + }); + }); + } +} + +const data = { + name: "queue", + description: "Send player queue.", +} + +export { data } \ No newline at end of file diff --git a/src/commands/remove.ts b/src/commands/music/remove.ts similarity index 59% rename from src/commands/remove.ts rename to src/commands/music/remove.ts index b46ae99..c60185d 100644 --- a/src/commands/remove.ts +++ b/src/commands/music/remove.ts @@ -1,22 +1,24 @@ -import { design } from "../config"; +import { design } from "../../config"; -import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; export async function run(client: MyClient, ctx: CommandInteraction) { const query = ctx.options.getString("position"); - if (ctx.guildId === null || query === null) + if (ctx.guild === null || query === null) return; - let player = client.players.getPlayer(ctx.guildId); + let player = client.players.getPlayer(ctx.guild.id); if (!player) return; + await ctx.deferReply(); + let state: boolean; if (!isNaN(Number(query))) { @@ -25,16 +27,17 @@ export async function run(client: MyClient, ctx: CommandInteraction) { state = await player.remove(query); } + let embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle("Could not find track specified."); + if (state) { - const embed = new MessageEmbed() + embed = new MessageEmbed() .setColor(design.color as ColorResolvable) .setTitle("Removed."); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } } + await ctx.editReply({ embeds: [embed] }); } const data = { diff --git a/src/commands/music/seek.ts b/src/commands/music/seek.ts new file mode 100644 index 0000000..064fda5 --- /dev/null +++ b/src/commands/music/seek.ts @@ -0,0 +1,66 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + const hours = ctx.options.getInteger("hrs"); + const minutes = ctx.options.getInteger("mins"); + const seconds = ctx.options.getInteger("secs"); + + if (ctx.guild === null || (hours === null && minutes === null && seconds === null)) + return; + + let time = 0; + + if (hours !== null) + time += hours * 3600; + if (minutes !== null) + time += minutes * 60; + if (seconds !== null) + time += seconds; + + + let player = client.players.getPlayer(ctx.guild.id); + + if (player) { + await ctx.deferReply(); + + await player.seek(time); + + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle(`Fastforwarded to ${hours}:${minutes}.${seconds}.`); + + await ctx.editReply({ embeds: [embed] }); + } +} + +const data = { + name: "seek", + description: "Fastforward to timecode.", + options: [ + { + name: "hrs", + description: "Hour to seek.", + type: "INTEGER", + required: false + }, + { + name: "mins", + description: "Minute to seek.", + type: "INTEGER", + required: false + }, + { + name: "secs", + description: "Second to seek.", + type: "INTEGER", + required: false + } + ] +} + +export { data } \ No newline at end of file diff --git a/src/commands/music/stop.ts b/src/commands/music/stop.ts new file mode 100644 index 0000000..aa679c8 --- /dev/null +++ b/src/commands/music/stop.ts @@ -0,0 +1,32 @@ +import { design } from "../../config"; + +import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js"; +import { MyClient } from "../../types/Client"; + + + +export async function run(client: MyClient, ctx: CommandInteraction) { + if (ctx.guild === null) + return; + + const player = client.players.getPlayer(ctx.guild.id); + + if (player) { + player.destroy(); + + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle(`Player is now destroyed.`); + + await ctx.reply({ embeds: [embed] }); + } + + +} + +const data = { + name: "stop", + description: "Destroys player.", +} + +export { data } \ No newline at end of file diff --git a/src/commands/play.ts b/src/commands/play.ts deleted file mode 100644 index fee4850..0000000 --- a/src/commands/play.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { design } from "../config"; - -import { ColorResolvable, CommandInteraction, GuildMember, Message, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { VoiceConnectionStatus } from "@discordjs/voice"; -import { run as connect } from "./connect"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - const query = ctx.options.getString("query"); - - if (ctx.guildId === null || query === null) - return; - - connect(client, ctx); - - const player = client.players.genPlayer(ctx.guildId); - - const track = await player.addTrack(query, ctx.user); - - if (!player.nowPlaying) { - await player.play(); - } - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle(track.title) - .setDescription("Has been pushed to queue.") - .setURL(track.url) - .setThumbnail(track.thumbnail) - .setAuthor(`@${ctx.user.tag}`, ctx.user.avatarURL() as string); - - if (ctx.channel) - player.setMessage(await ctx.channel.send({ embeds: [embed] })); - - console.log(`${track.title} added to ${ctx.guildId}`); -} - -const data = { - name: "play", - description: "Play a song or add it to queue.", - options: [ - { - name: "query", - description: "Title or URL of the song.", - type: "STRING", - required: true - } - ] -} - -export { data } \ No newline at end of file diff --git a/src/commands/queue.ts b/src/commands/queue.ts deleted file mode 100644 index 928abce..0000000 --- a/src/commands/queue.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { design } from "../config"; - -import { ButtonInteraction, CollectorFilter, ColorResolvable, CommandInteraction, Message, MessageActionRow, MessageButton, MessageComponentInteraction, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { generateQueue } from "../types/Misc"; -import { EmojiIdentifierResolvable } from "discord.js"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) - return; - - const player = client.players.getPlayer(ctx.guildId); - - if (player) { - const queue = player.queue; - - if (queue.length === 0) { - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle("Queue is empty."); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } - else { - let page = 0; - const maxPage = Math.floor(queue.length / 11); - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId("QueueTop") - .setLabel("TOP") - .setStyle("SECONDARY"), - new MessageButton() - .setCustomId("QueueUp") - .setLabel("UP") - .setStyle("SECONDARY"), - new MessageButton() - .setCustomId("QueueDown") - .setLabel("DOWN") - .setStyle("SECONDARY"), - new MessageButton() - .setCustomId("QueueEnd") - .setLabel("END") - .setStyle("SECONDARY") - ); - - await ctx.reply({ - content: generateQueue(queue, page, player.queuePosition), - components: [row] - }); - - const filter: CollectorFilter = (btn) => { - return btn.message === ctx.fetchReply() as unknown as Message; - }; - - if (ctx.channel) { - const collector = ctx.channel.createMessageComponentCollector({ filter, time: 15000 }); - - collector.on("collect", async (btn: ButtonInteraction) => { - switch (btn.customId) { - case "QueueTop": page = 0; - case "QueueUp": page = Math.max(page - 1, 0); - case "QueueDown": page = Math.min(page + 1, maxPage); - case "QueueEnd": page = maxPage; - } - - await btn.update({ - content: generateQueue(queue, page, player.queuePosition) - }); - }); - } - - } - } - - -} - -const data = { - name: "queue", - description: "Send player queue.", -} - -export { data } \ No newline at end of file diff --git a/src/commands/seek.ts b/src/commands/seek.ts deleted file mode 100644 index ce2ceff..0000000 --- a/src/commands/seek.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { design } from "../config"; - -import { ColorResolvable, CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - const time = ctx.options.getInteger("time"); - - if (ctx.guildId === null || time === null) - return; - - - let player = client.players.getPlayer(ctx.guildId); - - if (player) - await player.seek(time); -} - -const data = { - name: "seek", - description: "Fastforward to timecode.", - options: [ - { - name: "time", - description: "Timecode to seek.", - type: "INTEGER", - required: true - } - ] -} - -export { data } \ No newline at end of file diff --git a/src/commands/stop.ts b/src/commands/stop.ts deleted file mode 100644 index 81478f2..0000000 --- a/src/commands/stop.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { design } from "../config"; - -import { ButtonInteraction, ColorResolvable, CommandInteraction, Message, MessageActionRow, MessageButton, MessageEmbed } from "discord.js"; -import { MyClient } from "../types/Client"; -import { generateQueue } from "../types/Misc"; -import { EmojiIdentifierResolvable } from "discord.js"; - - - -export async function run(client: MyClient, ctx: CommandInteraction) { - if (ctx.guildId === null) - return; - - const player = client.players.getPlayer(ctx.guildId); - - if (player) { - player.destroy(); - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle(`Destroyed.`); - - try { - await ctx.reply({ embeds: [embed] }); - } catch { } - } - - -} - -const data = { - name: "stop", - description: "Destroys player.", -} - -export { data } \ No newline at end of file diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 0000000..1966e8e --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,13 @@ +import { Interaction } from "discord.js"; +import { MyClient } from "../types/Client"; + +export async function run(args: Interaction[], client: MyClient): Promise { + const [interaction] = args; + + if (interaction.isCommand() && client.commands.has(interaction.commandName)) { + const command = client.commands.get(interaction.commandName); + command.run(client, interaction); + } +} + +export const name = "interactionCreate"; \ No newline at end of file diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts new file mode 100644 index 0000000..72007d7 --- /dev/null +++ b/src/events/messageCreate.ts @@ -0,0 +1,12 @@ +import { Message } from "discord.js"; +import { MyClient } from "../types/Client"; + +export async function run(args: Message[], client: MyClient): Promise { + const [message] = args; + + if (message.guild) + await message.guild.commands.set(client.restCommands); +} + + +export const name = "messageCreate"; \ No newline at end of file diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 0000000..8a584b9 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,11 @@ +import { MyClient } from "../types/Client"; + +export async function run(args: unknown[], client: MyClient): Promise { + if (!client.user) + return; + console.log(`Logged in as ${client.user.tag}!`); + client.user.setActivity("on sound waves.", { type: "PLAYING" }); +} + + +export const name = "ready"; \ No newline at end of file diff --git a/src/types/Client.ts b/src/types/Client.ts index 9097045..9072559 100644 --- a/src/types/Client.ts +++ b/src/types/Client.ts @@ -1,9 +1,9 @@ -import { ApplicationCommandData, Client, Intents } from "discord.js"; +import { ApplicationCommandData, Client as DiscordClient, Intents } from "discord.js"; import { readdirSync } from "fs"; import { Music } from "./Music"; -export class MyClient extends Client { +export class MyClient extends DiscordClient { commands = new Map(); restCommands: ApplicationCommandData[] = []; players = new Music(); @@ -18,14 +18,28 @@ export class MyClient extends Client { ] }); - const cmdFiles = readdirSync(commandsPath).filter(file => file.endsWith(".js")); + readdirSync(commandsPath).forEach((dir) => { + const cmdFiles = readdirSync(`${commandsPath}/${dir}`).filter(file => file.endsWith(".js")); - for (const file of cmdFiles) { - import(`.${commandsPath}/${file}`).then((command) => { - const name = command.data.name; - this.restCommands.push(command.data); - this.commands.set(name, command); + for (const file of cmdFiles) { + import(`.${commandsPath}/${dir}/${file}`).then((command) => { + const name = command.data.name; + this.restCommands.push(command.data); + this.commands.set(name, command); + }); + } + }); + + const evntFiles = readdirSync(eventsPath).filter(file => file.endsWith(".js")); + + for (const file of evntFiles) { + import(`.${eventsPath}/${file}`).then((event) => { + this.on(event.name, (...args: unknown[]) => { + event.run(args, this); + }); }); } + } + } \ No newline at end of file diff --git a/src/types/Misc.ts b/src/types/Misc.ts index d70aa8a..5668cfd 100644 --- a/src/types/Misc.ts +++ b/src/types/Misc.ts @@ -1,4 +1,8 @@ -import { track } from "./interfaces"; +import { track } from "./Types"; + +export async function sleep(ms: number): Promise { + return new Promise(r => setTimeout(r, ms)); +} export function random_shuffle(array: Array) { let currentIndex = array.length, randomIndex; @@ -20,7 +24,7 @@ export function generateQueue(queue: track[], page: number, pos: number): string const track = queue[i]; if (i === pos) - response += `${i + 1}. ${'Current =>' + track.title.slice(0, 35).padEnd(45, " ")} ${track.duration / 60}:${track.duration % 60}\n`; + response += `${i + 1}. ${"Current => " + track.title.slice(0, 35).padEnd(45, " ")} ${Math.floor(track.duration / 60)}:${track.duration % 60}\n`; else response += `${ i + 1 }. ${track.title.slice(0, 35).padEnd(45, " ")} ${track.duration / 60}:${track.duration % 60}\n`; diff --git a/src/types/Music.ts b/src/types/Music.ts index 52f32d6..862ab9c 100644 --- a/src/types/Music.ts +++ b/src/types/Music.ts @@ -1,36 +1,46 @@ -import { VoiceConnection, AudioPlayer, createAudioResource, AudioPlayerStatus, joinVoiceChannel, createAudioPlayer, AudioResource, DiscordGatewayAdapterCreator } from "@discordjs/voice"; -import { ColorResolvable, Guild, Message, MessageEmbed, StageChannel, User, VoiceChannel } from "discord.js"; +import { + VoiceConnection, AudioPlayer, createAudioResource, + AudioPlayerStatus, joinVoiceChannel, createAudioPlayer, + AudioResource, DiscordGatewayAdapterCreator, VoiceConnectionStatus, + entersState +} from "@discordjs/voice"; + +import { + ColorResolvable, Guild, Message, + MessageEmbed, StageChannel, TextChannel, User, + VoiceChannel +} from "discord.js"; + import { design } from "../config"; import ytdl from "discord-ytdl-core"; -import { track, filter } from "./interfaces"; +import { track, filter, loopOption } from "./Types"; import { random_shuffle } from "./Misc"; import yts from "yt-search"; export class Player { - guild: Guild; - music: Music; + public guild: Guild; + public music: Music; - connection: VoiceConnection | undefined; - player: AudioPlayer | undefined; - message: Message | undefined; + public connection: VoiceConnection | undefined; + public player: AudioPlayer | undefined; - queue: track[] = []; - queuePosition = 0; - nowPlaying: AudioResource | undefined = undefined; - filters: filter[] = []; - repeatMode = 0; + public queue: track[] = []; + public queuePosition: number = 0; + public nowPlaying: AudioResource | undefined; + public filters: filter = {}; + public repeatMode: loopOption = "LOOP"; - constructor(clientMusic: Music, playerGuild: Guild) { + public channel: TextChannel; + public message: Message | undefined; + + constructor(playerGuild: Guild, clientMusic: Music, ctxChannel: TextChannel) { this.music = clientMusic; this.guild = playerGuild; + this.channel = ctxChannel; } - async play(seek = 0) { - if (!this.connection || this.queue.length === 0) - return; - - - const track = this.queue[this.queuePosition]; + private async _playerCreator(track: track, seek: number): Promise { + const player = createAudioPlayer(); const stream = ytdl(track.url, { opusEncoded: true, @@ -39,54 +49,103 @@ export class Player { seek: seek }); - const resource = createAudioResource(stream, { - inlineVolume: true + this.nowPlaying = createAudioResource(stream, { + inlineVolume: true, + metadata: track }); - this.player = createAudioPlayer(); - this.connection.subscribe(this.player); + await player.play(this.nowPlaying); - await this.player.play(resource); - this.nowPlaying = resource; + return player; + } + private _resourceEndResolvable(): boolean { + switch (this.repeatMode) { + case "NONE": { + if (++this.queuePosition >= this.queue.length) { + this.queuePosition = 0; + this.nowPlaying = undefined; + return false; + } + return true; + } + case "LOOP": { + this.queuePosition = (this.queuePosition + 1) % this.queue.length; + return true; + } + case "SONG": return true; + } + + return true; + } + + private async _messageResolvable(track: track): Promise { + if (this.message) { - const channel = this.message.channel; + this.channel = this.message.channel as TextChannel; try { await this.message.delete(); } catch (err) { console.log(err); } - - const embed = new MessageEmbed() - .setColor(design.color as ColorResolvable) - .setTitle(track.title) - .setURL(track.url) - .setThumbnail(track.thumbnail) - .setAuthor(`@${track.requester.tag}`, track.requester.avatarURL() as string) - .setDescription("Is now beeing played."); - - this.message = await channel.send({ embeds: [embed] }); } - - this.player.on(AudioPlayerStatus.Idle, async () => { - this.queuePosition = (this.queuePosition + 1) % this.queue.length; - await this.play(); - }); + const embed = new MessageEmbed() + .setColor(design.color as ColorResolvable) + .setTitle(track.title) + .setURL(track.url) + .setThumbnail(track.thumbnail) + .setAuthor(`@${track.requester.tag}`, track.requester.avatarURL() as string) + .setDescription("Is now beeing played."); + + this.message = await this.channel.send({ embeds: [embed] }); } - async connect(channel: VoiceChannel | StageChannel) { + private async _songFinder(query: string) { + return (await yts(query)).videos[0]; + } + + public async play(seek: number = 0): Promise { + if (!this.connection || this.queue.length === 0) + return; + + const track = this.queue[this.queuePosition]; + + this.player = await this._playerCreator(track, seek); + this.connection.subscribe(this.player); + + this.player.on(AudioPlayerStatus.Idle, async () => { + if (this._resourceEndResolvable()) + await this.play(); + }); + + await this._messageResolvable(track); + } + + connect(channel: VoiceChannel | StageChannel): void { this.connection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guildId, selfDeaf: true, adapterCreator: channel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator }); + + this.connection.on(VoiceConnectionStatus.Disconnected, async (oldState, newState) => { + try { + await Promise.race([ + entersState(this.connection as VoiceConnection, VoiceConnectionStatus.Signalling, 5_000), + entersState(this.connection as VoiceConnection, VoiceConnectionStatus.Connecting, 5_000), + ]); + } + catch (error) { + this.destroy(); + } + }); } async addTrack(query: string, requester: User, index?: number): Promise { - const songInfo = (await yts(query)).videos[0]; + const songInfo = await this._songFinder(query); const metadata: track = { url: songInfo.url, @@ -96,10 +155,10 @@ export class Player { requester: requester }; - if (!index) + if (!index || index >= this.queue.length) this.queue.push(metadata); else { - this.queue.splice(index, 0, metadata) + this.queue.splice(Math.max(0, index), 0, metadata) if (this.queuePosition >= index) this.queuePosition++; } @@ -117,7 +176,7 @@ export class Player { destroy() { if (this.connection) - this.connection.disconnect(); + this.connection.destroy(); this.music.delPlayer(this.guild.id); } @@ -143,20 +202,20 @@ export class Player { return false; } - remove(query: number | string): boolean { + async remove(query: number | string): Promise { if (typeof query === "number" && query < this.queue.length && query >= 0) { - if (query === this.queuePosition && this.player) - this.player.stop(); this.queue.splice(query); + if (query === this.queuePosition) + await this.play(); return true; } else { for (let i = 0; i < this.queue.length; i++) { const track = this.queue[i]; if (track.title.toLowerCase().includes(String(query).toLowerCase())) { - if (i === this.queuePosition && this.player) - this.player.stop(); this.queue.splice(i); + if (i === this.queuePosition) + await this.play(); return true; } } @@ -182,7 +241,7 @@ export class Player { } } - repeat(mode: number) { + repeat(mode: loopOption): void { this.repeatMode = mode; } @@ -194,19 +253,14 @@ export class Player { return false; } - shuffle() { + shuffle(): void { this.queue = random_shuffle(this.queue); } - skip() { + skip(): void { if (this.player) this.player.stop(); } - - setMessage(message: Message) { - this.message = message; - } - } export class Music { @@ -218,11 +272,11 @@ export class Music { return player; } - genPlayer(guildId: string): Player { - const player = this.players.get(guildId); + genPlayer(guild: Guild, music: Music, channel: TextChannel): Player { + const player = this.players.get(guild.id); if (!player) { - const newPlayer = new module.exports.Player(); - this.players.set(guildId, newPlayer); + const newPlayer = new module.exports.Player(guild, music, channel); + this.players.set(guild.id, newPlayer); return newPlayer; } else { diff --git a/src/types/interfaces.ts b/src/types/Types.ts similarity index 93% rename from src/types/interfaces.ts rename to src/types/Types.ts index 42cfcae..7061835 100644 --- a/src/types/interfaces.ts +++ b/src/types/Types.ts @@ -42,4 +42,6 @@ export interface filter { fadein?: boolean; dim?: boolean; earrape?: boolean; -} \ No newline at end of file +} + +export type loopOption = "LOOP" | "SONG" | "NONE" | "AUTO"; \ No newline at end of file