From b2bc8b928a9e2e8a6ec481a5ac7c3c4ab7250c8d Mon Sep 17 00:00:00 2001 From: "Vladimir (unknowableshade) Eremin" <60275123+unknowableshade@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:41:00 +0300 Subject: [PATCH] Modular concept --- .gitignore | 3 + src/commands/clear.ts | 22 -- src/commands/connect.ts | 38 --- src/commands/info.ts | 20 -- src/commands/insert.ts | 33 -- src/commands/ping.ts | 21 -- src/config.ts | 9 +- src/events/interaction-create.ts | 15 - src/index.ts | 8 +- src/lib/client.ts | 75 +++-- src/lib/types.ts | 282 +----------------- src/modules/core/commands/modules.ts | 14 + src/modules/core/commands/ping.ts | 21 ++ src/modules/core/events/interaction-create.ts | 19 ++ src/{ => modules/core}/events/ready.ts | 10 +- src/modules/core/lib/i18n.ts | 38 +++ src/modules/core/module.ts | 6 + src/modules/music/commands/clear.ts | 25 ++ src/modules/music/commands/connect.ts | 41 +++ src/{ => modules/music}/commands/filter.ts | 15 +- src/modules/music/commands/info.ts | 23 ++ src/modules/music/commands/insert.ts | 38 +++ src/{ => modules/music}/commands/jump.ts | 21 +- src/{ => modules/music}/commands/loop.ts | 25 +- src/{ => modules/music}/commands/next.ts | 13 +- src/{ => modules/music}/commands/pause.ts | 13 +- src/{ => modules/music}/commands/play.ts | 8 +- src/{ => modules/music}/commands/queue.ts | 9 +- src/{ => modules/music}/commands/remove.ts | 9 +- src/{ => modules/music}/commands/reshuffle.ts | 9 +- src/{ => modules/music}/commands/seek.ts | 9 +- src/{ => modules/music}/commands/stop.ts | 9 +- src/{ => modules/music}/commands/volume.ts | 9 +- .../music}/events/voice-state-update.ts | 13 +- src/{ => modules/music}/lib/fetch.ts | 14 +- src/{ => modules/music}/lib/filter.ts | 0 src/{ => modules/music}/lib/i18n.ts | 30 +- src/{ => modules/music}/lib/misc.ts | 4 +- src/{ => modules/music}/lib/music.ts | 0 src/{ => modules/music}/lib/player.ts | 18 +- src/{ => modules/music}/lib/queue.ts | 22 +- src/modules/music/lib/types.ts | 261 ++++++++++++++++ src/modules/music/module.ts | 10 + tsconfig.json | 2 +- 44 files changed, 709 insertions(+), 575 deletions(-) delete mode 100644 src/commands/clear.ts delete mode 100644 src/commands/connect.ts delete mode 100644 src/commands/info.ts delete mode 100644 src/commands/insert.ts delete mode 100644 src/commands/ping.ts delete mode 100644 src/events/interaction-create.ts create mode 100644 src/modules/core/commands/modules.ts create mode 100644 src/modules/core/commands/ping.ts create mode 100644 src/modules/core/events/interaction-create.ts rename src/{ => modules/core}/events/ready.ts (70%) create mode 100644 src/modules/core/lib/i18n.ts create mode 100644 src/modules/core/module.ts create mode 100644 src/modules/music/commands/clear.ts create mode 100644 src/modules/music/commands/connect.ts rename src/{ => modules/music}/commands/filter.ts (86%) create mode 100644 src/modules/music/commands/info.ts create mode 100644 src/modules/music/commands/insert.ts rename src/{ => modules/music}/commands/jump.ts (50%) rename src/{ => modules/music}/commands/loop.ts (50%) rename src/{ => modules/music}/commands/next.ts (50%) rename src/{ => modules/music}/commands/pause.ts (52%) rename src/{ => modules/music}/commands/play.ts (78%) rename src/{ => modules/music}/commands/queue.ts (88%) rename src/{ => modules/music}/commands/remove.ts (74%) rename src/{ => modules/music}/commands/reshuffle.ts (59%) rename src/{ => modules/music}/commands/seek.ts (78%) rename src/{ => modules/music}/commands/stop.ts (58%) rename src/{ => modules/music}/commands/volume.ts (70%) rename src/{ => modules/music}/events/voice-state-update.ts (64%) rename src/{ => modules/music}/lib/fetch.ts (73%) rename src/{ => modules/music}/lib/filter.ts (100%) rename src/{ => modules/music}/lib/i18n.ts (92%) rename src/{ => modules/music}/lib/misc.ts (95%) rename src/{ => modules/music}/lib/music.ts (100%) rename src/{ => modules/music}/lib/player.ts (95%) rename src/{ => modules/music}/lib/queue.ts (84%) create mode 100644 src/modules/music/lib/types.ts create mode 100644 src/modules/music/module.ts diff --git a/.gitignore b/.gitignore index 357b9b8..df05767 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ pids *.seed *.pid.lock +# Binaries +bin/ + # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release diff --git a/src/commands/clear.ts b/src/commands/clear.ts deleted file mode 100644 index 9b1660f..0000000 --- a/src/commands/clear.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; - -import Client from "../lib/client.js"; -import I18n from "../lib/i18n.js"; - -export async function run(client: Client, ctx: CommandInteraction) { - if (!ctx.guild || !ctx.channel) return; - - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); - - if (!player) return; - - const amount = player.Queue.Tracks.length; - - player.clear(); - - await ctx.reply({ embeds: [I18n.en.cleared(amount)] }); -} - -const data = new SlashCommandBuilder().setName("clear").setDescription("Clear the player's queue."); - -export { data }; diff --git a/src/commands/connect.ts b/src/commands/connect.ts deleted file mode 100644 index e72d730..0000000 --- a/src/commands/connect.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js"; - -import Client from "../lib/client.js"; -import I18n from "../lib/i18n.js"; - -export async function run(client: Client, ctx: CommandInteraction) { - if (!ctx.guild || !ctx.channel || !ctx.member) return; - - await ctx.deferReply(); - - const player = - client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel) || - client.modules.music.set(ctx.guild, ctx.channel as TextChannel); - - if (!player.Connection) { - const channel = (ctx.member as GuildMember).voice.channel; - if (channel) { - player.connect(channel); - - await ctx.editReply({ embeds: [I18n.en.okConnected(channel.name)] }); - } else { - client.modules.music.del(ctx.guild.id); - - await ctx.editReply({ embeds: [I18n.en.notConnected()] }); - return undefined; - } - } else { - await ctx.editReply({ embeds: [I18n.en.wasConnected()] }); - } - - return player; -} - -const data = new SlashCommandBuilder() - .setName("connect") - .setDescription("Connect me to a voice channel."); - -export { data }; diff --git a/src/commands/info.ts b/src/commands/info.ts deleted file mode 100644 index 2b2d6a2..0000000 --- a/src/commands/info.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; - -import Client from "../lib/client.js"; -import I18n from "../lib/i18n.js"; - -export async function run(client: Client, ctx: CommandInteraction) { - if (!ctx.guild || !ctx.channel || !ctx.member) return; - - await ctx.deferReply(); - - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); - - await ctx.editReply({ embeds: [I18n.en.playerInfo(player)] }); -} - -const data = new SlashCommandBuilder() - .setName("info") - .setDescription("Information about the playback."); - -export { data }; diff --git a/src/commands/insert.ts b/src/commands/insert.ts deleted file mode 100644 index 493cbb1..0000000 --- a/src/commands/insert.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; - -import Client from "../lib/client.js"; -import I18n from "../lib/i18n.js"; - -export async function run(client: Client, ctx: CommandInteraction) { - const pos = ctx.options.get("position")?.value as number; - const query = ctx.options.get("query")?.value as string; - - if (!ctx.guild || !query || !pos || !ctx.channel) return; - - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); - - if (!player) return; - - await ctx.deferReply(); - - const track = await player.add(query, ctx.user, pos - 1); - - await ctx.editReply({ embeds: [I18n.en.appended(track)] }); -} - -const data = new SlashCommandBuilder() - .setName("insert") - .setDescription("Insert a new track to specific position in the queue.") - .addStringOption((option) => - option.setName("query").setDescription("Title or URL of the song.").setRequired(true) - ) - .addIntegerOption((option) => - option.setName("position").setDescription("Position to insert.").setRequired(true) - ); - -export { data }; diff --git a/src/commands/ping.ts b/src/commands/ping.ts deleted file mode 100644 index 021a407..0000000 --- a/src/commands/ping.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CommandInteraction, SlashCommandBuilder } from "discord.js"; - -import Client from "../lib/client.js"; -import I18n from "../lib/i18n.js"; - -export async function run(client: Client, ctx: CommandInteraction) { - const start = Date.now(); - - if (!client.application) { - await ctx.reply({ embeds: [I18n.en.ping(undefined)] }); - return; - } - - await client.application.fetch(); - - await ctx.reply({ embeds: [I18n.en.ping(Date.now() - start)] }); -} - -const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me."); - -export { data }; diff --git a/src/config.ts b/src/config.ts index 6f936cc..2ade376 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,12 +4,11 @@ import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config = { - token: + discordToken: process.env.DISCORD_TOKEN ?? - "NzgzNDQwMjU3NjE3MzYyOTQ1.GQ0lLK.VftrGanpOtqfoKUQRkoXDCcbQqwMqBsybmwjGE", - commands: path.join(__dirname, "/commands/"), - events: path.join(__dirname, "/events/"), - binaries: path.join(__dirname, "/../bin/") + "NzgzNDQwMjU3NjE3MzYyOTQ1.Ghtbbs.kCCj2thmuKu5MsThHGSV5O0UUAFO3bwPlnQ1PM", + modulesDir: path.join(__dirname, "/modules/"), + binariesDir: path.join(__dirname, "/../bin/") }; export default config; diff --git a/src/events/interaction-create.ts b/src/events/interaction-create.ts deleted file mode 100644 index e2c2b86..0000000 --- a/src/events/interaction-create.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Interaction } from "discord.js"; - -import Client from "../lib/client.js"; - -export async function run(client: Client, args: Interaction[]): Promise { - const [interaction] = args; - - if (interaction.isCommand()) { - const command = client.Commands.get(interaction.commandName); - - if (command) command.run(client, interaction); - } -} - -export const name = "interactionCreate"; diff --git a/src/index.ts b/src/index.ts index 727b0cf..8a1a286 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,10 @@ +import { GatewayIntentBits } from "discord.js"; + import config from "./config.js"; import Client from "./lib/client.js"; -const client = new Client(); +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] +}); -client.login(config.token); +client.login(config.discordToken); diff --git a/src/lib/client.ts b/src/lib/client.ts index 9b8376d..9bb26ec 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -1,47 +1,68 @@ -import { Client as DiscordClient, GatewayIntentBits } from "discord.js"; -import { existsSync, readdirSync } from "fs"; +import { Client as DiscordClient, ClientOptions, Collection } from "discord.js"; +import { existsSync, lstatSync, readdirSync } from "fs"; import path from "path"; import { pathToFileURL } from "url"; import config from "../config.js"; -import Music from "./music.js"; -import { command, event } from "./types.js"; +import { Command, Event, Module } from "./types.js"; + +const MODULE_ENTRY = "module.js"; +const MODULE_COMMANDS = "commands"; +const MODULE_EVENTS = "events"; export default class Client extends DiscordClient { - private commands = new Map(); + public readonly commands = new Collection(); - get Commands(): Map { - return this.commands; + public readonly modules = new Collection(); + + public constructor(options: ClientOptions) { + super(options); + + for (const module of readdirSync(config.modulesDir)) { + const modulePath = path.join(config.modulesDir, module); + + if (!lstatSync(modulePath).isDirectory()) { + continue; + } + + import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then( + (data: Module) => this.loadModule(data, modulePath) + ); + } } - public readonly modules = { - music: new Music() - }; + private loadModule(data: Module, moduleDir: string): void { + this.modules.set(data.name, data); - public constructor() { - super({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] - }); + const commandsDir = path.join(moduleDir, MODULE_COMMANDS); - if (existsSync(config.commands)) { - const files = readdirSync(config.commands).filter((file) => file.endsWith(".js")); + if (existsSync(commandsDir)) { + for (const source of readdirSync(commandsDir)) { + const commandFile = path.join(commandsDir, source); - for (const file of files) { - import(pathToFileURL(path.join(config.commands, file)).toString()).then( - (cmd: command) => this.commands.set(cmd.data.name, cmd) + if (!lstatSync(commandFile).isFile() || !commandFile.endsWith(".js")) { + continue; + } + + import(pathToFileURL(commandFile).toString()).then((command: Command) => + this.commands.set(command.data.name, command) ); } } - if (existsSync(config.events)) { - const files = readdirSync(config.events).filter((file) => file.endsWith(".js")); + const eventsDir = path.join(moduleDir, MODULE_EVENTS); - for (const file of files) { - import(pathToFileURL(path.join(config.events, file)).toString()).then( - (evnt: event) => { - this.on(evnt.name, (...args: unknown[]) => evnt.run(this, args)); - } - ); + if (existsSync(eventsDir)) { + for (const source of readdirSync(eventsDir)) { + const eventFile = path.join(eventsDir, source); + + if (!lstatSync(eventFile).isFile() || !eventFile.endsWith(".js")) { + continue; + } + + import(pathToFileURL(eventFile).toString()).then((event: Event) => { + this.on(event.name, (...eventee: unknown[]) => event.execute(this, eventee)); + }); } } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 7dce71a..4ef6ede 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,273 +1,21 @@ -import { ApplicationCommandData, CommandInteraction, User } from "discord.js"; +import { ApplicationCommandData, CommandInteraction } from "discord.js"; import Client from "./client.js"; -export type track = { - url: string; - title: string; - thumbnail: string; - duration: number; - requester: User; -}; - -export type command = { - data: ApplicationCommandData; - run: (arg0: Client, arg1: CommandInteraction) => Promise; -}; - -export type event = { +export interface Module { name: string; - run: (arg0: Client, arg1: unknown[]) => Promise; -}; + description?: string; + author: string; + tag: string; + controller?: unknown; +} -export type loopOption = "LOOP" | "SONG" | "NONE" | "AUTO"; +export interface Command { + data: ApplicationCommandData; + execute: (client: Client, interaction: CommandInteraction) => Promise; +} -export type ytdlArgs = { - help?: boolean; - version?: boolean; - update?: boolean; - ignoreErrors?: boolean; - abortOnError?: boolean; - dumpUserAgent?: boolean; - listExtractors?: boolean; - extractorDescriptions?: boolean; - forceGenericExtractor?: boolean; - defaultSearch?: string; - igonreConfig?: boolean; - configLocation?: string; - flatPlaylist?: boolean; - markWatched?: boolean; - noColor?: boolean; - proxy?: string; - socketTimeout?: number; - sourceAddress?: string; - forceIpv4?: boolean; - forceIpv6?: boolean; - geoVerificationProxy?: string; - geoBypass?: boolean; - geoBypassCountry?: string; - geoBypassIpBlock?: string; - playlistStart?: number; - playlistEnd?: number | "last"; - playlistItems?: string; - matchTitle?: string; - rejectTitle?: string; - maxDownloads?: number; - minFilesize?: string; - maxFilesize?: string; - date?: string; - datebefore?: string; - dateafter?: string; - minViews?: number; - maxViews?: number; - matchFilter?: string; - noPlaylist?: boolean; - yesPlaylist?: boolean; - ageLimit?: number; - downloadArchive?: string; - includeAds?: boolean; - limitRate?: string; - retries?: number | "infinite"; - skipUnavailableFragments?: boolean; - abortOnUnavailableFragment?: boolean; - keepFragments?: boolean; - bufferSize?: string; - noResizeBuffer?: boolean; - httpChunkSize?: string; - playlistReverse?: boolean; - playlistRandom?: boolean; - xattrSetFilesize?: boolean; - hlsPreferNative?: boolean; - hlsPreferFfmpeg?: boolean; - hlsUseMpegts?: boolean; - externalDownloader?: string; - externalDownloaderArgs?: string; - batchFile?: string; - id?: boolean; - output?: string; - outputNaPlaceholder?: string; - autonumberStart?: number; - restrictFilenames?: boolean; - noOverwrites?: boolean; - continue?: boolean; - noPart?: boolean; - noMtime?: boolean; - writeDescription?: boolean; - writeInfoJson?: boolean; - writeAnnotations?: boolean; - loadInfoJson?: string; - cookies?: string; - cacheDir?: string; - noCacheDir?: boolean; - rmCacheDir?: boolean; - writeThumbnail?: boolean; - writeAllThumbnails?: boolean; - listThumbnails?: boolean; - quiet?: boolean; - noWarnings?: boolean; - simulate?: boolean; - skipDownload?: boolean; - getUrl?: boolean; - getTitle?: boolean; - getId?: boolean; - getThumbnail?: boolean; - getDuration?: boolean; - getFilename?: boolean; - getFormat?: boolean; - dumpJson?: boolean; - dumpSingleJson?: boolean; - printJson?: boolean; - newline?: boolean; - noProgress?: boolean; - consoleTitle?: boolean; - verbose?: boolean; - dumpPages?: boolean; - writePages?: boolean; - printTraffic?: boolean; - callHome?: boolean; - encoding?: string; - noCheckCertificate?: boolean; - preferInsecure?: boolean; - userAgent?: string; - referer?: string; - addHeader?: string; - bidiWorkaround?: boolean; - sleepInterval?: number; - maxSleepInterval?: number; - format?: string; - allFormats?: boolean; - preferFreeFormats?: boolean; - listFormats?: boolean; - youtubeSkipDashManifest?: boolean; - mergeOutputFormat?: string; - writeSub?: boolean; - writeAutoSub?: boolean; - allSubs?: boolean; - listSubs?: boolean; - subFormat?: string; - subLang?: string; - username?: string; - password?: string; - twofactor?: string; - netrc?: boolean; - videoPassword?: string; - apMso?: string; - apUsername?: string; - apPassword?: string; - apListMso?: boolean; - extractAudio?: boolean; - audioFormat?: string; - audioQuality?: number; - recodeVideo?: string; - postprocessorArgs?: string; - keepVideo?: boolean; - noPostOverwrites?: boolean; - embedSubs?: boolean; - embedThumbnail?: boolean; - addMetadata?: boolean; - metadataFromFile?: string; - xattrs?: boolean; - fixup?: string; - preferAvconv?: boolean; - preferFfmpeg?: boolean; - ffmpegLocation?: string; - exec?: string; - convertSubs?: string; -}; - -type ytdlFormat = { - asr: number; - filesize: number; - format_id: string; - format_note: string; - fps: number; - height: number; - quality: number; - tbr: number; - url: string; - width: number; - ext: string; - vcodec: string; - acodec: string; - abr: number; - downloader_options: unknown; - container: string; - format: string; - protocol: string; - http_headers: unknown; -}; - -type ytdlThumbnail = { - height: number; - url: string; - width: number; - resolution: string; - id: string; -}; - -type ytdlEntry = { - uploader: string; - duration: number; - id: string; - title: string; - url: string; -}; - -export type ytdlResponse = { - id: string; - title: string; - formats: ytdlFormat[]; - thumbnails: ytdlThumbnail[]; - description: string; - upload_date: string; - uploader: string; - uploader_id: string; - uploader_url: string; - channel_id: string; - channel_url: string; - duration: number; - view_count: number; - average_rating: number; - age_limit: number; - webpage_url: string; - categories: string[]; - tags: string[]; - is_live: boolean; - like_count: number; - dislike_count: number; - channel: string; - track: string; - artist: string; - creator: string; - alt_title: string; - extractor: string; - webpage_url_basename: string; - extractor_key: string; - playlist: string; - playlist_index: number; - thumbnail: string; - display_id: string; - requested_subtitles: unknown; - asr: number; - filesize: number; - format_id: string; - format_note: string; - fps: number; - height: number; - quality: number; - tbr: number; - url: string; - width: number; - ext: string; - vcodec: string; - acodec: string; - abr: number; - downloader_options: { http_chunk_size: number }; - container: string; - format: string; - protocol: string; - http_headers: unknown; - fulltitle: string; - _filename: string; - entries: ytdlEntry[]; -}; +export interface Event { + name: string; + execute: (client: Client, eventee: unknown[]) => Promise; +} diff --git a/src/modules/core/commands/modules.ts b/src/modules/core/commands/modules.ts new file mode 100644 index 0000000..2f916ec --- /dev/null +++ b/src/modules/core/commands/modules.ts @@ -0,0 +1,14 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + +import Client from "../../../lib/client.js"; +import i18n from "../lib/i18n.js"; + +async function execute(client: Client, interaction: CommandInteraction): Promise { + await interaction.reply({ embeds: [i18n.modules(Array.from(client.modules.values()))] }); +} + +const data = new SlashCommandBuilder() + .setName("modules") + .setDescription("List of availiable modules."); + +export { data, execute }; diff --git a/src/modules/core/commands/ping.ts b/src/modules/core/commands/ping.ts new file mode 100644 index 0000000..f9fcb49 --- /dev/null +++ b/src/modules/core/commands/ping.ts @@ -0,0 +1,21 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + +import Client from "../../../lib/client.js"; +import i18n from "../lib/i18n.js"; + +async function execute(client: Client, interaction: CommandInteraction): Promise { + const start = Date.now(); + + if (!client.application) { + await interaction.reply({ embeds: [i18n.ping(undefined)] }); + return; + } + + await client.application.fetch(); + + await interaction.reply({ embeds: [i18n.ping(Date.now() - start)] }); +} + +const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me."); + +export { data, execute }; diff --git a/src/modules/core/events/interaction-create.ts b/src/modules/core/events/interaction-create.ts new file mode 100644 index 0000000..dc4524e --- /dev/null +++ b/src/modules/core/events/interaction-create.ts @@ -0,0 +1,19 @@ +import { Interaction } from "discord.js"; + +import Client from "../../../lib/client.js"; + +async function execute(client: Client, eventee: Interaction[]): Promise { + const [interaction] = eventee; + + if (interaction.isCommand()) { + const command = client.commands.get(interaction.commandName); + + if (command) { + command.execute(client, interaction); + } + } +} + +const name = "interactionCreate"; + +export { execute, name }; diff --git a/src/events/ready.ts b/src/modules/core/events/ready.ts similarity index 70% rename from src/events/ready.ts rename to src/modules/core/events/ready.ts index e8acaab..55272c5 100644 --- a/src/events/ready.ts +++ b/src/modules/core/events/ready.ts @@ -1,9 +1,9 @@ import { generateDependencyReport } from "@discordjs/voice"; import { ActivityType } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; -export async function run(client: Client): Promise { +async function execute(client: Client): Promise { if (!client.user || !client.application) { console.log("No user or application is provided!"); process.exit(1); @@ -13,9 +13,11 @@ export async function run(client: Client): Promise { console.log(generateDependencyReport()); client.user.setActivity("over you", { type: ActivityType.Watching }); - const restCommands = Array.from(client.Commands.values()).map((value) => value.data); + const restCommands = Array.from(client.commands.values()).map((value) => value.data); client.application.commands.set(restCommands); } -export const name = "ready"; +const name = "ready"; + +export { execute, name }; diff --git a/src/modules/core/lib/i18n.ts b/src/modules/core/lib/i18n.ts new file mode 100644 index 0000000..4c686d6 --- /dev/null +++ b/src/modules/core/lib/i18n.ts @@ -0,0 +1,38 @@ +import { EmbedBuilder } from "discord.js"; + +import { Module } from "../../../lib/types"; + +const i18n = { + modules: (modules: Module[]) => { + return new EmbedBuilder() + .setColor("Random") + .setTitle("Modules") + .addFields( + modules.map((value) => { + return { + name: value.name + "@" + value.tag, + value: (value.description ?? "") + " | " + value.author, + inline: true + }; + }) + ); + }, + ping: (ping?: number) => { + const title = ping ? `I have pinged ${ping} ms late.` : "There is no application."; + + const comment = !ping + ? "How am I speaking then?" + : ping > 250 + ? "I'm kinda slowpoke ngl." + : ping > 200 + ? "I can do better than this." + : "Dream's speed!"; + + return new EmbedBuilder() + .setColor("Random") + .setTitle(`**${title}**`) + .setDescription(`_**${comment}**_`); + } +}; + +export default i18n; diff --git a/src/modules/core/module.ts b/src/modules/core/module.ts new file mode 100644 index 0000000..15f16a7 --- /dev/null +++ b/src/modules/core/module.ts @@ -0,0 +1,6 @@ +const name = "core"; +const description = "Quetza core functionality"; +const author = "unknowableshade"; +const tag = "2302.1"; + +export { author, description, name, tag }; diff --git a/src/modules/music/commands/clear.ts b/src/modules/music/commands/clear.ts new file mode 100644 index 0000000..3475d72 --- /dev/null +++ b/src/modules/music/commands/clear.ts @@ -0,0 +1,25 @@ +import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; + +import Client from "../../../lib/client.js"; +import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; + +async function execute(client: Client, interaction: CommandInteraction): Promise { + if (!interaction.guild || !interaction.channel) { + return; + } + + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); + + if (!player) return; + + const amount = player.Queue.Tracks.length; + + player.clear(); + + await interaction.reply({ embeds: [I18n.en.cleared(amount)] }); +} + +const data = new SlashCommandBuilder().setName("clear").setDescription("Clear the player's queue."); + +export { data, execute }; diff --git a/src/modules/music/commands/connect.ts b/src/modules/music/commands/connect.ts new file mode 100644 index 0000000..0c3460c --- /dev/null +++ b/src/modules/music/commands/connect.ts @@ -0,0 +1,41 @@ +import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js"; + +import Client from "../../../lib/client.js"; +import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; + +async function execute(client: Client, interaction: CommandInteraction) { + if (!interaction.guild || !interaction.channel || !interaction.member) { + return; + } + + await interaction.deferReply(); + + const player = + controller.get(interaction.guild.id, interaction.channel as TextChannel) || + controller.set(interaction.guild, interaction.channel as TextChannel); + + if (!player.Connection) { + const channel = (interaction.member as GuildMember).voice.channel; + if (channel) { + player.connect(channel); + + await interaction.editReply({ embeds: [I18n.en.okConnected(channel.name)] }); + } else { + controller.del(interaction.guild.id); + + await interaction.editReply({ embeds: [I18n.en.notConnected()] }); + return undefined; + } + } else { + await interaction.editReply({ embeds: [I18n.en.wasConnected()] }); + } + + return player; +} + +const data = new SlashCommandBuilder() + .setName("connect") + .setDescription("Connect me to a voice channel."); + +export { data, execute }; diff --git a/src/commands/filter.ts b/src/modules/music/commands/filter.ts similarity index 86% rename from src/commands/filter.ts rename to src/modules/music/commands/filter.ts index 038b53e..ff5b029 100644 --- a/src/commands/filter.ts +++ b/src/modules/music/commands/filter.ts @@ -1,20 +1,21 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { - const filter = ctx.options.get("filter")?.value as string; +async function execute(client: Client, interaction: CommandInteraction) { + const filter = interaction.options.get("filter")?.value as string; - if (!ctx.guild || !ctx.channel) return; + if (!interaction.guild || !interaction.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); if (!player) return; player.setFilter(filter || undefined); - await ctx.reply({ embeds: [I18n.en.filtered(filter || undefined)] }); + await interaction.reply({ embeds: [I18n.en.filtered(filter || undefined)] }); } const data = new SlashCommandBuilder() @@ -121,4 +122,4 @@ const data = new SlashCommandBuilder() .setRequired(true) ); -export { data }; +export { data, execute }; diff --git a/src/modules/music/commands/info.ts b/src/modules/music/commands/info.ts new file mode 100644 index 0000000..859f7b9 --- /dev/null +++ b/src/modules/music/commands/info.ts @@ -0,0 +1,23 @@ +import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; + +import Client from "../../../lib/client.js"; +import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; + +async function execute(client: Client, interaction: CommandInteraction) { + if (!interaction.guild || !interaction.channel || !interaction.member) { + return; + } + + await interaction.deferReply(); + + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); + + await interaction.editReply({ embeds: [I18n.en.playerInfo(player)] }); +} + +const data = new SlashCommandBuilder() + .setName("info") + .setDescription("Information about the playback."); + +export { data, execute }; diff --git a/src/modules/music/commands/insert.ts b/src/modules/music/commands/insert.ts new file mode 100644 index 0000000..9fd1578 --- /dev/null +++ b/src/modules/music/commands/insert.ts @@ -0,0 +1,38 @@ +import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; + +import Client from "../../../lib/client.js"; +import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; + +async function execute(client: Client, interaction: CommandInteraction) { + const pos = interaction.options.get("position")?.value as number; + const query = interaction.options.get("query")?.value as string; + + if (!interaction.guild || !query || !pos || !interaction.channel) { + return; + } + + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); + + if (!player) { + return; + } + + await interaction.deferReply(); + + const track = await player.add(query, interaction.user, pos - 1); + + await interaction.editReply({ embeds: [I18n.en.appended(track)] }); +} + +const data = new SlashCommandBuilder() + .setName("insert") + .setDescription("Insert a new track to specific position in the queue.") + .addStringOption((option) => + option.setName("query").setDescription("Title or URL of the song.").setRequired(true) + ) + .addIntegerOption((option) => + option.setName("position").setDescription("Position to insert.").setRequired(true) + ); + +export { data, execute }; diff --git a/src/commands/jump.ts b/src/modules/music/commands/jump.ts similarity index 50% rename from src/commands/jump.ts rename to src/modules/music/commands/jump.ts index c7de793..c5148b6 100644 --- a/src/commands/jump.ts +++ b/src/modules/music/commands/jump.ts @@ -1,23 +1,28 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { - const query = ctx.options.get("query")?.value as string; +async function execute(client: Client, interaction: CommandInteraction) { + const query = interaction.options.get("query")?.value as string; - if (!ctx.guild || !query || !ctx.channel) return; + if (!interaction.guild || !query || !interaction.channel) { + return; + } - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); - if (!player) return; + if (!player) { + return; + } const state = !isNaN(+query) && isFinite(+query) && !/e/i.test(query) ? player.jump(parseInt(query) - 1) : player.jump(query); - await ctx.reply({ embeds: [I18n.en.jumped(state)] }); + await interaction.reply({ embeds: [I18n.en.jumped(state)] }); } const data = new SlashCommandBuilder() @@ -27,4 +32,4 @@ const data = new SlashCommandBuilder() option.setName("query").setDescription("Position or title to jump to.").setRequired(true) ); -export { data }; +export { data, execute }; diff --git a/src/commands/loop.ts b/src/modules/music/commands/loop.ts similarity index 50% rename from src/commands/loop.ts rename to src/modules/music/commands/loop.ts index 6c4598d..bff0326 100644 --- a/src/commands/loop.ts +++ b/src/modules/music/commands/loop.ts @@ -1,21 +1,26 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; -import { loopOption } from "../lib/types.js"; +import { LoopOption } from "../lib/types.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { - const query = ctx.options.get("option")?.value as string; +async function execute(client: Client, interaction: CommandInteraction) { + const query = interaction.options.get("option")?.value as string; - if (!ctx.guild || !query || !ctx.channel) return; + if (!interaction.guild || !query || !interaction.channel) { + return; + } - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(interaction.guild.id, interaction.channel as TextChannel); - if (!player) return; + if (!player) { + return; + } - player.Queue.Loop = query as loopOption; + player.Queue.Loop = query as LoopOption; - await ctx.reply({ embeds: [I18n.en.looped(query as loopOption)] }); + await interaction.reply({ embeds: [I18n.en.looped(query as LoopOption)] }); } const data = new SlashCommandBuilder() @@ -34,4 +39,4 @@ const data = new SlashCommandBuilder() .setRequired(true) ); -export { data }; +export { data, execute }; diff --git a/src/commands/next.ts b/src/modules/music/commands/next.ts similarity index 50% rename from src/commands/next.ts rename to src/modules/music/commands/next.ts index 0afcd38..231d784 100644 --- a/src/commands/next.ts +++ b/src/modules/music/commands/next.ts @@ -1,12 +1,15 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { - if (!ctx.guild || !ctx.channel) return; +async function execute(client: Client, ctx: CommandInteraction) { + if (!ctx.guild || !ctx.channel) { + return; + } - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -17,4 +20,4 @@ export async function run(client: Client, ctx: CommandInteraction) { const data = new SlashCommandBuilder().setName("next").setDescription("Skip current song."); -export { data }; +export { data, execute }; diff --git a/src/commands/pause.ts b/src/modules/music/commands/pause.ts similarity index 52% rename from src/commands/pause.ts rename to src/modules/music/commands/pause.ts index fcd9aff..87b2571 100644 --- a/src/commands/pause.ts +++ b/src/modules/music/commands/pause.ts @@ -1,12 +1,15 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { - if (!ctx.guild || !ctx.channel) return; +async function execute(client: Client, ctx: CommandInteraction) { + if (!ctx.guild || !ctx.channel) { + return; + } - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -17,4 +20,4 @@ export async function run(client: Client, ctx: CommandInteraction) { const data = new SlashCommandBuilder().setName("pause").setDescription("Toggle pause on player."); -export { data }; +export { data, execute }; diff --git a/src/commands/play.ts b/src/modules/music/commands/play.ts similarity index 78% rename from src/commands/play.ts rename to src/modules/music/commands/play.ts index be844fb..7818a6c 100644 --- a/src/commands/play.ts +++ b/src/modules/music/commands/play.ts @@ -1,10 +1,10 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; -import { run as connect } from "./connect.js"; +import { execute as connect } from "./connect.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { const query = ctx.options.get("query")?.value as string; const player = await connect(client, ctx); @@ -29,4 +29,4 @@ const data = new SlashCommandBuilder() option.setName("query").setDescription("Title or URL of the song.") ); -export { data }; +export { data, execute }; diff --git a/src/commands/queue.ts b/src/modules/music/commands/queue.ts similarity index 88% rename from src/commands/queue.ts rename to src/modules/music/commands/queue.ts index a81b088..8c8764d 100644 --- a/src/commands/queue.ts +++ b/src/modules/music/commands/queue.ts @@ -10,13 +10,14 @@ import { TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { if (!ctx.guild || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -69,4 +70,4 @@ export async function run(client: Client, ctx: CommandInteraction) { const data = new SlashCommandBuilder().setName("queue").setDescription("Print out the queue."); -export { data }; +export { data, execute }; diff --git a/src/commands/remove.ts b/src/modules/music/commands/remove.ts similarity index 74% rename from src/commands/remove.ts rename to src/modules/music/commands/remove.ts index 7eb3e4f..983f022 100644 --- a/src/commands/remove.ts +++ b/src/modules/music/commands/remove.ts @@ -1,14 +1,15 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { const query = ctx.options.get("query")?.value as string; if (!ctx.guild || !query || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -27,4 +28,4 @@ const data = new SlashCommandBuilder() option.setName("query").setDescription("Position or title to remove.").setRequired(true) ); -export { data }; +export { data, execute }; diff --git a/src/commands/reshuffle.ts b/src/modules/music/commands/reshuffle.ts similarity index 59% rename from src/commands/reshuffle.ts rename to src/modules/music/commands/reshuffle.ts index 2f5fb78..1eb0c9e 100644 --- a/src/commands/reshuffle.ts +++ b/src/modules/music/commands/reshuffle.ts @@ -1,12 +1,13 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { if (!ctx.guild || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -17,4 +18,4 @@ export async function run(client: Client, ctx: CommandInteraction) { const data = new SlashCommandBuilder().setName("reshuffle").setDescription("Shuffle the queue."); -export { data }; +export { data, execute }; diff --git a/src/commands/seek.ts b/src/modules/music/commands/seek.ts similarity index 78% rename from src/commands/seek.ts rename to src/modules/music/commands/seek.ts index 4ac55ec..3d2a405 100644 --- a/src/commands/seek.ts +++ b/src/modules/music/commands/seek.ts @@ -1,16 +1,17 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { const hours = (ctx.options.get("hrs")?.value as number) || 0; const minutes = (ctx.options.get("mins")?.value as number) || 0; const seconds = (ctx.options.get("secs")?.value as number) || 0; if (!ctx.guild || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -30,4 +31,4 @@ const data = new SlashCommandBuilder() .addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek.")) .addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek.")); -export { data }; +export { data, execute }; diff --git a/src/commands/stop.ts b/src/modules/music/commands/stop.ts similarity index 58% rename from src/commands/stop.ts rename to src/modules/music/commands/stop.ts index eff47e6..aa7b5d5 100644 --- a/src/commands/stop.ts +++ b/src/modules/music/commands/stop.ts @@ -1,12 +1,13 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { if (!ctx.guild || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -17,4 +18,4 @@ export async function run(client: Client, ctx: CommandInteraction) { const data = new SlashCommandBuilder().setName("stop").setDescription("Destroy the player."); -export { data }; +export { data, execute }; diff --git a/src/commands/volume.ts b/src/modules/music/commands/volume.ts similarity index 70% rename from src/commands/volume.ts rename to src/modules/music/commands/volume.ts index ca83f3c..376ff07 100644 --- a/src/commands/volume.ts +++ b/src/modules/music/commands/volume.ts @@ -1,14 +1,15 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; +import { controller } from "../module.js"; -export async function run(client: Client, ctx: CommandInteraction) { +async function execute(client: Client, ctx: CommandInteraction) { const volume = (ctx.options.get("volume")?.value as number) || 100; if (!ctx.guild || !ctx.channel) return; - const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); + const player = controller.get(ctx.guild.id, ctx.channel as TextChannel); if (!player) return; @@ -24,4 +25,4 @@ const data = new SlashCommandBuilder() option.setName("volume").setDescription("Volume in percents.").setRequired(true) ); -export { data }; +export { data, execute }; diff --git a/src/events/voice-state-update.ts b/src/modules/music/events/voice-state-update.ts similarity index 64% rename from src/events/voice-state-update.ts rename to src/modules/music/events/voice-state-update.ts index 037255d..a937f24 100644 --- a/src/events/voice-state-update.ts +++ b/src/modules/music/events/voice-state-update.ts @@ -1,13 +1,14 @@ import { VoiceState } from "discord.js"; -import Client from "../lib/client.js"; +import Client from "../../../lib/client.js"; import I18n from "../lib/i18n.js"; import { asleep } from "../lib/misc.js"; +import { controller } from "../module.js"; -export async function run(client: Client, args: VoiceState[]): Promise { - const [oldState] = args; +async function execute(client: Client, eventee: VoiceState[]): Promise { + const [oldState] = eventee; - const player = client.modules.music.get(oldState.guild.id); + const player = controller.get(oldState.guild.id); if (!player || !oldState.channel || !client.user) return; @@ -20,4 +21,6 @@ export async function run(client: Client, args: VoiceState[]): Promise { } } -export const name = "voiceStateUpdate"; +const name = "voiceStateUpdate"; + +export { execute, name }; diff --git a/src/lib/fetch.ts b/src/modules/music/lib/fetch.ts similarity index 73% rename from src/lib/fetch.ts rename to src/modules/music/lib/fetch.ts index 96226e9..995570f 100644 --- a/src/lib/fetch.ts +++ b/src/modules/music/lib/fetch.ts @@ -2,12 +2,12 @@ import { execFile } from "child_process"; import path from "path"; import { promisify } from "util"; -import config from "../config.js"; -import { ytdlArgs, ytdlResponse } from "./types.js"; +import config from "../../../config.js"; +import { YtdlArgs, YtdlResponse } from "./types.js"; const execFileAsync = promisify(execFile); -function argumentsResolver(args: ytdlArgs): string[] { +function argumentsResolver(args: YtdlArgs): string[] { const options = Object.entries(args); return options.map(([key, value]) => { if (typeof value === "boolean" && value) { @@ -18,10 +18,10 @@ function argumentsResolver(args: ytdlArgs): string[] { }); } -async function ytdlExec(query: string, args: ytdlArgs): Promise { +async function ytdlExec(query: string, args: YtdlArgs): Promise { try { const executable = await execFileAsync( - path.join(config.binaries, "/youtube-dl"), + path.join(config.binariesDir, "/youtube-dl"), [query].concat(argumentsResolver(args)) ); return JSON.parse(executable.stdout); @@ -33,8 +33,8 @@ async function ytdlExec(query: string, args: ytdlArgs): Promise { + args: YtdlArgs +): Promise { let response = await ytdlExec(query, args); if (response && !response.extractor.includes("youtube")) { diff --git a/src/lib/filter.ts b/src/modules/music/lib/filter.ts similarity index 100% rename from src/lib/filter.ts rename to src/modules/music/lib/filter.ts diff --git a/src/lib/i18n.ts b/src/modules/music/lib/i18n.ts similarity index 92% rename from src/lib/i18n.ts rename to src/modules/music/lib/i18n.ts index 312f151..72fd363 100644 --- a/src/lib/i18n.ts +++ b/src/modules/music/lib/i18n.ts @@ -4,11 +4,11 @@ import { EmbedBuilder } from "discord.js"; import { loopMoji, secToISO, statusBarGenerator, volumeMoji } from "./misc.js"; import Player from "./player.js"; import Queue from "./queue.js"; -import { loopOption, track } from "./types.js"; +import { LoopOption, Track } from "./types.js"; export default class I18n { public static en = { - nowPlaying: (track: track) => + nowPlaying: (track: Track) => new EmbedBuilder() .setColor("LuminousVividPink") .setAuthor({ @@ -44,7 +44,7 @@ export default class I18n { ? `**✅ ${filter} was applied to the player.**` : "**✅ All filters were declined.**" ), - appended: (track?: track) => + appended: (track?: Track) => !track ? new EmbedBuilder() .setColor("Red") @@ -64,7 +64,7 @@ export default class I18n { state ? new EmbedBuilder().setColor("Green").setTitle("_**Perfoming jump.**_") : new EmbedBuilder().setColor("Red").setTitle("**❗ Bad _jump_ request.**"), - looped: (query: loopOption) => { + looped: (query: LoopOption) => { switch (query) { case "AUTO": { return new EmbedBuilder() @@ -93,7 +93,7 @@ export default class I18n { state ? new EmbedBuilder().setColor("Green").setTitle("_**▶️ Resumed.**_") : new EmbedBuilder().setColor("Yellow").setTitle("_**⏸️ Paused.**_"), - removed: (track?: track) => + removed: (track?: Track) => track ? new EmbedBuilder() .setColor("Green") @@ -208,7 +208,7 @@ export default class I18n { ]); } }, - queueDesigner: (queue: Queue, page: number, resource: AudioResource | undefined) => { + queueDesigner: (queue: Queue, page: number, resource: AudioResource | undefined) => { page = Math.max(0, Math.min(Math.floor(queue.Tracks.length / 11), page)); return new EmbedBuilder() @@ -249,22 +249,6 @@ export default class I18n { .setTitle("🥬 I have decided to leave to conserve my energy.") .setDescription("_**Since there was nobody in the voice channel.**_"), reshuffle: () => - new EmbedBuilder().setColor("Green").setTitle("**✅ Successfuly reshuffled queue.**"), - ping: (ping?: number) => { - const title = ping ? `I have pinged ${ping} ms late.` : "There is no application."; - - const comment = !ping - ? "How am I speaking then?" - : ping > 250 - ? "I'm kinda slowpoke ngl." - : ping > 200 - ? "I can do better than this." - : "Dream's speed!"; - - return new EmbedBuilder() - .setColor("Random") - .setTitle(`**${title}**`) - .setDescription(`_**${comment}**_`); - } + new EmbedBuilder().setColor("Green").setTitle("**✅ Successfuly reshuffled queue.**") }; } diff --git a/src/lib/misc.ts b/src/modules/music/lib/misc.ts similarity index 95% rename from src/lib/misc.ts rename to src/modules/music/lib/misc.ts index 63179f3..09d26c6 100644 --- a/src/lib/misc.ts +++ b/src/modules/music/lib/misc.ts @@ -1,4 +1,4 @@ -import { loopOption } from "./types.js"; +import { LoopOption } from "./types.js"; export function statusBarGenerator(duration: number, length: number): string { duration = Math.max(0, duration); @@ -33,7 +33,7 @@ export function volumeMoji(amount: number): string { return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇"; } -export function loopMoji(loop: loopOption): string { +export function loopMoji(loop: LoopOption): string { return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂"; } diff --git a/src/lib/music.ts b/src/modules/music/lib/music.ts similarity index 100% rename from src/lib/music.ts rename to src/modules/music/lib/music.ts diff --git a/src/lib/player.ts b/src/modules/music/lib/player.ts similarity index 95% rename from src/lib/player.ts rename to src/modules/music/lib/player.ts index 8c0043a..37c4cc8 100644 --- a/src/lib/player.ts +++ b/src/modules/music/lib/player.ts @@ -19,7 +19,7 @@ import I18n from "./i18n.js"; import { largestCommonSequence } from "./misc.js"; import Music from "./music.js"; import Queue from "./queue.js"; -import { track } from "./types.js"; +import { Track } from "./types.js"; export default class Player { private music: Music; @@ -52,8 +52,8 @@ export default class Player { return this.queue; } - private resource: AudioResource | undefined; - public get Resource(): AudioResource | undefined { + private resource: AudioResource | undefined; + public get Resource(): AudioResource | undefined { return this.resource; } @@ -84,7 +84,7 @@ export default class Player { this.player.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable()); } - private resourceGenerator(track: track, seek?: number) { + private resourceGenerator(track: Track, seek?: number) { const stream = ytdl(track.url, { opusEncoded: false, fmt: "mp3", @@ -116,7 +116,7 @@ export default class Player { } } - private async messageResolvable(track: track) { + private async messageResolvable(track: Track) { if (this.message) await this.message.delete(); this.message = await this.channel.send({ @@ -124,7 +124,7 @@ export default class Player { }); } - private async search(query: string, requester: User): Promise { + private async search(query: string, requester: User): Promise { const response = await fetchStream(query, { dumpSingleJson: true, noWarnings: true, @@ -136,7 +136,7 @@ export default class Player { if (!response) return undefined; - const tracks: track[] = []; + const tracks: Track[] = []; if (response.entries) { for (const entry of response.entries) { @@ -202,7 +202,7 @@ export default class Player { }); } - public async add(query: string, requester: User, index?: number): Promise { + public async add(query: string, requester: User, index?: number): Promise { const tracks = await this.search(query, requester); if (!tracks) return; @@ -234,7 +234,7 @@ export default class Player { return false; } - public remove(query?: number | string): track | undefined { + public remove(query?: number | string): Track | undefined { const oldPosition = this.queue.Position; let newPosition = typeof query === "number" ? query : this.queue.Position; diff --git a/src/lib/queue.ts b/src/modules/music/lib/queue.ts similarity index 84% rename from src/lib/queue.ts rename to src/modules/music/lib/queue.ts index 9ac2dee..236cbc9 100644 --- a/src/lib/queue.ts +++ b/src/modules/music/lib/queue.ts @@ -1,8 +1,8 @@ -import { loopOption, track } from "./types.js"; +import { LoopOption, Track } from "./types.js"; export default class Queue { - private tracks: track[] = []; - public get Tracks(): track[] { + private tracks: Track[] = []; + public get Tracks(): Track[] { return this.tracks; } private position = 0; @@ -10,11 +10,11 @@ export default class Queue { return this.position; } - private loop: loopOption = "NONE"; - public get Loop(): loopOption { + private loop: LoopOption = "NONE"; + public get Loop(): LoopOption { return this.loop; } - public set Loop(x: loopOption) { + public set Loop(x: LoopOption) { this.loop = x; } @@ -23,7 +23,7 @@ export default class Queue { return this.duration; } - public next(): track | undefined { + public next(): Track | undefined { if (this.loop === "SONG") return this.current(); if (this.loop === "AUTO") { @@ -59,7 +59,7 @@ export default class Queue { this.position = 0; } - public push(tracks: track[], index?: number): track { + public push(tracks: Track[], index?: number): Track { if (!index || index >= this.tracks.length) { this.tracks = this.tracks.concat(tracks); } else { @@ -74,7 +74,7 @@ export default class Queue { return this.tracks[index || this.tracks.length - 1]; } - public pop(query: number): track | undefined { + public pop(query: number): Track | undefined { if (query >= 0 && query < this.tracks.length) { const track = this.tracks[query]; @@ -87,7 +87,7 @@ export default class Queue { return undefined; } - public jump(query: number): track | undefined { + public jump(query: number): Track | undefined { if (query >= 0 && query < this.tracks.length) { const track = this.tracks[query]; this.position = query; @@ -97,7 +97,7 @@ export default class Queue { return undefined; } - public current(): track { + public current(): Track { return this.tracks[this.position]; } diff --git a/src/modules/music/lib/types.ts b/src/modules/music/lib/types.ts new file mode 100644 index 0000000..0752a65 --- /dev/null +++ b/src/modules/music/lib/types.ts @@ -0,0 +1,261 @@ +import { User } from "discord.js"; + +export interface Track { + url: string; + title: string; + thumbnail: string; + duration: number; + requester: User; +} + +export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO"; + +export interface YtdlArgs { + help?: boolean; + version?: boolean; + update?: boolean; + ignoreErrors?: boolean; + abortOnError?: boolean; + dumpUserAgent?: boolean; + listExtractors?: boolean; + extractorDescriptions?: boolean; + forceGenericExtractor?: boolean; + defaultSearch?: string; + igonreConfig?: boolean; + configLocation?: string; + flatPlaylist?: boolean; + markWatched?: boolean; + noColor?: boolean; + proxy?: string; + socketTimeout?: number; + sourceAddress?: string; + forceIpv4?: boolean; + forceIpv6?: boolean; + geoVerificationProxy?: string; + geoBypass?: boolean; + geoBypassCountry?: string; + geoBypassIpBlock?: string; + playlistStart?: number; + playlistEnd?: number | "last"; + playlistItems?: string; + matchTitle?: string; + rejectTitle?: string; + maxDownloads?: number; + minFilesize?: string; + maxFilesize?: string; + date?: string; + datebefore?: string; + dateafter?: string; + minViews?: number; + maxViews?: number; + matchFilter?: string; + noPlaylist?: boolean; + yesPlaylist?: boolean; + ageLimit?: number; + downloadArchive?: string; + includeAds?: boolean; + limitRate?: string; + retries?: number | "infinite"; + skipUnavailableFragments?: boolean; + abortOnUnavailableFragment?: boolean; + keepFragments?: boolean; + bufferSize?: string; + noResizeBuffer?: boolean; + httpChunkSize?: string; + playlistReverse?: boolean; + playlistRandom?: boolean; + xattrSetFilesize?: boolean; + hlsPreferNative?: boolean; + hlsPreferFfmpeg?: boolean; + hlsUseMpegts?: boolean; + externalDownloader?: string; + externalDownloaderArgs?: string; + batchFile?: string; + id?: boolean; + output?: string; + outputNaPlaceholder?: string; + autonumberStart?: number; + restrictFilenames?: boolean; + noOverwrites?: boolean; + continue?: boolean; + noPart?: boolean; + noMtime?: boolean; + writeDescription?: boolean; + writeInfoJson?: boolean; + writeAnnotations?: boolean; + loadInfoJson?: string; + cookies?: string; + cacheDir?: string; + noCacheDir?: boolean; + rmCacheDir?: boolean; + writeThumbnail?: boolean; + writeAllThumbnails?: boolean; + listThumbnails?: boolean; + quiet?: boolean; + noWarnings?: boolean; + simulate?: boolean; + skipDownload?: boolean; + getUrl?: boolean; + getTitle?: boolean; + getId?: boolean; + getThumbnail?: boolean; + getDuration?: boolean; + getFilename?: boolean; + getFormat?: boolean; + dumpJson?: boolean; + dumpSingleJson?: boolean; + printJson?: boolean; + newline?: boolean; + noProgress?: boolean; + consoleTitle?: boolean; + verbose?: boolean; + dumpPages?: boolean; + writePages?: boolean; + printTraffic?: boolean; + callHome?: boolean; + encoding?: string; + noCheckCertificate?: boolean; + preferInsecure?: boolean; + userAgent?: string; + referer?: string; + addHeader?: string; + bidiWorkaround?: boolean; + sleepInterval?: number; + maxSleepInterval?: number; + format?: string; + allFormats?: boolean; + preferFreeFormats?: boolean; + listFormats?: boolean; + youtubeSkipDashManifest?: boolean; + mergeOutputFormat?: string; + writeSub?: boolean; + writeAutoSub?: boolean; + allSubs?: boolean; + listSubs?: boolean; + subFormat?: string; + subLang?: string; + username?: string; + password?: string; + twofactor?: string; + netrc?: boolean; + videoPassword?: string; + apMso?: string; + apUsername?: string; + apPassword?: string; + apListMso?: boolean; + extractAudio?: boolean; + audioFormat?: string; + audioQuality?: number; + recodeVideo?: string; + postprocessorArgs?: string; + keepVideo?: boolean; + noPostOverwrites?: boolean; + embedSubs?: boolean; + embedThumbnail?: boolean; + addMetadata?: boolean; + metadataFromFile?: string; + xattrs?: boolean; + fixup?: string; + preferAvconv?: boolean; + preferFfmpeg?: boolean; + ffmpegLocation?: string; + exec?: string; + convertSubs?: string; +} + +interface YtdlFormat { + asr: number; + filesize: number; + format_id: string; + format_note: string; + fps: number; + height: number; + quality: number; + tbr: number; + url: string; + width: number; + ext: string; + vcodec: string; + acodec: string; + abr: number; + downloader_options: unknown; + container: string; + format: string; + protocol: string; + http_headers: unknown; +} + +interface YtdlThumbnail { + height: number; + url: string; + width: number; + resolution: string; + id: string; +} + +interface YtdlEntry { + uploader: string; + duration: number; + id: string; + title: string; + url: string; +} + +export interface YtdlResponse { + id: string; + title: string; + formats: YtdlFormat[]; + thumbnails: YtdlThumbnail[]; + description: string; + upload_date: string; + uploader: string; + uploader_id: string; + uploader_url: string; + channel_id: string; + channel_url: string; + duration: number; + view_count: number; + average_rating: number; + age_limit: number; + webpage_url: string; + categories: string[]; + tags: string[]; + is_live: boolean; + like_count: number; + dislike_count: number; + channel: string; + track: string; + artist: string; + creator: string; + alt_title: string; + extractor: string; + webpage_url_basename: string; + extractor_key: string; + playlist: string; + playlist_index: number; + thumbnail: string; + display_id: string; + requested_subtitles: unknown; + asr: number; + filesize: number; + format_id: string; + format_note: string; + fps: number; + height: number; + quality: number; + tbr: number; + url: string; + width: number; + ext: string; + vcodec: string; + acodec: string; + abr: number; + downloader_options: { http_chunk_size: number }; + container: string; + format: string; + protocol: string; + http_headers: unknown; + fulltitle: string; + _filename: string; + entries: YtdlEntry[]; +} diff --git a/src/modules/music/module.ts b/src/modules/music/module.ts new file mode 100644 index 0000000..c5906a1 --- /dev/null +++ b/src/modules/music/module.ts @@ -0,0 +1,10 @@ +import Music from "./lib/music.js"; + +const name = "music"; +const description = "Music module"; +const author = "unknowableshade"; +const tag = "2302.1"; + +const controller = new Music(); + +export { author, controller, description, name, tag }; diff --git a/tsconfig.json b/tsconfig.json index be64d00..ff5b49a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "skipLibCheck": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/commands", "src/events"] }