├── .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 | 
2 |
3 | [](https://discord.com/oauth2/authorize?client_id=887122733010411611&scope=bot%20applications.commands&permissions=2167425024)
4 | [](https://discord.gg/qX2CBrrUpf)
5 | [](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 | [](https://suitbot.xyz)
16 | [](https://github.com/MeridianGH/suitbot/projects/1)
17 | [](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 | [](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 | 
202 | 
203 |
204 | ### Code
205 | 
206 | 
207 | \
208 | [](https://www.codefactor.io/repository/github/meridiangh/suitbot)
209 | [](https://libraries.io/github/MeridianGH/suitbot)
210 | \
211 | [](https://www.npmjs.com/package/discord.js)
212 | [](https://www.npmjs.com/package/play-dl)
213 |
214 | ### GitHub
215 | [](https://github.com/MeridianGH/suitbot/issues)
216 | [](https://github.com/MeridianGH/suitbot/pulls)
217 | \
218 | [](https://github.com/MeridianGH/suitbot/commits)
219 | [](https://github.com/MeridianGH/suitbot/graphs/commit-activity)
220 | \
221 | [](https://github.com/MeridianGH/suitbot/stargazers)
222 | [](https://github.com/MeridianGH/suitbot/watchers)
223 |
224 | ### Dashboard
225 | [](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 | [](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 |
75 |
76 | <${Thumbnail} image=${track.thumbnail} />
77 |
80 |
81 |
82 | ${track.title}
83 | ${track.author}
84 | ${track.isStream ? '🔴 Live' : `${msToHMS(position)} / ${msToHMS(track.duration)}`}
85 | <${MusicControls} paused=${paused} repeatMode=${repeatMode} />
86 |
87 |
88 |
89 | <${VolumeControl} volume=${volume} />
90 | `
91 | }
92 |
93 | function Thumbnail({ image }) {
94 | return html`
95 |
96 |
97 |
98 |
99 | `
100 | }
101 |
102 | function MusicControls({ paused, repeatMode }) {
103 | return html`
104 |
105 | { send({ type: 'previous' }) }}>
106 | { send({ type: 'pause' }) }}>
107 | { send({ type: 'skip' }) }}>
108 |
109 | { send({ type: 'shuffle' }) }}>
110 | { send({ type: 'repeat' }) }}>
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 | ${volume}
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 | { send({ type: 'remove', index: i + 1 }) }}> { send({ type: 'skipto', index: i + 1 }) }}>
140 |
141 | `)
142 | }
143 | return html`
144 |
145 |
Queue
146 | <${QueueButtons} />
147 |
148 |
149 |
150 |
151 | #
152 | Track
153 | Author
154 | Duration
155 | Actions
156 |
157 |
158 |
159 | ${rows}
160 |
161 |
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 |
181 | { send({ type: 'filter', filter: event.target.value }) }}>
182 | Select a filter...
183 | None
184 | Bass Boost
185 | Classic
186 | 8D
187 | Earrape
188 | Karaoke
189 | Nightcore
190 | Superfast
191 | Vaporwave
192 |
193 |
194 |
{ send({ type: 'clear' }) }}> Clear queue
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 |
4 4
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 |
17 |
18 |
<%= uptime(djsClient.uptime) %>
19 |
Playing in <%= djsClient.lavalink.manager.players.size %> server(s).
20 |
21 |
22 |
23 |
24 | Icon
25 | Guild
26 | Members
27 | Channels
28 | Joined
29 |
30 |
31 |
32 | <% djsClient.guilds.cache.clone().sort((a, b) => { return a.joinedAt.getTime() - b.joinedAt.getTime() }).forEach(guild => { %>
33 |
34 |
35 | <%= guild.name %>
36 | <%= guild.memberCount %>
37 | <%= guild.channels.channelCountWithoutThreads %>
38 | <%= guild.joinedAt.toUTCString() %>
39 |
40 | <% }) %>
41 |
42 |
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 |
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 |
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 |
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 |
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 |
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 |