diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6b1750e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + target-branch: "develop" + labels: + - "dependencies" + + # npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + target-branch: "develop" + labels: + - "dependencies" \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..5b27493 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,7 @@ +dependencies: +- package-lock.json + +documentation: +- README.md +- LICENSE +- .env_example \ No newline at end of file diff --git a/.github/workflows/codeql-analyze.yml b/.github/workflows/codeql-analyze.yml new file mode 100644 index 0000000..d303165 --- /dev/null +++ b/.github/workflows/codeql-analyze.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Analyze" + +on: + push: + branches: [ master, stable ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '24 20 * * 6' + +jobs: + analyze: + name: CodeQL Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..4654e30 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,16 @@ +name: Labeler +on: [pull_request] + +jobs: + label: + name: Labeler + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/labeler@v3 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true \ No newline at end of file diff --git a/README.md b/README.md index 53033a4..b40c2b8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Internet Radio to your Discord guild This bot is using Gitea repo to get radio stations from [playlist.json](https://gitea.cwinfo.org/cwchristerw/radio/raw/branch/master/playlist.json) file. List is currently maintained by Christer Warén. You can use alternative list with same format when using RADIOX_STATIONSLISTURL environment variable. ## Docker -1. `docker build -t warengroup/eximiabots-radiox .` +1. `docker build -t warengroup/eximiabots-radiox . --pull` 2. `docker run --name radiox-dev -d --net host -e DISCORD_TOKEN= -v "$PWD/datastore":/usr/src/app/datastore/ warengroup/eximiabots-radiox` ## Join our Discord Server diff --git a/package-lock.json b/package-lock.json index e38a986..11549cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "typescript": "^4.4.2" }, "engines": { - "node": ">=16.6.0", + "node": ">=16.8.0", "npm": ">=7.0.0" } }, @@ -682,9 +682,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", - "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -3610,9 +3610,9 @@ } }, "node_modules/ts-node/node_modules/acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4441,9 +4441,9 @@ "requires": {} }, "acorn-walk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", - "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, "agent-base": { @@ -6621,9 +6621,9 @@ }, "dependencies": { "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true } } diff --git a/package.json b/package.json index 12182d4..3566310 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "typescript": "^4.4.2" }, "engines": { - "node": ">=16.6.0", + "node": ">=16.8.0", "npm": ">=7.0.0" } } \ No newline at end of file diff --git a/src/Client.ts b/src/Client.ts index 4dc9611..761e7cd 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -85,6 +85,10 @@ class RadioClient extends Client { process.on('SIGTERM', () => { require(`${events}SIGTERM`).execute(this); }); + + process.on('uncaughtException', (error) => { + require(`${events}uncaughtException`).execute(this, error); + }); this.on("error", error => { console.error(error); diff --git a/src/client/commands/bug.js b/src/client/commands/bug.js index a87a638..48091b1 100644 --- a/src/client/commands/bug.js +++ b/src/client/commands/bug.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'bug', description: 'Report a bug', - permission: 'none', category: 'info', async execute(interaction, client) { let message = {}; diff --git a/src/client/commands/help.js b/src/client/commands/help.js index 2b7da61..212346b 100644 --- a/src/client/commands/help.js +++ b/src/client/commands/help.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'help', description: 'Get help using bot', - permission: 'none', category: 'info', execute(interaction, client) { let message = {}; diff --git a/src/client/commands/invite.js b/src/client/commands/invite.js index 38db87a..5c66601 100644 --- a/src/client/commands/invite.js +++ b/src/client/commands/invite.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'invite', description: 'Invite Bot', - permission: 'none', category: 'info', execute(interaction, client) { let message = {}; diff --git a/src/client/commands/list.js b/src/client/commands/list.js index b0ce3fd..5a18b64 100644 --- a/src/client/commands/list.js +++ b/src/client/commands/list.js @@ -3,13 +3,15 @@ import Discord from "discord.js"; module.exports = { name: 'list', description: 'List radio stations', - permission: 'none', category: 'radio', execute(interaction, client) { let message = {}; if(!client.stations) { message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild); - return interaction.reply(client.messageEmojis["error"] + message.errorToGetPlaylist); + return interaction.reply({ + content: client.messageEmojis["error"] + message.errorToGetPlaylist, + ephemeral: true + }); } const radio = client.radio.get(interaction.guild.id); diff --git a/src/client/commands/maintenance.js b/src/client/commands/maintenance.js index b76af77..86f23e9 100644 --- a/src/client/commands/maintenance.js +++ b/src/client/commands/maintenance.js @@ -1,9 +1,9 @@ import Discord from "discord.js"; +import fetch from "node-fetch"; module.exports = { name: 'maintenance', description: 'Bot Maintenance', - permission: 'none', category: 'info', options: [ { type: "NUMBER", name: "action", description: "Select action", required: false} @@ -11,7 +11,10 @@ module.exports = { async execute(interaction, client) { let message = {}; - if(!client.funcs.isDev(client.config.devId, interaction.user.id)) return interaction.reply(client.messageEmojis["error"] + client.messages.notAllowed); + if(!client.funcs.isDev(client.config.devId, interaction.user.id)) return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.notAllowed, + ephemeral: true + }); let action = interaction.options?.getNumber("action") ?? interaction.values?.[0]; const options = new Array( { @@ -38,6 +41,12 @@ module.exports = { description: "", value: "6" }, + { + emoji: "<:RadioXList:688541155519889482>", + label: "Reload Stations", + description: "", + value: "7" + }, { emoji: "<:dnd:746069698139127831>", label: "Enable Maintenance Mode", @@ -87,17 +96,13 @@ module.exports = { break; case "4": client.user.setStatus('idle'); - setTimeout(function () { - client.funcs.saveRadios(client); - }, 5000); + client.funcs.saveRadios(client); client.user.setStatus('online'); break; case "5": client.user.setStatus('idle'); let guilds = await client.guilds.fetch(); - setTimeout(function () { - client.funcs.restoreRadios(client, guilds); - }, 5000); + client.funcs.restoreRadios(client, guilds); client.user.setStatus('online'); break; case "6": @@ -105,6 +110,18 @@ module.exports = { require(`../commands.js`).execute(client); client.user.setStatus('online'); break; + case "7": + try { + client.funcs.logger('Stations', 'Started fetching list – ' + client.config.stationslistUrl); + client.stations = await fetch(client.config.stationslistUrl) + .then(client.funcs.checkFetchStatus) + .then(response => response.json()); + + client.funcs.logger('Stations', 'Successfully fetched list'); + } catch (error) { + client.funcs.logger('Stations', 'Fetching list failed'); + } + break; case "8": client.user.setStatus('dnd'); break; diff --git a/src/client/commands/next.js b/src/client/commands/next.js index 56747d3..c4ae658 100644 --- a/src/client/commands/next.js +++ b/src/client/commands/next.js @@ -1,7 +1,6 @@ module.exports = { name: 'next', description: 'Next Station', - permission: 'none', category: 'radio', async execute(interaction, client, command) { if (client.funcs.check(client, interaction, command)) { @@ -17,8 +16,6 @@ module.exports = { ephemeral: true }); - interaction.deferUpdate(); - let url = station.stream[station.stream.default]; client.funcs.statisticsUpdate(client, interaction.guild, radio); @@ -28,7 +25,14 @@ module.exports = { radio.station = station; radio.textChannel = interaction.channel; radio.startTime = date.getTime(); - client.funcs.play(null, interaction.guild, client, url); + + if(interaction.isCommand()) { + client.funcs.play(interaction, interaction.guild, client, url); + } + if(interaction.isButton()) { + interaction.deferUpdate(); + client.funcs.play(null, interaction.guild, client, url); + } } } diff --git a/src/client/commands/nowplaying.js b/src/client/commands/nowplaying.js index e6b2e62..bf42da9 100644 --- a/src/client/commands/nowplaying.js +++ b/src/client/commands/nowplaying.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'nowplaying', description: 'Current Radio Station', - permission: 'none', category: 'radio', async execute(interaction, client, command) { if (client.funcs.check(client, interaction, command)) { diff --git a/src/client/commands/play.js b/src/client/commands/play.js index 3384238..fb29976 100644 --- a/src/client/commands/play.js +++ b/src/client/commands/play.js @@ -11,13 +11,15 @@ module.exports = { options: [ { type: "STRING", name: "query", description: "Select station", required: false} ], - permission: "none", category: "radio", async execute(interaction, client) { let message = {}; if(!client.stations) { message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild); - return interaction.reply(client.messageEmojis["error"] + message.errorToGetPlaylist); + return interaction.reply({ + content: client.messageEmojis["error"] + message.errorToGetPlaylist, + ephemeral: true + }); } let query = interaction.options?.getString("query") ?? interaction.values?.[0]; @@ -27,26 +29,32 @@ module.exports = { let url = query ? query.replace(/<(.+)>/g, "$1") : ""; const radio = client.radio.get(interaction.guild.id); const voiceChannel = interaction.member.voice.channel; - if (!radio) { - if (!interaction.member.voice.channel) - return interaction.reply({ - content: client.messageEmojis["error"] + client.messages.noVoiceChannel, - ephemeral: true - }); - } else { - if (voiceChannel !== radio.voiceChannel) - return interaction.reply({ - content: client.messageEmojis["error"] + client.messages.wrongVoiceChannel, - ephemeral: true - }); + if (!voiceChannel) return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.noVoiceChannel, + ephemeral: true + }); + if (radio) { + if (voiceChannel !== radio.voiceChannel) return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.wrongVoiceChannel, + ephemeral: true + }); } - if (!query) return interaction.reply(client.messages.noQuery); + if (!query) return interaction.reply({ + content: client.messages.noQuery, + ephemeral: true + }); const permissions = voiceChannel.permissionsFor(interaction.client.user); if (!permissions.has("CONNECT")) { - return interaction.reply(client.messageEmojis["error"] + client.messages.noPermsConnect); + return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.noPermsConnect, + ephemeral: true + }); } if (!permissions.has("SPEAK")) { - return interaction.reply(client.messageEmojis["error"] + client.messages.noPermsSpeak); + return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.noPermsSpeak, + ephemeral: true + }); } let station; const number = parseInt(query - 1); @@ -66,17 +74,15 @@ module.exports = { station = client.stations[number]; } } else { - if (query.length < 3) - return interaction.reply({ - content: client.messageEmojis["error"] + client.messages.tooShortSearch, - ephemeral: true - }); + if (query.length < 3) return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.tooShortSearch, + ephemeral: true + }); const sstation = await client.funcs.searchStation(query, client); - if (!sstation) - return interaction.reply({ - content: client.messageEmojis["error"] + client.messages.noSearchResults, - ephemeral: true - }); + if (!sstation) return interaction.reply({ + content: client.messageEmojis["error"] + client.messages.noSearchResults, + ephemeral: true + }); url = sstation.stream[sstation.stream.default]; station = sstation; } @@ -129,7 +135,10 @@ module.exports = { } catch (error) { console.log(error); client.radio.delete(interaction.guild.id); - return interaction.reply(client.messageEmojis["error"] + `An error occured: ${error}`); + return interaction.reply({ + content: client.messageEmojis["error"] + `An error occured: ${error}`, + ephemeral: true + }); } } }; \ No newline at end of file diff --git a/src/client/commands/prev.js b/src/client/commands/prev.js index eb7e572..df987b8 100644 --- a/src/client/commands/prev.js +++ b/src/client/commands/prev.js @@ -1,7 +1,6 @@ module.exports = { name: 'prev', description: 'Previous Station', - permission: 'none', category: 'radio', async execute(interaction, client, command) { if (client.funcs.check(client, interaction, command)) { @@ -17,8 +16,6 @@ module.exports = { ephemeral: true }); - interaction.deferUpdate(); - let url = station.stream[station.stream.default]; client.funcs.statisticsUpdate(client, interaction.guild, radio); @@ -28,7 +25,14 @@ module.exports = { radio.station = station; radio.textChannel = interaction.channel; radio.startTime = date.getTime(); - client.funcs.play(null, interaction.guild, client, url); + + if(interaction.isCommand()) { + client.funcs.play(interaction, interaction.guild, client, url); + } + if(interaction.isButton()) { + interaction.deferUpdate(); + client.funcs.play(null, interaction.guild, client, url); + } } } diff --git a/src/client/commands/statistics.js b/src/client/commands/statistics.js index 7e87510..df31e33 100644 --- a/src/client/commands/statistics.js +++ b/src/client/commands/statistics.js @@ -4,7 +4,6 @@ import Discord from "discord.js"; module.exports = { name: 'statistics', description: 'Show statistics', - permission: 'none', category: 'info', execute(interaction, client) { let message = {}; @@ -15,7 +14,10 @@ module.exports = { if(!client.stations) { message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild); - return interaction.reply(client.messageEmojis["error"] + message.errorToGetPlaylist); + return interaction.reply({ + content: client.messageEmojis["error"] + message.errorToGetPlaylist, + ephemeral: true + }); } if(!currentGuild || currentGuild && !currentGuild.statistics){ diff --git a/src/client/commands/status.js b/src/client/commands/status.js index 6a3ce7b..c40fdbd 100644 --- a/src/client/commands/status.js +++ b/src/client/commands/status.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'status', description: 'Bot Status', - permission: 'none', category: 'info', async execute(interaction, client) { let message = {}; diff --git a/src/client/commands/stop.js b/src/client/commands/stop.js index 9c53249..fbdb3e9 100644 --- a/src/client/commands/stop.js +++ b/src/client/commands/stop.js @@ -3,7 +3,6 @@ import Discord from "discord.js"; module.exports = { name: 'stop', description: 'Stop radio', - permission: 'none', category: 'radio', async execute(interaction, client, command) { const radio = client.radio.get(interaction.guild.id); diff --git a/src/client/events/SIGINT.js b/src/client/events/SIGINT.js index 55d2a0b..a63248a 100644 --- a/src/client/events/SIGINT.js +++ b/src/client/events/SIGINT.js @@ -1,23 +1,18 @@ -const { REST } = require('@discordjs/rest'); -const { Routes } = require('discord-api-types/v9'); -const { token, version } = require('../../config.js'); - module.exports = { name: 'SIGINT', - async execute(client) { + execute(client) { client.user.setStatus('dnd'); console.log("\n"); client.funcs.logger("Bot", "Closing"); console.log("\n"); + + client.funcs.saveRadios(client); - setTimeout(async function () { - let message = {}; - - if (!client.stations) return process.exit(); - - await client.funcs.saveRadios(client); - await process.exit(); - }, 5000); + setInterval(() => { + if(client.radio.size == 0){ + process.exit(); + } + }, 500); } } \ No newline at end of file diff --git a/src/client/events/SIGTERM.js b/src/client/events/SIGTERM.js index 52daa75..99e7af3 100644 --- a/src/client/events/SIGTERM.js +++ b/src/client/events/SIGTERM.js @@ -1,6 +1,6 @@ module.exports = { name: 'SIGTERM', - async execute(client) { + execute(client) { process.emit('SIGINT'); } } \ No newline at end of file diff --git a/src/client/events/uncaughtException.js b/src/client/events/uncaughtException.js new file mode 100644 index 0000000..5df8a0b --- /dev/null +++ b/src/client/events/uncaughtException.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'uncaughtException', + execute(client, error) { + console.log(error.stack); + process.emit('SIGINT'); + } +} \ No newline at end of file diff --git a/src/client/funcs/check.js b/src/client/funcs/check.js index 38fe48a..cfbbbc5 100644 --- a/src/client/funcs/check.js +++ b/src/client/funcs/check.js @@ -24,14 +24,6 @@ module.exports = function (client, interaction, command) { }); return false; } - if(!command.permission == 'none'){ - if (!permissions.has(command.permission)) { - message.noPerms = client.messages.noPerms.replace("%command.permission%", command.permission); - interaction.reply({ - content: client.messageEmojis["error"] + message.noPerms, - ephemeral: true - }); - return false; - } else return true; - } else return true; + + return true; }; diff --git a/src/client/funcs/play.js b/src/client/funcs/play.js index 24addeb..fe81608 100644 --- a/src/client/funcs/play.js +++ b/src/client/funcs/play.js @@ -27,7 +27,10 @@ module.exports = async function play(interaction, guild, client, url) { radio.connection?.destroy(); radio.audioPlayer?.stop(); client.radio.delete(guild.id); - return interaction.reply(client.messages.errorPlaying); + return interaction.reply({ + content: client.messages.errorPlaying, + ephemeral: true + }); }); message.nowplayingDescription = client.messages.nowplayingDescription.replace("%radio.station.name%", radio.station.name); diff --git a/src/client/funcs/saveRadios.js b/src/client/funcs/saveRadios.js index 5311727..30280e1 100644 --- a/src/client/funcs/saveRadios.js +++ b/src/client/funcs/saveRadios.js @@ -1,4 +1,4 @@ -module.exports = async function saveRadios(client) { +module.exports = function saveRadios(client) { let currentRadios = client.radio.keys(); let radio = currentRadios.next(); @@ -7,8 +7,8 @@ module.exports = async function saveRadios(client) { currentRadio.guild = client.datastore.getEntry(radio.value).guild; if (currentRadio) { - await client.funcs.statisticsUpdate(client, currentRadio.guild, currentRadio); - await client.funcs.saveState(client, currentRadio.guild, currentRadio); + client.funcs.statisticsUpdate(client, currentRadio.guild, currentRadio); + client.funcs.saveState(client, currentRadio.guild, currentRadio); currentRadio.connection?.destroy(); currentRadio.audioPlayer?.stop(); currentRadio.message?.delete();