diff --git a/.replit b/.replit index eb388a44e..95fd78844 100644 --- a/.replit +++ b/.replit @@ -1,2 +1,24 @@ -language="nodejs" -run="npm start" +run = "npm run start" + +[languages.typescript] +pattern = "**/{*.ts,*.js,*.tsx,*.jsx}" +syntax = "typescript" + +[languages.typescript.languageServer] +start = [ "typescript-language-server", "--stdio" ] + +[packager] +language = "nodejs" + +[packager.features] +packageSearch = true +guessImports = true + +[env] +XDG_CONFIG_HOME = "/home/runner/.config" + +[nix] +channel = "stable-21_11" + +[gitHubImport] +requiredFiles = [".replit", "replit.nix", ".config"] diff --git a/.swcrc b/.swcrc index dc76be63b..7970a7082 100644 --- a/.swcrc +++ b/.swcrc @@ -1,4 +1,5 @@ { + "$schema": "https://json.schemastore.org/swcrc", "jsc": { "parser": { "syntax": "typescript", diff --git a/Procfile b/Procfile deleted file mode 100644 index 9ebe8e88e..000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -worker: npm start diff --git a/README.md b/README.md index f158aab46..accfe26ae 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,9 @@ $ npm start ## Hosting Setup -### Heroku -You can host this bot to make it stay online on Heroku. - -Deploy to Heroku + + Setup Guide Video + ### Glitch You can use Glitch too for this project, featured with its code editor. @@ -46,7 +45,7 @@ You can use Glitch too for this project, featured with its code editor. 2. Go to [glitch.com](https://glitch.com) and make an account 3. Click **New Project** then **Import from GitHub**, specify the pop-up field with `https://github.com//rawon` (without `<>`) 4. Please wait for a while, this process takes some minutes -5. Find `.env` file and delete it, find `.env_example` file and rename it back to `.env` +5. Find `.env` file and delete it, then find `.env_example` file and rename it to `.env` 6. After specifying `.env`, open **Tools** > **Terminal** 7. Type `refresh`, and track the process from **Logs** diff --git a/app.json b/app.json deleted file mode 100644 index 546d4c450..000000000 --- a/app.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "Rawon", - "description": "A simple powerful Discord music bot built to fulfill your production desires. Easy to use, with no coding required.", - "logo": "https://api.clytage.org/assets/images/rawon.png", - "env": { - "DISCORD_TOKEN": { - "description": "What is your Discord bot's token? | Example: NTE5NjQ2MjIxNTU2Nzc2OTcw.XAcEQQ.0gjhNbGeWBsKP6FVuIyZWlG2cMd", - "required": true - }, - "MAIN_PREFIX": { - "description": "What should be the main prefix of your bot? | Example: !", - "required": true, - "value": "!" - }, - "ALT_PREFIX": { - "description": "What should be the alternative prefixes of your bot? | Example: \"?, {mention}\" | Formats: {mention} = @bot mention", - "required": false, - "value": "{mention}" - }, - "EMBED_COLOR": { - "description": "What should be your bot's embed color code? (hex) | Example: 22C9FF", - "required": false, - "value": "22C9FF" - }, - "LOCALE": { - "description": "What should be the language of your bot? | Example: en | Available: en, es, id", - "required": false, - "value": "en" - }, - "ACTIVITIES": { - "description": "Activity list, what text should be appear on your bot's status? | Example: \"Hello world!, My prefix is {prefix}\" | Formats: {prefix} = bot prefix, {userCount} = user amount, {textChannelCount} = text channel amount, {serverCount} = server amount, {playingCount} = amount of server playing music using the bot, {username} = bot username", - "required": false, - "value": "My default prefix is {prefix}, music with {userCount} users, {textChannelCount} text channels in {serverCount} guilds, 'Hello there, my name is {username}'" - }, - "ACTIVITY_TYPES": { - "description": "Activity type list. The order of this value is the same as ACTIVITIES. For example, first value of ACTIVITIES will use first value of this. | Example: \"PLAYING, COMPETING\" | Available: PLAYING, WATCHING, LISTENING, COMPETING", - "required": false, - "value": "PLAYING, LISTENING, WATCHING, PLAYING, COMPETING" - }, - "MAIN_GUILD": { - "description": "What is your server's ID? | Example: \"972407605295198258, 972407605295198258\"", - "required": false - }, - "STREAM_STRATEGY": { - "description": "Which youtube downloader do you want to use? But if you use play-dl, it will support a few sites. | Example: play-dl | Available: play-dl, yt-dlp", - "required": false, - "value": "yt-dlp" - }, - "ENABLE_SLASH_COMMAND": { - "description": "Do you want to enable slash command support? | Example: yes", - "required": false, - "value": "yes" - }, - "MUSIC_SELECTION_TYPE": { - "description": "Which music selection type do you want to use? | Example: selectmenu | Available: message (just like in the previous version), selectmenu (uses discord selection menu)", - "required": false, - "value": "message" - }, - "ENABLE_24_7_COMMAND": { - "description": "Do you want to enable the 24/7 command? | Example: no", - "required": false, - "value": "no" - }, - "STAY_IN_VC_AFTER_FINISHED": { - "description": "Do you want to make your bot not leaving the voice channel after playing a song? | Example: no", - "required": false, - "value": "no" - }, - "YES_EMOJI": { - "description": "What should be your bot's emoji for every success sentence? | Example: ✅", - "required": false, - "value": "✅" - }, - "NO_EMOJI": { - "description": "What should be your bot's emoji for every failed sentence? | Example: ❌", - "required": false, - "value": "❌" - } - }, - "repository": "https://github.com/Clytage/rawon", - "website": "https://rawon.clytage.org", - "formation": { - "worker": { - "quantity": 1, - "size": "free" - } - } -} diff --git a/index.js b/index.js index a56b346cd..9da5e9cf7 100644 --- a/index.js +++ b/index.js @@ -1,52 +1,37 @@ import { downloadExecutable } from "./yt-dlp-utils"; +import { existsSync, readFileSync, writeFileSync, rmSync } from "fs"; import { execSync } from "child_process"; -import { existsSync, rmSync } from "fs"; import { resolve } from "path"; import { Server } from "https"; import module from "module"; -const isGlitch = ( - process.env.PROJECT_DOMAIN !== undefined && - process.env.PROJECT_INVITE_TOKEN !== undefined && - process.env.API_SERVER_EXTERNAL !== undefined && - process.env.PROJECT_REMIX_CHAIN !== undefined); +const ensureEnv = arr => arr.every(x => process.env[x] !== undefined); -const isReplit = ( - process.env.REPLIT_DB_URL !== undefined && - process.env.REPL_ID !== undefined && - process.env.REPL_IMAGE !== undefined && - process.env.REPL_LANGUAGE !== undefined && - process.env.REPL_OWNER !== undefined && - process.env.REPL_PUBKEYS !== undefined && - process.env.REPL_SLUG !== undefined) +const isGlitch = ensureEnv([ + "PROJECT_DOMAIN", + "PROJECT_INVITE_TOKEN", + "API_SERVER_EXTERNAL", + "PROJECT_REMIX_CHAIN" +]); -const isGitHub = ( - process.env.GITHUB_ENV !== undefined && - process.env.GITHUB_EVENT_PATH !== undefined && - process.env.GITHUB_REPOSITORY_OWNER !== undefined && - process.env.GITHUB_RETENTION_DAYS !== undefined && - process.env.GITHUB_HEAD_REF !== undefined && - process.env.GITHUB_GRAPHQL_URL !== undefined && - process.env.GITHUB_API_URL !== undefined && - process.env.GITHUB_WORKFLOW !== undefined && - process.env.GITHUB_RUN_ID !== undefined && - process.env.GITHUB_BASE_REF !== undefined && - process.env.GITHUB_ACTION_REPOSITORY !== undefined && - process.env.GITHUB_ACTION !== undefined && - process.env.GITHUB_RUN_NUMBER !== undefined && - process.env.GITHUB_REPOSITORY !== undefined && - process.env.GITHUB_ACTION_REF !== undefined && - process.env.GITHUB_ACTIONS !== undefined && - process.env.GITHUB_WORKSPACE !== undefined && - process.env.GITHUB_JOB !== undefined && - process.env.GITHUB_SHA !== undefined && - process.env.GITHUB_RUN_ATTEMPT !== undefined && - process.env.GITHUB_REF !== undefined && - process.env.GITHUB_ACTOR !== undefined && - process.env.GITHUB_PATH !== undefined && - process.env.GITHUB_EVENT_NAME !== undefined && - process.env.GITHUB_SERVER_URL !== undefined -) +const isReplit = ensureEnv([ + "REPLIT_DB_URL", + "REPL_ID", + "REPL_IMAGE", + "REPL_LANGUAGE", + "REPL_OWNER", + "REPL_PUBKEYS", + "REPL_SLUG" +]); + +const isGitHub = ensureEnv([ + "GITHUB_ENV", + "GITHUB_REPOSITORY_OWNER", + "GITHUB_HEAD_REF", + "GITHUB_API_URL", + "GITHUB_REPOSITORY", + "GITHUB_SERVER_URL" +]); function npmInstall(deleteDir = false, forceInstall = false, additionalArgs = []) { if (deleteDir) { @@ -54,7 +39,8 @@ function npmInstall(deleteDir = false, forceInstall = false, additionalArgs = [] if (existsSync(modulesPath)) { rmSync(modulesPath, { - recursive: true + recursive: true, + force: true }); } } @@ -63,6 +49,17 @@ function npmInstall(deleteDir = false, forceInstall = false, additionalArgs = [] } if (isGlitch) { + const gitIgnorePath = resolve(process.cwd(), ".gitignore"); + try { + const data = readFileSync(gitIgnorePath, "utf8").toString(); + if (data.includes("dev.env")) { + writeFileSync(gitIgnorePath, data.replace("\ndev.env", "")); + console.info("Removed dev.env from .gitignore"); + } + } catch { + console.error("Failed to remove dev.env from .gitignore"); + } + try { console.info("[INFO] Trying to re-install modules..."); npmInstall(); @@ -84,24 +81,13 @@ if (isGlitch) { } } -if (isReplit) { - console.warn("[WARN] We haven't added stable support for running this bot using Replit, bugs and errors may come up."); - - if (Number(process.versions.node.split(".")[0]) < 16) { - console.info("[INFO] This Replit doesn't use Node.js v16 or newer, trying to install Node.js v16..."); - execSync(`npm i --save-dev node@16.6.1 && npm config set prefix=$(pwd)/node_modules/node && export PATH=$(pwd)/node_modules/node/bin:$PATH`); - console.info("[INFO] Node.js v16 has been installed, please restart the bot."); - process.exit(0); - } -} - if (isGitHub) { console.warn("[WARN] Running this bot using GitHub is not recommended."); } const require = module.createRequire(import.meta.url); -if (!isGlitch) { +if (!isGlitch && !isReplit) { try { require("ffmpeg-static"); } catch { diff --git a/package-lock.json b/package-lock.json index 79c36b41c..2dac8d875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rawon", - "version": "3.0.0", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rawon", - "version": "3.0.0", + "version": "3.1.0-dev", "license": "BSD-3-Clause", "dependencies": { "@discordjs/voice": "^0.11.0", @@ -108,6 +108,11 @@ "node": ">=16.9.0" } }, + "node_modules/@discordjs/voice/node_modules/discord-api-types": { + "version": "0.37.12", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.12.tgz", + "integrity": "sha512-SMBP4V6/A9mE7shBQAiTxNWnQlYTdiKMGvc7G23neayxaTJeFYh5FviJSWUa0BTdXcph1h/jT03Nbyv5XgZkzw==" + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -2791,6 +2796,12 @@ "prism-media": "^1.3.4", "tslib": "^2.4.0", "ws": "^8.8.1" + }, + "dependencies": { + "discord-api-types": { + "version": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.12.tgz", + "integrity": "sha512-SMBP4V6/A9mE7shBQAiTxNWnQlYTdiKMGvc7G23neayxaTJeFYh5FviJSWUa0BTdXcph1h/jT03Nbyv5XgZkzw==" + } } }, "@eslint/eslintrc": { diff --git a/package.json b/package.json index b32f49404..36d0e885d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rawon", - "version": "3.0.0", + "version": "3.1.0", "description": "A simple powerful Discord music bot built to fulfill your production desires. Easy to use, with no coding required.", "main": "index.js", "type": "module", diff --git a/replit.nix b/replit.nix new file mode 100644 index 000000000..6dd47d18d --- /dev/null +++ b/replit.nix @@ -0,0 +1,12 @@ +{ pkgs }: { + deps = [ + pkgs.python38 + pkgs.ffmpeg.bin + pkgs.yarn + pkgs.esbuild + pkgs.nodejs-16_x + + pkgs.nodePackages.typescript + pkgs.nodePackages.typescript-language-server + ]; +} diff --git a/src/commands/moderation/ModLogsCommand.ts b/src/commands/moderation/ModLogsCommand.ts index 950d35015..df36c95fb 100644 --- a/src/commands/moderation/ModLogsCommand.ts +++ b/src/commands/moderation/ModLogsCommand.ts @@ -103,7 +103,9 @@ export class ModLogsCommand extends BaseCommand { ctx.reply({ embeds: [ createEmbed("info") - .setAuthor(i18n.__("commands.moderation.modlogs.embedTitle")) + .setAuthor({ + name: i18n.__("commands.moderation.modlogs.embedTitle") + }) .addField( `${this.client.config.mainPrefix}modlogs enable`, i18n.__("commands.moderation.modlogs.slashEnableDescription") diff --git a/src/commands/music/DJCommand.ts b/src/commands/music/DJCommand.ts index 597be4d3c..37f92c6d5 100644 --- a/src/commands/music/DJCommand.ts +++ b/src/commands/music/DJCommand.ts @@ -43,7 +43,9 @@ export class DJCommand extends BaseCommand { ctx.reply({ embeds: [ createEmbed("info") - .setAuthor(i18n.__("commands.music.dj.embedTitle")) + .setAuthor({ + name: i18n.__("commands.music.dj.embedTitle") + }) .addField( `${this.client.config.mainPrefix}dj enable`, i18n.__("commands.music.dj.slashEnableDescription") diff --git a/src/commands/music/NowPlayingCommand.ts b/src/commands/music/NowPlayingCommand.ts index 541d8ddb2..71c266565 100644 --- a/src/commands/music/NowPlayingCommand.ts +++ b/src/commands/music/NowPlayingCommand.ts @@ -1,3 +1,5 @@ +import { createProgressBar } from "../../utils/functions/createProgressBar"; +import { normalizeTime } from "../../utils/functions/normalizeTime"; import { CommandContext } from "../../structures/CommandContext"; import { createEmbed } from "../../utils/functions/createEmbed"; import { haveQueue } from "../../utils/decorators/MusicUtil"; @@ -8,7 +10,7 @@ import i18n from "../../config"; import { MessageActionRow, MessageButton, MessageEmbed } from "discord.js"; import { AudioPlayerState, AudioResource } from "@discordjs/voice"; -@Command({ +@Command({ aliases: ["np"], description: i18n.__("commands.music.nowplaying.description"), name: "nowplaying", @@ -21,22 +23,25 @@ export class NowPlayingCommand extends BaseCommand { @haveQueue public async execute(ctx: CommandContext): Promise { function getEmbed(): MessageEmbed { - const song = ( - ( - ctx.guild?.queue?.player.state as - | (AudioPlayerState & { - resource: AudioResource | undefined; - }) - | undefined - )?.resource?.metadata as QueueSong | undefined - )?.song; + const res = (ctx.guild?.queue?.player.state as + | (AudioPlayerState & { + resource: AudioResource | undefined; + }) + | undefined)?.resource; + const song = (res?.metadata as QueueSong | undefined)?.song; - return createEmbed( + const embed = createEmbed( "info", - `${ctx.guild?.queue?.playing ? "▶" : "⏸"} **|** ${ - song ? `**[${song.title}](${song.url})**` : i18n.__("commands.music.nowplaying.emptyQueue") - }` + `${ctx.guild?.queue?.playing ? "▶" : "⏸"} **|** ` ).setThumbnail(song?.thumbnail ?? "https://api.clytage.org/assets/images/icon.png"); + + const curr = ~~(res!.playbackDuration / 1000); + embed.description += song + ? `**[${song.title}](${song.url})**\n` + + `${normalizeTime(curr)} ${createProgressBar(curr, song.duration)} ${normalizeTime(song.duration)}` + : i18n.__("commands.music.nowplaying.emptyQueue") + + return embed; } const buttons = new MessageActionRow().addComponents( diff --git a/src/commands/music/VolumeCommand.ts b/src/commands/music/VolumeCommand.ts index a4a5a3d3f..4007910b0 100644 --- a/src/commands/music/VolumeCommand.ts +++ b/src/commands/music/VolumeCommand.ts @@ -1,10 +1,11 @@ +import { createProgressBar } from "../../utils/functions/createProgressBar"; import { inVC, sameVC, validVC } from "../../utils/decorators/MusicUtil"; import { CommandContext } from "../../structures/CommandContext"; import { createEmbed } from "../../utils/functions/createEmbed"; import { BaseCommand } from "../../structures/BaseCommand"; import { Command } from "../../utils/decorators/Command"; import i18n from "../../config"; -import { Message } from "discord.js"; +import { Message, MessageActionRow, MessageButton } from "discord.js"; @Command({ aliases: ["vol"], @@ -26,21 +27,83 @@ export class VolumeCommand extends BaseCommand { @inVC @validVC @sameVC - public execute(ctx: CommandContext): Promise | undefined { + public async execute(ctx: CommandContext): Promise { const volume = Number(ctx.args[0] ?? ctx.options?.getNumber("volume", false)); const current = ctx.guild!.queue!.volume; if (isNaN(volume)) { - return ctx.reply({ + const buttons = new MessageActionRow().addComponents( + new MessageButton() + .setCustomId("10") + .setLabel("10%") + .setStyle("PRIMARY"), + new MessageButton() + .setCustomId("25") + .setLabel("25%") + .setStyle("PRIMARY"), + new MessageButton() + .setCustomId("50") + .setLabel("50%") + .setStyle("PRIMARY"), + new MessageButton() + .setCustomId("75") + .setLabel("75%") + .setStyle("PRIMARY"), + new MessageButton() + .setCustomId("100") + .setLabel("100%") + .setStyle("PRIMARY") + ); + + const msg = await ctx.reply({ embeds: [ createEmbed( "info", `🔊 **|** ${i18n.__mf("commands.music.volume.currentVolume", { volume: `\`${current}\`` - })}` + })}\n${current}% ${createProgressBar(current, 100)} 100%` ).setFooter({ text: i18n.__("commands.music.volume.changeVolume") }) - ] + ], + components: [buttons] }); + + const collector = msg.createMessageComponentCollector({ + filter: i => i.isButton() && i.user.id === ctx.author.id, + idle: 30000 + }); + + collector.on("collect", async i => { + const newContext = new CommandContext(i, [i.customId]); + const newVolume = Number(i.customId); + await this.execute(newContext); + + void msg.edit({ + embeds: [ + createEmbed( + "info", + `🔊 **|** ${i18n.__mf("commands.music.volume.currentVolume", { + volume: `\`${newVolume}\`` + })}\n${newVolume}% ${createProgressBar(newVolume, 100)} 100%` + ).setFooter({ text: i18n.__("commands.music.volume.changeVolume") }) + ], + components: [buttons] + }); + }) + .on("end", () => { + const cur = ctx.guild!.queue!.volume; + void msg.edit({ + embeds: [ + createEmbed( + "info", + `🔊 **|** ${i18n.__mf("commands.music.volume.currentVolume", { + volume: `\`${cur}\`` + })}\n${cur}% ${createProgressBar(cur, 100)} 100%` + ).setFooter({ text: i18n.__("commands.music.volume.changeVolume") }) + ], + components: [] + }); + }); + return; } if (volume <= 0) { return ctx.reply({ diff --git a/src/structures/ServerQueue.ts b/src/structures/ServerQueue.ts index 86a3de700..510734705 100644 --- a/src/structures/ServerQueue.ts +++ b/src/structures/ServerQueue.ts @@ -47,7 +47,6 @@ export class ServerQueue { }); this.player - // @ts-expect-error: Ignore a compile error due to typed emitter error .on("stateChange", (oldState, newState) => { if (newState.status === AudioPlayerStatus.Playing && oldState.status !== AudioPlayerStatus.Paused) { newState.resource.volume?.setVolumeLogarithmic(this.volume / 100); diff --git a/src/utils/functions/createProgressBar.ts b/src/utils/functions/createProgressBar.ts new file mode 100644 index 000000000..91d408838 --- /dev/null +++ b/src/utils/functions/createProgressBar.ts @@ -0,0 +1,5 @@ +export function createProgressBar(current: number, total: number): string { + const pos = Math.ceil(current / total * 10) || 1; + + return `${"━".repeat(pos - 1)}⬤${"─".repeat(10 - pos)}`; +} diff --git a/src/utils/functions/normalizeTime.ts b/src/utils/functions/normalizeTime.ts new file mode 100644 index 000000000..2928b4e73 --- /dev/null +++ b/src/utils/functions/normalizeTime.ts @@ -0,0 +1,12 @@ +function tS(num: number): string { + const s = num.toString(); + return s.length > 1 ? s : `0${s}`; +} + +export function normalizeTime(second: number): string { + const h = Math.floor(second / 3600); + const m = Math.floor((second % 3600) / 60); + const s = Math.floor(second % 60); + + return `${h > 0 ? `${tS(h)}:` : ""}${tS(m)}:${tS(s)}`; +} diff --git a/src/utils/handlers/general/handleVideos.ts b/src/utils/handlers/general/handleVideos.ts index c8a7d12da..51b5952a4 100644 --- a/src/utils/handlers/general/handleVideos.ts +++ b/src/utils/handlers/general/handleVideos.ts @@ -44,7 +44,9 @@ export async function handleVideos( author: ctx.author.id, edit: (i, e, p) => { e.setDescription(`\`\`\`\n${p}\`\`\``) - .setAuthor(opening) + .setAuthor({ + name: opening + }) .setFooter({ text: `• ${i18n.__mf("reusable.pageFooter", { actual: i + 1, total: pages.length })}` }); diff --git a/src/utils/structures/JSONDataManager.ts b/src/utils/structures/JSONDataManager.ts index 5993a47b9..e93837d53 100644 --- a/src/utils/structures/JSONDataManager.ts +++ b/src/utils/structures/JSONDataManager.ts @@ -16,7 +16,7 @@ export class JSONDataManager { public async save(data: () => T): Promise { await this.manager.add(async () => { const dat = data(); - await writeFile(this.fileDir, JSON.stringify(dat, null, 4)); + await writeFile(this.fileDir, JSON.stringify(dat)); return undefined; }); diff --git a/src/utils/structures/SongManager.ts b/src/utils/structures/SongManager.ts index 5f3210394..c51d5afdc 100644 --- a/src/utils/structures/SongManager.ts +++ b/src/utils/structures/SongManager.ts @@ -3,6 +3,8 @@ import { Rawon } from "../../structures/Rawon"; import { Collection, GuildMember, Snowflake, SnowflakeUtil } from "discord.js"; export class SongManager extends Collection { + private id = 0; + public constructor(public readonly client: Rawon, public readonly guild: GuildMember["guild"]) { super(); } @@ -10,7 +12,7 @@ export class SongManager extends Collection { public addSong(song: Song, requester: GuildMember): Snowflake { const key = SnowflakeUtil.generate(); const data: QueueSong = { - index: Date.now(), + index: this.id++, key, requester, song