Bläddra i källkod

Merge branch 'polishing' of github.com:Musare/MusareNode into polishing

Jonathan 4 år sedan
förälder
incheckning
713aec6a00

+ 0 - 4
README.md

@@ -52,11 +52,7 @@ We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running
     | `apis.recaptcha.secret`       | Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin). |
     | `apis.recaptcha.enabled`       | Keep at false to keep disabled. |
     | `apis.github` | Can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers). You need to fill in some values to create the OAuth application. The homepage is the homepage of frontend. The authorization callback url is the backend url with `/auth/github/authorize/callback` added at the end. For example `http://localhost:8080/auth/github/authorize/callback`. |
-    | `apis.discord.token` | Token for the Discord bot. |
-    | `apis.discord.loggingServer`  | Server ID of the Discord logging server. |
-    | `apis.discord.loggingChannel` | ID of the channel to be used in the Discord logging server. |
     | `apis.mailgun` | Can be obtained by setting up a [Mailgun account](http://www.mailgun.com/), or you can disable it. |
-    | `apis.spotify` | Can be obtained by setting up a [Spotify client id](https://developer.spotify.com/dashboard/applications), or you can disable it. |
     | `apis.discogs` | Can be obtained by setting up a [Discogs application](https://www.discogs.com/settings/developers), or you can disable it. |
     | `redis.url` | Should be left alone for Docker, and changed to `redis://localhost:6379/0` for non-Docker. |
     | `redis.password` | Should be the Redis password you either put in your `startRedis.cmd` file for Windows, or `.env` for docker. |

+ 0 - 10
backend/config/template.json

@@ -21,21 +21,11 @@
 			"secret": "",
 			"redirect_uri": ""
 		},
-		"discord": {
-			"token": "",
-			"loggingChannel": "",
-			"loggingServer": ""
-		},
 		"mailgun": {
 			"key": "",
 			"domain": "",
 			"enabled": false
 		},
-		"spotify": {
-			"client": "",
-			"secret": "",
-			"enabled": false
-		},
 		"discogs": {
 			"client": "",
 			"secret": "",

+ 372 - 79
backend/core.js

@@ -1,4 +1,3 @@
-import async from "async";
 import config from "config";
 
 class DeferredPromise {
@@ -11,6 +10,234 @@ class DeferredPromise {
 	}
 }
 
+class QueueTask {
+	// eslint-disable-next-line require-jsdoc
+	constructor(job, priority) {
+		this.job = job;
+		this.priority = priority;
+		this.job.setTask(this);
+	}
+}
+
+class Queue {
+	// eslint-disable-next-line require-jsdoc
+	constructor(handleTaskFunction, concurrency) {
+		this.handleTaskFunction = handleTaskFunction;
+		this.concurrency = concurrency;
+		this.queue = [];
+		this.runningTasks = [];
+		this.pausedTasks = [];
+		this.paused = false;
+	}
+
+	/**
+	 * Pauses the queue, meaning no new jobs can be started. Jobs can still be added to the queue, and already running tasks won't be paused.
+	 */
+	pause() {
+		this.paused = true;
+	}
+
+	/**
+	 * Resumes the queue.
+	 */
+	resume() {
+		this.paused = false;
+		setTimeout(() => {
+			this._handleQueue();
+		}, 0);
+	}
+
+	/**
+	 * Returns the amount of jobs in the queue.
+	 *
+	 * @returns {number} - amount of jobs in queue
+	 */
+	lengthQueue() {
+		return this.queue.length;
+	}
+
+	/**
+	 * Returns the amount of running jobs.
+	 *
+	 * @returns {number} - amount of running jobs
+	 */
+	lengthRunning() {
+		return this.runningTasks.length;
+	}
+
+	/**
+	 * Returns the amount of running jobs.
+	 *
+	 * @returns {number} - amount of running jobs
+	 */
+	lengthPaused() {
+		return this.pausedTasks.length;
+	}
+
+	/**
+	 * Adds a job to the queue, with a given priority.
+	 *
+	 * @param {object} job - the job that is to be added
+	 * @param {number} priority - the priority of the to be added job
+	 */
+	push(job, priority) {
+		this.queue.push(new QueueTask(job, priority));
+		setTimeout(() => {
+			this._handleQueue();
+		}, 0);
+	}
+
+	/**
+	 * Removes a job currently running from the queue.
+	 *
+	 * @param {object} job - the job to be removed
+	 */
+	removeRunningJob(job) {
+		this.runningTasks.remove(this.runningTasks.find(task => task.job.toString() === job.toString()));
+	}
+
+	/**
+	 * Pauses a job currently running from the queue.
+	 *
+	 * @param {object} job - the job to be pauses
+	 */
+	pauseRunningJob(job) {
+		const task = this.runningTasks.find(task => task.job.toString() === job.toString());
+		if (!task) {
+			console.log(
+				`Attempted to pause job ${job.name} (${job.toString()}), but couldn't find it in running tasks.`
+			);
+			return;
+		}
+		this.runningTasks.remove(task);
+		this.pausedTasks.push(task);
+	}
+
+	/**
+	 * Resumes a job currently paused, adding the job back to the front of the queue
+	 *
+	 * @param {object} job - the job to be pauses
+	 */
+	resumeRunningJob(job) {
+		const task = this.pausedTasks.find(task => task.job.toString() === job.toString());
+		if (!task) {
+			console.log(
+				`Attempted to resume job ${job.name} (${job.toString()}), but couldn't find it in paused tasks.`
+			);
+			return;
+		}
+		this.pausedTasks.remove(task);
+		this.queue.unshift(task);
+		setTimeout(() => {
+			this._handleQueue();
+		}, 0);
+	}
+
+	/**
+	 * Check if there's room for a job to be processed, and if there is, run it.
+	 */
+	_handleQueue() {
+		if (this.queue.length > 0) {
+			const task = this.queue.reduce((a, b) => (a.priority < b.priority ? a : b));
+			// console.log(`First task: `, task);
+			if (task) {
+				if ((!this.paused && this.runningTasks.length < this.concurrency) || task.priority === -1) {
+					this.queue.remove(task);
+					this.runningTasks.push(task);
+					this._handleTask(task);
+					setTimeout(() => {
+						this._handleQueue();
+					}, 0);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Handles a task, calling the handleTaskFunction provided in the constructor
+	 *
+	 * @param {object} task - the task to be handled
+	 */
+	_handleTask(task) {
+		this.handleTaskFunction(task.job).finally(() => {
+			this.runningTasks.remove(task);
+			this._handleQueue();
+		});
+	}
+}
+
+class Job {
+	// eslint-disable-next-line require-jsdoc
+	constructor(name, payload, onFinish, module, parentJob) {
+		this.name = name;
+		this.payload = payload;
+		this.response = null;
+		this.responseType = null;
+		this.onFinish = onFinish;
+		this.module = module;
+		this.parentJob = parentJob;
+		this.childJobs = [];
+		/* eslint-disable no-bitwise, eqeqeq */
+		this.uniqueId = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
+			const r = (Math.random() * 16) | 0;
+			const v = c == "x" ? r : (r & 0x3) | 0x8;
+			return v.toString(16);
+		});
+		this.status = "INITIALIZED";
+		this.task = null;
+	}
+
+	/**
+	 * Adds a child job to this job
+	 *
+	 * @param {object} childJob - the child job
+	 */
+	addChildJob(childJob) {
+		this.childJobs.push(childJob);
+	}
+
+	/**
+	 * Sets the job status
+	 *
+	 * @param {string} status - the new status
+	 */
+	setStatus(status) {
+		// console.log(`Job ${this.toString()} has changed status from ${this.status} to ${status}`);
+		this.status = status;
+	}
+
+	setTask(task) {
+		this.task = task;
+	}
+
+	/**
+	 * Returns the UUID of the job, allowing you to compare jobs with toString
+	 *
+	 * @returns {string} - the job's UUID/uniqueId
+	 */
+	toString() {
+		return this.uniqueId;
+	}
+
+	/**
+	 * Sets the response that will be provided to the onFinish DeferredPromise resolve/reject function, as soon as the job is done if it has no parent, or when the parent job is resumed
+	 *
+	 * @param {object} response - the response
+	 */
+	setResponse(response) {
+		this.response = response;
+	}
+
+	/**
+	 * Sets the response type that is paired with the response. If it is RESOLVE/REJECT, then it will resolve/reject with the response. If it is RESOLVED/REJECTED, then it has already resolved/rejected with the response.
+	 *
+	 * @param {string} responseType - the response type, so RESOLVE/REJECT/RESOLVED/REJECTED
+	 */
+	setResponseType(responseType) {
+		this.responseType = responseType;
+	}
+}
+
 class MovingAverageCalculator {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
@@ -53,10 +280,7 @@ export default class CoreClass {
 		this.name = name;
 		this.status = "UNINITIALIZED";
 		// this.log("Core constructor");
-		this.jobQueue = async.priorityQueue(
-			({ job, options }, callback) => this._runJob(job, options, callback),
-			10 // How many jobs can run concurrently
-		);
+		this.jobQueue = new Queue(job => this._runJob(job), 10);
 		this.jobQueue.pause();
 		this.runningJobs = [];
 		this.priorities = {};
@@ -181,14 +405,50 @@ export default class CoreClass {
 	 *
 	 * @param {string} name - the name of the job e.g. GET_PLAYLIST
 	 * @param {object} payload - any expected payload for the job itself
-	 * @param {object} options - object containing any additional options for the job
-	 * @param {boolean} options.isQuiet - whether or not the job should be advertised in the logs, useful for repetitive/unimportant jobs
-	 * @param {boolean} options.bypassQueue - UNKNOWN
+	 * @param {object} parentJob - the parent job, if any
+	 * @param {number} priority - custom priority. Optional.
 	 * @returns {Promise} - returns a promise
 	 */
-	runJob(name, payload, options = { isQuiet: false, bypassQueue: false }) {
+	runJob(name, payload, parentJob, priority) {
 		const deferredPromise = new DeferredPromise();
-		const job = { name, payload, onFinish: deferredPromise };
+		const job = new Job(name, payload, deferredPromise, this, parentJob);
+		this.log("INFO", `Queuing job ${name} (${job.toString()})`);
+		if (parentJob) {
+			parentJob.addChildJob(job);
+			if (parentJob.status === "RUNNING") {
+				this.log(
+					"INFO",
+					`Pausing job ${parentJob.name} (${parentJob.toString()}) since a child job has to run first`
+				);
+				parentJob.setStatus("WAITING_ON_CHILD_JOB");
+				parentJob.module.jobQueue.pauseRunningJob(parentJob);
+				// console.log(111, parentJob.module.jobQueue.length());
+				// console.log(
+				// 	222,
+				// 	parentJob.module.jobQueue.workersList().map(data => data.data.job)
+				// );
+			} else {
+				this.log(
+					"INFO",
+					`Not pausing job ${parentJob.name} (${parentJob.toString()}) since it's already paused`
+				);
+			}
+		}
+
+		// console.log(this);
+
+		// console.log(321, parentJob);
+
+		job.setStatus("QUEUED");
+
+		// if (options.bypassQueue) this._runJob(job, options, () => {});
+		// else {
+		const _priority = Math.min(
+			priority || Infinity,
+			parentJob ? parentJob.task.priority : Infinity,
+			this.priorities[name] ? this.priorities[name] : 10
+		);
+		this.jobQueue.push(job, _priority);
 
 		if (
 			config.debug &&
@@ -196,14 +456,10 @@ export default class CoreClass {
 			config.debug.captureJobs &&
 			config.debug.captureJobs.indexOf(name) !== -1
 		) {
-			this.moduleManager.debugJobs.all.push(job);
+			this.moduleManager.debugJobs.all.push({ job, _priority });
 		}
 
-		if (options.bypassQueue) this._runJob(job, options, () => { });
-		else {
-			const priority = this.priorities[name] ? this.priorities[name] : 10;
-			this.jobQueue.push({ job, options }, priority);
-		}
+		// }
 
 		return deferredPromise.promise;
 	}
@@ -224,70 +480,107 @@ export default class CoreClass {
 	 * @param {string} job.name - the name of the job e.g. GET_PLAYLIST
 	 * @param {string} job.payload - any expected payload for the job itself
 	 * @param {Promise} job.onFinish - deferred promise when the job is complete
-	 * @param {object} options - object containing any additional options for the job
-	 * @param {boolean} options.isQuiet - whether or not the job should be advertised in the logs, useful for repetitive/unimportant jobs
-	 * @param {boolean} options.bypassQueue - UNKNOWN
-	 * @param {Function} cb - Callback after the job has completed
+	 * @returns {Promise} - returns a promise
 	 */
-	_runJob(job, options, cb) {
-		if (!options.isQuiet) this.log("INFO", `Running job ${job.name}`);
-
-		const startTime = Date.now();
-
-		this.runningJobs.push(job);
-
-		const newThis = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
-
-		newThis.runJob = (...args) => {
-			if (args.length === 1) args.push({});
-			args[1].bypassQueue = true;
-
-			return this.runJob(...args);
-		};
-
-		this[job.name]
-			.apply(newThis, [job.payload])
-			.then(response => {
-				if (!options.isQuiet) this.log("INFO", `Ran job ${job.name} successfully`);
-				this.jobStatistics[job.name].successful += 1;
-				if (
-					config.debug &&
-					config.debug.stationIssue === true &&
-					config.debug.captureJobs &&
-					config.debug.captureJobs.indexOf(job.name) !== -1
-				) {
-					this.moduleManager.debugJobs.completed.push({
-						status: "success",
-						job,
-						response
+	_runJob(job) {
+		this.log("INFO", `Running job ${job.name} (${job.toString()})`);
+		return new Promise(resolve => {
+			const startTime = Date.now();
+
+			const previousStatus = job.status;
+			job.setStatus("RUNNING");
+			this.runningJobs.push(job);
+
+			if (previousStatus === "QUEUED") {
+				this.log("INFO", `Job ${job.name} (${job.toString()}) is queued, so calling it`);
+				this[job.name]
+					.apply(job, [job.payload])
+					.then(response => {
+						// if (!options.isQuiet)
+						this.log("INFO", `Ran job ${job.name} (${job.toString()}) successfully`);
+						job.setStatus("FINISHED");
+						job.setResponse(response);
+						this.jobStatistics[job.name].successful += 1;
+						job.setResponseType("RESOLVE");
+						if (
+							config.debug &&
+							config.debug.stationIssue === true &&
+							config.debug.captureJobs &&
+							config.debug.captureJobs.indexOf(job.name) !== -1
+						) {
+							this.moduleManager.debugJobs.completed.push({
+								status: "success",
+								job,
+								priority: job.task.priority,
+								response
+							});
+						}
+						// job.onFinish.resolve(response);
+					})
+					.catch(error => {
+						this.log("INFO", `Running job ${job.name} (${job.toString()}) failed`);
+						job.setStatus("FINISHED");
+						job.setResponse(error);
+						job.setResponseType("REJECT");
+						this.jobStatistics[job.name].failed += 1;
+						if (
+							config.debug &&
+							config.debug.stationIssue === true &&
+							config.debug.captureJobs &&
+							config.debug.captureJobs.indexOf(job.name) !== -1
+						) {
+							this.moduleManager.debugJobs.completed.push({
+								status: "error",
+								job,
+								error
+							});
+						}
+						// job.onFinish.reject(error);
+					})
+					.finally(() => {
+						const endTime = Date.now();
+						const executionTime = endTime - startTime;
+						this.jobStatistics[job.name].total += 1;
+						this.jobStatistics[job.name].averageTiming.update(executionTime);
+						this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
+						if (!job.parentJob) {
+							if (job.responseType === "RESOLVE") {
+								job.onFinish.resolve(job.response);
+								job.responseType = "RESOLVED";
+							} else if (job.responseType === "REJECT") {
+								job.onFinish.reject(job.response);
+								job.responseType = "REJECTED";
+							}
+						} else if (
+							job.parentJob &&
+							job.parentJob.childJobs.find(childJob => childJob.status !== "FINISHED") === undefined
+						) {
+							this.log(
+								"INFO",
+								`Requeing/resuming job ${
+									job.parentJob.name
+								} (${job.parentJob.toString()}) since all child jobs are complete.`
+							);
+							job.parentJob.setStatus("REQUEUED");
+							job.parentJob.module.jobQueue.resumeRunningJob(job.parentJob);
+						}
+						resolve();
 					});
-				}
-				job.onFinish.resolve(response);
-			})
-			.catch(error => {
-				this.log("INFO", `Running job ${job.name} failed`);
-				this.jobStatistics[job.name].failed += 1;
-				if (
-					config.debug &&
-					config.debug.stationIssue === true &&
-					config.debug.captureJobs &&
-					config.debug.captureJobs.indexOf(job.name) !== -1
-				) {
-					this.moduleManager.debugJobs.completed.push({
-						status: "error",
-						job,
-						error
-					});
-				}
-				job.onFinish.reject(error);
-			})
-			.finally(() => {
-				const endTime = Date.now();
-				const executionTime = endTime - startTime;
-				this.jobStatistics[job.name].total += 1;
-				this.jobStatistics[job.name].averageTiming.update(executionTime);
-				this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
-				cb();
-			});
+			} else {
+				this.log(
+					"INFO",
+					`Job ${job.name} (${job.toString()}) is re-queued, so resolving/rejecting all child jobs.`
+				);
+				job.childJobs.forEach(childJob => {
+					if (childJob.responseType === "RESOLVE") {
+						childJob.onFinish.resolve(childJob.response);
+						childJob.responseType = "RESOLVED";
+					} else if (childJob.responseType === "REJECT") {
+						childJob.onFinish.reject(childJob.response);
+						childJob.responseType = "REJECTED";
+					}
+				});
+			}
+		});
 	}
 }

+ 62 - 30
backend/index.js

@@ -3,6 +3,11 @@ import "./loadEnvVariables.js";
 import util from "util";
 import config from "config";
 
+// eslint-disable-next-line no-extend-native
+Array.prototype.remove = function (item) {
+	this.splice(this.indexOf(item), 1);
+};
+
 process.on("uncaughtException", err => {
 	if (err.code === "ECONNREFUSED" || err.code === "UNCERTAIN_STATE") return;
 	console.log(`UNCAUGHT EXCEPTION: ${err.stack}`);
@@ -147,12 +152,10 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 
 // 	allModulesInitialized() {
 // 		this.logger.success("MODULE_MANAGER", "All modules have started!");
-// 		this.modules["discord"].sendAdminAlertMessage("The backend server started successfully.", "#00AA00", "Startup", false, []);
 // 	}
 
 // 	aModuleFailed(failedModule) {
 // 		this.logger.error("MODULE_MANAGER", `A module has failed, locking down. Module: ${failedModule.name}`);
-// 		this.modules["discord"].sendAdminAlertMessage(`The backend server failed to start due to a failing module: ${failedModule.name}.`, "#AA0000", "Startup", false, []);
 
 // 		this._lockdown();
 // 	}
@@ -200,7 +203,6 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 // moduleManager.addModule("mail");
 // moduleManager.addModule("api");
 // moduleManager.addModule("app");
-// moduleManager.addModule("discord");
 // moduleManager.addModule("io");
 // moduleManager.addModule("logger");
 // moduleManager.addModule("notifications");
@@ -208,7 +210,6 @@ if (config.debug && config.debug.traceUnhandledPromises === true) {
 // moduleManager.addModule("playlists");
 // moduleManager.addModule("punishments");
 // moduleManager.addModule("songs");
-// moduleManager.addModule("spotify");
 // moduleManager.addModule("stations");
 // moduleManager.addModule("tasks");
 // moduleManager.addModule("utils");
@@ -245,6 +246,7 @@ class ModuleManager {
 			all: [],
 			completed: []
 		};
+		this.name = "MODULE_MANAGER";
 	}
 
 	/**
@@ -253,7 +255,7 @@ class ModuleManager {
 	 * @param {string} moduleName - the name of the module (also needs to be the same as the filename of a module located in the logic folder or "logic/moduleName/index.js")
 	 */
 	async addModule(moduleName) {
-		console.log("add module", moduleName);
+		this.log("INFO", "Adding module", moduleName);
 		// import(`./logic/${moduleName}`).then(Module => {
 		// 	// eslint-disable-next-line new-cap
 
@@ -285,12 +287,12 @@ class ModuleManager {
 			}
 		}); // ensures all modules are imported, then converts promise to the default export of the import
 
-		for (let moduleId = 0, moduleNames = Object.keys(this.modules); moduleId < moduleNames.length; moduleId += 1) {
-			const module = this.modules[moduleNames[moduleId]];
+		Object.keys(this.modules).every(moduleKey => {
+			const module = this.modules[moduleKey];
 
 			module.setModuleManager(this);
 
-			if (this.lockdown) break;
+			if (this.lockdown) return false;
 
 			module._initialize();
 
@@ -308,7 +310,8 @@ class ModuleManager {
 			// 	this.logger.info("MODULE_MANAGER", `${moduleName} dependencies have been completed`);
 			// 	module._initialize();
 			// });
-		}
+			return true;
+		});
 	}
 
 	/**
@@ -320,8 +323,8 @@ class ModuleManager {
 		if (this.modulesNotInitialized.indexOf(module) !== -1) {
 			this.modulesNotInitialized.splice(this.modulesNotInitialized.indexOf(module), 1);
 
-			console.log(
-				"MODULE_MANAGER",
+			this.log(
+				"INFO",
 				`Initialized: ${Object.keys(this.modules).length - this.modulesNotInitialized.length}/${
 					Object.keys(this.modules).length
 				}.`
@@ -338,7 +341,7 @@ class ModuleManager {
 	 */
 	onFail(module) {
 		if (this.modulesNotInitialized.indexOf(module) !== -1) {
-			console.log("A module failed to initialize!");
+			this.log("ERROR", "A module failed to initialize!");
 		}
 	}
 
@@ -347,14 +350,32 @@ class ModuleManager {
 	 *
 	 */
 	onAllModulesInitialized() {
-		console.log("All modules initialized!");
-		this.modules.discord.runJob("SEND_ADMIN_ALERT_MESSAGE", {
-			message: "The backend server started successfully.",
-			color: "#00AA00",
-			type: "Startup",
-			critical: false,
-			extraFields: []
-		});
+		this.log("INFO", "All modules initialized!");
+	}
+
+	/**
+	 * Creates a new log message
+	 *
+	 * @param {...any} args - anything to be included in the log message, the first argument is the type of log
+	 */
+	log(...args) {
+		const _arguments = Array.from(args);
+		const type = _arguments[0];
+
+		_arguments.splice(0, 1);
+		const start = `|${this.name.toUpperCase()}|`;
+		const numberOfSpacesNeeded = 20 - start.length;
+		_arguments.unshift(`${start}${Array(numberOfSpacesNeeded).join(" ")}`);
+
+		if (type === "INFO") {
+			_arguments[0] += "\x1b[36m";
+			_arguments.push("\x1b[0m");
+			console.log.apply(null, _arguments);
+		} else if (type === "ERROR") {
+			_arguments[0] += "\x1b[31m";
+			_arguments.push("\x1b[0m");
+			console.error.apply(null, _arguments);
+		}
 	}
 }
 
@@ -366,13 +387,11 @@ moduleManager.addModule("mail");
 moduleManager.addModule("activities");
 moduleManager.addModule("api");
 moduleManager.addModule("app");
-moduleManager.addModule("discord");
 moduleManager.addModule("io");
 moduleManager.addModule("notifications");
 moduleManager.addModule("playlists");
 moduleManager.addModule("punishments");
 moduleManager.addModule("songs");
-moduleManager.addModule("spotify");
 moduleManager.addModule("stations");
 moduleManager.addModule("tasks");
 moduleManager.addModule("utils");
@@ -388,27 +407,33 @@ process.stdin.on("data", data => {
 	if (command === "status") {
 		console.log("Status:");
 
-		for (
-			let moduleName = 0, moduleKeys = Object.keys(moduleManager.modules);
-			moduleName < moduleKeys.length;
-			moduleName += 1
-		) {
+		Object.keys(moduleManager.modules).forEach(moduleName => {
 			const module = moduleManager.modules[moduleName];
 			const tabsNeeded = 4 - Math.ceil((moduleName.length + 1) / 8);
 			console.log(
 				`${moduleName.toUpperCase()}${Array(tabsNeeded).join(
 					"\t"
-				)}${module.getStatus()}. Jobs in queue: ${module.jobQueue.length()}. Jobs in progress: ${module.jobQueue.running()}. Concurrency: ${
+				)}${module.getStatus()}. Jobs in queue: ${module.jobQueue.lengthQueue()}. Jobs in progress: ${module.jobQueue.lengthRunning()}. Jobs paused: ${module.jobQueue.lengthPaused()} Concurrency: ${
 					module.jobQueue.concurrency
 				}. Stage: ${module.getStage()}`
 			);
-		}
+		});
 		// moduleManager._lockdown();
 	}
 	if (command.startsWith("running")) {
 		const parts = command.split(" ");
 
-		console.log(moduleManager.modules[parts[1]].runningJobs);
+		console.log(moduleManager.modules[parts[1]].jobQueue.runningTasks);
+	}
+	if (command.startsWith("queued")) {
+		const parts = command.split(" ");
+
+		console.log(moduleManager.modules[parts[1]].jobQueue.queue);
+	}
+	if (command.startsWith("paused")) {
+		const parts = command.split(" ");
+
+		console.log(moduleManager.modules[parts[1]].jobQueue.pausedTasks);
 	}
 	if (command.startsWith("stats")) {
 		const parts = command.split(" ");
@@ -418,6 +443,13 @@ process.stdin.on("data", data => {
 	if (command.startsWith("debug")) {
 		moduleManager.modules.utils.runJob("DEBUG");
 	}
+
+	if (command.startsWith("eval")) {
+		const evalCommand = command.replace("eval ", "");
+		console.log(`Running eval command: ${evalCommand}`);
+		const response = eval(evalCommand);
+		console.log(`Eval response: `, response);
+	}
 });
 
 export default moduleManager;

+ 9 - 35
backend/logic/actions/apis.js

@@ -56,37 +56,6 @@ export default {
 		);
 	},
 
-	/**
-	 * Gets Spotify data
-	 *
-	 * @param session
-	 * @param title - the title of the song
-	 * @param artist - an artist for that song
-	 * @param cb
-	 */
-	getSpotifySongs: isAdminRequired((session, title, artist, cb) => {
-		async.waterfall(
-			[
-				next => {
-					utils
-						.runJob("GET_SONGS_FROM_SPOTIFY", { title, artist })
-						.then(songs => {
-							next(null, songs);
-						})
-						.catch(next);
-				}
-			],
-			songs => {
-				console.log(
-					"SUCCESS",
-					"APIS_GET_SPOTIFY_SONGS",
-					`User "${session.userId}" got Spotify songs for title "${title}" successfully.`
-				);
-				cb({ status: "success", songs });
-			}
-		);
-	}),
-
 	/**
 	 * Gets Discogs data
 	 *
@@ -151,10 +120,15 @@ export default {
 	 */
 	joinRoom: (session, page, cb) => {
 		if (page === "home") {
-			utils.runJob("SOCKET_JOIN_ROOM", {
-				socketId: session.socketId,
-				room: page
-			});
+			utils
+				.runJob("SOCKET_JOIN_ROOM", {
+					socketId: session.socketId,
+					room: page
+				})
+				.then()
+				.catch(err => {
+					console.log("ERROR", `Joining room failed: ${err.message}`);
+				});
 		}
 		cb({});
 	},

+ 1 - 1
backend/logic/actions/hooks/loginRequired.js

@@ -29,7 +29,7 @@ export default destination => (session, ...args) => {
 				console.log("LOGIN_REQUIRED", `User failed to pass login required check.`);
 				return cb({ status: "failure", message: err });
 			}
-			console.log("LOGIN_REQUIRED", `User "${session.userId}" passed login required check.`, false);
+			console.log("LOGIN_REQUIRED", `User "${session.userId}" passed login required check.`);
 			return destination(session, ...args);
 		}
 	);

+ 2 - 8
backend/logic/actions/queueSongs.js

@@ -137,9 +137,9 @@ const lib = {
 					let updated = false;
 
 					const $set = {};
-					for (let prop = 0, songKeys = Object.keys(updatedSong); prop < songKeys.length; prop += 1) {
+					Object.keys(updatedSong).forEach(prop => {
 						if (updatedSong[prop] !== song[prop]) $set[prop] = updatedSong[prop];
-					}
+					});
 
 					updated = true;
 					if (!updated) return next("No properties changed");
@@ -261,12 +261,6 @@ const lib = {
 						})
 						.catch(next);
 				},
-				/* (newSong, next) => {
-				utils.getSongFromSpotify(newSong, (err, song) => {
-					if (!song) next(null, newSong);
-					else next(err, song);
-				});
-			}, */
 				(newSong, next) => {
 					const song = new QueueSongModel(newSong);
 					song.save({ validateBeforeSave: false }, (err, song) => {

+ 112 - 123
backend/logic/actions/stations.js

@@ -1,5 +1,4 @@
 import async from "async";
-import underscore from "underscore";
 
 import { isLoginRequired, isOwnerRequired } from "./hooks";
 import db from "../db";
@@ -11,125 +10,115 @@ import notifications from "../notifications";
 import stations from "../stations";
 import activities from "../activities";
 
-const { _ } = underscore;
-
 // const logger = moduleManager.modules["logger"];
 
 const userList = {};
-let usersPerStation = {};
-let usersPerStationCount = {};
+const usersPerStation = {};
+const usersPerStationCount = {};
 
 // Temporarily disabled until the messages in console can be limited
-setInterval(async () => {
-	const stationsCountUpdated = [];
-	const stationsUpdated = [];
-
-	const oldUsersPerStation = usersPerStation;
-	usersPerStation = {};
-
-	const oldUsersPerStationCount = usersPerStationCount;
-	usersPerStationCount = {};
-
-	const userModel = await db.runJob("GET_MODEL", {
-		modelName: "user"
-	});
-
-	async.each(
-		Object.keys(userList),
-		(socketId, next) => {
-			utils.runJob("SOCKET_FROM_SESSION", { socketId }, { isQuiet: true }).then(socket => {
-				const stationId = userList[socketId];
-				if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
-					if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
-					if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
-					delete userList[socketId];
-					return next();
-				}
-				if (!usersPerStationCount[stationId]) usersPerStationCount[stationId] = 0;
-				usersPerStationCount[stationId] += 1;
-				if (!usersPerStation[stationId]) usersPerStation[stationId] = [];
-
-				return async.waterfall(
-					[
-						next => {
-							if (!socket.session || !socket.session.sessionId) return next("No session found.");
-							return cache
-								.runJob("HGET", {
-									table: "sessions",
-									key: socket.session.sessionId
-								})
-								.then(session => {
-									next(null, session);
-								})
-								.catch(next);
-						},
-
-						(session, next) => {
-							if (!session) return next("Session not found.");
-							return userModel.findOne({ _id: session.userId }, next);
-						},
-
-						(user, next) => {
-							if (!user) return next("User not found.");
-							if (usersPerStation[stationId].indexOf(user.username) !== -1)
-								return next("User already in the list.");
-							return next(null, user.username);
-						}
-					],
-					(err, username) => {
-						if (!err) {
-							usersPerStation[stationId].push(username);
-						}
-						next();
-					}
-				);
-			});
-			// TODO Code to show users
-		},
-		() => {
-			for (
-				let stationId = 0, stationKeys = Object.keys(usersPerStationCount);
-				stationId < stationKeys.length;
-				stationId += 1
-			) {
-				if (oldUsersPerStationCount[stationId] !== usersPerStationCount[stationId]) {
-					if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
-				}
-			}
-
-			for (
-				let stationId = 0, stationKeys = Object.keys(usersPerStation);
-				stationId < stationKeys.length;
-				stationId += 1
-			) {
-				if (
-					_.difference(usersPerStation[stationId], oldUsersPerStation[stationId]).length > 0 ||
-					_.difference(oldUsersPerStation[stationId], usersPerStation[stationId]).length > 0
-				) {
-					if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
-				}
-			}
-
-			stationsCountUpdated.forEach(stationId => {
-				console.log("INFO", "UPDATE_STATION_USER_COUNT", `Updating user count of ${stationId}.`);
-				cache.runJob("PUB", {
-					table: "station.updateUserCount",
-					value: stationId
-				});
-			});
-
-			stationsUpdated.forEach(stationId => {
-				console.log("INFO", "UPDATE_STATION_USER_LIST", `Updating user list of ${stationId}.`);
-				cache.runJob("PUB", {
-					table: "station.updateUsers",
-					value: stationId
-				});
-			});
-
-			// console.log("Userlist", usersPerStation);
-		}
-	);
-}, 3000);
+// setInterval(async () => {
+// 	const stationsCountUpdated = [];
+// 	const stationsUpdated = [];
+
+// 	const oldUsersPerStation = usersPerStation;
+// 	usersPerStation = {};
+
+// 	const oldUsersPerStationCount = usersPerStationCount;
+// 	usersPerStationCount = {};
+
+// 	const userModel = await db.runJob("GET_MODEL", {
+// 		modelName: "user"
+// 	});
+
+// 	async.each(
+// 		Object.keys(userList),
+// 		(socketId, next) => {
+// 			utils.runJob("SOCKET_FROM_SESSION", { socketId }, { isQuiet: true }).then(socket => {
+// 				const stationId = userList[socketId];
+// 				if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
+// 					if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
+// 					if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
+// 					delete userList[socketId];
+// 					return next();
+// 				}
+// 				if (!usersPerStationCount[stationId]) usersPerStationCount[stationId] = 0;
+// 				usersPerStationCount[stationId] += 1;
+// 				if (!usersPerStation[stationId]) usersPerStation[stationId] = [];
+
+// 				return async.waterfall(
+// 					[
+// 						next => {
+// 							if (!socket.session || !socket.session.sessionId) return next("No session found.");
+// 							return cache
+// 								.runJob("HGET", {
+// 									table: "sessions",
+// 									key: socket.session.sessionId
+// 								})
+// 								.then(session => {
+// 									next(null, session);
+// 								})
+// 								.catch(next);
+// 						},
+
+// 						(session, next) => {
+// 							if (!session) return next("Session not found.");
+// 							return userModel.findOne({ _id: session.userId }, next);
+// 						},
+
+// 						(user, next) => {
+// 							if (!user) return next("User not found.");
+// 							if (usersPerStation[stationId].indexOf(user.username) !== -1)
+// 								return next("User already in the list.");
+// 							return next(null, user.username);
+// 						}
+// 					],
+// 					(err, username) => {
+// 						if (!err) {
+// 							usersPerStation[stationId].push(username);
+// 						}
+// 						next();
+// 					}
+// 				);
+// 			});
+// 			// TODO Code to show users
+// 		},
+// 		() => {
+//			Object.keys(usersPerStationCount).forEach(stationId => {
+//				if (oldUsersPerStationCount[stationId] !== usersPerStationCount[stationId]) {
+//					if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
+//				}
+//			})
+//
+//			Object.keys(usersPerStation).forEach(stationId => {
+//				if (
+//					_.difference(usersPerStation[stationId], oldUsersPerStation[stationId]).length > 0 ||
+//					_.difference(oldUsersPerStation[stationId], usersPerStation[stationId]).length > 0
+//				) {
+//					if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
+//				}
+//			});
+//
+// 			stationsCountUpdated.forEach(stationId => {
+// 				console.log("INFO", "UPDATE_STATION_USER_COUNT", `Updating user count of ${stationId}.`);
+// 				cache.runJob("PUB", {
+// 					table: "station.updateUserCount",
+// 					value: stationId
+// 				});
+// 			});
+
+// 			stationsUpdated.forEach(stationId => {
+// 				console.log("INFO", "UPDATE_STATION_USER_LIST", `Updating user list of ${stationId}.`);
+// 				cache.runJob("PUB", {
+// 					table: "station.updateUsers",
+// 					value: stationId
+// 				});
+// 			});
+
+// 			// console.log("Userlist", usersPerStation);
+// 		}
+// 	);
+// }, 3000);
 
 cache.runJob("SUB", {
 	channel: "station.updateUsers",
@@ -160,9 +149,9 @@ cache.runJob("SUB", {
 				const sockets = await utils.runJob("GET_ROOM_SOCKETS", {
 					room: "home"
 				});
-				for (let socketId = 0, socketKeys = Object.keys(sockets); socketId < socketKeys.length; socketId += 1) {
-					const socket = sockets[socketKeys[socketId]];
-					const { session } = sockets[socketKeys[socketId]];
+				Object.keys(sockets).forEach(socketKey => {
+					const socket = sockets[socketKey];
+					const { session } = socket;
 					if (session.sessionId) {
 						cache
 							.runJob("HGET", {
@@ -183,7 +172,7 @@ cache.runJob("SUB", {
 									);
 							});
 					}
-				}
+				});
 			}
 		});
 	}
@@ -301,9 +290,9 @@ cache.runJob("SUB", {
 				const sockets = await utils.runJob("GET_ROOM_SOCKETS", {
 					room: "home"
 				});
-				for (let socketId = 0, socketKeys = Object.keys(sockets); socketId < socketKeys.length; socketId += 1) {
-					const socket = sockets[socketKeys[socketId]];
-					const { session } = sockets[socketKeys[socketId]];
+				Object.keys(sockets).forEach(socketKey => {
+					const socket = sockets[socketKey];
+					const { session } = socket;
 					if (session.sessionId) {
 						cache
 							.runJob("HGET", {
@@ -320,7 +309,7 @@ cache.runJob("SUB", {
 								}
 							});
 					}
-				}
+				});
 			}
 		});
 	}

+ 4 - 2
backend/logic/actions/users.js

@@ -118,7 +118,7 @@ cache.runJob("SUB", {
 	}
 });
 
-export default {
+const thisExport = {
 	/**
 	 * Lists all Users
 	 *
@@ -392,7 +392,7 @@ export default {
 					return cb({ status: "failure", message: err });
 				}
 
-				return module.exports.login(session, email, password, result => {
+				return thisExport.login(session, email, password, result => {
 					const obj = {
 						status: "success",
 						message: "Successfully registered."
@@ -1897,3 +1897,5 @@ export default {
 		);
 	})
 };
+
+export default thisExport;

+ 7 - 7
backend/logic/actions/utils.js

@@ -22,8 +22,9 @@ export default {
 								name: module.name,
 								status: module.status,
 								stage: module.stage,
-								jobsInQueue: module.jobQueue.length(),
-								jobsInProgress: module.jobQueue.running(),
+								jobsInQueue: module.jobQueue.lengthQueue(),
+								jobsInProgress: module.jobQueue.lengthRunning(),
+								jobsPaused: module.jobQueue.lengthPaused(),
 								concurrency: module.jobQueue.concurrency
 							};
 						})
@@ -59,8 +60,6 @@ export default {
 				}
 			],
 			async (err, module) => {
-				const jobsInQueue = module.jobQueue._tasks.heap.map(task => task.data);
-
 				// console.log(module.runningJobs);
 				if (err && err !== true) {
 					err = await utils.runJob("GET_ERROR", { error: err });
@@ -75,9 +74,10 @@ export default {
 					cb({
 						status: "success",
 						message: "Successfully got module info.",
-						runningJobs: module.runningJobs,
-						jobStatistics: module.jobStatistics,
-						jobsInQueue
+						// runningTasks: module.jobQueue.runningTasks,
+						// pausedTasks: module.jobQueue.pausedTasks,
+						// queuedTasks: module.jobQueue.queue,
+						jobStatistics: module.jobStatistics
 					});
 				}
 			}

+ 24 - 13
backend/logic/activities.js

@@ -2,10 +2,16 @@ import async from "async";
 
 import CoreClass from "../core";
 
-class ActivitiesModule extends CoreClass {
+// let ActivitiesModule;
+let DBModule;
+let UtilsModule;
+
+class _ActivitiesModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("activities");
+
+		// ActivitiesModule = this;
 	}
 
 	/**
@@ -15,9 +21,8 @@ class ActivitiesModule extends CoreClass {
 	 */
 	initialize() {
 		return new Promise(resolve => {
-			this.db = this.moduleManager.modules.db;
-			this.io = this.moduleManager.modules.io;
-			this.utils = this.moduleManager.modules.utils;
+			DBModule = this.moduleManager.modules.db;
+			UtilsModule = this.moduleManager.modules.utils;
 
 			resolve();
 		});
@@ -38,8 +43,7 @@ class ActivitiesModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						this.db
-							.runJob("GET_MODEL", { modelName: "activity" })
+						DBModule.runJob("GET_MODEL", { modelName: "activity" }, this)
 							.then(res => next(null, res))
 							.catch(next);
 					},
@@ -57,10 +61,13 @@ class ActivitiesModule extends CoreClass {
 					},
 
 					(activity, next) => {
-						this.utils
-							.runJob("SOCKETS_FROM_USER", {
+						UtilsModule.runJob(
+							"SOCKETS_FROM_USER",
+							{
 								userId: activity.userId
-							})
+							},
+							this
+						)
 							.then(response => {
 								response.sockets.forEach(socket => {
 									socket.emit("event:activity.create", activity);
@@ -72,9 +79,13 @@ class ActivitiesModule extends CoreClass {
 				],
 				async (err, activity) => {
 					if (err) {
-						err = await this.utils.runJob("GET_ERROR", {
-							error: err
-						});
+						err = await UtilsModule.runJob(
+							"GET_ERROR",
+							{
+								error: err
+							},
+							this
+						);
 						reject(new Error(err));
 					} else {
 						resolve({ activity });
@@ -85,4 +96,4 @@ class ActivitiesModule extends CoreClass {
 	}
 }
 
-export default new ActivitiesModule();
+export default new _ActivitiesModule();

+ 159 - 128
backend/logic/api.js

@@ -1,13 +1,25 @@
 import config from "config";
 
 import async from "async";
+// import crypto from "crypto";
 
 import CoreClass from "../core";
 
-class APIModule extends CoreClass {
+// let APIModule;
+let AppModule;
+// let DBModule;
+let PlaylistsModule;
+let UtilsModule;
+let PunishmentsModule;
+let CacheModule;
+// let NotificationsModule;
+
+class _APIModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("api");
+
+		// APIModule = this;
 	}
 
 	/**
@@ -17,14 +29,13 @@ class APIModule extends CoreClass {
 	 */
 	initialize() {
 		return new Promise((resolve, reject) => {
-			this.app = this.moduleManager.modules.app;
-			this.stations = this.moduleManager.modules.stations;
-			this.db = this.moduleManager.modules.db;
-			this.playlists = this.moduleManager.modules.playlists;
-			this.utils = this.moduleManager.modules.utils;
-			this.punishments = this.moduleManager.modules.punishments;
-			this.cache = this.moduleManager.modules.cache;
-			this.notifications = this.moduleManager.modules.notifications;
+			AppModule = this.moduleManager.modules.app;
+			// DBModule = this.moduleManager.modules.db;
+			PlaylistsModule = this.moduleManager.modules.playlists;
+			UtilsModule = this.moduleManager.modules.utils;
+			PunishmentsModule = this.moduleManager.modules.punishments;
+			CacheModule = this.moduleManager.modules.cache;
+			// NotificationsModule = this.moduleManager.modules.notifications;
 
 			const SIDname = config.get("cookie.SIDname");
 
@@ -33,10 +44,9 @@ class APIModule extends CoreClass {
 				async.waterfall(
 					[
 						next => {
-							this.utils
-								.runJob("PARSE_COOKIES", {
-									cookieString: req.headers.cookie
-								})
+							UtilsModule.runJob("PARSE_COOKIES", {
+								cookieString: req.headers.cookie
+							})
 								.then(res => {
 									SID = res[SIDname];
 									next(null);
@@ -50,9 +60,9 @@ class APIModule extends CoreClass {
 						},
 
 						next => {
-							this.cache
-								.runJob("HGET", { table: "sessions", key: SID })
-								.then(session => next(null, session));
+							CacheModule.runJob("HGET", { table: "sessions", key: SID }).then(session =>
+								next(null, session)
+							);
 						},
 
 						(session, next) => {
@@ -62,21 +72,18 @@ class APIModule extends CoreClass {
 
 							req.session = session;
 
-							return this.cache
-								.runJob("HSET", {
-									table: "sessions",
-									key: SID,
-									value: session
-								})
-								.then(session => {
-									next(null, session);
-								});
+							return CacheModule.runJob("HSET", {
+								table: "sessions",
+								key: SID,
+								value: session
+							}).then(session => {
+								next(null, session);
+							});
 						},
 
 						(res, next) => {
 							// check if a session's user / IP is banned
-							this.punishments
-								.runJob("GET_PUNISHMENTS", {})
+							PunishmentsModule.runJob("GET_PUNISHMENTS", {})
 								.then(punishments => {
 									const isLoggedIn = !!(req.session && req.session.refreshDate);
 									const userId = isLoggedIn ? req.session.userId : null;
@@ -111,8 +118,7 @@ class APIModule extends CoreClass {
 				);
 			};
 
-			this.app
-				.runJob("GET_APP", {})
+			AppModule.runJob("GET_APP", {})
 				.then(response => {
 					response.app.get("/", (req, res) => {
 						res.json({
@@ -123,8 +129,7 @@ class APIModule extends CoreClass {
 
 					response.app.get("/export/privatePlaylist/:playlistId", isLoggedIn, (req, res) => {
 						const { playlistId } = req.params;
-						this.playlists
-							.runJob("GET_PLAYLIST", { playlistId })
+						PlaylistsModule.runJob("GET_PLAYLIST", { playlistId })
 							.then(playlist => {
 								if (playlist.createdBy === req.session.userId)
 									res.json({ status: "success", playlist });
@@ -136,105 +141,131 @@ class APIModule extends CoreClass {
 					});
 
 					// response.app.get("/debug_station", async (req, res) => {
-					//     const responseObject = {};
-
-					//     const stationModel = await this.db.runJob(
-					//         "GET_MODEL",
-					//         {
-					//             modelName: "station",
-					//         }
-					//     );
-
-					//     async.waterfall([
-					//         next => {
-					//             stationModel.find({}, next);
-					//         },
-
-					//         (stations, next) => {
-					//             responseObject.mongo = {
-					//                 stations
-					//             };
-					//             next();
-					//         },
-
-					//         next => {
-					//             this.cache
-					//                 .runJob("HGETALL", { table: "stations" })
-					//                 .then(stations => {
-					//                     next(null, stations);
-					//                 })
-					//                 .catch(next);
-					//         },
-
-					//         (stations, next) => {
-					//             responseObject.redis = {
-					//                 stations
-					//             };
-					//             next();
-					//         },
-
-					//         next => {
-					//             responseObject.cryptoExamples = {};
-					//             responseObject.mongo.stations.forEach(station => {
-					//                 const payloadName = `stations.nextSong?id=${station._id}`;
-					//                 responseObject.cryptoExamples[station._id] = crypto
-					//                     .createHash("md5")
-					//                     .update(`_notification:${payloadName}_`)
-					//                     .digest("hex")
-					//             });
-					//             next();
-					//         },
-
-					//         next => {
-					//             this.notifications.pub.keys("*", next);
-					//         },
-
-					//         (redisKeys, next) => {
-					//             responseObject.redis = {
-					//                 ...redisKeys,
-					//                 ttl: {}
-					//             };
-					//             async.eachLimit(redisKeys, 1, (redisKey, next) => {
-					//                 this.notifications.pub.ttl(redisKey, (err, ttl) => {
-					//                     responseObject.redis.ttl[redisKey] = ttl;
-					//                     next(err);
-					//                 })
-					//             }, next);
-					//         },
-
-					//         next => {
-					//             responseObject.debugLogs = this.moduleManager.debugLogs.stationIssue;
-					//             next();
-					//         },
-
-					//         next => {
-					//             responseObject.debugJobs = this.moduleManager.debugJobs;
-					//             next();
-					//         }
-					//     ], (err, response) => {
-					//         if (err) {
-					//             console.log(err);
-					//             return res.json({
-					//                 error: err,
-					//                 objectSoFar: responseObject
-					//             });
-					//         }
-
-					//         res.json(responseObject);
-					//     });
+					// 	const responseObject = {};
+
+					// 	const stationModel = await DBModule.runJob("GET_MODEL", {
+					// 		modelName: "station"
+					// 	});
+
+					// 	async.waterfall(
+					// 		[
+					// 			next => {
+					// 				stationModel.find({}, next);
+					// 			},
+
+					// 			(stations, next) => {
+					// 				responseObject.mongo = {
+					// 					stations
+					// 				};
+					// 				next();
+					// 			},
+
+					// 			next => {
+					// 				CacheModule.runJob("HGETALL", { table: "stations" })
+					// 					.then(stations => {
+					// 						next(null, stations);
+					// 					})
+					// 					.catch(err => {
+					// 						console.log(err);
+					// 						next(err);
+					// 					});
+					// 			},
+
+					// 			(stations, next) => {
+					// 				responseObject.redis = {
+					// 					stations
+					// 				};
+					// 				next();
+					// 			},
+
+					// 			next => {
+					// 				responseObject.cryptoExamples = {};
+					// 				responseObject.mongo.stations.forEach(station => {
+					// 					const payloadName = `stations.nextSong?id=${station._id}`;
+					// 					responseObject.cryptoExamples[station._id] = crypto
+					// 						.createHash("md5")
+					// 						.update(`_notification:${payloadName}_`)
+					// 						.digest("hex");
+					// 				});
+					// 				next();
+					// 			},
+
+					// 			next => {
+					// 				NotificationsModule.pub.keys("*", next);
+					// 			},
+
+					// 			(redisKeys, next) => {
+					// 				responseObject.redis = {
+					// 					...redisKeys,
+					// 					ttl: {}
+					// 				};
+					// 				async.eachLimit(
+					// 					redisKeys,
+					// 					1,
+					// 					(redisKey, next) => {
+					// 						NotificationsModule.pub.ttl(redisKey, (err, ttl) => {
+					// 							responseObject.redis.ttl[redisKey] = ttl;
+					// 							next(err);
+					// 						});
+					// 					},
+					// 					next
+					// 				);
+					// 			},
+
+					// 			next => {
+					// 				responseObject.debugLogs = this.moduleManager.debugLogs.stationIssue;
+					// 				next();
+					// 			},
+
+					// 			next => {
+					// 				responseObject.debugJobs = this.moduleManager.debugJobs;
+					// 				next();
+					// 			}
+					// 		],
+					// 		(err, response) => {
+					// 			if (err) {
+					// 				console.log(err);
+					// 				return res.json({
+					// 					error: err,
+					// 					objectSoFar: responseObject
+					// 				});
+					// 			}
+
+					// 			const responseJson = JSON.stringify(responseObject, (key, value) => {
+					// 				// console.log(key, value);
+					// 				if (
+					// 					key === "module" ||
+					// 					key === "task" ||
+					// 					key === "onFinish" ||
+					// 					key === "server" ||
+					// 					key === "nsp" ||
+					// 					key === "socket" ||
+					// 					key === "res" ||
+					// 					key === "client" ||
+					// 					key === "_idleNext" ||
+					// 					key === "_idlePrev"
+					// 				) {
+					// 					return undefined;
+					// 				}
+					// 				if (key === "parentJob" && value) return value.toString();
+					// 				return value;
+					// 			});
+
+					// 			res.end(responseJson);
+					// 		}
+					// 	);
 					// });
 
 					// Object.keys(actions).forEach(namespace => {
-					//     Object.keys(actions[namespace]).forEach(action => {
-					//         let name = `/${namespace}/${action}`;
-
-					//         response.app.get(name, (req, res) => {
-					//             actions[namespace][action](null, result => {
-					//                 if (typeof cb === "function")
-					//                     return res.json(result);
-					//             });
-					//         });
-					//     });
+					// 	Object.keys(actions[namespace]).forEach(action => {
+					// 		const name = `/${namespace}/${action}`;
+
+					// 		response.app.get(name, (req, res) => {
+					// 			actions[namespace][action](null, result => {
+					// 				if (typeof cb === "function") return res.json(result);
+					// 			});
+					// 		});
+					// 	});
 					// });
 
 					resolve();
@@ -246,4 +277,4 @@ class APIModule extends CoreClass {
 	}
 }
 
-export default new APIModule();
+export default new _APIModule();

+ 44 - 42
backend/logic/app.js

@@ -12,10 +12,19 @@ import CoreClass from "../core";
 
 const { OAuth2 } = oauth;
 
-class AppModule extends CoreClass {
+let AppModule;
+let MailModule;
+let CacheModule;
+let DBModule;
+let ActivitiesModule;
+let UtilsModule;
+
+class _AppModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("app");
+
+		AppModule = this;
 	}
 
 	/**
@@ -25,12 +34,11 @@ class AppModule extends CoreClass {
 	 */
 	initialize() {
 		return new Promise(resolve => {
-			const { mail } = this.moduleManager.modules;
-			const { cache } = this.moduleManager.modules;
-			const { db } = this.moduleManager.modules;
-			const { activities } = this.moduleManager.modules;
-
-			this.utils = this.moduleManager.modules.utils;
+			MailModule = this.moduleManager.modules.mail;
+			CacheModule = this.moduleManager.modules.cache;
+			DBModule = this.moduleManager.modules.db;
+			ActivitiesModule = this.moduleManager.modules.activities;
+			UtilsModule = this.moduleManager.modules.utils;
 
 			const app = (this.app = express());
 			const SIDname = config.get("cookie.SIDname");
@@ -42,7 +50,7 @@ class AppModule extends CoreClass {
 			app.use(bodyParser.urlencoded({ extended: true }));
 
 			let userModel;
-			db.runJob("GET_MODEL", { modelName: "user" })
+			DBModule.runJob("GET_MODEL", { modelName: "user" })
 				.then(model => {
 					userModel = model;
 				})
@@ -126,7 +134,7 @@ class AppModule extends CoreClass {
 
 				const { state } = req.query;
 
-				const verificationToken = await this.utils.runJob("GENERATE_RANDOM_STRING", { length: 64 });
+				const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 });
 
 				return async.waterfall(
 					[
@@ -165,11 +173,10 @@ class AppModule extends CoreClass {
 								return async.waterfall(
 									[
 										next => {
-											cache
-												.runJob("HGET", {
-													table: "sessions",
-													key: state
-												})
+											CacheModule.runJob("HGET", {
+												table: "sessions",
+												key: state
+											})
 												.then(session => next(null, session))
 												.catch(next);
 										},
@@ -203,7 +210,7 @@ class AppModule extends CoreClass {
 										},
 
 										user => {
-											cache.runJob("PUB", {
+											CacheModule.runJob("PUB", {
 												channel: "user.linkGithub",
 												value: user._id
 											});
@@ -260,11 +267,9 @@ class AppModule extends CoreClass {
 						},
 
 						(user, next) => {
-							this.utils
-								.runJob("GENERATE_RANDOM_STRING", {
-									length: 12
-								})
-								.then(_id => next(null, user, _id));
+							UtilsModule.runJob("GENERATE_RANDOM_STRING", {
+								length: 12
+							}).then(_id => next(null, user, _id));
 						},
 
 						(user, _id, next) => {
@@ -294,14 +299,12 @@ class AppModule extends CoreClass {
 
 						// generate the url for gravatar avatar
 						(user, next) => {
-							this.utils
-								.runJob("CREATE_GRAVATAR", {
-									email: user.email.address
-								})
-								.then(url => {
-									user.avatar = { type: "gravatar", url };
-									next(null, user);
-								});
+							UtilsModule.runJob("CREATE_GRAVATAR", {
+								email: user.email.address
+							}).then(url => {
+								user.avatar = { type: "gravatar", url };
+								next(null, user);
+							});
 						},
 
 						// save the new user to the database
@@ -311,7 +314,7 @@ class AppModule extends CoreClass {
 
 						// add the activity of account creation
 						(user, next) => {
-							activities.runJob("ADD_ACTIVITY", {
+							ActivitiesModule.runJob("ADD_ACTIVITY", {
 								userId: user._id,
 								activityType: "created_account"
 							});
@@ -319,7 +322,7 @@ class AppModule extends CoreClass {
 						},
 
 						(user, next) => {
-							mail.runJob("GET_SCHEMA", {
+							MailModule.runJob("GET_SCHEMA", {
 								schemaName: "verifyEmail"
 							}).then(verifyEmailSchema => {
 								verifyEmailSchema(address, body.login, user.email.verificationToken, err => {
@@ -330,7 +333,7 @@ class AppModule extends CoreClass {
 					],
 					async (err, userId) => {
 						if (err && err !== true) {
-							err = await this.utils.runJob("GET_ERROR", {
+							err = await UtilsModule.runJob("GET_ERROR", {
 								error: err
 							});
 
@@ -343,17 +346,16 @@ class AppModule extends CoreClass {
 							return redirectOnErr(res, err);
 						}
 
-						const sessionId = await this.utils.runJob("GUID", {});
-						const sessionSchema = await cache.runJob("GET_SCHEMA", {
+						const sessionId = await UtilsModule.runJob("GUID", {});
+						const sessionSchema = await CacheModule.runJob("GET_SCHEMA", {
 							schemaName: "session"
 						});
 
-						return cache
-							.runJob("HSET", {
-								table: "sessions",
-								key: sessionId,
-								value: sessionSchema(sessionId, userId)
-							})
+						return CacheModule.runJob("HSET", {
+							table: "sessions",
+							key: sessionId,
+							value: sessionSchema(sessionId, userId)
+						})
 							.then(() => {
 								const date = new Date();
 								date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);
@@ -449,7 +451,7 @@ class AppModule extends CoreClass {
 	 */
 	SERVER() {
 		return new Promise(resolve => {
-			resolve(this.server);
+			resolve(AppModule.server);
 		});
 	}
 
@@ -460,7 +462,7 @@ class AppModule extends CoreClass {
 	 */
 	GET_APP() {
 		return new Promise(resolve => {
-			resolve({ app: this.app });
+			resolve({ app: AppModule.app });
 		});
 	}
 
@@ -472,4 +474,4 @@ class AppModule extends CoreClass {
 	// }
 }
 
-export default new AppModule();
+export default new _AppModule();

+ 25 - 18
backend/logic/cache/index.js

@@ -9,10 +9,14 @@ import CoreClass from "../../core";
 const pubs = {};
 const subs = {};
 
-class CacheModule extends CoreClass {
+let CacheModule;
+
+class _CacheModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("cache");
+
+		CacheModule = this;
 	}
 
 	/**
@@ -84,8 +88,8 @@ class CacheModule extends CoreClass {
 	 */
 	QUIT() {
 		return new Promise(resolve => {
-			if (this.client.connected) {
-				this.client.quit();
+			if (CacheModule.client.connected) {
+				CacheModule.client.quit();
 				Object.keys(pubs).forEach(channel => pubs[channel].quit());
 				Object.keys(subs).forEach(channel => subs[channel].client.quit());
 			}
@@ -113,7 +117,7 @@ class CacheModule extends CoreClass {
 			// automatically stringify objects and arrays into JSON
 			if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);
 
-			this.client.hset(payload.table, key, value, err => {
+			CacheModule.client.hset(payload.table, key, value, err => {
 				if (err) return reject(new Error(err));
 				return resolve(JSON.parse(value));
 			});
@@ -134,9 +138,11 @@ class CacheModule extends CoreClass {
 		return new Promise((resolve, reject) => {
 			let { key } = payload;
 
+			if (!key) return reject(new Error("Invalid key!"));
+			if (!payload.table) return reject(new Error("Invalid table!"));
 			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 
-			this.client.hget(payload.table, key, (err, value) => {
+			return CacheModule.client.hget(payload.table, key, (err, value) => {
 				if (err) return reject(new Error(err));
 				try {
 					value = JSON.parse(value);
@@ -167,7 +173,7 @@ class CacheModule extends CoreClass {
 
 			if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 
-			this.client.hdel(payload.table, key, err => {
+			CacheModule.client.hdel(payload.table, key, err => {
 				if (err) return reject(new Error(err));
 				return resolve();
 			});
@@ -185,7 +191,7 @@ class CacheModule extends CoreClass {
 	HGETALL(payload) {
 		// table, cb, parseJson = true
 		return new Promise((resolve, reject) => {
-			this.client.hgetall(payload.table, (err, obj) => {
+			CacheModule.client.hgetall(payload.table, (err, obj) => {
 				if (err) return reject(new Error(err));
 				if (obj)
 					Object.keys(obj).forEach(key => {
@@ -211,7 +217,7 @@ class CacheModule extends CoreClass {
 		// channel, value, stringifyJson = true
 		return new Promise((resolve, reject) => {
 			/* if (pubs[channel] === undefined) {
-            pubs[channel] = redis.createClient({ url: this.url });
+            pubs[channel] = redis.createClient({ url: CacheModule.url });
             pubs[channel].on('error', (err) => console.error);
             } */
 
@@ -220,7 +226,7 @@ class CacheModule extends CoreClass {
 			if (["object", "array"].includes(typeof value)) value = JSON.stringify(value);
 
 			// pubs[channel].publish(channel, value);
-			this.client.publish(payload.channel, value, err => {
+			CacheModule.client.publish(payload.channel, value, err => {
 				if (err) reject(err);
 				else resolve();
 			});
@@ -241,18 +247,19 @@ class CacheModule extends CoreClass {
 			if (subs[payload.channel] === undefined) {
 				subs[payload.channel] = {
 					client: redis.createClient({
-						url: this.url,
-						password: this.password
+						url: CacheModule.url,
+						password: CacheModule.password
 					}),
 					cbs: []
 				};
 
 				subs[payload.channel].client.on("message", (channel, message) => {
-					try {
-						message = JSON.parse(message);
-					} catch (err) {
-						console.error(err);
-					}
+					if (message.startsWith("[") || message.startsWith("{"))
+						try {
+							message = JSON.parse(message);
+						} catch (err) {
+							console.error(err);
+						}
 
 					return subs[channel].cbs.forEach(cb => cb(message));
 				});
@@ -275,9 +282,9 @@ class CacheModule extends CoreClass {
 	 */
 	GET_SCHEMA(payload) {
 		return new Promise(resolve => {
-			resolve(this.schemas[payload.schemaName]);
+			resolve(CacheModule.schemas[payload.schemaName]);
 		});
 	}
 }
 
-export default new CacheModule();
+export default new _CacheModule();

+ 8 - 4
backend/logic/db/index.js

@@ -16,10 +16,14 @@ const isLength = (string, min, max) => !(typeof string !== "string" || string.le
 
 mongoose.Promise = bluebird;
 
-class DBModule extends CoreClass {
+let DBModule;
+
+class _DBModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("db");
+
+		DBModule = this;
 	}
 
 	/**
@@ -271,7 +275,7 @@ class DBModule extends CoreClass {
 	 */
 	GET_MODEL(payload) {
 		return new Promise(resolve => {
-			resolve(this.models[payload.modelName]);
+			resolve(DBModule.models[payload.modelName]);
 		});
 	}
 
@@ -284,7 +288,7 @@ class DBModule extends CoreClass {
 	 */
 	GET_SCHEMA(payload) {
 		return new Promise(resolve => {
-			resolve(this.schemas[payload.schemaName]);
+			resolve(DBModule.schemas[payload.schemaName]);
 		});
 	}
 
@@ -299,4 +303,4 @@ class DBModule extends CoreClass {
 	}
 }
 
-export default new DBModule();
+export default new _DBModule();

+ 0 - 114
backend/logic/discord.js

@@ -1,114 +0,0 @@
-import config from "config";
-import Discord from "discord.js";
-
-import CoreClass from "../core";
-
-class DiscordModule extends CoreClass {
-	// eslint-disable-next-line require-jsdoc
-	constructor() {
-		super("discord");
-	}
-
-	/**
-	 * Initialises the discord module
-	 *
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	initialize() {
-		return new Promise((resolve, reject) => {
-			this.log("INFO", "Discord initialize");
-
-			this.client = new Discord.Client();
-			this.adminAlertChannelId = config.get("apis.discord").loggingChannel;
-
-			this.client.on("ready", () => {
-				this.log("INFO", `Logged in as ${this.client.user.tag}!`);
-
-				if (this.getStatus() === "INITIALIZING") {
-					resolve();
-				} else if (this.getStatus() === "RECONNECTING") {
-					this.log("INFO", `Discord client reconnected.`);
-					this.setStatus("READY");
-				}
-			});
-
-			this.client.on("disconnect", () => {
-				this.log("INFO", `Discord client disconnected.`);
-
-				if (this.getStatus() === "INITIALIZING") reject();
-				else {
-					this.setStatus("DISCONNECTED");
-				}
-			});
-
-			this.client.on("reconnecting", () => {
-				this.log("INFO", `Discord client reconnecting.`);
-				this.setStatus("RECONNECTING");
-			});
-
-			this.client.on("error", err => {
-				this.log("INFO", `Discord client encountered an error: ${err.message}.`);
-			});
-
-			this.client.login(config.get("apis.discord").token);
-		});
-	}
-
-	/**
-	 * Adds a new activity to the database
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.color - The colour of the alert title
-	 * @param {string} payload.message - The message to send as the alert
-	 * @param {string} payload.type - The type of alert e.g. Startup
-	 * @param {boolean} payload.critical - If the message is service critical
-	 * @param {Array} payload.extraFields - Any extra fields to show in the discord message
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	SEND_ADMIN_ALERT_MESSAGE(payload) {
-		return new Promise((resolve, reject) => {
-			const channel = this.client.channels.find(channel => channel.id === this.adminAlertChannelId);
-
-			if (channel !== null) {
-				const richEmbed = new Discord.RichEmbed();
-				richEmbed.setAuthor(
-					"Musare Logger",
-					`${config.get("domain")}/favicon-194x194.png`,
-					config.get("domain")
-				);
-				richEmbed.setColor(payload.color);
-				richEmbed.setDescription(payload.message);
-				// richEmbed.setFooter("Footer", "https://musare.com/favicon-194x194.png");
-				// richEmbed.setImage("https://musare.com/favicon-194x194.png");
-				// richEmbed.setThumbnail("https://musare.com/favicon-194x194.png");
-				richEmbed.setTimestamp(new Date());
-				richEmbed.setTitle("MUSARE ALERT");
-				richEmbed.setURL(config.get("domain"));
-				richEmbed.addField("Type:", payload.type, true);
-				richEmbed.addField("Critical:", payload.critical ? "True" : "False", true);
-				payload.extraFields.forEach(extraField => {
-					richEmbed.addField(extraField.name, extraField.value, extraField.inline);
-				});
-
-				channel
-					.send(payload.message, { embed: richEmbed })
-					.then(message =>
-						resolve({
-							status: "success",
-							message: `Successfully sent admin alert message: ${message}`
-						})
-					)
-					.catch(() => reject(new Error("Couldn't send admin alert message")));
-			} else {
-				reject(new Error("Channel was not found"));
-			}
-			// if (true) {
-			//     resolve({});
-			// } else {
-			//     reject(new Error("Nothing changed."));
-			// }
-		});
-	}
-}
-
-export default new DiscordModule();

+ 97 - 87
backend/logic/io.js

@@ -9,10 +9,19 @@ import socketio from "socket.io";
 import actions from "./actions";
 import CoreClass from "../core";
 
-class IOModule extends CoreClass {
+let IOModule;
+let AppModule;
+let CacheModule;
+let UtilsModule;
+let DBModule;
+let PunishmentsModule;
+
+class _IOModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("io");
+
+		IOModule = this;
 	}
 
 	/**
@@ -23,18 +32,20 @@ class IOModule extends CoreClass {
 	async initialize() {
 		this.setStage(1);
 
-		const { app } = this.moduleManager.modules;
-		const { cache } = this.moduleManager.modules;
-		const { utils } = this.moduleManager.modules;
-		const { db } = this.moduleManager.modules;
-		const { punishments } = this.moduleManager.modules;
+		AppModule = this.moduleManager.modules.app;
+		CacheModule = this.moduleManager.modules.cache;
+		UtilsModule = this.moduleManager.modules.utils;
+		DBModule = this.moduleManager.modules.db;
+		PunishmentsModule = this.moduleManager.modules.punishments;
+
+		this.userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" });
 
 		this.setStage(2);
 
 		const SIDname = config.get("cookie.SIDname");
 
 		// TODO: Check every 30s/, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
-		const server = await app.runJob("SERVER");
+		const server = await AppModule.runJob("SERVER");
 		this._io = socketio(server);
 
 		return new Promise(resolve => {
@@ -57,14 +68,12 @@ class IOModule extends CoreClass {
 				return async.waterfall(
 					[
 						next => {
-							utils
-								.runJob("PARSE_COOKIES", {
-									cookieString: socket.request.headers.cookie
-								})
-								.then(res => {
-									SID = res[SIDname];
-									next(null);
-								});
+							UtilsModule.runJob("PARSE_COOKIES", {
+								cookieString: socket.request.headers.cookie
+							}).then(res => {
+								SID = res[SIDname];
+								next(null);
+							});
 						},
 
 						next => {
@@ -73,9 +82,11 @@ class IOModule extends CoreClass {
 						},
 
 						next => {
-							cache.runJob("HGET", { table: "sessions", key: SID }).then(session => {
-								next(null, session);
-							});
+							CacheModule.runJob("HGET", { table: "sessions", key: SID })
+								.then(session => {
+									next(null, session);
+								})
+								.catch(next);
 						},
 
 						(session, next) => {
@@ -85,21 +96,18 @@ class IOModule extends CoreClass {
 
 							socket.session = session;
 
-							return cache
-								.runJob("HSET", {
-									table: "sessions",
-									key: SID,
-									value: session
-								})
-								.then(session => {
-									next(null, session);
-								});
+							return CacheModule.runJob("HSET", {
+								table: "sessions",
+								key: SID,
+								value: session
+							}).then(session => {
+								next(null, session);
+							});
 						},
 
 						(res, next) => {
 							// check if a session's user / IP is banned
-							punishments
-								.runJob("GET_PUNISHMENTS", {})
+							PunishmentsModule.runJob("GET_PUNISHMENTS", {})
 								.then(punishments => {
 									const isLoggedIn = !!(socket.session && socket.session.refreshDate);
 									const userId = isLoggedIn ? socket.session.userId : null;
@@ -202,32 +210,31 @@ class IOModule extends CoreClass {
 				socket.on("error", console.error);
 
 				if (socket.session.sessionId) {
-					cache
-						.runJob("HGET", {
-							table: "sessions",
-							key: socket.session.sessionId
-						})
+					CacheModule.runJob("HGET", {
+						table: "sessions",
+						key: socket.session.sessionId
+					})
 						.then(session => {
 							if (session && session.userId) {
-								db.runJob("GET_MODEL", { modelName: "user" }).then(userModel => {
-									userModel.findOne({ _id: session.userId }, (err, user) => {
-										if (err || !user) return socket.emit("ready", false);
-
-										let role = "";
-										let username = "";
-										let userId = "";
-										if (user) {
-											role = user.role;
-											username = user.username;
-											userId = session.userId;
-										}
-
-										return socket.emit("ready", true, role, username, userId);
-									});
+								IOModule.userModel.findOne({ _id: session.userId }, (err, user) => {
+									if (err || !user) return socket.emit("ready", false);
+
+									let role = "";
+									let username = "";
+									let userId = "";
+									if (user) {
+										role = user.role;
+										username = user.username;
+										userId = session.userId;
+									}
+
+									return socket.emit("ready", true, role, username, userId);
 								});
 							} else socket.emit("ready", false);
 						})
-						.catch(() => socket.emit("ready", false));
+						.catch(() => {
+							socket.emit("ready", false);
+						});
 				} else socket.emit("ready", false);
 
 				// have the socket listen for each action
@@ -256,53 +263,56 @@ class IOModule extends CoreClass {
 							}
 							this.log("INFO", "IO_ACTION", `A user executed an action. Action: ${namespace}.${action}.`);
 
+							let failedGettingSession = false;
 							// load the session from the cache
-							cache
-								.runJob("HGET", {
+							if (socket.session.sessionId)
+								await CacheModule.runJob("HGET", {
 									table: "sessions",
 									key: socket.session.sessionId
 								})
-								.then(session => {
-									// make sure the sockets sessionId isn't set if there is no session
-									if (socket.session.sessionId && session === null) delete socket.session.sessionId;
-
-									try {
-										// call the action, passing it the session, and the arguments socket.io passed us
-										return actions[namespace][action].apply(
-											null,
-											[socket.session].concat(args).concat([
-												result => {
-													this.log(
-														"INFO",
-														"IO_ACTION",
-														`Response to action. Action: ${namespace}.${action}. Response status: ${result.status}`
-													);
-													// respond to the socket with our message
-													if (typeof cb === "function") cb(result);
-												}
-											])
-										);
-									} catch (err) {
+									.then(session => {
+										// make sure the sockets sessionId isn't set if there is no session
+										if (socket.session.sessionId && session === null)
+											delete socket.session.sessionId;
+									})
+									.catch(() => {
+										failedGettingSession = true;
 										if (typeof cb === "function")
 											cb({
 												status: "error",
-												message: "An error occurred while executing the specified action."
+												message: "An error occurred while obtaining your session"
 											});
-
-										return this.log(
-											"ERROR",
-											"IO_ACTION_ERROR",
-											`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
-										);
-									}
-								})
-								.catch(() => {
+									});
+							if (!failedGettingSession)
+								try {
+									// call the action, passing it the session, and the arguments socket.io passed us
+									actions[namespace][action].apply(
+										null,
+										[socket.session].concat(args).concat([
+											result => {
+												this.log(
+													"INFO",
+													"IO_ACTION",
+													`Response to action. Action: ${namespace}.${action}. Response status: ${result.status}`
+												);
+												// respond to the socket with our message
+												if (typeof cb === "function") cb(result);
+											}
+										])
+									);
+								} catch (err) {
 									if (typeof cb === "function")
 										cb({
 											status: "error",
-											message: "An error occurred while obtaining your session"
+											message: "An error occurred while executing the specified action."
 										});
-								});
+
+									this.log(
+										"ERROR",
+										"IO_ACTION_ERROR",
+										`Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
+									);
+								}
 						});
 					});
 				});
@@ -321,9 +331,9 @@ class IOModule extends CoreClass {
 	 */
 	IO() {
 		return new Promise(resolve => {
-			resolve(this._io);
+			resolve(IOModule._io);
 		});
 	}
 }
 
-export default new IOModule();
+export default new _IOModule();

+ 9 - 5
backend/logic/mail/index.js

@@ -4,10 +4,14 @@ import Mailgun from "mailgun-js";
 
 import CoreClass from "../../core";
 
-class MailModule extends CoreClass {
+let MailModule;
+
+class _MailModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("mail");
+
+		MailModule = this;
 	}
 
 	/**
@@ -47,8 +51,8 @@ class MailModule extends CoreClass {
 	 */
 	SEND_MAIL(payload) {
 		return new Promise(resolve => {
-			if (this.enabled)
-				this.mailgun.messages().send(payload.data, () => {
+			if (MailModule.enabled)
+				MailModule.mailgun.messages().send(payload.data, () => {
 					resolve();
 				});
 			else resolve();
@@ -64,9 +68,9 @@ class MailModule extends CoreClass {
 	 */
 	GET_SCHEMA(payload) {
 		return new Promise(resolve => {
-			resolve(this.schemas[payload.schemaName]);
+			resolve(MailModule.schemas[payload.schemaName]);
 		});
 	}
 }
 
-export default new MailModule();
+export default new _MailModule();

+ 38 - 20
backend/logic/notifications.js

@@ -4,14 +4,18 @@ import crypto from "crypto";
 import redis from "redis";
 
 import CoreClass from "../core";
-import utils from "./utils";
 
-class NotificationsModule extends CoreClass {
+let NotificationsModule;
+let UtilsModule;
+
+class _NotificationsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("notifications");
 
 		this.subscriptions = [];
+
+		NotificationsModule = this;
 	}
 
 	/**
@@ -24,6 +28,8 @@ class NotificationsModule extends CoreClass {
 			const url = (this.url = config.get("redis").url);
 			const password = (this.password = config.get("redis").password);
 
+			UtilsModule = this.moduleManager.modules.utils;
+
 			this.pub = redis.createClient({
 				url,
 				password,
@@ -90,9 +96,13 @@ class NotificationsModule extends CoreClass {
 
 				this.pub.config("GET", "notify-keyspace-events", async (err, response) => {
 					if (err) {
-						const formattedErr = await utils.runJob("GET_ERROR", {
-							error: err
-						});
+						const formattedErr = await UtilsModule.runJob(
+							"GET_ERROR",
+							{
+								error: err
+							},
+							this
+						);
 						this.log(
 							"ERROR",
 							"NOTIFICATIONS_INITIALIZE",
@@ -159,14 +169,14 @@ class NotificationsModule extends CoreClass {
 	SCHEDULE(payload) {
 		return new Promise((resolve, reject) => {
 			const time = Math.round(payload.time);
-			this.log(
+			NotificationsModule.log(
 				"STATION_ISSUE",
 				`SCHEDULE - Time: ${time}; Name: ${payload.name}; Key: ${crypto
 					.createHash("md5")
 					.update(`_notification:${payload.name}_`)
 					.digest("hex")}; StationId: ${payload.station._id}; StationName: ${payload.station.name}`
 			);
-			this.pub.set(
+			NotificationsModule.pub.set(
 				crypto.createHash("md5").update(`_notification:${payload.name}_`).digest("hex"),
 				"",
 				"PX",
@@ -191,20 +201,25 @@ class NotificationsModule extends CoreClass {
 	 */
 	SUBSCRIBE(payload) {
 		return new Promise(resolve => {
-			this.log(
+			NotificationsModule.log(
 				"STATION_ISSUE",
 				`SUBSCRIBE - Name: ${payload.name}; Key: ${crypto
 					.createHash("md5")
 					.update(`_notification:${payload.name}_`)
 					.digest("hex")}, StationId: ${payload.station._id}; StationName: ${payload.station.name}; Unique: ${
 					payload.unique
-				}; SubscriptionExists: ${!!this.subscriptions.find(
+				}; SubscriptionExists: ${!!NotificationsModule.subscriptions.find(
 					subscription => subscription.originalName === payload.name
 				)};`
 			);
-			if (payload.unique && !!this.subscriptions.find(subscription => subscription.originalName === payload.name))
+			if (
+				payload.unique &&
+				!!NotificationsModule.subscriptions.find(subscription => subscription.originalName === payload.name)
+			)
 				return resolve({
-					subscription: this.subscriptions.find(subscription => subscription.originalName === payload.name)
+					subscription: NotificationsModule.subscriptions.find(
+						subscription => subscription.originalName === payload.name
+					)
 				});
 
 			const subscription = {
@@ -213,7 +228,7 @@ class NotificationsModule extends CoreClass {
 				cb: payload.cb
 			};
 
-			this.subscriptions.push(subscription);
+			NotificationsModule.subscriptions.push(subscription);
 
 			return resolve({ subscription });
 		});
@@ -229,8 +244,8 @@ class NotificationsModule extends CoreClass {
 	REMOVE(payload) {
 		// subscription
 		return new Promise(resolve => {
-			const index = this.subscriptions.indexOf(payload.subscription);
-			if (index) this.subscriptions.splice(index, 1);
+			const index = NotificationsModule.subscriptions.indexOf(payload.subscription);
+			if (index) NotificationsModule.subscriptions.splice(index, 1);
 			resolve();
 		});
 	}
@@ -245,19 +260,22 @@ class NotificationsModule extends CoreClass {
 	UNSCHEDULE(payload) {
 		// name
 		return new Promise((resolve, reject) => {
-			this.log(
+			NotificationsModule.log(
 				"STATION_ISSUE",
 				`UNSCHEDULE - Name: ${payload.name}; Key: ${crypto
 					.createHash("md5")
 					.update(`_notification:${payload.name}_`)
 					.digest("hex")}`
 			);
-			this.pub.del(crypto.createHash("md5").update(`_notification:${payload.name}_`).digest("hex"), err => {
-				if (err) reject(err);
-				else resolve();
-			});
+			NotificationsModule.pub.del(
+				crypto.createHash("md5").update(`_notification:${payload.name}_`).digest("hex"),
+				err => {
+					if (err) reject(err);
+					else resolve();
+				}
+			);
 		});
 	}
 }
 
-export default new NotificationsModule();
+export default new _NotificationsModule();

+ 75 - 84
backend/logic/playlists.js

@@ -2,10 +2,17 @@ import async from "async";
 
 import CoreClass from "../core";
 
-class PlaylistsModule extends CoreClass {
+let PlaylistsModule;
+let CacheModule;
+let DBModule;
+let UtilsModule;
+
+class _PlaylistsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("playlists");
+
+		PlaylistsModule = this;
 	}
 
 	/**
@@ -16,12 +23,12 @@ class PlaylistsModule extends CoreClass {
 	async initialize() {
 		this.setStage(1);
 
-		this.cache = this.moduleManager.modules.cache;
-		this.db = this.moduleManager.modules.db;
-		this.utils = this.moduleManager.modules.utils;
+		CacheModule = this.moduleManager.modules.cache;
+		DBModule = this.moduleManager.modules.db;
+		UtilsModule = this.moduleManager.modules.utils;
 
-		const playlistModel = await this.db.runJob("GET_MODEL", { modelName: "playlist" });
-		const playlistSchema = await this.cache.runJob("GET_SCHEMA", { schemaName: "playlist" });
+		this.playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" });
+		this.playlistSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "playlist" });
 
 		this.setStage(2);
 
@@ -30,8 +37,7 @@ class PlaylistsModule extends CoreClass {
 				[
 					next => {
 						this.setStage(3);
-						this.cache
-							.runJob("HGETALL", { table: "playlists" })
+						CacheModule.runJob("HGETALL", { table: "playlists" })
 							.then(playlists => {
 								next(null, playlists);
 							})
@@ -48,14 +54,13 @@ class PlaylistsModule extends CoreClass {
 						return async.each(
 							playlistIds,
 							(playlistId, next) => {
-								playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
+								PlaylistsModule.playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
 									if (err) next(err);
 									else if (!playlist) {
-										this.cache
-											.runJob("HDEL", {
-												table: "playlists",
-												key: playlistId
-											})
+										CacheModule.runJob("HDEL", {
+											table: "playlists",
+											key: playlistId
+										})
 											.then(() => next())
 											.catch(next);
 									} else next();
@@ -67,7 +72,7 @@ class PlaylistsModule extends CoreClass {
 
 					next => {
 						this.setStage(5);
-						playlistModel.find({}, next);
+						PlaylistsModule.playlistModel.find({}, next);
 					},
 
 					(playlists, next) => {
@@ -75,12 +80,11 @@ class PlaylistsModule extends CoreClass {
 						async.each(
 							playlists,
 							(playlist, cb) => {
-								this.cache
-									.runJob("HSET", {
-										table: "playlists",
-										key: playlist._id,
-										value: playlistSchema(playlist)
-									})
+								CacheModule.runJob("HSET", {
+									table: "playlists",
+									key: playlist._id,
+									value: PlaylistsModule.playlistSchemaCache(playlist)
+								})
 									.then(() => cb())
 									.catch(next);
 							},
@@ -90,7 +94,7 @@ class PlaylistsModule extends CoreClass {
 				],
 				async err => {
 					if (err) {
-						const formattedErr = await this.utils.runJob("GET_ERROR", {
+						const formattedErr = await UtilsModule.runJob("GET_ERROR", {
 							error: err
 						});
 						reject(new Error(formattedErr));
@@ -108,21 +112,11 @@ class PlaylistsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_PLAYLIST(payload) {
-		return new Promise((resolve, reject) => {
-			let playlistModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "playlist" })
-				.then(model => {
-					playlistModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						this.cache
-							.runJob("HGETALL", { table: "playlists" })
+						CacheModule.runJob("HGETALL", { table: "playlists" }, this)
 							.then(playlists => {
 								next(null, playlists);
 							})
@@ -137,14 +131,17 @@ class PlaylistsModule extends CoreClass {
 						return async.each(
 							playlistIds,
 							(playlistId, next) => {
-								playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
+								PlaylistsModule.playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
 									if (err) next(err);
 									else if (!playlist) {
-										this.cache
-											.runJob("HDEL", {
+										CacheModule.runJob(
+											"HDEL",
+											{
 												table: "playlists",
 												key: playlistId
-											})
+											},
+											this
+										)
 											.then(() => next())
 											.catch(next);
 									} else next();
@@ -155,28 +152,34 @@ class PlaylistsModule extends CoreClass {
 					},
 
 					next => {
-						this.cache
-							.runJob("HGET", {
+						CacheModule.runJob(
+							"HGET",
+							{
 								table: "playlists",
 								key: payload.playlistId
-							})
+							},
+							this
+						)
 							.then(playlist => next(null, playlist))
 							.catch(next);
 					},
 
 					(playlist, next) => {
 						if (playlist) return next(true, playlist);
-						return playlistModel.findOne({ _id: payload.playlistId }, next);
+						return PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
 					},
 
 					(playlist, next) => {
 						if (playlist) {
-							this.cache
-								.runJob("HSET", {
+							CacheModule.runJob(
+								"HSET",
+								{
 									table: "playlists",
 									key: payload.playlistId,
 									value: playlist
-								})
+								},
+								this
+							)
 								.then(playlist => {
 									next(null, playlist);
 								})
@@ -188,8 +191,8 @@ class PlaylistsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve(playlist);
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -201,25 +204,16 @@ class PlaylistsModule extends CoreClass {
 	 */
 	UPDATE_PLAYLIST(payload) {
 		// playlistId, cb
-		return new Promise((resolve, reject) => {
-			let playlistModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "playlist" })
-				.then(model => {
-					playlistModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						playlistModel.findOne({ _id: payload.playlistId }, next);
+						PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
 					},
 
 					(playlist, next) => {
 						if (!playlist) {
-							this.cache.runJob("HDEL", {
+							CacheModule.runJob("HDEL", {
 								table: "playlists",
 								key: payload.playlistId
 							});
@@ -227,12 +221,15 @@ class PlaylistsModule extends CoreClass {
 							return next("Playlist not found");
 						}
 
-						return this.cache
-							.runJob("HSET", {
+						return CacheModule.runJob(
+							"HSET",
+							{
 								table: "playlists",
 								key: payload.playlistId,
 								value: playlist
-							})
+							},
+							this
+						)
 							.then(playlist => {
 								next(null, playlist);
 							})
@@ -243,8 +240,8 @@ class PlaylistsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve(playlist);
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -256,28 +253,22 @@ class PlaylistsModule extends CoreClass {
 	 */
 	DELETE_PLAYLIST(payload) {
 		// playlistId, cb
-		return new Promise((resolve, reject) => {
-			let playlistModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "playlist" })
-				.then(model => {
-					playlistModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						playlistModel.deleteOne({ _id: payload.playlistId }, next);
+						PlaylistsModule.playlistModel.deleteOne({ _id: payload.playlistId }, next);
 					},
 
 					(res, next) => {
-						this.cache
-							.runJob("HDEL", {
+						CacheModule.runJob(
+							"HDEL",
+							{
 								table: "playlists",
 								key: payload.playlistId
-							})
+							},
+							this
+						)
 							.then(() => next())
 							.catch(next);
 					}
@@ -286,9 +277,9 @@ class PlaylistsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve();
 				}
-			);
-		});
+			)
+		);
 	}
 }
 
-export default new PlaylistsModule();
+export default new _PlaylistsModule();

+ 67 - 84
backend/logic/punishments.js

@@ -2,10 +2,17 @@ import async from "async";
 import mongoose from "mongoose";
 import CoreClass from "../core";
 
-class PunishmentsModule extends CoreClass {
+let PunishmentsModule;
+let CacheModule;
+let DBModule;
+let UtilsModule;
+
+class _PunishmentsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("punishments");
+
+		PunishmentsModule = this;
 	}
 
 	/**
@@ -16,21 +23,19 @@ class PunishmentsModule extends CoreClass {
 	async initialize() {
 		this.setStage(1);
 
-		this.cache = this.moduleManager.modules.cache;
-		this.db = this.moduleManager.modules.db;
-		this.io = this.moduleManager.modules.io;
-		this.utils = this.moduleManager.modules.utils;
+		CacheModule = this.moduleManager.modules.cache;
+		DBModule = this.moduleManager.modules.db;
+		UtilsModule = this.moduleManager.modules.utils;
 
-		const punishmentModel = await this.db.runJob("GET_MODEL", { modelName: "punishment" });
-		const punishmentSchema = await this.db.runJob("GET_SCHEMA", { schemaName: "punishment" });
+		this.punishmentModel = this.PunishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" });
+		this.punishmentSchemaCache = await DBModule.runJob("GET_SCHEMA", { schemaName: "punishment" });
 
 		return new Promise((resolve, reject) =>
 			async.waterfall(
 				[
 					next => {
 						this.setStage(2);
-						this.cache
-							.runJob("HGETALL", { table: "punishments" })
+						CacheModule.runJob("HGETALL", { table: "punishments" })
 							.then(punishments => {
 								next(null, punishments);
 							})
@@ -47,14 +52,13 @@ class PunishmentsModule extends CoreClass {
 						return async.each(
 							punishmentIds,
 							(punishmentId, cb) => {
-								punishmentModel.findOne({ _id: punishmentId }, (err, punishment) => {
+								PunishmentsModule.punishmentModel.findOne({ _id: punishmentId }, (err, punishment) => {
 									if (err) next(err);
 									else if (!punishment)
-										this.cache
-											.runJob("HDEL", {
-												table: "punishments",
-												key: punishmentId
-											})
+										CacheModule.runJob("HDEL", {
+											table: "punishments",
+											key: punishmentId
+										})
 											.then(() => {
 												cb();
 											})
@@ -68,7 +72,7 @@ class PunishmentsModule extends CoreClass {
 
 					next => {
 						this.setStage(4);
-						punishmentModel.find({}, next);
+						PunishmentsModule.punishmentModel.find({}, next);
 					},
 
 					(punishments, next) => {
@@ -78,12 +82,11 @@ class PunishmentsModule extends CoreClass {
 							(punishment, next) => {
 								if (punishment.active === false || punishment.expiresAt < Date.now()) return next();
 
-								return this.cache
-									.runJob("HSET", {
-										table: "punishments",
-										key: punishment._id,
-										value: punishmentSchema(punishment, punishment._id)
-									})
+								return CacheModule.runJob("HSET", {
+									table: "punishments",
+									key: punishment._id,
+									value: PunishmentsModule.punishmentSchemaCache(punishment, punishment._id)
+								})
 									.then(() => next())
 									.catch(next);
 							},
@@ -93,7 +96,7 @@ class PunishmentsModule extends CoreClass {
 				],
 				async err => {
 					if (err) {
-						const formattedErr = await this.utils.runJob("GET_ERROR", { error: err });
+						const formattedErr = await UtilsModule.runJob("GET_ERROR", { error: err });
 						reject(new Error(formattedErr));
 					} else resolve();
 				}
@@ -112,8 +115,7 @@ class PunishmentsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						this.cache
-							.runJob("HGETALL", { table: "punishments" })
+						CacheModule.runJob("HGETALL", { table: "punishments" }, this)
 							.then(punishmentsObj => next(null, punishmentsObj))
 							.catch(next);
 					},
@@ -121,15 +123,11 @@ class PunishmentsModule extends CoreClass {
 					(punishments, next) => {
 						let filteredPunishments = [];
 
-						for (
-							let id = 0, punishmentKeys = Object.keys(punishments);
-							id < punishmentKeys.length;
-							id += 1
-						) {
-							const punishment = punishments[id];
+						Object.keys(punishments).forEach(punishmentKey => {
+							const punishment = punishments[punishmentKey];
 							punishment.punishmentId = id;
 							punishments.push(punishment);
-						}
+						});
 
 						filteredPunishments = punishments.filter(punishment => {
 							if (punishment.expiresAt < Date.now()) punishmentsToRemove.push(punishment);
@@ -143,12 +141,14 @@ class PunishmentsModule extends CoreClass {
 						async.each(
 							punishmentsToRemove,
 							(punishment, next2) => {
-								this.cache
-									.runJob("HDEL", {
+								CacheModule.runJob(
+									"HDEL",
+									{
 										table: "punishments",
 										key: punishment.punishmentId
-									})
-									.finally(() => next2());
+									},
+									this
+								).finally(() => next2());
 							},
 							() => {
 								next(null, punishments);
@@ -172,42 +172,39 @@ class PunishmentsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_PUNISHMENT(payload) {
-		return new Promise((resolve, reject) => {
-			let punishmentModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "punishment" })
-				.then(model => {
-					punishmentModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
 						if (!mongoose.Types.ObjectId.isValid(payload.id)) return next("Id is not a valid ObjectId.");
-						return this.cache
-							.runJob("HGET", {
+						return CacheModule.runJob(
+							"HGET",
+							{
 								table: "punishments",
 								key: payload.id
-							})
+							},
+							this
+						)
 							.then(punishment => next(null, punishment))
 							.catch(next);
 					},
 
 					(punishment, next) => {
 						if (punishment) return next(true, punishment);
-						return punishmentModel.findOne({ _id: payload.id }, next);
+						return PunishmentsModule.punishmentModel.findOne({ _id: payload.id }, next);
 					},
 
 					(punishment, next) => {
 						if (punishment) {
-							this.cache
-								.runJob("HSET", {
+							CacheModule.runJob(
+								"HSET",
+								{
 									table: "punishments",
 									key: payload.id,
 									value: punishment
-								})
+								},
+								this
+							)
 								.then(punishment => {
 									next(null, punishment);
 								})
@@ -219,8 +216,8 @@ class PunishmentsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve(punishment);
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -235,7 +232,7 @@ class PunishmentsModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						this.runJob("GET_PUNISHMENTS", {})
+						PunishmentsModule.runJob("GET_PUNISHMENTS", {}, this)
 							.then(punishments => {
 								next(null, punishments);
 							})
@@ -268,28 +265,11 @@ class PunishmentsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	ADD_PUNISHMENT(payload) {
-		return new Promise((resolve, reject) => {
-			let PunishmentModel;
-			let PunishmentSchema;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "punishment" })
-				.then(model => {
-					PunishmentModel = model;
-				})
-				.catch(console.error);
-
-			this.db
-				.runJob("GET_SCHEMA", { schemaName: "punishment" })
-				.then(model => {
-					PunishmentSchema = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						const punishment = new PunishmentModel({
+						const punishment = new PunishmentsModule.PunishmentModel({
 							type: payload.type,
 							value: payload.value,
 							reason: payload.reason,
@@ -305,12 +285,15 @@ class PunishmentsModule extends CoreClass {
 					},
 
 					(punishment, next) => {
-						this.cache
-							.runJob("HSET", {
+						CacheModule.runJob(
+							"HSET",
+							{
 								table: "punishments",
 								key: punishment._id,
-								value: PunishmentSchema(punishment, punishment._id)
-							})
+								value: PunishmentsModule.punishmentSchemaCache(punishment, punishment._id)
+							},
+							this
+						)
 							.then(() => next())
 							.catch(next);
 					},
@@ -324,9 +307,9 @@ class PunishmentsModule extends CoreClass {
 					if (err) return reject(new Error(err));
 					return resolve(punishment);
 				}
-			);
-		});
+			)
+		);
 	}
 }
 
-export default new PunishmentsModule();
+export default new _PunishmentsModule();

+ 67 - 93
backend/logic/songs.js

@@ -2,10 +2,17 @@ import async from "async";
 import mongoose from "mongoose";
 import CoreClass from "../core";
 
-class SongsModule extends CoreClass {
+let SongsModule;
+let CacheModule;
+let DBModule;
+let UtilsModule;
+
+class _SongsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("songs");
+
+		SongsModule = this;
 	}
 
 	/**
@@ -16,13 +23,12 @@ class SongsModule extends CoreClass {
 	async initialize() {
 		this.setStage(1);
 
-		this.cache = this.moduleManager.modules.cache;
-		this.db = this.moduleManager.modules.db;
-		this.io = this.moduleManager.modules.io;
-		this.utils = this.moduleManager.modules.utils;
+		CacheModule = this.moduleManager.modules.cache;
+		DBModule = this.moduleManager.modules.db;
+		UtilsModule = this.moduleManager.modules.utils;
 
-		const songModel = await this.db.runJob("GET_MODEL", { modelName: "song" });
-		const songSchema = await this.cache.runJob("GET_SCHEMA", { schemaName: "song" });
+		this.songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
+		this.songSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
 
 		this.setStage(2);
 
@@ -31,8 +37,7 @@ class SongsModule extends CoreClass {
 				[
 					next => {
 						this.setStage(2);
-						this.cache
-							.runJob("HGETALL", { table: "songs" })
+						CacheModule.runJob("HGETALL", { table: "songs" })
 							.then(songs => {
 								next(null, songs);
 							})
@@ -49,14 +54,13 @@ class SongsModule extends CoreClass {
 						return async.each(
 							songIds,
 							(songId, next) => {
-								songModel.findOne({ songId }, (err, song) => {
+								SongsModule.songModel.findOne({ songId }, (err, song) => {
 									if (err) next(err);
 									else if (!song)
-										this.cache
-											.runJob("HDEL", {
-												table: "songs",
-												key: songId
-											})
+										CacheModule.runJob("HDEL", {
+											table: "songs",
+											key: songId
+										})
 											.then(() => next())
 											.catch(next);
 									else next();
@@ -68,7 +72,7 @@ class SongsModule extends CoreClass {
 
 					next => {
 						this.setStage(4);
-						songModel.find({}, next);
+						SongsModule.songModel.find({}, next);
 					},
 
 					(songs, next) => {
@@ -76,12 +80,11 @@ class SongsModule extends CoreClass {
 						async.each(
 							songs,
 							(song, next) => {
-								this.cache
-									.runJob("HSET", {
-										table: "songs",
-										key: song.songId,
-										value: songSchema(song)
-									})
+								CacheModule.runJob("HSET", {
+									table: "songs",
+									key: song.songId,
+									value: SongsModule.songSchemaCache(song)
+								})
 									.then(() => next())
 									.catch(next);
 							},
@@ -91,7 +94,7 @@ class SongsModule extends CoreClass {
 				],
 				async err => {
 					if (err) {
-						err = await this.utils.runJob("GET_ERROR", { error: err });
+						err = await UtilsModule.runJob("GET_ERROR", { error: err });
 						reject(new Error(err));
 					} else resolve();
 				}
@@ -107,22 +110,12 @@ class SongsModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET_SONG(payload) {
-		return new Promise((resolve, reject) => {
-			let songModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "song" })
-				.then(model => {
-					songModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
 						if (!mongoose.Types.ObjectId.isValid(payload.id)) return next("Id is not a valid ObjectId.");
-						return this.cache
-							.runJob("HGET", { table: "songs", key: payload.id })
+						return CacheModule.runJob("HGET", { table: "songs", key: payload.id }, this)
 							.then(song => {
 								next(null, song);
 							})
@@ -131,18 +124,20 @@ class SongsModule extends CoreClass {
 
 					(song, next) => {
 						if (song) return next(true, song);
-						return songModel.findOne({ _id: payload.id }, next);
+						return SongsModule.songModel.findOne({ _id: payload.id }, next);
 					},
 
 					(song, next) => {
 						if (song) {
-							this.cache
-								.runJob("HSET", {
+							CacheModule.runJob(
+								"HSET",
+								{
 									table: "songs",
 									key: payload.id,
 									value: song
-								})
-								.then(song => next(null, song));
+								},
+								this
+							).then(song => next(null, song));
 						} else next("Song not found.");
 					}
 				],
@@ -150,8 +145,8 @@ class SongsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve({ song });
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -162,28 +157,19 @@ class SongsModule extends CoreClass {
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET_SONG_FROM_ID(payload) {
-		return new Promise((resolve, reject) => {
-			let songModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "song" })
-				.then(model => {
-					songModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						songModel.findOne({ songId: payload.songId }, next);
+						SongsModule.songModel.findOne({ songId: payload.songId }, next);
 					}
 				],
 				(err, song) => {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve({ song });
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -195,37 +181,31 @@ class SongsModule extends CoreClass {
 	 */
 	UPDATE_SONG(payload) {
 		// songId, cb
-		return new Promise((resolve, reject) => {
-			let songModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "song" })
-				.then(model => {
-					songModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						songModel.findOne({ _id: payload.songId }, next);
+						SongsModule.songModel.findOne({ _id: payload.songId }, next);
 					},
 
 					(song, next) => {
 						if (!song) {
-							this.cache.runJob("HDEL", {
+							CacheModule.runJob("HDEL", {
 								table: "songs",
 								key: payload.songId
 							});
 							return next("Song not found.");
 						}
 
-						return this.cache
-							.runJob("HSET", {
+						return CacheModule.runJob(
+							"HSET",
+							{
 								table: "songs",
 								key: payload.songId,
 								value: song
-							})
+							},
+							this
+						)
 							.then(song => {
 								next(null, song);
 							})
@@ -236,8 +216,8 @@ class SongsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve(song);
 				}
-			);
-		});
+			)
+		);
 	}
 
 	/**
@@ -249,28 +229,22 @@ class SongsModule extends CoreClass {
 	 */
 	DELETE_SONG(payload) {
 		// songId, cb
-		return new Promise((resolve, reject) => {
-			let songModel;
-
-			this.db
-				.runJob("GET_MODEL", { modelName: "song" })
-				.then(model => {
-					songModel = model;
-				})
-				.catch(console.error);
-
-			return async.waterfall(
+		return new Promise((resolve, reject) =>
+			async.waterfall(
 				[
 					next => {
-						songModel.deleteOne({ songId: payload.songId }, next);
+						SongsModule.songModel.deleteOne({ songId: payload.songId }, next);
 					},
 
 					next => {
-						this.cache
-							.runJob("HDEL", {
+						CacheModule.runJob(
+							"HDEL",
+							{
 								table: "songs",
 								key: payload.songId
-							})
+							},
+							this
+						)
 							.then(() => next())
 							.catch(next);
 					}
@@ -279,9 +253,9 @@ class SongsModule extends CoreClass {
 					if (err && err !== true) return reject(new Error(err));
 					return resolve();
 				}
-			);
-		});
+			)
+		);
 	}
 }
 
-export default new SongsModule();
+export default new _SongsModule();

+ 0 - 118
backend/logic/spotify.js

@@ -1,118 +0,0 @@
-import config from "config";
-import async from "async";
-import oauth from "oauth";
-
-import CoreClass from "../core";
-
-const { OAuth2 } = oauth;
-
-let apiResults = {
-	access_token: "",
-	token_type: "",
-	expires_in: 0,
-	expires_at: 0,
-	scope: ""
-};
-
-class SpotifyModule extends CoreClass {
-	// eslint-disable-next-line require-jsdoc
-	constructor() {
-		super("spotify");
-	}
-
-	/**
-	 * Initialises the spotify module
-	 *
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	initialize() {
-		return new Promise((resolve, reject) => {
-			this.cache = this.moduleManager.modules.cache;
-			this.utils = this.moduleManager.modules.utils;
-
-			const client = config.get("apis.spotify.client");
-			const secret = config.get("apis.spotify.secret");
-
-			this.SpotifyOauth = new OAuth2(client, secret, "https://accounts.spotify.com/", null, "api/token", null);
-
-			async.waterfall(
-				[
-					next => {
-						this.setStage(2);
-						this.cache
-							.runJob("HGET", { table: "api", key: "spotify" })
-							.then(data => {
-								next(null, data);
-							})
-							.catch(next);
-					},
-
-					(data, next) => {
-						this.setStage(3);
-						if (data) apiResults = data;
-						next();
-					}
-				],
-				async err => {
-					if (err) {
-						err = await this.utils.runJob("GET_ERROR", {
-							error: err
-						});
-						reject(new Error(err));
-					} else {
-						resolve();
-					}
-				}
-			);
-		});
-	}
-
-	/**
-	 * Returns the request token for the Spotify api if one exists, otherwise creates a new one
-	 *
-	 * @returns {Promise} - returns a promise (resolve, reject)
-	 */
-	GET_TOKEN() {
-		return new Promise(resolve => {
-			if (Date.now() > apiResults.expires_at) {
-				this.runJob("REQUEST_TOKEN").then(() => {
-					resolve(apiResults.access_token);
-				});
-			} else resolve(apiResults.access_token);
-		});
-	}
-
-	/**
-	 * Creates a request token for the Spotify api
-	 *
-	 * @returns {Promise} - returns a promise (resolve, reject)
-	 */
-	REQUEST_TOKEN() {
-		return new Promise(resolve => {
-			async.waterfall(
-				[
-					next => {
-						this.log("INFO", "SPOTIFY_REQUEST_TOKEN", "Requesting new Spotify token.");
-						this.SpotifyOauth.getOAuthAccessToken("", { grant_type: "client_credentials" }, next);
-					},
-					(accessToken, refreshToken, results, next) => {
-						apiResults = results;
-						apiResults.expires_at = Date.now() + results.expires_in * 1000;
-
-						this.cache
-							.runJob("HSET", {
-								table: "api",
-								key: "spotify",
-								value: apiResults,
-								stringifyJson: true
-							})
-							.finally(() => next());
-					}
-				],
-				() => resolve()
-			);
-		});
-	}
-}
-
-export default new SpotifyModule();

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 295 - 262
backend/logic/stations.js


+ 80 - 84
backend/logic/tasks.js

@@ -8,12 +8,19 @@ import Timer from "../classes/Timer.class";
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-class TasksModule extends CoreClass {
+let TasksModule;
+let CacheModule;
+let StationsModule;
+let UtilsModule;
+
+class _TasksModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("tasks");
 
 		this.tasks = {};
+
+		TasksModule = this;
 	}
 
 	/**
@@ -25,28 +32,27 @@ class TasksModule extends CoreClass {
 		return new Promise(resolve => {
 			// return reject(new Error("Not fully migrated yet."));
 
-			this.cache = this.moduleManager.modules.cache;
-			this.stations = this.moduleManager.modules.stations;
-			this.notifications = this.moduleManager.modules.notifications;
-			this.utils = this.moduleManager.modules.utils;
+			CacheModule = this.moduleManager.modules.cache;
+			StationsModule = this.moduleManager.modules.stations;
+			UtilsModule = this.moduleManager.modules.utils;
 
 			// this.createTask("testTask", testTask, 5000, true);
 
-			this.runJob("CREATE_TASK", {
+			TasksModule.runJob("CREATE_TASK", {
 				name: "stationSkipTask",
-				fn: this.checkStationSkipTask,
+				fn: TasksModule.checkStationSkipTask,
 				timeout: 1000 * 60 * 30
 			});
 
-			this.runJob("CREATE_TASK", {
+			TasksModule.runJob("CREATE_TASK", {
 				name: "sessionClearTask",
-				fn: this.sessionClearingTask,
+				fn: TasksModule.sessionClearingTask,
 				timeout: 1000 * 60 * 60 * 6
 			});
 
-			this.runJob("CREATE_TASK", {
+			TasksModule.runJob("CREATE_TASK", {
 				name: "logFileSizeCheckTask",
-				fn: this.logFileSizeCheckTask,
+				fn: TasksModule.logFileSizeCheckTask,
 				timeout: 1000 * 60 * 60
 			});
 
@@ -66,7 +72,7 @@ class TasksModule extends CoreClass {
 	 */
 	CREATE_TASK(payload) {
 		return new Promise((resolve, reject) => {
-			this.tasks[payload.name] = {
+			TasksModule.tasks[payload.name] = {
 				name: payload.name,
 				fn: payload.fn,
 				timeout: payload.timeout,
@@ -75,7 +81,7 @@ class TasksModule extends CoreClass {
 			};
 
 			if (!payload.paused) {
-				this.runJob("RUN_TASK", { name: payload.name })
+				TasksModule.runJob("RUN_TASK", { name: payload.name }, this)
 					.then(() => resolve())
 					.catch(err => reject(err));
 			} else resolve();
@@ -93,7 +99,7 @@ class TasksModule extends CoreClass {
 		const taskName = { payload };
 
 		return new Promise(resolve => {
-			if (this.tasks[taskName].timer) this.tasks[taskName].timer.pause();
+			if (TasksModule.tasks[taskName].timer) TasksModule.tasks[taskName].timer.pause();
 			resolve();
 		});
 	}
@@ -107,7 +113,7 @@ class TasksModule extends CoreClass {
 	 */
 	RESUME_TASK(payload) {
 		return new Promise(resolve => {
-			this.tasks[payload.name].timer.resume();
+			TasksModule.tasks[payload.name].timer.resume();
 			resolve();
 		});
 	}
@@ -121,12 +127,16 @@ class TasksModule extends CoreClass {
 	 */
 	RUN_TASK(payload) {
 		return new Promise(resolve => {
-			const task = this.tasks[payload.name];
+			const task = TasksModule.tasks[payload.name];
 			if (task.timer) task.timer.pause();
 
-			task.fn.apply(this).then(() => {
+			task.fn.apply(null).then(() => {
 				task.lastRan = Date.now();
-				task.timer = new Timer(() => this.runJob("RUN_TASK", { name: payload.name }), task.timeout, false);
+				task.timer = new Timer(
+					() => TasksModule.runJob("RUN_TASK", { name: payload.name }),
+					task.timeout,
+					false
+				);
 				resolve();
 			});
 		});
@@ -139,12 +149,11 @@ class TasksModule extends CoreClass {
 	 */
 	checkStationSkipTask() {
 		return new Promise(resolve => {
-			this.log("INFO", "TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
+			TasksModule.log("INFO", "TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
 			async.waterfall(
 				[
 					next => {
-						this.cache
-							.runJob("HGETALL", { table: "stations" })
+						CacheModule.runJob("HGETALL", { table: "stations" })
 							.then(response => next(null, response))
 							.catch(next);
 					},
@@ -157,16 +166,14 @@ class TasksModule extends CoreClass {
 								const timeElapsed = Date.now() - station.startedAt - station.timePaused;
 								if (timeElapsed <= station.currentSong.duration) return next2();
 
-								this.log(
+								TasksModule.log(
 									"ERROR",
 									"TASK_STATIONS_SKIP_CHECK",
 									`Skipping ${station._id} as it should have skipped already.`
 								);
-								return this.stations
-									.runJob("INITIALIZE_STATION", {
-										stationId: station._id
-									})
-									.then(() => next2());
+								return StationsModule.runJob("INITIALIZE_STATION", {
+									stationId: station._id
+								}).then(() => next2());
 							},
 							() => next()
 						);
@@ -184,13 +191,12 @@ class TasksModule extends CoreClass {
 	 */
 	sessionClearingTask() {
 		return new Promise(resolve => {
-			this.log("INFO", "TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`);
+			TasksModule.log("INFO", "TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`);
 
 			async.waterfall(
 				[
 					next => {
-						this.cache
-							.runJob("HGETALL", { table: "sessions" })
+						CacheModule.runJob("HGETALL", { table: "sessions" })
 							.then(sessions => next(null, sessions))
 							.catch(next);
 					},
@@ -212,59 +218,49 @@ class TasksModule extends CoreClass {
 									return next2();
 
 								if (!session) {
-									this.log("INFO", "TASK_SESSION_CLEAR", "Removing an empty session.");
-									return this.cache
-										.runJob("HDEL", {
-											table: "sessions",
-											key: sessionId
-										})
-										.finally(() => {
-											next2();
-										});
+									TasksModule.log("INFO", "TASK_SESSION_CLEAR", "Removing an empty session.");
+									return CacheModule.runJob("HDEL", {
+										table: "sessions",
+										key: sessionId
+									}).finally(() => {
+										next2();
+									});
 								}
 								if (!session.refreshDate) {
 									session.refreshDate = Date.now();
-									return this.cache
-										.runJob("HSET", {
-											table: "sessions",
-											key: sessionId,
-											value: session
-										})
-										.finally(() => next2());
+									return CacheModule.runJob("HSET", {
+										table: "sessions",
+										key: sessionId,
+										value: session
+									}).finally(() => next2());
 								}
 								if (Date.now() - session.refreshDate > 60 * 60 * 24 * 30 * 1000) {
-									return this.utils
-										.runJob("SOCKETS_FROM_SESSION_ID", {
-											sessionId: session.sessionId
-										})
-										.then(response => {
-											if (response.sockets.length > 0) {
-												session.refreshDate = Date.now();
-												this.cache
-													.runJob("HSET", {
-														table: "sessions",
-														key: sessionId,
-														value: session
-													})
-													.finally(() => {
-														next2();
-													});
-											} else {
-												this.log(
-													"INFO",
-													"TASK_SESSION_CLEAR",
-													`Removing session ${sessionId} for user ${session.userId} since inactive for 30 days and not currently in use.`
-												);
-												this.cache
-													.runJob("HDEL", {
-														table: "sessions",
-														key: session.sessionId
-													})
-													.finally(() => next2());
-											}
-										});
+									return UtilsModule.runJob("SOCKETS_FROM_SESSION_ID", {
+										sessionId: session.sessionId
+									}).then(response => {
+										if (response.sockets.length > 0) {
+											session.refreshDate = Date.now();
+											CacheModule.runJob("HSET", {
+												table: "sessions",
+												key: sessionId,
+												value: session
+											}).finally(() => {
+												next2();
+											});
+										} else {
+											TasksModule.log(
+												"INFO",
+												"TASK_SESSION_CLEAR",
+												`Removing session ${sessionId} for user ${session.userId} since inactive for 30 days and not currently in use.`
+											);
+											CacheModule.runJob("HDEL", {
+												table: "sessions",
+												key: session.sessionId
+											}).finally(() => next2());
+										}
+									});
 								}
-								this.log("ERROR", "TASK_SESSION_CLEAR", "This should never log.");
+								TasksModule.log("ERROR", "TASK_SESSION_CLEAR", "This should never log.");
 								return next2();
 							},
 							() => next()
@@ -283,7 +279,7 @@ class TasksModule extends CoreClass {
 	 */
 	logFileSizeCheckTask() {
 		return new Promise((resolve, reject) => {
-			this.log("INFO", "TASK_LOG_FILE_SIZE_CHECK", `Checking the size for the log files.`);
+			TasksModule.log("INFO", "TASK_LOG_FILE_SIZE_CHECK", `Checking the size for the log files.`);
 			async.each(
 				["all.log", "debugStation.log", "error.log", "info.log", "success.log"],
 				(fileName, next) => {
@@ -299,26 +295,26 @@ class TasksModule extends CoreClass {
 				},
 				async err => {
 					if (err && err !== true) {
-						err = await this.utils.runJob("GET_ERROR", { error: err });
+						err = await UtilsModule.runJob("GET_ERROR", { error: err });
 						return reject(new Error(err));
 					}
 					if (err === true) {
-						this.log(
+						TasksModule.log(
 							"ERROR",
 							"LOGGER_FILE_SIZE_WARNING",
 							"************************************WARNING*************************************"
 						);
-						this.log(
+						TasksModule.log(
 							"ERROR",
 							"LOGGER_FILE_SIZE_WARNING",
 							"***************ONE OR MORE LOG FILES APPEAR TO BE MORE THAN 25MB****************"
 						);
-						this.log(
+						TasksModule.log(
 							"ERROR",
 							"LOGGER_FILE_SIZE_WARNING",
 							"****MAKE SURE TO REGULARLY CLEAR UP THE LOG FILES, MANUALLY OR AUTOMATICALLY****"
 						);
-						this.log(
+						TasksModule.log(
 							"ERROR",
 							"LOGGER_FILE_SIZE_WARNING",
 							"********************************************************************************"
@@ -332,4 +328,4 @@ class TasksModule extends CoreClass {
 	}
 }
 
-export default new TasksModule();
+export default new _TasksModule();

+ 111 - 184
backend/logic/utils.js

@@ -5,7 +5,11 @@ import crypto from "crypto";
 import request from "request";
 import CoreClass from "../core";
 
-class UtilsModule extends CoreClass {
+let UtilsModule;
+let IOModule;
+let CacheModule;
+
+class _UtilsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
 		super("utils");
@@ -13,6 +17,8 @@ class UtilsModule extends CoreClass {
 		this.youtubeRequestCallbacks = [];
 		this.youtubeRequestsPending = 0;
 		this.youtubeRequestsActive = false;
+
+		UtilsModule = this;
 	}
 
 	/**
@@ -22,10 +28,8 @@ class UtilsModule extends CoreClass {
 	 */
 	initialize() {
 		return new Promise(resolve => {
-			this.io = this.moduleManager.modules.io;
-			this.db = this.moduleManager.modules.db;
-			this.spotify = this.moduleManager.modules.spotify;
-			this.cache = this.moduleManager.modules.cache;
+			IOModule = this.moduleManager.modules.io;
+			CacheModule = this.moduleManager.modules.cache;
 
 			resolve();
 		});
@@ -79,16 +83,20 @@ class UtilsModule extends CoreClass {
 			let cookies;
 
 			try {
-				cookies = this.runJob("PARSE_COOKIES", {
-					cookieString: payload.cookieString
-				});
+				cookies = UtilsModule.runJob(
+					"PARSE_COOKIES",
+					{
+						cookieString: payload.cookieString
+					},
+					this
+				);
 			} catch (err) {
 				return reject(err);
 			}
 
 			delete cookies[payload.cookieName];
 
-			return resolve(this.toString(cookies));
+			return resolve();
 		});
 	}
 
@@ -124,10 +132,14 @@ class UtilsModule extends CoreClass {
 		const promises = [];
 		for (let i = 0; i < payload.length; i += 1) {
 			promises.push(
-				this.runJob("GET_RANDOM_NUMBER", {
-					min: 0,
-					max: chars.length - 1
-				})
+				UtilsModule.runJob(
+					"GET_RANDOM_NUMBER",
+					{
+						min: 0,
+						max: chars.length - 1
+					},
+					this
+				)
 			);
 		}
 
@@ -150,7 +162,7 @@ class UtilsModule extends CoreClass {
 	 */
 	async GET_SOCKET_FROM_ID(payload) {
 		// socketId
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => resolve(io.sockets.sockets[payload.socketId]));
 	}
@@ -248,7 +260,7 @@ class UtilsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	async SOCKET_FROM_SESSION(payload) {
 		// socketId
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise((resolve, reject) => {
 			const ns = io.of("/");
@@ -268,7 +280,7 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKETS_FROM_SESSION_ID(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => {
 			const ns = io.of("/");
@@ -300,7 +312,7 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKETS_FROM_USER(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise((resolve, reject) => {
 			const ns = io.of("/");
@@ -311,11 +323,14 @@ class UtilsModule extends CoreClass {
 					Object.keys(ns.connected),
 					(id, next) => {
 						const { session } = ns.connected[id];
-						this.cache
-							.runJob("HGET", {
+						CacheModule.runJob(
+							"HGET",
+							{
 								table: "sessions",
 								key: session.sessionId
-							})
+							},
+							this
+						)
 							.then(session => {
 								if (session && session.userId === payload.userId) sockets.push(ns.connected[id]);
 								next();
@@ -343,7 +358,7 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKETS_FROM_IP(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => {
 			const ns = io.of("/");
@@ -353,11 +368,14 @@ class UtilsModule extends CoreClass {
 					Object.keys(ns.connected),
 					(id, next) => {
 						const { session } = ns.connected[id];
-						this.cache
-							.runJob("HGET", {
+						CacheModule.runJob(
+							"HGET",
+							{
 								table: "sessions",
 								key: session.sessionId
-							})
+							},
+							this
+						)
 							.then(session => {
 								if (session && ns.connected[id].ip === payload.ip) sockets.push(ns.connected[id]);
 								next();
@@ -382,7 +400,7 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKETS_FROM_USER_WITHOUT_CACHE(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => {
 			const ns = io.of("/");
@@ -414,15 +432,21 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKET_LEAVE_ROOMS(payload) {
-		const socket = await this.runJob("SOCKET_FROM_SESSION", {
-			socketId: payload.socketId
-		});
+		const socket = await UtilsModule.runJob(
+			"SOCKET_FROM_SESSION",
+			{
+				socketId: payload.socketId
+			},
+			this
+		);
 
 		return new Promise(resolve => {
 			const { rooms } = socket;
-			for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+
+			Object.keys(rooms).forEach(roomKey => {
+				const room = rooms[roomKey];
 				socket.leave(room);
-			}
+			});
 
 			return resolve();
 		});
@@ -437,15 +461,20 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async SOCKET_JOIN_ROOM(payload) {
-		const socket = await this.runJob("SOCKET_FROM_SESSION", {
-			socketId: payload.socketId
-		});
+		const socket = await UtilsModule.runJob(
+			"SOCKET_FROM_SESSION",
+			{
+				socketId: payload.socketId
+			},
+			this
+		);
 
 		return new Promise(resolve => {
 			const { rooms } = socket;
-			for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+			Object.keys(rooms).forEach(roomKey => {
+				const room = rooms[roomKey];
 				socket.leave(room);
-			}
+			});
 
 			socket.join(payload.room);
 
@@ -457,15 +486,20 @@ class UtilsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	async SOCKET_JOIN_SONG_ROOM(payload) {
 		// socketId, room
-		const socket = await this.runJob("SOCKET_FROM_SESSION", {
-			socketId: payload.socketId
-		});
+		const socket = await UtilsModule.runJob(
+			"SOCKET_FROM_SESSION",
+			{
+				socketId: payload.socketId
+			},
+			this
+		);
 
 		return new Promise(resolve => {
 			const { rooms } = socket;
-			for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
-				if (room.indexOf("song.") !== -1) socket.leave(rooms);
-			}
+			Object.keys(rooms).forEach(roomKey => {
+				const room = rooms[roomKey];
+				if (room.indexOf("song.") !== -1) socket.leave(room);
+			});
 
 			socket.join(payload.room);
 
@@ -478,16 +512,17 @@ class UtilsModule extends CoreClass {
 	SOCKETS_JOIN_SONG_ROOM(payload) {
 		// sockets, room
 		return new Promise(resolve => {
-			for (let id = 0, socketKeys = Object.keys(payload.sockets); id < socketKeys.length; id += 1) {
-				const socket = payload.sockets[socketKeys[id]];
+			Object.keys(payload.sockets).forEach(socketKey => {
+				const socket = payload.sockets[socketKey];
 
 				const { rooms } = socket;
-				for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+				Object.keys(rooms).forEach(roomKey => {
+					const room = rooms[roomKey];
 					if (room.indexOf("song.") !== -1) socket.leave(room);
-				}
+				});
 
 				socket.join(payload.room);
-			}
+			});
 
 			return resolve();
 		});
@@ -498,13 +533,14 @@ class UtilsModule extends CoreClass {
 	SOCKETS_LEAVE_SONG_ROOMS(payload) {
 		// sockets
 		return new Promise(resolve => {
-			for (let id = 0, socketKeys = Object.keys(payload.sockets); id < socketKeys.length; id += 1) {
-				const socket = payload.sockets[socketKeys[id]];
+			Object.keys(payload.sockets).forEach(socketKey => {
+				const socket = payload.sockets[socketKey];
 				const { rooms } = socket;
-				for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
+				Object.keys(rooms).forEach(roomKey => {
+					const room = rooms[roomKey];
 					if (room.indexOf("song.") !== -1) socket.leave(room);
-				}
-			}
+				});
+			});
 			resolve();
 		});
 	}
@@ -518,16 +554,16 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async EMIT_TO_ROOM(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => {
 			const { sockets } = io.sockets;
-			for (let id = 0, socketKeys = Object.keys(sockets); id < socketKeys.length; id += 1) {
-				const socket = sockets[socketKeys[id]];
+			Object.keys(sockets).forEach(socketKey => {
+				const socket = sockets[socketKey];
 				if (socket.rooms[payload.room]) {
 					socket.emit(...payload.args);
 				}
-			}
+			});
 
 			return resolve();
 		});
@@ -541,15 +577,15 @@ class UtilsModule extends CoreClass {
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async GET_ROOM_SOCKETS(payload) {
-		const io = await this.io.runJob("IO", {});
+		const io = await IOModule.runJob("IO", {}, this);
 
 		return new Promise(resolve => {
 			const { sockets } = io.sockets;
 			const roomSockets = [];
-			for (let id = 0, socketKeys = Object.keys(sockets); id < socketKeys.length; id += 1) {
-				const socket = sockets[socketKeys[id]];
+			Object.keys(sockets).forEach(socketKey => {
+				const socket = sockets[socketKey];
 				if (socket.rooms[payload.room]) roomSockets.push(socket);
-			}
+			});
 
 			return resolve(roomSockets);
 		});
@@ -565,9 +601,9 @@ class UtilsModule extends CoreClass {
 	GET_SONG_FROM_YOUTUBE(payload) {
 		// songId, cb
 		return new Promise((resolve, reject) => {
-			this.youtubeRequestCallbacks.push({
+			UtilsModule.youtubeRequestCallbacks.push({
 				cb: () => {
-					this.youtubeRequestsActive = true;
+					UtilsModule.youtubeRequestsActive = true;
 					const youtubeParams = [
 						"part=snippet,contentDetails,statistics,status",
 						`id=${encodeURIComponent(payload.songId)}`,
@@ -575,10 +611,10 @@ class UtilsModule extends CoreClass {
 					].join("&");
 
 					request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
-						this.youtubeRequestCallbacks.splice(0, 1);
-						if (this.youtubeRequestCallbacks.length > 0) {
-							this.youtubeRequestCallbacks[0].cb(this.youtubeRequestCallbacks[0].songId);
-						} else this.youtubeRequestsActive = false;
+						UtilsModule.youtubeRequestCallbacks.splice(0, 1);
+						if (UtilsModule.youtubeRequestCallbacks.length > 0) {
+							UtilsModule.youtubeRequestCallbacks[0].cb(UtilsModule.youtubeRequestCallbacks[0].songId);
+						} else UtilsModule.youtubeRequestsActive = false;
 
 						if (err) {
 							console.error(err);
@@ -635,8 +671,8 @@ class UtilsModule extends CoreClass {
 				songId: payload.songId
 			});
 
-			if (!this.youtubeRequestsActive) {
-				this.youtubeRequestCallbacks[0].cb(this.youtubeRequestCallbacks[0].songId);
+			if (!UtilsModule.youtubeRequestsActive) {
+				UtilsModule.youtubeRequestCallbacks[0].cb(UtilsModule.youtubeRequestCallbacks[0].songId);
 			}
 		});
 	}
@@ -763,9 +799,13 @@ class UtilsModule extends CoreClass {
 
 						if (!payload.musicOnly) return resolve({ songs });
 						return local
-							.runJob("FILTER_MUSIC_VIDEOS_YOUTUBE", {
-								videoIds: songs.slice()
-							})
+							.runJob(
+								"FILTER_MUSIC_VIDEOS_YOUTUBE",
+								{
+									videoIds: songs.slice()
+								},
+								this
+							)
 							.then(filteredSongs => {
 								resolve({ filteredSongs, songs });
 							});
@@ -777,119 +817,6 @@ class UtilsModule extends CoreClass {
 		});
 	}
 
-	/**
-	 * Gets the details of a song from the Spotify API
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {object} payload.song - the song object (song.title etc.)
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	async GET_SONG_FROM_SPOTIFY(payload) {
-		// song
-		const token = await this.spotify.runJob("GET_TOKEN", {});
-
-		return new Promise((resolve, reject) => {
-			if (!config.get("apis.spotify.enabled")) return reject(new Error("Spotify is not enabled."));
-
-			const song = { ...payload.song };
-
-			const spotifyParams = [`q=${encodeURIComponent(payload.song.title)}`, `type=track`].join("&");
-
-			const options = {
-				url: `https://api.spotify.com/v1/search?${spotifyParams}`,
-				headers: {
-					Authorization: `Bearer ${token}`
-				}
-			};
-
-			return request(options, (err, res, body) => {
-				if (err) console.error(err);
-				body = JSON.parse(body);
-				if (body.error) console.error(body.error);
-				for (let i = 0, bodyKeys = Object.keys(body); i < bodyKeys.length; i += 1) {
-					const { items } = body[i];
-					for (let j = 0, itemKeys = Object.keys(body); j < itemKeys.length; j += 1) {
-						const item = items[j];
-						let hasArtist = false;
-						for (let k = 0; k < item.artists.length; k += 1) {
-							const artist = item.artists[k];
-							if (song.title.indexOf(artist.name) !== -1) hasArtist = true;
-						}
-						if (hasArtist && song.title.indexOf(item.name) !== -1) {
-							song.duration = item.duration_ms / 1000;
-							song.artists = item.artists.map(artist => artist.name);
-							song.title = item.name;
-							song.explicit = item.explicit;
-							song.thumbnail = item.album.images[1].url;
-							break;
-						}
-					}
-				}
-
-				resolve({ song });
-			});
-		});
-	}
-
-	/**
-	 * Returns the details of multiple songs from the Spotify API
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {object} payload.title - the query/title of a song to search the API with
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	async GET_SONGS_FROM_SPOTIFY(payload) {
-		// title, artist
-		const token = await this.spotify.runJob("GET_TOKEN", {});
-
-		return new Promise((resolve, reject) => {
-			if (!config.get("apis.spotify.enabled")) return reject(new Error("Spotify is not enabled."));
-
-			const spotifyParams = [`q=${encodeURIComponent(payload.title)}`, `type=track`].join("&");
-
-			const options = {
-				url: `https://api.spotify.com/v1/search?${spotifyParams}`,
-				headers: {
-					Authorization: `Bearer ${token}`
-				}
-			};
-
-			return request(options, (err, res, body) => {
-				if (err) return console.error(err);
-				body = JSON.parse(body);
-				if (body.error) return console.error(body.error);
-
-				const songs = [];
-
-				for (let i = 0, bodyKeys = Object.keys(body); i < bodyKeys.length; i += 1) {
-					const { items } = body[i];
-					for (let j = 0, itemKeys = Object.keys(body); j < itemKeys.length; j += 1) {
-						const item = items[j];
-						let hasArtist = false;
-						for (let k = 0; k < item.artists.length; k += 1) {
-							const localArtist = item.artists[k];
-							if (payload.artist.toLowerCase() === localArtist.name.toLowerCase()) hasArtist = true;
-						}
-						if (
-							hasArtist &&
-							(payload.title.indexOf(item.name) !== -1 || item.name.indexOf(payload.title) !== -1)
-						) {
-							const song = {};
-							song.duration = item.duration_ms / 1000;
-							song.artists = item.artists.map(artist => artist.name);
-							song.title = item.name;
-							song.explicit = item.explicit;
-							song.thumbnail = item.album.images[1].url;
-							songs.push(song);
-						}
-					}
-				}
-
-				return resolve({ songs });
-			});
-		});
-	}
-
 	/**
 	 * Shuffles an array of songs
 	 *
@@ -966,4 +893,4 @@ class UtilsModule extends CoreClass {
 	}
 }
 
-export default new UtilsModule();
+export default new _UtilsModule();

+ 1 - 2
backend/package.json

@@ -22,7 +22,6 @@
     "config": "^3.3.1",
     "cookie-parser": "^1.4.5",
     "cors": "^2.8.5",
-    "discord.js": "^11.6.4",
     "express": "^4.17.1",
     "mailgun-js": "^0.22.0",
     "moment": "^2.24.0",
@@ -44,4 +43,4 @@
     "prettier": "^2.2.1",
     "trace-unhandled": "^1.2.1"
   }
-}
+}

+ 1 - 1
frontend/dist/index.tpl.html

@@ -8,7 +8,7 @@
 	<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
 	<meta name='keywords' content='music, musare, listen, station, station, radio, edm, chill, community, official, rooms, room, party, good, mus, pop'>
 	<meta name='description' content='On Musare you can listen to lots of different songs, playing 24/7 in our official stations and in user-made community stations!'>
-	<meta name='copyright' content='© Copyright Musare 2015-2020 All Right Reserved'>
+	<meta name='copyright' content='© Copyright Musare 2015-2021 All Right Reserved'>
 
 	<link rel='apple-touch-icon' sizes='57x57' href='/assets/favicon/apple-touch-icon-57x57.png?v=06042016'>
 	<link rel='apple-touch-icon' sizes='60x60' href='/assets/favicon/apple-touch-icon-60x60.png?v=06042016'>

+ 1 - 1
frontend/src/components/layout/MainFooter.vue

@@ -51,7 +51,7 @@
 					>
 					<router-link title="News" to="/news">News</router-link>
 				</div>
-				<p>© Copyright Musare 2015 - 2020</p>
+				<p>© Copyright Musare 2015 - 2021</p>
 			</div>
 		</div>
 	</footer>

+ 64 - 2
frontend/src/pages/Admin/tabs/NewStatistics.vue

@@ -18,6 +18,7 @@
 									<th>Stage</th>
 									<th>Jobs in queue</th>
 									<th>Jobs in progress</th>
+									<th>Jobs paused</th>
 									<th>Concurrency</th>
 								</tr>
 							</thead>
@@ -38,6 +39,7 @@
 									<td>{{ moduleItem.stage }}</td>
 									<td>{{ moduleItem.jobsInQueue }}</td>
 									<td>{{ moduleItem.jobsInProgress }}</td>
+									<td>{{ moduleItem.jobsPaused }}</td>
 									<td>{{ moduleItem.concurrency }}</td>
 								</tr>
 							</tbody>
@@ -52,7 +54,67 @@
 				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
 			>
 				<header class="card-header">
-					<p class="card-header-title">Average Logs</p>
+					<p class="card-header-title">Running tasks</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<table class="table">
+							<thead>
+								<tr>
+									<th>Name</th>
+									<th>Payload</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr
+									v-for="job in module.runningTasks"
+									:key="JSON.stringify(job)"
+								>
+									<td>{{ job.name }}</td>
+									<td>
+										{{ JSON.stringify(job.payload) }}
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">Paused tasks</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<table class="table">
+							<thead>
+								<tr>
+									<th>Name</th>
+									<th>Payload</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr
+									v-for="job in module.pausedTasks"
+									:key="JSON.stringify(job)"
+								>
+									<td>{{ job.name }}</td>
+									<td>
+										{{ JSON.stringify(job.payload) }}
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">Queued tasks</p>
 				</header>
 				<div class="card-content">
 					<div class="content">
@@ -65,7 +127,7 @@
 							</thead>
 							<tbody>
 								<tr
-									v-for="job in module.runningJobs"
+									v-for="job in module.queuedTasks"
 									:key="JSON.stringify(job)"
 								>
 									<td>{{ job.name }}</td>

+ 6 - 8
frontend/src/pages/Station/AddSongToQueue.vue

@@ -178,10 +178,9 @@
 											class="button is-danger"
 											href="#"
 											@click="
-												addSongToQueue(result.id) &&
-													togglePlaylistSelection(
-														playlist._id
-													)
+												togglePlaylistSelection(
+													playlist._id
+												)
 											"
 											v-if="
 												isPlaylistSelected(playlist._id)
@@ -196,10 +195,9 @@
 										<a
 											class="button is-success"
 											@click="
-												addSongToQueue(result.id) &&
-													togglePlaylistSelection(
-														playlist._id
-													)
+												togglePlaylistSelection(
+													playlist._id
+												)
 											"
 											href="#"
 											v-else

Vissa filer visades inte eftersom för många filer har ändrats