diff --git a/.gitignore b/.gitignore index ff272f9..71c1404 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ dist logs .env -commands.json \ No newline at end of file + +commands.json +sessions.json \ No newline at end of file diff --git a/biome.json b/biome.json index 31dc227..e52935d 100644 --- a/biome.json +++ b/biome.json @@ -32,7 +32,7 @@ "noExcessiveCognitiveComplexity": { "level": "warn", "options": { - "maxAllowedComplexity": 30 + "maxAllowedComplexity": 32 } } }, diff --git a/package.json b/package.json index 86a27c4..628cde5 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "stelle-music", - "version": "0.2.9.5-BLAZER", + "version": "0.3.0-BLAZER", "description": "A music bot.", "main": "./dist/index.js", "type": "module", - "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1", + "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab", "homepage": "https://github.com/Ganyu-Studios/stelle-music#readme", "scripts": { "build": "tsc", "typecheck": "tsc --noEmit", "clean": "node ./scripts/clean.js && pnpm build", "start": "node ./dist/index.js", - "dev": "tsx watch ./src/index.ts --debug", + "dev": "tsx ./src/index.ts --debug", "lint": "biome lint --write ./src", "format": "biome check --write ./src", "prepare": "husky" @@ -34,17 +34,18 @@ "dependencies": { "@prisma/client": "^5.22.0", "lavalink-client": "^2.4.1", + "meowdb": "^2.2.3", "seyfert": "github:tiramisulabs/seyfert", "yunaforseyfert": "^1.0.4" }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/node": "^22.9.0", - "husky": "^9.1.6", + "@types/node": "^22.10.1", + "husky": "^9.1.7", "lint-staged": "^15.2.10", "prisma": "^5.22.0", "tsx": "^4.19.2", - "typescript": "^5.6.3" + "typescript": "^5.7.2" }, "imports": { "#stelle/client": "./dist/structures/client/Stelle.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47b0d85..8ead6a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,12 @@ importers: lavalink-client: specifier: ^2.4.1 version: 2.4.1 + meowdb: + specifier: ^2.2.3 + version: 2.2.3 seyfert: specifier: github:tiramisulabs/seyfert - version: https://codeload.github.com/tiramisulabs/seyfert/tar.gz/e4233e6a4019734abc4824964acff1a49a69691d + version: https://codeload.github.com/tiramisulabs/seyfert/tar.gz/c14c7d296d3a33804178ab0c15a07589d08a83eb yunaforseyfert: specifier: ^1.0.4 version: 1.0.4 @@ -25,11 +28,11 @@ importers: specifier: ^1.9.4 version: 1.9.4 '@types/node': - specifier: ^22.9.0 - version: 22.9.0 + specifier: ^22.10.1 + version: 22.10.1 husky: - specifier: ^9.1.6 - version: 9.1.6 + specifier: ^9.1.7 + version: 9.1.7 lint-staged: specifier: ^15.2.10 version: 15.2.10 @@ -40,8 +43,8 @@ importers: specifier: ^4.19.2 version: 4.19.2 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.7.2 + version: 5.7.2 packages: @@ -266,8 +269,8 @@ packages: '@prisma/get-platform@5.22.0': resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - '@types/node@22.9.0': - resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + '@types/node@22.10.1': + resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} @@ -304,8 +307,8 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - cross-spawn@7.0.5: - resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} debug@4.3.7: @@ -360,8 +363,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - husky@9.1.6: - resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true @@ -405,6 +408,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + meowdb@2.2.3: + resolution: {integrity: sha512-D2Arz1yZtJ40vWFmUblMKPyuvzXbdKJ5DTMmsnWNRBR0AeFd7eI/oqDXD5tsyHSH7W2jm1i1Y39bD6eTKrdMAA==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -467,8 +473,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - seyfert@https://codeload.github.com/tiramisulabs/seyfert/tar.gz/e4233e6a4019734abc4824964acff1a49a69691d: - resolution: {tarball: https://codeload.github.com/tiramisulabs/seyfert/tar.gz/e4233e6a4019734abc4824964acff1a49a69691d} + seyfert@https://codeload.github.com/tiramisulabs/seyfert/tar.gz/c14c7d296d3a33804178ab0c15a07589d08a83eb: + resolution: {tarball: https://codeload.github.com/tiramisulabs/seyfert/tar.gz/c14c7d296d3a33804178ab0c15a07589d08a83eb} version: 2.1.0 shebang-command@2.0.0: @@ -519,13 +525,13 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -690,9 +696,9 @@ snapshots: dependencies: '@prisma/debug': 5.22.0 - '@types/node@22.9.0': + '@types/node@22.10.1': dependencies: - undici-types: 6.19.8 + undici-types: 6.20.0 ansi-escapes@7.0.0: dependencies: @@ -721,7 +727,7 @@ snapshots: commander@12.1.0: {} - cross-spawn@7.0.5: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -766,7 +772,7 @@ snapshots: execa@8.0.1: dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -793,7 +799,7 @@ snapshots: human-signals@5.0.0: {} - husky@9.1.6: {} + husky@9.1.7: {} is-fullwidth-code-point@4.0.0: {} @@ -849,6 +855,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + meowdb@2.2.3: {} + merge-stream@2.0.0: {} micromatch@4.0.8: @@ -897,7 +905,7 @@ snapshots: rfdc@1.4.1: {} - seyfert@https://codeload.github.com/tiramisulabs/seyfert/tar.gz/e4233e6a4019734abc4824964acff1a49a69691d: {} + seyfert@https://codeload.github.com/tiramisulabs/seyfert/tar.gz/c14c7d296d3a33804178ab0c15a07589d08a83eb: {} shebang-command@2.0.0: dependencies: @@ -944,9 +952,9 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - typescript@5.6.3: {} + typescript@5.7.2: {} - undici-types@6.19.8: {} + undici-types@6.20.0: {} which@2.0.2: dependencies: diff --git a/src/commands/music/nowplaying.ts b/src/commands/music/nowplaying.ts index af4a94f..7f5afaf 100644 --- a/src/commands/music/nowplaying.ts +++ b/src/commands/music/nowplaying.ts @@ -2,6 +2,7 @@ import { Command, type CommandContext, Declare, LocalesT, type User } from "seyf import { StelleOptions } from "#stelle/decorators"; import { StelleCategory } from "#stelle/types"; +import { EmbedColors } from "seyfert/lib/common/index.js"; import { TimeFormat } from "#stelle/utils/TimeFormat.js"; import { createBar } from "#stelle/utils/functions/utils.js"; @@ -26,7 +27,15 @@ export default class NowPlayingCommand extends Command { if (!player) return; const track = player.queue.current; - if (!track) return; + if (!track) + return ctx.editOrReply({ + embeds: [ + { + description: messages.events.noPlayer, + color: EmbedColors.Red, + }, + ], + }); await ctx.editOrReply({ embeds: [ @@ -36,9 +45,9 @@ export default class NowPlayingCommand extends Command { description: messages.commands.nowplaying({ title: track.info.title, url: track.info.uri, - duration: TimeFormat.toHumanize(track.info.duration), + duration: TimeFormat.toDotted(track.info.duration), author: track.info.author, - position: TimeFormat.toHumanize(player.position), + position: TimeFormat.toDotted(player.position), requester: (track.requester as User).id, bar: createBar(player), }), diff --git a/src/commands/music/play.ts b/src/commands/music/play.ts index 4f2e62d..8733976 100644 --- a/src/commands/music/play.ts +++ b/src/commands/music/play.ts @@ -78,7 +78,7 @@ const options = { @LocalesT("locales.play.name", "locales.play.description") export default class PlayCommand extends Command { public override async run(ctx: CommandContext): Promise { - const { options, client, guildId, channelId, member, author } = ctx; + const { options, client, guildId, channelId, member } = ctx; const { query } = options; if (!(guildId && member)) return; @@ -102,11 +102,21 @@ export default class PlayCommand extends Command { selfDeaf: true, }); + const { client: _c1, ...clientUser } = client.me; + const { client: _c2, ...trackRequester } = ctx.author; + if (!player.connected) await player.connect(); - const { loadType, playlist, tracks } = await player.search({ query, source: searchEngine }, author); + const { loadType, playlist, tracks } = await player.search( + { query, source: searchEngine }, + { + ...trackRequester, + tag: ctx.author.tag, + }, + ); - player.set("commandContext", ctx); + player.set("me", clientUser); + player.set("localeString", await ctx.getLocaleString()); if (!bot) bot = client.cache.voiceStates?.get(client.me.id, guildId); if (voice.isStage() && bot?.suppress) await bot.setSuppress(false); diff --git a/src/commands/music/queue.ts b/src/commands/music/queue.ts index 4fe1b2d..1380d69 100644 --- a/src/commands/music/queue.ts +++ b/src/commands/music/queue.ts @@ -10,7 +10,15 @@ import { EmbedPaginator } from "#stelle/utils/Paginator.js"; integrationTypes: ["GuildInstall"], contexts: ["Guild"], }) -@StelleOptions({ cooldown: 5, category: StelleCategory.Music, checkPlayer: true, inVoice: true, sameVoice: true, checkNodes: true }) +@StelleOptions({ + cooldown: 5, + category: StelleCategory.Music, + checkPlayer: true, + inVoice: true, + sameVoice: true, + checkNodes: true, + checkQueue: true, +}) @LocalesT("locales.queue.name", "locales.queue.description") export default class QueueCommand extends Command { public override async run(ctx: CommandContext) { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index c15db58..a09780c 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -5,12 +5,15 @@ export default createEvent({ run: async (guild, client) => { if (guild.unavailable) return; + const owner = await guild.fetchOwner(); + const embed = new Embed() .setColor(client.config.color.success) .setTitle("A new guild added me!") .setDescription("`📦` A new guild has added me! I hope I can be helpful in this journey.") .addFields( { name: "`📜` Name", value: `\`${guild.name}\``, inline: true }, + { name: "`👤` Owner", value: `\`${owner?.displayName ?? "Unknown"}\``, inline: true }, { name: "`🏮` ID", value: `\`${guild.id}\``, inline: true }, { name: "`👥` Members", value: `\`${guild.memberCount}\``, inline: true }, ); diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts index 898102c..d63ec5f 100644 --- a/src/events/guildDelete.ts +++ b/src/events/guildDelete.ts @@ -6,12 +6,15 @@ export default createEvent({ if (guild.unavailable) return; if (!(guild instanceof Guild)) return; + const owner = await guild.fetchOwner(); + const embed = new Embed() .setColor(client.config.color.success) .setTitle("A guild removed me!") .setDescription("`📦` A guild removed me... I think I was not helpful...") .addFields( { name: "`📜` Name", value: `\`${guild.name}\``, inline: true }, + { name: "`👤` Owner", value: `\`${owner?.displayName ?? "Unknown"}\``, inline: true }, { name: "`🏮` ID", value: `\`${guild.id}\``, inline: true }, { name: "`👥` Members", value: `\`${guild.memberCount}\``, inline: true }, ); diff --git a/src/lavalink/node/raw.ts b/src/lavalink/node/raw.ts index ac102ee..c2163a7 100644 --- a/src/lavalink/node/raw.ts +++ b/src/lavalink/node/raw.ts @@ -4,8 +4,5 @@ import { DEBUG_MODE } from "#stelle/data/Constants.js"; export default new Lavalink({ name: "raw", type: "node", - run: (client, node, payload) => { - if (!DEBUG_MODE) return; - return client.debugger?.info(node.id, payload); - }, + run: (client, node, payload) => DEBUG_MODE && client.debugger?.info(`[Node ${node.id}] Payload: `, payload), }); diff --git a/src/lavalink/node/ready.ts b/src/lavalink/node/ready.ts index 1d551f6..c907341 100644 --- a/src/lavalink/node/ready.ts +++ b/src/lavalink/node/ready.ts @@ -3,5 +3,8 @@ import { Lavalink } from "#stelle/classes"; export default new Lavalink({ name: "connect", type: "node", - run: (client, node) => client.logger.info(`Music - The node: ${node.id} is now connected.`), + run: async (client, node) => { + await node.updateSession(true, client.config.resumeTime); + return client.logger.info(`Music - The node: ${node.id} is now connected.`); + }, }); diff --git a/src/lavalink/player/debug.ts b/src/lavalink/player/debug.ts new file mode 100644 index 0000000..edde41e --- /dev/null +++ b/src/lavalink/player/debug.ts @@ -0,0 +1,8 @@ +import { Lavalink } from "#stelle/classes"; +import { DEBUG_MODE } from "#stelle/data/Constants.js"; + +export default new Lavalink({ + name: "debug", + type: "manager", + run: (client, key, data) => DEBUG_MODE && client.debugger?.info(`[Lavalink ${key}] Data:`, data), +}); diff --git a/src/lavalink/player/destroy.ts b/src/lavalink/player/destroy.ts new file mode 100644 index 0000000..c093c19 --- /dev/null +++ b/src/lavalink/player/destroy.ts @@ -0,0 +1,11 @@ +import { Lavalink, sessions } from "#stelle/classes"; +import { DEBUG_MODE } from "#stelle/data/Constants.js"; + +export default new Lavalink({ + name: "playerDestroy", + type: "manager", + run: (client, player) => { + sessions.delete(player.guildId); + return DEBUG_MODE && client.logger.debug(`[Lavalink PlayerDestroy] Destroyed player for guild ${player.guildId}`); + }, +}); diff --git a/src/lavalink/player/empty.ts b/src/lavalink/player/empty.ts index 4ba3071..e906dd7 100644 --- a/src/lavalink/player/empty.ts +++ b/src/lavalink/player/empty.ts @@ -1,6 +1,6 @@ import { Lavalink } from "#stelle/classes"; -import { type CommandContext, Embed } from "seyfert"; +import { Embed } from "seyfert"; export default new Lavalink({ name: "queueEnd", @@ -13,13 +13,13 @@ export default new Lavalink({ await client.messages.edit(messageId, player.textChannelId, { components: [] }).catch(() => null); - const ctx = player.get("commandContext"); - if (!ctx) return; + const locale = player.get("localeString"); + if (!locale) return; const voice = await client.channels.fetch(player.voiceChannelId); if (!voice.is(["GuildStageVoice", "GuildVoice"])) return; - const { messages } = await ctx.getLocale(); + const { messages } = client.t(locale).get(); const embed = new Embed().setDescription(messages.events.playerEnd).setColor(client.config.color.success).setTimestamp(); diff --git a/src/lavalink/player/resumed.ts b/src/lavalink/player/resumed.ts new file mode 100644 index 0000000..3336e3b --- /dev/null +++ b/src/lavalink/player/resumed.ts @@ -0,0 +1,57 @@ +import { Lavalink, sessions } from "#stelle/classes"; +import type { StellePlayerJson } from "#stelle/types"; + +export default new Lavalink({ + name: "resumed", + type: "node", + run: async (client, node, _, players) => { + if (!Array.isArray(players)) return; + + for (const data of players) { + const session = sessions.get(data.guildId); + if (!session) continue; + + if (data.state.connected) { + const player = client.manager.createPlayer({ + guildId: data.guildId, + voiceChannelId: session.voiceChannelId, + textChannelId: session.textChannelId, + selfDeaf: session.options?.selfDeaf, + selfMute: session.options?.selfMute, + volume: client.manager.options.playerOptions?.volumeDecrementer + ? Math.round(data.volume / client.manager.options.playerOptions.volumeDecrementer) + : data.volume, + node: node.id, + applyVolumeAsFilter: session.options.applyVolumeAsFilter, + instaUpdateFiltersFix: session.options.instaUpdateFiltersFix, + vcRegion: session.options.vcRegion, + }); + + if (!player.get("messageId")) player.set("messageId", session.messageId); + if (!player.get("enabledAutoplay")) player.set("enabledAutoplay", session.enabledAutoplay); + if (!player.get("me")) player.set("me", session.me); + if (!player.get("localeString")) player.set("localeString", session.localeString); + + await player.connect(); + + player.filterManager.data = data.filters; + + if (data.track) player.queue.current = client.manager.utils.buildTrack(data.track, session.me); + + if (!player.queue.previous.length) player.queue.previous.unshift(...session.queue!.previous); + if (!player.queue.tracks.length) player.queue.add(session.queue!.tracks); + + player.lastPosition = data.state.position; + player.lastPositionChange = Date.now(); + player.ping.lavalink = data.state.ping; + + player.paused = data.paused; + player.playing = !data.paused && !!data.track; + + await player.queue.utils.save(); + } else { + sessions.delete(data.guildId); + } + } + }, +}); diff --git a/src/lavalink/player/start.ts b/src/lavalink/player/start.ts index 2ca2cda..8779fcb 100644 --- a/src/lavalink/player/start.ts +++ b/src/lavalink/player/start.ts @@ -1,4 +1,4 @@ -import { ActionRow, Button, type CommandContext, Embed, type User } from "seyfert"; +import { ActionRow, Button, Embed, type User } from "seyfert"; import { Lavalink } from "#stelle/classes"; import { ButtonStyle } from "seyfert/lib/types/index.js"; @@ -15,13 +15,13 @@ export default new Lavalink({ const isAutoplay = player.get("enabledAutoplay") ?? false; - const ctx = player.get("commandContext"); - if (!ctx) return; + const locale = player.get("localeString"); + if (!locale) return; const voice = await client.channels.fetch(player.voiceChannelId); if (!voice.is(["GuildStageVoice", "GuildVoice"])) return; - const { messages } = await ctx.getLocale(); + const { messages } = client.t(locale).get(); const duration = track.info.isStream ? messages.commands.play.live diff --git a/src/lavalink/player/update.ts b/src/lavalink/player/update.ts new file mode 100644 index 0000000..5e596c3 --- /dev/null +++ b/src/lavalink/player/update.ts @@ -0,0 +1,38 @@ +import type { ClientUser } from "seyfert"; +import { Lavalink, sessions } from "#stelle/classes"; +import type { StellePlayerJson } from "#stelle/types"; + +import { DEBUG_MODE } from "#stelle/data/Constants.js"; + +export default new Lavalink({ + name: "playerUpdate", + type: "manager", + run: (client, oldPlayer, newPlayer) => { + const newPlayerJson = newPlayer.toJSON(); + + if ( + !oldPlayer || + oldPlayer.voiceChannelId !== newPlayerJson.voiceChannelId || + oldPlayer.textChannelId !== newPlayerJson.textChannelId || + oldPlayer.options.selfDeaf !== newPlayerJson.options.selfDeaf || + oldPlayer.options.selfMute !== newPlayerJson.options.selfDeaf || + oldPlayer.nodeId !== newPlayerJson.nodeId || + oldPlayer.nodeSessionId !== newPlayerJson.nodeSessionId || + oldPlayer.options.applyVolumeAsFilter !== newPlayerJson.options.applyVolumeAsFilter || + oldPlayer.options.instaUpdateFiltersFix !== newPlayerJson.options.instaUpdateFiltersFix || + oldPlayer.options.vcRegion !== newPlayerJson.options.vcRegion + ) { + if (newPlayerJson.queue?.current) newPlayerJson.queue.current.userData = {}; + + sessions.set(newPlayer.guildId, { + ...newPlayerJson, + messageId: newPlayer.get("messageId"), + enabledAutoplay: newPlayer.get("enabledAutoplay"), + localeString: newPlayer.get("localeString"), + me: newPlayer.get("me"), + }); + + return DEBUG_MODE && client.logger.debug(`[Lavalink PlayerUpdate] Saved new player data for guild ${newPlayer.guildId}`); + } + }, +}); diff --git a/src/middlewares/commands/cooldown.ts b/src/middlewares/commands/cooldown.ts index d8751a3..d6456d8 100644 --- a/src/middlewares/commands/cooldown.ts +++ b/src/middlewares/commands/cooldown.ts @@ -5,15 +5,14 @@ import { EmbedColors } from "seyfert/lib/common/index.js"; import { MessageFlags } from "seyfert/lib/types/index.js"; export const checkCooldown = createMiddleware(async ({ context, next, pass }) => { - // This will make Nobody happy. + // This will make someone happy. if (context.isComponent()) return next(); const { client, command } = context; const { cooldowns } = client; - if (command.onlyDeveloper) return next(); - if (!command) return pass(); + if (command.onlyDeveloper) return next(); const cooldown = (command.cooldown ?? 3) * 1000; const timeNow = Date.now(); diff --git a/src/structures/client/Stelle.ts b/src/structures/client/Stelle.ts index f785e56..9cec8b7 100644 --- a/src/structures/client/Stelle.ts +++ b/src/structures/client/Stelle.ts @@ -22,13 +22,34 @@ import { DEBUG_MODE, THINK_MESSAGES } from "#stelle/data/Constants.js"; * Main Stelle class. */ export class Stelle extends Client { + /** + * Stelle cooldowns collection. + */ public readonly cooldowns: LimitedCollection = new LimitedCollection(); + + /** + * Stelle configuration. + */ public readonly config: StelleConfiguration = Configuration; + + /** + * Stelle "token". + */ public readonly token = "🌟" as const; + /** + * The timestamp when Stelle is ready. + */ public readyTimestamp: number = 0; + /** + * Stelle manager instance. + */ public readonly manager: StelleManager; + + /** + * Stelle database instance. + */ public readonly database: StelleDatabase; /** diff --git a/src/structures/client/modules/Manager.ts b/src/structures/client/modules/Manager.ts index e941634..358cf4a 100644 --- a/src/structures/client/modules/Manager.ts +++ b/src/structures/client/modules/Manager.ts @@ -3,6 +3,7 @@ import { LavalinkManager, type SearchPlatform, type Track } from "lavalink-clien import { StelleHandler } from "#stelle/utils/classes/client/Handler.js"; import type { UsingClient } from "seyfert"; +import { DEBUG_MODE } from "#stelle/data/Constants.js"; import { autoPlayFunction } from "#stelle/utils/functions/autoplay.js"; /** @@ -23,6 +24,17 @@ export class StelleManager extends LavalinkManager { queueOptions: { maxPreviousTracks: 25, }, + advancedOptions: { + enableDebugEvents: DEBUG_MODE, + debugOptions: { + logCustomSearches: DEBUG_MODE, + noAudio: DEBUG_MODE, + playerDestroy: { + debugLog: DEBUG_MODE, + dontThrowError: DEBUG_MODE, + }, + }, + }, playerOptions: { defaultSearchPlatform: "spsearch", onDisconnect: { diff --git a/src/structures/listeners/client/playerListener.ts b/src/structures/listeners/client/playerListener.ts index 2d9168f..607abdf 100644 --- a/src/structures/listeners/client/playerListener.ts +++ b/src/structures/listeners/client/playerListener.ts @@ -1,4 +1,4 @@ -import type { CommandContext, UsingClient, VoiceState } from "seyfert"; +import type { UsingClient, VoiceState } from "seyfert"; import { EmbedColors } from "seyfert/lib/common/index.js"; import { TimeFormat } from "#stelle/utils/TimeFormat.js"; @@ -14,10 +14,11 @@ export async function playerListener(client: UsingClient, newState: VoiceState, if (!player) return; if (!(player.textChannelId && player.voiceChannelId)) return; - const ctx = player.get("commandContext"); - if (!ctx) return; - const { messages } = await ctx.getLocale(); + const locale = player.get("localeString"); + if (!locale) return; + + const { messages } = client.t(locale).get(); const channel = await client.channels.fetch(player.voiceChannelId); if (!channel.is(["GuildStageVoice", "GuildVoice"])) return; diff --git a/src/structures/utils/Decorators.ts b/src/structures/utils/Decorators.ts index a223c7c..73d9d83 100644 --- a/src/structures/utils/Decorators.ts +++ b/src/structures/utils/Decorators.ts @@ -1,7 +1,7 @@ import type { BaseCommand } from "seyfert"; import type { NonCommandOptions, Options } from "#stelle/types"; -type Instantiable = { new (...arg: any[]): T }; +type Instantiable = new (...arg: any[]) => T; export function StelleOptions>(options: A extends Instantiable ? Options : NonCommandOptions) { return (target: A) => diff --git a/src/structures/utils/Errors.ts b/src/structures/utils/Errors.ts index 1470880..bf81477 100644 --- a/src/structures/utils/Errors.ts +++ b/src/structures/utils/Errors.ts @@ -32,3 +32,10 @@ export class InvalidPageNumber extends Error { this.name = "Stelle [InvalidPageNumber]"; } } + +export class InvalidSessionId extends Error { + constructor(message: string) { + super(message); + this.name = "Stelle [InvalidSessionId]"; + } +} diff --git a/src/structures/utils/Logger.ts b/src/structures/utils/Logger.ts index 40664a6..123f2f0 100644 --- a/src/structures/utils/Logger.ts +++ b/src/structures/utils/Logger.ts @@ -151,3 +151,15 @@ export function customLogger(_this: Logger, level: LogLevels, args: unknown[]): return [text, ...args]; } + +Logger.customize(customLogger); +Logger.dirname = "logs"; + +/** + * The logger instance. + */ +export const logger = new Logger({ + name: "[Stelle]", + saveOnFile: true, + active: true, +}); diff --git a/src/structures/utils/TimeFormat.ts b/src/structures/utils/TimeFormat.ts index 3d11bf7..f44920e 100644 --- a/src/structures/utils/TimeFormat.ts +++ b/src/structures/utils/TimeFormat.ts @@ -22,7 +22,6 @@ const createMsFormater = (isNormalMode = true, order: typeof TimeUnitsOrder = Ti const isDottedMode = isNormalMode === false; - // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 🐧 function baseFormater(time: number = 0, isChild = false): [string, number] { let targetPosition = 0; let targetUnitValue = 1; diff --git a/src/structures/utils/classes/client/Lavalink.ts b/src/structures/utils/classes/client/Lavalink.ts index 787f33c..70fe9b9 100644 --- a/src/structures/utils/classes/client/Lavalink.ts +++ b/src/structures/utils/classes/client/Lavalink.ts @@ -5,8 +5,17 @@ import type { AllEvents, LavalinkEvent, LavalinkEventRun, LavalinkEventType } fr * Stelle Lavalink events main class. */ export class Lavalink implements LavalinkEvent { + /** + * The event name. + */ readonly name: K; + /** + * The event type. + */ readonly type: LavalinkEventType; + /** + * The event run function. + */ readonly run: LavalinkEventRun; /** diff --git a/src/structures/utils/classes/client/Sessions.ts b/src/structures/utils/classes/client/Sessions.ts new file mode 100644 index 0000000..e3af807 --- /dev/null +++ b/src/structures/utils/classes/client/Sessions.ts @@ -0,0 +1,92 @@ +import type { LavalinkNodeOptions, PlayerJson } from "lavalink-client"; +import { InvalidSessionId } from "#stelle/errors"; + +import MeowDB from "meowdb"; + +/** + * Lavalink node options without the `sessionId`. + */ +type NonResumableOptions = Omit; + +/** + * Stelle Lavalink sessions main class. + */ +export class StelleSessions { + /** + * The storage instance. + */ + readonly storage = new MeowDB({ + dir: process.cwd(), + name: "sessions", + }); + + /** + * The nodes map. + */ + readonly nodes: Map = new Map( + Object.entries(this.storage.all()).map(([_, session]) => [session.nodeId!, session.nodeSessionId!]), + ); + + /** + * + * Set a player session. + * @param guildId The node id. + * @param object The session id. + * @returns The current instance. + */ + public set(guildId: string, object: T): this { + this.storage.set(guildId, object); + return this; + } + + /** + * + * Get a player session. + * @param guildId The node id. + * @returns The session id. + */ + public get(guildId: string): T | undefined { + return this.storage.get(guildId); + } + + /** + * + * Delete a player session. + * @param guildId The node id. + * @returns If the session was deleted. + */ + public delete(guildId: string): boolean { + return this.storage.delete(guildId); + } + + /** + * + * Get a node session. + * @param nodeId The node id. + * @returns The session id. + */ + public getNode(nodeId: string): string | undefined { + return this.nodes.get(nodeId); + } + + /** + * + * Resolve the nodes options. + * @param nodes The array of nodes. + * @returns + */ + public resolve(nodes: NonResumableOptions[]): LavalinkNodeOptions[] { + if (nodes.some((node) => "sessionId" in node && typeof node.sessionId === "string")) + throw new InvalidSessionId("The 'sessionId' property is not allowed in the node options."); + + return nodes.map((node) => ({ + ...node, + sessionId: this.getNode(node.id ?? `${node.host}:${node.port}`), + })); + } +} + +/** + * The Lavalink sessions instance. + */ +export const sessions = new StelleSessions(); diff --git a/src/structures/utils/classes/index.ts b/src/structures/utils/classes/index.ts index 112bd4e..c4661f6 100644 --- a/src/structures/utils/classes/index.ts +++ b/src/structures/utils/classes/index.ts @@ -1,2 +1,3 @@ export { Lavalink } from "./client/Lavalink.js"; export { StelleCache } from "./client/Cache.js"; +export { sessions } from "./client/Sessions.js"; diff --git a/src/structures/utils/data/Configuration.ts b/src/structures/utils/data/Configuration.ts index d6ace23..0e1a021 100644 --- a/src/structures/utils/data/Configuration.ts +++ b/src/structures/utils/data/Configuration.ts @@ -1,5 +1,6 @@ import type { StelleConfiguration } from "#stelle/types"; import { ms } from "#stelle/utils/TimeFormat.js"; +import { sessions } from "../classes/client/Sessions.js"; /** * Stelle configuration. @@ -11,6 +12,7 @@ export const Configuration: StelleConfiguration = { prefixes: ["st!"], defaultLocale: "en-US", disconnectTime: ms("30s"), + resumeTime: ms("1min"), developerIds: [ "391283181665517568", // <-- JustEvil ], @@ -20,7 +22,7 @@ export const Configuration: StelleConfiguration = { "1213361742571241492", // <-- Team Genesis "1003825077969764412", // <-- Seyfert ], - nodes: [ + nodes: sessions.resolve([ { id: "SN #1", // <--- AKA Stelle Node host: "localhost", @@ -30,7 +32,7 @@ export const Configuration: StelleConfiguration = { retryAmount: 10, retryDelay: ms("10s"), }, - ], + ]), color: { success: 0x8d86a8, extra: 0xece8f1, diff --git a/src/structures/utils/data/Constants.ts b/src/structures/utils/data/Constants.ts index 6d8ac14..c07942b 100644 --- a/src/structures/utils/data/Constants.ts +++ b/src/structures/utils/data/Constants.ts @@ -13,7 +13,7 @@ const packageJSON = JSON.parse(await readFile("./package.json", "utf-8")); export const BOT_VERSION: string = packageJSON.version; /** - * Check if Stelle is running un DEBUG MODE. + * Check if Stelle is running in DEBUG MODE. */ export const DEBUG_MODE: boolean = getFlag("--debug"); diff --git a/src/structures/utils/functions/autoplay.ts b/src/structures/utils/functions/autoplay.ts index a66b441..4a6d256 100644 --- a/src/structures/utils/functions/autoplay.ts +++ b/src/structures/utils/functions/autoplay.ts @@ -1,5 +1,5 @@ import type { Player, SourceNames, Track, UnresolvedTrack } from "lavalink-client"; -import type { CommandContext } from "seyfert"; +import type { ClientUser } from "seyfert"; type ResolvableTrack = UnresolvedTrack | Track; @@ -29,8 +29,8 @@ export async function autoPlayFunction(player: Player, lastTrack?: Track): Promi await player.queue.utils.save(); } - const ctx = player.get("commandContext"); - if (!ctx) return; + const me = player.get("me"); + if (!me) return; const filterTracks = (tracks: ResolvableTrack[]) => tracks.filter( @@ -42,8 +42,8 @@ export async function autoPlayFunction(player: Player, lastTrack?: Track): Promi ); const requester = { - ...ctx.client.me, - tag: ctx.client.me.username, + ...me, + tag: me.username, }; if (lastTrack.info.sourceName === "spotify") { diff --git a/src/structures/utils/functions/overrides.ts b/src/structures/utils/functions/overrides.ts index 4792a01..cbcbdaa 100644 --- a/src/structures/utils/functions/overrides.ts +++ b/src/structures/utils/functions/overrides.ts @@ -5,6 +5,13 @@ import { MessageFlags } from "seyfert/lib/types/index.js"; import { formatOptions } from "./formatter.js"; +/** + * + * The Stelle's default error handler. + * @param ctx The context of the command. + * @param error The error that was thrown. + * @returns + */ export async function onRunError(ctx: AnyContext, error: unknown) { const { messages } = await ctx.getLocale(); @@ -22,6 +29,13 @@ export async function onRunError(ctx: AnyContext, error: unknown) { }); } +/** + * + * The Stelle's default error handler for missing permissions. + * @param ctx The context of the command. + * @param permissions The permissions that the user is missing. + * @returns + */ export async function onPermissionsFail(ctx: AnyContext, permissions: PermissionStrings) { const { messages } = await ctx.getLocale(); @@ -43,6 +57,13 @@ export async function onPermissionsFail(ctx: AnyContext, permissions: Permission }); } +/** + * + * The Stelle's default error handler for missing bot permissions. + * @param ctx The context of the command. + * @param permissions The permissions that the bot is missing. + * @returns + */ export async function onBotPermissionsFail(ctx: AnyContext, permissions: PermissionStrings) { const { messages } = await ctx.getLocale(); @@ -64,6 +85,12 @@ export async function onBotPermissionsFail(ctx: AnyContext, permissions: Permiss }); } +/** + * + * The Stelle's default error handler for invalid options. + * @param ctx The context of the command. + * @returns + */ export async function onOptionsError(ctx: AnyContext) { if (!ctx.isChat()) return; diff --git a/src/structures/utils/functions/utils.ts b/src/structures/utils/functions/utils.ts index 23f1590..ad8c7e8 100644 --- a/src/structures/utils/functions/utils.ts +++ b/src/structures/utils/functions/utils.ts @@ -10,10 +10,16 @@ export const customContext = extendContext((interaction) => ({ /** * * Get the locale from the database. - * @returns + * @returns The locales object. */ getLocale: async (): Promise => interaction.client.t(await interaction.client.database.getLocale(interaction.guildId!)).get(), + /** + * + * Get the locale string from the database. + * @returns The locale string. + */ + getLocaleString: () => interaction.client.database.getLocale(interaction.guildId!), })); /** @@ -76,7 +82,7 @@ export const getFlag = (flag: string) => process.argv.includes(flag); * @param error The error. * @returns */ -export const getDepth = (error: any): string => inspect(error, { depth: 0 }); +export const getDepth = (error: any, depth: number = 0): string => inspect(error, { depth }); /** * diff --git a/src/structures/utils/types/client/StelleConfiguration.ts b/src/structures/utils/types/client/StelleConfiguration.ts index c0b23da..5e72475 100644 --- a/src/structures/utils/types/client/StelleConfiguration.ts +++ b/src/structures/utils/types/client/StelleConfiguration.ts @@ -58,6 +58,11 @@ export interface StelleConfiguration { * @default ms("30s") */ disconnectTime: number; + /** + * Stelle node(s) resume time. + * @default ms("1min") + */ + resumeTime: number; /** Stelle developer id(s). */ developerIds: string[]; /** Stelle developer guild id(s). */ @@ -68,5 +73,8 @@ export interface StelleConfiguration { color: StelleColors; /** Stelle channels. */ channels: StelleChannels; + /** + * Stelle cache. + */ cache: StelleCache; } diff --git a/src/structures/utils/types/client/StelleLavalink.ts b/src/structures/utils/types/client/StelleLavalink.ts index cfe5888..3943847 100644 --- a/src/structures/utils/types/client/StelleLavalink.ts +++ b/src/structures/utils/types/client/StelleLavalink.ts @@ -2,10 +2,24 @@ import type { LavalinkManagerEvents, NodeManagerEvents } from "lavalink-client"; import type { UsingClient } from "seyfert"; import type { Awaitable } from "seyfert/lib/common/index.js"; +/** + * All lavalink events. + */ export type AllEvents = LavalinkManagerEvents & NodeManagerEvents; + +/** + * Lavalink event run function. + */ export type LavalinkEventRun = (client: UsingClient, ...args: Parameters) => Awaitable; + +/** + * Lavalink event type. + */ export type LavalinkEventType = K extends keyof NodeManagerEvents ? "node" : "manager"; +/** + * Lavalink event interface. + */ export interface LavalinkEvent { /** The event name. */ name: K; diff --git a/src/structures/utils/types/index.ts b/src/structures/utils/types/index.ts index 9cb9f8a..d13bcd9 100644 --- a/src/structures/utils/types/index.ts +++ b/src/structures/utils/types/index.ts @@ -1,4 +1,5 @@ -import type { InternalRuntimeConfig, InternalRuntimeConfigHTTP } from "seyfert/lib/client/base.js"; +import type { PlayerJson } from "lavalink-client"; +import type { ClientUser } from "seyfert"; import type { PermissionFlagsBits } from "seyfert/lib/types/index.js"; export type { StelleConfiguration } from "./client/StelleConfiguration.js"; @@ -7,10 +8,14 @@ export type { AllEvents, LavalinkEvent, LavalinkEventRun, LavalinkEventType } fr export type PermissionNames = keyof typeof PermissionFlagsBits; export type AutoplayMode = "enabled" | "disabled"; export type PausedMode = "pause" | "resume"; -export type InternalRuntime = InternalRuntimeConfigHTTP | InternalRuntimeConfig; - export type NonCommandOptions = Omit; +export type StellePlayerJson = PlayerJson & { + messageId?: string; + enabledAutoplay?: boolean; + me?: ClientUser; + localeString?: string; +}; export interface Options { /** The cooldown. */ cooldown?: number;