├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── eslint.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── commands └── help.js ├── config_example.json ├── dashboard ├── assets │ ├── img │ │ ├── activities.png │ │ ├── banner.png │ │ ├── generic_server.png │ │ ├── header.png │ │ ├── image_placeholder.png │ │ ├── language.png │ │ ├── logo.png │ │ ├── moderation.png │ │ ├── music.png │ │ ├── slash_commands.png │ │ └── suit.png │ └── style.css ├── dashboard.js ├── queue │ ├── near-silence.mp3 │ ├── queue.css │ └── queue.js ├── templates │ ├── 404.ejs │ ├── admin.ejs │ ├── commands.ejs │ ├── dashboard.ejs │ ├── index.ejs │ ├── legal │ │ ├── privacy.ejs │ │ └── terms.ejs │ ├── partials │ │ ├── footer.ejs │ │ └── header.ejs │ └── queue.ejs └── websocket.js ├── deploy-commands.js ├── disabled_commands ├── feedback │ ├── bugreport.js │ ├── github.js │ └── suggestion.js ├── general │ ├── activity.js │ ├── dashboard.js │ ├── help.js │ ├── info.js │ ├── invite.js │ ├── language.js │ ├── ping.js │ ├── serverinfo.js │ └── userinfo.js ├── moderation │ ├── ban.js │ ├── kick.js │ ├── move.js │ ├── moveall.js │ ├── purge.js │ └── slowmode.js └── music │ ├── clear.js │ ├── filter.js │ ├── lyrics.js │ ├── nowplaying.js │ ├── pause.js │ ├── play.js │ ├── previous.js │ ├── queue.js │ ├── remove.js │ ├── repeat.js │ ├── resume.js │ ├── search.js │ ├── seek.js │ ├── shuffle.js │ ├── skip.js │ ├── stop.js │ └── volume.js ├── events ├── guildCreate.js ├── guildDelete.js ├── interactionCreate.js └── ready.js ├── language ├── lang │ ├── de.js │ ├── en-US.js │ ├── fi.js │ ├── ja.js │ └── pt-BR.js └── locale.js ├── main.js ├── music ├── ExtendedSearch.js ├── FilterManager.js ├── lavalink.js └── lavalink │ ├── Lavalink.jar │ └── template.yml ├── package.json └── utilities ├── config.js ├── database.js ├── logging.js └── utilities.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": ["eslint:recommended"], 8 | "ignorePatterns": ["**/*.ejs"], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "root": true, 14 | "rules": { 15 | "array-bracket-newline": ["warn", { "multiline": true }], 16 | "arrow-body-style": ["warn", "as-needed"], 17 | "arrow-parens": "warn", 18 | "arrow-spacing": "warn", 19 | "block-spacing": "warn", 20 | "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], 21 | "camelcase": "warn", 22 | "comma-dangle": ["warn", "never"], 23 | "comma-spacing": "warn", 24 | "comma-style": "warn", 25 | "curly": ["warn", "all"], 26 | "dot-location": ["warn", "property"], 27 | "dot-notation": "warn", 28 | "eol-last": "warn", 29 | "implicit-arrow-linebreak": "warn", 30 | "indent": ["warn", 2, { "SwitchCase": 1 }], 31 | "key-spacing": "warn", 32 | "keyword-spacing": "warn", 33 | "no-extra-parens": "warn", 34 | "no-multi-spaces": "warn", 35 | "no-multiple-empty-lines": ["warn", { "max": 2, "maxEOF": 0 }], 36 | "no-trailing-spaces": "warn", 37 | "no-unused-vars": ["warn", { "ignoreRestSiblings": true }], 38 | "no-whitespace-before-property": "warn", 39 | "object-curly-newline": ["warn", { "multiline": true }], 40 | "object-curly-spacing": ["warn", "always"], 41 | "quotes": ["warn", "single"], 42 | "semi": ["warn", "never"], 43 | "space-before-blocks": "warn", 44 | "space-before-function-paren": ["warn", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], 45 | "switch-colon-spacing": "warn" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ main ] 15 | pull_request: 16 | branches: [ main ] 17 | workflow_dispatch: 18 | 19 | jobs: 20 | eslint: 21 | name: Run eslint scanning 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | security-events: write 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v3 29 | 30 | - name: Install ESLint 31 | run: | 32 | npm install eslint@8.10.0 33 | npm install @microsoft/eslint-formatter-sarif@2.1.7 34 | 35 | - name: Run ESLint 36 | run: npx eslint . 37 | --config .eslintrc.json 38 | --ext .js,.jsx,.ts,.tsx 39 | --format @microsoft/eslint-formatter-sarif 40 | --output-file eslint-results.sarif 41 | continue-on-error: true 42 | 43 | - name: Upload analysis results to GitHub 44 | uses: github/codeql-action/upload-sarif@v2 45 | with: 46 | sarif_file: eslint-results.sarif 47 | wait-for-processing: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | config.json 4 | package-lock.json 5 | music/lavalink/logs/ 6 | music/lavalink/application.yml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://repository-images.githubusercontent.com/406747355/0c0fcbbd-8dab-4259-a5d6-d8cc5069ef37) 2 | 3 | [![Invite](https://img.shields.io/static/v1?style=for-the-badge&logo=discord&label=Invite&message=SuitBot&color=000000)](https://discord.com/oauth2/authorize?client_id=887122733010411611&scope=bot%20applications.commands&permissions=2167425024) 4 | [![Discord](https://shields.io/discord/610498937874546699?style=for-the-badge&logo=discord&label=discord)](https://discord.gg/qX2CBrrUpf) 5 | [![License](https://img.shields.io/github/license/MeridianGH/suitbot?logo=gnu&style=for-the-badge)](https://github.com/MeridianGH/suitbot/blob/main/LICENSE.md) 6 | 7 | > This repository has been archived in favor of the newer and improved, but slightly stripped down version **[Kalliope](https://github.com/MeridianGH/Kalliope)**. 8 | 9 | # SuitBot 10 | 11 | > A lightweight music and general purpose bot with dashboard, that uses slash commands and buttons to be as user-friendly as possible! 12 | 13 | 14 | 15 | [![Dashboard](https://img.shields.io/static/v1?style=for-the-badge&logo=google%20chrome&label=&message=Dashboard&color=212121)](https://suitbot.xyz) 16 | [![Progress Board](https://img.shields.io/static/v1?style=for-the-badge&logo=trello&label=&message=Progress%20Board&color=212121)](https://github.com/MeridianGH/suitbot/projects/1) 17 | [![Discord Server](https://img.shields.io/static/v1?style=for-the-badge&logo=discord&label=&message=Discord%20Server&color=212121)](https://discord.gg/qX2CBrrUpf) 18 | 19 |
20 | Table of Contents 21 | 22 | - [Invite](#invite) 23 | - [Features](#features) 24 | - [Commands](#commands) 25 | - [Installation](#installation) 26 | - [Stats](#stats) 27 | - [Licensing](#licensing) 28 |
29 | 30 | --- 31 | 32 | ## Invite 33 | > Disclaimer: The bot is still in development, so expect some bugs or features that might not work 100% yet. Please report any bugs or suggestions via the respective commands. 34 | 35 | [![Invite](https://img.shields.io/static/v1?style=for-the-badge&logo=discord&label=&message=Invite&color=212121)](https://discord.com/oauth2/authorize?client_id=887122733010411611&scope=bot%20applications.commands&permissions=2167425024) 36 | 37 | ## Features 38 | - Slash commands 39 | - Use commands directly integrated in Discord 40 | - No more guessing with variables 41 | - Quick overview of all commands 42 | 43 | 44 | - Music 45 | - Supports many sources (YouTube, Spotify, Bandcamp, SoundCloud, Twitch, Vimeo or any other HTTP source) 46 | - Supports playlists and livestreams 47 | - Interactive Web Dashboard 48 | - Pause, Skip, Remove, Volume and more commands 49 | 50 | 51 | - Language support 52 | - Supports multiple languages 53 | - Change the language for your server using `/language` 54 | - Add your own language by contacting me on the Discord server 55 | 56 | 57 | - Activities 58 | - Create invites for Discord Activities 59 | - YouTube Together and a lot of fun minigames 60 | - Have fun with everyone in your voice channel 61 | 62 | 63 | - Basic Moderation 64 | - Info commands (User, Server, Avatar) 65 | - Kick, Ban, Move, Slowmode and more commands 66 | - Permission check on commands 67 | 68 | ## Commands 69 | SuitBot uses slash commands to integrate itself into the server. You can easily access its commands directly by typing `/` in your chat window. 70 | 71 |
72 | Show all commands 73 | 74 | ### General 75 | | Command | Description | 76 | |-------------|-------------------------------------------| 77 | | /activity | Creates a Discord activity. | 78 | | /dashboard | Sends a link to the dashboard. | 79 | | /help | Replies with help on how to use this bot. | 80 | | /info | Shows info about the bot. | 81 | | /invite | Sends an invite link for the bot. | 82 | | /language | Changes the bots language. | 83 | | /ping | Replies with the current latency. | 84 | | /serverinfo | Shows info about the server. | 85 | | /userinfo | Shows info about a user. | 86 | 87 | ### Music 88 | | Command | Description | 89 | |-------------|-------------------------------------------------------------------| 90 | | /clear | Clears the queue. | 91 | | /filter | Sets filter modes for the player. | 92 | | /lyrics | Shows the lyrics of the currently playing song. | 93 | | /nowplaying | Shows the currently playing song. | 94 | | /pause | Pauses playback. | 95 | | /play | Searches and plays a song or playlist from YouTube or Spotify. | 96 | | /previous | Plays the previous track. | 97 | | /queue | Displays the queue. | 98 | | /remove | Removes the specified track from the queue. | 99 | | /repeat | Sets the current repeat mode. | 100 | | /resume | Resumes playback. | 101 | | /search | Searches five songs from YouTube and lets you select one to play. | 102 | | /seek | Skips to the specified point in the current track. | 103 | | /shuffle | Shuffles the queue. | 104 | | /skip | Skips the current track or to a specified point in the queue. | 105 | | /stop | Stops playback. | 106 | | /volume | Sets the volume of the music player. | 107 | 108 | ### Moderation 109 | | Command | Description | 110 | |-----------|---------------------------------------------------------------| 111 | | /ban | Bans a user. | 112 | | /kick | Kicks a user. | 113 | | /move | Moves the mentioned user to the specified channel. | 114 | | /moveall | Moves all users from the first channel to the second channel. | 115 | | /purge | Clears a specified amount of messages. | 116 | | /slowmode | Sets the rate limit of the current channel. | 117 | 118 | ### Feedback 119 | | Command | Description | 120 | |-------------|---------------------------------------| 121 | | /bugreport | Reports a bug to the developer. | 122 | | /github | Sends a link to the repo of this bot. | 123 | | /suggestion | Sends a suggestion to the developer. | 124 |
125 | 126 | ## Installation 127 | It is not recommended to try and install SuitBot yourself. \ 128 | The bot is not designed to be easily installable and requires many complicated steps to set up. 129 | 130 | If you want a self-hostable bot, keep looking around GitHub for better alternatives! 131 | 132 | If you nevertheless decide to try and host a custom version of SuitBot yourself keep on reading. 133 | 134 |
135 | Installation 136 | 137 | ## Local Installation 138 | 139 | ### Prerequisites 140 | - Node.js v16.x 141 | - FFmpeg v4.4 142 | - Java v13.x 143 | 144 | ### Installing 145 | ```shell 146 | git clone https://github.com/MeridianGH/suitbot.git 147 | cd suitbot 148 | npm install 149 | ``` 150 | 151 | ### Configuration 152 | Rename `config_example.json` to `config.json` and replace the placeholders inside with your info: 153 | - A Discord Bot Token (**[Guide](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot)**) 154 | - Your Application ID which you can find the the `General Information` tab in your Discord application. 155 | - Your Client Secret which is under `OAuth2` in your Discord application. 156 | - The Guild ID of the server in which you want to test the bot. To get this ID, activate `Developer Mode` in Discord's options and right-click your server. 157 | - Your User ID of your Discord account which will be your Admin-Account for the bot. Right-click yourself with `Developer Mode` activated. 158 | - Get your YouTube keys like described in this **[Guide](https://github.com/Walkyst/lavaplayer-fork/issues/18)**. Once you have `PAPISID` and `PSID` set them in the config. 159 | - Create a Genius API application **[here](https://docs.genius.com/)**, generate an access token and paste it here. Can be an empty string. 160 | 161 | ### Setting up 162 | #### Discord 163 | Go to your Discord Application, go to `OAuth2` and add `http://localhost/callback` to `Redirects`. 164 | 165 | #### Domain 166 | Replace the domain in `dashboard.js` with your domain. \ 167 | If you want to redirect from HTTP to HTTPS, make sure to replace the domains in the function `forceDomain()` as well. 168 | 169 | #### Database 170 | Install PostgreSQL and create a database `suitbot`. 171 | If you choose to name it differently, set the database URL in `database.js`. 172 | 173 | Create a table using the following command: 174 | ``` 175 | CREATE TABLE servers ( 176 | id varchar(30) UNIQUE NOT NULL, 177 | locale varchar(5) NOT NULL 178 | ); 179 | ``` 180 | 181 | ### Deploying 182 | Use `node deploy-commands.js` to update and add commands in the guild you specified and `node deploy-commands.js global` to update the commands globally.\ 183 | Guild commands are refreshed instantly while global commands can take up to an hour. 184 | 185 | Start the bot with 186 | ```shell 187 | node . 188 | ``` 189 | \ 190 | To start the bot for production use one of these specific for your platform 191 | ```shell 192 | npm run start:win 193 | npm run start:unix 194 | ``` 195 | --- 196 |
197 | 198 | ## Stats 199 | 200 | ### Size 201 | ![Lines of code](https://img.shields.io/tokei/lines/github/MeridianGH/suitbot?style=for-the-badge) 202 | ![GitHub repo size](https://img.shields.io/github/repo-size/MeridianGH/suitbot?style=for-the-badge) 203 | 204 | ### Code 205 | ![GitHub top language](https://img.shields.io/github/languages/top/MeridianGH/suitbot?style=for-the-badge) 206 | ![GitHub language count](https://img.shields.io/github/languages/count/MeridianGH/suitbot?style=for-the-badge) 207 | \ 208 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/MeridianGH/suitbot?style=for-the-badge)](https://www.codefactor.io/repository/github/meridiangh/suitbot) 209 | [![Libraries.io](https://img.shields.io/librariesio/github/MeridianGH/suitbot?style=for-the-badge)](https://libraries.io/github/MeridianGH/suitbot) 210 | \ 211 | [![discord.js](https://img.shields.io/github/package-json/dependency-version/MeridianGH/suitbot/discord.js?color=44b868&logo=npm&style=for-the-badge)](https://www.npmjs.com/package/discord.js) 212 | [![erelajs](https://img.shields.io/github/package-json/dependency-version/MeridianGH/suitbot/erela.js?color=44b868&logo=npm&style=for-the-badge)](https://www.npmjs.com/package/play-dl) 213 | 214 | ### GitHub 215 | [![GitHub issues](https://img.shields.io/github/issues/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/issues) 216 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/pulls) 217 | \ 218 | [![GitHub last commit](https://img.shields.io/github/last-commit/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/commits) 219 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/graphs/commit-activity) 220 | \ 221 | [![GitHub Repo stars](https://img.shields.io/github/stars/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/stargazers) 222 | [![GitHub watchers](https://img.shields.io/github/watchers/MeridianGH/suitbot?style=for-the-badge)](https://github.com/MeridianGH/suitbot/watchers) 223 | 224 | ### Dashboard 225 | [![Website](https://img.shields.io/website?down_message=offline&label=dashboard&style=for-the-badge&up_message=online&url=https%3A%2F%2Fsuitbot.xyz)](https://suitbot.xyz) 226 | 227 | ## Licensing 228 | If you want to host your own version of SuitBot, with or without modifications to the source code or plan to use any part of this source code, you must disclose the source and reference this repository/license. 229 | 230 | [![License](https://img.shields.io/github/license/MeridianGH/suitbot?logo=gnu&style=for-the-badge)](https://github.com/MeridianGH/suitbot/blob/main/LICENSE.md) 231 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | import {EmbedBuilder, SlashCommandBuilder} from "discord.js"; 2 | 3 | export const { data, execute } = { 4 | data: new SlashCommandBuilder() 5 | .setName('help') 6 | .setDescription('Replies with help on how to use this bot.'), 7 | async execute(interaction) { 8 | const embed = new EmbedBuilder() 9 | .setAuthor({ name: 'The future of SuitBot.', iconURL: interaction.member.user.displayAvatarURL() }) 10 | .setTitle('SuitBot will be shutting down shortly.') 11 | .setThumbnail(interaction.client.user.displayAvatarURL()) 12 | .setDescription('Due to various circumstances and stagnant development SuitBot will be shutting down shortly.\n' + 13 | 'The website has been offline for a few weeks and will not come back and all commands have been deactivated.\n\n' + 14 | 'In the meantime I have been working on a new Discord bot focused on bringing music to your channels.\n\n' + 15 | 'The new bot (called Kalliope) took over and improved on the music features of SuitBot, especially the web interface.\n' + 16 | 'While the era of SuitBot may be over, Kalliope will continue its legacy...' 17 | ) 18 | .addFields([{ name: '\u200b', value: '[Check out kalliope.cc](https://kalliope.cc)' }]) 19 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 20 | await interaction.reply({ embeds: [embed] }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "yourToken", 3 | "appId": "yourApplicationID", 4 | "clientSecret": "yourClientSecret", 5 | "guildId": "yourGuildID", 6 | "adminId": "yourUserID", 7 | "papisid": "yourPAPISID", 8 | "psid": "yourPSID", 9 | "geniusClientToken": "yourGeniusClientAccessToken" 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/assets/img/activities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/activities.png -------------------------------------------------------------------------------- /dashboard/assets/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/banner.png -------------------------------------------------------------------------------- /dashboard/assets/img/generic_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/generic_server.png -------------------------------------------------------------------------------- /dashboard/assets/img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/header.png -------------------------------------------------------------------------------- /dashboard/assets/img/image_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/image_placeholder.png -------------------------------------------------------------------------------- /dashboard/assets/img/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/language.png -------------------------------------------------------------------------------- /dashboard/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/logo.png -------------------------------------------------------------------------------- /dashboard/assets/img/moderation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/moderation.png -------------------------------------------------------------------------------- /dashboard/assets/img/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/music.png -------------------------------------------------------------------------------- /dashboard/assets/img/slash_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/slash_commands.png -------------------------------------------------------------------------------- /dashboard/assets/img/suit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/assets/img/suit.png -------------------------------------------------------------------------------- /dashboard/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #0a0a0a; 3 | --background-light: #111111; 4 | --background-lighter: #212121; 5 | --text-color: #eeeeee; 6 | --text-hover: #aaaaaa; 7 | --button-color: #232323; 8 | --button-hover: #ff0000; 9 | } 10 | 11 | @media (min-width: 992px) { h1.title { font-size: 100px !important; } } 12 | 13 | body { 14 | color: var(--text-color); 15 | background-color: var(--background); 16 | background-image: url("img/header.png"); 17 | background-size: 100%; 18 | background-repeat: no-repeat; 19 | padding-top: 60px; 20 | font-size: 1em !important; 21 | font-family: 'Roboto', sans-serif; 22 | text-align: left; 23 | } 24 | 25 | /* Hyperlinks */ 26 | a { 27 | color: var(--text-color); 28 | text-decoration: none; 29 | transition: color 0.5s; 30 | } 31 | a:hover { 32 | color: var(--text-hover); 33 | text-decoration: none; 34 | } 35 | 36 | /* Navbar */ 37 | .navbar-dark, .navbar-expand-lg .navbar-nav { 38 | border: 0; 39 | background-color: var(--background); 40 | text-align: center; 41 | } 42 | .navbar-dark .navbar-nav .nav-link, .navbar-dark .navbar-brand { 43 | color: var(--text-color); 44 | } 45 | .navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-brand:hover { 46 | color: var(--text-hover); 47 | } 48 | .dropdown-menu, .dropdown-item { 49 | color: var(--text-color); 50 | background-color: var(--background-light); 51 | } 52 | .dropdown-menu { 53 | border: 2px solid var(--background-lighter); 54 | } 55 | .dropdown-item:hover { 56 | color: var(--text-hover); 57 | background-color: var(--background-light); 58 | } 59 | 60 | /* Card */ 61 | .card { 62 | background-color: var(--background-light); 63 | border: 1px solid var(--button-color); 64 | margin-top: 50px; 65 | margin-bottom: 50px; 66 | } 67 | .card-header { 68 | background-color: var(--background); 69 | border-color: var(--text-color) !important; 70 | } 71 | .card-img-top { 72 | border-radius: 0; 73 | } 74 | .server-card { 75 | background-color: var(--background-lighter); 76 | width: 18rem; 77 | margin: 2rem; 78 | border-radius: 3px; 79 | } 80 | 81 | /* Buttons */ 82 | .button { 83 | color: var(--text-color); 84 | background-color: var(--button-color); 85 | height: 30px; 86 | margin-right: 5px; 87 | padding: 0 5px 0 5px; 88 | border: none; 89 | border-radius: 5px; 90 | outline: none; 91 | vertical-align: middle; 92 | text-align: center; 93 | transition: background-color 0.5s; 94 | } 95 | .button:hover { 96 | background-color: var(--button-hover); 97 | } 98 | .button:disabled { 99 | background-color: var(--button-color); 100 | } 101 | .button.icon { 102 | width: 30px; 103 | color: var(--text-color); 104 | background: inherit; 105 | height: 30px; 106 | font-size: 20px; 107 | padding: 0 5px 0 5px; 108 | border: none; 109 | outline: none; 110 | vertical-align: middle; 111 | transition: color 0.5s; 112 | } 113 | .button.icon:hover { 114 | color: var(--text-hover); 115 | } 116 | .button.select:hover { 117 | background-color: var(--button-color); 118 | cursor: pointer; 119 | } 120 | 121 | /* Table */ 122 | .table > :not(caption) > * > * { 123 | color: var(--text-color); 124 | background-color: var(--background-light); 125 | } 126 | th { 127 | color: #ff0000 !important; 128 | } 129 | td { 130 | vertical-align: middle; 131 | } 132 | 133 | .dashboard-button { 134 | color: var(--text-color); 135 | background-color: transparent; 136 | border: 5px solid var(--text-color); 137 | transition: background-color 0.5s; 138 | } 139 | .dashboard-button:hover { 140 | color: var(--background); 141 | background-color: var(--text-color); 142 | border: 5px solid var(--text-color); 143 | } 144 | 145 | .category-title { 146 | font-family: 'Bebas Neue', sans-serif; 147 | font-size: 25px; 148 | color: var(--button-hover); 149 | } 150 | -------------------------------------------------------------------------------- /dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import session from 'express-session' 3 | import minify from 'express-minify' 4 | import ejs from 'ejs' 5 | import memorystore from 'memorystore' 6 | const MemoryStore = memorystore(session) 7 | import { Routes } from 'discord-api-types/v10' 8 | 9 | import fs from 'fs' 10 | import path from 'path' 11 | import { fileURLToPath } from 'url' 12 | import { randomBytes } from 'crypto' 13 | import { marked } from 'marked' 14 | import { setupWebsocket } from './websocket.js' 15 | import { adminId, appId, clientSecret } from '../utilities/config.js' 16 | import { logging } from '../utilities/logging.js' 17 | 18 | const app = express() 19 | 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 21 | 22 | export function startDashboard(client) { 23 | const port = 80 24 | const domain = 'https://suitbot.xyz' 25 | const host = process.env.NODE_ENV === 'production' ? domain : 'http://localhost' 26 | 27 | // Set EJS engine 28 | app.engine('ejs', ejs.renderFile) 29 | app.set('view engine', 'ejs') 30 | 31 | // Minify and set files 32 | app.use(minify()) 33 | app.use('/assets', express.static(path.join(__dirname, 'assets'))) 34 | app.use('/queue', express.static(path.join(__dirname, 'queue'))) 35 | 36 | // Session storage 37 | app.use(session({ 38 | store: new MemoryStore({ checkPeriod: 86400000 }), 39 | secret: randomBytes(32).toString('hex'), 40 | resave: false, 41 | saveUninitialized: false 42 | })) 43 | 44 | const render = (req, res, template, data = {}) => { 45 | const baseData = { djsClient: client, path: req.path, user: req.session.user ?? null } 46 | res.render(path.join(__dirname, 'templates', template), Object.assign(baseData, data)) 47 | } 48 | 49 | const checkAuth = (req, res, next) => { 50 | if (req.session.user) { return next() } 51 | req.session.backURL = req.url 52 | res.redirect('/login') 53 | } 54 | 55 | // Login endpoint. 56 | app.get('/login', (req, res) => { 57 | const loginUrl = `https://discordapp.com/api/oauth2/authorize?client_id=${appId}&scope=identify%20guilds&response_type=code&redirect_uri=${encodeURIComponent(`${host}/callback`)}` 58 | if (!req.session.user) { return res.redirect(loginUrl) } 59 | }) 60 | 61 | // Callback endpoint. 62 | app.get('/callback', async (req, res) => { 63 | if (!req.query.code) { return res.redirect('/') } 64 | 65 | const body = new URLSearchParams({ 'client_id': appId, 'client_secret': clientSecret, 'code': req.query.code, 'grant_type': 'authorization_code', 'redirect_uri': `${host}/callback` }) 66 | const token = await client.rest.post(Routes.oauth2TokenExchange(), { body: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, auth: false, passThroughBody: true }).catch((e) => { logging.warn('Error while fetching token while authenticating: ' + e) }) 67 | if (!token?.access_token) { return res.redirect('/login') } 68 | 69 | const user = await client.rest.get(Routes.user(), { headers: { authorization: `${token.token_type} ${token.access_token}` }, auth: false }).catch((e) => { logging.warn('Error while fetching user while authenticating: ' + e) }) 70 | const guilds = await client.rest.get(Routes.userGuilds(), { headers: { authorization: `${token.token_type} ${token.access_token}` }, auth: false }).catch((e) => { logging.warn('Error while fetching guilds while authenticating: ' + e) }) 71 | if (!user || !guilds) { return res.redirect('/login') } 72 | 73 | user.guilds = guilds 74 | req.session.user = user 75 | 76 | if (req.session.backURL) { 77 | res.redirect(req.session.backURL) 78 | req.session.backURL = null 79 | } else { 80 | res.redirect('/') 81 | } 82 | }) 83 | 84 | // Logout endpoint. 85 | app.get('/logout', (req, res) => { 86 | req.session.destroy(() => { 87 | res.redirect('/') 88 | }) 89 | }) 90 | 91 | // Index endpoint. 92 | app.get('/', (req, res) => { 93 | render(req, res, 'index.ejs') 94 | }) 95 | 96 | // Dashboard endpoint. 97 | app.get('/dashboard', checkAuth, (req, res) => { 98 | render(req, res, 'dashboard.ejs') 99 | }) 100 | 101 | // Queue endpoint. 102 | app.get('/dashboard/:guildId', checkAuth, (req, res) => { 103 | const guild = client.guilds.cache.get(req.params.guildId) 104 | if (!guild) { return res.redirect('/dashboard') } 105 | render(req, res, 'queue.ejs', { guild: guild }) 106 | }) 107 | 108 | // Commands endpoint. 109 | app.get('/commands', (req, res) => { 110 | const readme = fs.readFileSync('./README.md').toString() 111 | const markdown = marked.parse(readme.substring(readme.indexOf('### General'), readme.indexOf('## Installation'))) 112 | render(req, res, 'commands.ejs', { markdown: markdown }) 113 | }) 114 | 115 | // Admin endpoint. 116 | app.get('/admin', checkAuth, (req, res) => { 117 | if (req.session.user.id !== adminId) { return res.redirect('/') } 118 | render(req, res, 'admin.ejs') 119 | }) 120 | 121 | // Terms of Service endpoint. 122 | app.get('/terms', (req, res) => { 123 | render(req, res, 'legal/terms.ejs') 124 | }) 125 | 126 | // Privacy Policy endpoint. 127 | app.get('/privacy', (req, res) => { 128 | render(req, res, 'legal/privacy.ejs') 129 | }) 130 | 131 | // 404 132 | app.get('*', (req, res) => { 133 | render(req, res, '404.ejs') 134 | }) 135 | 136 | client.dashboard = app.listen(port, null, null, () => { 137 | logging.success(`Dashboard is up and running on port ${port}.`) 138 | }) 139 | client.dashboard.host = host 140 | 141 | // WebSocket 142 | const wss = setupWebsocket(client, host) 143 | client.dashboard.update = function(player) { 144 | client.dashboard.emit('update', player) 145 | } 146 | 147 | client.dashboard.shutdown = () => { 148 | client.dashboard.close() 149 | wss.closeAllConnections() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /dashboard/queue/near-silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/dashboard/queue/near-silence.mp3 -------------------------------------------------------------------------------- /dashboard/queue/queue.css: -------------------------------------------------------------------------------- 1 | .queue-title { 2 | font-family: 'Bebas Neue', sans-serif; 3 | } 4 | 5 | .alert { 6 | min-width: 25%; 7 | max-width: 75%; 8 | position: absolute; 9 | right: 1rem; 10 | transition: opacity 1s; 11 | display: flex; 12 | align-items: center; 13 | z-index: 1; 14 | } 15 | 16 | ul.horizontal-list { 17 | padding: 0; 18 | margin: 0; 19 | list-style: none; 20 | display: flex; 21 | align-items: center; 22 | } 23 | ul.horizontal-list li { 24 | display: inline-block; 25 | } 26 | 27 | .nowplaying-container { 28 | height: 200px; 29 | margin-bottom: 10px; 30 | } 31 | 32 | .thumbnail-container { 33 | height: 200px; 34 | width: 356px; 35 | margin-right: 20px; 36 | position: relative; 37 | overflow: hidden; 38 | border-radius: 5px 5px 0 0; 39 | } 40 | .thumbnail-backdrop { 41 | height: 200px; 42 | width: 356px; 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | object-fit: cover; 47 | filter: blur(10px); 48 | } 49 | .thumbnail { 50 | height: 200px; 51 | width: 356px; 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | object-fit: contain; 56 | } 57 | 58 | .progress-container { 59 | width: 356px; 60 | background-color: var(--button-color) 61 | } 62 | .progress-bar { 63 | width: 0; 64 | height: 5px; 65 | background-color: #ff0000 66 | } 67 | 68 | .volume-display { 69 | color: var(--text-color); 70 | background-color: var(--button-color); 71 | height: 30px; 72 | width: 65px; 73 | margin-right: 5px; 74 | padding-left: 5px; 75 | padding-right: 5px; 76 | border: none; 77 | border-radius: 5px; 78 | outline: none; 79 | vertical-align: bottom; 80 | text-align: center; 81 | } 82 | .volume-slider { 83 | -webkit-appearance: none; 84 | margin: 0 0 8px 0; 85 | vertical-align: middle; 86 | height: 10px; 87 | width: 286px; 88 | border-radius: 5px; 89 | cursor: pointer; 90 | background-color: var(--button-color); 91 | } 92 | .volume-slider::-webkit-slider-thumb, .volume-slider::-moz-range-thumb { 93 | -webkit-appearance: none; 94 | height: 15px; 95 | width: 15px; 96 | border-radius: 100%; 97 | border-width: 5px; 98 | border-color: var(--button-color); 99 | background-color: var(--text-color); 100 | transition: background-color 0.5s; 101 | } 102 | .volume-slider:hover::-webkit-slider-thumb, .volume-slider:hover::-moz-range-thumb { 103 | background-color: var(--button-hover); 104 | } 105 | 106 | .queue-button-container { 107 | display: flex; 108 | justify-content: space-between; 109 | margin-bottom: 10px; 110 | } 111 | 112 | .textfield { 113 | color: var(--text-color); 114 | background: var(--button-color); 115 | width: 150px; 116 | height: 30px; 117 | margin-right: 5px; 118 | padding: 0 5px; 119 | border: none; 120 | border-radius: 5px; 121 | outline: none; 122 | vertical-align: middle; 123 | } 124 | 125 | .loader { 126 | width: 100%; 127 | text-align: center; 128 | font-size: 2em; 129 | color: var(--text-color) 130 | } 131 | 132 | -------------------------------------------------------------------------------- /dashboard/queue/queue.js: -------------------------------------------------------------------------------- 1 | /* global htm, React, ReactDOM, guildId, userId */ 2 | // noinspection JSUnresolvedFunction,JSUnresolvedVariable 3 | 4 | const html = htm.bind(React.createElement) 5 | 6 | const websocket = new WebSocket(location.hostname === 'localhost' ? 'ws://localhost' : `wss://${location.hostname}`) 7 | function send(data) { 8 | data.guildId = guildId 9 | data.userId = userId 10 | websocket.send(JSON.stringify(data)) 11 | } 12 | 13 | const msToHMS = (ms) => { 14 | let totalSeconds = ms / 1000 15 | const hours = Math.floor(totalSeconds / 3600).toString() 16 | totalSeconds %= 3600 17 | const minutes = Math.floor(totalSeconds / 60).toString() 18 | const seconds = Math.floor(totalSeconds % 60).toString() 19 | return hours === '0' ? `${minutes}:${seconds.padStart(2, '0')}` : `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}` 20 | } 21 | 22 | function App() { 23 | const [player, setPlayer] = React.useState(null) 24 | const [toast, setToast] = React.useState(null) 25 | 26 | React.useEffect(() => { 27 | websocket.addEventListener('open', () => { 28 | send({ type: 'request' }) 29 | }) 30 | websocket.addEventListener('message', (message) => { 31 | document.getElementById('loader')?.remove() 32 | const data = JSON.parse(message.data) 33 | if (data.toast) { 34 | setToast(data.toast) 35 | } else { 36 | setPlayer(data) 37 | } 38 | }) 39 | }, []) 40 | React.useEffect(() => { 41 | const interval = setInterval(() => { 42 | if (player && !player.paused && player.current && !player.current.isStream) { 43 | if (player.position >= player.current.duration) { 44 | clearInterval(interval) 45 | setPlayer({ ...player, position: player.current.duration }) 46 | } else { 47 | setPlayer({ ...player, position: player.position += 1000 }) 48 | } 49 | } 50 | }, 1000 * (1 / player?.timescale ?? 1)) 51 | 52 | return () => { 53 | clearInterval(interval) 54 | } 55 | }, [player]) 56 | 57 | if (!player) { return null } 58 | if (!player.current) { return html`
Nothing currently playing!
Join a voice channel and type "/play" to get started!
` } 59 | return html` 60 |
61 | <${MediaSession} track=${player.current} paused=${player.paused} /> 62 | <${Toast} toast=${toast} /> 63 | <${NowPlaying} track=${player.current} paused=${player.paused} position=${player.position} repeatMode=${player.repeatMode} volume=${player.volume} /> 64 |
65 | <${Queue} tracks=${player.queue} /> 66 |
67 | ` 68 | } 69 | 70 | function NowPlaying({ track, position, paused, repeatMode, volume }) { 71 | return html` 72 |

Now Playing

73 |
74 | 88 |
89 | <${VolumeControl} volume=${volume} /> 90 | ` 91 | } 92 | 93 | function Thumbnail({ image }) { 94 | return html` 95 |
96 | Thumbnail Background 97 | Video Thumbnail 98 |
99 | ` 100 | } 101 | 102 | function MusicControls({ paused, repeatMode }) { 103 | return html` 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 |
112 | ` 113 | } 114 | 115 | function VolumeControl(props) { 116 | const [volume, setVolume] = React.useState(props.volume) 117 | React.useEffect(() => { 118 | setVolume(props.volume) 119 | }, [props.volume]) 120 | 121 | return html` 122 |
123 | 124 | { setVolume(event.target.value) }} onMouseUp=${(event) => { send({ type: 'volume', volume: event.target.value }) }} /> 125 |
126 | ` 127 | } 128 | 129 | function Queue({ tracks }) { 130 | // noinspection JSMismatchedCollectionQueryUpdate 131 | const rows = [] 132 | for (let i = 0; i < tracks.length; i++) { 133 | rows.push(html` 134 | 135 | ${i + 1} 136 | ${tracks[i].title} 137 | ${tracks[i].author} 138 | ${tracks[i].isStream ? '🔴 Live' : msToHMS(tracks[i].duration)} 139 | 140 | 141 | `) 142 | } 143 | return html` 144 |
145 |

