diff --git a/dashboard/.env b/dashboard/.env
new file mode 100644
index 0000000..279c513
--- /dev/null
+++ b/dashboard/.env
@@ -0,0 +1,12 @@
+API_URL=localhost:4000/api/v1,
+BOT_ROLE_ID=861875883190255646,
+BOT_TOKEN=ODYxODg2OTEyNjE2OTg4Njcy.YOQUvQ._nTVCqkKoQFkYLLqK9VwWefVGBQ,
+CLIENT_ID=861886912616988672,
+CLIENT_SECRET=861886912616988672,
+DASHBOARD_URL=localhost:3000,
+DEV_ROLE_ID=861876106130489415,
+DOWNTIME_CHANNEL_ID=861879504447995925,
+GUILD_ID=861875493245419521,
+LOG_CHANNEL_ID=861879665232576534,
+MONGO_URI=mongodb+srv://wumpusdiscordbotlist:1234S978@classic.as7wj.mongodb.net/myFirstDatabase?retryWrites=true&w=majority,
+PORT=3000
\ No newline at end of file
diff --git a/dashboard/assets/css/charts.css b/dashboard/assets/css/charts.css
new file mode 100644
index 0000000..a932ba0
--- /dev/null
+++ b/dashboard/assets/css/charts.css
@@ -0,0 +1,37 @@
+.charts {
+ display: flex;
+ justify-content: space-between;
+ text-align: center;
+}
+
+.charts svg {
+ display: flex;
+ float: left;
+}
+
+.ct-series-a .ct-line,
+.ct-series-a .ct-point {
+ stroke: var(--success);
+ stroke-width: 4px;
+}
+.ct-series-b .ct-line,
+.ct-series-b .ct-point {
+ stroke: var(--danger);
+ stroke-width: 4px;
+}
+
+.ct-series-a .ct-area {
+ fill: var(--success);
+}
+.ct-series-b .ct-area {
+ fill: var(--danger);
+}
+
+.ct-grids line,
+.ct-labels span,
+.ct-grid.ct-horizontal {
+ color: var(--font);
+}
+.ct-grid {
+ stroke: transparent;
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/commands.css b/dashboard/assets/css/commands.css
new file mode 100644
index 0000000..de6361a
--- /dev/null
+++ b/dashboard/assets/css/commands.css
@@ -0,0 +1,20 @@
+#search {
+ max-width: 300px;
+}
+#search + button {
+ height: 64px;
+}
+
+li {
+ cursor: pointer;
+}
+
+.commands li {
+ margin: 5px 0;
+ border-radius: 5px;
+ border: 1px solid var(--font) !important;
+}
+
+.list-group-item {
+ background-color: inherit;
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/index.css b/dashboard/assets/css/index.css
new file mode 100644
index 0000000..46f298c
--- /dev/null
+++ b/dashboard/assets/css/index.css
@@ -0,0 +1,3 @@
+.jumbotron {
+ margin-top: 25vh;
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/leaderboard.css b/dashboard/assets/css/leaderboard.css
new file mode 100644
index 0000000..3f57a7e
--- /dev/null
+++ b/dashboard/assets/css/leaderboard.css
@@ -0,0 +1,21 @@
+h1 img {
+ height: 64px;
+}
+
+li.list-group-item {
+ background-color: var(--background-secondary);
+ color: var(--heading);
+}
+
+li:nth-of-type(1) {
+ background: linear-gradient(to bottom, #C5B358 0%, #B6A449 100%);
+ color: white;
+}
+li:nth-of-type(2) {
+ background: linear-gradient(to bottom, #C0C0C0 0%, #B1B1B1 100%);
+ color: white;
+}
+li:nth-of-type(3) {
+ background: linear-gradient(to bottom, #CD7F32 0%, #BE7023 100%);
+ color: white;
+}
diff --git a/dashboard/assets/css/main.css b/dashboard/assets/css/main.css
new file mode 100644
index 0000000..5b21299
--- /dev/null
+++ b/dashboard/assets/css/main.css
@@ -0,0 +1,29 @@
+body {
+ overflow-x: hidden;
+ background-color: var(--background-primary);
+ color: var(--font);
+}
+
+h1, h2, h3, h4, h5 {
+ color: var(--heading);
+}
+
+code.hljs {
+ background-color: var(--background-secondary);
+}
+
+.user-avatar {
+ max-width: 36px;
+}
+
+.navbar a {
+ color: var(--heading);
+}
+
+.table {
+ color: var(--heading);
+}
+
+.dropdown-menu {
+ background-color: inherit;
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/music.css b/dashboard/assets/css/music.css
new file mode 100644
index 0000000..dadbd34
--- /dev/null
+++ b/dashboard/assets/css/music.css
@@ -0,0 +1,45 @@
+.card-header img {
+ width: 128px;
+ height: 128px;
+ object-fit: cover;
+ border-radius: 15px;
+}
+
+.buttons,
+#volume {
+ text-align: center;
+}
+.q-control,
+.position {
+ display: flex;
+ justify-content: center;
+}
+
+.form-control,
+.btn {
+ box-shadow: none !important;
+ outline: none !important;
+}
+
+.track-list img {
+ width: 64px;
+ height: 64px;
+ object-fit: cover;
+ border-radius: 10px;
+}
+
+#trackSearch {
+ margin-top: 20px;
+ max-height: 42px;
+}
+
+#volume {
+ cursor: pointer;
+}
+#volume input {
+ opacity: 0;
+ transition: opacity .3s ease-in-out;
+}
+#volume:hover input {
+ opacity: 1;
+}
diff --git a/dashboard/assets/css/sidebar.css b/dashboard/assets/css/sidebar.css
new file mode 100644
index 0000000..7609f05
--- /dev/null
+++ b/dashboard/assets/css/sidebar.css
@@ -0,0 +1,97 @@
+.icon, .icon img {
+ font-size: x-large;
+
+ height: 52px;
+ width: 52px;
+
+ transition: border-radius .3s ease-in-out;
+}
+.icon:hover, .icon img:hover {
+ border-radius: 15% !important;
+}
+.abbr {
+ padding-top: 7.5px;
+}
+
+.hamburger {
+ cursor: pointer;
+ font-size: xxx-large;
+
+ bottom: 0;
+ position: absolute;
+}
+
+#sidebar {
+ padding: 10px;
+ background-color: var(--background-tertiary);
+}
+#sidebar,
+#sidebarExtension {
+ height: 100vh;
+ float: left;
+}
+
+/* extension */
+#sidebarExtension {
+ width: 275px;
+ transition: .3s;
+ opacity: 1;
+ background-color: var(--background-secondary);
+}
+#sidebarExtension .large-icon {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%;
+}
+#sidebarExtension .large-icon,
+#sidebarExtension img {
+ font-size: xx-large;
+
+ width: 96px;
+ height: 96px;
+}
+#sidebarExtension .abbr {
+ padding-top: 25px;
+}
+#sidebarExtension h4 {
+ cursor: pointer;
+}
+
+#sidebarExtension.closed {
+ width: 0;
+ padding: 0px 0;
+}
+#sidebarExtension.closed {
+ opacity: 0;
+}
+
+/* tabs */
+.tabs a {
+ margin: 2px 10px;
+ padding: 5px;
+ padding-left: 10px !important;
+ border-radius: 5px;
+
+ text-decoration: none;
+ color: var(--font);
+}
+.tabs a.active,
+.tabs a:hover {
+ background-color: var(--background-primary);
+}
+
+.tabs .category {
+ padding: 24px 0 4px 24px;
+ font-size: 12px;
+ text-transform: uppercase;
+ font-weight: 700;
+
+ color: gray;
+ cursor: pointer;
+
+ transition: color .3s ease-in-out;
+}
+.tabs .category:hover {
+ color: var(--heading);
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/theme.css b/dashboard/assets/css/theme.css
new file mode 100644
index 0000000..1f8c5fa
--- /dev/null
+++ b/dashboard/assets/css/theme.css
@@ -0,0 +1,22 @@
+:root,
+:root[theme='ASCETIC'] {
+ --primary: black;
+
+ --heading: black;
+ --font: black;
+
+ --background-primary: white;
+ --background-secondary: #F8F9FA;
+ --background-tertiary: #F8F9FA;
+}
+
+:root[theme='DISCORD'] {
+ --primary: #7289DA;
+
+ --heading: white;
+ --font: #99AAB5;
+
+ --background-primary: #36393F;
+ --background-secondary: #2F3136;
+ --background-tertiary: #202225;
+}
\ No newline at end of file
diff --git a/dashboard/assets/css/utils.css b/dashboard/assets/css/utils.css
new file mode 100644
index 0000000..95d65fd
--- /dev/null
+++ b/dashboard/assets/css/utils.css
@@ -0,0 +1,28 @@
+.round {
+ border-radius: 50% !important;
+}
+
+.shadow {
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06) !important;
+}
+
+.cursor-pointer {
+ cursor: pointer !important;
+}
+
+/* buttons */
+.btn-gradient {
+ background-image: linear-gradient(135deg, var(--secondary), var(--info));
+ color: white;
+}
+.btn-gradient:hover {
+ color: white;
+}
+
+/* text */
+.text-gradient {
+ background: linear-gradient(135deg, var(--secondary), var(--info));
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
\ No newline at end of file
diff --git a/dashboard/assets/js/charts.js b/dashboard/assets/js/charts.js
new file mode 100644
index 0000000..a0d89f5
--- /dev/null
+++ b/dashboard/assets/js/charts.js
@@ -0,0 +1,11 @@
+function initChart(selector, ...log) {
+ new Chartist.Line(selector, {
+ labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ series: [...log]
+ }, { low: Math.min(log), showArea: true });
+}
+
+initChart('.joins-chart', joinsLog, leavesLog);
+initChart('.punishments-chart', punishmentsLog);
+initChart('.messages-chart', messagesLog);
+initChart('.commands-chart', commandsLog);
\ No newline at end of file
diff --git a/dashboard/assets/js/commands.js b/dashboard/assets/js/commands.js
new file mode 100644
index 0000000..b95aa99
--- /dev/null
+++ b/dashboard/assets/js/commands.js
@@ -0,0 +1,51 @@
+$('.categories li').on('click', setCategory);
+
+function setCategory() {
+ blank();
+
+ const selected = $(this);
+ selected.addClass('active');
+
+ const categoryCommands = $(`.commands .${selected[0].id}`);
+ categoryCommands.show();
+
+ updateResultsText(categoryCommands);
+}
+function blank() {
+ $('.categories li').removeClass('active');
+ $('.commands li').hide();
+}
+
+$('#search + button').on('click', () => {
+ const query = $('#search input').val();
+ if (!query.trim()) {
+ updateResultsText(commands);
+ return $('.commands li').show();
+ }
+
+ const results = new Fuse(commands, {
+ isCaseSensitive: false,
+ keys: [
+ { name: 'name', weight: 1 },
+ { name: 'category', weight: 0.5 }
+ ]
+ })
+ .search(query)
+ .map(r => r.item);
+
+ blank();
+
+ for (const command of results)
+ $(`#${command.name}Command`).show();
+
+ updateResultsText(results);
+});
+
+function updateResultsText(arr) {
+ $('#commandError').text(
+ (arr.length <= 0)
+ ? 'There is nothing to see here.'
+ : '');
+}
+
+setCategory.bind($('.categories li')[0])();
\ No newline at end of file
diff --git a/dashboard/assets/js/guild.js b/dashboard/assets/js/guild.js
new file mode 100644
index 0000000..254de2a
--- /dev/null
+++ b/dashboard/assets/js/guild.js
@@ -0,0 +1,23 @@
+$('.tabs a, #sidebarExtension h4').on('click', function() {
+ $('.tabs a').removeClass('active');
+ setModule($(this).attr('id'));
+});
+
+function setModule(name) {
+ $('.module').hide();
+ $(`#${name}Module`).show();
+ $(`#${name}`).addClass('active');
+}
+
+$('input').on('input', function() {
+ $(this)[0].checkValidity()
+ ? $(this).removeClass('border border-danger')
+ : $(this).addClass('border border-danger');
+
+ $('button.btn.btn-success')
+ .attr('disabled', !$('form')[0].checkValidity());
+});
+
+setModule('overview');
+
+hljs.initHighlightingOnLoad();
\ No newline at end of file
diff --git a/dashboard/assets/js/main.js b/dashboard/assets/js/main.js
new file mode 100644
index 0000000..04fdde1
--- /dev/null
+++ b/dashboard/assets/js/main.js
@@ -0,0 +1,3 @@
+$(function () {
+ $('[data-toggle="tooltip"]').tooltip()
+})
\ No newline at end of file
diff --git a/dashboard/assets/js/music/html-music-wrapper.js b/dashboard/assets/js/music/html-music-wrapper.js
new file mode 100644
index 0000000..1eaa143
--- /dev/null
+++ b/dashboard/assets/js/music/html-music-wrapper.js
@@ -0,0 +1,92 @@
+class HTMLMusicWrapper {
+ #music;
+
+ get currentTimestamp() {
+ const position = this.#music.position;
+
+ const minutes = Math.floor(position / 60).toString().padStart(2, '0');
+ const seconds = Math.floor(position - (minutes * 60)).toString().padStart(2, '0');
+ return `${minutes}:${seconds}`;
+ }
+
+ set apiError(error) {
+ if (!error)
+ return $('#musicAPIError').addClass('d-none');
+
+ $('#musicAPIError').removeClass('d-none');
+ $('#musicAPIError').text(error.message ?? 'Unknown error.');
+ }
+
+ constructor(musicClient) {
+ this.#music = musicClient;
+
+ setInterval(() => this.#updateSeeker(), 1000);
+ }
+
+ #updateSeeker() {
+ if (!this.#music.isPlaying || this.#music.isPaused) return;
+
+ this.#music.position++;
+
+ $('#seekTrack input').val(this.#music.position);
+ $('.current').text(this.currentTimestamp);
+ }
+
+ updateList() {
+ $('.now-playing').html(this.#nowPlaying());
+
+ const track = (this.#music.isPlaying) ? this.#music.list[0] : null;
+ if (track) {
+ $('.current').text(this.currentTimestamp);
+ $('.duration').text(track.duration.timestamp);
+ $('#seekTrack input').attr('max', track.duration.seconds);
+ } else {
+ $('.current, .duration').text(`00:00`);
+ }
+
+ $('.track-list').html(
+ (!this.#music.isPlaying)
+ ? '
No tracks here.
'
+ : this.#music.list
+ .map(this.#htmlTrack)
+ .join()
+ );
+
+ $('.track .remove').on('click', async () => {
+ const index = $('.track .remove').index('.remove');
+ await this.#music.remove(index);
+ });
+ }
+
+ toggle() {
+ $('#toggleTrack i').toggleClass('fa-pause');
+ $('#toggleTrack i').toggleClass('fa-play');
+ }
+
+ #nowPlaying() {
+ if (!this.#music.isPlaying) return ``;
+
+ const track = this.#music.list[0];
+ return `
+
+ ${track.title}
+ ${track.author.name}
+ `;
+ }
+
+ #htmlTrack(track) {
+ return `
+
+
+
+ ${track.title}
+ ${track.author.name}
+
+
+ ${track.duration.timestamp}
+
+
+
+ `;
+ }
+}
diff --git a/dashboard/assets/js/music/music-wrapper.js b/dashboard/assets/js/music/music-wrapper.js
new file mode 100644
index 0000000..624d7ff
--- /dev/null
+++ b/dashboard/assets/js/music/music-wrapper.js
@@ -0,0 +1,110 @@
+class MusicWrapper {
+ #endpoint = `/api/guilds/${guildId}/music`;
+ #html = new HTMLMusicWrapper(this);
+
+ isPaused = $('#toggleTrack i').hasClass('fa-play');
+ list = [];
+ position = +$('#seekTrack input').val();
+
+ get isPlaying() {
+ return this.list.length > 0;
+ }
+
+ async #fetch(action) {
+ try {
+ const res = await fetch(`${this.#endpoint}/${action}`, {
+ headers: { Authorization: key }
+ });
+ const json = await res.json();
+ if (!res.ok)
+ throw json;
+
+ return json;
+ } catch (error) {
+ this.#html.apiError = error;
+ throw error;
+ }
+ }
+
+ async play(query) {
+ try {
+ await this.#fetch(`play?q=${query}`);
+ this.#html.apiError = null;
+ } catch {}
+ await this.updateList();
+ }
+
+ async stop() {
+ try {
+ await this.#fetch(`stop`);
+ this.#html.apiError = null;
+ this.position = 0;
+ } catch {}
+ await this.updateList();
+ }
+
+ async toggle() {
+ try {
+ await this.#fetch(`toggle`);
+ this.#html.apiError = null;
+ } catch {}
+ }
+
+ async setVolume(value) {
+ try {
+ await this.#fetch(`volume?v=${value}`);
+ this.#html.apiError = null;
+ } catch {}
+ }
+
+ async seek(to) {
+ try {
+ await this.#fetch(`seek?to=${to}`);
+ this.position = to;
+
+ this.#html.apiError = null;
+ } catch {}
+ }
+
+ async toggle() {
+ try {
+ await this.#fetch(`toggle`);
+ this.isPaused = !this.isPaused;
+
+ this.#html.apiError = null;
+ this.#html.toggle();
+ } catch {}
+ }
+
+ async remove(index) {
+ try {
+ const list = await this.#fetch(`remove?i=${index}`);
+
+ this.#html.apiError = null;
+ await this.updateList(list);
+ } catch {}
+ }
+
+ async shuffle() {
+ try {
+ const list = await this.#fetch(`shuffle`);
+
+ this.#html.apiError = null;
+ await this.updateList(list);
+ } catch {}
+ }
+
+ async skip() {
+ try {
+ const list = await this.#fetch(`skip`);
+
+ this.#html.apiError = null;
+ await this.updateList(list);
+ } catch {}
+ }
+
+ async updateList(list = null) {
+ this.list = list ?? await this.#fetch('list');
+ this.#html.updateList();
+ }
+}
diff --git a/dashboard/assets/js/music/music.js b/dashboard/assets/js/music/music.js
new file mode 100644
index 0000000..d49ed16
--- /dev/null
+++ b/dashboard/assets/js/music/music.js
@@ -0,0 +1,22 @@
+$(async () => {
+ const music = new MusicWrapper();
+ await music.updateList();
+
+ $('#skipTrack').on('click', () => music.skip());
+ $('#toggleTrack').on('click', () => music.toggle());
+ $('#shuffleList').on('click', () => music.shuffle());
+ $('#stopTrack').on('click', () => music.stop());
+ $('#trackSearch').on('click', async () => {
+ const query = $('.q-control input').val();
+ await music.play(query);
+ });
+
+ $('#seekTrack input').on('input', async function() {
+ const to = +$(this).val();
+ await music.seek(to);
+ });
+ $('#volume input').on('input', async function() {
+ const value = +$(this).val();
+ await music.setVolume(value);
+ });
+});
diff --git a/dashboard/assets/js/sidebar.js b/dashboard/assets/js/sidebar.js
new file mode 100644
index 0000000..a10d518
--- /dev/null
+++ b/dashboard/assets/js/sidebar.js
@@ -0,0 +1,3 @@
+$('.hamburger').on('click', function() {
+ $('#sidebarExtension').toggleClass('closed');
+});
\ No newline at end of file
diff --git a/dashboard/assets/js/theme.js b/dashboard/assets/js/theme.js
new file mode 100644
index 0000000..79545f5
--- /dev/null
+++ b/dashboard/assets/js/theme.js
@@ -0,0 +1,11 @@
+$('#themeSelect').on('change', function() {
+ setTheme($(this).val());
+});
+
+function setTheme(theme) {
+ localStorage.setItem('theme', theme);
+ $('#themeSelect').val(theme);
+ $('html').attr('theme', theme);
+}
+
+setTheme(localStorage.getItem('theme'));
diff --git a/dashboard/modules/api-utils.js b/dashboard/modules/api-utils.js
new file mode 100644
index 0000000..14e9cba
--- /dev/null
+++ b/dashboard/modules/api-utils.js
@@ -0,0 +1,4 @@
+module.exports.sendError = (res, {
+ code = 400,
+ message = 'An unknown error occurred.'
+}) => res.status(400).json({ code, message });
diff --git a/dashboard/modules/audit-logger.js b/dashboard/modules/audit-logger.js
new file mode 100644
index 0000000..31737a5
--- /dev/null
+++ b/dashboard/modules/audit-logger.js
@@ -0,0 +1,9 @@
+const logs = require('../../data/logs');
+
+module.exports = new class {
+ async change(id, change) {
+ const log = await logs.get(id);
+ log.changes.push(change);
+ await log.save();
+ }
+}
diff --git a/dashboard/modules/auth-client.js b/dashboard/modules/auth-client.js
new file mode 100644
index 0000000..17b97af
--- /dev/null
+++ b/dashboard/modules/auth-client.js
@@ -0,0 +1,8 @@
+const OAuthClient = require('disco-oauth');
+const config = require('../../config.js');
+
+const client = new OAuthClient(config.bid, config.secret);
+client.setRedirect(`${config.dashboardURL}/auth`);
+client.setScopes('identify', 'guilds');
+
+module.exports = client;
\ No newline at end of file
diff --git a/dashboard/modules/middleware.js b/dashboard/modules/middleware.js
new file mode 100644
index 0000000..f8902e6
--- /dev/null
+++ b/dashboard/modules/middleware.js
@@ -0,0 +1,137 @@
+const sessions = require('./sessions');
+const { sendError } = require('./api-utils');
+const bot = require('../../xp');
+const musicHandler = require('../../handlers/music-handler');
+
+module.exports.updateGuilds = async (req, res, next) => {
+ try {
+ const key = res.cookies.get('key')
+ || req.get('Authorization');
+ if (key) {
+ const { guilds } = await sessions.get(key);
+ res.locals.guilds = guilds;
+ }
+ } finally {
+ return next();
+ }
+};
+
+module.exports.updateUser = async (req, res, next) => {
+ try {
+ const key = res.cookies.get('key')
+ req.get('Authorization');
+ if (key) {
+ const { authUser } = await sessions.get(key);
+ res.locals.user = authUser;
+ }
+ } finally {
+ return next();
+ }
+};
+
+module.exports.updateMusicPlayer = async (req, res, next) => {
+ try {
+ const requestor = bot.guilds.cache
+ .get(req.params.id).members.cache.get(res.locals.user.id)
+
+ if (!requestor)
+ throw new TypeError('You shall not pass.');
+
+ res.locals.requestor = requestor;
+ res.locals.player = function(options){
+ musicHandler.get({
+ guildId: req.params.id,
+ voiceChannel: !requestor.voice ? null : requestor.voice.channel,
+ userxd : requestor,
+ track: options.songname,
+ res: res
+ });
+ }
+ res.locals.player.play = function(options){
+
+
+ musicHandler.play({
+ guildId: req.params.id,
+ voiceChannel: !requestor.voice ? null : requestor.voice.channel,
+ track: options.songname,
+ userxd : requestor,
+ res: res
+ });
+ }
+
+ res.locals.player.isPaused = function(){
+ let queue = bot.queue.get(req.params.id);
+ if(queue.playing !== false)
+ {
+ return false;
+ }
+ else {
+ return true;
+ }
+
+ }
+ res.locals.player.shuffle = function()
+ {
+ let queue = bot.queue.get(req.params.id);
+
+ let songs = queue.songs;
+ for (let i = songs.length - 1; i > 1; i--) {
+ let j = 1 + Math.floor(Math.random() * i);
+ [songs[i], songs[j]] = [songs[j], songs[i]];
+ }
+ queue.songs = songs;
+ bot.queue.set(req.params.id, queue);
+ }
+ res.locals.player.settings = function(){
+ let queue = bot.queue.get(req.params.id);
+ return queue;
+ }
+
+ res.locals.player.skip = function(){
+ let queue = bot.queue.get(req.params.id);
+ queue.connection.dispatcher.end('Okie skipped!')
+ }
+ res.locals.player.settings.volume = function(options){
+ let queue = bot.queue.get(req.params.id);
+ queue.connection.dispatcher.setVolumeLogarithmic(options.rate);
+ }
+ res.locals.player.end = function(){
+ let queue = bot.queue.get(req.params.id);
+ queue.songs = []
+ queue.connection.dispatcher.end("done");
+ }
+ res.locals.player.queue = function(){
+ let queue = bot.queue.get(req.params.id);
+ return queue.songs;
+ }
+ res.locals.player.pause = function(){
+ let queue = bot.queue.get(req.params.id);
+ queue.connection.dispatcher.pause();
+ }
+ res.locals.player.resume = function(){
+ let queue = bot.queue.get(req.params.id);
+ queue.connection.dispatcher.resume();
+ }
+ res.locals.player.stop = function(){
+ let queue = bot.queue.get(req.params.id);
+ queue.connection.dispatcher.end("done");
+ }
+ return next();
+ } catch (error) {
+
+ sendError(res, { message: error.message });
+ }
+};
+
+module.exports.validateGuild = async (req, res, next) => {
+ res.locals.guild = res.locals.guilds.find(g => g.id === req.params.id);
+ return (res.locals.guild)
+ ? next()
+ : res.render('errors/404');
+};
+
+module.exports.validateUser = async (req, res, next) => {
+ return (res.locals.user)
+ ? next()
+ : res.render('errors/401');
+};
\ No newline at end of file
diff --git a/dashboard/modules/rate-limiter.js b/dashboard/modules/rate-limiter.js
new file mode 100644
index 0000000..2d5bec7
--- /dev/null
+++ b/dashboard/modules/rate-limiter.js
@@ -0,0 +1,10 @@
+const rateLimit = require('express-rate-limit');
+const RateLimitStore = require('rate-limit-mongo');
+const config = require('../../config.js');
+
+module.exports = rateLimit({
+ max: 300,
+ message: 'You are being rate limited.',
+ store: new RateLimitStore({ uri: config.mongourl }),
+ windowMs: 60 * 1000
+});
diff --git a/dashboard/modules/sessions.js b/dashboard/modules/sessions.js
new file mode 100644
index 0000000..e72ed51
--- /dev/null
+++ b/dashboard/modules/sessions.js
@@ -0,0 +1,40 @@
+const authClient = require('./auth-client');
+const bot = require('../../xp');
+
+const sessions = new Map();
+
+function get(key) {
+ return sessions.get(key) || create(key);
+}
+
+async function create(key) {
+ setTimeout(() => sessions.delete(key), 5 * 60 * 1000);
+ await update(key);
+
+ return sessions.get(key);
+}
+
+async function update(key) {
+ return sessions
+ .set(key, {
+ authUser: await authClient.getUser(key),
+ guilds: getManageableGuilds(await authClient.getGuilds(key))
+ });
+}
+
+function getManageableGuilds(authGuilds) {
+ const guilds = [];
+ for (const id of authGuilds.keys()) {
+ const isManager = authGuilds
+ .get(id).permissions
+ .includes('MANAGE_GUILD');
+ const guild = bot.guilds.cache.get(id);
+ if (!guild || !isManager) continue;
+
+ guilds.push(guild);
+ }
+ return guilds;
+}
+
+module.exports.get = get;
+module.exports.update = update;
\ No newline at end of file
diff --git a/dashboard/routes/auth-routes.js b/dashboard/routes/auth-routes.js
new file mode 100644
index 0000000..144ced8
--- /dev/null
+++ b/dashboard/routes/auth-routes.js
@@ -0,0 +1,41 @@
+const config = require('../../config.js');
+const express = require('express');
+const authClient = require('../modules/auth-client');
+const sessions = require('../modules/sessions');
+
+const router = express.Router();
+
+router.get('/invite', (req, res) =>
+ res.redirect(`https://discord.com/api/oauth2/authorize?client_id=874338511464046622&permissions=0&redirect_uri=https%3A%2F%2Fdyno-clone.dhvitop.repl.co%2Fauth&scope=bot`));
+
+router.get('/login', (req, res) =>
+ res.redirect(`https://discord.com/api/oauth2/authorize?client_id=874338511464046622&redirect_uri=https%3A%2F%2Fdyno-clone.dhvitop.repl.co%2Fauth&response_type=code&scope=identify%20guilds`));
+
+router.get('/auth-guild', async (req, res) => {
+ try {
+ const key = res.cookies.get('key');
+ await sessions.update(key);
+ } finally {
+ res.redirect('/dashboard');
+ }
+});
+
+router.get('/auth', async (req, res) => {
+ try {
+ const code = req.query.code;
+ const key = await authClient.getAccess(code);
+
+ res.cookies.set('key', key);
+ res.redirect('/dashboard');
+ } catch {
+ res.redirect('/');
+ }
+});
+
+router.get('/logout', (req, res) => {
+ res.cookies.set('key', '');
+
+ res.redirect('/');
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/dashboard/routes/dashboard-routes.js b/dashboard/routes/dashboard-routes.js
new file mode 100644
index 0000000..420bfbf
--- /dev/null
+++ b/dashboard/routes/dashboard-routes.js
@@ -0,0 +1,44 @@
+const express = require('express');
+const { validateGuild, updateMusicPlayer } = require('../modules/middleware');
+const log = require('../modules/audit-logger');
+const guilds = require('../../data/guilds');
+const logs = require('../../data/logs');
+const bot = require('../../xp');
+
+const router = express.Router();
+
+router.get('/dashboard', (req, res) => res.render('dashboard/index'));
+
+router.get('/servers/:id', validateGuild, updateMusicPlayer,
+ async (req, res) => res.render('dashboard/show', {
+ savedGuild: await guilds.get(req.params.id),
+ savedLog: await logs.get(req.params.id),
+ users: bot.users.cache,
+ player: res.locals.player,
+ key: res.cookies.get('key')
+ }));
+
+router.put('/servers/:id/:module', validateGuild, async (req, res) => {
+ try {
+ const { id, module } = req.params;
+
+ const savedGuild = await guilds.get(id);
+
+ await log.change(id, {
+ at: new Date(),
+ by: res.locals.user.id,
+ module,
+ new: {...savedGuild[module]},
+ old: {...req.body}
+ });
+
+ savedGuild[module] = req.body;
+ await savedGuild.save();
+
+ res.redirect(`/servers/${id}`);
+ } catch {
+ res.render('errors/400');
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/dashboard/routes/music-routes.js b/dashboard/routes/music-routes.js
new file mode 100644
index 0000000..6ed9128
--- /dev/null
+++ b/dashboard/routes/music-routes.js
@@ -0,0 +1,123 @@
+const express = require('express');
+const { sendError } = require('../modules/api-utils');
+
+const router = express.Router({ mergeParams: true });
+
+router.get('/play', async (req, res) => {
+ try {
+ let test = !res.locals.requestor.voice ? false : true;
+ if(test === false)
+ {
+
+
+ sendError(res, { message: "You Should be Connected to Any Voice Channel on The Selected Guild" });
+ }
+ const track = await res.locals.player.play({songname : req.query.q, userxd: res.locals.requestor});
+ res.json({ message: `${track}`});
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/stop', async (req, res) => {
+ try {
+ res.locals.player.end();
+
+ res.json({ code: 200, message: 'Success!' });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/toggle', async (req, res) => {
+ try {
+
+ if(res.locals.player.isPaused === true)
+ {
+
+ res.locals.player.resume();
+ }
+ else {
+ res.locals.player.pause();
+ }
+
+
+
+ res.json({ message: 'Success' });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/volume', async (req, res) => {
+ try {
+ res.locals.player.settings.volume({rate:req.query.v});
+
+ res.json({ message: 'Success' });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/seek', async (req, res) => {
+ try {
+
+ let queue = res.locals.player.settings;
+ queue.connection.dispatcher.end('Okie skipped!')
+
+ res.json({ message: 'Success' });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/list', async (req, res) => {
+ try {
+ res.json({ message: `${res.locals.player.queue}`});
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/remove', async (req, res) => {
+ try {
+ const index = +req.query.i;
+ let queue = res.locals.player.settings;
+ queue.connection.dispatcher.stop('Okie skipped!')
+
+ res.json({ message: 'Success' });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/shuffle', async (req, res) => {
+ try {
+ res.locals.player.shuffle();
+
+ res.json({ message: `${res.locals.player.settings.songs}` });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+router.get('/skip', async (req, res) => {
+ try {
+ await res.locals.player.skip();
+
+ res.json({ message: `${res.locals.player.settings.songs}` });
+ } catch (error) {
+ console.log(error);
+ sendError(res, error);
+ }
+});
+
+module.exports = router;
diff --git a/dashboard/routes/root-routes.js b/dashboard/routes/root-routes.js
new file mode 100644
index 0000000..e22773c
--- /dev/null
+++ b/dashboard/routes/root-routes.js
@@ -0,0 +1,48 @@
+const express = require('express');
+const {readdirSync} = require("fs");
+const bot = require('../../xp');
+const users = require('../../data/users');
+const commands = new Map();
+ readdirSync('./commands/').forEach(dir => {
+
+ const commandsxd = readdirSync(`./commands/${dir}/`).filter(file => file.endsWith('.js'));
+ for(let file of commandsxd){
+ let pull = require(`../../commands/${dir}/${file}`);
+
+ if(pull){
+ commands.set(pull.name, pull);
+
+
+ }
+ }
+ });
+
+const router = express.Router();
+
+router.get('/', (req, res) => res.render('index'));
+
+router.get('/commands', (req, res) => res.render('commands', {
+ subtitle: 'Commands',
+ categories: [
+ { name: 'Auto Mod', icon: 'fas fa-gavel' },
+ { name: 'Economy', icon: 'fas fa-coins' },
+ { name: 'General', icon: 'fas fa-star' },
+ { name: 'Music', icon: 'fas fa-music' }
+ ],
+ commands: Array.from(commands.values()),
+ commandsString: JSON.stringify(Array.from(commands.values()))
+}));
+
+router.get('/leaderboard/:id', async (req, res) => {
+ const guildxd = bot.guilds.cache.get(req.params.id);
+ if (!guildxd)
+ return res.render('errors/404');
+
+ const savedUsers = (await users.getInGuild(req.params.id))
+ .sort((a, b) => (a.coins < b.coins) ? 1 : -1)
+ .slice(0, 100);
+
+ res.render('dashboard/leaderboard', { guildxd, savedUsers });
+});
+
+module.exports = router;
diff --git a/dashboard/server.js b/dashboard/server.js
new file mode 100644
index 0000000..1c6a9a9
--- /dev/null
+++ b/dashboard/server.js
@@ -0,0 +1,46 @@
+const bodyParser = require('body-parser');
+const cookies = require('cookies');
+const express = require('express');
+const methodOverride = require('method-override');
+const middleware = require('./modules/middleware');
+const rateLimit = require('./modules/rate-limiter');
+const { sendError } = require('./modules/api-utils');
+
+const authRoutes = require('./routes/auth-routes');
+const dashboardRoutes = require('./routes/dashboard-routes');
+const rootRoutes = require('./routes/root-routes');
+const musicRoutes = require('./routes/music-routes');
+
+const app = express();
+
+app.set('views', __dirname + '/views');
+app.set('view engine', 'pug');
+
+app.use(rateLimit);
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(methodOverride('_method'));
+app.use(cookies.express('a', 'b', 'c'));
+
+app.use(express.static(`${__dirname}/assets`));
+app.locals.basedir = `${__dirname}/assets`;
+
+app.use('/api/guilds/:id/music',
+ middleware.updateUser,
+ middleware.validateUser,
+ middleware.updateGuilds,
+ middleware.validateGuild,
+ middleware.updateMusicPlayer,
+ musicRoutes
+);
+app.use('/api', (req, res) => res.json({ hello: 'earth' }));
+app.use('/api/*', (req, res) => sendError(res, { code: 404, message: 'Not found.' }));
+
+app.use('/',
+ middleware.updateUser, rootRoutes,
+ authRoutes,
+ middleware.validateUser, middleware.updateGuilds, dashboardRoutes
+);
+app.all('*', (req, res) => res.render('errors/404'));
+
+const port = process.env.PORT || 3000;
+app.listen(port, () => console.log(`Server is live on port ${port}`));
\ No newline at end of file
diff --git a/dashboard/views/commands.pug b/dashboard/views/commands.pug
new file mode 100644
index 0000000..26bc90e
--- /dev/null
+++ b/dashboard/views/commands.pug
@@ -0,0 +1,37 @@
+include includes/mixins
+
+doctype
+html(lang='en')
+ head
+ include includes/header.pug
+
+ script(src='https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js', defer)
+ script(src='/js/commands.js', defer)
+ script(defer).
+ let commands = !{commandsString};
+
+ link(rel='stylesheet', href='/css/commands.css')
+ body
+ include includes/navbar.pug
+ .container
+ .jumbotron.text-center.bg-transparent
+ h1.display-3 Commands
+ p.lead View the commands for 1PG.
+ hr
+
+ section#commands
+ .d-flex.justify-content-center
+ #search.form-group.p-3
+ input.form-control(type='search')
+ button.btn
+ i.fas.fa-search
+ .row
+ .col-sm-3.categories
+ ul.list-group.mb-2
+ each category in categories
+ +category(category)
+ .col-sm-9.commands
+ ul.list-group.mb-2
+ each command in commands
+ +command(command)
+ p#commandError
\ No newline at end of file
diff --git a/dashboard/views/dashboard/extensions/audit-log.pug b/dashboard/views/dashboard/extensions/audit-log.pug
new file mode 100644
index 0000000..54211a0
--- /dev/null
+++ b/dashboard/views/dashboard/extensions/audit-log.pug
@@ -0,0 +1,21 @@
+section#auditLogModule.module.container.px-5
+ .jumbotron.bg-transparent.pb-0
+ h1.display-4.text-center Audit Log
+ .px-5.mt-5.d-flex
+ table.table.table-responsive-xl
+ thead
+ tr
+ th(scope='col') #
+ th(scope='col') At
+ th(scope='col') By
+ th(scope='col') Old
+ th(scope='col') New
+ tbody
+ each change, i in savedLog.changes.reverse()
+ tr
+ if i < 3
+ th(scope='row') #{savedLog.changes.length - i}
+ td #{new Date(change.at).toLocaleString()}
+ td #[+user(users.get(change.by))]
+ td #[pre #[code #{JSON.stringify(change.old, null, 2)}]]
+ td #[pre #[code #{JSON.stringify(change.new, null, 2)}]]
\ No newline at end of file
diff --git a/dashboard/views/dashboard/index.pug b/dashboard/views/dashboard/index.pug
new file mode 100644
index 0000000..1dafc3c
--- /dev/null
+++ b/dashboard/views/dashboard/index.pug
@@ -0,0 +1,29 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+
+ script(src='/js/sidebar.js', defer)
+ link(rel='stylesheet', href='/css/sidebar.css')
+ body
+ include ../includes/sidebar.pug
+
+ #sidebarExtension
+ header.text-center.pt-4
+ .large-icon.bg-white.round
+ img.round(src=user.avatarUrl(128), alt=user.username)
+ h4.pt-2 #{user.username}
+ span.text-muted #{'#' + user.discriminator.toString().padStart(4, '0')}
+
+ .p-4
+ label Theme
+ select#themeSelect.form-control
+ option(value='ASCETIC', selected) Ascetic (Default)
+ option(value='DISCORD') Discord
+
+ include ../includes/navbar.pug
+
+ .container.jumbotron.text-center.bg-transparent
+ h1.display-3 Dashboard
+ hr
+ p.lead Manage your servers with the 1PG dashboard.
\ No newline at end of file
diff --git a/dashboard/views/dashboard/leaderboard.pug b/dashboard/views/dashboard/leaderboard.pug
new file mode 100644
index 0000000..c246498
--- /dev/null
+++ b/dashboard/views/dashboard/leaderboard.pug
@@ -0,0 +1,25 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+
+ link(rel='stylesheet', href='/css/leaderboard.css')
+ body
+ include ../includes/navbar.pug
+ header.text-center
+ h1
+
+ span #{guildxd.name} Leaderboard
+ p.lead View the top 100 richest users in #{guildxd.name}.
+
+ ul.list-group.container.mt-5
+ each savedUser, index in savedUsers
+ - var user = guildxd.members.cache.get(savedUser.id).user;
+ if user
+ li.list-group-item
+ span
+ strong.round.mr-3 #{index + 1}
+ img.round.user-avatar.mr-2(src=user.displayAvatarURL({ dynamic: true }))
+ span.lead #{user.username}
+ span.text-muted ##{user.discriminator}
+ span.float-right.pt-2 #{savedUser.coins} #[i.fas.fa-coins.text-warning]
\ No newline at end of file
diff --git a/dashboard/views/dashboard/modules/auto-mod.pug b/dashboard/views/dashboard/modules/auto-mod.pug
new file mode 100644
index 0000000..adb2b6e
--- /dev/null
+++ b/dashboard/views/dashboard/modules/auto-mod.pug
@@ -0,0 +1,5 @@
+section#autoModModule.module.container.px-5
+ .jumbotron.bg-transparent.pb-0
+ h1.display-4.text-center Auto-mod
+ .form-group.mt-5
+ .row
\ No newline at end of file
diff --git a/dashboard/views/dashboard/modules/economy.pug b/dashboard/views/dashboard/modules/economy.pug
new file mode 100644
index 0000000..100d1aa
--- /dev/null
+++ b/dashboard/views/dashboard/modules/economy.pug
@@ -0,0 +1,5 @@
+section#economyModule.module.container.px-5
+ .jumbotron.bg-transparent.pb-0
+ h1.display-4.text-center Economy
+ .form-group.mt-5
+ .row
\ No newline at end of file
diff --git a/dashboard/views/dashboard/modules/general.pug b/dashboard/views/dashboard/modules/general.pug
new file mode 100644
index 0000000..4e40e3a
--- /dev/null
+++ b/dashboard/views/dashboard/modules/general.pug
@@ -0,0 +1,22 @@
+section#generalModule.module.container.px-5
+ form(action='/servers/' + guild.id + '/general?_method=PUT', method='POST')
+ .jumbotron.bg-transparent.pb-0
+ h1.display-4.text-center General
+ .form-group.my-5
+ .row
+ .col-4
+ label Prefix
+ input.form-control(type='text', name='prefix', value=savedGuild.general.prefix,
+ maxlength='5', required)
+ .col-4
+ label Blacklisted Channels
+ input(name='blacklistedChannelIds', type='hidden', value='')
+ select.form-control(name='blacklistedChannelIds[]', multiple)
+ each channel of Array.from(guild.channels.cache.filter(c => c.type === 'text').values())
+ if savedGuild.general.blacklistedChannelIds.includes(channel.id)
+ option(value=channel.id, selected) ##{channel.name}
+ else
+ option(value=channel.id) ##{channel.name}
+ .col-4
+ .d-flex.justify-content-center
+ button.btn.btn-success #[i.fas.fa-rocket] Save
\ No newline at end of file
diff --git a/dashboard/views/dashboard/modules/music.pug b/dashboard/views/dashboard/modules/music.pug
new file mode 100644
index 0000000..4a66f62
--- /dev/null
+++ b/dashboard/views/dashboard/modules/music.pug
@@ -0,0 +1,38 @@
+section#musicModule.module.container.px-5
+ .jumbotron.bg-transparent.pb-0
+ h1.display-4.text-center Music
+ .form-group.mt-5
+
+ - const track = ((!player.settings.songs ? 0 : player.settings.songs.length) > 0) ? player.settings.songs[0].title : null;
+ .card.bg-transparent.shadow.p-3
+ #musicAPIError.alert.alert-danger.d-none
+ .card-header.bg-transparent.row
+ .col
+ h5 Player
+ .now-playing
+ .col.mt-5
+ .controls.row
+ .col-3
+ .col-6.buttons
+ button#toggleTrack.btn.text-gradient #[i.fas(class=player.isPaused ? 'fa-play' : 'fa-pause')]
+ button#stopTrack.btn.text-gradient #[i.fas.fa-square]
+ button#skipTrack.btn.text-gradient #[i.fas.fa-forward]
+ button#shuffleList.btn.text-gradient #[i.fas.fa-random]
+ #volume.col-3
+ i.fas.fa-volume-up.text-gradient
+ input.form-control(type='range', value='0', max='1', step='0.1')
+ #seekTrack
+ input.form-control(type='range', value=(Math.floor(player.position || 0) / 1000))
+ .position
+ span.current 00:00
+ span.text-muted.px-1 /
+ span.duration.text-muted 00:00
+
+ .card-body
+ h5 Queue
+ .q-control.mb-3
+ label Search
+ input.form-control
+ button#trackSearch.ml-2.mb-1.btn.btn-gradient +
+ .track-list
+
\ No newline at end of file
diff --git a/dashboard/views/dashboard/modules/overview.pug b/dashboard/views/dashboard/modules/overview.pug
new file mode 100644
index 0000000..d30a6d7
--- /dev/null
+++ b/dashboard/views/dashboard/modules/overview.pug
@@ -0,0 +1,41 @@
+section#overviewModule.module.px-5
+ .form-group.mt-5
+ .row.text-center
+ .col-lg-3.col-md-6
+ .border.rounded.m-3.p-3
+ p.uppercase
+ i.fas.fa-user-ninja
+ strong.ml-1 Owner
+ p.mb-0 #{guild.owner.user.tag}
+ .col-lg-3.col-md-6
+ .border.rounded.m-3.p-3
+ p.uppercase
+ i.fas.fa-user-alt
+ strong.ml-1 Members
+ p.mb-0 #{guild.memberCount}
+ .col-lg-3.col-md-6
+ .border.rounded.m-3.p-3
+ p.uppercase
+ i.fas.fa-at
+ strong.ml-1 Roles
+ p.mb-0 #{guild.roles.cache.size}
+ .col-lg-3.col-md-6
+ .border.rounded.m-3.p-3
+ p.uppercase
+ i.fas.fa-hashtag
+ strong.ml-1 Channels
+ p.mb-0 #{guild.channels.cache.size}
+
+ .charts.mt-5.px-4
+ .card-dark.float-left
+ h3 Joins/Leaves
+ .joins-chart
+ .card-dark.float-left
+ h3 Punishments
+ .punishments-chart
+ .card-dark.float-left
+ h3 Messages
+ .messages-chart
+ .card-dark.float-left
+ h3 Commands
+ .commands-chart
\ No newline at end of file
diff --git a/dashboard/views/dashboard/show.pug b/dashboard/views/dashboard/show.pug
new file mode 100644
index 0000000..5c07a04
--- /dev/null
+++ b/dashboard/views/dashboard/show.pug
@@ -0,0 +1,61 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+ include ../includes/mixins.pug
+
+ script(defer).
+ const commandsLog = [!{savedLog.commands}];
+ const messagesLog = [!{savedLog.messages}];
+ const punishmentsLog = [!{savedLog.punishments}];
+ const joinsLog = [!{savedLog.joins}];
+ const leavesLog = [!{savedLog.leaves}];
+ const guildId = '#{savedGuild.id}';
+ const key = '#{key}';
+
+ script(src='https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.js', defer)
+ script(src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.3.2/highlight.min.js', defer)
+ script(src='/js/charts.js', defer)
+ script(src='/js/sidebar.js', defer)
+ script(src='/js/guild.js', defer)
+
+ script(src='/js/music/html-music-wrapper.js', defer)
+ script(src='/js/music/music-wrapper.js', defer)
+ script(src='/js/music/music.js', defer)
+
+ link(rel='stylesheet', href='https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.css')
+ link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.3.2/styles/ascetic.min.css')
+ link(rel='stylesheet', href='/css/sidebar.css')
+ link(rel='stylesheet', href='/css/charts.css')
+ link(rel='stylesheet', href='/css/music.css')
+ body
+ include ../includes/sidebar.pug
+
+ #sidebarExtension
+ header.text-center.pt-4
+ .large-icon.bg-white.round
+ if guild.icon
+ img.round(src=guild.iconURL({ dynamic: true, size: 128 }), alt=guild.name)
+ else
+ p.abbr #{guild.nameAcronym}
+ h4#overview.pt-2 #{guild.name}
+ .tabs.navbar-nav
+ .category Modules
+ a#autoMod.cursor-pointer #[i.fas.fa-gavel.pr-1.text-muted] Auto-mod
+ a#economy.cursor-pointer #[i.fas.fa-coins.pr-1.text-muted] Economy
+ a#general.cursor-pointer #[i.fas.fa-star.pr-1.text-muted] General
+ a#music.cursor-pointer #[i.fas.fa-music.pr-1.text-muted] Music
+
+ .category Other
+ a#auditLog.cursor-pointer #[i.fas.fa-book.pr-1.text-muted] Audit Log
+ a.cursor-pointer(href='/leaderboard/' + guild.id) #[i.fas.fa-trophy.pr-1.text-muted] Leaderboard
+
+ include ../includes/navbar.pug
+
+ include modules/overview
+ include modules/auto-mod
+ include modules/economy
+ include modules/general
+ include modules/music
+
+ include extensions/audit-log
\ No newline at end of file
diff --git a/dashboard/views/errors/400.pug b/dashboard/views/errors/400.pug
new file mode 100644
index 0000000..46e7940
--- /dev/null
+++ b/dashboard/views/errors/400.pug
@@ -0,0 +1,12 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+
+ link(rel='stylesheet', href='/css/index.css')
+ body
+ .jumbotron.text-center.bg-transparent
+ h1.display-3 400
+ p.lead This planet denied your request.
+ i.fas.fa-times.pl-2
+ a.btn.btn-dark(href='/') Return Home
\ No newline at end of file
diff --git a/dashboard/views/errors/401.pug b/dashboard/views/errors/401.pug
new file mode 100644
index 0000000..66ac1ca
--- /dev/null
+++ b/dashboard/views/errors/401.pug
@@ -0,0 +1,12 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+
+ link(rel='stylesheet', href='/css/index.css')
+ body
+ .jumbotron.text-center.bg-transparent
+ h1.display-3 401
+ p.lead This planet is heavily guarded.
+ i.fas.fa-exclamation.pl-2
+ a.btn.btn-dark(href='/') Return Home
\ No newline at end of file
diff --git a/dashboard/views/errors/404.pug b/dashboard/views/errors/404.pug
new file mode 100644
index 0000000..5d24a1d
--- /dev/null
+++ b/dashboard/views/errors/404.pug
@@ -0,0 +1,12 @@
+doctype
+html(lang='en')
+ head
+ include ../includes/header.pug
+
+ link(rel='stylesheet', href='/css/index.css')
+ body
+ .jumbotron.text-center.bg-transparent
+ h1.display-3 404
+ p.lead You are lost on the wrong planet.
+ i.fas.fa-question.pl-2
+ a.btn.btn-dark(href='/') Return Home
\ No newline at end of file
diff --git a/dashboard/views/includes/header.pug b/dashboard/views/includes/header.pug
new file mode 100644
index 0000000..083d9c4
--- /dev/null
+++ b/dashboard/views/includes/header.pug
@@ -0,0 +1,17 @@
+meta(charset="UTF-8")
+meta(name="viewport", content="width=device-width, initial-scale=1.0")
+title 1PG - #{subtitle || 'Best Discord Bot'}
+
+script(src='https://code.jquery.com/jquery-3.5.1.slim.min.js', integrity='sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj', crossorigin='anonymous', defer)
+script(src='https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js', integrity='sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN', crossorigin='anonymous', defer)
+script(src='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js', integrity='sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV', crossorigin='anonymous', defer)
+
+script(src='/js/theme.js', defer)
+script(src='/js/main.js', defer)
+
+link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css', integrity='sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z', crossorigin='anonymous')
+link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css')
+
+link(rel='stylesheet', href='/css/theme.css')
+link(rel='stylesheet', href='/css/main.css')
+link(rel='stylesheet', href='/css/utils.css')
\ No newline at end of file
diff --git a/dashboard/views/includes/mixins.pug b/dashboard/views/includes/mixins.pug
new file mode 100644
index 0000000..26ed85f
--- /dev/null
+++ b/dashboard/views/includes/mixins.pug
@@ -0,0 +1,10 @@
+mixin category(category)
+ li.list-group-item(id=category.name) #[i(class=category.icon)] #{category.name}
+
+mixin command(command)
+ li.list-group-item(id=command.name + 'Command', class=command.category) #{command.name}
+
+mixin user(user)
+ span(title=user.id)
+ img.user-avatar.round.mr-2(src=user.displayAvatarURL({ dynamic: true }))
+ h5.d-inline #{user.username}
\ No newline at end of file
diff --git a/dashboard/views/includes/navbar.pug b/dashboard/views/includes/navbar.pug
new file mode 100644
index 0000000..ee989f6
--- /dev/null
+++ b/dashboard/views/includes/navbar.pug
@@ -0,0 +1,23 @@
+nav.navbar.navbar-expand-lg
+ a.navbar-brand(href='/') 1PG
+ button.navbar-toggler(type='button', data-toggle='collapse', data-target='#navbar', aria-controls='navbar', aria-expanded='false', aria-label='Toggle navigation')
+ span.navbar-toggler-icon
+ #navbar.collapse.navbar-collapse
+ .navbar-nav
+ a.nav-link(href='/commands')
+ i.fas.fa-code
+ span.pl-1 Commands
+ .navbar-nav.ml-auto
+ if user
+ .dropdown
+ .dropdown-toggle(type='button', data-toggle='dropdown', aria-haspopup='true', aria-expanded='false')
+ img.user-avatar.round(src=user.avatarUrl(32))
+ span.pl-2 #{user.username}
+ .dropdown-menu.dropdown-menu-right
+ a.dropdown-item(href='/dashboard') #[i.fas.fa-cogs.text-muted] Dashboard
+ hr
+ a.dropdown-item(href='/logout') #[i.fas.fa-user-slash.text-muted] Logout
+ else
+ a.nav-link(href='/login')
+ i.fas.fa-sign-in-alt
+ span.pl-1 Login
\ No newline at end of file
diff --git a/dashboard/views/includes/sidebar.pug b/dashboard/views/includes/sidebar.pug
new file mode 100644
index 0000000..32129ae
--- /dev/null
+++ b/dashboard/views/includes/sidebar.pug
@@ -0,0 +1,18 @@
+#sidebar.float-left
+ a(href='/dashboard')
+ .icon.round.shadow
+ img.round(alt=user.username, src=user.avatarUrl(64))
+ hr
+ each guild in guilds
+ a(href='/servers/' + guild.id)
+ .icon.round.shadow.my-2(data-toggle='tooltip', data-placement='right', title=guild.name)
+ if guild.icon
+ img.round(alt=guild.name, src=guild.iconURL({ dynamic: true, size: 64 }))
+ else
+ p.text-center.abbr #{guild.nameAcronym}
+ a(href='/invite')
+ .icon
+ p.abbr.text-success.text-center
+ i.fas.fa-plus
+ .hamburger.px-1
+ i.fas.fa-bars
\ No newline at end of file
diff --git a/dashboard/views/index.pug b/dashboard/views/index.pug
new file mode 100644
index 0000000..0696840
--- /dev/null
+++ b/dashboard/views/index.pug
@@ -0,0 +1,13 @@
+doctype
+html(lang='en')
+ head
+ include includes/header.pug
+
+ link(rel='stylesheet', href='/css/index.css')
+ body
+ include includes/navbar.pug
+ .jumbotron.text-center.bg-transparent
+ h1.display-3 1PG
+ p.lead The best bot in the known universe and milky way.
+ i.fas.fa-star.pl-2
+ a.btn.btn-dark(href='/invite') Add
\ No newline at end of file