Queue

146 | <${QueueButtons} /> 147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | ${rows} 160 | 161 |
#TrackAuthorDurationActions
162 |
163 |
164 | ` 165 | } 166 | 167 | function QueueButtons() { 168 | const input = React.createRef() 169 | const handlePlay = (event) => { 170 | event.preventDefault() 171 | send({ type: 'play', query: input.current.value }) 172 | input.current.value = '' 173 | } 174 | return html` 175 |
176 |
177 |
178 | 179 | 180 | 181 | 193 |
194 | 195 |
196 | ` 197 | } 198 | 199 | function Toast(props) { 200 | const [toast, setToast] = React.useState(props.toast) 201 | const [opacity, setOpacity] = React.useState(1) 202 | React.useEffect(() => { 203 | if (!props.toast || !props.toast.message) { return } 204 | setToast(props.toast) 205 | setOpacity(1) 206 | 207 | const timeouts = [] 208 | timeouts.push(setTimeout(() => { 209 | setOpacity(0) 210 | timeouts.push(setTimeout(() => { 211 | setToast(undefined) 212 | }, 1000)) 213 | }, 5000)) 214 | 215 | return () => { 216 | timeouts.forEach((timeout) => { clearTimeout(timeout) }) 217 | } 218 | }, [props.toast]) 219 | 220 | if (!toast) { return null } 221 | return html` 222 |
223 | ${toast.message} 224 |
225 | ` 226 | } 227 | 228 | function MediaSession({ track, paused }) { 229 | React.useEffect(async () => { 230 | if (navigator.userAgent.indexOf('Firefox') !== -1) { 231 | const audio = document.createElement('audio') 232 | audio.src = '/queue/near-silence.mp3' 233 | audio.volume = 0.00001 234 | audio.load() 235 | await audio.play().catch(() => { 236 | const div = document.getElementById('autoplay-alert') 237 | div.classList.add('alert', 'alert-danger', 'alert-dismissible', 'fade', 'show') 238 | div.setAttribute('role', 'alert') 239 | div.style.cssText = 'position: fixed; right: 1em; bottom: 0;' 240 | div.innerHTML = 'Autoplay seems to be disabled. Enable Media Autoplay to use media buttons to control the music bot!' 241 | }) 242 | setTimeout(() => audio.pause(), 100) 243 | } 244 | }, []) 245 | React.useEffect(() => { 246 | function htmlDecode(input) { return new DOMParser().parseFromString(input, 'text/html').documentElement.textContent } 247 | 248 | navigator.mediaSession.metadata = new MediaMetadata({ 249 | title: htmlDecode(track.title), 250 | artist: htmlDecode(track.author), 251 | album: htmlDecode(track.author), 252 | artwork: [{ src: htmlDecode(track.thumbnail) }] 253 | }) 254 | navigator.mediaSession.playbackState = paused ? 'paused' : 'playing' 255 | 256 | navigator.mediaSession.setActionHandler('play', () => { send({ type: 'pause' }) }) 257 | navigator.mediaSession.setActionHandler('pause', () => { send({ type: 'pause' }) }) 258 | navigator.mediaSession.setActionHandler('nexttrack', () => { send({ type: 'skip' }) }) 259 | navigator.mediaSession.setActionHandler('previoustrack', () => { send({ type: 'previous' }) }) 260 | }, [track, paused]) 261 | return html`
` 262 | } 263 | 264 | const domContainer = document.querySelector('#react-container') 265 | ReactDOM.render(html`<${App} />`, domContainer) 266 | -------------------------------------------------------------------------------- /dashboard/templates/404.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { djsClient, user, path, title: "SuitBot" }) %> 2 | 3 |
4 |
5 |

44

6 |

Page not found!

7 |

We searched the entire wardrobe, but couldn't find a matching suit! :(

8 | Home 9 |
10 |
11 | 12 | <%- include("partials/footer") %> 13 | -------------------------------------------------------------------------------- /dashboard/templates/admin.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { djsClient, user, path, title: 'Admin Panel' }) %> 2 | <% function uptime (ms) { 3 | let totalSeconds = (ms / 1000) 4 | const days = Math.floor(totalSeconds / 86400) 5 | totalSeconds %= 86400 6 | const hours = Math.floor(totalSeconds / 3600) 7 | totalSeconds %= 3600 8 | const minutes = Math.floor(totalSeconds / 60) 9 | const seconds = Math.floor(totalSeconds % 60) 10 | return `Uptime: ${days} days, ${hours} hours, ${minutes} minutes and ${seconds} seconds.` 11 | } %> 12 | 13 |
14 |
15 |
Admin Panel
16 |
17 |
18 |
<%= uptime(djsClient.uptime) %>
19 |
Playing in <%= djsClient.lavalink.manager.players.size %> server(s).
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% djsClient.guilds.cache.clone().sort((a, b) => { return a.joinedAt.getTime() - b.joinedAt.getTime() }).forEach(guild => { %> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <% }) %> 41 | 42 |
IconGuildMembersChannelsJoined
<%= guild.name %><%= guild.memberCount %><%= guild.channels.channelCountWithoutThreads %><%= guild.joinedAt.toUTCString() %>
43 |
44 |
45 |
46 | 47 | <%- include("partials/footer") %> 48 | -------------------------------------------------------------------------------- /dashboard/templates/commands.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { djsClient, user, path, title: "SuitBot" }) %> 2 | 3 | 7 | 8 |
9 |
10 |
Commands
11 |
12 |
13 | <%- markdown %> 14 |
15 |
16 | 17 | <%- include("partials/footer") %> 18 | -------------------------------------------------------------------------------- /dashboard/templates/dashboard.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { djsClient, user, path, title: "Your Servers" }) %> 2 | 3 |
4 |
5 |
Your Servers
6 |
7 |
8 |
9 | <% user.guilds.forEach(guild => { if (!djsClient.guilds.cache.get(guild.id)) { return } %> 10 |
11 | 12 |
13 | <%- djsClient.lavalink.getPlayer(guild.id) ? `` : '' %> 14 |
<%= guild.name %>
15 |

<%- djsClient.lavalink.getPlayer(guild.id) ? `Now Playing:
${djsClient.lavalink.getPlayer(guild.id).queue.current.title}` : 'Nothing playing on this server.' %>

16 |
17 |
18 | <% }) %> 19 |
20 |
21 |
22 | 23 | <%- include("partials/footer") %> 24 | -------------------------------------------------------------------------------- /dashboard/templates/index.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Due to new policies by Discord disallowing bots to play videos from YouTube, SuitBot will not be verified anymore.
This means that it will be unable to join more than 100 servers.
I'm currently working on creating a new bot that will allow users to self-host their own instance.
5 |
6 | 7 |
8 | <%- include("partials/header", { djsClient, user, path, title: "SuitBot" }) %> 9 | 10 |
11 |
12 |
13 |
14 |

SuitBot
Dashboard

15 |
Serving <%- djsClient.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0) %> users
in <%- djsClient.guilds.cache.size %> servers.
16 | Dashboard 17 |
18 |
19 |
20 |
21 |
Features
22 |
23 |
24 | Slash Commands 25 |
    26 |
  • Use commands directly integrated in Discord
  • 27 |
  • No more guessing with variables
  • 28 |
  • Quick overview of all commands
  • 29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 | Music 37 |
    38 |
  • Supports many sources (YouTube, Spotify, Bandcamp, SoundCloud, Twitch, Vimeo or any other HTTP source)
  • 39 |
  • Supports playlists and livestreams
  • 40 |
  • Interactive Web Dashboard
  • 41 |
  • Pause, Skip, Remove, Volume and more commands
  • 42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Language support 54 |
    55 |
  • Supports multiple languages
  • 56 |
  • Change the language for your server using "/language"
  • 57 |
  • Add your own language by contacting me on the Discord server
  • 58 |
59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Activities 70 |
    71 |
  • Create invites for Discord Activities
  • 72 |
  • YouTube Together and a lot of fun minigames
  • 73 |
  • Have fun with everyone in your voice channel
  • 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Basic Moderation 86 |
    87 |
  • Info commands (User, Server, Avatar)
  • 88 |
  • Kick, Ban, Move, Slowmode and more commands
  • 89 |
  • Permission check on commands
  • 90 |
91 |
92 |
93 | 94 |
95 |
96 |
97 |
98 |
99 | More Features 100 |
    101 |
  • Request any features you'd like to see with /suggestion
  • 102 |
  • Found a bug? Report it with /bugreport
  • 103 |
  • Check the GitHub repository to see what features are planned!
  • 104 |
105 |
106 |
107 |
108 | All Commands
109 | Commands 110 |
111 |
112 | 119 | 120 | <%- include("partials/footer") %> 121 | -------------------------------------------------------------------------------- /dashboard/templates/legal/privacy.ejs: -------------------------------------------------------------------------------- 1 | <%- include("../partials/header", { djsClient, user, path, title: "SuitBot" }) %> 2 | 3 |
4 |
5 |
Privacy Policy
6 |
7 |
8 |

SuitBot Privacy Policy

9 | 10 |

When using SuitBot, we collect data about you to bring you a better experience.

11 | 12 |

Last Modified: 12th June 2022.

13 | 14 |

What personal information do we collect from the people that use SuitBot?

15 | 16 |
    17 |
  • Public information about your Discord server. This includes your server ID, server name, server icon, server locale, channel names, channel IDs and role IDs
  • 18 |
  • Public information about the members of your Discord server. This includes member IDs, names, discriminators, icons and online status.
  • 19 |
  • Any errors that have occurred while using commands and what event from your discord server triggered it.
  • 20 |
  • When logging in with Discord on our website (https://suitbot.xyz) SuitBot stores public information about you in a session to let you stay logged in. These sessions expire no later than 24 hours of inactivity.
  • 21 |
22 | 23 |

Some information listed above may only be stored when specific commands are used.

24 | 25 |

How do we use your information?

26 | 27 |

Your information is used within the bot for features which require said information. The information used will not be stored longer than necessary and deleted very soon after your interaction with SuitBot is complete. This excludes your server ID and locale which is stored indefinitely until you discontinue your usage of SuitBot by removing it from your Discord server.

28 | 29 |

Agreement

30 | 31 |

By using SuitBot, you agree to the terms of this Privacy Policy. If you do not agree to these terms you must not use our service.

32 | 33 |

Changes to Policy

34 | 35 |

We reserve the right to update or modify this Privacy Policy at any time and from time to time without prior notice. Please review this policy periodically, and especially before you provide any information. This Privacy Policy was last updated on the date indicated above. Your continued use of the services after any changes or revisions to this Privacy Policy shall indicate your agreement with the terms of such revised Privacy Policy.

36 |
37 |
38 | 39 | <%- include("../partials/footer") %> 40 | -------------------------------------------------------------------------------- /dashboard/templates/legal/terms.ejs: -------------------------------------------------------------------------------- 1 | <%- include("../partials/header", { djsClient, user, path, title: "SuitBot" }) %> 2 | 3 |
4 |
5 |
Terms of Service
6 |
7 |
8 |

SuitBot Terms of Service

9 | 10 |

These Terms of Service ("Terms"), are a legal agreement between SuitBot ("Us", "We" or "Our") and you ("you", "your", the "user", or the "end user"). By using or interacting with the SuitBot Bot Instance (the "Bot") or the website located at https://suitbot.xyz (the "Site"), which are collectively referred to as the "Service", you ("User") agree to be bound by these Terms.

11 | 12 |

We reserve the right, at our sole discretion, to modify or revise these Terms at any time, and you agree to be bound by such modifications or revisions. 13 | Any such change or modification will be effective immediately, and your continued use of the Service will constitute your acceptance of, and agreement to, such changes or modifications. 14 | If you object to any change or modification, your sole recourse shall be to cease using the Service.

15 | 16 |

Last Modified: 12th June 2022.

17 | 18 |

Rights to use the Service

19 | 20 |

Subject to your compliance with these Terms, we grant you a limited, revocable, non-exclusive, non-transferable, non-sublicensable license to use and access the Service, solely for your personal, non-commercial use.

21 | 22 |

You agree not to (and not to attempt to) (i) use the Service for any use or purpose other than as expressly permitted by these Terms or (ii) copy, adapt, modify, prepare derivative works based upon, distribute, license, sell, transfer, publicly display, publicly perform, transmit, stream, broadcast or otherwise exploit the Service or any portion of the Service, except as expressly permitted in these Terms. No licenses or rights are granted to you by implication or otherwise under any intellectual property rights owned or controlled by us, except for the permissions and rights expressly granted in these Terms.

23 | 24 |

We reserve the right to modify or discontinue, temporarily or permanently, the Service (or any part thereof) with or without notice. We reserve the right to refuse any user access to the Services without notice for any reason, including but not limited to a violation of the Terms.

25 | 26 |

If you violate these Terms, we reserve the right to issue you a warning regarding the violation or immediately terminate or suspend your use of the service. You agree that we need not provide you notice before terminating or suspending your access, but we may do so.

27 | 28 |

Audio Streaming

29 | 30 |

SuitBot offers multiple sources ("Sources") to stream audio from. These sources can be streamed to various platforms SuitBot supports for you to listen to.

31 | 32 |

By you, the end user, requesting audio to be streamed from these sources, you accept that SuitBot will have no liability arising from your use of or access to any of that third-party's content, service or website.

33 | 34 |

Third-Party Services

35 | 36 |

We use third-party services to help us provide the Service, but such use does not indicate that we endorse them or are responsible or liable for their actions. In addition, the Service may link to third-party websites to facilitate its provision of services to you. If you use these links, you will leave the Service. We are not responsible for nor do we endorse these third-party websites or the organizations sponsoring such third-party websites or their products or services, whether or not we are affiliated with such third-party websites. You agree that we are not responsible or liable for any loss or damage of any sort incurred as a result of any such dealings you may have on or through a third-party website or as a result of the presence of any third-party advertising on the Service.

37 | 38 |

Disclaimer of Warranty

39 | 40 |

THE SERVICES AND THE SERVICE MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. IN ADDITION, WHILE THE WE ATTEMPT TO PROVIDE A GOOD USER EXPERIENCE, SUITBOT CANNOT AND DO NOT REPRESENT OR WARRANT THAT THE SERVICES WILL ALWAYS BE SECURE OR ERROR-FREE OR THAT THE SERVICES WILL ALWAYS FUNCTION WITHOUT DELAYS, DISRUPTIONS, OR IMPERFECTIONS. THE FOREGOING DISCLAIMERS SHALL APPLY TO THE EXTENT PERMITTED BY APPLICABLE LAW.

41 | 42 |

Limitation of Liability

43 | 44 |

TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL SUITBOT BE LIABLE TO YOU OR TO ANY THIRD PERSON FOR ANY CONSEQUENTIAL, INCIDENTAL, SPECIAL, PUNITIVE OR OTHER INDIRECT DAMAGES, INCLUDING ANY LOST PROFITS OR LOST DATA, ARISING FROM YOUR USE OF THE SERVICE OR OTHER MATERIALS ON, ACCESSED THROUGH OR DOWNLOADED FROM THE SERVICE, WHETHER BASED ON WARRANTY, CONTRACT, TORT, OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT WE HAVE BEEN ADVISED OF THE POSSIBILITY OF THESE DAMAGES.

45 |
46 |
47 | 48 | <%- include("../partials/footer") %> -------------------------------------------------------------------------------- /dashboard/templates/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dashboard/templates/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= title %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 53 | 54 | -------------------------------------------------------------------------------- /dashboard/templates/queue.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { djsClient, user, path, title: `Queue | ${guild.name}` }) %> 2 | 3 | 4 | 5 |
6 |
7 |
<%= guild.name %>
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | <%- include("partials/footer") %> -------------------------------------------------------------------------------- /dashboard/websocket.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js' 2 | import { server as WebsocketServer } from 'websocket' 3 | import { msToHMS } from '../utilities/utilities.js' 4 | import { getLanguage } from '../language/locale.js' 5 | 6 | const clients = {} 7 | 8 | function simplifyPlayer(player) { 9 | if (!player) { return {} } 10 | return { 11 | guild: player.guild, 12 | queue: player.queue, 13 | current: player.queue.current, 14 | paused: player.paused, 15 | volume: player.volume, 16 | filter: player.filter, 17 | position: player.position, 18 | timescale: player.timescale, 19 | repeatMode: player.queueRepeat ? 'queue' : player.trackRepeat ? 'track' : 'none' 20 | } 21 | } 22 | 23 | function send(ws, data) { 24 | ws.sendUTF(JSON.stringify(Object.assign({}, data))) 25 | } 26 | 27 | export function setupWebsocket(client, domain) { 28 | const wss = new WebsocketServer({ httpServer: client.dashboard }) 29 | 30 | // noinspection JSUnresolvedFunction 31 | wss.on('request', (request) => { 32 | if (request.origin !== domain) { return request.reject() } 33 | const ws = request.accept(null, request.origin) 34 | let guildId, userId 35 | 36 | ws.on('message', async (message) => { 37 | if (message.type !== 'utf8') { return } 38 | const data = JSON.parse(message.utf8Data) 39 | 40 | // Add to client manager 41 | guildId = data.guildId 42 | userId = data.userId 43 | clients[data.guildId] = { ...clients[data.guildId], [data.userId]: ws } 44 | 45 | // Fetch guild, user and queue 46 | const guild = client.guilds.cache.get(data.guildId) 47 | if (!guild) { return ws.close() } 48 | const user = await client.users.cache.get(data.userId) 49 | if (!user) { return ws.close() } 50 | const player = client.lavalink.getPlayer(guild.id) 51 | if (!player) { return send(ws, {}) } 52 | 53 | if (data.type === 'request') { return send(ws, simplifyPlayer(player)) } 54 | 55 | if (guild.members.cache.get(user.id)?.voice.channel !== guild.members.me.voice.channel) { return send(ws, { toast: { message: 'You need to be in the same voice channel as the bot to use this command!', type: 'danger' } }) } 56 | 57 | let toast = null 58 | switch (data.type) { 59 | case 'previous': { 60 | if (player.position > 5000) { 61 | await player.seek(0) 62 | break 63 | } 64 | try { 65 | if (player.previousTracks.length === 0) { return send(ws, { toast: { message: 'You can\'t skip to a previous song!' } }) } 66 | const track = player.previousTracks.pop() 67 | player.queue.add(track, 0) 68 | player.manager.once('trackEnd', (player) => { player.queue.add(player.previousTracks.pop(), 0) }) 69 | player.stop() 70 | toast = { message: 'Skipped to previous song.', type: 'info' } 71 | } catch (e) { 72 | await player.seek(0) 73 | } 74 | break 75 | } 76 | case 'pause': { 77 | player.pause(!player.paused) 78 | toast = { message: player.paused ? 'Paused.' : 'Resumed.', type: 'info' } 79 | break 80 | } 81 | case 'skip': { 82 | player.stop() 83 | toast = { message: 'Skipped.', type: 'info' } 84 | break 85 | } 86 | case 'shuffle': { 87 | player.queue.shuffle() 88 | toast = { message: 'Shuffled the queue.', type: 'info' } 89 | break 90 | } 91 | case 'repeat': { 92 | player.trackRepeat ? player.setQueueRepeat(true) : player.queueRepeat ? player.setTrackRepeat(false) : player.setTrackRepeat(true) 93 | toast = { message: `Set repeat mode to "${player.queueRepeat ? 'Queue' : player.trackRepeat ? 'Track' : 'None'}"`, type: 'info' } 94 | break 95 | } 96 | case 'volume': { 97 | player.setVolume(data.volume) 98 | break 99 | } 100 | case 'filter': { 101 | // noinspection JSUnresolvedFunction 102 | player.setFilter(data.filter) 103 | toast = { message: `Set filter to "${data.filter}"`, type: 'info' } 104 | break 105 | } 106 | case 'play': { 107 | if (!data.query) { return } 108 | const result = await player.search(data.query, user) 109 | if (result.loadType === 'LOAD_FAILED' || result.loadType === 'NO_MATCHES') { return send(ws, { toast: { message: 'There was an error while adding your song/playlist to the queue.', type: 'danger' } }) } 110 | const lang = getLanguage(await client.database.getLocale(guild.id)).play 111 | 112 | const embed = new EmbedBuilder() 113 | .setAuthor({ name: lang.author, iconURL: user.displayAvatarURL() }) 114 | .setFooter({ text: 'SuitBot Web Dashboard', iconURL: client.user.displayAvatarURL() }) 115 | 116 | if (result.loadType === 'PLAYLIST_LOADED') { 117 | player.queue.add(result.tracks) 118 | if (player.state !== 'CONNECTED') { await player.connect() } 119 | if (!player.playing && !player.paused && player.queue.totalSize === result.tracks.length) { await player.play() } 120 | 121 | // noinspection JSUnresolvedVariable 122 | embed 123 | .setTitle(result.playlist.name) 124 | .setURL(result.playlist.uri) 125 | .setThumbnail(result.playlist.thumbnail) 126 | .addFields([ 127 | { name: lang.fields.amount.name, value: lang.fields.amount.value(result.tracks.length), inline: true }, 128 | { name: lang.fields.author.name, value: result.playlist.author, inline: true }, 129 | { name: lang.fields.position.name, value: `${player.queue.indexOf(result.tracks[0]) + 1}-${player.queue.indexOf(result.tracks[result.tracks.length - 1]) + 1}`, inline: true } 130 | ]) 131 | toast = { message: `Added playlist "${result.playlist.name}" to the queue.`, type: 'info' } 132 | } else { 133 | const track = result.tracks[0] 134 | player.queue.add(track) 135 | if (player.state !== 'CONNECTED') { await player.connect() } 136 | if (!player.playing && !player.paused && !player.queue.length) { await player.play() } 137 | 138 | embed 139 | .setTitle(track.title) 140 | .setURL(track.uri) 141 | .setThumbnail(track.thumbnail) 142 | .addFields([ 143 | { name: lang.fields.duration.name, value: track.isStream ? '🔴 Live' : msToHMS(track.duration), inline: true }, 144 | { name: lang.fields.author.name, value: track.author, inline: true }, 145 | { name: lang.fields.position.name, value: (player.queue.indexOf(track) + 1).toString(), inline: true } 146 | ]) 147 | toast = { message: `Added track "${track.title}" to the queue.`, type: 'info' } 148 | } 149 | await client.channels.cache.get(player.textChannel).send({ embeds: [embed] }) 150 | break 151 | } 152 | case 'clear': { 153 | player.queue.clear() 154 | toast = { message: 'Cleared the queue.', type: 'info' } 155 | break 156 | } 157 | case 'remove': { 158 | const track = player.queue.remove(data.index - 1)[0] 159 | toast = { message: `Removed track #${data.index}: "${track.title}"`, type: 'info' } 160 | break 161 | } 162 | case 'skipto': { 163 | const track = player.queue[data.index - 1] 164 | player.stop(data.index) 165 | toast = { message: `Skipped to #${data.index}: "${track.title}"`, type: 'info' } 166 | break 167 | } 168 | } 169 | if (toast) { send(ws, { toast: toast }) } 170 | client.dashboard.update(player) 171 | }) 172 | 173 | ws.on('close', () => { 174 | // Remove from client manager 175 | if (!guildId || !userId) { return } 176 | delete clients[guildId][userId] 177 | if (Object.keys(clients[guildId]).length === 0) { delete clients[guildId] } 178 | }) 179 | }) 180 | 181 | client.dashboard.on('update', (player) => { 182 | if (clients[player.guild]) { 183 | for (const user of Object.keys(clients[player.guild])) { 184 | const ws = clients[player.guild][user] 185 | setTimeout(() => { send(ws, simplifyPlayer(player)) }, 1000) 186 | } 187 | } 188 | }) 189 | 190 | return wss 191 | } 192 | -------------------------------------------------------------------------------- /deploy-commands.js: -------------------------------------------------------------------------------- 1 | import { REST } from '@discordjs/rest' 2 | import { Routes } from 'discord-api-types/v10' 3 | import { getFilesRecursively } from './utilities/utilities.js' 4 | import { appId, guildId, token } from './utilities/config.js' 5 | import { logging } from './utilities/logging.js' 6 | 7 | const commands = [] 8 | 9 | for (const file of getFilesRecursively('./commands')) { 10 | const command = await import(`./${file}`) 11 | commands.push(command.data.toJSON()) 12 | } 13 | 14 | const rest = new REST({ version: '10' }).setToken(token) 15 | 16 | if (process.argv.includes('global')) { 17 | rest.put(Routes.applicationCommands(appId), { body: commands }) 18 | .then(() => logging.info('Successfully registered global application commands.')) 19 | .catch((error) => logging.error(error)) 20 | } else if (process.argv.includes('clear')) { 21 | Promise.all([ rest.put(Routes.applicationCommands(appId), { body: [] }), rest.put(Routes.applicationGuildCommands(appId, guildId), { body: [] })]) 22 | .then(() => logging.info('Successfully cleared application commands.')) 23 | .catch((error) => logging.error(error)) 24 | } else { 25 | rest.put(Routes.applicationGuildCommands(appId, guildId), { body: commands }) 26 | .then(() => logging.info('Successfully registered guild application commands.')) 27 | .catch((error) => logging.error(error)) 28 | } 29 | -------------------------------------------------------------------------------- /disabled_commands/feedback/bugreport.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { simpleEmbed } from '../../utilities/utilities.js' 3 | import { guildId } from '../../utilities/config.js' 4 | import { getLanguage } from '../../language/locale.js' 5 | 6 | export const { data, execute } = { 7 | data: new SlashCommandBuilder() 8 | .setName('bugreport') 9 | .setDescription('Reports a bug to the developer.') 10 | .addStringOption((option) => option.setName('bug').setDescription('A description of the bug.').setRequired(true)), 11 | async execute(interaction) { 12 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).bugreport 13 | const bug = interaction.options.getString('bug') 14 | const developerGuild = interaction.client.guilds.cache.get(guildId) 15 | const bugReportChannel = developerGuild.channels.cache.find((channel) => channel.name === 'bug-reports' && channel.isTextBased()) 16 | 17 | const embed = new EmbedBuilder() 18 | .setAuthor({ name: 'Bug report received', iconURL: interaction.member.user.displayAvatarURL() }) 19 | .setTitle(`By \`${interaction.member.user.tag}\` in \`${interaction.guild.name}\``) 20 | .setDescription(bug) 21 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 22 | 23 | bugReportChannel?.send({ embeds: [embed] }) 24 | 25 | await interaction.reply(simpleEmbed(lang.other.response)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /disabled_commands/feedback/github.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('github') 7 | .setDescription('Sends a link to the source code of this bot.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).github 10 | const embed = new EmbedBuilder() 11 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 12 | .setTitle(lang.title) 13 | .setURL('https://github.com/MeridianGH/suitbot') 14 | .setThumbnail(interaction.client.user.displayAvatarURL()) 15 | .setDescription(lang.description) 16 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 17 | await interaction.reply({ embeds: [embed] }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/feedback/suggestion.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { simpleEmbed } from '../../utilities/utilities.js' 3 | import { guildId } from '../../utilities/config.js' 4 | import { getLanguage } from '../../language/locale.js' 5 | 6 | export const { data, execute } = { 7 | data: new SlashCommandBuilder() 8 | .setName('suggestion') 9 | .setDescription('Sends a suggestion to the developer.') 10 | .addStringOption((option) => option.setName('suggestion').setDescription('The suggestion to send.').setRequired(true)), 11 | async execute(interaction) { 12 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).suggestion 13 | const suggestion = interaction.options.getString('suggestion') 14 | const developerGuild = interaction.client.guilds.cache.get(guildId) 15 | const suggestionChannel = developerGuild.channels.cache.find((channel) => channel.name === 'suggestions' && channel.isTextBased()) 16 | 17 | const embed = new EmbedBuilder() 18 | .setAuthor({ name: 'Suggestion received', iconURL: interaction.member.user.displayAvatarURL() }) 19 | .setTitle(`By \`${interaction.member.user.tag}\` in \`${interaction.guild.name}\``) 20 | .setDescription(suggestion) 21 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 22 | 23 | suggestionChannel?.send({ embeds: [embed], fetchReply: true }).then(async (message) => { await message.react('✅'); await message.react('❌') }) 24 | 25 | await interaction.reply(simpleEmbed(lang.other.response)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /disabled_commands/general/activity.js: -------------------------------------------------------------------------------- 1 | import { errorEmbed } from '../../utilities/utilities.js' 2 | import { REST } from '@discordjs/rest' 3 | import { ChannelType, Routes } from 'discord-api-types/v10' 4 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 5 | import { getLanguage } from '../../language/locale.js' 6 | 7 | export const { data, execute } = { 8 | data: new SlashCommandBuilder() 9 | .setName('activity') 10 | .setDescription('Creates a Discord activity.') 11 | .addStringOption((option) => option.setName('activity').setDescription('The activity to create.').setRequired(true).addChoices( 12 | { name: 'Watch Together', value: '880218394199220334' }, 13 | { name: 'Chess in the Park', value: '832012774040141894' }, 14 | { name: 'Checkers in the Park', value: '832013003968348200' }, 15 | { name: 'Fishington.io', value: '814288819477020702' }, 16 | { name: 'Betrayal.io', value: '773336526917861400' }, 17 | { name: 'Poker Night', value: '755827207812677713' }, 18 | { name: 'Letter Tile', value: '879863686565621790' }, 19 | { name: 'Word Snacks', value: '879863976006127627' }, 20 | { name: 'Doodle Crew', value: '878067389634314250' }, 21 | { name: 'Spellcast', value: '852509694341283871' } 22 | )) 23 | .addChannelOption((option) => option.setName('channel').setDescription('The voice channel to create the activity in.').addChannelTypes(ChannelType.GuildVoice).setRequired(true)), 24 | async execute(interaction) { 25 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).activity 26 | const channel = interaction.options.getChannel('channel') 27 | if (channel.type !== ChannelType.GuildVoice) { return await interaction.reply(errorEmbed(lang.errors.voiceChannel, true)) } 28 | 29 | const rest = new REST({ version: '10' }).setToken(interaction.client.token) 30 | await rest.post(Routes.channelInvites(channel.id), { body: { 'target_application_id': interaction.options.getString('activity'), 'target_type': 2 } }) 31 | .then(async (response) => { 32 | if (response.error || !response.code) { return interaction.reply(errorEmbed(lang.errors.generic, true)) } 33 | if (response.code === '50013') { return interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 34 | 35 | const embed = new EmbedBuilder() 36 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 37 | .setTitle(lang.title) 38 | .setURL(`https://discord.gg/${response.code}`) 39 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 40 | 41 | await interaction.reply({ embeds: [embed] }) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /disabled_commands/general/dashboard.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('dashboard') 7 | .setDescription('Sends a link to the dashboard.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).dashboard 10 | const embed = new EmbedBuilder() 11 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 12 | .setTitle(lang.title) 13 | .setURL(interaction.client.dashboard.host) 14 | .setThumbnail(interaction.client.user.displayAvatarURL()) 15 | .setDescription(lang.description) 16 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 17 | await interaction.reply({ embeds: [embed] }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/general/help.js: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import fs from 'fs' 3 | import { getLanguage } from '../../language/locale.js' 4 | import { logging } from '../../utilities/logging.js' 5 | 6 | const categories = ['general', 'music', 'moderation', 'feedback'] 7 | 8 | export const { data, execute } = { 9 | data: new SlashCommandBuilder() 10 | .setName('help') 11 | .setDescription('Replies with help on how to use this bot.') 12 | .addStringOption((option) => option.setName('category').setDescription('The category to display first.').addChoices( 13 | { name: 'General', value: '1' }, 14 | { name: 'Music', value: '2' }, 15 | { name: 'Moderation', value: '3' }, 16 | { name: 'Feedback', value: '4' } 17 | )), 18 | async execute(interaction) { 19 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).help 20 | 21 | const pages = [] 22 | 23 | const embed = new EmbedBuilder() 24 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 25 | .setTitle(lang.title) 26 | .setThumbnail(interaction.client.user.displayAvatarURL()) 27 | .setDescription(lang.description) 28 | .addFields([ 29 | { name: '➕ ' + lang.fields.invite.name, value: `[${lang.fields.invite.value}](https://discord.com/oauth2/authorize?client_id=887122733010411611&scope=bot%20applications.commands&permissions=2167425024)`, inline: true }, 30 | { name: '🌐 ' + lang.fields.website.name, value: `[${interaction.client.dashboard.host.replace(/^https?:\/\//, '')}](${interaction.client.dashboard.host})`, inline: true }, 31 | { name: '\u200b', value: '\u200b', inline: true }, 32 | { name: '<:github:923336812410306630> ' + lang.fields.github.name, value: `[${lang.fields.github.value}](https://github.com/MeridianGH/suitbot)`, inline: true }, 33 | { name: '<:discord:934041553209548840> ' + lang.fields.discord.name, value: `[${lang.fields.discord.value}](https://discord.gg/qX2CBrrUpf)`, inline: true }, 34 | { name: '\u200b', value: '\u200b', inline: true }, 35 | { name: '\u200b', value: lang.fields.buttons.value + '\nChange language with `/language`.', inline: false } 36 | ]) 37 | .setFooter({ text: `SuitBot | ${lang.other.page} ${pages.length + 1}/${categories.length + 1}`, iconURL: interaction.client.user.displayAvatarURL() }) 38 | pages.push(embed) 39 | 40 | for (const category of categories) { 41 | let description = '' 42 | const commands = fs.readdirSync('./commands/' + category) 43 | for (const command of commands) { 44 | const commandData = await import(`../${category}/${command}`) 45 | description = description + `\`${commands.indexOf(command) + 1}.\` **/${commandData.data.name}:** ${commandData.data.description}\n\n` 46 | } 47 | description = description + '\u2015'.repeat(34) 48 | 49 | const embed = new EmbedBuilder() 50 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 51 | .setTitle(`${categories.indexOf(category) + 1}. ${category.replace(/(^[a-z])/i, (str, first) => first.toUpperCase() )}`) 52 | .setDescription(description) 53 | .setFooter({ text: `SuitBot | ${lang.other.page} ${pages.length + 1}/${categories.length + 1}`, iconURL: interaction.client.user.displayAvatarURL() }) 54 | pages.push(embed) 55 | } 56 | 57 | const previous = new ButtonBuilder() 58 | .setCustomId('previous') 59 | .setLabel(lang.other.previous) 60 | .setStyle(ButtonStyle.Primary) 61 | const next = new ButtonBuilder() 62 | .setCustomId('next') 63 | .setLabel(lang.other.next) 64 | .setStyle(ButtonStyle.Primary) 65 | 66 | let currentIndex = Math.max(Number(interaction.options.getString('category')), 0) 67 | const message = await interaction.reply({ embeds: [pages[currentIndex]], components: [new ActionRowBuilder().setComponents([previous.setDisabled(currentIndex === 0), next.setDisabled(currentIndex === pages.length - 1)])], fetchReply: true }) 68 | 69 | // Collect button interactions (when a user clicks a button) 70 | const collector = message.createMessageComponentCollector({ idle: 300000 }) 71 | collector.on('collect', async (buttonInteraction) => { 72 | buttonInteraction.customId === 'previous' ? currentIndex -= 1 : currentIndex += 1 73 | await buttonInteraction.update({ embeds: [pages[currentIndex]], components: [new ActionRowBuilder().setComponents([previous.setDisabled(currentIndex === 0), next.setDisabled(currentIndex === pages.length - 1)])] }) 74 | }) 75 | collector.on('end', async () => { 76 | const fetchedMessage = await message.fetch(true).catch((e) => { logging.warn(`Failed to edit message components: ${e}`) }) 77 | await fetchedMessage?.edit({ components: [new ActionRowBuilder().setComponents(fetchedMessage.components[0].components.map((component) => ButtonBuilder.from(component.toJSON()).setDisabled(true)))] }) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /disabled_commands/general/info.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('info') 7 | .setDescription('Shows info about the bot.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).info 10 | let totalSeconds = interaction.client.uptime / 1000 11 | const days = Math.floor(totalSeconds / 86400) 12 | totalSeconds %= 86400 13 | const hours = Math.floor(totalSeconds / 3600) 14 | totalSeconds %= 3600 15 | const minutes = Math.floor(totalSeconds / 60) 16 | const seconds = Math.floor(totalSeconds % 60) 17 | const uptime = `${days} days, ${hours} hours, ${minutes} minutes and ${seconds} seconds.` 18 | 19 | const embed = new EmbedBuilder() 20 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 21 | .setTitle(lang.title) 22 | .setThumbnail(interaction.client.user.displayAvatarURL()) 23 | .addFields([ 24 | { name: lang.fields.servers.name, value: interaction.client.guilds.cache.size.toString(), inline: true }, 25 | { name: lang.fields.uptime.name, value: uptime, inline: true }, 26 | { name: lang.fields.memoryUsage.name, value: `heapUsed: ${Math.floor(process.memoryUsage().heapUsed / 1024 / 1024 * 100)}MB | heapTotal: ${Math.floor(process.memoryUsage().heapTotal / 1024 / 1024 * 100)}MB`, inline: false } 27 | ]) 28 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 29 | 30 | await interaction.reply({ embeds: [embed] }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /disabled_commands/general/invite.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('invite') 7 | .setDescription('Sends an invite link for the bot.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).invite 10 | const embed = new EmbedBuilder() 11 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 12 | .setTitle(lang.title) 13 | .setURL('https://discord.com/oauth2/authorize?client_id=887122733010411611&scope=bot%20applications.commands&permissions=2167425024') 14 | .setThumbnail(interaction.client.user.displayAvatarURL()) 15 | .setDescription(lang.description) 16 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 17 | await interaction.reply({ embeds: [embed] }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/general/language.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | import { errorEmbed } from '../../utilities/utilities.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('language') 8 | .setDescription('Changes the bots language.') 9 | .addStringOption((option) => option.setName('language').setDescription('The language to select.').setRequired(true).addChoices( 10 | { name: 'English', value: 'en-US' }, 11 | { name: 'Deutsch', value: 'de' }, 12 | { name: 'Suomi', value: 'fi' }, 13 | { name: '日本語', value: 'ja' }, 14 | { name: 'Português do Brasil', value: 'pt-BR' } 15 | )), 16 | async execute(interaction) { 17 | const langCode = interaction.options.getString('language') 18 | const lang = getLanguage(langCode).language 19 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 20 | 21 | await interaction.client.database.setLocale(interaction.guild.id, langCode) 22 | const embed = new EmbedBuilder() 23 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 24 | .setTitle(lang.title) 25 | .setDescription(lang.description(langCode)) 26 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 27 | 28 | await interaction.reply({ embeds: [embed] }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /disabled_commands/general/ping.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('ping') 7 | .setDescription('Replies with the current latency.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).ping 10 | const embed = new EmbedBuilder() 11 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 12 | .setTitle(lang.title) 13 | .setThumbnail(interaction.client.user.displayAvatarURL()) 14 | .setDescription('Ping: Pinging...\nAPI Latency: Pinging...') 15 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 16 | 17 | const message = await interaction.reply({ embeds: [embed], fetchReply: true }) 18 | const ping = message.createdTimestamp - interaction.createdTimestamp 19 | 20 | embed.setDescription(`Ping: ${ping}ms\nAPI Latency: ${Math.round(interaction.client.ws.ping)}ms`) 21 | await interaction.editReply({ embeds: [embed] }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /disabled_commands/general/serverinfo.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { getLanguage } from '../../language/locale.js' 3 | 4 | export const { data, execute } = { 5 | data: new SlashCommandBuilder() 6 | .setName('serverinfo') 7 | .setDescription('Shows info about the server.'), 8 | async execute(interaction) { 9 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).serverinfo 10 | const guild = interaction.guild 11 | const created = Math.floor(guild.createdAt.getTime() / 1000) 12 | const embed = new EmbedBuilder() 13 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 14 | .setTitle(guild.name) 15 | .setThumbnail(guild.iconURL({ dynamic: true, size: 1024 })) 16 | .addFields([ 17 | { name: lang.fields.members.name, value: guild.memberCount.toString(), inline: true }, 18 | { name: lang.fields.channels.name, value: guild.channels.channelCountWithoutThreads.toString(), inline: true }, 19 | { name: lang.fields.boosts.name, value: guild.premiumSubscriptionCount.toString() ?? '0', inline: true }, 20 | { name: lang.fields.owner.name, value: `<@${guild.ownerId}>`, inline: true }, 21 | { name: lang.fields.guildId.name, value: guild.id, inline: true }, 22 | { name: '\u200b', value: '\u200b', inline: true }, 23 | { name: lang.fields.created.name, value: ` ()`, inline: false } 24 | ]) 25 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 26 | 27 | await interaction.reply({ embeds: [embed] }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /disabled_commands/general/userinfo.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, GuildMember, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('userinfo') 8 | .setDescription('Shows info about a user.') 9 | .addMentionableOption((option) => option.setName('user').setDescription('The user to get info from.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).userinfo 12 | const member = interaction.options.getMentionable('user') 13 | if (!(member instanceof GuildMember)) { return await interaction.reply(errorEmbed(lang.errors.invalidUser, true)) } 14 | 15 | const created = Math.floor(member.user.createdAt.getTime() / 1000) 16 | const joined = Math.floor(member.joinedAt.getTime() / 1000) 17 | const embed = new EmbedBuilder() 18 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 19 | .setTitle(member.displayName) 20 | .setThumbnail(member.user.displayAvatarURL({ size: 1024 })) 21 | .addFields([ 22 | { name: lang.fields.fullName.name, value: member.user.tag, inline: true }, 23 | { name: lang.fields.nickname.name, value: member.nickname ?? '-', inline: true }, 24 | { name: lang.fields.bot.name, value: member.user.bot ? '✅' : '❌', inline: true }, 25 | { name: lang.fields.id.name, value: member.id, inline: true }, 26 | { name: lang.fields.profile.name, value: `<@${member.id}>`, inline: true }, 27 | { name: lang.fields.avatarURL.name, value: `[${lang.fields.avatarURL.value}](${member.user.displayAvatarURL({ size: 1024 })})`, inline: true }, 28 | { name: lang.fields.created.name, value: ` ()` }, 29 | { name: lang.fields.joined.name, value: ` ()` } 30 | ]) 31 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 32 | 33 | await interaction.reply({ embeds: [embed] }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /disabled_commands/moderation/ban.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('ban') 8 | .setDescription('Bans a user.') 9 | .addMentionableOption((option) => option.setName('user').setDescription('The user to ban.').setRequired(true)) 10 | .addStringOption((option) => option.setName('reason').setDescription('The reason for the ban.')), 11 | async execute(interaction) { 12 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).ban 13 | const member = interaction.options.getMentionable('user') 14 | const reason = interaction.options.getString('reason') 15 | 16 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.BanMembers)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 17 | if (!(member instanceof GuildMember)) { return await interaction.reply(errorEmbed(lang.errors.invalidUser, true)) } 18 | if (!member.bannable) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 19 | 20 | await member.ban({ reason: reason }).catch(async () => await interaction.reply(errorEmbed(lang.errors.generic, true))) 21 | 22 | const embed = new EmbedBuilder() 23 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 24 | .setTitle(member.displayName) 25 | .setThumbnail(member.user.displayAvatarURL({ size: 1024 })) 26 | .setDescription(lang.description(reason)) 27 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 28 | 29 | await interaction.reply({ embeds: [embed] }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /disabled_commands/moderation/kick.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('kick') 8 | .setDescription('Kicks a user.') 9 | .addMentionableOption((option) => option.setName('user').setDescription('The user to kick.').setRequired(true)) 10 | .addStringOption((option) => option.setName('reason').setDescription('The reason for the kick.')), 11 | async execute(interaction) { 12 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).kick 13 | const member = interaction.options.getMentionable('user') 14 | const reason = interaction.options.getString('reason') 15 | 16 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.KickMembers)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 17 | if (!(member instanceof GuildMember)) { return await interaction.reply(errorEmbed(lang.errors.invalidUser, true)) } 18 | if (!member.kickable) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 19 | 20 | await member.kick(reason).catch(async () => await interaction.reply(errorEmbed(lang.errors.generic, true))) 21 | 22 | const embed = new EmbedBuilder() 23 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 24 | .setTitle(member.displayName) 25 | .setThumbnail(member.user.displayAvatarURL({ size: 1024 })) 26 | .setDescription(lang.description(reason)) 27 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 28 | 29 | await interaction.reply({ embeds: [embed] }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /disabled_commands/moderation/move.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { ChannelType } from 'discord-api-types/v10' 4 | import { getLanguage } from '../../language/locale.js' 5 | 6 | export const { data, execute } = { 7 | data: new SlashCommandBuilder() 8 | .setName('move') 9 | .setDescription('Moves the mentioned user to the specified channel.') 10 | .addMentionableOption((option) => option.setName('user').setDescription('The user to move.').setRequired(true)) 11 | .addChannelOption((option) => option.setName('channel').setDescription('The channel to move to.').addChannelTypes(ChannelType.GuildVoice).setRequired(true)), 12 | async execute(interaction) { 13 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).move 14 | const member = interaction.options.getMentionable('user') 15 | const channel = interaction.options.getChannel('channel') 16 | 17 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.MoveMembers)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 18 | if (!(member instanceof GuildMember)) { return await interaction.reply(errorEmbed(lang.errors.invalidUser, true)) } 19 | if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.MoveMembers)) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 20 | 21 | await member.voice.setChannel(channel) 22 | 23 | const embed = new EmbedBuilder() 24 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 25 | .setTitle(`${member.displayName} → ${channel.name}`) 26 | .setThumbnail(member.user.displayAvatarURL({ size: 1024 })) 27 | .setDescription(lang.description(member.displayName, channel.name)) 28 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 29 | 30 | await interaction.reply({ embeds: [embed] }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /disabled_commands/moderation/moveall.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { ChannelType } from 'discord-api-types/v10' 4 | import { getLanguage } from '../../language/locale.js' 5 | 6 | export const { data, execute } = { 7 | data: new SlashCommandBuilder() 8 | .setName('moveall') 9 | .setDescription('Moves all users from the first channel to the second channel.') 10 | .addChannelOption((option) => option.setName('channel1').setDescription('The channel to move from.').addChannelTypes(ChannelType.GuildVoice).setRequired(true)) 11 | .addChannelOption((option) => option.setName('channel2').setDescription('The channel to move to.').addChannelTypes(ChannelType.GuildVoice).setRequired(true)), 12 | async execute(interaction) { 13 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).moveall 14 | const channel1 = interaction.options.getChannel('channel1') 15 | const channel2 = interaction.options.getChannel('channel2') 16 | 17 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.MoveMembers)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 18 | if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.MoveMembers)) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 19 | 20 | for (const user of channel1.members) { 21 | await user[1].voice.setChannel(channel2) 22 | } 23 | 24 | const embed = new EmbedBuilder() 25 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 26 | .setTitle(`${channel1.name} → ${channel2.name}`) 27 | .setThumbnail(interaction.guild.iconURL()) 28 | .setDescription(lang.description(channel1.name, channel2.name)) 29 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 30 | 31 | await interaction.reply({ embeds: [embed] }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /disabled_commands/moderation/purge.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('purge') 8 | .setDescription('Clears a specified amount of messages.') 9 | .addIntegerOption((option) => option.setName('amount').setDescription('The amount of messages to clear.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).purge 12 | let amount = interaction.options.getInteger('amount') 13 | amount = amount.toString() 14 | 15 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageMessages)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 16 | if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.ManageMessages)) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 17 | if (amount < 1 || amount > 100) { return await interaction.reply(errorEmbed(lang.errors.index, true)) } 18 | 19 | await interaction.channel.messages.fetch({ limit: amount }).then((messages) => { 20 | interaction.channel.bulkDelete(messages) 21 | }) 22 | await interaction.reply(simpleEmbed(lang.other.response(amount), true)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /disabled_commands/moderation/slowmode.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('slowmode') 8 | .setDescription('Sets the rate limit of the current channel.') 9 | .addIntegerOption((option) => option.setName('seconds').setDescription('The new rate limit in seconds.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).slowmode 12 | const seconds = interaction.options.getInteger('seconds') 13 | 14 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageChannels)) { return await interaction.reply(errorEmbed(lang.errors.userMissingPerms, true)) } 15 | if (!interaction.guild.members.me.permissions.has(PermissionsBitField.Flags.ManageChannels)) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 16 | 17 | await interaction.channel.setRateLimitPerUser(seconds) 18 | 19 | const embed = new EmbedBuilder() 20 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 21 | .setTitle(`#${interaction.channel.name}`) 22 | .setThumbnail(interaction.guild.iconURL()) 23 | .setDescription(lang.description(interaction.channel.name, seconds)) 24 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 25 | 26 | await interaction.reply({ embeds: [embed] }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /disabled_commands/music/clear.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('clear') 8 | .setDescription('Clears the queue.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).clear 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | player.queue.clear() 16 | await interaction.reply(simpleEmbed('🗑️ ' + lang.other.response)) 17 | interaction.client.dashboard.update(player) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/music/filter.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('filter') 8 | .setDescription('Sets filter modes for the player.') 9 | .addStringOption((option) => option.setName('filter').setDescription('The filter to select.').setRequired(true).addChoices( 10 | { name: 'None', value: 'none' }, 11 | { name: 'Bass Boost', value: 'bassboost' }, 12 | { name: 'Classic', value: 'classic' }, 13 | { name: '8D', value: 'eightd' }, 14 | { name: 'Earrape', value: 'earrape' }, 15 | { name: 'Karaoke', value: 'karaoke' }, 16 | { name: 'Nightcore', value: 'nightcore' }, 17 | { name: 'Superfast', value: 'superfast' }, 18 | { name: 'Vaporwave', value: 'vaporwave' } 19 | )), 20 | async execute(interaction) { 21 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).filter 22 | const filter = interaction.options.getString('filter') 23 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 24 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 25 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 26 | 27 | // noinspection JSUnresolvedFunction 28 | player.setFilter(filter) 29 | await interaction.reply(simpleEmbed(lang.other.response(filter))) 30 | interaction.client.dashboard.update(player) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /disabled_commands/music/lyrics.js: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed } from '../../utilities/utilities.js' 3 | import ytdl from 'ytdl-core' 4 | import { geniusClientToken } from '../../utilities/config.js' 5 | import genius from 'genius-lyrics' 6 | import { getLanguage } from '../../language/locale.js' 7 | import { logging } from '../../utilities/logging.js' 8 | 9 | const Genius = new genius.Client(geniusClientToken) 10 | 11 | export const { data, execute } = { 12 | data: new SlashCommandBuilder() 13 | .setName('lyrics') 14 | .setDescription('Shows the lyrics of the currently playing song.'), 15 | async execute(interaction) { 16 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).lyrics 17 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 18 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 19 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 20 | await interaction.deferReply() 21 | 22 | const info = await ytdl.getInfo(player.queue.current.uri) 23 | const title = info.videoDetails.media.category === 'Music' ? info.videoDetails.media.artist + ' ' + info.videoDetails.media.song : player.queue.current.title 24 | 25 | try { 26 | const song = (await Genius.songs.search(title))[0] 27 | const lyrics = await song.lyrics() 28 | 29 | const lines = lyrics.split('\n') 30 | const pages = [''] 31 | let index = 0 32 | for (let i = 0; i < lines.length; i++) { 33 | if (pages[index].length + lines[i].length > 4096) { 34 | index++ 35 | pages[index] = '' 36 | } 37 | pages[index] += '\n' + lines[i] 38 | } 39 | 40 | const isOnePage = pages.length === 1 41 | 42 | const previous = new ButtonBuilder() 43 | .setCustomId('previous') 44 | .setLabel(lang.other.previous) 45 | .setStyle(ButtonStyle.Primary) 46 | const next = new ButtonBuilder() 47 | .setCustomId('next') 48 | .setLabel(lang.other.next) 49 | .setStyle(ButtonStyle.Primary) 50 | 51 | const embed = new EmbedBuilder() 52 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 53 | .setTitle(player.queue.current.title) 54 | .setURL(song.url) 55 | .setThumbnail(player.queue.current.thumbnail) 56 | .setDescription(pages[0]) 57 | .setFooter({ text: `SuitBot | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'} | ${lang.other.genius}`, iconURL: interaction.client.user.displayAvatarURL() }) 58 | 59 | const message = await interaction.editReply({ embeds: [embed], components: isOnePage ? [] : [new ActionRowBuilder().setComponents([previous.setDisabled(true), next.setDisabled(false)])], fetchReply: true }) 60 | 61 | if (!isOnePage) { 62 | // Collect button interactions (when a user clicks a button) 63 | const collector = message.createMessageComponentCollector({ idle: 300000 }) 64 | let currentIndex = 0 65 | collector.on('collect', async (buttonInteraction) => { 66 | buttonInteraction.customId === 'previous' ? currentIndex -= 1 : currentIndex += 1 67 | await buttonInteraction.update({ 68 | embeds: [ 69 | new EmbedBuilder() 70 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 71 | .setTitle(player.queue.current.title) 72 | .setURL(song.url) 73 | .setThumbnail(player.queue.current.thumbnail) 74 | .setDescription(pages[currentIndex]) 75 | .setFooter({ text: `SuitBot | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'} | ${lang.other.genius}`, iconURL: interaction.client.user.displayAvatarURL() }) 76 | ], 77 | components: [new ActionRowBuilder().setComponents([previous.setDisabled(currentIndex === 0), next.setDisabled(currentIndex === pages.length - 1)])] 78 | }) 79 | }) 80 | collector.on('end', async () => { 81 | const fetchedMessage = await message.fetch(true).catch((e) => { logging.warn(`Failed to edit message components: ${e}`) }) 82 | await fetchedMessage?.edit({ components: [new ActionRowBuilder().setComponents([fetchedMessage.components[0].components.map((component) => ButtonBuilder.from(component.toJSON()).setDisabled(true))])] }) 83 | }) 84 | } 85 | } catch { 86 | await interaction.editReply({ 87 | embeds: [ 88 | new EmbedBuilder() 89 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 90 | .setTitle(player.queue.current.title) 91 | .setURL(player.queue.current.uri) 92 | .setThumbnail(player.queue.current.thumbnail) 93 | .setDescription(lang.other.noResults) 94 | .setFooter({ text: `SuitBot | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'} | ${lang.other.genius}`, iconURL: interaction.client.user.displayAvatarURL() }) 95 | ] 96 | }) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /disabled_commands/music/nowplaying.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { addMusicControls, errorEmbed, msToHMS } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('nowplaying') 8 | .setDescription('Shows the currently playing song.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).nowplaying 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | const track = player.queue.current 16 | 17 | const progress = Math.round(20 * player.position / player.queue.current.duration) 18 | const progressBar = '▬'.repeat(progress) + '🔘' + ' '.repeat(20 - progress) + '\n' + msToHMS(player.position) + '/' + msToHMS(player.queue.current.duration) 19 | 20 | const embed = new EmbedBuilder() 21 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 22 | .setTitle(track.title) 23 | .setURL(track.uri) 24 | .setThumbnail(track.thumbnail) 25 | .addFields([ 26 | { name: lang.fields.duration.name, value: track.isStream ? '🔴 Live' : `\`${progressBar}\``, inline: true }, 27 | { name: lang.fields.author.name, value: track.author, inline: true }, 28 | { name: lang.fields.requestedBy.name, value: track.requester.toString(), inline: true } 29 | ]) 30 | .setFooter({ text: `SuitBot | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'}`, iconURL: interaction.client.user.displayAvatarURL() }) 31 | 32 | const message = await interaction.reply({ embeds: [embed], fetchReply: true }) 33 | await addMusicControls(message, player) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /disabled_commands/music/pause.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('pause') 8 | .setDescription('Pauses or resumes playback.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).pause 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | player.pause(!player.paused) 16 | await interaction.reply(simpleEmbed(player.paused ? '⏸ ' + lang.other.paused : '▶ ' + lang.other.resumed)) 17 | interaction.client.dashboard.update(player) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/music/play.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from 'discord.js' 2 | import { addMusicControls, errorEmbed, msToHMS } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('play') 8 | .setDescription('Plays a song or playlist from YouTube.') 9 | .addStringOption((option) => option.setName('query').setDescription('The query to search for.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).play 12 | const channel = interaction.member.voice.channel 13 | if (!channel) { return await interaction.reply(errorEmbed(lang.errors.noVoiceChannel, true)) } 14 | if (interaction.guild.members.me.voice.channel && channel !== interaction.guild.members.me.voice.channel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 15 | if (!interaction.guild.members.me.permissionsIn(channel).has([PermissionsBitField.Flags.Connect, PermissionsBitField.Flags.Speak])) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 16 | await interaction.deferReply() 17 | 18 | const player = interaction.client.lavalink.createPlayer(interaction) 19 | 20 | const query = interaction.options.getString('query') 21 | const result = await player.search(query, interaction.member) 22 | if (result.loadType === 'LOAD_FAILED' || result.loadType === 'NO_MATCHES') { return await interaction.editReply(errorEmbed(lang.errors.generic)) } 23 | 24 | if (result.loadType === 'PLAYLIST_LOADED') { 25 | player.queue.add(result.tracks) 26 | 27 | if (player.state !== 'CONNECTED') { 28 | if (!interaction.member.voice.channel) { 29 | player.destroy() 30 | return await interaction.editReply(errorEmbed(lang.errors.noVoiceChannel)) 31 | } 32 | player.setVoiceChannel(interaction.member.voice.channel.id) 33 | await player.connect() 34 | } 35 | if (!player.playing && !player.paused && player.queue.totalSize === result.tracks.length) { await player.play() } 36 | interaction.client.dashboard.update(player) 37 | 38 | // noinspection JSUnresolvedVariable 39 | const embed = new EmbedBuilder() 40 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 41 | .setTitle(result.playlist.name) 42 | .setURL(result.playlist.uri) 43 | .setThumbnail(result.playlist.thumbnail) 44 | .addFields([ 45 | { name: lang.fields.amount.name, value: lang.fields.amount.value(result.tracks.length), inline: true }, 46 | { name: lang.fields.author.name, value: result.playlist.author, inline: true }, 47 | { name: lang.fields.position.name, value: `${player.queue.indexOf(result.tracks[0]) + 1}-${player.queue.indexOf(result.tracks[result.tracks.length - 1]) + 1}`, inline: true } 48 | ]) 49 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 50 | const message = await interaction.editReply({ embeds: [embed] }) 51 | await addMusicControls(message, player) 52 | } else { 53 | const track = result.tracks[0] 54 | player.queue.add(track) 55 | if (player.state !== 'CONNECTED') { 56 | if (!interaction.member.voice.channel) { 57 | player.destroy() 58 | return await interaction.editReply(errorEmbed(lang.errors.noVoiceChannel)) 59 | } 60 | player.setVoiceChannel(interaction.member.voice.channel.id) 61 | await player.connect() 62 | } 63 | if (!player.playing && !player.paused && !player.queue.length) { await player.play() } 64 | interaction.client.dashboard.update(player) 65 | 66 | const embed = new EmbedBuilder() 67 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 68 | .setTitle(track.title) 69 | .setURL(track.uri) 70 | .setThumbnail(track.thumbnail) 71 | .addFields([ 72 | { name: lang.fields.duration.name, value: track.isStream ? '🔴 Live' : msToHMS(track.duration), inline: true }, 73 | { name: lang.fields.author.name, value: track.author, inline: true }, 74 | { name: lang.fields.position.name, value: (player.queue.indexOf(track) + 1).toString(), inline: true } 75 | ]) 76 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 77 | const message = await interaction.editReply({ embeds: [embed] }) 78 | await addMusicControls(message, player) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /disabled_commands/music/previous.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('previous') 8 | .setDescription('Plays the previous track.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).previous 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | if (player.previousTracks.length === 0) { return await interaction.reply(errorEmbed(lang.errors.generic, true)) } 16 | 17 | const track = player.previousTracks.pop() 18 | player.queue.add(track, 0) 19 | player.manager.once('trackEnd', (player) => { player.queue.add(player.previousTracks.pop(), 0) }) 20 | player.stop() 21 | 22 | await interaction.reply(simpleEmbed('⏮ ' + lang.other.response(`\`#0\`: **${track.title}**`))) 23 | interaction.client.dashboard.update(player) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /disabled_commands/music/queue.js: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, msToHMS } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | import { logging } from '../../utilities/logging.js' 5 | 6 | export const { data, execute } = { 7 | data: new SlashCommandBuilder() 8 | .setName('queue') 9 | .setDescription('Displays the queue.'), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).queue 12 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 13 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 14 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 15 | 16 | const queue = player.queue 17 | const pages = [] 18 | 19 | if (queue.length === 0) { 20 | // Format single page with no upcoming songs. 21 | let description = lang.other.dashboard(interaction.client.dashboard.host) + '\n\n' 22 | description += `${lang.other.nowPlaying}\n[${queue.current.title}](${queue.current.uri}) | \`${queue.current.isStream ? '🔴 Live' : msToHMS(queue.current.duration)}\`\n\n` 23 | description += lang.other.noUpcomingSongs + '\u2015'.repeat(34) 24 | 25 | const embed = new EmbedBuilder() 26 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 27 | .setDescription(description) 28 | .setFooter({ text: `SuitBot | ${lang.other.page} 1/1 | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'}`, iconURL: interaction.client.user.displayAvatarURL() }) 29 | pages.push(embed) 30 | } else if (queue.length > 0 && queue.length <= 10) { 31 | // Format single page. 32 | let description = lang.other.dashboard(interaction.client.dashboard.host) + '\n\n' 33 | description += `${lang.other.nowPlaying}\n[${queue.current.title}](${queue.current.uri}) | \`${queue.current.isStream ? '🔴 Live' : msToHMS(queue.current.duration)}\`\n\n` 34 | for (const track of queue) { description += `\`${queue.indexOf(track) + 1}.\` [${track.title}](${track.uri}) | \`${track.isStream ? '🔴 Live' : msToHMS(track.duration)}\`\n\n` } 35 | description += `**${lang.other.songsInQueue(queue.length)} | ${lang.other.totalDuration(msToHMS(queue.duration))}**\n${'\u2015'.repeat(34)}` 36 | 37 | const embed = new EmbedBuilder() 38 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 39 | .setDescription(description) 40 | .setFooter({ text: `SuitBot | ${lang.other.page} 1/1 | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'}`, iconURL: interaction.client.user.displayAvatarURL() }) 41 | pages.push(embed) 42 | } else { 43 | // Format all pages. 44 | for (let i = 0; i < queue.length; i += 10) { 45 | const tracks = queue.slice(i, i + 10) 46 | 47 | let description = lang.other.dashboard(interaction.client.dashboard.host) + '\n\n' 48 | description += `${lang.other.nowPlaying}\n[${queue.current.title}](${queue.current.uri}) | \`${queue.current.isStream ? '🔴 Live' : msToHMS(queue.current.duration)}\`\n\n` 49 | for (const track of tracks) { description += `\`${queue.indexOf(track) + 1}.\` [${track.title}](${track.uri}) | \`${track.isStream ? '🔴 Live' : msToHMS(track.duration)}\`\n\n` } 50 | description += `**${lang.other.songsInQueue(queue.length)} | ${lang.other.totalDuration(msToHMS(queue.duration))}**\n${'\u2015'.repeat(34)}` 51 | 52 | const embed = new EmbedBuilder() 53 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 54 | .setDescription(description) 55 | .setFooter({ text: `SuitBot | ${lang.other.page} ${pages.length + 1}/${Math.ceil(queue.length / 10)} | ${lang.other.repeatModes.repeat}: ${player.queueRepeat ? '🔁 ' + lang.other.repeatModes.queue : player.trackRepeat ? '🔂 ' + lang.other.repeatModes.track : '❌'}`, iconURL: interaction.client.user.displayAvatarURL() }) 56 | pages.push(embed) 57 | } 58 | } 59 | 60 | const isOnePage = pages.length === 1 61 | 62 | const previous = new ButtonBuilder() 63 | .setCustomId('previous') 64 | .setLabel(lang.other.previous) 65 | .setStyle(ButtonStyle.Primary) 66 | const next = new ButtonBuilder() 67 | .setCustomId('next') 68 | .setLabel(lang.other.next) 69 | .setStyle(ButtonStyle.Primary) 70 | 71 | const message = await interaction.reply({ embeds: [pages[0]], components: isOnePage ? [] : [new ActionRowBuilder().setComponents([previous.setDisabled(true), next.setDisabled(false)])], fetchReply: true }) 72 | 73 | if (!isOnePage) { 74 | // Collect button interactions (when a user clicks a button), 75 | const collector = message.createMessageComponentCollector({ idle: 300000 }) 76 | let currentIndex = 0 77 | collector.on('collect', async (buttonInteraction) => { 78 | buttonInteraction.customId === 'previous' ? currentIndex -= 1 : currentIndex += 1 79 | await buttonInteraction.update({ embeds: [pages[currentIndex]], components: [new ActionRowBuilder({ components: [previous.setDisabled(currentIndex === 0), next.setDisabled(currentIndex === pages.length - 1)] })] }) 80 | }) 81 | collector.on('end', async () => { 82 | const fetchedMessage = await message.fetch(true).catch((e) => { logging.warn(`Failed to edit message components: ${e}`) }) 83 | await fetchedMessage?.edit({ components: [new ActionRowBuilder().setComponents(fetchedMessage.components[0].components.map((component) => ButtonBuilder.from(component.toJSON()).setDisabled(true)))] }) 84 | }) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /disabled_commands/music/remove.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('remove') 8 | .setDescription('Removes the specified track from the queue.') 9 | .addIntegerOption((option) => option.setName('track').setDescription('The track to remove.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).remove 12 | const index = interaction.options.getInteger('track') 13 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 14 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 15 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 16 | 17 | if (index < 1 || index > player.queue.length) { return await interaction.reply(errorEmbed(lang.errors.index(player.queue.length), true)) } 18 | const track = player.queue.remove(index - 1)[0] 19 | await interaction.reply(simpleEmbed('🗑️ ' + lang.other.response(`\`#${index}\`: **${track.title}**`))) 20 | interaction.client.dashboard.update(player) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /disabled_commands/music/repeat.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('repeat') 8 | .setDescription('Sets the current repeat mode.') 9 | .addStringOption((option) => option.setName('mode').setDescription('The mode to set.').setRequired(true).addChoices( 10 | { name: 'None', value: 'none' }, 11 | { name: 'Track', value: 'track' }, 12 | { name: 'Queue', value: 'queue' } 13 | )), 14 | async execute(interaction) { 15 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).repeat 16 | const mode = interaction.options.getString('mode') 17 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 18 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 19 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 20 | 21 | mode === 'track' ? player.setTrackRepeat(true) : mode === 'queue' ? player.setQueueRepeat(true) : player.setTrackRepeat(false) 22 | await interaction.reply(simpleEmbed(lang.other.response(player.queueRepeat ? lang.other.repeatModes.queue + ' 🔁' : player.trackRepeat ? lang.other.repeatModes.track + ' 🔂' : lang.other.repeatModes.none + ' ▶'))) 23 | interaction.client.dashboard.update(player) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /disabled_commands/music/resume.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('resume') 8 | .setDescription('Resumes playback.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).resume 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | if (!player.paused) { return await interaction.reply(errorEmbed(lang.errors.notPaused, true)) } 15 | 16 | player.pause(false) 17 | await interaction.reply(simpleEmbed('▶ ' + lang.other.response)) 18 | interaction.client.dashboard.update(player) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /disabled_commands/music/search.js: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, EmbedBuilder, PermissionsBitField, SelectMenuBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { addMusicControls, errorEmbed, msToHMS } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('search') 8 | .setDescription('Searches five songs from YouTube and lets you select one to play.') 9 | .addStringOption((option) => option.setName('query').setDescription('The query to search for.').setRequired(true)), 10 | async execute(interaction) { 11 | const { search: lang, play } = getLanguage(await interaction.client.database.getLocale(interaction.guildId)) 12 | const channel = interaction.member.voice.channel 13 | if (!channel) { return await interaction.reply(errorEmbed(lang.errors.noVoiceChannel, true)) } 14 | if (interaction.guild.members.me.voice.channel && channel !== interaction.guild.members.me.voice.channel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 15 | if (!interaction.guild.members.me.permissionsIn(channel).has([PermissionsBitField.Flags.Connect, PermissionsBitField.Flags.Speak])) { return await interaction.reply(errorEmbed(lang.errors.missingPerms, true)) } 16 | await interaction.deferReply() 17 | 18 | const player = interaction.client.lavalink.createPlayer(interaction) 19 | 20 | const query = interaction.options.getString('query') 21 | const result = await player.search(query, interaction.member) 22 | if (result.loadType !== 'SEARCH_RESULT') { return await interaction.editReply(errorEmbed(lang.errors.generic)) } 23 | const tracks = result.tracks.slice(0, 5).map((track, index) => ({ label: track.title, description: track.author, value: index.toString() })) 24 | 25 | const selectMenu = new SelectMenuBuilder() 26 | .setCustomId('search') 27 | .setPlaceholder(lang.other.select) 28 | .addOptions(...tracks) 29 | 30 | const embedMessage = await interaction.editReply({ 31 | embeds: [ 32 | new EmbedBuilder() 33 | .setAuthor({ name: lang.author, iconURL: interaction.member.user.displayAvatarURL() }) 34 | .setTitle(lang.title(interaction.options.getString('query'))) 35 | .setThumbnail(result.tracks[0].thumbnail) 36 | .setFooter({ text: `SuitBot | ${lang.other.expires}`, iconURL: interaction.client.user.displayAvatarURL() }) 37 | ], 38 | components: [new ActionRowBuilder().setComponents(selectMenu)] 39 | }) 40 | 41 | const collector = embedMessage.createMessageComponentCollector({ time: 60000, filter: async (c) => { await c.deferUpdate(); return c.user.id === interaction.user.id } }) 42 | collector.on('collect', async (menuInteraction) => { 43 | const track = result.tracks[Number(menuInteraction.values[0])] 44 | player.queue.add(track) 45 | if (player.state !== 'CONNECTED') { 46 | if (!interaction.member.voice.channel) { 47 | player.destroy() 48 | return await interaction.editReply(Object.assign(errorEmbed(lang.errors.noVoiceChannel), { components: [] })) 49 | } 50 | player.setVoiceChannel(interaction.member.voice.channel.id) 51 | await player.connect() 52 | } 53 | if (!player.playing && !player.paused && !player.queue.length) { await player.play() } 54 | interaction.client.dashboard.update(player) 55 | 56 | const embed = new EmbedBuilder() 57 | .setAuthor({ name: play.author, iconURL: interaction.member.user.displayAvatarURL() }) 58 | .setTitle(track.title) 59 | .setURL(track.uri) 60 | .setThumbnail(track.thumbnail) 61 | .addFields([ 62 | { name: play.fields.duration.name, value: track.isStream ? '🔴 Live' : msToHMS(track.duration), inline: true }, 63 | { name: play.fields.author.name, value: track.author, inline: true }, 64 | { name: play.fields.position.name, value: (player.queue.indexOf(track) + 1).toString(), inline: true } 65 | ]) 66 | .setFooter({ text: 'SuitBot', iconURL: interaction.client.user.displayAvatarURL() }) 67 | 68 | const message = await menuInteraction.editReply({ embeds: [embed], components: [] }) 69 | collector.stop() 70 | await addMusicControls(message, player) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /disabled_commands/music/seek.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, msToHMS, simpleEmbed, timeToMs } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('seek') 8 | .setDescription('Skips to the specified point in the current track.') 9 | .addStringOption((option) => option.setName('time').setDescription('The time to skip to. Can be seconds or HH:MM:SS.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).seek 12 | const time = timeToMs(interaction.options.getString('time')) 13 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 14 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 15 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 16 | if (player.queue.current.isStream) { return await interaction.reply(errorEmbed(lang.errors.isLive, true)) } 17 | if (time < 0 || time > player.queue.current.duration) { return await interaction.reply(errorEmbed(lang.errors.index(player.queue.current.duration), true)) } 18 | 19 | player.seek(time) 20 | await interaction.reply(simpleEmbed('⏩ ' + lang.other.response(msToHMS(time)))) 21 | interaction.client.dashboard.update(player) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /disabled_commands/music/shuffle.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('shuffle') 8 | .setDescription('Shuffles the queue.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).shuffle 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | player.queue.shuffle() 16 | await interaction.reply(simpleEmbed('🔀 ' + lang.other.response)) 17 | interaction.client.dashboard.update(player) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/music/skip.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('skip') 8 | .setDescription('Skips the current track or to a specified point in the queue.') 9 | .addIntegerOption((option) => option.setName('track').setDescription('The track to skip to.')), 10 | async execute(interaction) { 11 | const { skip: lang, stop } = getLanguage(await interaction.client.database.getLocale(interaction.guildId)) 12 | const index = interaction.options.getInteger('track') 13 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 14 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 15 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 16 | if (index > player.queue.length && player.queue.length > 0) { return await interaction.reply(errorEmbed(lang.errors.index(player.queue.length))) } 17 | 18 | if (player.queue.length === 0) { 19 | player.destroy() 20 | await interaction.reply(simpleEmbed('⏹ ' + stop.other.response)) 21 | interaction.client.dashboard.update(player) 22 | return 23 | } 24 | 25 | if (index) { 26 | const track = player.queue[index - 1] 27 | player.stop(index) 28 | await interaction.reply(simpleEmbed('⏭ ' + lang.other.skippedTo(`\`#${index}\`: **${track.title}**`))) 29 | } else { 30 | player.stop() 31 | await interaction.reply(simpleEmbed('⏭ ' + lang.other.skipped)) 32 | } 33 | interaction.client.dashboard.update(player) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /disabled_commands/music/stop.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('stop') 8 | .setDescription('Stops playback.'), 9 | async execute(interaction) { 10 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).stop 11 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 12 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 13 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 14 | 15 | player.destroy() 16 | await interaction.reply(simpleEmbed('⏹ ' + lang.other.response)) 17 | interaction.client.dashboard.update(player) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /disabled_commands/music/volume.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import { errorEmbed, simpleEmbed } from '../../utilities/utilities.js' 3 | import { getLanguage } from '../../language/locale.js' 4 | 5 | export const { data, execute } = { 6 | data: new SlashCommandBuilder() 7 | .setName('volume') 8 | .setDescription('Sets the volume of the music player.') 9 | .addIntegerOption((option) => option.setName('volume').setDescription('The volume to set the player to.').setRequired(true)), 10 | async execute(interaction) { 11 | const lang = getLanguage(await interaction.client.database.getLocale(interaction.guildId)).volume 12 | const volume = Math.min(Math.max(interaction.options.getInteger('volume'), 0), 100) 13 | const player = interaction.client.lavalink.getPlayer(interaction.guild.id) 14 | if (!player || !player.queue.current) { return await interaction.reply(errorEmbed(lang.errors.nothingPlaying, true)) } 15 | if (interaction.member.voice.channel?.id !== player.voiceChannel) { return await interaction.reply(errorEmbed(lang.errors.sameChannel, true)) } 16 | 17 | player.setVolume(volume) 18 | await interaction.reply(simpleEmbed('🔊 ' + lang.other.response(volume))) 19 | interaction.client.dashboard.update(player) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | import { logging } from '../utilities/logging.js' 2 | 3 | export const { data, execute } = { 4 | data: { name: 'guildCreate' }, 5 | async execute(guild) { 6 | logging.info(`Joined a new guild: ${guild.name}.`) 7 | await guild.client.database.addServer(guild) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /events/guildDelete.js: -------------------------------------------------------------------------------- 1 | import { logging } from '../utilities/logging.js' 2 | 3 | export const { data, execute } = { 4 | data: { name: 'guildDelete' }, 5 | async execute(guild) { 6 | logging.info(`Removed from guild: ${guild.name}.`) 7 | await guild.client.database.removeServer(guild) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | import { errorEmbed } from '../utilities/utilities.js' 2 | import { logging } from '../utilities/logging.js' 3 | 4 | export const { data, execute } = { 5 | data: { name: 'interactionCreate' }, 6 | async execute(interaction) { 7 | if (!interaction.isCommand()) { return } 8 | if (interaction.guild === null) { return await interaction.reply(errorEmbed('Commands are not supported in DMs.\nPlease use the bot in a server.')) } 9 | 10 | const command = interaction.client.commands.get(interaction.commandName) 11 | if (!command) { return } 12 | 13 | try { 14 | command.execute(interaction) 15 | } catch (error) { 16 | console.error(error) 17 | return await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }) 18 | } 19 | 20 | logging.info(`${interaction.user.tag} triggered /${interaction.commandName} in #${interaction.channel.name}/${interaction.guild.name}.`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | import { startDashboard } from '../dashboard/dashboard.js' 2 | import { logging } from '../utilities/logging.js' 3 | 4 | let iconURL = null 5 | export { iconURL } 6 | export const { data, execute } = { 7 | data: { name: 'ready', once: true }, 8 | async execute(client) { 9 | const now = new Date() 10 | const date = now.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' }) + ' - ' + now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }) 11 | logging.success(`${client.user.tag} connected to Discord at ${date}`) 12 | // startDashboard(client) 13 | iconURL = client.user.displayAvatarURL() 14 | // await client.database.addAllServers(client.guilds.cache) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /language/lang/en-US.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnusedGlobalSymbols 2 | 3 | // General 4 | export const activity = { 5 | author: 'Activity.', 6 | title: 'Click here to open Activity', 7 | errors: { 8 | generic: 'An error occurred while creating your activity!', 9 | voiceChannel: 'You can only specify a voice channel!', 10 | missingPerms: 'The bot is missing permissions to perform that action.' 11 | } 12 | } 13 | export const dashboard = { 14 | author: 'Dashboard.', 15 | title: 'SuitBot Dashboard', 16 | description: 'The bots dashboard website.' 17 | } 18 | export const help = { 19 | author: 'Help.', 20 | title: 'SuitBot Help Page', 21 | description: 'This module lists every command SuitBot currently supports.\n\nTo use a command start by typing `/` followed by the command you want to execute. You can also use Discord\'s integrated auto-completion for commands.\n\n', 22 | fields: { 23 | invite: { name: 'Invite', value: 'Click here to invite' }, 24 | website: { name: 'Website', value: null }, 25 | github: { name: 'Source code', value: 'GitHub' }, 26 | discord: { name: 'Discord', value: 'Invite' }, 27 | buttons: { name: null, value: 'Press the buttons below to switch pages and display more info.' } 28 | }, 29 | other: { 30 | page: 'Page', 31 | previous: 'Previous', 32 | next: 'Next' 33 | } 34 | } 35 | export const info = { 36 | author: 'Info.', 37 | title: 'Bot Information', 38 | fields: { 39 | servers: { name: 'Servers', value: null }, 40 | uptime: { name: 'Uptime', value: null }, 41 | memoryUsage: { name: 'Memory Usage', value: null } 42 | } 43 | } 44 | export const invite = { 45 | author: 'Invite.', 46 | title: 'Invite SuitBot', 47 | description: 'Click this link to invite SuitBot to your server!' 48 | } 49 | export const language = { 50 | author: 'Language.', 51 | title: 'Change language', 52 | description: (langCode) => `Set language to \`${langCode}\`.`, 53 | errors: { userMissingPerms: 'You do not have permission to execute this command!' } 54 | } 55 | export const ping = { 56 | author: 'Ping', 57 | title: 'Bot & API Latency' 58 | } 59 | export const serverinfo = { 60 | author: 'Server Information.', 61 | fields: { 62 | members: { name: 'Members', value: null }, 63 | channels: { name: 'Channels', value: null }, 64 | boosts: { name: 'Boosts', value: null }, 65 | owner: { name: 'Owner', value: null }, 66 | guildId: { name: 'Guild ID', value: null }, 67 | created: { name: 'Created', value: null } 68 | } 69 | } 70 | export const userinfo = { 71 | author: 'User Information.', 72 | fields: { 73 | fullName: { name: 'Full name', value: null }, 74 | nickname: { name: 'Nickname', value: null }, 75 | bot: { name: 'Bot', value: null }, 76 | id: { name: 'ID', value: null }, 77 | profile: { name: 'Profile', value: null }, 78 | avatarURL: { name: 'Avatar URL', value: 'Avatar URL' }, 79 | created: { name: 'Created', value: null }, 80 | joined: { name: 'Joined', value: null } 81 | }, 82 | other: { 83 | online: 'Online', 84 | idle: 'Idle', 85 | dnd: 'Do not disturb', 86 | offline: 'Offline' 87 | }, 88 | errors: { invalidUser: 'You can only specify a valid user!' } 89 | } 90 | 91 | // Music 92 | const musicErrors = { 93 | nothingPlaying: 'Nothing currently playing.\nStart playback with `/play`!', 94 | sameChannel: 'You need to be in the same voice channel as the bot to use this command!', 95 | noVoiceChannel: 'You need to be in a voice channel to use this command.', 96 | missingPerms: 'The bot does not have the correct permissions to play in your voice channel!' 97 | } 98 | const repeatModes = { 99 | repeat: 'Repeat', 100 | none: 'None', 101 | track: 'Track', 102 | queue: 'Queue' 103 | } 104 | export const clear = { 105 | other: { response: 'Cleared the queue.' }, 106 | errors: { 107 | nothingPlaying: musicErrors.nothingPlaying, 108 | sameChannel: musicErrors.sameChannel 109 | } 110 | } 111 | export const filter = { 112 | other: { response: (filter) => `Set filter to ${filter}.` }, 113 | errors: { 114 | nothingPlaying: musicErrors.nothingPlaying, 115 | sameChannel: musicErrors.sameChannel 116 | } 117 | } 118 | export const lyrics = { 119 | author: 'Lyrics.', 120 | other: { 121 | repeatModes, 122 | genius: 'Provided by genius.com', 123 | noResults: 'No results found!', 124 | previous: 'Previous', 125 | next: 'Next' 126 | }, 127 | errors: { 128 | nothingPlaying: musicErrors.nothingPlaying, 129 | sameChannel: musicErrors.sameChannel 130 | } 131 | } 132 | export const nowplaying = { 133 | author: 'Now Playing...', 134 | fields: { 135 | duration: { name: 'Duration', value: null }, 136 | author: { name: 'Author', value: null }, 137 | requestedBy: { name: 'Requested By', value: null } 138 | }, 139 | other: { repeatModes }, 140 | errors: { 141 | nothingPlaying: musicErrors.nothingPlaying, 142 | sameChannel: musicErrors.sameChannel 143 | } 144 | } 145 | export const pause = { 146 | other: { 147 | paused: 'Paused.', 148 | resumed: 'Resumed.' 149 | }, 150 | errors: { 151 | nothingPlaying: musicErrors.nothingPlaying, 152 | sameChannel: musicErrors.sameChannel 153 | } 154 | } 155 | export const play = { 156 | author: 'Added to queue.', 157 | fields: { 158 | amount: { name: 'Amount', value: (amount) => `${amount} songs` }, 159 | duration: { name: 'Duration', value: null }, 160 | author: { name: 'Author', value: null }, 161 | position: { name: 'Position', value: null } 162 | }, 163 | errors: { 164 | generic: 'There was an error while adding your song to the queue.', 165 | noVoiceChannel: musicErrors.noVoiceChannel, 166 | sameChannel: musicErrors.sameChannel, 167 | missingPerms: musicErrors.missingPerms 168 | } 169 | } 170 | export const previous = { 171 | other: { response: (track) => `Playing previous track ${track}.` }, 172 | errors: { 173 | generic: 'You can\'t use the command `/previous` right now!', 174 | nothingPlaying: musicErrors.nothingPlaying, 175 | sameChannel: musicErrors.sameChannel 176 | } 177 | } 178 | export const queue = { 179 | author: 'Queue.', 180 | other: { 181 | dashboard: (url) => `Still using old and boring commands? Use the new [web dashboard](${url}) instead!`, 182 | nowPlaying: 'Now Playing:', 183 | noUpcomingSongs: 'No upcoming songs.\nAdd songs with `/play`!\n', 184 | songsInQueue: (amount) => `${amount} songs in queue`, 185 | totalDuration: (duration) => `${duration} total duration`, 186 | page: 'Page', 187 | previous: 'Previous', 188 | next: 'Next', 189 | repeatModes 190 | }, 191 | errors: { 192 | nothingPlaying: musicErrors.nothingPlaying, 193 | sameChannel: musicErrors.sameChannel 194 | } 195 | } 196 | export const remove = { 197 | other: { response: (track) => `Removed track ${track}.` }, 198 | errors: { 199 | index: (index) => `You can only specify a song number between 1-${index}.`, 200 | nothingPlaying: musicErrors.nothingPlaying, 201 | sameChannel: musicErrors.sameChannel 202 | } 203 | } 204 | export const repeat = { 205 | other: { 206 | response: (mode) => `Set repeat mode to ${mode}.`, 207 | repeatModes 208 | }, 209 | errors: { 210 | nothingPlaying: musicErrors.nothingPlaying, 211 | sameChannel: musicErrors.sameChannel 212 | } 213 | } 214 | export const resume = { 215 | other: { response: 'Resumed.' }, 216 | errors: { 217 | notPaused: 'The queue is not paused!', 218 | nothingPlaying: musicErrors.nothingPlaying, 219 | sameChannel: musicErrors.sameChannel 220 | } 221 | } 222 | export const search = { 223 | author: 'Search Results.', 224 | title: (query) => `Here are the search results for your search\n"${query}":`, 225 | other: { 226 | select: 'Select a song...', 227 | expires: 'This embed expires after one minute.' 228 | }, 229 | errors: { 230 | generic: 'There was an error while adding your song to the queue.', 231 | noVoiceChannel: musicErrors.noVoiceChannel, 232 | sameChannel: musicErrors.sameChannel, 233 | missingPerms: musicErrors.missingPerms 234 | } 235 | } 236 | export const seek = { 237 | other: { response: (time) => `Skipped to ${time}.` }, 238 | errors: { 239 | isLive: 'You can\'t seek in a livestream!', 240 | index: (time) => `You can only seek between 0:00-${time}!`, 241 | nothingPlaying: musicErrors.nothingPlaying, 242 | sameChannel: musicErrors.sameChannel 243 | } 244 | } 245 | export const shuffle = { 246 | other: { response: 'Shuffled the queue.' }, 247 | errors: { 248 | nothingPlaying: musicErrors.nothingPlaying, 249 | sameChannel: musicErrors.sameChannel 250 | } 251 | } 252 | export const skip = { 253 | other: { 254 | skipped: 'Skipped.', 255 | skippedTo: (track) => `Skipped to ${track}.` 256 | }, 257 | errors: { 258 | index: (index) => `You can only specify a song number between 1-${index}.`, 259 | nothingPlaying: musicErrors.nothingPlaying, 260 | sameChannel: musicErrors.sameChannel 261 | } 262 | } 263 | export const stop = { 264 | other: { response: 'Stopped.' }, 265 | errors: { 266 | nothingPlaying: musicErrors.nothingPlaying, 267 | sameChannel: musicErrors.sameChannel 268 | } 269 | } 270 | export const volume = { 271 | other: { response: (volume) => `Set volume to ${volume}%.` }, 272 | errors: { 273 | nothingPlaying: musicErrors.nothingPlaying, 274 | sameChannel: musicErrors.sameChannel 275 | } 276 | } 277 | 278 | // Moderation 279 | const moderationErrors = { 280 | userMissingPerms: 'You do not have permission to execute this command!', 281 | invalidUser: 'You can only specify a valid user!' 282 | } 283 | export const ban = { 284 | author: 'Banned User.', 285 | description: (reason) => `Reason: \`\`\`${reason}\`\`\``, 286 | errors: { 287 | userMissingPerms: moderationErrors.userMissingPerms, 288 | invalidUser: moderationErrors.invalidUser, 289 | missingPerms: 'The bot is missing permissions to ban that user!', 290 | generic: 'There was an error when banning this user.' 291 | } 292 | } 293 | export const kick = { 294 | author: 'Kicked User.', 295 | description: (reason) => `Reason: \`\`\`${reason}\`\`\``, 296 | errors: { 297 | userMissingPerms: moderationErrors.userMissingPerms, 298 | invalidUser: moderationErrors.invalidUser, 299 | missingPerms: 'The bot is missing permissions to kick that user!', 300 | generic: 'There was an error when kicking this user.' 301 | } 302 | } 303 | export const move = { 304 | author: 'Moved User.', 305 | description: (username, channel) => `Moved \`${username}\` to \`${channel}\`.`, 306 | errors: { 307 | userMissingPerms: moderationErrors.userMissingPerms, 308 | invalidUser: moderationErrors.invalidUser, 309 | missingPerms: 'The bot is missing permissions to move that user!' 310 | } 311 | } 312 | export const moveall = { 313 | author: 'Moved All Users.', 314 | description: (channel1, channel2) => `Moved all users from \`${channel1}\` to \`${channel2}\`.`, 315 | errors: { 316 | userMissingPerms: moderationErrors.userMissingPerms, 317 | missingPerms: 'The bot is missing permissions to move users!' 318 | } 319 | } 320 | export const purge = { 321 | other: { response: (amount) => `Deleted ${amount} message(s).` }, 322 | errors: { 323 | userMissingPerms: moderationErrors.userMissingPerms, 324 | missingPerms: 'The bot is missing permissions to delete messages!', 325 | index: 'You can only delete between 1-100 messages!' 326 | } 327 | } 328 | export const slowmode = { 329 | author: 'Set Slowmode.', 330 | description: (channel, seconds) => `Set the rate limit of #${channel} to ${seconds}s.`, 331 | errors: { 332 | userMissingPerms: moderationErrors.userMissingPerms, 333 | missingPerms: 'The bot is missing permissions to manage channels!' 334 | } 335 | } 336 | 337 | // Feedback 338 | export const bugreport = { other: { response: 'Your bug report was sent successfully!' } } 339 | export const github = { 340 | author: 'GitHub.', 341 | title: 'GitHub Repository', 342 | description: 'The source code for this bot along with helpful information.' 343 | } 344 | export const suggestion = { other: { response: 'Your suggestion was sent successfully!' } } 345 | 346 | // Other 347 | export const serverShutdown = { 348 | title: 'Server shutdown.', 349 | description: 'The server the bot is hosted on has been forced to shut down.\nThe bot should be up and running again in a few minutes.' 350 | } 351 | -------------------------------------------------------------------------------- /language/lang/fi.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnusedGlobalSymbols 2 | 3 | // General 4 | export const activity = { 5 | author: 'Aktiviteetti.', 6 | title: 'Klikkaa tästä avataksesi aktiviteetin', 7 | errors: { 8 | generic: 'Virhe tapahtui luodessa aktiviteettiasi!', 9 | voiceChannel: 'Voit valita ainoastaan puhe-kanavan!', 10 | missingPerms: 'Botilla ei ole tarpeeksi oikeuksia suorittaa tuota.' 11 | } 12 | } 13 | export const dashboard = { 14 | author: 'Paneeli.', 15 | title: 'SuitBot paneeli', 16 | description: 'Boatin paneeli nettisivu.' 17 | } 18 | export const help = { 19 | author: 'Apua.', 20 | title: 'SuitBot Apu Sivu', 21 | description: 'Tämä moduuli listaa kaikki SuitBotin komennot.\n\nKäyttääksesi komentoa, aloita kirjoitamalla `/` sitten kirjoita haluamasi komento perään. Voit myös käyttää Discordin automaattista Slash Command sivua.\n\n', 22 | fields: { 23 | invite: { name: 'Kutsu', value: 'Klikkaa tästä kutsuaksesi' }, 24 | website: { name: 'Nettisivu', value: null }, 25 | github: { name: 'Lähdekoodi', value: 'GitHub' }, 26 | discord: { name: 'Discord', value: 'Kutsu' }, 27 | buttons: { name: null, value: 'Klikkaa nappeja alhaalla vaihtaaksesi sivua haluamaasi.' } 28 | }, 29 | other: { 30 | page: 'Sivu', 31 | previous: 'Edellinen', 32 | next: 'Seuraava' 33 | } 34 | } 35 | export const info = { 36 | author: 'Info.', 37 | title: 'Botista Tietoa', 38 | fields: { 39 | servers: { name: 'Palvelimet', value: null }, 40 | uptime: { name: 'Käynnissäoloaika', value: null }, 41 | memoryUsage: { name: 'Muistin Käyttö', value: null } 42 | } 43 | } 44 | export const invite = { 45 | author: 'Kutsu.', 46 | title: 'Kutsu SuitBot', 47 | description: 'Klikkaa tästä linkistä kutsuaksesi SuitBotin palvelimellesi!' 48 | } 49 | export const language = { 50 | author: 'Kieli.', 51 | title: 'Vaihda kieltä', 52 | description: (langCode) => `Kieli asetettiin \`${langCode}\`.`, 53 | errors: { userMissingPerms: 'Sinulla ei ole oikeutta suorittaa tätä komentoa!' } 54 | } 55 | export const ping = { 56 | author: 'Viive.', 57 | title: 'Botin & APIn Viive' 58 | } 59 | export const serverinfo = { 60 | author: 'Palvelimen Tiedot.', 61 | fields: { 62 | members: { name: 'Jäsenet', value: null }, 63 | channels: { name: 'Kanavat', value: null }, 64 | boosts: { name: 'Nostatukset', value: null }, 65 | owner: { name: 'Omistaja', value: null }, 66 | guildId: { name: 'Palvelimen ID', value: null }, 67 | created: { name: 'Luotu', value: null } 68 | } 69 | } 70 | export const userinfo = { 71 | author: 'Käyttäjän Tiedot.', 72 | fields: { 73 | fullName: { name: 'Koko Nimi', value: null }, 74 | nickname: { name: 'Lempinimi', value: null }, 75 | bot: { name: 'Botti', value: null }, 76 | id: { name: 'ID', value: null }, 77 | profile: { name: 'Profiili', value: null }, 78 | avatarURL: { name: 'Avatarin Osoite', value: 'Avatar URL' }, 79 | created: { name: 'Luotu', value: null }, 80 | joined: { name: 'Liittynyt', value: null } 81 | }, 82 | other: { 83 | online: 'Paikalla', 84 | idle: 'Muualla', 85 | dnd: 'Älä häiritse', 86 | offline: 'Poissa' 87 | }, 88 | errors: { invalidUser: 'Voit valita vain oikean käyttäjän!' } 89 | } 90 | 91 | // Music 92 | const musicErrors = { 93 | nothingPlaying: 'Mikään ei soi tällä hetkellä.\nKäynnistä musiikki komennolla `/play`!', 94 | sameChannel: 'Sinun pitää olla samassa ääni kanavassa kuin bottikin!', 95 | noVoiceChannel: 'Sinun pitää olla ääni kanavalla käyttääksesi komentoa.', 96 | missingPerms: 'Botilla ei ole tarpeeksi oikeuksia soittaakseen musiikkia kanavallasi!' 97 | } 98 | const repeatModes = { 99 | repeat: 'Toist', 100 | none: 'Ei Mitään', 101 | track: 'Soitto', 102 | queue: 'Jono' 103 | } 104 | export const clear = { 105 | other: { response: 'Tyhjennettiin jono.' }, 106 | errors: { 107 | nothingPlaying: musicErrors.nothingPlaying, 108 | sameChannel: musicErrors.sameChannel 109 | } 110 | } 111 | export const filter = { 112 | other: { response: (filter) => `Asetettiin filtteri ${filter}.` }, 113 | errors: { 114 | nothingPlaying: musicErrors.nothingPlaying, 115 | sameChannel: musicErrors.sameChannel 116 | } 117 | } 118 | export const lyrics = { 119 | author: 'Sanat.', 120 | other: { 121 | repeatModes, 122 | genius: 'Tarjoaa genius.com', 123 | noResults: 'Sanoja ei löytynyt!', 124 | previous: 'Edellinen', 125 | next: 'Seuraava' 126 | }, 127 | errors: { 128 | nothingPlaying: musicErrors.nothingPlaying, 129 | sameChannel: musicErrors.sameChannel 130 | } 131 | } 132 | export const nowplaying = { 133 | author: 'Nyt Soitetaan...', 134 | fields: { 135 | duration: { name: 'Kesto', value: null }, 136 | author: { name: 'Tekijä', value: null }, 137 | requestedBy: { name: 'Pyytäjä', value: null } 138 | }, 139 | other: { repeatModes }, 140 | errors: { 141 | nothingPlaying: musicErrors.nothingPlaying, 142 | sameChannel: musicErrors.sameChannel 143 | } 144 | } 145 | export const pause = { 146 | other: { 147 | paused: 'Pausettu.', 148 | resumed: 'Jatketaan.' 149 | }, 150 | errors: { 151 | nothingPlaying: musicErrors.nothingPlaying, 152 | sameChannel: musicErrors.sameChannel 153 | } 154 | } 155 | export const play = { 156 | author: 'Lisätty jonoon.', 157 | fields: { 158 | amount: { name: 'Määrä', value: (amount) => `${amount} musiikkia` }, 159 | duration: { name: 'Kesto', value: null }, 160 | author: { name: 'Tekijä', value: null }, 161 | position: { name: 'Paikka', value: null } 162 | }, 163 | errors: { 164 | generic: 'Virhe tapahtui lisätessä musiikkiasi jonoon.', 165 | noVoiceChannel: musicErrors.noVoiceChannel, 166 | sameChannel: musicErrors.sameChannel, 167 | missingPerms: musicErrors.missingPerms 168 | } 169 | } 170 | export const previous = { 171 | other: { response: (track) => `Soitetaan edellinen musiikki ${track}.` }, 172 | errors: { 173 | generic: 'Et voi käyttää komentoa `/previous` juuri nyt!', 174 | nothingPlaying: musicErrors.nothingPlaying, 175 | sameChannel: musicErrors.sameChannel 176 | } 177 | } 178 | export const queue = { 179 | author: 'Jono.', 180 | other: { 181 | dashboard: (url) => `Vielä käytät vabnhoja komentoja? Kokeile uutta [paneelia](${url}) sen sijaan!`, 182 | nowPlaying: 'Nyt Soitetaan:', 183 | noUpcomingSongs: 'Ei uusia musiikkeja tulossa.\nLisää musiikkeja `/play` komennolla!\n', 184 | songsInQueue: (amount) => `${amount} musiikkia jonossa`, 185 | totalDuration: (duration) => `${duration} jonon koko kesto`, 186 | page: 'Sivu', 187 | previous: 'Edellinen', 188 | next: 'Seuraava', 189 | repeatModes 190 | }, 191 | errors: { 192 | nothingPlaying: musicErrors.nothingPlaying, 193 | sameChannel: musicErrors.sameChannel 194 | } 195 | } 196 | export const remove = { 197 | other: { response: (track) => `Poistettiin musiikki ${track}.` }, 198 | errors: { 199 | index: (index) => `Voit valita musiikin 1-${index} väliltä.`, 200 | nothingPlaying: musicErrors.nothingPlaying, 201 | sameChannel: musicErrors.sameChannel 202 | } 203 | } 204 | export const repeat = { 205 | other: { 206 | response: (mode) => `Asetettiin toisto moodi ${mode}.`, 207 | repeatModes 208 | }, 209 | errors: { 210 | nothingPlaying: musicErrors.nothingPlaying, 211 | sameChannel: musicErrors.sameChannel 212 | } 213 | } 214 | export const resume = { 215 | other: { response: 'Jatketaan.' }, 216 | errors: { 217 | notPaused: 'Jono ei ole pysäytetty!', 218 | nothingPlaying: musicErrors.nothingPlaying, 219 | sameChannel: musicErrors.sameChannel 220 | } 221 | } 222 | export const search = { 223 | author: 'Haun Tulokset.', 224 | title: (query) => `Tässä on tulokset haullesi:\n"${query}":`, 225 | other: { 226 | select: 'Valitse musiikki...', 227 | expires: 'Tämä valikko vanhenee 1-minuutin kuluttua.' 228 | }, 229 | errors: { 230 | generic: 'Virhe tapahtui lisätessä musiikkiasi jonoon.', 231 | noVoiceChannel: musicErrors.noVoiceChannel, 232 | sameChannel: musicErrors.sameChannel, 233 | missingPerms: musicErrors.missingPerms 234 | } 235 | } 236 | export const seek = { 237 | other: { response: (time) => `Skipattiin ${time}.` }, 238 | errors: { 239 | isLive: 'Et voi skipata live-striimissä!', 240 | index: (time) => `Voi skipata vain 0:00-${time} väliltä!`, 241 | nothingPlaying: musicErrors.nothingPlaying, 242 | sameChannel: musicErrors.sameChannel 243 | } 244 | } 245 | export const shuffle = { 246 | other: { response: 'Sekoitetaan jono.' }, 247 | errors: { 248 | nothingPlaying: musicErrors.nothingPlaying, 249 | sameChannel: musicErrors.sameChannel 250 | } 251 | } 252 | export const skip = { 253 | other: { 254 | skipped: 'Skipattu.', 255 | skippedTo: (track) => `Skipattiin ${track}.` 256 | }, 257 | errors: { 258 | index: (index) => `Voit valita musiikin 1-${index} väliltä.`, 259 | nothingPlaying: musicErrors.nothingPlaying, 260 | sameChannel: musicErrors.sameChannel 261 | } 262 | } 263 | export const stop = { 264 | other: { response: 'Pysäytetty.' }, 265 | errors: { 266 | nothingPlaying: musicErrors.nothingPlaying, 267 | sameChannel: musicErrors.sameChannel 268 | } 269 | } 270 | export const volume = { 271 | other: { response: (volume) => `Asetettiin voluumi ${volume}%.` }, 272 | errors: { 273 | nothingPlaying: musicErrors.nothingPlaying, 274 | sameChannel: musicErrors.sameChannel 275 | } 276 | } 277 | 278 | // Moderation 279 | const moderationErrors = { 280 | userMissingPerms: 'Sinulla ei ole oikeutta suorittaa tätä komentoa!', 281 | invalidUser: 'Voi valita vain oikean käyttäjän!' 282 | } 283 | export const ban = { 284 | author: 'Annettiin porttikielto käyttäjälle.', 285 | description: (reason) => `Syy: \`\`\`${reason}\`\`\``, 286 | errors: { 287 | userMissingPerms: moderationErrors.userMissingPerms, 288 | invalidUser: moderationErrors.invalidUser, 289 | missingPerms: 'Botilla ei ole oikeuksia antaa porttikieltoa kyseiselle käyttäjälle!', 290 | generic: 'Virhe tapahtui antaessa porttikieltoa käyttäjälle.' 291 | } 292 | } 293 | export const kick = { 294 | author: 'Potkittiin käyttäjä.', 295 | description: (reason) => `Syy: \`\`\`${reason}\`\`\``, 296 | errors: { 297 | userMissingPerms: moderationErrors.userMissingPerms, 298 | invalidUser: moderationErrors.invalidUser, 299 | missingPerms: 'Botilla ei ole oikeuksia antaa potkuja kyseiselle käyttäjälle!', 300 | generic: 'Virhe tapahtui antaessa potkuja käyttäjälle.' 301 | } 302 | } 303 | export const move = { 304 | author: 'Siirrettiin Käyttäjä.', 305 | description: (username, channel) => `Siirrettiin \`${username}\` kanavalle \`${channel}\`.`, 306 | errors: { 307 | userMissingPerms: moderationErrors.userMissingPerms, 308 | invalidUser: moderationErrors.invalidUser, 309 | missingPerms: 'Botilla ei ole oikeuksia siirtää kyseistä käyttäjää!' 310 | } 311 | } 312 | export const moveall = { 313 | author: 'Siirrettiin kaikki käyttäjät.', 314 | description: (channel1, channel2) => `Siirrettiin kaikki käyttäjät \`${channel1}\` kanavalle \`${channel2}\`.`, 315 | errors: { 316 | userMissingPerms: moderationErrors.userMissingPerms, 317 | missingPerms: 'Botilla ei ole oikeuksia siirtää käyttäjiä!' 318 | } 319 | } 320 | export const purge = { 321 | other: { response: (amount) => `Poistettiin ${amount} viesti(ä).` }, 322 | errors: { 323 | userMissingPerms: moderationErrors.userMissingPerms, 324 | missingPerms: 'Botilla ei ole oikeuksia poistaa viestejä!', 325 | index: 'Voit poistaa vain 1-100 viestiä!' 326 | } 327 | } 328 | export const slowmode = { 329 | author: 'Aseta hitaustila.', 330 | description: (channel, seconds) => `Asetettiin kanavan #${channel} ratelimitti ${seconds}sekunttiin.`, 331 | errors: { 332 | userMissingPerms: moderationErrors.userMissingPerms, 333 | missingPerms: 'Botilla ei ole oikeuksia hallita kanavia!' 334 | } 335 | } 336 | 337 | // Feedback 338 | export const bugreport = { other: { response: 'Bug-reporttisi on lähetetty!' } } 339 | export const github = { 340 | author: 'GitHub.', 341 | title: 'GitHub Repositorio', 342 | description: 'Botin lähde koodi tiedon kanssa.' 343 | } 344 | export const suggestion = { other: { response: 'Sinun ehdotuksesi on lähetetty!' } } 345 | 346 | // Other 347 | export const serverShutdown = { 348 | title: 'Palvelimen sammutus.', 349 | description: 'Palvelin jolla botti hostataan on sammunut.\nBotin pitäisi olla pian käynnissä taas.' 350 | } 351 | -------------------------------------------------------------------------------- /language/lang/ja.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnusedGlobalSymbols 2 | 3 | // General 4 | export const activity = { 5 | author: 'アクティビティー', 6 | title: 'ここからアクティビティーを開始できます。', 7 | errors: { 8 | generic: 'アクティビティー開始中にエラーが発生しました!', 9 | voiceChannel: 'ボイスチャンネルを指定してください!', 10 | missingPerms: 'ボットの権限が足りません!' 11 | } 12 | } 13 | export const dashboard = { 14 | author: 'ダッシュボード', 15 | title: 'SuitBotダッシュボード', 16 | description: 'ボットのダッシュボードウエブサイト' 17 | } 18 | export const help = { 19 | author: 'ヘルプ', 20 | title: 'SuitBotヘルプページ', 21 | description: 'こちらからSuitBotのコマンド一覧が見れます。\n\nコマンドを利用するには `/` を入力して、出てきたコマンドを使ってください。\n\n', 22 | fields: { 23 | invite: { name: '招待', value: 'こちらからSuitBotを招待できます。' }, 24 | website: { name: 'サイト', value: null }, 25 | github: { name: 'ソースコード', value: 'GitHub' }, 26 | discord: { name: 'Discord', value: 'サポートサーバー' }, 27 | buttons: { name: null, value: '下のボタンでページ変更ができます。' } 28 | }, 29 | other: { 30 | page: 'ページ', 31 | previous: '前', 32 | next: '次' 33 | } 34 | } 35 | export const info = { 36 | author: '詳細情報', 37 | title: 'ボットの詳細情報', 38 | fields: { 39 | servers: { name: '参加サーバー数', value: null }, 40 | uptime: { name: 'アップタイム', value: null }, 41 | memoryUsage: { name: 'メモリー使用率', value: null } 42 | } 43 | } 44 | export const invite = { 45 | author: '招待', 46 | title: 'SuitBotを招待する', 47 | description: 'こちらからSuitBotをサーバーに招待できます!' 48 | } 49 | export const language = { 50 | author: '言語', 51 | title: 'ボット言語変更', 52 | description: (langCode) => `ボット言語を \`${langCode}\` に設定しました。`, 53 | errors: { userMissingPerms: 'このコマンドを利用する権限がありません!' } 54 | } 55 | export const ping = { 56 | author: 'Ping', 57 | title: 'ボットとAPIの応答速度' 58 | } 59 | export const serverinfo = { 60 | author: 'サーバー情報', 61 | fields: { 62 | members: { name: 'メンバー数', value: null }, 63 | channels: { name: 'チャンネル数', value: null }, 64 | boosts: { name: 'サーバーブースト数', value: null }, 65 | owner: { name: 'オーナー', value: null }, 66 | guildId: { name: 'サーバーID', value: null }, 67 | created: { name: '作成時', value: null } 68 | } 69 | } 70 | export const userinfo = { 71 | author: 'ユーザー情報', 72 | fields: { 73 | fullName: { name: 'ユーザー名', value: null }, 74 | nickname: { name: 'ニックネーム', value: null }, 75 | bot: { name: 'Bot', value: null }, 76 | id: { name: 'ユーザーID', value: null }, 77 | profile: { name: 'メンション', value: null }, 78 | avatarURL: { name: 'アバター画像URL', value: 'Avatar URL' }, 79 | created: { name: 'アカウント作成時', value: null }, 80 | joined: { name: 'サーバー参加時', value: null } 81 | }, 82 | other: { 83 | online: 'オンライン', 84 | idle: '退席', 85 | dnd: '取り込み中', 86 | offline: 'オフライン' 87 | }, 88 | errors: { invalidUser: '無効なユーザーが指定されました!' } 89 | } 90 | 91 | // Music 92 | const musicErrors = { 93 | nothingPlaying: '現在何も再生されていません。\n`/play` で再生を開始しよう!', 94 | sameChannel: 'ボットと同じVCに参加してください!', 95 | noVoiceChannel: 'VCに参加してください!', 96 | missingPerms: 'ボットに発言権限がありません!' 97 | } 98 | const repeatModes = { 99 | repeat: 'ループ', 100 | none: 'なし', 101 | track: '一曲ループ', 102 | queue: '再生待ちを全てループ' 103 | } 104 | export const clear = { 105 | other: { response: '再生待ちを削除しました。' }, 106 | errors: { 107 | nothingPlaying: musicErrors.nothingPlaying, 108 | sameChannel: musicErrors.sameChannel 109 | } 110 | } 111 | export const filter = { 112 | other: { response: (filter) => `オーディオフィルタを${filter}に設定しました。` }, 113 | errors: { 114 | nothingPlaying: musicErrors.nothingPlaying, 115 | sameChannel: musicErrors.sameChannel 116 | } 117 | } 118 | export const lyrics = { 119 | author: '歌詞', 120 | other: { 121 | repeatModes, 122 | genius: 'genius.comより提供', 123 | noResults: '歌詞が見つかりません!', 124 | previous: '前', 125 | next: '次' 126 | }, 127 | errors: { 128 | nothingPlaying: musicErrors.nothingPlaying, 129 | sameChannel: musicErrors.sameChannel 130 | } 131 | } 132 | export const nowplaying = { 133 | author: '現在再生中', 134 | fields: { 135 | duration: { name: '継続時間', value: null }, 136 | author: { name: '作成者', value: null }, 137 | requestedBy: { name: 'リクエストしたユーザー', value: null } 138 | }, 139 | other: { repeatModes }, 140 | errors: { 141 | nothingPlaying: musicErrors.nothingPlaying, 142 | sameChannel: musicErrors.sameChannel 143 | } 144 | } 145 | export const pause = { 146 | other: { 147 | paused: '再生を一時停止しました。', 148 | resumed: '再生を再開しました。' 149 | }, 150 | errors: { 151 | nothingPlaying: musicErrors.nothingPlaying, 152 | sameChannel: musicErrors.sameChannel 153 | } 154 | } 155 | export const play = { 156 | author: '再生待ちに追加しました。', 157 | fields: { 158 | amount: { name: '曲数', value: (amount) => `${amount}曲` }, 159 | duration: { name: '継続時間', value: null }, 160 | author: { name: '作成者', value: null }, 161 | position: { name: '再生待ちの順番', value: null } 162 | }, 163 | errors: { 164 | generic: '再生待ち追加時にエラーが発生しました。', 165 | noVoiceChannel: musicErrors.noVoiceChannel, 166 | sameChannel: musicErrors.sameChannel, 167 | missingPerms: musicErrors.missingPerms 168 | } 169 | } 170 | export const previous = { 171 | other: { response: (track) => `前の曲${track}に飛びました。` }, 172 | errors: { 173 | generic: '現在 `/previous` は利用できません!', 174 | nothingPlaying: musicErrors.nothingPlaying, 175 | sameChannel: musicErrors.sameChannel 176 | } 177 | } 178 | export const queue = { 179 | author: '再生待ちリスト', 180 | other: { 181 | dashboard: (url) => `まだコマンドなんか使ってるの?便利な[Webダッシュボード](${url})を使ってみよう!`, 182 | nowPlaying: '現在再生中', 183 | noUpcomingSongs: '再生待ちの曲がありません。\n`/play` で曲を追加しよう!\n', 184 | songsInQueue: (amount) => `${amount}曲が再生待ち`, 185 | totalDuration: (duration) => `総継続時間 ${duration}`, 186 | page: 'ページ', 187 | previous: '前', 188 | next: '次', 189 | repeatModes 190 | }, 191 | errors: { 192 | nothingPlaying: musicErrors.nothingPlaying, 193 | sameChannel: musicErrors.sameChannel 194 | } 195 | } 196 | export const remove = { 197 | other: { response: (track) => `${track}を削除しました。` }, 198 | errors: { 199 | index: (index) => `1-${index}の間の数字を指定してください。`, 200 | nothingPlaying: musicErrors.nothingPlaying, 201 | sameChannel: musicErrors.sameChannel 202 | } 203 | } 204 | export const repeat = { 205 | other: { 206 | response: (mode) => `ループモードを${mode}に設定しました。`, 207 | repeatModes 208 | }, 209 | errors: { 210 | nothingPlaying: musicErrors.nothingPlaying, 211 | sameChannel: musicErrors.sameChannel 212 | } 213 | } 214 | export const resume = { 215 | other: { response: '再生を再開しました。' }, 216 | errors: { 217 | notPaused: '再生が一時停止されていません!', 218 | nothingPlaying: musicErrors.nothingPlaying, 219 | sameChannel: musicErrors.sameChannel 220 | } 221 | } 222 | export const search = { 223 | author: '検索結果', 224 | title: (query) => `「${query}」の検索結果です。`, 225 | other: { 226 | select: '曲を選択してください。', 227 | expires: '一分以内に選択してください。' 228 | }, 229 | errors: { 230 | generic: '再生待ち追加時にエラーが発生しました。', 231 | noVoiceChannel: musicErrors.noVoiceChannel, 232 | sameChannel: musicErrors.sameChannel, 233 | missingPerms: musicErrors.missingPerms 234 | } 235 | } 236 | export const seek = { 237 | other: { response: (time) => `${time}に動きました。` }, 238 | errors: { 239 | isLive: 'ライブ配信は巻き戻し・早送りできません!', 240 | index: (time) => `0:00-${time} の間の時間を指定してください。`, 241 | nothingPlaying: musicErrors.nothingPlaying, 242 | sameChannel: musicErrors.sameChannel 243 | } 244 | } 245 | export const shuffle = { 246 | other: { response: '再生待ちをシャッフルしました。' }, 247 | errors: { 248 | nothingPlaying: musicErrors.nothingPlaying, 249 | sameChannel: musicErrors.sameChannel 250 | } 251 | } 252 | export const skip = { 253 | other: { 254 | skipped: '曲を飛ばしました。', 255 | skippedTo: (track) => `${track} に飛びました。` 256 | }, 257 | errors: { 258 | index: (index) => `1-${index} の間の数字を指定してください。`, 259 | nothingPlaying: musicErrors.nothingPlaying, 260 | sameChannel: musicErrors.sameChannel 261 | } 262 | } 263 | export const stop = { 264 | other: { response: '再生を停止しました。' }, 265 | errors: { 266 | nothingPlaying: musicErrors.nothingPlaying, 267 | sameChannel: musicErrors.sameChannel 268 | } 269 | } 270 | export const volume = { 271 | other: { response: (volume) => `音量を${volume}%に設定しました。` }, 272 | errors: { 273 | nothingPlaying: musicErrors.nothingPlaying, 274 | sameChannel: musicErrors.sameChannel 275 | } 276 | } 277 | 278 | // Moderation 279 | const moderationErrors = { 280 | userMissingPerms: 'このコマンドを利用する権限がありません!', 281 | invalidUser: '無効なユーザーが指定されました!' 282 | } 283 | export const ban = { 284 | author: 'ユーザーをBAN', 285 | description: (reason) => `理由: \`\`\`${reason}\`\`\``, 286 | errors: { 287 | userMissingPerms: moderationErrors.userMissingPerms, 288 | invalidUser: moderationErrors.invalidUser, 289 | missingPerms: 'ボットにユーザーをBANする権限がありません!', 290 | generic: 'BAN処理時にエラーが発生しました!' 291 | } 292 | } 293 | export const kick = { 294 | author: 'ユーザーを追放', 295 | description: (reason) => `理由: \`\`\`${reason}\`\`\``, 296 | errors: { 297 | userMissingPerms: moderationErrors.userMissingPerms, 298 | invalidUser: moderationErrors.invalidUser, 299 | missingPerms: 'ボットにユーザーを追放する権限がありません!', 300 | generic: '追放処理時にエラーが発生されました!' 301 | } 302 | } 303 | export const move = { 304 | author: 'ユーザー移動', 305 | description: (username, channel) => `\`${username}\` を \`${channel}\` に移動しました。`, 306 | errors: { 307 | userMissingPerms: moderationErrors.userMissingPerms, 308 | invalidUser: moderationErrors.invalidUser, 309 | missingPerms: 'ボットにユーザーを移動する権限がありません!' 310 | } 311 | } 312 | export const moveall = { 313 | author: '全ユーザー移動', 314 | description: (channel1, channel2) => `全ユーザーを \`${channel1}\` から \`${channel2}\` に移動しました。`, 315 | errors: { 316 | userMissingPerms: moderationErrors.userMissingPerms, 317 | missingPerms: 'ボットにユーザーを移動する権限がありません!' 318 | } 319 | } 320 | export const purge = { 321 | other: { response: (amount) => `メッセージ${amount}個削除しました。` }, 322 | errors: { 323 | userMissingPerms: moderationErrors.userMissingPerms, 324 | missingPerms: 'ボットにメッセージを削除する権限がありません!', 325 | index: '1-100の間のメッセージ数を指定してください!' 326 | } 327 | } 328 | export const slowmode = { 329 | author: '低速モード', 330 | description: (channel, seconds) => `#${channel}の低速モードを${seconds}秒に設定しました。`, 331 | errors: { 332 | userMissingPerms: moderationErrors.userMissingPerms, 333 | missingPerms: 'ボットにチャンネルを管理する権限がありません!' 334 | } 335 | } 336 | 337 | // Feedback 338 | export const bugreport = { other: { response: '障害報告が送信されました!' } } 339 | export const github = { 340 | author: 'ソースコード', 341 | title: 'GitHubレポジトリー', 342 | description: 'こちらからSuitBotのソースコードと詳細情報が見れます。' 343 | } 344 | export const suggestion = { other: { response: '機能リクエストが送信されました!' } } 345 | 346 | // Other 347 | export const serverShutdown = { 348 | title: 'サーバー停止', 349 | description: 'ボットのサーバーが何らかの理由で強制停止されました。\n再稼働まで数分お待ちください。' 350 | } 351 | -------------------------------------------------------------------------------- /language/locale.js: -------------------------------------------------------------------------------- 1 | import * as de from './lang/de.js' 2 | import * as enUS from './lang/en-US.js' 3 | import * as fi from './lang/fi.js' 4 | import * as ja from './lang/ja.js' 5 | import * as ptBR from './lang/pt-BR.js' 6 | 7 | // https://discord.com/developers/docs/reference#locales 8 | 9 | const locales = { 10 | 'de': de, 11 | 'en-US': enUS, 12 | 'fi': fi, 13 | 'ja': ja, 14 | 'pt-BR': ptBR 15 | } 16 | 17 | export function getLanguage(locale) { 18 | if (!(locale in locales)) { return locales['en-US'] } 19 | return locales[locale] 20 | } 21 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import { ActivityType, Client, Collection, EmbedBuilder, GatewayIntentBits } from 'discord.js' 2 | import database from './utilities/database.js' 3 | import { Lavalink } from './music/lavalink.js' 4 | import { getFilesRecursively } from './utilities/utilities.js' 5 | import fs from 'fs' 6 | 7 | import { adminId, token } from './utilities/config.js' 8 | import { getLanguage } from './language/locale.js' 9 | import { iconURL } from './events/ready.js' 10 | import { logging } from './utilities/logging.js' 11 | 12 | const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates], presence: { status: 'dnd', activities: [{ name: '/help | The future of SuitBot...', type: ActivityType.Playing }] } }) 13 | // client.database = database 14 | // client.lavalink = new Lavalink(client) 15 | // await client.lavalink.initialize() 16 | 17 | // Commands 18 | client.commands = new Collection() 19 | for (const file of getFilesRecursively('./commands')) { 20 | const command = await import(`./${file}`) 21 | client.commands.set(command.data.name, command) 22 | } 23 | 24 | // Events 25 | for (const file of getFilesRecursively('./events')) { 26 | const event = await import(`./${file}`) 27 | if (event.data.once) { 28 | client.once(event.data.name, (...args) => event.execute(...args)) 29 | } else { 30 | client.on(event.data.name, (...args) => event.execute(...args)) 31 | } 32 | } 33 | process.on('SIGTERM', shutdown) 34 | process.on('SIGINT', shutdown) 35 | process.on('uncaughtException', async (error) => { 36 | logging.warn(`Ignoring uncaught exception: ${error} | ${error.stack.split(/\r?\n/)[1].split('\\').pop().slice(0, -1).trim()}`) 37 | if (client.isReady()) { 38 | fs.writeFileSync('error.txt', error.stack) 39 | const user = await client.users.fetch(adminId) 40 | await user.send({ content: `\`New Exception | ${error}\``, files: ['error.txt'] }) 41 | fs.unlink('error.txt', () => {}) 42 | } 43 | }) 44 | 45 | // Shutdown Handling 46 | async function shutdown() { 47 | /* logging.info(`Closing ${client.lavalink.manager.players.size} queues.`) 48 | for (const entry of client.lavalink.manager.players) { 49 | const player = entry[1] 50 | const lang = getLanguage(await client.database.getLocale(player.guild)).serverShutdown 51 | // noinspection JSUnresolvedFunction 52 | await client.channels.cache.get(player.textChannel).send({ 53 | embeds: [ 54 | new EmbedBuilder() 55 | .setTitle(lang.title) 56 | .setDescription(lang.description) 57 | .setFooter({ text: 'SuitBot', iconURL: iconURL }) 58 | .setColor([255, 0, 0]) 59 | ] 60 | }) 61 | player.destroy() 62 | }*/ 63 | client.destroy() 64 | client.dashboard?.shutdown() 65 | logging.info('Received SIGTERM, shutting down.') 66 | process.exit(0) 67 | } 68 | 69 | // Login 70 | await client.login(token) 71 | -------------------------------------------------------------------------------- /music/ExtendedSearch.js: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'erela.js' 2 | import spotifyUrlInfo from 'spotify-url-info' 3 | import fetch from 'isomorphic-unfetch' 4 | import ytpl from 'ytpl' 5 | 6 | const spotify = spotifyUrlInfo(fetch) 7 | 8 | const buildResult = (loadType, tracks, error, playlist) => ({ 9 | loadType, 10 | tracks: tracks ?? [], 11 | playlist: playlist ? { ...playlist, duration: tracks.reduce((acc, cur) => acc + (cur.duration || 0), 0) } : null, 12 | exception: error ? { message: error, severity: 'COMMON' } : null 13 | }) 14 | 15 | export class ExtendedSearch extends Plugin { 16 | constructor() { 17 | super() 18 | } 19 | 20 | load(manager) { 21 | this.manager = manager 22 | this._search = manager.search.bind(manager) 23 | manager.search = this.search.bind(this) 24 | } 25 | 26 | async search(query, requestedBy) { 27 | query = query.query ?? query 28 | 29 | // Split off query parameters 30 | if (query.startsWith('https://')) { query = query.split('&')[0] } 31 | 32 | // YouTube Shorts 33 | const shortsRegex = /https:\/\/(www\.)?youtube\.com\/shorts\/(.*)$/ 34 | if (query.match(shortsRegex)) { query = query.replace('shorts/', 'watch?v=') } 35 | 36 | // YouTube Playlists 37 | const playlistRegex = /https:\/\/(www\.)?youtube\.com\/playlist(.*)$/ 38 | if (query.match(playlistRegex)) { 39 | try { 40 | const data = ytpl.validateID(query) ? await ytpl(query) : null 41 | const result = await this._search(query, requestedBy) 42 | result.playlist = Object.assign(result.playlist, { name: data.title, author: data.author.name, thumbnail: data.bestThumbnail.url, uri: data.url }) 43 | return result 44 | } catch (e) { 45 | return buildResult('LOAD_FAILED', null, e.message ?? null, null) 46 | } 47 | } 48 | 49 | // Spotify 50 | const spotifyRegex = /(?:https:\/\/open\.spotify\.com\/|spotify:)(?:.+)?(track|playlist|album)[/:]([A-Za-z0-9]+)/ 51 | const type = query.match(spotifyRegex)?.[1] 52 | try { 53 | switch (type) { 54 | case 'track': { 55 | const data = await this.getTrack(query, requestedBy) 56 | return buildResult('TRACK_LOADED', data.tracks, null, null) 57 | } 58 | case 'playlist': { 59 | const data = await this.getPlaylist(query, requestedBy) 60 | return buildResult('PLAYLIST_LOADED', data.tracks, null, data.playlist) 61 | } 62 | case 'album': { 63 | const data = await this.getAlbumTracks(query, requestedBy) 64 | return buildResult('PLAYLIST_LOADED', data.tracks, null, data.playlist) 65 | } 66 | } 67 | } catch (e) { 68 | return buildResult('LOAD_FAILED', null, e.message ?? null, null) 69 | } 70 | 71 | // Use best thumbnail available 72 | const search = await this._search(query, requestedBy) 73 | for (const track of search.tracks) { track.thumbnail = await this.getBestThumbnail(track) } 74 | 75 | return search 76 | } 77 | 78 | async getTrack(query, requestedBy) { 79 | const data = await spotify.getData(query) 80 | // noinspection JSUnresolvedVariable 81 | const track = { 82 | author: data.artists[0].name, 83 | duration: data.duration_ms, 84 | thumbnail: data.coverArt?.sources[0]?.url, 85 | title: data.artists[0].name + ' - ' + data.name, 86 | uri: data.external_urls.spotify 87 | } 88 | return { tracks: [ await this.findClosestTrack(track, requestedBy) ] } 89 | } 90 | 91 | async getPlaylist(query, requestedBy) { 92 | const data = await spotify.getData(query) 93 | // noinspection JSUnresolvedVariable 94 | const tracks = await Promise.all(data.tracks.items.map( 95 | async (playlistTrack) => await this.findClosestTrack({ 96 | author: playlistTrack.track.artists[0].name, 97 | duration: playlistTrack.track.duration_ms, 98 | thumbnail: playlistTrack.track.album?.images[0]?.url, 99 | title: playlistTrack.track.artists[0].name + ' - ' + playlistTrack.track.name, 100 | uri: playlistTrack.track.external_urls.spotify 101 | }, requestedBy)) 102 | ) 103 | // noinspection JSUnresolvedVariable 104 | return { tracks, playlist: { name: data.name, author: data.owner.display_name, thumbnail: data.images[0]?.url, uri: data.external_urls.spotify } } 105 | } 106 | 107 | async getAlbumTracks(query, requestedBy) { 108 | const data = await spotify.getData(query) 109 | // noinspection JSUnresolvedVariable 110 | const tracks = await Promise.all(data.tracks.items.map( 111 | async (track) => await this.findClosestTrack({ 112 | author: track.artists[0].name, 113 | duration: track.duration_ms, 114 | thumbnail: data.images[0]?.url, 115 | title: track.artists[0].name + ' - ' + track.name, 116 | uri: track.external_urls.spotify 117 | }, requestedBy)) 118 | ) 119 | // noinspection JSUnresolvedVariable 120 | return { tracks, playlist: { name: data.name, author: data.artists[0].name, thumbnail: data.images[0]?.url, uri: data.external_urls.spotify } } 121 | } 122 | 123 | async findClosestTrack(data, requestedBy, retries = 5) { 124 | if (retries <= 0) { return } 125 | const tracks = (await this.manager.search(data.title, requestedBy)).tracks.slice(0, 5) 126 | const track = 127 | tracks.find((track) => track.title.toLowerCase().includes('official audio')) ?? 128 | tracks.find((track) => track.duration >= data.duration - 1500 && track.duration <= data.duration + 1500) ?? 129 | tracks.find((track) => track.author.endsWith('- Topic') || track.author === data.author) ?? 130 | tracks[0] 131 | if (!track) { return await this.findClosestTrack(data, requestedBy, retries - 1) } 132 | const { author, title, thumbnail, uri } = data 133 | Object.assign(track, { author, title, thumbnail, uri }) 134 | return track 135 | } 136 | 137 | async getBestThumbnail(track) { 138 | for (const size of ['maxresdefault', 'hqdefault', 'mqdefault', 'default']) { 139 | const thumbnail = track.displayThumbnail(size) 140 | if (!thumbnail) { continue } 141 | if ((await fetch(thumbnail)).ok) { return thumbnail } 142 | } 143 | return track.thumbnail 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /music/FilterManager.js: -------------------------------------------------------------------------------- 1 | // noinspection JSCheckFunctionSignatures,JSUnresolvedVariable,JSUnusedGlobalSymbols 2 | 3 | import { Plugin, Structure } from 'erela.js' 4 | 5 | export class FilterManager extends Plugin { 6 | load() { 7 | Structure.extend('Player', (Player) => class extends Player { 8 | constructor() { 9 | super(...arguments) 10 | this.filter = 'none' 11 | this.filters = { 12 | 'none': { 13 | op: 'filters', 14 | guildId: this.guild 15 | }, 16 | 'bassboost': { 17 | op: 'filters', 18 | guildId: this.guild, 19 | equalizer: [ 20 | { band: 0, gain: 0.6 }, 21 | { band: 1, gain: 0.7 }, 22 | { band: 2, gain: 0.8 }, 23 | { band: 3, gain: 0.55 }, 24 | { band: 4, gain: 0.25 }, 25 | { band: 5, gain: 0 }, 26 | { band: 6, gain: -0.25 }, 27 | { band: 7, gain: -0.45 }, 28 | { band: 8, gain: -0.55 }, 29 | { band: 9, gain: -0.7 }, 30 | { band: 10, gain: -0.3 }, 31 | { band: 11, gain: -0.25 }, 32 | { band: 12, gain: 0 }, 33 | { band: 13, gain: 0 }, 34 | { band: 14, gain: 0 } 35 | ] 36 | }, 37 | 'classic': { 38 | op: 'filters', 39 | guildId: this.guild, 40 | equalizer: [ 41 | { band: 0, gain: 0.375 }, 42 | { band: 1, gain: 0.350 }, 43 | { band: 2, gain: 0.125 }, 44 | { band: 3, gain: 0 }, 45 | { band: 4, gain: 0 }, 46 | { band: 5, gain: 0.125 }, 47 | { band: 6, gain: 0.550 }, 48 | { band: 7, gain: 0.050 }, 49 | { band: 8, gain: 0.125 }, 50 | { band: 9, gain: 0.250 }, 51 | { band: 10, gain: 0.200 }, 52 | { band: 11, gain: 0.250 }, 53 | { band: 12, gain: 0.300 }, 54 | { band: 13, gain: 0.250 }, 55 | { band: 14, gain: 0.300 } 56 | ] 57 | }, 58 | 'eightd': { 59 | op: 'filters', 60 | guildId: this.guild, 61 | equalizer: [], 62 | rotation: { rotationHz: 0.2 } 63 | }, 64 | 'earrape': { 65 | op: 'filters', 66 | guildId: this.guild, 67 | equalizer: [ 68 | { band: 0, gain: 0.6 }, 69 | { band: 1, gain: 0.67 }, 70 | { band: 2, gain: 0.67 }, 71 | { band: 3, gain: 0 }, 72 | { band: 4, gain: -0.5 }, 73 | { band: 5, gain: 0.15 }, 74 | { band: 6, gain: -0.45 }, 75 | { band: 7, gain: 0.23 }, 76 | { band: 8, gain: 0.35 }, 77 | { band: 9, gain: 0.45 }, 78 | { band: 10, gain: 0.55 }, 79 | { band: 11, gain: 0.6 }, 80 | { band: 12, gain: 0.55 }, 81 | { band: 13, gain: 0 } 82 | ] 83 | }, 84 | 'karaoke': { 85 | op: 'filters', 86 | guildId: this.guild, 87 | equalizer: [], 88 | karaoke: { 89 | level: 1.0, 90 | monoLevel: 1.0, 91 | filterBand: 220.0, 92 | filterWidth: 100.0 93 | } 94 | }, 95 | 'nightcore': { 96 | op: 'filters', 97 | guildId: this.guild, 98 | equalizer: [], 99 | timescale: { 100 | speed: 1.3, 101 | pitch: 1.3 102 | } 103 | }, 104 | 'superfast': { 105 | op: 'filters', 106 | guildId: this.guild, 107 | equalizer: [], 108 | timescale: { 109 | speed: 1.3, 110 | pitch: 1.0 111 | } 112 | }, 113 | 'vaporwave': { 114 | op: 'filters', 115 | guildId: this.guild, 116 | timescale: { 117 | speed: 0.85, 118 | pitch: 0.90 119 | } 120 | } 121 | } 122 | } 123 | 124 | setFilter(filter) { 125 | if (!this.filters[filter]) { 126 | this.filter = 'none' 127 | return this.node.send(this.filters.noFilter) 128 | } 129 | this.filter = filter 130 | this.node.send(this.filters[filter]) 131 | } 132 | 133 | get timescale() { 134 | return this.filters[this.filter].timescale?.speed ?? 1.0 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /music/lavalink.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnresolvedVariable 2 | 3 | import { spawn } from 'child_process' 4 | import { Manager } from 'erela.js' 5 | import { ExtendedSearch } from './ExtendedSearch.js' 6 | import { simpleEmbed } from '../utilities/utilities.js' 7 | import { FilterManager } from './FilterManager.js' 8 | import yaml from 'js-yaml' 9 | import fs from 'fs' 10 | import { papisid, psid } from '../utilities/config.js' 11 | import http from 'http' 12 | import { logging } from '../utilities/logging.js' 13 | 14 | export class Lavalink { 15 | constructor(client) { 16 | this.client = client 17 | this.manager = new Manager({ 18 | nodes: [ 19 | { 20 | host: 'localhost', 21 | port: 2333, 22 | password: 'youshallnotpass' 23 | } 24 | ], 25 | plugins: [ 26 | new ExtendedSearch(), 27 | new FilterManager() 28 | ], 29 | send(id, payload) { 30 | // noinspection JSIgnoredPromiseFromCall 31 | client.guilds.cache.get(id)?.shard.send(payload) 32 | } 33 | }) 34 | .on('nodeConnect', (node) => { logging.info(`Node ${node.options.identifier} connected`) }) 35 | .on('nodeError', (node, error) => { logging.error(`Node ${node.options.identifier} had an error: ${error.message}`) }) 36 | .on('playerCreate', (player) => { 37 | player.previousTracks = [] 38 | }) 39 | .on('trackStart', (player) => { 40 | this.client.dashboard.update(player) 41 | }) 42 | .on('trackEnd', (player, track) => { 43 | player.previousTracks.push(track) 44 | player.previousTracks = player.previousTracks.slice(-11) 45 | }) 46 | .on('queueEnd', (player) => { 47 | this.client.dashboard.update(player) 48 | setTimeout(() => { if (!player.playing && !player.queue.current) { player.destroy() } }, 30000) 49 | }) 50 | 51 | this.client.once('ready', () => this.manager.init(this.client.user.id)) 52 | this.client.on('raw', (d) => this.manager.updateVoiceState(d)) 53 | this.client.on('voiceStateUpdate', (oldState, newState) => this._voiceUpdate(oldState, newState)) 54 | } 55 | 56 | async initialize() { 57 | const doc = yaml.load(fs.readFileSync('./music/lavalink/template.yml'), {}) 58 | doc.lavalink.server.youtubeConfig.PAPISID = papisid 59 | doc.lavalink.server.youtubeConfig.PSID = psid 60 | fs.writeFileSync('./music/lavalink/application.yml', yaml.dump(doc, {})) 61 | 62 | if (await this._portInUse(doc.server.port)) { 63 | logging.warn(`A server (possibly Lavalink) is already active on port ${doc.server.port}.`) 64 | logging.warn('Continuing, but expect errors if the server already running isn\'t Lavalink.') 65 | return 66 | } 67 | 68 | return new Promise((resolve) => { 69 | const timeout = setTimeout(() => { 70 | logging.error('Failed to start Lavalink within 30s.') 71 | process.exit() 72 | }, 30000) 73 | 74 | const lavalink = spawn('cd ./music/lavalink && java -jar Lavalink.jar', { shell: true }) 75 | const onData = (data) => { 76 | data = data.toString().trim() 77 | if (data.includes('Undertow started')) { 78 | logging.success('Successfully started Lavalink.') 79 | lavalink.stdout.removeListener('data', onData) 80 | clearTimeout(timeout) 81 | resolve() 82 | } else if (data.toLowerCase().includes('failed')) { 83 | logging.error('Failed to start Lavalink.') 84 | lavalink.stdout.removeListener('data', onData) 85 | clearTimeout(timeout) 86 | process.exit() 87 | } 88 | } 89 | lavalink.stdout.on('data', onData) 90 | 91 | process.on('SIGTERM', () => { lavalink.kill() }) 92 | process.on('SIGINT', () => { lavalink.kill() }) 93 | }) 94 | } 95 | 96 | _portInUse(port) { 97 | return new Promise((resolve) => { 98 | const server = http.createServer() 99 | server.listen(port, () => { 100 | server.close() 101 | resolve(false) 102 | }) 103 | server.on('error', () => { resolve(true) }) 104 | }) 105 | } 106 | 107 | _voiceUpdate(oldState, newState) { 108 | const player = this.manager.get(newState.guild.id) 109 | if (!player) { return } 110 | 111 | // Client events 112 | if (newState.guild.members.me.id === newState.member.id) { 113 | // Disconnect 114 | 115 | if (!newState.channelId) { return player.destroy() } 116 | 117 | // Muted 118 | if (oldState.serverMute !== newState.serverMute) { player.pause(newState.serverMute) } 119 | 120 | // Stage Channel 121 | if (newState.channel.type === 'GUILD_STAGE_VOICE') { 122 | // Join 123 | if (!oldState.channel) { 124 | return newState.guild.members.me.voice.setSuppressed(false).catch(async () => { 125 | player.pause(true) 126 | await newState.guild.members.me.voice.setRequestToSpeak(true) 127 | }) 128 | } 129 | // Suppressed 130 | if (oldState.suppress !== newState.suppress) { 131 | return player.pause(newState.suppress) 132 | } 133 | } 134 | return 135 | } 136 | 137 | // Channel empty 138 | if (oldState?.guild.channels.cache.get(player.voiceChannel).members.size === 1) { 139 | oldState?.guild.channels.cache.get(player.textChannel).send(simpleEmbed('Left the voice channel because it was empty.')) 140 | return player.destroy() 141 | } 142 | } 143 | 144 | getPlayer(guildId) { 145 | return this.manager.get(guildId) 146 | } 147 | 148 | createPlayer(interaction) { 149 | return this.manager.create({ guild: interaction.guild.id, voiceChannel: interaction.member.voice.channel?.id, textChannel: interaction.channel.id, volume: 50 }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /music/lavalink/Lavalink.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeridianGH/suitbot/f5d663d8af71c1a1f815334eb8b88df0fc3e2804/music/lavalink/Lavalink.jar -------------------------------------------------------------------------------- /music/lavalink/template.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 2333 3 | address: 0.0.0.0 4 | lavalink: 5 | server: 6 | password: "youshallnotpass" 7 | sources: 8 | youtube: true 9 | bandcamp: true 10 | soundcloud: true 11 | twitch: true 12 | vimeo: true 13 | http: true 14 | local: false 15 | bufferDurationMs: 400 16 | frameBufferDurationMs: 5000 17 | youtubePlaylistLoadLimit: 6 18 | playerUpdateInterval: 5 19 | youtubeSearchEnabled: true 20 | soundcloudSearchEnabled: true 21 | gc-warnings: true 22 | youtubeConfig: 23 | PAPISID: "" 24 | PSID: "" 25 | 26 | metrics: 27 | prometheus: 28 | enabled: false 29 | endpoint: /metrics 30 | 31 | sentry: 32 | dsn: "" 33 | environment: "" 34 | 35 | logging: 36 | file: 37 | max-history: 30 38 | max-size: 1GB 39 | path: ./logs/ 40 | 41 | level: 42 | root: INFO 43 | lavalink: INFO 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suitbot", 3 | "description": "A lightweight music and general purpose bot, that uses slash commands and buttons to be as user-friendly as possible!", 4 | "homepage": "https://suitbot.xyz", 5 | "author": "Meridian", 6 | "license": "Apache-2.0", 7 | "main": "main.js", 8 | "type": "module", 9 | "engines": { 10 | "node": "16.x" 11 | }, 12 | "scripts": { 13 | "start:win": "set NODE_ENV=production && node .", 14 | "start:unix": "NODE_ENV=production node ." 15 | }, 16 | "dependencies": { 17 | "@discordjs/rest": "^1.1.0", 18 | "discord-api-types": "^0.37.10", 19 | "discord.js": "^14.3.0", 20 | "ejs": "^3.0.2", 21 | "erela.js": "github:menudocs/erela.js#build", 22 | "express": "^4.17.1", 23 | "express-minify": "^1.0.0", 24 | "express-session": "^1.17.2", 25 | "genius-lyrics": "^4.4.0", 26 | "isomorphic-unfetch": "^3.1.0", 27 | "js-yaml": "^4.1.0", 28 | "lodash": "^4.17.21", 29 | "marked": "^4.1.0", 30 | "memorystore": "^1.6.6", 31 | "postgres": "^3.2.4", 32 | "spotify-url-info": "^3.1.8", 33 | "websocket": "^1.0.34", 34 | "ytdl-core": "^4.11.2", 35 | "ytpl": "^2.3.0" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^8.23.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /utilities/config.js: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs' 2 | 3 | const file = new URL('../config.json', import.meta.url) 4 | const config = existsSync(file) ? JSON.parse(readFileSync(file)) : {} 5 | 6 | export const { token, appId, clientSecret, guildId, adminId, papisid, psid, geniusClientToken } = config 7 | -------------------------------------------------------------------------------- /utilities/database.js: -------------------------------------------------------------------------------- 1 | import postgres from 'postgres' 2 | 3 | const sql = postgres('postgres://postgres:postgres@localhost:5432/suitbot') 4 | 5 | export default { 6 | sql: sql, 7 | getLocale: async function getLocale(guildId) { 8 | return (await sql`select locale from servers where id = ${guildId};`)[0]?.locale ?? 'en-US' 9 | }, 10 | setLocale: async function setLocale(guildId, locale) { 11 | await sql`update servers set ${sql({ locale: locale })} where id = ${guildId};` 12 | }, 13 | addServer: async function addServer(guild) { 14 | await sql`insert into servers (id, locale) values (${guild.id}, ${guild.preferredLocale});` 15 | }, 16 | addAllServers: async function addAllServers(guilds) { 17 | guilds = guilds.map((guild) => ({ id: guild.id, locale: guild.preferredLocale })) 18 | await sql`insert into servers ${sql(guilds)} on conflict (id) do nothing;` 19 | }, 20 | removeServer: async function removeServer(guild) { 21 | await sql`delete from servers where id = ${guild.id};` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utilities/logging.js: -------------------------------------------------------------------------------- 1 | export class logging { 2 | static error(message) { console.error('%s \x1b[31m%s\x1b[0m', this.time(), message) } 3 | static warn(message) { console.warn('%s \x1b[33m%s\x1b[0m', this.time(), message) } 4 | static info(message) { console.info('%s \x1b[36m%s\x1b[0m', this.time(), message) } 5 | static success(message) { console.log('%s \x1b[32m%s\x1b[0m', this.time(), message) } 6 | 7 | static time() { 8 | const now = new Date() 9 | return `[${now.toLocaleTimeString()}]` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /utilities/utilities.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnusedGlobalSymbols 2 | 3 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js' 4 | import fs from 'fs' 5 | import path from 'path' 6 | import { iconURL } from '../events/ready.js' 7 | import _ from 'lodash' 8 | import { getLanguage } from '../language/locale.js' 9 | import { logging } from './logging.js' 10 | 11 | export function simpleEmbed(content, ephemeral = false) { 12 | return { 13 | embeds: [ 14 | new EmbedBuilder() 15 | .setDescription(content) 16 | .setFooter({ text: 'SuitBot', iconURL: iconURL }) 17 | ], 18 | ephemeral: ephemeral 19 | } 20 | } 21 | 22 | export function errorEmbed(content, ephemeral = false) { 23 | return { 24 | embeds: [ 25 | new EmbedBuilder() 26 | .setDescription(content) 27 | .setFooter({ text: 'SuitBot', iconURL: iconURL }) 28 | .setColor([255, 0, 0]) 29 | ], 30 | ephemeral: ephemeral 31 | } 32 | } 33 | 34 | export function sleep(seconds) { 35 | return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) 36 | } 37 | 38 | export function objectDifference(oldObject, newObject) { 39 | return { 40 | old: _.pickBy(oldObject, (value, key) => !_.isEqual(value, newObject[key])), 41 | new: _.pickBy(newObject, (value, key) => !_.isEqual(oldObject[key], value)) 42 | } 43 | } 44 | 45 | export function msToHMS(ms) { 46 | let totalSeconds = ms / 1000 47 | const hours = Math.floor(totalSeconds / 3600).toString() 48 | totalSeconds %= 3600 49 | const minutes = Math.floor(totalSeconds / 60).toString() 50 | const seconds = Math.floor(totalSeconds % 60).toString() 51 | return hours === '0' ? `${minutes}:${seconds.padStart(2, '0')}` : `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}` 52 | } 53 | 54 | export function timeToMs(time) { 55 | const times = time.split(':') 56 | let seconds = 0; let secondsInUnit = 1 57 | while (times.length > 0) { 58 | seconds += secondsInUnit * parseInt(times.pop()) 59 | secondsInUnit *= 60 60 | } 61 | return seconds * 1000 62 | } 63 | 64 | export function getFilesRecursively(directory, files) { 65 | const contents = fs.readdirSync(directory) 66 | files = files ?? [] 67 | for (const file of contents) { 68 | const absolute = path.join(directory, file) 69 | if (fs.statSync(absolute).isDirectory()) { 70 | getFilesRecursively(absolute, files) 71 | } else { 72 | files.push(absolute) 73 | } 74 | } 75 | return files 76 | } 77 | 78 | export async function addMusicControls(message, player) { 79 | const { previous, pause, skip, stop, dashboard } = getLanguage(await message.client.database.getLocale(message.guildId)) 80 | const previousButton = new ButtonBuilder() 81 | .setCustomId('previous') 82 | .setEmoji('⏮') 83 | .setStyle(ButtonStyle.Secondary) 84 | const pauseButton = new ButtonBuilder() 85 | .setCustomId('pause') 86 | .setEmoji('⏯') 87 | .setStyle(ButtonStyle.Secondary) 88 | const skipButton = new ButtonBuilder() 89 | .setCustomId('skip') 90 | .setEmoji('⏭') 91 | .setStyle(ButtonStyle.Secondary) 92 | const stopButton = new ButtonBuilder() 93 | .setCustomId('stop') 94 | .setEmoji('⏹') 95 | .setStyle(ButtonStyle.Secondary) 96 | const dashboardButton = new ButtonBuilder() 97 | .setURL(`${message.client.dashboard.host}/dashboard/${message.guildId}`) 98 | .setLabel(dashboard.author) 99 | .setStyle(ButtonStyle.Link) 100 | 101 | message.edit({ components: [new ActionRowBuilder().setComponents([previousButton, pauseButton, skipButton, stopButton, dashboardButton])] }) 102 | 103 | const collector = message.createMessageComponentCollector({ idle: 300000 }) 104 | collector.on('collect', async (buttonInteraction) => { 105 | if (buttonInteraction.member.voice.channel?.id !== player.voiceChannel) { return await buttonInteraction.reply(errorEmbed(previous.errors.sameChannel, true)) } 106 | 107 | switch (buttonInteraction.customId) { 108 | case 'previous': { 109 | if (player.position > 5000) { 110 | await player.seek(0) 111 | await buttonInteraction.deferUpdate() 112 | break 113 | } 114 | try { 115 | if (player.previousTracks.length === 0) { return await buttonInteraction.reply(errorEmbed(previous.errors.generic, true)) } 116 | const track = player.previousTracks.pop() 117 | player.queue.add(track, 0) 118 | player.manager.once('trackEnd', (player) => { player.queue.add(player.previousTracks.pop(), 0) }) 119 | player.stop() 120 | await buttonInteraction.reply(simpleEmbed('⏮ ' + previous.other.response(`\`#0\`: **${track.title}**`), true)) 121 | } catch (e) { 122 | await player.seek(0) 123 | await buttonInteraction.deferUpdate() 124 | } 125 | break 126 | } 127 | case 'pause': { 128 | player.pause(!player.paused) 129 | await buttonInteraction.reply(simpleEmbed(player.paused ? '⏸ ' + pause.other.paused : '▶ ' + pause.other.resumed, true)) 130 | break 131 | } 132 | case 'skip': { 133 | if (player.queue.length === 0) { 134 | player.destroy() 135 | await buttonInteraction.reply(simpleEmbed('⏹ ' + stop.other.response, true)) 136 | break 137 | } 138 | player.stop() 139 | await buttonInteraction.reply(simpleEmbed('⏭ ' + skip.other.skipped, true)) 140 | break 141 | } 142 | case 'stop': { 143 | player.destroy() 144 | await buttonInteraction.reply(simpleEmbed('⏹ ' + stop.other.response, true)) 145 | break 146 | } 147 | } 148 | message.client.dashboard.update(player) 149 | }) 150 | collector.on('end', async () => { 151 | const fetchedMessage = await message.fetch(true).catch((e) => { logging.warn(`Failed to edit message components: ${e}`) }) 152 | await fetchedMessage?.edit({ components: [new ActionRowBuilder().setComponents(fetchedMessage.components[0].components.map((component) => ButtonBuilder.from(component.toJSON()).setDisabled(true)))] }) 153 | }) 154 | } 155 | --------------------------------------------------------------------------------